From ec92f4b680890709bc74d12d0a415da648fe2fb9 Mon Sep 17 00:00:00 2001 From: Laser Cat <91504177+lzr-cat@users.noreply.github.com> Date: Thu, 3 Feb 2022 12:29:00 -0600 Subject: [PATCH 001/378] fix: update run_block.py (#10014) * fix: update run_block.py - add asset id - add parent directory to load transactions_generator_ref_list from a directory * fix: remove un used import * fix: Call to function ref_list_to_args with too few arguments; should be no fewer than 2. * core: remove unused function * fix: missed a conflict, opps * core: remove unused import * fix: linting --- tests/tools/test_run_block.py | 14 ++++++------ tools/run_block.py | 42 +++++++++++++---------------------- 2 files changed, 23 insertions(+), 33 deletions(-) diff --git a/tests/tools/test_run_block.py b/tests/tools/test_run_block.py index 9a4cd2ccf857..cc5b88873267 100644 --- a/tests/tools/test_run_block.py +++ b/tests/tools/test_run_block.py @@ -48,7 +48,7 @@ def test_block_no_generator(): with open(dirname / "300000.json") as f: full_block = json.load(f) - cat_list = run_json_block(full_block, constants) + cat_list = run_json_block(full_block, dirname, constants) assert not cat_list @@ -58,10 +58,10 @@ def test_block_retired_cat_with_memo(): with open(dirname / "396963.json") as f: full_block = json.load(f) - cat_list = run_json_block(full_block, constants) + cat_list = run_json_block(full_block, dirname, constants) assert cat_list - assert cat_list[0].tail_hash == "86bf9abe0600edf96b2e0fa928d19435b5aa756a9c9151c4b53c2c3da258502f" + assert cat_list[0].asset_id == "86bf9abe0600edf96b2e0fa928d19435b5aa756a9c9151c4b53c2c3da258502f" assert cat_list[0].memo == "Hello, please find me, I'm a memo!" assert cat_list[0].npc.coin_name.hex() == "244854a6fadf837b0fbb78d19b94b0de24fd2ffb440e7c0ec7866104b2aecd16" assert cat_list[0].npc.puzzle_hash.hex() == "4aa945b657928602e59d37ad165ba12008d1dbee3a7be06c9bd19b4f00da456c" @@ -78,10 +78,10 @@ def test_block_retired_cat_no_memo(): with open(dirname / "392111.json") as f: full_block = json.load(f) - cat_list = run_json_block(full_block, constants) + cat_list = run_json_block(full_block, dirname, constants) assert cat_list - assert cat_list[0].tail_hash == "86bf9abe0600edf96b2e0fa928d19435b5aa756a9c9151c4b53c2c3da258502f" + assert cat_list[0].asset_id == "86bf9abe0600edf96b2e0fa928d19435b5aa756a9c9151c4b53c2c3da258502f" assert not cat_list[0].memo assert cat_list[0].npc.coin_name.hex() == "f419f6b77fa56b2cf0e93818d9214ec6023fb6335107dd6e6d82dfa5f4cbb4f6" assert cat_list[0].npc.puzzle_hash.hex() == "714655375fc8e4e3545ecdc671ea53e497160682c82fe2c6dc44c4150dc845b4" @@ -99,10 +99,10 @@ def test_block_cat(): with open(dirname / "149988.json") as f: full_block = json.load(f) - cat_list = run_json_block(full_block, constants) + cat_list = run_json_block(full_block, dirname, constants) assert cat_list - assert cat_list[0].tail_hash == "8829a36776a15477a7f41f8fb6397752922374b60be7d3b2d7881c54b86b32a1" + assert cat_list[0].asset_id == "8829a36776a15477a7f41f8fb6397752922374b60be7d3b2d7881c54b86b32a1" assert not cat_list[0].memo assert cat_list[0].npc.coin_name.hex() == "4314b142cecfd6121474116e5a690d6d9b2e8c374e1ebef15235b0f3de4e2508" assert cat_list[0].npc.puzzle_hash.hex() == "ddc37f3cbb49e3566b8638c5aaa93d5e10ee91dfd5d8ce37ad7175432d7209aa" diff --git a/tools/run_block.py b/tools/run_block.py index c46baee9949c..16b96d9ea75c 100644 --- a/tools/run_block.py +++ b/tools/run_block.py @@ -37,7 +37,8 @@ """ import json from dataclasses import dataclass -from typing import List, TextIO, Tuple, Dict +from pathlib import Path +from typing import List, Tuple, Dict import click @@ -49,7 +50,6 @@ from chia.types.blockchain_format.coin import Coin from chia.types.condition_opcodes import ConditionOpcode from chia.types.condition_with_args import ConditionWithArgs -from chia.types.full_block import FullBlock from chia.types.generator_types import BlockGenerator from chia.types.name_puzzle_condition import NPC from chia.util.config import load_config @@ -61,12 +61,12 @@ @dataclass class CAT: - tail_hash: str + asset_id: str memo: str npc: NPC def cat_to_dict(self): - return {"tail_hash": self.tail_hash, "memo": self.memo, "npc": npc_to_dict(self.npc)} + return {"asset_id": self.asset_id, "memo": self.memo, "npc": npc_to_dict(self.npc)} def condition_with_args_to_dict(condition_with_args: ConditionWithArgs): @@ -115,7 +115,7 @@ def run_generator( if not matched: continue - _, tail_hash, _ = curried_args + _, asset_id, _ = curried_args memo = "" result = puzzle.run(solution) @@ -158,7 +158,7 @@ def run_generator( coin = Coin(parent.atom, puzzle_hash, int_from_bytes(amount.atom)) cat_list.append( CAT( - tail_hash=bytes(tail_hash).hex()[2:], + asset_id=bytes(asset_id).hex()[2:], memo=memo, npc=NPC(coin.name(), puzzle_hash, [(op, cond) for op, cond in conds.items()]), ) @@ -167,25 +167,15 @@ def run_generator( return cat_list -def ref_list_to_args(ref_list: List[uint32]) -> List[SerializedProgram]: +def ref_list_to_args(ref_list: List[uint32], root_path: Path) -> List[SerializedProgram]: args = [] for height in ref_list: - with open(f"{height}.json", "r") as f: + with open(root_path / f"{height}.json", "rb") as f: program_str = json.load(f)["block"]["transactions_generator"] args.append(SerializedProgram.fromhex(program_str)) return args -def run_full_block(block: FullBlock, constants: ConsensusConstants) -> List[CAT]: - generator_args = ref_list_to_args(block.transactions_generator_ref_list) - if block.transactions_generator is None or block.transactions_info is None: - raise RuntimeError("transactions_generator of FullBlock is null") - block_generator = BlockGenerator(block.transactions_generator, generator_args, []) - return run_generator( - block_generator, constants, min(constants.MAX_BLOCK_COST_CLVM, block.transactions_info.cost), block.height - ) - - def run_generator_with_args( generator_program_hex: str, generator_args: List[SerializedProgram], @@ -201,13 +191,13 @@ def run_generator_with_args( @click.command() -@click.argument("file", type=click.File("rb")) -def cmd_run_json_block_file(file): +@click.argument("filename", type=click.Path(exists=True), default="testnet10.396963.json") +def cmd_run_json_block_file(filename): """`file` is a file containing a FullBlock in JSON format""" - return run_json_block_file(file) + return run_json_block_file(Path(filename)) -def run_json_block(full_block, constants: ConsensusConstants) -> List[CAT]: +def run_json_block(full_block, parent: Path, constants: ConsensusConstants) -> List[CAT]: ref_list = full_block["block"]["transactions_generator_ref_list"] tx_info: dict = full_block["block"]["transactions_info"] generator_program_hex: str = full_block["block"]["transactions_generator"] @@ -215,18 +205,18 @@ def run_json_block(full_block, constants: ConsensusConstants) -> List[CAT]: cat_list: List[CAT] = [] if tx_info and generator_program_hex: cost = tx_info["cost"] - args = ref_list_to_args(ref_list) + args = ref_list_to_args(ref_list, parent) cat_list = run_generator_with_args(generator_program_hex, args, constants, cost, height) return cat_list -def run_json_block_file(file: TextIO): - full_block = json.load(file) +def run_json_block_file(filename: Path): + full_block = json.load(filename.open("rb")) # pull in current constants from config.yaml _, constants = get_config_and_constants() - cat_list = run_json_block(full_block, constants) + cat_list = run_json_block(full_block, filename.parent.absolute(), constants) cat_list_json = json.dumps([cat.cat_to_dict() for cat in cat_list]) print(cat_list_json) From 13bf50833c9da1de8062b79c64d0e96fc30c30f6 Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Thu, 3 Feb 2022 19:31:49 +0100 Subject: [PATCH 002/378] add tool to test adding every block from a blockchain database (#10076) * add tool to test adding every block from a blockchain database, as a sanity check for a new version of the full node * exercise full_node in artificial sync from scratch test * add test exercising test_full_sync.py --- tests/tools/test-blockchain-db.sqlite | Bin 0 -> 405504 bytes tests/tools/test_full_sync.py | 12 ++++ tools/test_full_sync.py | 77 ++++++++++++++++++++++++++ 3 files changed, 89 insertions(+) create mode 100644 tests/tools/test-blockchain-db.sqlite create mode 100644 tests/tools/test_full_sync.py create mode 100755 tools/test_full_sync.py diff --git a/tests/tools/test-blockchain-db.sqlite b/tests/tools/test-blockchain-db.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..8828af12f545d2bf6723075123abf0639f58d26a GIT binary patch literal 405504 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 None: + + with tempfile.TemporaryDirectory() as root_dir: + + root_path = Path(root_dir) + chia_init(root_path, should_check_keys=False) + config = load_config(root_path, "config.yaml") + + overrides = config["network_overrides"]["constants"][config["selected_network"]] + constants = DEFAULT_CONSTANTS.replace_str_to_bytes(**overrides) + full_node = FullNode( + config["full_node"], + root_path=root_path, + consensus_constants=constants, + ) + + try: + await full_node._start() + + print() + counter = 0 + async with aiosqlite.connect(file) as in_db: + + rows = await in_db.execute("SELECT header_hash, height, block FROM full_blocks ORDER BY height") + + block_batch = [] + + start_time = time() + async for r in rows: + block = FullBlock.from_bytes(zstd.decompress(r[2])) + + block_batch.append(block) + if len(block_batch) < 32: + continue + + success, advanced_peak, fork_height, coin_changes = await full_node.receive_block_batch( + block_batch, None, None # type: ignore[arg-type] + ) + assert success + assert advanced_peak + counter += len(block_batch) + print(f"\rheight {counter} {counter/(time() - start_time):0.2f} blocks/s ", end="") + block_batch = [] + finally: + print("closing full node") + full_node._close() + await full_node._await_closed() + + +@click.command() +@click.argument("file", type=click.Path(), required=True) +@click.argument("db-version", type=int, required=False, default=2) +def main(file: Path, db_version) -> None: + asyncio.run(run_sync_test(Path(file), db_version)) + + +if __name__ == "__main__": + # pylint: disable = no-value-for-parameter + main() From b0a19582f579ef67037045dd29ef784d7e3c6069 Mon Sep 17 00:00:00 2001 From: Mariano Sorgente <3069354+mariano54@users.noreply.github.com> Date: Thu, 3 Feb 2022 13:34:26 -0500 Subject: [PATCH 003/378] check_keys: check 50 instead of 500, farm default observer (#10083) * check_keys: check 100 instead of 500, and also observer * check_keys: check 50, and address PR comments --- chia/cmds/init_funcs.py | 41 +++++++++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/chia/cmds/init_funcs.py b/chia/cmds/init_funcs.py index 2741185b40a6..7bd52ac2c3d6 100644 --- a/chia/cmds/init_funcs.py +++ b/chia/cmds/init_funcs.py @@ -22,7 +22,6 @@ save_config, unflatten_properties, ) -from chia.util.ints import uint32 from chia.util.keychain import Keychain from chia.util.path import mkdir, path_from_root from chia.util.ssl_check import ( @@ -33,7 +32,13 @@ check_and_fix_permissions_for_ssl_file, fix_ssl, ) -from chia.wallet.derive_keys import master_sk_to_pool_sk, master_sk_to_wallet_sk +from chia.wallet.derive_keys import ( + master_sk_to_pool_sk, + master_sk_to_wallet_sk_intermediate, + master_sk_to_wallet_sk_unhardened_intermediate, + _derive_path, + _derive_path_unhardened, +) from chia.cmds.configure import configure private_node_names = {"full_node", "wallet", "farmer", "harvester", "timelord", "daemon"} @@ -74,19 +79,39 @@ def check_keys(new_root: Path, keychain: Optional[Keychain] = None) -> None: all_targets = [] stop_searching_for_farmer = "xch_target_address" not in config["farmer"] stop_searching_for_pool = "xch_target_address" not in config["pool"] - number_of_ph_to_search = 500 + number_of_ph_to_search = 50 selected = config["selected_network"] prefix = config["network_overrides"]["config"][selected]["address_prefix"] + + intermediates = {} + for sk, _ in all_sks: + intermediates[bytes(sk)] = { + "observer": master_sk_to_wallet_sk_unhardened_intermediate(sk), + "non-observer": master_sk_to_wallet_sk_intermediate(sk), + } + for i in range(number_of_ph_to_search): if stop_searching_for_farmer and stop_searching_for_pool and i > 0: break for sk, _ in all_sks: + intermediate_n = intermediates[bytes(sk)]["non-observer"] + intermediate_o = intermediates[bytes(sk)]["observer"] + all_targets.append( - encode_puzzle_hash(create_puzzlehash_for_pk(master_sk_to_wallet_sk(sk, uint32(i)).get_g1()), prefix) + encode_puzzle_hash( + create_puzzlehash_for_pk(_derive_path_unhardened(intermediate_o, [i]).get_g1()), prefix + ) ) - if all_targets[-1] == config["farmer"].get("xch_target_address"): + all_targets.append( + encode_puzzle_hash(create_puzzlehash_for_pk(_derive_path(intermediate_n, [i]).get_g1()), prefix) + ) + if all_targets[-1] == config["farmer"].get("xch_target_address") or all_targets[-2] == config["farmer"].get( + "xch_target_address" + ): stop_searching_for_farmer = True - if all_targets[-1] == config["pool"].get("xch_target_address"): + if all_targets[-1] == config["pool"].get("xch_target_address") or all_targets[-2] == config["pool"].get( + "xch_target_address" + ): stop_searching_for_pool = True # Set the destinations, if necessary @@ -99,7 +124,7 @@ def check_keys(new_root: Path, keychain: Optional[Keychain] = None) -> None: updated_target = True elif config["farmer"]["xch_target_address"] not in all_targets: print( - f"WARNING: using a farmer address which we don't have the private" + f"WARNING: using a farmer address which we might not have the private" f" keys for. We searched the first {number_of_ph_to_search} addresses. Consider overriding " f"{config['farmer']['xch_target_address']} with {all_targets[0]}" ) @@ -112,7 +137,7 @@ def check_keys(new_root: Path, keychain: Optional[Keychain] = None) -> None: updated_target = True elif config["pool"]["xch_target_address"] not in all_targets: print( - f"WARNING: using a pool address which we don't have the private" + f"WARNING: using a pool address which we might not have the private" f" keys for. We searched the first {number_of_ph_to_search} addresses. Consider overriding " f"{config['pool']['xch_target_address']} with {all_targets[0]}" ) From 22d664794bec4a983483938bdab93f82d30ac4f1 Mon Sep 17 00:00:00 2001 From: Patrick Maslana <79757486+pmaslana@users.noreply.github.com> Date: Thu, 3 Feb 2022 11:48:47 -0700 Subject: [PATCH 004/378] Updated the list of names that will be listed as reviewers for the automated mozilla-ca approval. (#10093) --- .github/workflows/mozilla-ca-cert.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/mozilla-ca-cert.yml b/.github/workflows/mozilla-ca-cert.yml index bdeab604232c..b097ec13fb00 100644 --- a/.github/workflows/mozilla-ca-cert.yml +++ b/.github/workflows/mozilla-ca-cert.yml @@ -27,6 +27,6 @@ jobs: branch: mozilla-ca-updates commit-message: "adding ca updates" delete-branch: true - reviewers: "justinengland,hoffmang9,cmmarslender,nirajpathak13" + reviewers: "justinengland,cmmarslender,nirajpathak13,pmaslana,austinsirkin,Starttoaster,wallentx" title: "CA Cert updates" token: "${{ secrets.GITHUB_TOKEN }}" From e0aa80b42892d7d42dfb98edd96488987febd947 Mon Sep 17 00:00:00 2001 From: Patrick Maslana <79757486+pmaslana@users.noreply.github.com> Date: Thu, 3 Feb 2022 14:25:02 -0700 Subject: [PATCH 005/378] Fix the reviewers names (#10097) * Fix the reviewers names * Removed space from the names of the reviewers. --- .github/workflows/mozilla-ca-cert.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/mozilla-ca-cert.yml b/.github/workflows/mozilla-ca-cert.yml index b097ec13fb00..4bcd7333e18a 100644 --- a/.github/workflows/mozilla-ca-cert.yml +++ b/.github/workflows/mozilla-ca-cert.yml @@ -27,6 +27,6 @@ jobs: branch: mozilla-ca-updates commit-message: "adding ca updates" delete-branch: true - reviewers: "justinengland,cmmarslender,nirajpathak13,pmaslana,austinsirkin,Starttoaster,wallentx" + reviewers: "wjblanke,emlowe" title: "CA Cert updates" token: "${{ secrets.GITHUB_TOKEN }}" From 6872e75a48b1ebf952bdecd244784cae75c16c2d Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Fri, 4 Feb 2022 01:09:04 +0100 Subject: [PATCH 006/378] benchmark for blockchain.get_block_generator() (#9999) --- benchmarks/block_ref.py | 100 ++++++++++++++++++++++++++++ benchmarks/transaction_height_delta | Bin 0 -> 439288 bytes chia/consensus/blockchain.py | 7 +- chia/types/block_protocol.py | 21 ++++++ 4 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 benchmarks/block_ref.py create mode 100644 benchmarks/transaction_height_delta create mode 100644 chia/types/block_protocol.py diff --git a/benchmarks/block_ref.py b/benchmarks/block_ref.py new file mode 100644 index 000000000000..84a80edc7245 --- /dev/null +++ b/benchmarks/block_ref.py @@ -0,0 +1,100 @@ +import click +import aiosqlite +import asyncio +import time +import random +import os + +from typing import Optional, List +from pathlib import Path +from dataclasses import dataclass + +from chia.consensus.blockchain import Blockchain +from chia.consensus.default_constants import DEFAULT_CONSTANTS +from chia.full_node.block_store import BlockStore +from chia.full_node.coin_store import CoinStore +from chia.full_node.hint_store import HintStore +from chia.types.blockchain_format.program import SerializedProgram +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.util.db_version import lookup_db_version +from chia.util.db_wrapper import DBWrapper +from chia.util.ints import uint32 + +# the first transaction block. Each byte in transaction_height_delta is the +# number of blocks to skip forward to get to the next transaction block +transaction_block_heights = [] +last = 225698 +file_path = os.path.realpath(__file__) +for delta in open(Path(file_path).parent / "transaction_height_delta", "rb").read(): + new = last + delta + transaction_block_heights.append(new) + last = new + + +@dataclass(frozen=True) +class BlockInfo: + prev_header_hash: bytes32 + transactions_generator: Optional[SerializedProgram] + transactions_generator_ref_list: List[uint32] + + +def random_refs() -> List[uint32]: + ret = random.sample(transaction_block_heights, DEFAULT_CONSTANTS.MAX_GENERATOR_REF_LIST_SIZE) + random.shuffle(ret) + return [uint32(i) for i in ret] + + +REPETITIONS = 100 + + +async def main(db_path: Path): + + random.seed(0x213FB154) + + async with aiosqlite.connect(db_path) as connection: + await connection.execute("pragma journal_mode=wal") + await connection.execute("pragma synchronous=FULL") + await connection.execute("pragma query_only=ON") + db_version: int = await lookup_db_version(connection) + + db_wrapper = DBWrapper(connection, db_version=db_version) + block_store = await BlockStore.create(db_wrapper) + hint_store = await HintStore.create(db_wrapper) + coin_store = await CoinStore.create(db_wrapper) + + start_time = time.time() + # make configurable + reserved_cores = 4 + blockchain = await Blockchain.create( + coin_store, block_store, DEFAULT_CONSTANTS, hint_store, db_path.parent, reserved_cores + ) + + peak = blockchain.get_peak() + timing = 0.0 + for i in range(REPETITIONS): + block = BlockInfo( + peak.header_hash, + SerializedProgram.from_bytes(bytes.fromhex("80")), + random_refs(), + ) + + start_time = time.time() + gen = await blockchain.get_block_generator(block) + one_call = time.time() - start_time + timing += one_call + assert gen is not None + + print(f"get_block_generator(): {timing/REPETITIONS:0.3f}s") + + blockchain.shut_down() + + +@click.command() +@click.argument("db-path", type=click.Path()) +def entry_point(db_path: Path): + asyncio.run(main(Path(db_path))) + + +if __name__ == "__main__": + # pylint: disable = no-value-for-parameter + entry_point() diff --git a/benchmarks/transaction_height_delta b/benchmarks/transaction_height_delta new file mode 100644 index 0000000000000000000000000000000000000000..85e5bddd7d8e03ad054a8558bacac523f9d75f5b GIT binary patch literal 439288 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

rD@TohRg>Erq2t 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

{|ZO;7qE!?e(BFjNef@m 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 bool: return False async def get_block_generator( - self, block: Union[FullBlock, UnfinishedBlock], additional_blocks=None + self, block: BlockInfo, additional_blocks: Dict[bytes32, FullBlock] = None ) -> Optional[BlockGenerator]: if additional_blocks is None: additional_blocks = {} @@ -895,7 +896,7 @@ async def get_block_generator( else: # First tries to find the blocks in additional_blocks reorg_chain: Dict[uint32, FullBlock] = {} - curr: Union[FullBlock, UnfinishedBlock] = block + curr = block additional_height_dict = {} while curr.prev_header_hash in additional_blocks: prev: FullBlock = additional_blocks[curr.prev_header_hash] diff --git a/chia/types/block_protocol.py b/chia/types/block_protocol.py new file mode 100644 index 000000000000..47ed24f3fcda --- /dev/null +++ b/chia/types/block_protocol.py @@ -0,0 +1,21 @@ +from typing import List, Optional + +from typing_extensions import Protocol + +from chia.types.blockchain_format.program import SerializedProgram +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.util.ints import uint32 + + +class BlockInfo(Protocol): + @property + def prev_header_hash(self) -> bytes32: + pass + + @property + def transactions_generator(self) -> Optional[SerializedProgram]: + pass + + @property + def transactions_generator_ref_list(self) -> List[uint32]: + pass From bd82689e9a74e854e4238495745c21bd567919e1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 3 Feb 2022 18:05:37 -0800 Subject: [PATCH 007/378] adding ca updates (#10095) Co-authored-by: ChiaAutomation --- mozilla-ca | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mozilla-ca b/mozilla-ca index b1b808ab9300..20338a007589 160000 --- a/mozilla-ca +++ b/mozilla-ca @@ -1 +1 @@ -Subproject commit b1b808ab930004fc6b4afc4b248dee0a136f3f00 +Subproject commit 20338a00758970843652262219f4e0f639a6feed From 08f0817bd632a6e0665992d3a99f89edc51ab16e Mon Sep 17 00:00:00 2001 From: William Blanke Date: Thu, 3 Feb 2022 18:10:32 -0800 Subject: [PATCH 008/378] update gui 1d15ea86711811d717715bf5566773739faca282 --- chia-blockchain-gui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia-blockchain-gui b/chia-blockchain-gui index a0259211584c..1d15ea867118 160000 --- a/chia-blockchain-gui +++ b/chia-blockchain-gui @@ -1 +1 @@ -Subproject commit a0259211584cb68405cc6c673ec46fcf7468a43d +Subproject commit 1d15ea86711811d717715bf5566773739faca282 From 2f2593661c842b70a0e848752f12777f2df3ed18 Mon Sep 17 00:00:00 2001 From: William Blanke Date: Thu, 3 Feb 2022 18:12:16 -0800 Subject: [PATCH 009/378] update gui 1d15ea86711811d717715bf5566773739faca282 --- chia-blockchain-gui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia-blockchain-gui b/chia-blockchain-gui index 1d15ea867118..26646cbba6c4 160000 --- a/chia-blockchain-gui +++ b/chia-blockchain-gui @@ -1 +1 @@ -Subproject commit 1d15ea86711811d717715bf5566773739faca282 +Subproject commit 26646cbba6c4e04d390e1830e430252e81a33f87 From f16bd948ee60e0d26f9f2caeaa7449d1e075b9e3 Mon Sep 17 00:00:00 2001 From: Gene Hoffman <30377676+hoffmang9@users.noreply.github.com> Date: Thu, 3 Feb 2022 21:05:20 -0800 Subject: [PATCH 010/378] Correct comment about max future block timestamp (#10105) --- chia/consensus/block_body_validation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia/consensus/block_body_validation.py b/chia/consensus/block_body_validation.py index c9dfe57d74c4..0ed75c90b18a 100644 --- a/chia/consensus/block_body_validation.py +++ b/chia/consensus/block_body_validation.py @@ -156,7 +156,7 @@ async def validate_block_body( removals_puzzle_dic: Dict[bytes32, bytes32] = {} cost: uint64 = uint64(0) - # In header validation we check that timestamp is not more that 10 minutes into the future + # In header validation we check that timestamp is not more that 5 minutes into the future # 6. No transactions before INITIAL_TRANSACTION_FREEZE timestamp # (this test has been removed) From fa256b8149435b7c60c00e5d3295f3c6c91a399c Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Fri, 4 Feb 2022 17:46:17 +0100 Subject: [PATCH 011/378] fix typo in 'chia init --db-v1' (#10109) --- chia/cmds/init_funcs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia/cmds/init_funcs.py b/chia/cmds/init_funcs.py index 7bd52ac2c3d6..0b3c05859292 100644 --- a/chia/cmds/init_funcs.py +++ b/chia/cmds/init_funcs.py @@ -427,7 +427,7 @@ def chia_init( config: Dict if v1_db: config = load_config(root_path, "config.yaml") - db_pattern = config["database_path"] + db_pattern = config["full_node"]["database_path"] new_db_path = db_pattern.replace("_v2_", "_v1_") config["full_node"]["database_path"] = new_db_path save_config(root_path, "config.yaml", config) From eaa6b8beabd646861b9121649a9eb292f9629322 Mon Sep 17 00:00:00 2001 From: Jeff Date: Fri, 4 Feb 2022 09:49:20 -0800 Subject: [PATCH 012/378] Rename migrated standalone_wallet wallet DB to v2 (#10086) * Rename migrated standalone_wallet wallet db to use '_new.sqlite' suffix * Removed unnecessary format string * Wallet DB is now named "v2" instead of "_new" --- chia/util/initial-config.yaml | 3 ++- chia/wallet/wallet_node.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/chia/util/initial-config.yaml b/chia/util/initial-config.yaml index e1f04232d6bc..bae4c7fd8e1f 100644 --- a/chia/util/initial-config.yaml +++ b/chia/util/initial-config.yaml @@ -474,7 +474,8 @@ wallet: port: 8444 testing: False - database_path: wallet/db/blockchain_wallet_v1_CHALLENGE_KEY.sqlite + # v2 used by the light wallet sync protocol + database_path: wallet/db/blockchain_wallet_v2_CHALLENGE_KEY.sqlite # wallet_peers_path is deprecated and has been replaced by wallet_peers_file_path wallet_peers_path: wallet/db/wallet_peers.sqlite wallet_peers_file_path: wallet/db/wallet_peers.dat diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index a01747a06480..adaf010183c9 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -209,8 +209,8 @@ async def _start( .replace("CHALLENGE", self.config["selected_network"]) .replace("KEY", db_path_key_suffix) ) - path = path_from_root(self.root_path, f"{db_path_replaced}_new") - standalone_path = path_from_root(STANDALONE_ROOT_PATH, f"{db_path_replaced}_new") + path = path_from_root(self.root_path, db_path_replaced.replace("v1", "v2")) + standalone_path = path_from_root(STANDALONE_ROOT_PATH, f"{db_path_replaced.replace('v2', 'v1')}_new") if not path.exists(): if standalone_path.exists(): path.write_bytes(standalone_path.read_bytes()) From f2fe1dca6266f9b1f8ab61798ad40e5985f50b23 Mon Sep 17 00:00:00 2001 From: Jeff Date: Fri, 4 Feb 2022 16:43:51 -0800 Subject: [PATCH 013/378] Support for performing incremental legacy key migration. (#10085) * CLI support for performing an incremental keyring migration. This handles the case where new keys were created by an older client or the light wallet, and those keys then need to be moved to keyring.yaml. * Opportunistically perform a silent incremental keyring migration when the GUI unlocks the keyring. Track when keyring migration occurs so that we don't needlessly attempt on each GUI launch. ~/.chia_keys/.last_legacy_migration will contain the app version that last attempted migration. * Formatting & linter fixes * Tests for `chia keys migrate`. Missing a test for incremental migration. * Additional keyring migration tests * Formatting updates from black in files unrelated to this branch. * Revert "Formatting updates from black in files unrelated to this branch." This reverts commit a85030e8e0ea7406683efd8ae41e224c861e08ff. * Exit loop if remaining_keys <= 0 * Linter fix? Manually making this change as black doesn't identify any issues locally. * Linter fix again... --- chia/cmds/keys.py | 8 + chia/cmds/keys_funcs.py | 44 ++++++ chia/daemon/server.py | 14 ++ chia/util/keychain.py | 117 +++++++++++++- chia/util/keyring_wrapper.py | 19 ++- tests/core/cmds/test_keys.py | 295 ++++++++++++++++++++++++++++++++++- 6 files changed, 489 insertions(+), 8 deletions(-) diff --git a/chia/cmds/keys.py b/chia/cmds/keys.py index 8390a132f3e9..243f1aca6d18 100644 --- a/chia/cmds/keys.py +++ b/chia/cmds/keys.py @@ -136,6 +136,14 @@ def verify_cmd(message: str, public_key: str, signature: str): verify(message, public_key, signature) +@keys_cmd.command("migrate", short_help="Attempt to migrate keys to the Chia keyring") +@click.pass_context +def migrate_cmd(ctx: click.Context): + from .keys_funcs import migrate_keys + + migrate_keys() + + @keys_cmd.group("derive", short_help="Derive child keys or wallet addresses") @click.option( "--fingerprint", diff --git a/chia/cmds/keys_funcs.py b/chia/cmds/keys_funcs.py index 8727fc154fb0..deb22d75b8f2 100644 --- a/chia/cmds/keys_funcs.py +++ b/chia/cmds/keys_funcs.py @@ -180,6 +180,50 @@ def verify(message: str, public_key: str, signature: str): print(AugSchemeMPL.verify(public_key, messageBytes, signature)) +def migrate_keys(): + from chia.util.keyring_wrapper import KeyringWrapper + from chia.util.misc import prompt_yes_no + + # Check if the keyring needs a full migration (i.e. if it's using the old keyring) + if Keychain.needs_migration(): + KeyringWrapper.get_shared_instance().migrate_legacy_keyring_interactive() + else: + keys_to_migrate, legacy_keyring = Keychain.get_keys_needing_migration() + if len(keys_to_migrate) > 0 and legacy_keyring is not None: + print(f"Found {len(keys_to_migrate)} key(s) that need migration:") + for key, _ in keys_to_migrate: + print(f"Fingerprint: {key.get_g1().get_fingerprint()}") + + print() + response = prompt_yes_no("Migrate these keys? (y/n) ") + if response: + keychain = Keychain() + for sk, seed_bytes in keys_to_migrate: + mnemonic = bytes_to_mnemonic(seed_bytes) + keychain.add_private_key(mnemonic, "") + fingerprint = sk.get_g1().get_fingerprint() + print(f"Added private key with public key fingerprint {fingerprint}") + + print(f"Migrated {len(keys_to_migrate)} key(s)") + + print("Verifying migration results...", end="") + if Keychain.verify_keys_present(keys_to_migrate): + print(" Verified") + print() + response = prompt_yes_no("Remove key(s) from old keyring? (y/n) ") + if response: + legacy_keyring.delete_keys(keys_to_migrate) + print(f"Removed {len(keys_to_migrate)} key(s) from old keyring") + print("Migration complete") + else: + print(" Failed") + sys.exit(1) + else: + print("No keys need migration") + + Keychain.mark_migration_checked_for_current_version() + + def _clear_line_part(n: int): # Move backward, overwrite with spaces, then move backward again sys.stdout.write("\b" * n) diff --git a/chia/daemon/server.py b/chia/daemon/server.py index cfbfcca1644e..bf3119c59d5d 100644 --- a/chia/daemon/server.py +++ b/chia/daemon/server.py @@ -385,6 +385,8 @@ async def keyring_status(self) -> Dict[str, Any]: "passphrase_hint": passphrase_hint, "passphrase_requirements": requirements, } + # Help diagnose GUI launch issues + self.log.debug(f"Keyring status: {response}") return response async def unlock_keyring(self, request: Dict[str, Any]) -> Dict[str, Any]: @@ -398,6 +400,18 @@ async def unlock_keyring(self, request: Dict[str, Any]) -> Dict[str, Any]: if Keychain.master_passphrase_is_valid(key, force_reload=True): Keychain.set_cached_master_passphrase(key) success = True + + # Attempt to silently migrate legacy keys if necessary. Non-fatal if this fails. + try: + if not Keychain.migration_checked_for_current_version(): + self.log.info("Will attempt to migrate legacy keys...") + Keychain.migrate_legacy_keys_silently() + self.log.info("Migration of legacy keys complete.") + else: + self.log.debug("Skipping legacy key migration (previously attempted).") + except Exception: + self.log.exception("Failed to migrate keys silently. Run `chia keys migrate` manually.") + # Inform the GUI of keyring status changes self.keyring_status_changed(await self.keyring_status(), "wallet_ui") else: diff --git a/chia/util/keychain.py b/chia/util/keychain.py index e9d79a536c27..6c59b0546db5 100644 --- a/chia/util/keychain.py +++ b/chia/util/keychain.py @@ -235,10 +235,17 @@ class Keychain: list of all keys. """ - def __init__(self, user: Optional[str] = None, service: Optional[str] = None): + def __init__(self, user: Optional[str] = None, service: Optional[str] = None, force_legacy: bool = False): self.user = user if user is not None else default_keychain_user() self.service = service if service is not None else default_keychain_service() - self.keyring_wrapper = KeyringWrapper.get_shared_instance() + if force_legacy: + legacy_keyring_wrapper = KeyringWrapper.get_legacy_instance() + if legacy_keyring_wrapper is not None: + self.keyring_wrapper = legacy_keyring_wrapper + else: + return None + else: + self.keyring_wrapper = KeyringWrapper.get_shared_instance() @unlocks_keyring(use_passphrase_cache=True) def _get_pk_and_entropy(self, user: str) -> Optional[Tuple[G1Element, bytes]]: @@ -399,6 +406,27 @@ def delete_key_by_fingerprint(self, fingerprint: int): index += 1 pkent = self._get_pk_and_entropy(get_private_key_user(self.user, index)) + def delete_keys(self, keys_to_delete: List[Tuple[PrivateKey, bytes]]): + """ + Deletes all keys in the list. + """ + remaining_keys = {str(x[0]) for x in keys_to_delete} + index = 0 + pkent = self._get_pk_and_entropy(get_private_key_user(self.user, index)) + while index <= MAX_KEYS and len(remaining_keys) > 0: + if pkent is not None: + mnemonic = bytes_to_mnemonic(pkent[1]) + seed = mnemonic_to_seed(mnemonic, "") + sk = AugSchemeMPL.key_gen(seed) + sk_str = str(sk) + if sk_str in remaining_keys: + self.keyring_wrapper.delete_passphrase(self.service, get_private_key_user(self.user, index)) + remaining_keys.remove(sk_str) + index += 1 + pkent = self._get_pk_and_entropy(get_private_key_user(self.user, index)) + if len(remaining_keys) > 0: + raise ValueError(f"{len(remaining_keys)} keys could not be found for deletion") + def delete_all_keys(self): """ Deletes all keys from the keychain. @@ -462,6 +490,44 @@ def needs_migration() -> bool: """ return KeyringWrapper.get_shared_instance().using_legacy_keyring() + @staticmethod + def migration_checked_for_current_version() -> bool: + """ + Returns a bool indicating whether the current client version has checked the legacy keyring + for keys needing migration. + """ + + def compare_versions(version1: str, version2: str) -> int: + # Making the assumption that versions will be of the form: x[x].y[y].z[z] + # We limit the number of components to 3, with each component being up to 2 digits long + ver1: List[int] = [int(n[:2]) for n in version1.split(".")[:3]] + ver2: List[int] = [int(n[:2]) for n in version2.split(".")[:3]] + if ver1 > ver2: + return 1 + elif ver1 < ver2: + return -1 + else: + return 0 + + migration_version_file: Path = KeyringWrapper.get_shared_instance().keys_root_path / ".last_legacy_migration" + if migration_version_file.exists(): + current_version_str = pkg_resources.get_distribution("chia-blockchain").version + with migration_version_file.open("r") as f: + last_migration_version_str = f.read().strip() + return compare_versions(current_version_str, last_migration_version_str) <= 0 + + return False + + @staticmethod + def mark_migration_checked_for_current_version(): + """ + Marks the current client version as having checked the legacy keyring for keys needing migration. + """ + migration_version_file: Path = KeyringWrapper.get_shared_instance().keys_root_path / ".last_legacy_migration" + current_version_str = pkg_resources.get_distribution("chia-blockchain").version + with migration_version_file.open("w") as f: + f.write(current_version_str) + @staticmethod def handle_migration_completed(): """ @@ -493,6 +559,53 @@ def migrate_legacy_keyring( KeyringWrapper.get_shared_instance().migrate_legacy_keyring(cleanup_legacy_keyring=cleanup_legacy_keyring) + @staticmethod + def get_keys_needing_migration() -> Tuple[List[Tuple[PrivateKey, bytes]], Optional["Keychain"]]: + legacy_keyring: Optional[Keychain] = Keychain(force_legacy=True) + if legacy_keyring is None: + return [], None + keychain = Keychain() + all_legacy_sks = legacy_keyring.get_all_private_keys() + all_sks = keychain.get_all_private_keys() + set_legacy_sks = {str(x[0]) for x in all_legacy_sks} + set_sks = {str(x[0]) for x in all_sks} + missing_legacy_keys = set_legacy_sks - set_sks + keys_needing_migration = [x for x in all_legacy_sks if str(x[0]) in missing_legacy_keys] + + return keys_needing_migration, legacy_keyring + + @staticmethod + def verify_keys_present(keys_to_verify: List[Tuple[PrivateKey, bytes]]) -> bool: + """ + Verifies that the given keys are present in the keychain. + """ + keychain = Keychain() + all_sks = keychain.get_all_private_keys() + set_sks = {str(x[0]) for x in all_sks} + keys_present = set_sks.issuperset(set(map(lambda x: str(x[0]), keys_to_verify))) + return keys_present + + @staticmethod + def migrate_legacy_keys_silently(): + """ + Migrates keys silently, without prompting the user. Requires that keyring.yaml already exists. + Does not attempt to delete migrated keys from their old location. + """ + if Keychain.needs_migration(): + raise RuntimeError("Full keyring migration is required. Cannot run silently.") + + keys_to_migrate, _ = Keychain.get_keys_needing_migration() + if len(keys_to_migrate) > 0: + keychain = Keychain() + for _, seed_bytes in keys_to_migrate: + mnemonic = bytes_to_mnemonic(seed_bytes) + keychain.add_private_key(mnemonic, "") + + if not Keychain.verify_keys_present(keys_to_migrate): + raise RuntimeError("Failed to migrate keys. Legacy keyring left intact.") + + Keychain.mark_migration_checked_for_current_version() + @staticmethod def passphrase_is_optional() -> bool: """ diff --git a/chia/util/keyring_wrapper.py b/chia/util/keyring_wrapper.py index 69e9339b894d..642d773c5aa6 100644 --- a/chia/util/keyring_wrapper.py +++ b/chia/util/keyring_wrapper.py @@ -49,7 +49,7 @@ def get_os_passphrase_store() -> Optional[OSPassphraseStore]: return None -def check_legacy_keyring_keys_present(keyring: Union[MacKeyring, WinKeyring]) -> bool: +def check_legacy_keyring_keys_present(keyring: LegacyKeyring) -> bool: from keyring.credentials import SimpleCredential from chia.util.keychain import default_keychain_user, default_keychain_service, get_private_key_user, MAX_KEYS @@ -104,14 +104,21 @@ class KeyringWrapper: cached_passphrase_is_validated: bool = False legacy_keyring = None - def __init__(self, keys_root_path: Path = DEFAULT_KEYS_ROOT_PATH): + def __init__(self, keys_root_path: Path = DEFAULT_KEYS_ROOT_PATH, force_legacy: bool = False): """ Initializes the keyring backend based on the OS. For Linux, we previously used CryptFileKeyring. We now use our own FileKeyring backend and migrate the data from the legacy CryptFileKeyring (on write). """ self.keys_root_path = keys_root_path - self.refresh_keyrings() + if force_legacy: + legacy_keyring = get_legacy_keyring_instance() + if check_legacy_keyring_keys_present(legacy_keyring): + self.keyring = legacy_keyring + else: + return None + else: + self.refresh_keyrings() def refresh_keyrings(self): self.keyring = None @@ -188,6 +195,10 @@ def get_shared_instance(create_if_necessary=True): def cleanup_shared_instance(): KeyringWrapper.__shared_instance = None + @staticmethod + def get_legacy_instance() -> Optional["KeyringWrapper"]: + return KeyringWrapper(force_legacy=True) + def get_keyring(self): """ Return the current keyring backend. The legacy keyring is preferred if it's in use @@ -422,6 +433,8 @@ def migrate_legacy_keys(self) -> MigrationResults: for (user, passphrase) in user_passphrase_pairs: self.keyring.set_password(service, user, passphrase) + Keychain.mark_migration_checked_for_current_version() + return KeyringWrapper.MigrationResults( original_private_keys, self.legacy_keyring, service, [user for (user, _) in user_passphrase_pairs] ) diff --git a/tests/core/cmds/test_keys.py b/tests/core/cmds/test_keys.py index 10467b83376b..25b9fd7100de 100644 --- a/tests/core/cmds/test_keys.py +++ b/tests/core/cmds/test_keys.py @@ -1,16 +1,20 @@ import os +import pkg_resources import pytest import re +from blspy import PrivateKey from chia.cmds.chia import cli from chia.cmds.keys import delete_all_cmd, generate_and_print_cmd, show_cmd, sign_cmd, verify_cmd from chia.util.config import load_config -from chia.util.keychain import generate_mnemonic -from chia.util.keyring_wrapper import KeyringWrapper +from chia.util.file_keyring import FileKeyring +from chia.util.keychain import DEFAULT_USER, DEFAULT_SERVICE, Keychain, generate_mnemonic +from chia.util.keyring_wrapper import DEFAULT_KEYS_ROOT_PATH, KeyringWrapper, LegacyKeyring from click.testing import CliRunner, Result +from keyring.backend import KeyringBackend from pathlib import Path from tests.util.keyring import TempKeyring -from typing import Dict +from typing import Dict, List, Optional, Tuple TEST_MNEMONIC_SEED = ( @@ -21,6 +25,46 @@ TEST_FINGERPRINT = 2877570395 +class DummyLegacyKeyring(KeyringBackend): + + # Fingerprint 2474840988 + KEY_0 = ( + "89e29e5f9c3105b2a853475cab2392468cbfb1d65c3faabea8ebc78fe903fd279e56a8d93f6325fc6c3d833a2ae74832" + "b8feaa3d6ee49998f43ce303b66dcc5abb633e5c1d80efe85c40766135e4a44c" + ) + + # Fingerprint 4149609062 + KEY_1 = ( + "8b0d72288727af6238fcd9b0a663cd7d4728738fca597d0046cbb42b6432e0a5ae8026683fc5f9c73df26fb3e1cec2c8" + "ad1b4f601107d96a99f6fa9b9d2382918fb1e107fb6655c7bdd8c77c1d9c201f" + ) + + # Fingerprint 3618811800 + KEY_2 = ( + "8b2a26ba319f83bd3da5b1b147a817ecc4ca557f037c9db1cfedc59b16ee6880971b7d292f023358710a292c8db0eb82" + "35808f914754ae24e493fad9bc7f654b0f523fb406973af5235256a39bed1283" + ) + + def __init__(self, populate: bool = True): + self.service_dict = {} + + if populate: + self.service_dict[DEFAULT_SERVICE] = { + f"wallet-{DEFAULT_USER}-0": DummyLegacyKeyring.KEY_0, + f"wallet-{DEFAULT_USER}-1": DummyLegacyKeyring.KEY_1, + f"wallet-{DEFAULT_USER}-2": DummyLegacyKeyring.KEY_2, + } + + def get_password(self, service, username, password=None): + return self.service_dict.get(service, {}).get(username) + + def set_password(self, service, username, password): + self.service_dict.setdefault(service, {})[username] = password + + def delete_password(self, service, username): + del self.service_dict[service][username] + + class TestKeysCommands: @pytest.fixture(scope="function") def empty_keyring(self): @@ -41,6 +85,30 @@ def mnemonic_seed_file(self, tmp_path): f.write(TEST_MNEMONIC_SEED) return seed_file + @pytest.fixture(scope="function") + def setup_keyringwrapper(self, tmp_path): + KeyringWrapper.cleanup_shared_instance() + KeyringWrapper.set_keys_root_path(tmp_path) + _ = KeyringWrapper.get_shared_instance() + yield + KeyringWrapper.cleanup_shared_instance() + KeyringWrapper.set_keys_root_path(DEFAULT_KEYS_ROOT_PATH) + + @pytest.fixture(scope="function") + def setup_legacy_keyringwrapper(self, tmp_path, monkeypatch): + def mock_setup_keyring_file_watcher(_): + pass + + # Silence errors in the watchdog module during testing + monkeypatch.setattr(FileKeyring, "setup_keyring_file_watcher", mock_setup_keyring_file_watcher) + + KeyringWrapper.cleanup_shared_instance() + KeyringWrapper.set_keys_root_path(tmp_path) + KeyringWrapper.get_shared_instance().legacy_keyring = DummyLegacyKeyring() + yield + KeyringWrapper.cleanup_shared_instance() + KeyringWrapper.set_keys_root_path(DEFAULT_KEYS_ROOT_PATH) + def test_generate_with_new_config(self, tmp_path, empty_keyring): """ Generate a new config and a new key. Verify that the config has @@ -681,3 +749,224 @@ def test_derive_child_keys(self, tmp_path, keyring_with_one_key): ) != -1 ) + + def test_migration_not_needed(self, tmp_path, setup_keyringwrapper, monkeypatch): + """ + Test the `chia keys migrate` command when no migration is necessary + """ + + def mock_keychain_needs_migration() -> bool: + return False + + monkeypatch.setattr(Keychain, "needs_migration", mock_keychain_needs_migration) + + def mock_keychain_get_keys_needing_migration() -> Tuple[List[Tuple[PrivateKey, bytes]], Optional[Keychain]]: + return [], None + + monkeypatch.setattr(Keychain, "get_keys_needing_migration", mock_keychain_get_keys_needing_migration) + + runner = CliRunner() + result: Result = runner.invoke( + cli, + [ + "--root-path", + os.fspath(tmp_path), + "keys", + "migrate", + ], + ) + + assert result.exit_code == 0 + assert result.output.find("No keys need migration") != -1 + + def test_migration_full(self, tmp_path, setup_legacy_keyringwrapper): + """ + Test the `chia keys migrate` command when a full migration is needed + """ + + legacy_keyring = KeyringWrapper.get_shared_instance().legacy_keyring + + assert legacy_keyring is not None + assert len(legacy_keyring.service_dict[DEFAULT_SERVICE]) == 3 + + runner = CliRunner() + init_result: Result = runner.invoke( + cli, + ["--root-path", os.fspath(tmp_path), "init"], + ) + + assert init_result.exit_code == 0 + + runner = CliRunner() + result: Result = runner.invoke( + cli, + [ + "--root-path", + os.fspath(tmp_path), + "keys", + "migrate", + ], + input="n\ny\ny\n", # Prompts: 'n' = don't set a passphrase, 'y' = begin migration, 'y' = remove legacy keys + ) + + assert result.exit_code == 0 + assert KeyringWrapper.get_shared_instance().using_legacy_keyring() is False # legacy keyring unset + assert type(KeyringWrapper.get_shared_instance().keyring) is FileKeyring # new keyring set + assert len(Keychain().get_all_public_keys()) == 3 # new keyring has 3 keys + assert len(legacy_keyring.service_dict[DEFAULT_SERVICE]) == 0 # legacy keys removed + + current_version_str = pkg_resources.get_distribution("chia-blockchain").version + last_migration_version_str = ( + KeyringWrapper.get_shared_instance().keys_root_path / ".last_legacy_migration" + ).read_text() + assert last_migration_version_str == current_version_str # last migration version set + + def test_migration_incremental(self, tmp_path, keyring_with_one_key, monkeypatch): + KeyringWrapper.set_keys_root_path(tmp_path) + KeyringWrapper.cleanup_shared_instance() + + keychain = keyring_with_one_key + legacy_keyring = DummyLegacyKeyring() + + def mock_get_legacy_keyring_instance() -> Optional[LegacyKeyring]: + nonlocal legacy_keyring + return legacy_keyring + + from chia.util import keyring_wrapper + + monkeypatch.setattr(keyring_wrapper, "get_legacy_keyring_instance", mock_get_legacy_keyring_instance) + + assert len(keychain.get_all_private_keys()) == 1 + assert keychain.keyring_wrapper.legacy_keyring is None + assert legacy_keyring is not None + assert len(legacy_keyring.service_dict[DEFAULT_SERVICE]) == 3 + + runner = CliRunner() + init_result: Result = runner.invoke( + cli, + ["--root-path", os.fspath(tmp_path), "init"], + ) + + assert init_result.exit_code == 0 + + runner = CliRunner() + result: Result = runner.invoke( + cli, + [ + "--root-path", + os.fspath(tmp_path), + "keys", + "migrate", + ], + input="y\ny\n", # Prompts: 'y' = migrate keys, 'y' = remove legacy keys + ) + + assert result.exit_code == 0 + assert KeyringWrapper.get_shared_instance().using_legacy_keyring() is False # legacy keyring is not set + assert type(KeyringWrapper.get_shared_instance().keyring) is FileKeyring # new keyring set + assert len(Keychain().get_all_public_keys()) == 4 # new keyring has 4 keys + assert len(legacy_keyring.service_dict[DEFAULT_SERVICE]) == 0 # legacy keys removed + + current_version_str = pkg_resources.get_distribution("chia-blockchain").version + last_migration_version_str = ( + KeyringWrapper.get_shared_instance().keys_root_path / ".last_legacy_migration" + ).read_text() + assert last_migration_version_str == current_version_str # last migration version set + + def test_migration_silent(self, tmp_path, keyring_with_one_key, monkeypatch): + KeyringWrapper.set_keys_root_path(tmp_path) + KeyringWrapper.cleanup_shared_instance() + + keychain = keyring_with_one_key + legacy_keyring = DummyLegacyKeyring() + + def mock_get_legacy_keyring_instance() -> Optional[LegacyKeyring]: + nonlocal legacy_keyring + return legacy_keyring + + from chia.util import keyring_wrapper + + monkeypatch.setattr(keyring_wrapper, "get_legacy_keyring_instance", mock_get_legacy_keyring_instance) + + assert len(keychain.get_all_private_keys()) == 1 + assert len(Keychain().get_all_private_keys()) == 1 + assert keychain.keyring_wrapper.legacy_keyring is None + assert legacy_keyring is not None + assert len(legacy_keyring.service_dict[DEFAULT_SERVICE]) == 3 + + keys_needing_migration, legacy_migration_keychain = Keychain.get_keys_needing_migration() + assert len(keys_needing_migration) == 3 + assert legacy_migration_keychain is not None + + Keychain.migrate_legacy_keys_silently() + + assert type(KeyringWrapper.get_shared_instance().keyring) is FileKeyring # new keyring set + assert len(Keychain().get_all_public_keys()) == 4 # new keyring has 4 keys + assert len(legacy_keyring.service_dict[DEFAULT_SERVICE]) == 3 # legacy keys still intact + + def test_migration_silent_keys_already_present(self, tmp_path, keyring_with_one_key, monkeypatch): + KeyringWrapper.set_keys_root_path(tmp_path) + KeyringWrapper.cleanup_shared_instance() + + keychain = keyring_with_one_key + pkent_str = keychain.keyring_wrapper.get_passphrase(DEFAULT_SERVICE, f"wallet-{DEFAULT_USER}-0") + legacy_keyring = DummyLegacyKeyring(populate=False) + legacy_keyring.set_password(DEFAULT_SERVICE, f"wallet-{DEFAULT_USER}-0", pkent_str) + + def mock_get_legacy_keyring_instance() -> Optional[LegacyKeyring]: + nonlocal legacy_keyring + return legacy_keyring + + from chia.util import keyring_wrapper + + monkeypatch.setattr(keyring_wrapper, "get_legacy_keyring_instance", mock_get_legacy_keyring_instance) + + assert len(keychain.get_all_private_keys()) == 1 + assert len(legacy_keyring.service_dict[DEFAULT_SERVICE]) == 1 + + keys_needing_migration, legacy_migration_keychain = Keychain.get_keys_needing_migration() + assert len(keys_needing_migration) == 0 + assert legacy_migration_keychain is not None + + Keychain.migrate_legacy_keys_silently() + + assert type(KeyringWrapper.get_shared_instance().keyring) is FileKeyring # new keyring set + assert len(Keychain().get_all_public_keys()) == 1 # keyring has 1 key + assert len(legacy_keyring.service_dict[DEFAULT_SERVICE]) == 1 # legacy keys still intact + + def test_migration_checked(self, tmp_path, monkeypatch): + KeyringWrapper.set_keys_root_path(tmp_path) + KeyringWrapper.cleanup_shared_instance() + + assert Keychain.migration_checked_for_current_version() is False + + dist_version = "" + + class DummyDistribution: + def __init__(self, version): + self.version = version + + def mock_get_distribution_version(_) -> DummyDistribution: + nonlocal dist_version + return DummyDistribution(dist_version) + + monkeypatch.setattr(pkg_resources, "get_distribution", mock_get_distribution_version) + + dist_version = "1.2.11.dev123" + assert pkg_resources.get_distribution("chia-blockchain").version == "1.2.11.dev123" + + Keychain.mark_migration_checked_for_current_version() + + last_migration_version_str = ( + KeyringWrapper.get_shared_instance().keys_root_path / ".last_legacy_migration" + ).read_text() + assert last_migration_version_str == "1.2.11.dev123" # last migration version set + + assert Keychain.migration_checked_for_current_version() is True + + dist_version = "1.2.11.dev345" + assert Keychain.migration_checked_for_current_version() is True # We don't check the build number + dist_version = "1.2.10.dev111" + assert Keychain.migration_checked_for_current_version() is True # Checked version > current version + dist_version = "1.3.0.dev100" + assert Keychain.migration_checked_for_current_version() is False # Checked version < current version From f67ca2c5a3b3d4080b7b39bf3a49257b22c7c1bc Mon Sep 17 00:00:00 2001 From: Jeff Date: Mon, 7 Feb 2022 11:06:36 -0800 Subject: [PATCH 014/378] STANDALONE_ROOT_PATH now expands to a different env variable to avoid (#10114) conflicting with DEFAULT_ROOT_PATH when CHIA_ROOT is set. When the GUI launches chia services, CHIA_ROOT is set, which was preventing the wallet backend from copying existing DBs from the standalone_wallet. --- chia/util/default_root.py | 4 +++- chia/wallet/wallet_node.py | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/chia/util/default_root.py b/chia/util/default_root.py index 960be0d1c7ce..6998044484d4 100644 --- a/chia/util/default_root.py +++ b/chia/util/default_root.py @@ -2,6 +2,8 @@ from pathlib import Path DEFAULT_ROOT_PATH = Path(os.path.expanduser(os.getenv("CHIA_ROOT", "~/.chia/mainnet"))).resolve() -STANDALONE_ROOT_PATH = Path(os.path.expanduser(os.getenv("CHIA_ROOT", "~/.chia/standalone_wallet"))).resolve() +STANDALONE_ROOT_PATH = Path( + os.path.expanduser(os.getenv("CHIA_STANDALONE_WALLET_ROOT", "~/.chia/standalone_wallet")) +).resolve() DEFAULT_KEYS_ROOT_PATH = Path(os.path.expanduser(os.getenv("CHIA_KEYS_ROOT", "~/.chia_keys"))).resolve() diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index adaf010183c9..0c9cefd18e7f 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -213,6 +213,7 @@ async def _start( standalone_path = path_from_root(STANDALONE_ROOT_PATH, f"{db_path_replaced.replace('v2', 'v1')}_new") if not path.exists(): if standalone_path.exists(): + self.log.info(f"Copying wallet db from {standalone_path} to {path}") path.write_bytes(standalone_path.read_bytes()) mkdir(path.parent) From 42ead0721198a51cc951f03edd2eff6d882056cd Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Mon, 7 Feb 2022 16:14:23 -0500 Subject: [PATCH 015/378] more gitignore (pytest-monitor, git, vim) (#10012) * more gitignore (pytest-monitor, git, vim) * / * tidy * .gitignore *.prof * also ignore *.pstats --- .gitignore | 236 ++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 215 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index 63946d75bdf8..683611dc87d2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,9 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class +# The last section of this file is generated. Please locate custom edits +# outside of that area and only update that section using the original site +# that generated it. Links provided below. + # C extensions -*.so **/*.o **/*.DS_Store @@ -30,21 +29,11 @@ chia-blockchain.tar.gz.tar.gz # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -#*.spec -build_scripts/*.dmg build_scripts/build - # Installer logs -pip-log.txt -pip-delete-this-directory.txt **/*.egg-info -# Unit test / coverage reports -.cache -.pytest_cache/ - # PoSpace plots **/*.dat **/*.dat.tmp @@ -53,14 +42,9 @@ pip-delete-this-directory.txt # pyenv .python-version -.eggs -.venv -venv +venv*/ activate -# mypy -.mypy_cache/ - # Editors .vscode .idea @@ -87,6 +71,10 @@ vdf_bench main.sym *.recompiled +# Profiling +*.prof +*.pstats + # Dev config react # chia-blockchain-gui/src/dev_config.js # React built app @@ -101,3 +89,209 @@ win_code_sign_cert.p12 # chia-blockchain wheel build folder build/ + +# pytest-monitor +# https://pytest-monitor.readthedocs.io/en/latest/operating.html?highlight=.pymon#storage +.pymon + + +# ===== ===== +# DO NOT EDIT BELOW - GENERATED +# ===== ===== +# +# If you want to modify below please use the site linked below to generate an update + +# Created by https://www.toptal.com/developers/gitignore/api/python,git,vim +# Edit at https://www.toptal.com/developers/gitignore?templates=python,git,vim + +### Git ### +# Created by git for backups. To disable backups in Git: +# $ git config --global mergetool.keepBackup false +*.orig + +# Created by git when using merge tools for conflicts +*.BACKUP.* +*.BASE.* +*.LOCAL.* +*.REMOTE.* +*_BACKUP_*.txt +*_BASE_*.txt +*_LOCAL_*.txt +*_REMOTE_*.txt + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintainted in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Vim ### +# Swap +[._]*.s[a-v][a-z] +!*.svg # comment out if you don't need vector files +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +*~ +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ + +# End of https://www.toptal.com/developers/gitignore/api/python,git,vim From b5f3122fc61adeda3b2307cffcdec1e838e4ff7e Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Mon, 7 Feb 2022 16:15:35 -0500 Subject: [PATCH 016/378] import ConsensusConstants from actual definition (#10131) --- chia/util/make_test_constants.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/chia/util/make_test_constants.py b/chia/util/make_test_constants.py index 5a05a289c819..659c75b06574 100644 --- a/chia/util/make_test_constants.py +++ b/chia/util/make_test_constants.py @@ -1,6 +1,7 @@ from typing import Dict -from chia.consensus.default_constants import DEFAULT_CONSTANTS, ConsensusConstants +from chia.consensus.constants import ConsensusConstants +from chia.consensus.default_constants import DEFAULT_CONSTANTS def make_test_constants(test_constants_overrides: Dict) -> ConsensusConstants: From d404d0e92b001b9b9b2568ddfe7ce5284331bf65 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Mon, 7 Feb 2022 16:16:13 -0500 Subject: [PATCH 017/378] just put __init__.py files where we have .py files (plus subdirectories) (#10129) * just put __init__.py files where we have .py files * and the rest * remove unused ignore --- benchmarks/__init__.py | 0 benchmarks/block_store.py | 4 +--- build_scripts/__init__.py | 0 build_scripts/npm_linux_deb/__init__.py | 0 build_scripts/npm_linux_rpm/__init__.py | 0 build_scripts/npm_macos/__init__.py | 0 build_scripts/npm_macos_m1/__init__.py | 0 build_scripts/npm_windows/__init__.py | 0 tests/build-init-files.py | 2 +- tools/__init__.py | 0 10 files changed, 2 insertions(+), 4 deletions(-) create mode 100644 benchmarks/__init__.py create mode 100644 build_scripts/__init__.py create mode 100644 build_scripts/npm_linux_deb/__init__.py create mode 100644 build_scripts/npm_linux_rpm/__init__.py create mode 100644 build_scripts/npm_macos/__init__.py create mode 100644 build_scripts/npm_macos_m1/__init__.py create mode 100644 build_scripts/npm_windows/__init__.py create mode 100644 tools/__init__.py diff --git a/benchmarks/__init__.py b/benchmarks/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/benchmarks/block_store.py b/benchmarks/block_store.py index 08f268fffaf0..a4c5a6f86545 100644 --- a/benchmarks/block_store.py +++ b/benchmarks/block_store.py @@ -29,9 +29,7 @@ def rand_class_group_element() -> ClassgroupElement: - # TODO: address hint errors and remove ignores - # error: Argument 1 to "ClassgroupElement" has incompatible type "bytes"; expected "bytes100" [arg-type] - return ClassgroupElement(rand_bytes(100)) # type: ignore[arg-type] + return ClassgroupElement(rand_bytes(100)) def rand_vdf() -> VDFInfo: diff --git a/build_scripts/__init__.py b/build_scripts/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/build_scripts/npm_linux_deb/__init__.py b/build_scripts/npm_linux_deb/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/build_scripts/npm_linux_rpm/__init__.py b/build_scripts/npm_linux_rpm/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/build_scripts/npm_macos/__init__.py b/build_scripts/npm_macos/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/build_scripts/npm_macos_m1/__init__.py b/build_scripts/npm_macos_m1/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/build_scripts/npm_windows/__init__.py b/build_scripts/npm_windows/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/build-init-files.py b/tests/build-init-files.py index da621fff1495..9a5db271b651 100755 --- a/tests/build-init-files.py +++ b/tests/build-init-files.py @@ -35,7 +35,7 @@ def command(verbose, root_str): stream_handler = logging.StreamHandler() logger.addHandler(stream_handler) - tree_roots = ["chia", "tests"] + tree_roots = ["benchmarks", "build_scripts", "chia", "tests", "tools"] failed = False root = pathlib.Path(root_str).resolve() directories = sorted( diff --git a/tools/__init__.py b/tools/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 From 8aeefdec0234c3e84c9ab714f956581b7cf85a98 Mon Sep 17 00:00:00 2001 From: Adam Kelly <338792+aqk@users.noreply.github.com> Date: Mon, 7 Feb 2022 16:03:56 -0800 Subject: [PATCH 018/378] Fix running blocks with generators that use back-references (#10102) * Fix running blocks with generators that use back-references * Update tools/run_block.py Co-authored-by: Thomas Pytleski Co-authored-by: Thomas Pytleski --- chia/types/generator_types.py | 2 +- tests/tools/442734.json | 140 ++++++++++++++++++++++++++++++++++ tests/tools/466212.json | 132 ++++++++++++++++++++++++++++++++ tests/tools/test_run_block.py | 11 +++ tools/run_block.py | 11 +-- 5 files changed, 290 insertions(+), 6 deletions(-) create mode 100644 tests/tools/442734.json create mode 100644 tests/tools/466212.json diff --git a/chia/types/generator_types.py b/chia/types/generator_types.py index a3fa98cef869..060f3d85d987 100644 --- a/chia/types/generator_types.py +++ b/chia/types/generator_types.py @@ -27,5 +27,5 @@ class BlockGenerator(Streamable): program: SerializedProgram generator_refs: List[SerializedProgram] - # the heights are only used when creating blocks, never when validating + # the heights are only used when creating new blocks, never when validating block_height_list: List[uint32] diff --git a/tests/tools/442734.json b/tests/tools/442734.json new file mode 100644 index 000000000000..04280a921458 --- /dev/null +++ b/tests/tools/442734.json @@ -0,0 +1,140 @@ +{ + "block": { + "challenge_chain_ip_proof": { + "normalized_to_identity": false, + "witness": "0x02008888e6ad9f207d79397fa5edf88fcdf9b8d7894cb5cd7f6d99ffaa809ffa0c835c367306f66becb3688fb8e1343314d6ba37d71f8a574e406433156fe22265230757ef9daf342f67212695cdc0ebe79ea875ba05eccdb0b82e7086b1c345e50b010000000000000fe9d4997bb8ecc6c9bc5c9341f38b0b03729c6cf6965ca505397d188e6b8281e0b78cd5030092ea62bc192f97930511324b6393ef811ff8baeb91f9c9176f17ef4fb1da3c4a6035f10b5ec8881987793a61a85a5ee4b6f75f59e7a44d2f17c7b239a4b6ea31df3b976f7c22a125ee50b9a5c1d60ab2e98ba91414e481cce7da441549c6de43010000000000002fbd7c8a396d4af95b4e6174341c87da8b5ea978363613da232067d9ad3e79ca052056c502006f6d034b6d4c8686954e634cabb9a3e9ed7487fb4e82bead8103b0c4582ce1f92edf3522bbe1dc59f161e2e6dc465c674fd77c249d0aca67cb8b40af4547db5615557220cf535e39e5cfd7dc39f576b9b0137d44d2aa8ea2fe2e256586fc4a450100", + "witness_type": 2 + }, + "challenge_chain_sp_proof": { + "normalized_to_identity": false, + "witness": "0x01008b018d28b9a08c14781108f821a1cf5c24609082b6ccdbdf45208f98717b012877e932ee029f082cd451d37acdc24efc9d3857eea0c3e77257435f94a048442d82691869f758007287bcfe11e7e6f140d252158b7630ffb7a1608e6cfbe8cf16010000000000000193e8f1a6c730d28c6b6026163b25e2050bdd6bb9673f35444bc8ef123c3645248de1590200c470cdd88c030a6e4d44fb9bddb71aba6ddcbc3d3f5e26f10f03ab2d1d5c4f8d72fa125a7a253e1ceede7ee7770993c54f45103da6494b0977d2ae041db2d0385f526ae8273b28e89e21db7a3720338dbd185a9d9ca55ebe28ce02b6ac1ddd670100000000000004bb54a34ac2c5243c5151e2c15bb051cbb0535766b2b0d2c9d7dae5df73cc0f4829a0d501008cb0fc7748cd04c6100be80afba4550ffc2ab04dda1039075280d3b03695b8d6bad745521c8e418dd307d1446f5098cf2affafc8f195241ce770ba703558cc0b6df15a48f4805db3db8b9c6bfe122b4527feb7675bf309f50f0992641f17a6050100", + "witness_type": 2 + }, + "finished_sub_slots": [], + "foliage": { + "foliage_block_data": { + "extension_data": "0x0000000000000000000000000000000000000000000000000000000003a2c7c9", + "farmer_reward_puzzle_hash": "0xef4efadd838306f2240e6f782387604d2b20e5e692aba561ea31fe8b888e70bf", + "pool_signature": null, + "pool_target": { + "max_height": 0, + "puzzle_hash": "0xa076a4cd8c39e4046f37c3df72c41b4589a737e54a0a8538c9e67b53739de992" + }, + "unfinished_reward_block_hash": "0xceccbbcf6fa82a1d31449ade29a1e90cb17b4a4e7676816b08af8138accf985d" + }, + "foliage_block_data_signature": "0xaa511c5b8da75be0421fd398b8b3ad0aa26e7e079255231ae1e36193f60f5dccb83346006da21b8ff6dfab534a2af4d50c6c4f6b9de33d616107c8d43021a7d18988fa59165539b24cf08c776dfe64197842b95ab1cb36e576f5245649969b95", + "foliage_transaction_block_hash": "0x7d41f45daff5734d4a603782eef4999e000fc389fcd803241cb81362704b20e2", + "foliage_transaction_block_signature": "0x8a340614a2dc13828789df562fe8e5f8312fbf6638a354f8c1aba1698fb7319aa0df72827c2d58ce84cb51b380c5b676096eb699129b3880327688775ad9c74aaf0b610912b35e69d7f69a069f649765907e54038efc7904cc84b4070bb851d9", + "prev_block_hash": "0x6931666ebd9e910953d489a8c1c94ad5b5839f7d49f790be100047ef586dd6c4", + "reward_block_hash": "0x33a63f38e479dd74c6f2fd693af2f0f7cd6be455e7e6f6d0ff6637aec5e5d4c2" + }, + "foliage_transaction_block": { + "additions_root": "0x1d5f66b1961c73c6c10e010ff6f69339df117e8f1b964024f1949c71f37df68f", + "filter_hash": "0xb057213daa2707fdbbe44f25c427ae5f3b07a62e1732283d6e2ced5571cfbd90", + "prev_transaction_block_hash": "0x6931666ebd9e910953d489a8c1c94ad5b5839f7d49f790be100047ef586dd6c4", + "removals_root": "0x1b180fcac1ef59d2d4546c436d34ebeff67c1f10a62b206c6025c951112f3676", + "timestamp": 1642467461, + "transactions_info_hash": "0xe87821f06cd1ae7502ab23dea6ebfbc6badbb463b9752883fd041565e4cc9a15" + }, + "infused_challenge_chain_ip_proof": { + "normalized_to_identity": false, + "witness": "0x03008f7986cb783c59e1dc8b97b1b772200091a736853c6c3440b80514f1d66a14645981224a9a76e2ec5c25dfec671051563b29f48c63692f97aae58d6b592690162024a1709404ec744b9267d91146e0296829c2d6d0446ebd66c7a153938c582d020100000000000fe9d4d7f275a463277877ee3ac3b00fae2eac6d11e1e27598f858a68ffbdb3dfced32f90300671854cc3d3e7639baa7d4853bc391c0ba839b8ab5016909750bf15e9a5594101f51f0b54ba52ebd8cfc92b1e3d09752fe1b30e7c7c5ac68b8784cc8944f700b70821a6d9e39432764e8943659117c956c7e72d861ce57c260d0d16fa58d810e020100000000002fbd7c8beb148671db7c02efa968ae3375d5407fc44fdc9d109a66ba345df99e649e47e7000091633cf1ea1db071b4f4fd0cf5ac1cc77aa822f54a4071cc62afb65ccb42809b054562ad59ef7bcf03854d1cef57f3692e0f9960becb2790797b054fbbdd0c60f9f468817f72fdf55734bb33f84298ca2e2b1b91efbd7d4a28b4f81b1bbc735e0100", + "witness_type": 2 + }, + "reward_chain_block": { + "challenge_chain_ip_vdf": { + "challenge": "0xa920fee64e77669d7ac6b6ed583cff2787908f07171a82b59ebdd76a74308fe1", + "number_of_iterations": 48956150, + "output": { + "data": "0x02006bf63e7319703dec363b01a8aac9474ac915118cb9f056c0e8b123d047413b9b2a8eb3876f8242d800b186e04a77063c1152ee54c3b917cfea4ad07a97eff6413ed5dd9a486d78c752fb4307bf7c0bc6d4f7c0a994beb7ac8ae4a24e193e07600100" + } + }, + "challenge_chain_sp_signature": "0xb5436ceb17c3cb8ca2f5bc283983023284a793997063bf5435e77f0440f61f4d98f6894f6349f76b81b92653dcc7aba20bd1c56486738ba7bf6c2d786478c4c58ac111c4c2adccc0734669ea676f767eefda47fe7b3d182925ba29f6c9bf6d86", + "challenge_chain_sp_vdf": { + "challenge": "0xa920fee64e77669d7ac6b6ed583cff2787908f07171a82b59ebdd76a74308fe1", + "number_of_iterations": 44728320, + "output": { + "data": "0x03002c44d8f632fd8aeb32d708eaaf65effae1359929b7273c675bd803988533100ad00f4c76f12c0e04f0dcc8c089d8017b6fe0fadf94bded36259f97574c84f74a9583d17463b7cb2266ed3ca890cb8c12dfe88076c466ff1ba65eaf98cb09291f0100" + } + }, + "height": 442734, + "infused_challenge_chain_ip_vdf": { + "challenge": "0x75db068f99ff3f119d88ef6537d7b64851f15d645290f9be566dd446194f28d2", + "number_of_iterations": 4693104, + "output": { + "data": "0x010013724cfc58c336c845e1cea7a47dca9bfb84822432b5e723640c7de0c50a0287987c3167196526af96c516e224e3f169dd9430a024c919bd401b96453a90f306acd977b09c9ead697ed535dad1c38d4e457bd61d6240f4841182e6805c199e040702" + } + }, + "is_transaction_block": true, + "pos_ss_cc_challenge_hash": "0xa920fee64e77669d7ac6b6ed583cff2787908f07171a82b59ebdd76a74308fe1", + "proof_of_space": { + "challenge": "0x7a500e1775196ca2c786ad0544d2787dd996febb2f69de53282b5d3fdbdecef7", + "plot_public_key": "0x95ae3758ea1718924faf2406e74a625d9a696e3c8d99e860df75bd4caca6e76da8bae7741c844d1c26e90f1823e9b9f9", + "pool_contract_puzzle_hash": "0xa076a4cd8c39e4046f37c3df72c41b4589a737e54a0a8538c9e67b53739de992", + "pool_public_key": null, + "proof": "0x72a71c001a9254629df6ba00f4bab26764fea0ab1863a2fc4b88b2a3dc60fb545f1e810696bf2bcc0b7ad4b1b3fc76079b37b69cb9d45fb7f7865f8790ebe661f2dd8afd5a4d9e2c7160f7968cd967952d9c9ce0a7d644da6fa85bd56c5aec90f8dab76f49918383217810175873ed684deb2d2985d3ea6976d97afec77aa2655bb643888aada3f74b33d64ff07d998f891f58b680f964c1bd47b8fbbb0be7e1f04744e742c40c3af6e7823f7adbb8e47d2dff698575c65263fdfbe2345d85900965a51fb44b38fd1d9f1738e1fb68ea5d573b115ad6da3e75d6303de16eb5e0c896d719f51392241be152d8f133ef2b3cbbfdd0a43a9d5d6a5ed55cd400474e", + "size": 32 + }, + "reward_chain_ip_vdf": { + "challenge": "0x0c40da45f0686c5c2a8ec49813885bdae42d39294bf9e55a170aee94ba25844a", + "number_of_iterations": 4693104, + "output": { + "data": "0x01002212488da7d38f224fc52a39da1987319a482660a21732899884b1be20b0ecfbbe477e26f5fcb62104031acef465c8a8439fd0ad93a92e4e13433f884655ec65075cc9b057f798b4ce35fdd49a43c141375bdbb7d028c75d3e5656359d5c97590100" + } + }, + "reward_chain_sp_signature": "0xaf1fe08031b0affda5f3a3d580a2e8b40cfb2d64e33eb6bbafd8dfcd0a084e64069012e424ba6f9b9440a285d3119b761103faac2e61e98873252f2ffc9f88f3eff45097675878312ba8956363204894e83f76f75df51328edaadf8d38d31447", + "reward_chain_sp_vdf": { + "challenge": "0x0c40da45f0686c5c2a8ec49813885bdae42d39294bf9e55a170aee94ba25844a", + "number_of_iterations": 465274, + "output": { + "data": "0x0000755daadad08c60a4ec70c1c67161a14ed59ae9e49c2f2cc7851b0f9e88e783b7584baea824b2a21afd1cc308136ea3500445ad08fb454f024590180e85fa8c0280b2ece22717eb9877383da0022f2f959cceeb94c3a2b1701dc2022e9ad6b4040b05" + } + }, + "signage_point_index": 42, + "total_iters": 1008312976118, + "weight": 14074564916 + }, + "reward_chain_ip_proof": { + "normalized_to_identity": false, + "witness": "0x0300eea948a9b896b7e2d57e4282931d54b170481739817ddb39f895a6b24a7df138b86679324fa07e03c52fcb5740ebb44068cc4329e77e6622e8ac884f34bd313979dc7fc01e7fc8e7882f705d7de362983eae5f52ebfbf2e4228954e7c4b4b843010000000000000fe9d4fd448b215d0306e4dd0dcbee862decd51dd90d5516969218461cb5dddf6a53ab6d0300d3559a0019567a96b76f56d66d604cc31c6cbf85173d4d386abe9e53d7d402a25a41921022ae59988398aaef77729b9a7e7de3b632ed9db8297128d2a281012c3d97da8272132cea12de2be630ca384fb118cfd260ffd975cdc876f211ad0b27020000000000002fbd7cb0408f2cd89d577fd42b153ea86452d8ab1c6b7594a13e1ea8b01563215f4954c50200bc31881b484119a0ef64ffc06d7f4b4a1523755fffba626ddc39dc07623b003314caae119977758899d4f7939ab11a88a936f222592da2b23e7f532ed2a9e81cf737aa856f316d45a0c750bc6656c5a498c0f1a4e1e2301de7afc30f9f95f31a0200", + "witness_type": 2 + }, + "reward_chain_sp_proof": { + "normalized_to_identity": false, + "witness": "0x0000fd0eebf818803a623168891d90cf0c00d22ff9cec06a0d96102d78fcee57c865fd06ac418a11cb5e987bb0da563d383f9b2790633cebd446890cc7e95bfc1c3b6d70f4c69a8f409ff9006882b061483c2aed2abafa65e24763d9079838c9fa4b010000000000000193e897c8f16151f73f72cb65b2d438267c2b693cb519b22e0260f185ff0fd3e2ba52d30200bf010410d38c32ef397cfc0599eaecccecc2010e5e8cd29e90d037e6136db59e79d7d3ad8f40c0b35416a17fc70f17e9685f897696d42c29c871afda70db71451dab01832c5d6c96ac636b13e3b371e5476cb627bddb7dcec779106e5761564f0100000000000004bb54868768f482f82e7b3d75e12afdb9fccf9d810f543bec4b3d85501b74da7cb629d5000032ec97c4e6134670a7fedf1a0cb736c6531e1c942b712345275bc0eb18a76f5ab118aa390a4c8d42b72376e1826006eb8ee543adc6ce872c8e0f74c5f5adbc0db94a8997df12fea072ff942e5ab232537140d263fd1b223fc8c96d368451c41f0302", + "witness_type": 2 + }, + "transactions_generator": "0xff01ffffffa0d496582b69863c633cb61cde7f7814cd5247cbdf580d5ce8d6fd4d67c9264b2cffff02ffff01ff02ffff01ff02ffff03ff0bffff01ff02ffff03ffff09ff05ffff1dff0bffff1effff0bff0bffff02ff06ffff04ff02ffff04ff17ff8080808080808080ffff01ff02ff17ff2f80ffff01ff088080ff0180ffff01ff04ffff04ff04ffff04ff05ffff04ffff02ff06ffff04ff02ffff04ff17ff80808080ff80808080ffff02ff17ff2f808080ff0180ffff04ffff01ff32ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff06ffff04ff02ffff04ff09ff80808080ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ffff04ffff01b0a30beed664fcc9fe249e9c7a1bc42ccd3dac60f5276e1f4ab8879b2fae10e0ba02008767c5408a0db17f5f6a5b4e1d1bff018080ff8600e8d4a51000ffff80ffff01ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f202780ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f202880ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f202980ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f202a80ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f202b80ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f202c80ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f202d80ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f202e80ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f202f80ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f203080ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f203180ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f203280ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f203380ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f203480ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f203580ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f203680ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f203780ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f203880ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f203980ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f203a80ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f203b80ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f203c80ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f203d80ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f203e80ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f203f80ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f204080ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f204180ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f204280ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f204380ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f204480ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f204580ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f204680ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f204780ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f204880ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f204980ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f204a80ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f204b80ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f204c80ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f204d80ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f204e80ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f204f80ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f205080ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f205180ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f205280ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f205380ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f205480ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f205580ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f205680ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f205780ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85023d1f205880ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85047a2ef95880ffff33ffa09545d73ebd8087cffeb3fdd2be4132e59c24a0a34b0be927e538dc3ba597ebcbff85746a52880180ffff34ff830f424080ffff3cffa00e51bebb19157582df23ee84a65e4ba5dccbabfa3481ef23f8bbbef27030977c8080ff8080808080", + "transactions_generator_ref_list": [], + "transactions_info": { + "aggregated_signature": "0x940d837ba5055a7593f01123c7dd858e76b346e4f2b8274b4c4d942ad93638b671ae2ebefe03fa612a4be48225b4efa206eae12a467992a0e51a44311cbdcaa03795dd7f39ab3fb15d157bc8814579b21b98bb69cf28662e1b3b5a92bb66a01f", + "cost": 128697450, + "fees": 1000000, + "generator_refs_root": "0x0101010101010101010101010101010101010101010101010101010101010101", + "generator_root": "0x406a2aa2eb37ea87e34f87aaa592b107af7b175741714a38ff0f04f856abb0bf", + "reward_claims_incorporated": [ + { + "amount": 1750000000000, + "parent_coin_info": "0xae83525ba8d1dd3f09b277de18ca3e430000000000000000000000000006c16d", + "puzzle_hash": "0x0ea986421d3bcc6d073038fe4c120483860376d8c1072d6be8b2c1d3d48d3812" + }, + { + "amount": 250000000000, + "parent_coin_info": "0xfc0af20d20c4b3e92ef2a48bd291ccb20000000000000000000000000006c16d", + "puzzle_hash": "0x0ea986421d3bcc6d073038fe4c120483860376d8c1072d6be8b2c1d3d48d3812" + }, + { + "amount": 1750000000000, + "parent_coin_info": "0xae83525ba8d1dd3f09b277de18ca3e430000000000000000000000000006c16c", + "puzzle_hash": "0x05377d7dfc11533d647af0ab733504f83f4371dae04d26da2d3bda459edcd9e1" + }, + { + "amount": 250000000000, + "parent_coin_info": "0xfc0af20d20c4b3e92ef2a48bd291ccb20000000000000000000000000006c16c", + "puzzle_hash": "0x05377d7dfc11533d647af0ab733504f83f4371dae04d26da2d3bda459edcd9e1" + } + ] + } + }, + "success": true +} diff --git a/tests/tools/466212.json b/tests/tools/466212.json new file mode 100644 index 000000000000..13772006fb6d --- /dev/null +++ b/tests/tools/466212.json @@ -0,0 +1,132 @@ +{ + "block": { + "challenge_chain_ip_proof": { + "normalized_to_identity": false, + "witness": "0x010088f69d58293f33d57eda9a1d92b3313de8a93e4a442c751ac4139043a006a476f73053b8dd45f029e3f7c8599b044dab16d1f182e5ad68524ab3a2e11ddba0045573caf6dbf9f5da6f98286b0cbbbc568622b6fd7b8a0beda29cec7738f430020e0c0000000000039788e25080addfda674497bf1ca319dccec62f9c6a0d72208cf8aa812ca28e21ea3ae902004a62942e8d4fec07c18566b0f45cd0156af4ac95e9e937884a918c752d5e606f2eed2f95a2ac0a63d96a3a93f8edc528769148ada0625d369959238eb2c7d03b0966c3f74f699eb55fb144f54c7f539af5193bd3f6b67a5a2a97aace94c4c474010000000000000ac5d095ae7863aed94b846486bce92bfe6981ab56989c68823ab758cee03e3927c245310100b54d79bb823b6cb2040935e41fedc688b7368dcb795d7c2238c0afc52d91baf0f59bc37df6e3964dac824de16e269a849173a686ab087cd42b5debdb6465457dc4f99491a597664fdb45503e6d4d8c57c9cc6b456b0ec2a7784aebd7716f2a200100", + "witness_type": 2 + }, + "challenge_chain_sp_proof": { + "normalized_to_identity": false, + "witness": "0x00004feb84373558be58fb24985cf9a2cb04e234a4fb43bccff6ba83f1287d418f2fb057de3302e6919e307b03c521e0d4c22231240893cdcbe106ab28249666fb15dd933118c1d290a48523f23507b1de54c050b317e4fe50575d373092d28f5924020000000000000253509970e638d27a60f8ded1463f2d9746b0cf0b77b95a23c4efcbcecb1f8ea59aa7d50100c388774cfb8decb8c22f01c194bea34d57b586434f0a5fa42bbce28edb75c92836fd816797770a4c8f096743a6b4867dac1b6f07fd70bec3c8ff6f63095a0c086a0cdcfb80eec6124d02efdaa3dae622df9dda8acca7440d6df4d377432688050200000000000006f98cf7c53b118badfdb8c19270ba717a1ef3e2f3852c75ac9995297e8d166f7e481db30300e17280a2adfb7e084924bf63fef342c7edfeb6ac8b5b7cb52e23ca5b05d5991b7cc1e2a485bd61ca69637a9717f85511d11efc8ba34ccfe3397876dc1bb20a2fcaaf34eccb6ec0fc07ac571e9713dd17353b42c193955af708b69d824ab8de210100", + "witness_type": 2 + }, + "finished_sub_slots": [], + "foliage": { + "foliage_block_data": { + "extension_data": "0x0000000000000000000000000000000000000000000000000000000003a2c7c9", + "farmer_reward_puzzle_hash": "0xef4efadd838306f2240e6f782387604d2b20e5e692aba561ea31fe8b888e70bf", + "pool_signature": null, + "pool_target": { + "max_height": 0, + "puzzle_hash": "0xa076a4cd8c39e4046f37c3df72c41b4589a737e54a0a8538c9e67b53739de992" + }, + "unfinished_reward_block_hash": "0x3b8215960b2ec60b1b0d9d7b421a7eb5fa3e805ed0c009d447064fc96be08ab3" + }, + "foliage_block_data_signature": "0xb917339eea8846b078931ae119915a253930c39f9c6873a1016e291564aaba613973cd38ffa0eeb1a934c3be948f31e90cd96b51c63ea74d8dd0dae5f8d19d27cf6d1a72b347efa1d4fc0255dc866f7f83619a8227edf2ee0afe74af29715b88", + "foliage_transaction_block_hash": "0x0f6a2abcd9280eb6ff29671bee79c6d6022af54d71b0e852c465149cdccfe557", + "foliage_transaction_block_signature": "0x84936604278736a6b5b429e5ef3ea04d38d5c3673c619596b6ae9a1f63bc26e6f44e4acba3096ef921f5ea9cf693769206de3d30f37b680f70073a3b80f94e7be80a0783f7c66afd8113073bf05dac4403341248777901df82cd9746c070f411", + "prev_block_hash": "0x3a450be440c720e0751bdb815d5f280cee5c95da55dd543b86c3a6d50e2b2346", + "reward_block_hash": "0x849b59574d01ac38d7ce438c59852bd6d75b34a22c22c17a72621af9d0e6f31e" + }, + "foliage_transaction_block": { + "additions_root": "0x6f890f9efd53becee2f6323282adb98a9a3ad66d969369bec2d5df0ebd6bfb52", + "filter_hash": "0x3f7133a56bf900d38e5700bc4d514e9ca9f6d0c79cf186d337346ef2235afe0f", + "prev_transaction_block_hash": "0x0cec1c41bede66305f48fc5a717375b00a684cd7905492e7220663bfd424cb9d", + "removals_root": "0x476898e79f5ed9d578b692dca003298a8c0d34f81c6dea28c7f757ddf87c00ae", + "timestamp": 1642906850, + "transactions_info_hash": "0xf305a83ed870e7bb83b4a4dff13c616ee51f1a8b48536d7ee9d8ee1515872dca" + }, + "infused_challenge_chain_ip_proof": { + "normalized_to_identity": false, + "witness": "0x010048283dc4bf9015c9101b99dab1e4ef8783634a6b29332775ed451225907df36a55e3ba54c6e56cf3bcaa1acd650dccf26d3551962c84d869380226c813ff187ceb54b7d007040170c2673491637ec1cfd036f1f86c3014bd83c6ecd9a7aaf87b01000000000000039788dc29c639e634b49f3afbb92df82058551c46bcffcc9abae6ef271ed12e4aca3e1f0300598176e56ab6b4878654cd894db78d48e01f52dbd9d90405d24416fa90c518fff91f973823c013f1974dec8c0bec4afe11cff52fdad85fe5c9c2199e3c3ada41ae5dbfa1791fce435bde5bb15cca7b8ec8686a4c3bad0ce50ff331f93d18135f010000000000000ac5d09240b9e151ec9c18efb7c1043d7a1f29c77fee5c1c80a59611bc9932ca7c49719b0100bc34971183ce61bf20c2fdb8e860ab8d6c52ea9fdfa742cc2940b5e8a8ed8f450e015a2d41a06afd98bfd0e1fc583334af6a0c4053a27980c8c3228c372d2d6131b83e4cb32d0de54ff2d566cb7d47819db484d772b5fe4175c95e18d3dc95360100", + "witness_type": 2 + }, + "reward_chain_block": { + "challenge_chain_ip_vdf": { + "challenge": "0xb31db47aeecd46a4298cb16d409be185c9b506e016c39e7fa785d1ccddfc80c1", + "number_of_iterations": 34738123, + "output": { + "data": "0x02005b646940ee1959785e8ccff3c6c3971b6f0ec5bb46c4ae9e9b39412e11dfd4f414fe6665b51ed6860d2df569a56d694d77c51c566932c89a45e251bb838ba7254f7f0bf14a81f228154bc76c390e6061c0bb07da67608c30f8b0d24e5029784c0100" + } + }, + "challenge_chain_sp_signature": "0xa4c12c6a5fdd6585f7c6115f78320b060fe470ff9ce2f080e8c05d606ee22ef9cc8b7093d2be74fe7eb0201c106367650dde9a1f81b8a5d81b92f4b0d613a9adebf47e5d645b281962d2b3af89cc928fd993bd7b6166fbb6ee9305a357e8fe7a", + "challenge_chain_sp_vdf": { + "challenge": "0xb31db47aeecd46a4298cb16d409be185c9b506e016c39e7fa785d1ccddfc80c1", + "number_of_iterations": 30883840, + "output": { + "data": "0x0000eec9ae678b0ff839edf7eb0021a9a1def50a9fa64911be69e6c80f2eda673fda43cff6bb03aec375ee70a0659f5fffa5e28b88f406088d7a11b28e631726a965774f42099db93f00419ae3397d5c3f8dbb3805e1f202ca542b8e734427621e210100" + } + }, + "height": 466212, + "infused_challenge_chain_ip_vdf": { + "challenge": "0x4572bbb71a934ff79a302b19ea589938e01b696275a938e9dadbd4c9d8760274", + "number_of_iterations": 1059149, + "output": { + "data": "0x0300a1243f44c7df3445d98247e4b2dfce838441ab7d39200229e076907ad6aed73a90cbf46b2246a97507f392723ecc54834a964db11a59d35af65c239add26452543d0d9093d0457255dd598ff7b9cc992a5e2d24d1a6f958fa99d7de16dda9c2d0100" + } + }, + "is_transaction_block": true, + "pos_ss_cc_challenge_hash": "0xb31db47aeecd46a4298cb16d409be185c9b506e016c39e7fa785d1ccddfc80c1", + "proof_of_space": { + "challenge": "0x3e9f9acc34a3c8a34fd5b1511de32eafea879ea01afaa2db2bc5bb865d62e57a", + "plot_public_key": "0x99e464c82213e385bcb0a226b4d50ef0b211227354361c532b293ad2272127cf63753ea741f116d6ad3e5736060064cf", + "pool_contract_puzzle_hash": "0xa076a4cd8c39e4046f37c3df72c41b4589a737e54a0a8538c9e67b53739de992", + "pool_public_key": null, + "proof": "0x20d8c9790ed987d1debf577c3bef7f03326d2323d162f0d475636da87eef8062767c11becdd233850181d3ef2f08a0143d7bff51c536b8282d10d6141f6515a321d69fde215e0a04e51eb3640d5c22a8ed2a71f90c96ea2c0f65d3b5e22a833b861efbbbb102d8195dcbdf458c2b35dae7da82dc85c00c51464e2c2d7ba638a71cf02aac8612e99a10b9cd3175b5f7aeeb5f9829f068b70c9eb094b9ca04d204ae688c718fc3c5187f62626bbdd4ef7c01a047e477e057ea2ae03d06a906d826ee228abf1c8594665580b1835bff60f826dd69faa5cd41dc5014f21049a82bb560f053ab044092905a5d725ef39d6c5b2e9fdba5413eea4355d7d658e62bbc98", + "size": 32 + }, + "reward_chain_ip_vdf": { + "challenge": "0x37629aa97993ff3ac0414aaf01178e1b0426aec9f6d87500e2f38c18c1545840", + "number_of_iterations": 1059149, + "output": { + "data": "0x010000a1bca4fa4b36526047983db0041378f475f5ef55d4b9e1d624b4e1fb437d5def8851a97ec15924c8a73f523677be08c8c20a458fbbeafe6fca615d10216305f91e0843e99782184932091c84efdca4947866e6ae4139980de871cb9949df020603" + } + }, + "reward_chain_sp_signature": "0x8899ef405157ab741cd48844e03ab1ea8af940cbee4125547d62a0378fac335bc4edd323785ccaa6d2170db0658513e801db23a6c7c4d6c36bab0be8ba5c317cf980bfbded637d14373db268e12ea2fe8e8225245c1c2115a6e851dc17c16e9b", + "reward_chain_sp_vdf": { + "challenge": "0xe92c22c92a4cd76aafc3b1216306ee2a6a5caa0fec2c706ae0623885e4a7326f", + "number_of_iterations": 685767, + "output": { + "data": "0x01005da53052a9c220df720e62975b027c4bebb7f154da959aefe0281356705bed3c4826cc7c8bce7f03680cecf111d839850b895704bb868ab87f17af62906d495f0bf8381a5a2f5254734dad6552282c9508888e34d65d213f5ee4533d612f436f0100" + } + }, + "signage_point_index": 29, + "total_iters": 1058790838219, + "weight": 16346233652 + }, + "reward_chain_ip_proof": { + "normalized_to_identity": false, + "witness": "0x0000735830ac431d1e64fa587f8931939c8e284f7d6e009a0e816a09c148ae42a00f8a773420e8793f0168f771c59ac5a7b2aae81729830eca1a25c4fce1d6888160db55e967a9df050e789cbd0c1dde7288f1aa0d8785b66d81fbfff30724b27f4001000000000000039788d8292b0b34295437d4c9306670a99eeb241a3d1aa4fda45ed518b64d0848680e4f0100a45b1635b49f405bef13edaa36fa24414ec1a61cbd253e49da3420e3c499739ab1ed4ed769af3b0c4c98183dc49bf4b20b6b8e89c30e57944de282161b81cc134b8a2b7059c73bf7fc570decefb5b521c099fbd90b7e35150cd471cb3bd13b1b020000000000000ac5d0e88da807573e46d1864fe7184b5eeac3ddf937a91bd1d98716d3be145e4ecf2a7d0000a19f785edf4be662c3b7a396e327e92bf8346a6d3f684f7fef716823938b6d4a45c49ca368f679605ae530a6c70ecbe4c0ec61b743713265681c72308baf2b4e9580c7af8a13b3f2b018202df0f57e764b8d49e697a6fb8f0b116e3958e1dd4d0100", + "witness_type": 2 + }, + "reward_chain_sp_proof": { + "normalized_to_identity": false, + "witness": "0x01007e642fe1be97372c10e94678938dab513820bba2ab0dde0953372118a08b347412e8811f0a904cd8b54730036ecb8704ae4a0e76cfaf41c2030cb90bd8efc14feb6e16990614095e49cd9b032955c1ab84643c4cac417b51c2e6ff5554d7855001000000000000025350c0fc43999b2942f36e7d8eed1322f2f798a5532215caa1fdc0e00e082a82d611750100cc74287f3f54428500944346d17a03fa040e7df44568357cbf0f9e708547dc442718856823a4ec0345c290861dd16224d0ac37e868941ab356c528115eac8f2a1bdc54139d5e05447838c2e41a09c71bdf26ee14bedb78a4c1213851256a34370100000000000006f98c9f4dcfc42d86398179ec482b4cbb158dadbfc94ba28357b4d26ae05827f718a171000045275f9b99f2208fd5aea222c1d2f820200d7c217d6e8def16b1433d1c984fb95c4428f7cdb8f4ae94c209ca0dd9c1e366fac9d862cc84607f70400547a54328d3b4d8e49cffc5c33265b42eafbeafe4a848a8b547fe0db460b416a010b1c2430100", + "witness_type": 2 + }, + "transactions_generator": "0xff02ffff01ff02ffff01ff04ffff02ff02ffff04ff02ffff04ff05ffff04ff0bffff04ff5fffff04ff81bfffff04ffff0cff82027fff17ff2f80ff8080808080808080ff8080ffff04ffff01ff02ffff03ff17ffff01ff04ffff02ff0bffff04ff2fffff04ff05ffff04ff5fffff04ff27ff808080808080ffff02ff02ffff04ff02ffff04ff05ffff04ff0bffff04ff37ffff04ff2fffff04ff5fff808080808080808080ff8080ff0180ff018080ffff04ffff01ff02ff02ffff04ffff0eff05ff0bff1780ff808080ffff04ffff01ff04ff47ffff04ffff02ff05ffff04ff02ffff04ff0bffff04ff8197ffff01ff84ff0180808080808080ffff04ff81a7ff81d7808080ffff04ffff0127ffff04ffff01820115ffff04ffff01ffffffa08878861c20e8036735b8c667a840197a9606a1d68dc9e7eabc678f3d5d071cc2ff85008f47c80a80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40480ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40680ffff3cffa0785adf9cb597be1356d9f77a6ee98058243862c339bae3f4c77ff52eb76eb2f28080ff80808080ffffffa0c5e4695847cfe602eda89abdab27fb0bba2b66733acf471c238ed7e5aac6ef1fff8423d1f20480ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20480ffff3cffa06ad2223efb50ac20c450c8220852c01a7b9b4c2332125dcd516297b9e7b928228080ff80808080ffffffa05d1caca691c232927c6612df890fa79b476935539f4951b616302caa062d7269ff8400b0585e80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585e80ffff3cffa0dcf60f0202ef5ab585f8b57503c20c87acfeb05f5df5aa9d4cda0ee680d6ea878080ff80808080ffffffa0304322cc7b535a72f08d2d1055f6ee554e6a329ff528802946de6b72d548adc3ff8447a3e40b80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20680ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20580ffff3cffa09f510ab6b6cbfb4e2164443cea96176008acc1bcaa443c6238053b38f84ab2688080ff80808080ffffffa0461405660baa199c6208805f749a7ba493ab4a111d03b47ffc8340465b6c328bff85008f47c81580ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40b80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40a80ffff3cffa0f9869c321516f79cf359992cc4b98f4ebda7b48ac4a778d46a8db25d58c0226f8080ff80808080ffffffa0a6eae07f57a427d0b1fa42d5999a30309cf4e0128a5b5421d1117da27367e178ff85008f47c80e80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40680ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40880ffff3cffa072e0dd6085c7280de9d80ead2ed855f7953032c5a94278850384b32d30c66dd18080ff80808080ffffffa00255aee3ff3d1f91dce9407936aab66a9bad6206c810091a4f13c0e04b2c03efff8400b0585880ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585880ffff3cffa042674916c41e603a4d54876d3202d6b51c3db3a03e8dc38942cfc5d77e976a398080ff80808080ffffffa0f26b737d7ef8252bf3e69caa10a2aee77c6bfb64e74bfb34cd23ad28b3cd4418ff840160b0bc80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff840160b0bc80ffff3cffa0f7c827ac0b567a39245990ac4559ff3b768ec2a6fa05e2ed5ac96a13045229948080ff80808080ffffffa073414843519eebccaa6a903d0db13fdece1e2c446f615b356fe0ad2949e27cefff8402c1616680ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b280ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b480ffff3cffa01a5002d89030bd3fa07197a8375cdf8ad37e0dccd027defc7a8478b53c9a1bd98080ff80808080ffffffa0517b3396ef31e8a8fc2fb3dd0ca5592eeb6dc2f4b3745e4f4cb7f3439dc7f425ff840160b0be80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585e80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586080ffff3cffa0583c293d867a724de27dd9d1ae7e740b8790742369edb03c8c08becda8880f6a8080ff80808080ffffffa0396a31474df2fe6b99262854c86414cc83ffeea728b3eb3fa4ec9b4c3ede9030ff8400b0585c80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585c80ffff3cffa0cb3921fb559002838f3bc84c545403f73f9cbd48ff9f427b67b8615aa77dd12d8080ff80808080ffffffa06bc60f7a3ac7ed38145122899e59fd5403e4fd8ed9258fe8e37a5f352d113655ff840160b0b680ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585a80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585c80ffff3cffa0c8cef403cf2ce971648796e5f71301c330a814a8918c12922ff7490b2f9361518080ff80808080ffffffa06405d18ebe8767bdf04d342176815250e3f9bad8c3bdf04b04bdfc4d63305570ff8402c1616780ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b480ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b380ffff3cffa0525d0ced2d0ee0be1651061ae9e93de25ffacbf95ceb7dea65b72da85ce4bf338080ff80808080ffffffa0c1c4c69b300dd9f5fbe35f3db56cf19a563dc2fa15055a5e0e0ece0fdaf8db62ff8423d1f20580ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20580ffff3cffa0b197c94aac16fd87f9542c52229af95f9cf412b863283554a45328197b8aaeea8080ff80808080ffffffa0122328830b9de5560c2202b7570df4eedc702e50ccfac6c206d3018e894dc914ff840160b0c180ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586180ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586080ffff3cffa076571a17713e1cd3a0d6f6ea178b9fc59a2a51ac7f826b3b713cee66271bda798080ff80808080ffffffa0f0a44d8ecd481ea15321dd50554236fe6b73c56f8985f62ce2084d2bd99c5695ff8400b0585a80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585a80ffff3cffa01e4adeac5f401cea7071e233a0676b4780bfb810dc386fe9908392504f1b69288080ff80808080ffffffa032863b1bf312f5a70b8a6ff999bb18579353a372dbc12640547e7eba4df1cb66ff8423d1f20380ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20380ffff3cffa0338919f9bb1e2e0a24a7d84bf51f1c29e10f13fafedf91515d1c6a1272131b9b8080ff80808080ffffffa0840ac9610926c54235a798a15bb80e5cc56d794bd6a023024248f6e4f1f24cb1ff8400b0586080ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0586080ffff3cffa0142ce793e8b4a737ac35ec5bf3826310f32445e0114dc33a19fecb9ec32ef5d28080ff80808080ffffffa06b77823f048ac18fe859b499b81d1f88279738b55131eef383c7ab223442dd4aff85008f47c81080ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40780ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40980ffff3cffa09f6bd086bfbfefeb616d37404a3402af9d75eb03c5cef4fa67e9e470838162c58080ff80808080ffffffa01e1e3dd49192585886710bb3545174616cd0a5bb37d539285f5f66deee549d95ff840160b0b580ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585b80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585a80ffff3cffa09742401b3f3a37bff8963137b03c224b75d9c4162c0b39ef77f8add3868b8f808080ff80808080ffffffa08b2f2e0e3b67585aa04cf90d285191f29d974eb7b4146dd853104c6d6acbb6bdff8400b0585f80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585f80ffff3cffa0f0ca71a097c1e3ca4e50d73882ea57034eab98681ef4aea04b989a73d65119bf8080ff80808080ffffffa08ea43de6c2bc4cd361f7c1ca565a1ec223612f8b0680c2f2fa8eac945a062531ff8447a3e40680ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20280ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20480ffff3cffa074198d84154c5f3d290131e0b520a9eaf4229a299ff54db2e1d5fd9093856f2f8080ff80808080ffffffa01f855489215b3096b7dde047ba33fd0044397455f4c3e0f5fd579853eee8564fff840160b0b880ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585b80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585d80ffff3cffa0912d2c5a5cd2730e1e80a36612fe4a5a0bcffba457ea5965a263055b16782ec18080ff80808080ffffffa0af457caa43f98a0698f546a8cab8488148e9bfc890367a26e92ea6802fe14468ff85008f47c80c80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40580ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40780ffff3cffa08cb0c968c79f03821c9858a39c647c0e8ffbe033dc1ae074ef9f705266cb0c088080ff80808080ffffffa08eedb83b2727fb46a7419d6d66da328732fde7f8973c4fa67f9bc52ef4262ba6ff840160b0c780ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586480ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586380ffff3cffa044a1af123d98e52f77817050969b0fe84ea2b80eca37488f20865d3a633adb2e8080ff80808080ffffffa0e6972021c0de4a7a2633ea347c09cd8455ad4bf38e403e4b8c16daa1788bf153ff8423d1f20480ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20480ffff3cffa0957532146bdccaa5ba355e75744a354431012422ea48a4ee186f3e1b0c3524928080ff80808080ffffffa052cbaf088145f1a3f50fb2e1a4b69b0a92c36927ccd700811b94b0757c2b92ddff8400b0585a80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585a80ffff3cffa089fa112c3f143deeb88649ed171381b6c63323d8600b3d28eb90cc570a0020408080ff80808080ffffffa063005c31ef4de5e50235297e4f17c80c412a060d6ce1c0113ccd0f55b584487cff85008f47c80f80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40880ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40780ffff3cffa0b93f00b218fdcf3f1e858822166a9d0f2ca36ee71e9acc34cc0da534c912e0fb8080ff80808080ffffffa0234b917751a67e3d19b0f646e154b097dc18da35db86dde730f2f17dddccd817ff8402c1616e80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b680ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b880ffff3cffa0e32dd795e6ffb2b5de6e6decfa026dce8e340eafea13fd397c87918ad3ea318a8080ff80808080ffffffa0234b917751a67e3d19b0f646e154b097dc18da35db86dde730f2f17dddccd817ff8402c1616f80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b880ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b780ffff3cffa05e68c58569ec096497c18f46fe87e327d7bf3b64341cca4a11af8de77c837f838080ff80808080ffffffa0aba8f2399dc79dba33404efd4331ae4c2b9739d7337f2704eb9dfe354e48fadbff840160b0b780ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585c80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585b80ffff3cffa089f898b4496440a8ffea96696b3dfce31fa1d614a93c577c36b05f84dc4003778080ff80808080ffffffa0a01dc2e6b59ceb221acaf0ce1f719acdbe8a1a34f9e1e7d35f03c51ca096fbd8ff850e8d4a50ff80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff850746a5288080ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff850746a5287f80ffff3cffa0323166933c8ee86b324bc6d3e8ae59d564b4cc112ae1cc43620e0c6b710d1a278080ff80808080ffffffa0b568d17af1fdd26f82196a8bd2a94e3d46a1060f524600d94f8f1949e6045eedff8402c1616c80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b580ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b780ffff3cffa0f88bddf200f1b9e723e1e4876ea075bb4605b30501a20da250e6800dd2ba22ca8080ff80808080ffffffa005846c12ff1d617e109a7fccba49e92cc2b924e4e21d38b3840df5458c54d948ff8400b0585c80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585c80ffff3cffa0512bc6ba46e39b56d9741fb5537fd6233477d191a9236decf11559308c7e6bdc8080ff80808080ffffffa05da6ed28b88426f9e50f42000f02803ca554694383cf41bdd72da15599832b1dff8400b0585c80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585c80ffff3cffa01c4afe55c00f8d571caac4353f9b3b146c16d72fe876289b5530ba3a5b66bd868080ff80808080ffffffa0a78255b747f8a8291f365b6167e18eeee3358f518779b1e90c9440f266a53e27ff8402c14ea780ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160a75480ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160a75380ffff3cffa0f15dd51b0b41b819bc614a2ab600f6da70d446678d09ace932cb88b3495520a78080ff80808080ffffffa0b30aea52d8c2225197314756fb1695af77b9f44c2bff5af79dd12bc01b7edacfff8400b0585980ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585980ffff3cffa0be35f4a5bda4a6a5b47967342f1cfc89af1f2f77c55d048aa15152377b3c52ab8080ff80808080ffffffa0ced2354b8b5d29db7eb970cb5583848f1792a64d7b504d453d4830b011184656ff83582c2d80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff83582c2d80ffff3cffa02a02785ae916f8005a68723a407d0ccb45e005f75b7b267001a43ce09051145a8080ff80808080ffffffa08df94e8d085e74a811f2f9e7331bceb4c4dedc9ff71ff7de0ac6de13a187902fff8402c1617680ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0ba80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0bc80ffff3cffa0741deae38c858f02f8c4e7da6c41dcd331ddb2a58df83d42b8ea900c3cebaa488080ff80808080ffffffa06ad649065d96a983d5fe0d73e537ff0de4c79b45ff269cdac622d0ade7f2ee14ff840160b0c280ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586080ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586280ffff3cffa086a156483af17a976ccf795c66c052e36f2550093081bbb05ad1a7c2dd79ba058080ff80808080ffffffa07001d37dc41e38204a526613b02e85a3ef2440142d40acca00c6135954e4747eff8447a3e40b80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20680ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20580ffff3cffa0814f421409b8fd862a811a6b305c14352a0b8c24c35abc4e69cf63d49ebd5f688080ff80808080ffffffa080cfe0b9c4495befe31e3e344c3cde5747c5b98bca677dc80b8a99280203d28dff83582c3080ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff83582c3080ffff3cffa08e8b2125cb3c17fa7855f5934195a001303da79480106cf4c1ba098de1d3f9bb8080ff80808080ffffffa01c759e45ec66fdcd1c4177ffaad06e4958667f514d21740f3ab8cc8a64774c7aff8400b0585f80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585f80ffff3cffa02224f74f1cb51b845901073650eadc9063f5ec80c2f3c9d00eadc09fb593c88b8080ff80808080ffffffa098993f57159e75c9851ff1af169a38aab13c09411172cd2e6bee56749f094b9fff8447a3e40c80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20580ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20780ffff3cffa05b77369e332e3d864759264d575387f46ecab07e5bdb454e09d9aef2e60bd9c98080ff80808080ffffffa05589f2207f11788b915380865814ef11a85bff08fe603d27db2e19ebd0b1fd69ff8447a3e40980ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20580ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20480ffff3cffa0dbb3159014ede8af10b0b9d539be461dea65ad97993b00e2581be8f123402e8e8080ff80808080ffffffa0c928cb387e4f0a69ef8aa2aa73841a90dbc55fd90fb985c7298de22dd30f2428ff8447a3e40f80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20880ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20780ffff3cffa08b61f79424cf7599a8623165381636c8780f4123916a43c827fad51eb9d110bc8080ff80808080ffffffa05fd78f4d722caab57288a6cd1832e4d47e014fcf91e4dc2d201b47cda25b4d96ff8400b0585d80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585d80ffff3cffa0f7850177b64b62e065692eb74f4dbfd9b66cf252109dbb1a3ae51d85436e1d978080ff80808080ffffffa08d5cb65566168f7e4364e83da3b71862a192c4b7075c542f1a99f86ee3482abcff8400b0585a80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585a80ffff3cffa0858a1f05b14b3883b47084a6d5b9ce554e41e49bf8068ec0aa6c2c59e05a37698080ff80808080ffffffa0f9696f37b02165cbda3679cb90c6cab2e288327bbe0303f866f0d6556119e5d9ff85008f47c80d80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40780ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40680ffff3cffa0e720bce229df15673c32067c0462fc13d78a89ae57e2ae19e47fc7088836af7c8080ff80808080ffffffa07583422d0fae81ec2b74bb7c7b4f4a66b7aae8828706610e359e935628539d2aff8447a3e40e80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20680ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20880ffff3cffa0f12f42f4373fbf985cc2ca81fe1725c50a769d0a963b5ea8e4b6facca68944668080ff80808080ffffffa04592956ac3730d64face621e260905f6a1184a12dfbd8b7e7f75ccd8495c792dff83582c2d80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff83582c2d80ffff3cffa07fa01aa5614a661a7011825538a492b62ca2944366165090f41cbe8b0bfb8dad8080ff80808080ffffffa0a907989be1e136e9bdb13f446505ae04e387c92b033a94872ab196c4b58f3c17ff8402c1617380ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0ba80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b980ffff3cffa06dcb947795317ad408601c861ad29cd4137e0641c9edfa344df9f7c6f6d15b868080ff80808080ffffffa03e24822728e6cb265ba5cc904a5e8a09acbd720b405e12bc4c7740d56541922cff8400b0585c80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585c80ffff3cffa093127d9b10b356eb5b6d4f850407bd7af9420500ce578ea966e60f68924480ab8080ff80808080ffffffa021098b118686a33ad764a6ea3def66fd119386544874a31a622df49f07ecd874ff8447a3e40a80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20480ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20680ffff3cffa08a005a1a309a354a34159e52782ae0bbdf0e906907dcb5593f925d717c1020928080ff80808080ffffffa0ba143ce8f167b4dc3f6d1c0868c1387cb9ca25cc3633b03ce3a2e6cb8730893dff84015ec7d480ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff84015ec7d480ffff3cffa0968b0d141ad89247234d88250d1981b7618c5f4642c7760de87e67ad0353d0878080ff80808080ffffffa08adc77779cd51c235b48b8656a2db847e6c7dfb3ad34ce942a64311dc79884c0ff83582c2b80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff83582c2b80ffff3cffa0adfef8c4085bf4e2a797d29237c88121fcaeba6de1a06e44b0240a02faa2ebae8080ff80808080ffffffa020e59e866a31d9039ba5f4ec6b20aad6a127e5554d72ae2e2c30193a383dd74cff8447a3e40f80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20880ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20780ffff3cffa0ad954e8e43deb3a468b0e8d8db867195e88eb69486206feeb6fd9739c87f0abb8080ff80808080ffffffa0e6b873a87aac0333acee94b0222fa6cb1d992110cf5527a1578ef375136be40dff85008f47c80f80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40880ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40780ffff3cffa08146ad91ca7c949dda4bc6f7ce20b4b7bb263b63f21edbfd9852a0fd6fa43b2f8080ff80808080ffffffa07b0edf7f6bb489ec2b40657753e7fc5ea954a7c5a9a40c3f3009f5696c36b214ff840160b0c480ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586180ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586380ffff3cffa0492050d937c5d602cbc9dcb261a5824e266b71e5ddee6a85992b2469c58832ae8080ff80808080ffffffa0a7cfdcaa046bdb370c91eb2e08f5d4ab298b1822bad738e73aeae0260a0c7350ff840160b0c480ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586180ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586380ffff3cffa0e33b3b52cd67d77b68d6f0101767370373adaeb34e13dcaa6460b3a6881e390c8080ff80808080ffffffa0616d3ae1b99f54e2e38a5c01b13cdf4d6d76856803d93ea378f229ec78efdf0bff8423d1f20180ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20180ffff3cffa07fa5f6212a801d5cb838cc4a503191c9f8114b5ab3e8ef7a6ac8315e500e17d08080ff80808080ffffffa0e272a0ad059d83a657a69dd19e540c373235cc2fdb78b25810c2b22da0fea209ff8400b0585c80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585c80ffff3cffa0f7d81abfd1493b3e56ce75df11cd32e29cdae6c6b37d5f92d84824c7b92542c28080ff80808080ffffffa0a2e7b4badd0ff0c260c53687ac29c5c646a6f76d07a583ec6e361ad0de0dcdbaff840160b0b680ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585a80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585c80ffff3cffa0bdb935dd4ccad0dc3d364327518ec9eee8b74f08de353685d64b8122f3be3ba58080ff80808080ffffffa04fde59076748d0043a4892cecbce74bbd39bd1400206651d739bd47bdba70c81ff85008f47c80b80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40680ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40580ffff3cffa09fcf946f6e74e58e03cca94fc4b7f0aa144246cd46eac2d31d4ed9a330ca91ac8080ff80808080ffffffa05dd584607fb995f6497b9f8a59ce3d99a9a233b1af1a394dbf67ccc0074dc0e3ff8400b0585e80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585e80ffff3cffa081644191003a00b14ed956852b60b1a1605390028fabf92cb6add26e69b0b6688080ff80808080ffffffa03871d6cf73542613bad26ff6172510e2be7635064873c516140a5939952ba1deff840160b0b680ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585a80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585c80ffff3cffa052c300407f1dacf4a3465f32e55e45f0beea92ee3b5dbebd9e495cbc2c17e0728080ff80808080ffffffa093ae14eb1ffa0c0f69906927750821be3f8e83292f7a35ad0a3230f2727d424dff8400b0585c80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585c80ffff3cffa0ca63b0b91a124e5295754b7c6111a0fd0d75109e112f3ce06943c206a3fa157e8080ff80808080ffffffa0d5c376b1caa928d5e15756721fd650cd9d2c7d9e9b652f3c535f68cb275fde5eff8402c14e9680ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160a74a80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160a74c80ffff3cffa0ab54d89a30436259927a48c3065f02cc96e9152ca2a17860e3b6535af26d0b928080ff80808080ffffffa00f59c4013c59c7a189de5953a20bbde02967bf044b7a58ee559d5e113ccb6cc2ff8402c1616880ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b380ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b580ffff3cffa05ccd49dcbc1837e35fd757f4ef16d12237fb22b0716e00053e774724f1b281038080ff80808080ffffffa09ee74cf1cc05ffd8e49f5d0be075652a481f51274d1e9a0a02a267433118ddcfff8400b0585b80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585b80ffff3cffa0a18ebed52a1b8ece5f99e015f060ebc27da02042996d51bb52bff52c1f0604c98080ff80808080ffffffa0d7cb503b2d0bbdb15b0f73417a1d159071ec1ed26aff5c02ae6937f17128979fff840160b0c080ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585f80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586180ffff3cffa072dadb6acb241e60f5bf3aa93dba1c0f8582cf050dfaef725f5fa925975a6fb38080ff80808080ffffffa07a2e6655fd2072cd20c44223f6d7175acd49914f4bd57a153604c44805e17deeff8423d1f20280ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20280ffff3cffa0f3cb9a238aed6b1e61316b707f1ceeef1e695da320c3bb6ed2142cafb86dd7268080ff80808080ffffffa02f400307767b5b43fa0765ad62571469111753d15af67ad8ea3c06819dca9ffaff840160b0b980ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585d80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585c80ffff3cffa0da9d680a369aeb278ecdd3be7726524eb2025f2c5ff5be3743339260801730658080ff80808080ffffffa06405d18ebe8767bdf04d342176815250e3f9bad8c3bdf04b04bdfc4d63305570ff8402c1616580ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b380ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b280ffff3cffa0743eca63bd0978e5fb1be4a1939d0b036d4173025f691e2f1d7a3274f03541108080ff80808080ffffffa0106f7eafc6e57d0c5f36f748a4c7a30b686b026741bb6f6f2073a8a12c5aff4eff8400b0585a80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585a80ffff3cffa0a2ffb1fb08d0413052e27173be2eb9101ae0ed81ecfce12353cb0c3b88e71fa78080ff80808080ffffffa04a313994c18f1b0a4e563af1835764a157fda68e8e750753515979bb412b05c9ff8400b0585e80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585e80ffff3cffa02b9e1b545ce7da01747d506b0ab8c4720d4ced8d685b1c8ceb152e48390a12d48080ff80808080ffffffa072ceb8e2eb1811953798158c3d989d825f78b396daa08bed2981c28d6da2e0e2ff840160b0b880ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585b80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585d80ffff3cffa02dd3b3d315ca7dfa47e90fbbeca7ff69c7bff97f3229e91d78a223f26bd866df8080ff80808080ffffffa077495825db8106a8a2612fa4454957a7ae724ebc59988dc87b8f194dc86e006cff8447a3e40780ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20480ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20380ffff3cffa0039028e4b90ca1db2f5d39e825dae08b8f47612ae151ec55f3bab19fe21b46c38080ff80808080ffffffa05ada8f21b2c8c8823013ee8567bd84125c9a762f428f395fc32f4585618667e8ff840160b0c280ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586080ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586280ffff3cffa0017d567675dae0683e8125828f4a3c3c48d1149050a39b69c678e39f6af356228080ff80808080ffffffa034facecd85936ef1d7137e62a5acd2906b0960399509f6c0547de6dade483ec7ff8400b0585d80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585d80ffff3cffa07005674fbf15ece8591eb505ec5481482fa9eadd868ffb4256ed39534960c93e8080ff80808080ffffffa0f9d2311d7d6a9b41d5361ebd137c2723612e2fc007bf3e0bd25f7f44b5f4d8a1ff8423d1f20380ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20380ffff3cffa07d206b1521004fd917f2fdeec52ff341e3ebc2ffc7198e4e6101cb3a3f7f46398080ff80808080ffffffa091c21525b17ec34390566df445cef111dc4ba445e87d20ed948bcc4f9772928aff85008f47c81280ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40880ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40a80ffff3cffa037e82bdc150732ad26414a7293f73b4fb40d9be245e8bca331a0d1e13884daf98080ff80808080ffffffa057cd00968d77f6f07721a4ed119ca84ffd0284ca2745c2876da693973d2e36acff8402c14ea280ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160a75080ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160a75280ffff3cffa0393b24efc459bf6f878455adce4f7d243b7619375ed6fb9d89016f88e2a77d4e8080ff80808080ffffffa063463b5fd97714e6ce1f11c81e59535ce3c26a7b1aebbea40f26dfb8a4417caaff8423d1f20780ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20780ffff3cffa01ad799bb4dff9797917c034a58a2fe8cb4b681491bf0fa959323550e740d6cf38080ff80808080ffffffa02731507e5cd841da794c4db2faf937245ad03702cde101041bbbeb69baac693aff8447a3e41080ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20780ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20980ffff3cffa086346f41c788660bc0620a45ff47b28972dffe3d7c7ad0bee463400903aa031b8080ff80808080ffffffa04fb9afccaff0f57fe4cf73f6bb481ed920c40f0d54306bb0d3aab9ec21fbff10ff8423d1f20480ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20480ffff3cffa097d75c5703ac0210141c10f7d72ea0318297a3f109a6cdb4f14116a9725476758080ff80808080ffffffa03d336ca8f7dd7ba1e34c846b53e95cb6014cd2eba50b23a6b86e2ca6065b3a78ff8447a3e41080ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20780ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20980ffff3cffa0b71ec0308a4c7140b22a05d9de4783c217f1d70f0150392e3ad0ef1d3cf2db7c8080ff80808080ffffffa0cfcb852ca3768cb18672341b5ad9e6e8df1122d1df2da3111cf749669b385eeaff8400b0585d80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585d80ffff3cffa0ec02b1ec50e9dff144a268f1279cd890c02ffb768beacd1cd4dc2761c5008c008080ff80808080ffffffa07c1d55712fb1e5bc2e9fccc96040b5111024f57613b85ad23376c83ff99b7cc6ff8402c1617980ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0bd80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0bc80ffff3cffa0eb708a72d6c047a7e2f6e3407fff77beee487dc1aecb1963f8d47ec1039f8e288080ff80808080ffffffa018966331bc725236a541e6b51b13228c43a21b9b28a156ed5b003a76a4a019e5ff8423d1f20980ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20980ffff3cffa0eefd356008f4ccdd817870abc286658d5814032fb4358579cc969aa1bec0d0e98080ff80808080ffffffa013e311405b030daff210a3fd0cf61641f9100e04e42d3b4ba4aa9b09f14408bcff8447a3e40980ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20580ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20480ffff3cffa08cb6f27dde0ac9c693be9e4bba03c3b05b2b7fde6fd188f1ae6b6e16e12ad7958080ff80808080ffffffa0b3cac0f846d343387b00aba98b0dd6c7cb4bdf7464545b97c1e4a30ad824e0c7ff8400b0585e80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585e80ffff3cffa0cba6c10111b673108c9c7ea810e5707e8369bfe5cfb8bb69192a7f21491bbb858080ff80808080ffffffa02b61e8c24f4624debbf4d9c550deba7bd77f8e279343e70c23f2d05aa67f492aff8447a3e41180ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20980ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20880ffff3cffa020d14be89d42d3c5b6cee02b01e80cbd0caf9650b215d9615f366325b908fcbb8080ff80808080ffffffa0d5c376b1caa928d5e15756721fd650cd9d2c7d9e9b652f3c535f68cb275fde5eff8402c14e9580ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160a74b80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160a74a80ffff3cffa0a972d04a963cc344d8bc8bd47de7876a2fabe42713ea15b54e9343db6b86b4be8080ff80808080ffffffa02475ad46c4fd863adfe8b921982fae7aaa4bb3689f33e1c426a18515d04b4cbeff8400b0585a80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585a80ffff3cffa0287705b2e95f1714a14a7e2af44017309590f286a3ab6d9424d9879d3b1932828080ff80808080ffffffa02ba2394b88dccfc5d1352cf8ae2d3ff1fcc80a262f92bdc3d51da7f0778df548ff8447a3e40f80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20880ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20780ffff3cffa087c67b877aaed9d329415d6876f5e165e2b55295c7e9c158d30eda1357cbad2c8080ff80808080ffffffa04454a39198e22fe8a9570c6fa6b7733f702170f2c064060f924177f750a8b585ff8400b0585980ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585980ffff3cffa0bd5bd2505ab58075fd576fd1a15b9fd3ea0781ad18b441d1411715055dc83d538080ff80808080ffffffa0f92a54bbde54131be7943425dbcc89e09ccfce3646cb8e1ebc0e013349e6b78dff8447a3e40480ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20180ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20380ffff3cffa0e5e31e5f75c26d3e777520c6d42ef03c8ec06849026e37eeff63ac7447fd9ef88080ff80808080ffffffa08ea43de6c2bc4cd361f7c1ca565a1ec223612f8b0680c2f2fa8eac945a062531ff8447a3e40580ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20380ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20280ffff3cffa092ff32d80ca10beaded72345bca45e6f6ae715d74768449e2c196d62156a0ad88080ff80808080ffffffa0cd9986fbf5ecd29a4827c65fb4c4d399925a21c180ae9a6f32182b2a6ea9b0f9ff840160b0b180ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff840160b0b180ffff3cffa0dec7622d28ce8bdd70ddb716122c86e96cf5c0465d74f8babde0dc224f3b961a8080ff80808080ffffffa0bb6eb1d65cd99c0190cf44375de7be03544c5764f866650d46705e83d7a56703ff8400b0585980ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585980ffff3cffa0a9850118d550f062f19f0e67961dae16531b392635ecf753d5d2bc6ab103879c8080ff80808080ffffffa01a6a240389268c8d7730e2f3cd56462497c3b599bc83a57941c11379c2454ffcff85008f47c80a80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40480ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40680ffff3cffa0bb7e2b86699c8fae4611b37eb407e7ead2f9492355bcc7120285153feaf15d9d8080ff80808080ffffffa0c29a7890db81abf2990c04247712847abafe0c07107d6e52ad0268cedccb177dff8447a3e41180ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20980ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20880ffff3cffa0daffd0a01b03217651a674d7b1fe507eecef8404f44e9fc227e4c3f546e434728080ff80808080ffffffa00374832834d6cbdc3212e775e48d75c040f4ab612e5ddde64bf3b9c10dc124b3ff8423d1f20780ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20780ffff3cffa077987c6975aa6dc5be5eddd0f31a66812d32f1407472a28a720257cc943287a08080ff80808080ffffffa05589f2207f11788b915380865814ef11a85bff08fe603d27db2e19ebd0b1fd69ff8447a3e40b80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20680ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20580ffff3cffa01b0fc99121e00a1e45592dd9fc102de3c429de47b2b07d0a8db5a3aba44870a58080ff80808080ffffffa0fe81f39cb69bbcf4a26fdaec9f19d374dc6327157742ce248a6cc24b1b4c7906ff8423d1f20380ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20380ffff3cffa0766f0b8f771d58bc14998792fa4f80e6c9c30533bcce8f4c71e4d7822d6050b18080ff80808080ffffffa06869bd5515cf15789fef4b5994d46a1d93a78e31ba8d3f30e2d1a3e165e0d610ff8447a3e40b80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20680ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20580ffff3cffa049d41f35d7de4a4a0771d9697e920cfcdadaf78493256b1dfaae9798f57d4a268080ff80808080ffffffa0f05f3a96cdfdd6accf5402a1705abcca2ff9ca958b35f15551ac029a16309446ff840160b0b480ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585980ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585b80ffff3cffa008379c3a07fe6356ea76b91e69f5df48d7ad2cdfbbf77d21372fdd30765662978080ff80808080ffffffa032e81d5035e6b71f553f3a7137814c595ccda1e97e8bdb91b12ac5cc5a0751d5ff84015ec7d380ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff84015ec7d380ffff3cffa09b235a572252bbe1194ab4cc13c285e321c0281a4bfeb7b0b63911c9b3b7cade8080ff80808080ffffffa0c5450f6366ec81e3c18d3c30ae4c8e9ca6d418316dd02491a72300b8633c452eff85008f47c81280ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40880ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40a80ffff3cffa00a929a4da227403c4e76470962d57a2ea7816f54f77f82fd38fd5d491aad714e8080ff80808080ffffffa0a90d1637505354df67bdeb5bbd9367cfef47c2d741519cdd93b9797e50ef77c0ff8400b0585e80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585e80ffff3cffa0cf12c6bf7535e5ded43ccbbfd0d503522bf66fc7356955304c2e4eae49f2ebf98080ff80808080ffffffa0f8b75dd56ad5c757e72e847e09cc2f6c5ea683835b2f55f2e65433130464690fff85008f47c81480ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40980ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40b80ffff3cffa02084725033509b03395e8fde6173cf971aca5319e0ea746c1f364794de8ebab78080ff80808080ffffffa014ee0dc301d137bdfef0c1505437d74942b1efeb5fc121698766189bc45127f3ff8447a3e40b80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20680ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20580ffff3cffa0cd90b206c2a36ad3ea05fa4e7774e26d8e33337bca64e1b9f81e53fce910314b8080ff80808080ffffffa099f010db51ba681f2fd9c4a56837064063ec35c3f50e08fcfb57b97c0170a788ff8423d1f20380ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20380ffff3cffa08c55d60ca4f328541df49d8d9af09e0da265a1c8ea164b93f92134502f4319758080ff80808080ffffffa07bf570866bc98c92f4e7140675ad6fd2cbd77e25e7f13005eab7e33184a68ea2ff8400b0585c80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585c80ffff3cffa0507d3d57090ae074f58a34ca61cfa50641ca71e4e7b3c210c07cd287281172158080ff80808080ffffffa056006bafd96958821a5d20cb3098bd9d5791cf73312cb10cdb11388874520b9dff85008f47c81480ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40980ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40b80ffff3cffa07d7cee7e762e97d76cd275eeeffc7ae3d3a9929a476dec2d0c5c83a7f932a7af8080ff80808080ffffffa0e774521ad0c9374d09022ced698d77259b2e00ce007595b3f2ec854ab7f59187ff840160b0bb80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585e80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585d80ffff3cffa011f1bc7fe733fca37934b84ba76f211f385b1ba30a020c1fe5b91cb209d526588080ff80808080ffffffa004bbfcb77adaaa4306364477bf605d6e0ad1a21a71de065d8648473b1b3264dcff85008f47c81480ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40980ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40b80ffff3cffa07b3fe365c2ab50afceaa9ace9cabfd050cdab6152570a9391f8874eec9d6c8348080ff80808080ffffffa0c8237c9e5c9a48a1d70937fd43e934a0bc9423d2957c402337bf1b9774b096aeff8423d1f20580ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20580ffff3cffa0768e6edaed111467670976137c2e3cb06107182ae64f7f0438d3a663ec29995f8080ff80808080ffffffa0c7774da3768ecda6d7fa09e0c7b88917d19f9e80d363cd236eb350ae8f38b33aff8423d1f20480ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20480ffff3cffa046ad1c6ae73d65f42ee2aa4cda2695d4cd5b5bad8eeb4d8e19e4e37c8a54f1838080ff80808080ffffffa090c8c197fd2aa7a60dade52100fef96ce016443f21648606194d02bc6c15a7cfff8447a3e40c80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20580ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20780ffff3cffa00d997f502d2d3c0b0a2d6f943a41f454b0d83753e13b50eaf6a56c351907640d8080ff80808080ffffffa0fc86114b97ee899a7680f47fe859b135c4de1f2a429d52d89a1127bd01f7085fff840160b0c580ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586380ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586280ffff3cffa0ed4e77d5efbb2e70559da692eacf43c3ba8741b512e5b7c3db05f2a49f368e748080ff80808080ffffffa0a2e7b4badd0ff0c260c53687ac29c5c646a6f76d07a583ec6e361ad0de0dcdbaff840160b0b880ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585b80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585d80ffff3cffa044e87fc37792d99745d3afd8cf98143e4f191ea43a05385ebbda6bbfafc95cfd8080ff80808080ffffffa08a89efa5600227cc82463146dcdff04b02115303bf7116fd17b3cbf42797feb0ff8400b0585d80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585d80ffff3cffa04041f97c0a8f90da8d567078295aed9e3c8de8e3acf5ee07a256bd813643a9bb8080ff80808080ffffffa028e7c56c2b26f8f543a91573a16bf96016879280c3362ac7138cbe4d4a39e211ff8400b0585e80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585e80ffff3cffa08868a68f1c36bfb6884a4f7a7ca6f5f5d280a757372b5100ddc29cc878b443f28080ff80808080ffffffa0abea892be3f96b22825a1eff7656931e69b981ea6ba01ec6b38647bd2c4310a2ff8447a3e40980ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20580ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20480ffff3cffa05f6f52bc0e2581d0dd6fd12e54d522921d0a8eebd434bd5a9aba43a483bca3428080ff80808080ffffffa0e0669c6d8bf1333c44e24d88d540ec63cd3c60ae3df990d80f7dcbbd115d8c1fff8447a3e40c80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20580ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20780ffff3cffa0f2ad09646a667aba289686e47610e0cad820f7b3cb865fc91f68698711c0b4bf8080ff80808080ffffffa03e87447b560c67cb11b672c5fdc942878850ed3e1495f6d2af2b7fa30f64c5a5ff8402c1616b80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b680ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b580ffff3cffa0a4787b7c0d61d1aa6cc01ac1db83ca5061dd6bc99add3417dc5069f00517f1ea8080ff80808080ffffffa008c415be7e52b0b4eb43f7f9d15a62400c26c59b7c989e0f1121865e8c737d6aff8400b0585d80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585d80ffff3cffa0e89d7a8f538e7b4fcfa3dc1b030fa4d18ef184f73479ae0b7792a358795aa2858080ff80808080ffffffa01c41b192f6a5542ba1c4377e5c39b3a4de4a0be40a5d46cee31937b1ec8861afff8400b0585d80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585d80ffff3cffa0383bd15a87a5bd16370dd8fdc5355abab4dc879824725504497a557b76f822a18080ff80808080ffffffa01fedd5382bb8da21554b6aad4c3cb6f546dc1874e1567defc19d574d758c7e5eff8402c1616c80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b580ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b780ffff3cffa06c689d3f2af9f0195427fbc58de746b948db10ba35872e693e969574f9f0bb038080ff80808080ffffffa083320cf0a1e0742dba24add0c3cb15023efce8e492ddcc5cfb99deae65688a9fff840160b0b980ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585d80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585c80ffff3cffa032371eee525bdbe890381bcc1c1ec0e86276b4b1bc3c8371841d579965feeca58080ff80808080ffffffa0a7dbb40634d0f30228582d7019d31ede54051fb4c9bcbfbeba10563798b298cdff8400b0585980ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585980ffff3cffa02ce6151947d0ab04e7c8c5c67fd58558584b6f21eacd0383f1f5accb704efb818080ff80808080ffffffa035f401240ee0cc196594c2f57378538ae86c2f53249c15046eb2f780762ffde4ff8423d1f20480ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20480ffff3cffa0f579049a4f3954b14dc4ee1c87a153dbba3c33567ddd1726fc76700bb13b5c168080ff80808080ffffffa05c22c586a7d716dc93356167a3eb1a350facbdf183e849b60d7db72b8e2651f2ff8402c1617780ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0bc80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0bb80ffff3cffa0e311945fc96154cb478d74acf90be1bf08c6482f991459c7d609d3051e67f7d68080ff80808080ffffffa0f880e075500dba69409eddfc251fa4ec4ddd025f326cbe14f9083882a7abdaa1ff8447a3e40680ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20280ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20480ffff3cffa06d63a9e6ca683807c865baf8715fe6a7da2ce2996329f0d148fd75fcfb8906258080ff80808080ffffffa045dfef331eca21498d5fbdcad5751a5dccee76be309274720290eca2bee0fdebff8423d1f20480ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20480ffff3cffa01fd14791db46f8662f76cd29986a797a79c889d0b20333c6c6063c9bee9f37508080ff80808080ffffffa0e35f79bdea7e263ea4c4bfb1fc95d8b70d6a5903deea160ab962b15432afb905ff840160b0bd80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585f80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585e80ffff3cffa07f4fff25da88101928bf2daa49bca82f5510eaeb3b0d2c8553eb3309b2b55e548080ff80808080ffffffa048be6a715bcc75412698675f68ad5efaa199145e21dfb26b36d0f7325d7c6106ff8400b0585c80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585c80ffff3cffa06812ec5dd11f9ae5ed98f10bf22f0573512333e1bffabdda0f03d5ba3f05dfa48080ff80808080ffffffa04d196e49ceecebfbd9e4d09353c453a1b3ed397d20a89202ac72c0d52f906af7ff85008f47c80f80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40880ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40780ffff3cffa070d38a6a5d222cf97793d437aa7e5e640650c4f92083cd244520f686c8386dff8080ff80808080ffffffa04d3ac56d49a0b0437623c23f5dfc66c37806741ac8480d3e8fdadaf98b36fc0eff8402c1616d80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b780ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b680ffff3cffa00f16a7aa0b689bb58da7aad836673d3ce8514d17930fced384295043b64938238080ff80808080ffffffa036a9abe750adc5e58e87b7201079db558a07bfdb71b03881d907c98108a98427ff85008f47c81080ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40780ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40980ffff3cffa07efacfd57b1afc46eab5e820655e908efd015bfdc593eae0da4bca6e9e7ddf6b8080ff80808080ffffffa00fb62dee2d0d7931c2506728049b50213f62e7c188c2f5e73a239e12a623aaecff8400b0585b80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585b80ffff3cffa04872e0e7c9b96a13b9e9723b497b9893e74add67e9d778934160a7ad43f855ad8080ff80808080ffffffa0b3867ade00c2aad8b0dade0f5a6f8bccc9f5e7a90e696aa22fd4e3c2a60e4487ff8400b0585b80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585b80ffff3cffa02c6e60b68ccbcc44d5be0533533ee93f7905d268c86e65728970cf5e8cdd253d8080ff80808080ffffffa076b1cc36f630ecf26c369353bb247cf4c125661c09aaabcc0ca63a7c1e24c5c9ff85008f47c80d80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40780ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40680ffff3cffa073afb4a140ad07c510945d9be4ab4f5620cbb1624accdff0a1cfc278f74ea6668080ff80808080ffffffa094a558c0406255a99c46dc582dde983ac9a5cc902a3363d4aa48ecdfe9e541b6ff840160b0c680ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586280ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586480ffff3cffa0176759ddb3c7d48f8083b5498618540234a2c699a8903d1dca1b2bd6e3841b738080ff80808080ffffffa04e9fe555ef9b2c90258ffde7735f8b29485254cb7f7c72225fde309f6f33e51fff8400b0585a80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585a80ffff3cffa027dbfe095f3917444ff1d441f29fb2c1cbff8b0bc65fd55dde13397a03ca38978080ff80808080ffffffa030d378a60d5f43e7ac1ba6dfe0608a83905e370c89a737a1fbedb51c9a03529eff840160b0bf80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586080ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585f80ffff3cffa0b88df7cca947edfd08ed2aed4656e3e6446c8d3f060894fbec63601a3a6875538080ff80808080ffffffa073009bae4c9267d5bf4d71d0aae092f7c331936d2257e56047913f55546585eeff8400b0585c80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585c80ffff3cffa0019d99dd493618a8e0b0e1b5d183db876bb4a705584aab0d954c49fcc55c10d08080ff80808080ffffffa06c0dce17e589cc178bc43142ee89b663139e921f701c82dde8def57977e8a678ff8447a3e40a80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20480ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20680ffff3cffa0ef8b5b4750e180666c97bbe9e135e531ff1069adb6142cda985cfb9956c194398080ff80808080ffffffa0bedace578eaee42c9434a7b9e6c222a51039dad2ce90c0081773fb9b7fab774aff8402c1616e80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b680ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b880ffff3cffa096a8e7f2ed71eae104adcc30b84e1e5e79a3d211b45477a10dbde67cbe4773ee8080ff80808080ffffffa09c4dec4cfbcfd084b07b59244b68b3fa4b072d9ac1269746c090cda419707874ff8400b0585d80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585d80ffff3cffa01f4b48356da7cbce240f50bc039f862db6b88ba711e53cddf61a0bd04d47318e8080ff80808080ffffffa0b3e7a3211c81900afba047983a9be37ec40634a9c05af3204de34c33139ffe9eff8423d1f20580ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20580ffff3cffa07f81d3e1b909937ed31563af92086f73420b8931001bb53314f2fd1414be2c588080ff80808080ffffffa026e623835062d7613ec1d0bd94482156eef72ed262cb835bc5282bd54dbf4a8eff8402c1616380ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b280ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b180ffff3cffa0525e71a4b36b1b65a3c5516b128f115b849d4dbad1e1c10e7b59ffa8bbea770c8080ff80808080ffffffa0a25e58e8c4e5f45bc8853ae319961cbcdd81260618ea1b9d66e07dbb632ad693ff840160b0c180ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586180ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586080ffff3cffa0fa2e511cbfadf77667f0fa0805cadfd965403463281174a0cb9e8e59133cb5f78080ff80808080ffffffa0f970a6639876d47253950743f853ef0e72ce4a3e5756fd45e6e914d946e5d650ff8400b0585c80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585c80ffff3cffa09477dab5fe03556718f0d150d749c450d39c55d95743d2ce03b4539c49d797628080ff80808080ffffffa06869bd5515cf15789fef4b5994d46a1d93a78e31ba8d3f30e2d1a3e165e0d610ff8447a3e40d80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20780ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20680ffff3cffa02ea5478b1cc2877b548c37f607abe514bbbe3ba168663c8a2b49849b16794eb18080ff80808080ffffffa082c5c91dd121aa61156c406d0f6fb8d448b951254b322113060e8927605d3d95ff8400b0585d80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585d80ffff3cffa05c0ddeb91ffdae7de70dc9136d83908936992ac137d3a7267934519c10844c8e8080ff80808080ffffffa092e4fc9ce595d261794777da86350171159b9335214a517d941d052c30b9e863ff840160b0c480ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586180ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586380ffff3cffa03243a5736b5b2e7d0e5e8d3d8b8df9c578b4cffab4b34b4db91b07233786f2f38080ff80808080ffffffa0c2195ffadc39f352ba019158eb57dac439b484fede90ba62ae4da4384428ad50ff8423d1f20380ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20380ffff3cffa05112471d300ed3886597688d8bcdfaa71e8acc150c8db49aa930d12cf18551438080ff80808080ffffffa04ded3078963282c2b017519c6bd1680f9ac404431bd38bac3905bf6116ddf284ff85008f47c80d80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40780ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40680ffff3cffa0b44ba6030b4592ae240b42e7b43264dc77db76c50a1a2e74adbd1086d335fe148080ff80808080ffffffa06ca4a434bab5a83fe015831829d2aef704e82e9a4e3ecec9c5bf9f50123481e0ff83582c2e80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff83582c2e80ffff3cffa0a55d9e54bf2da7df5967674bce1721ac9bf8859ccc039a83bc216119e80453668080ff80808080ffffffa0f5c3e789b7fec3ae36f96b0789ed96e00a01fe261f3894ae825ab8e442e5dc72ff840160b0ca80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586480ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586680ffff3cffa02e45e8418aea5d7c73dcf557348e3ba34fc5e8df8f9d3af94ce36adea0f0a8778080ff80808080ffffffa031ff480c6f54831488220b21e187cf1ce62e65f8807435b2ca95cd066ee2e3eeff8402c14e9e80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160a74e80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160a75080ffff3cffa0d1eda1b016bb5a6101e20498fb981c3b493445ba46756338f9efc9a5a20e26858080ff80808080ffffffa0cef2e00bbf13d98e1916e97073c35c5e0a9183dd79ffb875e54f07ed73c4605aff840160b0b680ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585a80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585c80ffff3cffa0fb4d53615ebef414db1d92af0e1953dd36ef00026f95a9190766f783d50956298080ff80808080ffffffa0c0b15efb9e6e5572ebfa8eac9b65c2dbd34d367864c72770224aefac344a1c0eff8423d1f20580ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20580ffff3cffa04a9f708ac6e6bc86c9f5956f78a2f9cd5734f53af9b556fb9053b7a7baf71b558080ff80808080ffffffa018aeb68168bf7414894cbd339386f58203c54e8944fff724d269a34fc5525952ff8400b0585d80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585d80ffff3cffa0e6d56e7174bebb1d15f3bf2d444c72c1a90564fc5e3545ec7ab85fb0d538e3008080ff80808080ffffffa0169de5d3f25461b39dbc06b10cd4f4ad830ae3e1c72a516122fe403d7fdef827ff8447a3e40e80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20680ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20880ffff3cffa072406fa347fd847efcfdbab83cd7fe97f94dc0d38035c8c79c25e69c08f26daa8080ff80808080ffffffa0079bb5b1300bba4499b49f16d967d45fb3ae98ab643dfa918964743389624284ff85008f47c81180ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40980ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40880ffff3cffa042708c50643d107e49d732f40fcd9e5523064307676652b578ca0ee4d359aefe8080ff80808080ffffffa032f552b6d88d145b31133103dc2296727e2d1c63c72469d10681a2c546ed403cff840160b0b480ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585980ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585b80ffff3cffa01812d354e1bbfd6b4798ee074fe0e79c34f0fd739fca4dea0ea0a53bee89e7918080ff80808080ffffffa09b1dc866a54cd0ccc909870639a24a381d951b80510655beef15b6de465ce880ff8400b0585b80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585b80ffff3cffa03806e0f228c788b1ee8f9be710630f2b4b2cfc8453bb2f54c3f81066a86c901a8080ff80808080ffffffa08c38e3f621c5d9beb295695d3151c2aea6737fe6c0ee4222d58c36ae01b085d8ff8400b0586080ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0586080ffff3cffa0ebf149d1820fcf8c6a6c49d3247bea79babd7963c5b1ddc42d8246b1788de3d88080ff80808080ffffffa0633fabb57ed04295c207ec3288e67cf2bee5e910d62063434fc23bbc14b4a5b4ff8447a3e40f80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20880ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20780ffff3cffa0277835db8dafbd6107747beca0ce81e6ae39633912c029d9f6a081bc891e02748080ff80808080ffffffa0e8430c15e7a5e1844d9bc076a8ba05479a9f1eff4c85ecd2ffaf0977d074254dff840160b0b980ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585d80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585c80ffff3cffa0698df4e91fef4beffec9a61650fc525137bc9db765a9f9e7e1881ec901d17d308080ff80808080ffffffa04798c0ce33c872d625e831d81e028483c3c487213452128531ab015e61d8622aff8400b0585b80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585b80ffff3cffa036d51552276cd62b929bd2f8bdaf32950a7c7e4dd561e4e6eb0e4c89532560f38080ff80808080ffffffa0aba8f2399dc79dba33404efd4331ae4c2b9739d7337f2704eb9dfe354e48fadbff840160b0b880ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585b80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585d80ffff3cffa017864652a4b1e9a5b7e436becb06218863731f288ba24508f0b4de02f2f6c3b78080ff80808080ffffffa03220fb97dba1fc328ae9a4b5d17ee6fe5204de03fd3133a6443f9d3052fb2b6fff8423d1f20280ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20280ffff3cffa0dd5ec28d79c49f105186f00d4bdf30076f182bde833363cbbab90a41822fa6b18080ff80808080ffffffa0aa6426e351b76cda88dd842f748df8be450050fcd4560a5a13e588255fc19a78ff840160b0b880ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585b80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585d80ffff3cffa08312f83aacb0d2b2805d5e3e83b4094d8dfa60a1c91804a948847c947fe96b988080ff80808080ffffffa036a9abe750adc5e58e87b7201079db558a07bfdb71b03881d907c98108a98427ff85008f47c80f80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40880ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40780ffff3cffa07de795eba4fb7834dc53f718aa795b71f83b12667708e8aff2c896e37702079c8080ff80808080ffffffa062af968d42ae61ff0f6d61868b5f7b6b0ccbf757d5cc225f7c68fb8d8938ab87ff8400b0585a80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585a80ffff3cffa01c6c8e9b2f1c1a3fd67dbc50b7cd82c2abf2e3967f700d38527586be762534d88080ff80808080ffffffa029a50f86a0b1d32d789772f6e8f8b1083f99b8b92770e424b4eb2e81ddeea3ceff8447a3e40e80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20680ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20880ffff3cffa0cf880364c99246d602a5195fa957fe18d9f4c81d2193abb0e7d6e80514d2014f8080ff80808080ffffffa0abf9178fa4cf202267fa8ac9f8cadcdb680bdd43d680cc3784d7552150b6fb81ff8400b0585d80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585d80ffff3cffa0a030f6b3c5130f953a31ad3363aa411166278c37ea2cc438f59a5a9a705fd1298080ff80808080ffffffa099825fe7f1c0218bd16bbc146b24601f303016ffda3f5dcc515cbd524fdd0cd9ff8402c1617680ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0ba80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0bc80ffff3cffa03a237227c22df8ddec0fafb99d91ae161d294bb7095dab55b33241ce9db813258080ff80808080ffffffa0cbe1df6b2a3ac03ec0840a3ba56ae6e3ed05418b5fb63b3176e0daf84eec506bff8423d1f20380ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20380ffff3cffa0dbce5ab1bed905a056b3e105f200a40399ae83512758a35c5e455ad4b781be5a8080ff80808080ffffffa055f5d98630c6be89f9ad9302ae29ae57bf96b984f1fd7f6951bee7e9fd978129ff8447a3e40f80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20880ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20780ffff3cffa0f3aa729e9ebc85dd5bf8b35d60d8855fe7e36e742f1192334467cf04c73b04f78080ff80808080ffffffa0c87f198a0f24811863b18e5ae067b1fd641afeea76c30160b23197623028f8d8ff8423d1f20780ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20780ffff3cffa0cf305fb45bf61b99790d708d288be866f16a8716c4c7351eb1e4d4e8ea552db18080ff80808080ffffffa04e278ef7dd70aaec8e3d9c101c37b9c4140bf445f25381b5da53ec641ea43e20ff8447a3e40f80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20880ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20780ffff3cffa07c1102a5977bd35b80c07a4d7379879275a98090f9a0882e3103a6321a47c9368080ff80808080ffffffa0ac79a5305259646df5e33462968472b9f70f6909d1781a5b83c5747bfc5520ffff8402c14e9e80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160a74e80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160a75080ffff3cffa0ca25954b3dc709409381d47c8392bcbfcdb752235385b3900ce0aee272d141e88080ff80808080ffffffa0914264c6c2b99630cbfc2dee996738de9432c81b820cf2b4cb3ad91dc60b19d5ff8423d1f20580ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20580ffff3cffa05f24c00bab833fa84cebe60feb4cb97fd32a14b74cd61a7441e1501372887a8b8080ff80808080ffffffa0d9550b0ff7ad309d903a1078cfde2034ba6c9ca517dba91e7094ed1787d0a1baff8402c14ea080ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160a74f80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160a75180ffff3cffa0f28a655aa386a12379326000219cf3052d85046c41630d67182a8f58d94bb8ee8080ff80808080ffffffa0f8a7da578a56f8e46da8ea564178404262646bf25f22400b8109f5041215c85cff83582c2d80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff83582c2d80ffff3cffa05310e887af70071851a9e5774efa5d830b470f18996f8dccd82f612787c31e838080ff80808080ffffffa02731507e5cd841da794c4db2faf937245ad03702cde101041bbbeb69baac693aff8447a3e41180ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20980ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20880ffff3cffa0615af55ab87e702dcdc7f2ed7ab1615bc34e3a172c28ae4ad81f27392632b1358080ff80808080ffffffa0ebc779082ecc9ce727d06363d48ec07fb0e6bae56ba9f0b6afd04d4a46077d4eff8402c1616480ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b180ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b380ffff3cffa095075e6d7d332d05befdfdc7a40bf4c67382138772b7d98694e8a9c36c9e0fc18080ff80808080ffffffa0724aee3b879467676ccf4b604df68cc794a5e5105497e8f805c19bf061583867ff8423d1f20380ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20380ffff3cffa05b2a32d041aca86d4a4475e681b590116183c3fa73fae63d1c85a8301f580fac8080ff80808080ffffffa0b23e2dd48f03325b1d3fc53a735fd8a6e2afcce2648f3611711d0930073c6955ff8400b0585f80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585f80ffff3cffa0e9ee0e8386b299377b0dbd153da0a2167750246daa15bc7a2ed051c8020b048c8080ff80808080ffffffa0eaf96563b6d3f852c68b930ebd65c41cd7aa325d5a6038fa19e0f247aa987aa2ff8447a3e41180ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20980ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20880ffff3cffa099a985eca458ba105e603f6496894c3aa99d4c8bcb2c3afcc42fcd51e6205bab8080ff80808080ffffffa0306170a7665cd6421884204bbcd165b156c7efc1d007d74d94dc8cd71e6fd90dff8447a3e40c80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20580ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20780ffff3cffa0830360b8f7b8dff767ca54b49d670ee80da7c68cf520611cbd52ffe9061424868080ff80808080ffffffa0b38842223ad1ced65afeb05056256f6b898d0a919f782f670370eb03f3a50f82ff8447a3e40f80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20880ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20780ffff3cffa0152ddb465499d64f0ba87b6241af2e8c4d94dca4e26ca242953e09ad87e0fb118080ff80808080ffffffa06ab71bdd644a08b56cfa2d8ebfe5fc6d3e50fbc0cac94dba9a77160df6822f45ff8400b0585a80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585a80ffff3cffa0d2287797e1931a6ce4721e78868e68ca79cbe70286de812905f2f0387b9316cf8080ff80808080ffffffa0472f515262a03d0dccd15374d703cf969ae55f96cb1e30a2f3a26c8b49a4b2b5ff8400b0585e80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585e80ffff3cffa073eea14f075532c209d3e1490e3aea35495e371ab26dc444f176f5929017d7688080ff80808080ffffffa092e4fc9ce595d261794777da86350171159b9335214a517d941d052c30b9e863ff840160b0c380ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586280ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586180ffff3cffa07d9d89efd744fc6e4bf82281b61227d3b1dd4cbbf8f0a0d78302075a1fd4c3698080ff80808080ffffffa04f2dd6e9f0bf5635b2d1404f616074179f4dd4207645b8a19a9fce85cba087f0ff840160b0bd80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585f80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585e80ffff3cffa0c12da7be90586233a35984b643939b201257891d87f86150a5a90bc22dc1765c8080ff80808080ffffffa0a59480400b31d8c474cb334b5759c10ff68a8058a004d86202045a057285c972ff8423d1f20680ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20680ffff3cffa01a8fb0976b014b1df6efdb9717bddf34e8fa85cb52aa36052e80ec6916c836148080ff80808080ffffffa0091f41c928db813f538ccd83422da4c1e55b67195e3d9a155436a39a70e7f88dff8402c1617580ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0bb80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0ba80ffff3cffa08e8edf230c125b6d74ba24b9b6863968b3f4c1137b6a313fe9861bff8624629c8080ff80808080ffffffa0fc86114b97ee899a7680f47fe859b135c4de1f2a429d52d89a1127bd01f7085fff840160b0c780ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586480ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586380ffff3cffa00b5528554abf29e221c4d7158ac99da47384153719c6bd954c0efcf86c5eff618080ff80808080ffffffa04a7ea363673a2a3e4fe48bc069f865bdf049bb1f5c8519bbdb70f17af5d8bb98ff8423d1f20780ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20780ffff3cffa06e77ce65b8bf6aa239b328efaf3086e167c86fdce2d18531ffd20fb918fcd9648080ff80808080ffffffa09391cfc648b55ea69eb66705e6f45e0b0d6b1cb43b2d5a8e4f2b421cd9e96a99ff8400b0585a80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585a80ffff3cffa0950907476e73dfa0669cc63f597127b86a08660d247d9dc3ae66275d2f37ad558080ff80808080ffffffa000634c2ceba9a6dbbf1dbcd6242b6da3374a94adb8a7d8dcb33be20d0dff27b8ff8447a3e40980ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20580ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20480ffff3cffa0ad297e5bf89a1114ecf309a7c6cd24327d1551745c9ef1c85ce05b0154b96eae8080ff80808080ffffffa0e675e47c24c3a5be9cdaae4bca0d60fa548e3afb23e22f0763de13011983bc98ff8400b0585780ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585780ffff3cffa0e7cce28581f79553443ca0f6e9f6ba7b16b8b87649d92437e6568ea88c17fc898080ff80808080ffffffa077c08b76ad651d0ea084da5c246a913991efd743059d872588b7626616cda5f0ff8402c1616880ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b380ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b580ffff3cffa06cec42976ab62cf085db62c2d9714b0229f4103cfacc41fc93fd643cfcfd76d58080ff80808080ffffffa0755813ef36530c6e9cf4cdf31b8fc08e89102cd10c02153c85708c39a3327ef9ff8400b0585c80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585c80ffff3cffa070258d07fa5a3793c12f09f44c79b9564da6b71db389c5e1ac68a0121f56695d8080ff80808080ffffffa0c91ff165ba014a50b2fdd1042374369e3315a9fa687bc6c0b889082eb1da6e0cff8402c1617980ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0bd80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0bc80ffff3cffa04b567c4e00c886e163740aaecae16dede669404d9be90cac9d2fe77edabb32d78080ff80808080ffffffa026e623835062d7613ec1d0bd94482156eef72ed262cb835bc5282bd54dbf4a8eff8402c1616580ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b380ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b280ffff3cffa0e02243525f58a07f915b12794aee085aaa6f90e119616deaa8592c22980a3d0b8080ff80808080ffffffa0374553db85cbf45ce3fdaebc8ab6ddf3abe7838f6159f745ec92aae1aeb6fd5fff8400b0586080ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0586080ffff3cffa0a84b66b32b68f9a7bc7cfa7e8e8287c4dd88b0a85af76bf68db65a842aceb36c8080ff80808080ffffffa0220c153c6cb71a9e3ddef6664cde2dae4efd0d17b14ed414e4cd7b4681b3c60bff8400b0585880ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585880ffff3cffa0fa2cd6a842d75b7adb062487e9b1570fd9fc60db11c5b6ba11f1a492aa10c43d8080ff80808080ffffffa073414843519eebccaa6a903d0db13fdece1e2c446f615b356fe0ad2949e27cefff8402c1616780ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b480ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b380ffff3cffa02593c24c1d4ce39939651340f5f11784a183ae1ad65ef5f2b258a02d7586bc4d8080ff80808080ffffffa07001d37dc41e38204a526613b02e85a3ef2440142d40acca00c6135954e4747eff8447a3e40980ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20580ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20480ffff3cffa048bca216d31078fc0a400cc4b953f2952b832896dc030b31068a76af1cf665358080ff80808080ffffffa035c2ee668f0cc7eb1fda11f8d40eaf4c5afbd9e27b88389307667bc75e64869fff85008f47c81480ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40980ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40b80ffff3cffa0dd89fd16556c1c349dd510c350b0ee73728c9f557a9e3725af00ac94dc20ec038080ff80808080ffffffa04a3c4dd674e5392b2673addb00953a011b6faa54a686681ff3b7c459650ae904ff8423d1f20780ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20780ffff3cffa0202ebe66faffac5a2b51ba3c0253cfb56d1b7fbcb0f640998f551567dce6caa98080ff80808080ffffffa0aa11c16c12227263ee0b3a7388f7c446f587be0ba119d306f3a1e49c0297c06aff8400b0585980ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585980ffff3cffa028c3a39174bff5f46d904a591163674f88927cfbcc03b2a9061501cc8a4e1f8b8080ff80808080ffffffa0cb5bff71769a8f89859fff7a75adba96a3a897afd543cc13cdb0c08a2eaace43ff840160b0ca80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586480ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586680ffff3cffa0bb6619058be8016783d74c2a726f0f6afa4e794fdad489a0373ecff95b89fa848080ff80808080ffffffa0a6eae07f57a427d0b1fa42d5999a30309cf4e0128a5b5421d1117da27367e178ff85008f47c80c80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40580ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40780ffff3cffa098bfc7a958d4263d6ed297108ae7ffc9abe2ac8bea434d6a94877771bf1361fb8080ff80808080ffffffa01777c0d2cc0487d5490c182972b5387803e527c600daaaaa6a788d877b923a48ff8423d1f20280ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20280ffff3cffa016247068d01dd553c44b2e3ae69c56df10234280fe7d671daf4732274dc8725e8080ff80808080ffffffa04e1902db3126a138ce788847895350518fd9ed0e64dec37e58771f09e9a7cff8ff8400b0585e80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585e80ffff3cffa050e07ea539de2d3b717bd809b7361002a61809d61847ba84ace005aea01e98f28080ff80808080ffffffa0368baf048dc1762b320db51eb354b95750d0186ddf993aeb75dda1ca50a719a4ff840160b0b980ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585d80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585c80ffff3cffa03af24a5350d839250202fdb4a22a210e25f1aa333d615ffd754c3dfa577e7d2f8080ff80808080ffffffa0e479448ecc1fcafaeaf1054ca843a90cfd93e94f941eb18ff9ec46c5aae4b3f4ff85008f47c80d80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40780ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40680ffff3cffa0e51db193f7ff21469eedecf7defefcd72ea098d07f805ae4c10fb68a945c79878080ff80808080ffffffa087630bf119e545527bdd0b5a6a50dfce68a8383b6ef361c2fcc86d1fa420b7f2ff8423d1f20580ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20580ffff3cffa0574d5e27a51e2dac31fb5d6007f6228e25269cb482450990783b477c9914e71a8080ff80808080ffffffa02b6f1a89aa93971c0b43e6529ea33d4496ec669995b2aed5992bae15d35d93beff8447a3e41280ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20880ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20a80ffff3cffa0cf9ce6f33fba30282a61f32898c64678196cca9c99bd319d180d1d22beba89dc8080ff80808080ffffffa0e021c88d66259c1a4afbea99368fd1e9875180cbb53ae66aa4375cb836666f15ff8447a3e40880ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8447a3e40880ffff3cffa03c3821643fa448da544c7eb7414c0b9fde838827ad92278cab8650c133131ff18080ff80808080ffffffa0ff59c7ed374d56a928ab4381bb5b874dbbed7454acbb60dcc69983aad104fe65ff8447a3e40880ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20380ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20580ffff3cffa0e1ea1b90bfa3b19c085cab34ce5233f1e7bf14457f5708cc96ade7d0e6c707188080ff80808080ffffffa018912c28d04dd493a8c42e1de4a03ae18b06c73425639672cca992ffe2e25787ff8402c14e9680ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160a74a80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160a74c80ffff3cffa0fe208e6e975a26d344f27f902864189e922c31ea98462235aa63e76f55ad47988080ff80808080ffffffa00705aa3071979f993633fca0a434d352db239d419d8a736518f7226544aae1d8ff85008f47c81180ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40980ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40880ffff3cffa073d4889f8bc7cc186e187efe0b33e3bac125257183062f35b7323fe57b4b6cea8080ff80808080ffffffa0310b54073486f56d6029695f082f844f64dc60ef57fd236cdcdaad692c32ed29ff8423d1f20680ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20680ffff3cffa0d9a1ea08047ce46047f48013dc66cf639826fdcf07d5515fe21d9479322feed38080ff80808080ffffffa00ce782ab0b98db845575fc4cbdaacf112bebadb9e994ba02c1a35767e372ec8cff8400b0585b80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585b80ffff3cffa096f36b29ce9407037885b2e37b3d20747e96114a1c2f18c84fcc3f9c011ee8ec8080ff80808080ffffffa0f1a08cac8c77c6831b63a22ae8b1c5233a130914621aa01fb2c0225cc3b159d1ff8447a3e41080ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20780ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20980ffff3cffa07a0a01844a3717f072afe60c717ce96dd775230e66ae40d912d22d81a0cbd3e98080ff80808080ffffffa0eb85b90b83ddd11f2e38e03d8d13d515bf96f148248ac0f046dc59d362687f7fff8402c1616380ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b280ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b180ffff3cffa037f1cdcf27388666cb29641b0975959f855d5ba307d228885bdcdff55741c1468080ff80808080ffffffa093c34c18377e8da4768b6ec1bcee8f2a2ae509052bb09d9860caf8b4ce8cdb8dff8400b0585e80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585e80ffff3cffa09a99fa1e340fae3cd3918fcee9250a920467deac56b458841084a6d76d74ff038080ff80808080ffffffa063ff41e3978599889274a4a75835009d7342f689da6d5a7d69c7a53a5aa5158aff8400b0585880ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585880ffff3cffa07c28f5aec8b9baedb82a30af0abf46cd8e20e17d3b6d8af82ab14e30b46ca6b48080ff80808080ffffffa0e774521ad0c9374d09022ced698d77259b2e00ce007595b3f2ec854ab7f59187ff840160b0b980ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585d80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585c80ffff3cffa0c42d9538e758380c8e2847d21bf048adf022e059b6773741aefcab7d28a8ebcd8080ff80808080ffffffa08f26784ac66b051f49f508426f1b39c752787ddb0c2e63686db65fac5f1e8fa5ff8447a3e41180ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20980ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20880ffff3cffa071e083852182bad1ae20db05bd69a2e69ebdcad36c02c6ca4d5a9477c287b61d8080ff80808080ffffffa0343610bcf643d411a74070e35319fd1cf9de23c4320b1b1407917916b84677ffff840160b0b580ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585b80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585a80ffff3cffa069820e1e7eaf49836b274339a6105cf2b77c7c93eb450d3295c1ef70836fa8b78080ff80808080ffffffa0288df15fd1729353d5ab22196ddf1bb1d5cdbbff5accbe09595f00e872571abfff8423d1f20280ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20280ffff3cffa0c3b849926999f7124505e5298d24ec7df8c852c2e30ae5b56262a20b71bd43568080ff80808080ffffffa042eb4571934422503657bdb60d9610e4ccc00168258b70f1b62c9b3db6f58dd1ff8400b0585e80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585e80ffff3cffa0296d1685ed08ff21bce278b410fcde79dbc326c4b9fe73519a55f7affabbf2928080ff80808080ffffffa077c08b76ad651d0ea084da5c246a913991efd743059d872588b7626616cda5f0ff8402c1616680ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b280ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b480ffff3cffa06d64031e2f4ab6eb87536f0fe3d98cd82959340881a9cad2303833d371a07d508080ff80808080ffffffa00e5b4737ba7f952428b4a11750db8c20146d7e129ab263bbe6022062fa5421eeff840160b0be80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585e80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586080ffff3cffa015075ead7c29ba2768cb9fbfe0fbec044af7664be7ac8b68ce9e87405e20be4e8080ff80808080ffffffa014ae615f373fb87923bf43436d98355b142255974ab16ec8e51b7e33c893b4eaff8400b0585880ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585880ffff3cffa03695f356734b731b7a4420d2a287b85f374576b54c30cc66aa5ba997df14389a8080ff80808080ffffffa0f7f4fa4e7b3dd6516b8486a4b368573219de8f3adf8ef40c495352abbdb1d000ff840160b0b280ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585880ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585a80ffff3cffa05942a883b29f6f15b9bac721aa37e9b383336699d05474860daca8fe98d8679d8080ff80808080ffffffa0db4f0061b1b7583c3a09f4ef02fbdac586c7764f24dff784841687651ba36eefff8400b0585980ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585980ffff3cffa0ca0ecbabb3236c3816118122e6c9d098512a3c040fdd6602a8ff2e13fe7d630c8080ff80808080ffffffa0ecdf97db4e0900ec16436e04385690a4c0e629319575fec7ebe5695983687b83ff8402c14ea480ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160a75180ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160a75380ffff3cffa088b73f7480982ca845b3d69f0aae70e3a9f2df1619f66de4e818a40f650cc12d8080ff80808080ffffffa0eaf96563b6d3f852c68b930ebd65c41cd7aa325d5a6038fa19e0f247aa987aa2ff8447a3e41080ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20780ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20980ffff3cffa0c515ff1e291d4ecb3cea7dfb50631abd190413146d7389a5cdbfe6c9d4fd17698080ff80808080ffffffa0a1539583fce33bea0c61282958c4c6517c8443923d4677c917b23cfb3fd2252cff8447a3e40a80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8447a3e40a80ffff3cffa047b0acaac5561267e2def44c4e1be9e5f17c5462956ea2258331de5c5a59ff398080ff80808080ffffffa0a95a54f648643a40006deca8ed37dba68671e1883cfeb50879955349b713280eff840160b0c280ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586080ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586280ffff3cffa02517672f465fd06a92edbd50b79d5f55a77acf1e0348274223f7613cec3cd9d38080ff80808080ffffffa0171b6af073e30bffef3b46d4084ff1c937554d233c031016ea637693cbf177ecff8400b0585980ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585980ffff3cffa08da87f18886875284f059d6593c1a79131cfd456a6accdb8017af7fa745dba418080ff80808080ffffffa013e311405b030daff210a3fd0cf61641f9100e04e42d3b4ba4aa9b09f14408bcff8447a3e40880ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20380ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20580ffff3cffa00e7101d5280ae3f87aa2ba6b362fadfcca7ed1414430d14e5056d01b303885138080ff80808080ffffffa0e8fb5e592201b1c248ae909241296454b5d91f3e5c6954aecb2f2c071d321530ff840160b0bc80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585d80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585f80ffff3cffa0f7e5be1f403e422d87f1eaff0edbca17ac5ffea8bb712d8f740834bf6d33c8c48080ff80808080ffffffa094d299be40c2e9cb53555042977b819ae6432e54e04317f5e0c496001538ad90ff8400b0585e80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585e80ffff3cffa02ad0bf957e7428f065417162ba4e71292c7bc59a64aa624590c27e2e6ca9d99d8080ff80808080ffffffa0e185bfd8ee0f1081c95f167d05a5a717cdac71f6f6c65be9826b01248cf570b3ff840160b0bc80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff840160b0bc80ffff3cffa020b8d653d7520495c37a87d048e0721b06ac0646809aaea486542ff9ebebdae98080ff80808080ffffffa004edbdc93659ce82d53918397d19b31c076925235cfb34e7530e89f5ec2038f5ff840160b0bb80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585e80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585d80ffff3cffa0c11a22777604ce03ede32dd1df236c451c828edf10cd03b2db12c63476e703398080ff80808080ffffffa0686c13b1e03c41912c4ea59c4c3af9f548973ddbeb5430b77285bf87455b71f3ff840160b0c280ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586080ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586280ffff3cffa09af3c6a83378ac9bcda35ebad665b89f62846ace550296a77b6db3b3e37dab3c8080ff80808080ffffffa0b7d3014d4ee4ee9e73659fe99391e953822732d532ffc7658dd61fb821564c25ff8400b0585c80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585c80ffff3cffa0b3cf833ecae038d234fe2832f695b5d2a717883af54f21b73d6ea2201d2aeee18080ff80808080ffffffa0c264912fc063869af9db409d1d4fbba9e6367f13418b42ae28933c7ad47eb029ff840160b0ba80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585c80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585e80ffff3cffa046cc6f4db33693a7de7cadad5804840c2bcced3d292ffa886307100439f821138080ff80808080ffffffa0d910c89d46c5c487dc32c19acf8f6e9121028dbf75379239aa2ee1718f79d67fff8400b0585d80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585d80ffff3cffa069963f05f77155dd2287d9a4ec55bb20e2dc56147e8e5daf926ca585f132a9038080ff80808080ffffffa025a6e1880ea066b6822b04e7bce11c8179e639aabe3387763a845bdf67e81d6cff840160b0bd80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585f80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585e80ffff3cffa080d6ef94d940ff30b370bc375981292190bb270c0b06f4299c9ada8f07d48d768080ff80808080ffffffa0e6c74155a4ca029c8abfe836e62574596052edf6966b22e66a1bf477ca6bd508ff8447a3e40680ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20280ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20480ffff3cffa092ca355c098dc6be8496791dfc053a575844be19a5fe2451686760836012457b8080ff80808080ffffffa0b2feca201441c4ed9c36bd7e9490eb53ac3fe69d97b3fbdc4f23000c8307c44eff8400b0585a80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585a80ffff3cffa04dfa8c31e13b81a82615cb8d0121b252cc7e05c0502a2c219a2d6ba45bfee8678080ff80808080ffffffa0713b026ceff79a392c890184e432ef668c7be91d7e060b6ba51cc95fbdd82d15ff83582c2e80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff83582c2e80ffff3cffa0a4b99ca299db344c1d896caa2a3347b986493301e7a14061e0433ac9bbd808ed8080ff80808080ffffffa0cc36785cddf6a9b49b2f5a64b479d24c621934dd824c8fb2f553ad863beb626fff840160b0c880ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586380ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586580ffff3cffa09cce3216bcc8311dd63d631d901be65100687471e397d8e3c69041da3b7d8f338080ff80808080ffffffa0b07b348bfaaa6dd540fba22239867ff86dd45dcd1c38a9cd156b8988e221db53ff85008f47c81080ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40780ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40980ffff3cffa0b373e5607d45abbc331f30c16d3de0decb910c9f6dd353636ef4b800eb1397f88080ff80808080ffffffa0fcca60a7d3f3b6b95e90724e8df412c4ea26e94341b94980f791740e9b7c16aeff85008f47c81280ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40880ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40a80ffff3cffa05610fa63f79f3899561ce6fd0ef0fb5e844bd706ec8ce2142dd1a6a0bbdd6d7b8080ff80808080ffffffa01e2b0ff38327008da1b94e5a4c85c80cb4962229bd24ae0c996b89d0891c8cfbff8423d1f20380ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20380ffff3cffa0f5f56203a9bcbd3c4b0101a8bcfcec5bed8330986c4be3af3f37c55f6759d3dd8080ff80808080ffffffa0d6b377ee5058b9311ee176b7c7a12e9da1baafd1a0b0368f701cf58cea0a9ad0ff8400b0585e80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585e80ffff3cffa0b9065d43e5d781df82fb0a123338379a747b8496e0c1ed1a7936fb51e678a0518080ff80808080ffffffa0e6c74155a4ca029c8abfe836e62574596052edf6966b22e66a1bf477ca6bd508ff8447a3e40780ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20480ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20380ffff3cffa09d3071ef4caf9d6014b703c976e83aad18ebd517da2b405bd0c31337a1d8a45d8080ff80808080ffffffa0e034187cfdd793a7913d11ad92b75e778468244d6b858216252f28d9d2063ab4ff840160b0b480ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585980ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585b80ffff3cffa01e9ff724fb636855f0495c2a2641c69af9c1aef3aea01c8a847ddaaa8e42be8a8080ff80808080ffffffa08f79f57f4a3c0a9f7b2a5afb5ecd0d8e150e34127e8fe51fe9ac87d37b2f2573ff840160b0b580ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585b80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585a80ffff3cffa06dc3c26862d671774a488f0b5daa75aec31ad1260fa51bdadef25388929ee72c8080ff80808080ffffffa0328b724803200e39a318a9f2cf3060230975113a0eb9166ee63c0376c450e120ff8400b0585d80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585d80ffff3cffa093901f3e5ca76590797d28971972cfb9eb56ba88300cc632917d2e15af5302398080ff80808080ffffffa0811b18174d57790099533377bfe85307da01d8e8cbed6be501f605dff67d154dff8400b0585980ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585980ffff3cffa0cd547f444be66ec8754891dbec23f579c2f20b7dac4bf2fa250b720a1284c6a98080ff80808080ffffffa00d79c561daa27c71cb8bcec80bba890e2234adf59530a9f66864c04905147578ff840160b0c680ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586280ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586480ffff3cffa029068cee7c976054ca7f19e8b9fdf0eb7c4b8263e833f34034257de4727a9fd68080ff80808080ffffffa094a558c0406255a99c46dc582dde983ac9a5cc902a3363d4aa48ecdfe9e541b6ff840160b0c480ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586180ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586380ffff3cffa0caffd2f3fdeedb36ec80e5a00528d3d6854e8cb7ed7c59951580201bec204e2a8080ff80808080ffffffa074794f22b530cd241bb95e4912bfcd3f7b4a2a0b50b9cd4b357a6a136a50f7f4ff8400b0586080ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0586080ffff3cffa08972f5a64708e419717aea0ae47456a38a2e391f97bc6d3b1707424b30ecb2dd8080ff80808080ffffffa09697501e1ed74464058c7d7f19d5069c9dae60ae11938a813f73d4fab712cbd3ff8423d1f20380ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20380ffff3cffa00beccec0a7ba0b4da33707fd3ab6a623062242fdfb60eac169ee2132569099308080ff80808080ffffffa0a436cef9567905f9446e49a38516c63ce2bc8fe292d76277a77f7983c1f742bcff8402c1617280ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b880ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0ba80ffff3cffa0c1302cac4191b3f19ad45f082ba22c069058515eb50752eae66c5a8d17a3ea638080ff80808080ffffffa0f5c3e789b7fec3ae36f96b0789ed96e00a01fe261f3894ae825ab8e442e5dc72ff840160b0c980ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586580ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586480ffff3cffa0b7b475b79b221dbb7e26fee50861853b45a5de55a73c0076c4726018e42a18f18080ff80808080ffffffa04f2dd6e9f0bf5635b2d1404f616074179f4dd4207645b8a19a9fce85cba087f0ff840160b0bc80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585d80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585f80ffff3cffa08cbf574a4619f66778b458256db3365a77e0f30194f316ff1aeb5506e087407d8080ff80808080ffffffa027b74d6c39a795c00cabd0c14f83d266e7b7cba2ba15fd35e0068bbc21ca80c4ff8400b0585d80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585d80ffff3cffa0e4256982b783149b4239f11cf61d05bcf1db576b549e230a13e0eed9057283bc8080ff80808080ffffffa02e8c50bb9e782b51c9a35de78a8072fe86104a5c4a13cb96c6a0d17efc7b28c4ff8400b0585c80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585c80ffff3cffa0488f71548430588e9ce0712e23013bab34e94da3cf8602c437a3eaaffae792c88080ff80808080ffffffa0b8d91dcede036960b85020fc3e02fa35aa49c628db8df8e5fa72be6df9964d5fff8447a3e40d80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20780ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20680ffff3cffa0e58d687c4a054d8048b9fe71b5f72597308afd21e44006abf6644c38cc7248f08080ff80808080ffffffa0cb5bff71769a8f89859fff7a75adba96a3a897afd543cc13cdb0c08a2eaace43ff840160b0c980ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586580ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586480ffff3cffa0bdf7fde5d7e054221e9bdc81606ed2c88348ae39991b37c6acfc2f2fb01458db8080ff80808080ffffffa080d0b30f28e8975fae454be9c52671278785be0125cb74b4884f045f8fb7a747ff8400b0585e80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585e80ffff3cffa0780e34027a99a1f93ae11af5c56efc19e4b0abdedebd0336b256eacfb535b3ca8080ff80808080ffffffa004caabecb1375febe89e593ddb25099c615e59543fc4d78b35bec2791ed891d7ff8423d1f20480ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20480ffff3cffa0849bb4064ff5eee9717a2837e7e992b32e3fa63e862fcb6a34e0777a689d661f8080ff80808080ffffffa069fa436d8c2bf61eff4de1d874271591df21ef64b5c058e827c56a6304d3d2ceff8402c1616880ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b380ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b580ffff3cffa0cff0d8c92386dbf4d4610f5d89d21cfd3656e80251e5ab438de1e2cd1587307b8080ff80808080ffffffa090148940d10bd6564518e37b3ebd8f2cc6e1919736e0fad653873ad5b1bf3d82ff8402c14ea880ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160a75380ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160a75580ffff3cffa02e106d57df7e568e94a1b9f37d501c3ad413690e1cd7b74e613fceaa780b7bbb8080ff80808080ffffffa0f7b9e4c6f508b969c0a984c4c064b95f7c9bac54d886bfefb46c6e42e07c18b2ff851d1a94a1ff80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff850e8d4a510080ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff850e8d4a50ff80ffff3cffa09e6f1421a64a8a783d750fad113daaaccdd396028cf1102e235b4a103460acfe8080ff80808080ffffffa045036d56600acb1eacd02f71b78264a53b290139c796d73ced92300facb912f1ff8400b0585980ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585980ffff3cffa050f73656301ba6514227d5b9ba48110c809c75b3b4073dd96688a9ce8db5a7358080ff80808080ffffffa0f65d0fd4b395a912bb572ad4065620ba4d3d377fcb6fcff1b02c0f4c2ec60da1ff840160b0bc80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff840160b0bc80ffff3cffa0bddbe2f578ca0ce3b007b135225d1ae0e265cc4c5596502ffc7022112b118ab98080ff80808080ffffffa01d8dddbb1f0966504e1bf562016f22f61db1658e728e59ef72335a2ed0a82d96ff85008f47c80f80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40880ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40780ffff3cffa0f38473e5c9694042c11853c899d35a51037ac3fb6bb7e57690372c1c434cee0a8080ff80808080ffffffa0c1973bfc8710e4930c45b45a01136110b68524067f62c554c22929ae66667916ff840160b0c480ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586180ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586380ffff3cffa0b6d483a89edf652d9a78903663fb54656bf9f691aa82a3da78d443b187dbec3e8080ff80808080ffffffa05955cd1b6c7b6ed4092faf0acc5ec431ee24c4116e08fd44cd1468c24c0d67e7ff8402c14e9c80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160a74d80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160a74f80ffff3cffa02bf2039d2f2bd2c3852178100f8a30b21e954c2de4184ec35417150e7f6c409c8080ff80808080ffffffa03eec429524bd39336def3318629e4f72851b5854758435153f4dfe3cdddbf560ff8400b0585e80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585e80ffff3cffa0c42c6db8a56c43dc24a1c39eebb3cbc23ad7669af958303b8ad874593ee198178080ff80808080ffffffa0fb5a0d6743ec30d6ac9a71e7020b85331bef80d0c905a8c10002e7b252af5ad8ff83582c2e80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff83582c2e80ffff3cffa0c00d4b99226c17efb177cb6f97dc80378107b5c26947d527b39246c460171d938080ff80808080ffffffa0b07b348bfaaa6dd540fba22239867ff86dd45dcd1c38a9cd156b8988e221db53ff85008f47c80f80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40880ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40780ffff3cffa0fa930d27d6774214b5f8e16439272814a953c8479f2bf00e43332bd8ab7502518080ff80808080ffffffa0abea892be3f96b22825a1eff7656931e69b981ea6ba01ec6b38647bd2c4310a2ff8447a3e40a80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20480ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20680ffff3cffa065f83a2a399c8749388887a39f03a033c8b3747a108908641e7917fa211a969c8080ff80808080ffffffa01b45ebdb5caf7ef51cfbc897019d634d4f71f0dfd57689d7aabe7328d83d80eeff8400b0585c80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585c80ffff3cffa0ed528decba65606edb2498d17f48a12d7ebf006a0fd942443b133b8ef1fe25318080ff80808080ffffffa072858c8439183a2da34e35d88444380ab07a3a7a8585a4a42d0883e17f86bd3dff840160b0bf80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586080ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585f80ffff3cffa0e0ca135b7eec1dde96972cf927997da933fc8aa52db9c5f6e501c04cabba99c48080ff80808080ffffffa0701082445dab6b13a83cae8739f395f8e20a6d036d9c7317617612b71d89b61cff8423d1f20280ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20280ffff3cffa0e9f86df487e48a5fc809302d77c1e63d59318a370bf1ad39ac178558035ede958080ff80808080ffffffa00f59c4013c59c7a189de5953a20bbde02967bf044b7a58ee559d5e113ccb6cc2ff8402c1616a80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b480ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b680ffff3cffa0093bb5e5449fac7e429e1156d2ca7d0f2f7aa613f55620368928ff35d1f8e54d8080ff80808080ffffffa096431fb64b9cbf358b157c77f744659c29b3b80e848e147abcf6062d0806e5bcff8402c1617580ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0bb80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0ba80ffff3cffa0ab17266550f9200b23773ac9e3cb3061e79b6630b68e90af50ebcedbfac2ede78080ff80808080ffffffa0dcc8fa156f8989d22a0fd22ac85ab820043c4a327ddd02978de73ea27f864d07ff8423d1f20480ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20480ffff3cffa00d53cf42edfb4a5bf14f78f89bdb4883ba9ffc35adee5fb67f376f83103531a18080ff80808080ffffffa0ecdf97db4e0900ec16436e04385690a4c0e629319575fec7ebe5695983687b83ff8402c14ea380ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160a75280ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160a75180ffff3cffa012b248aae2862d90abde03a16557f07523f22ab6e8ef94150b5cb59c1ac5d1668080ff80808080ffffffa060000c899f629a76d9277e09c88e5d247657832fcf93f262a7385fa8e4278862ff8400b0585980ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585980ffff3cffa08218a019e06721320b3521f66d11423159b5ff3045b34e9489e0ce23920c39bd8080ff80808080ffffffa0ed562ec1e8fb34d7e53d76d01108f51a61d62d0205d4ea9b44a0f8af603bad8bff8402c1616580ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b380ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b280ffff3cffa0fe50b72f3eec66823836eaeabcf51af3daf6c26a7a2417ef1e2b49ae84f69a5e8080ff80808080ffffffa0c929151f3e9c37efb5c5a7138952550f1c53c1b3d2eb295b4afd93df97eab840ff8402c1617280ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b880ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0ba80ffff3cffa0bf6769e8eaed460ae0923381f3f6267055e660e35aa33aa989930bcc7a411a378080ff80808080ffffffa07e6a8b13320fd7724a24ea0a5f151fc220b44fe458310e19013636ad4e6159b1ff8400b0585a80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585a80ffff3cffa0a24d3d4cd277f5a3f56307fcefc2d3c7cb800f11d652c93367b22ea87701f7628080ff80808080ffffffa0dfc409b290f2fad91334341f215006f7520c2264bbcb0911c39e1e246396d051ff83582c2d80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff83582c2d80ffff3cffa0305dbb55da3b2949f8939cf87a9d7242b9af1f39e7ac250182d1adbc4e23185e8080ff80808080ffffffa0f9696f37b02165cbda3679cb90c6cab2e288327bbe0303f866f0d6556119e5d9ff85008f47c80b80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40680ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40580ffff3cffa0bd079e1ee8f4969ee9111de841c5817ec09bd92aaf80611a189fac1641cf8d1a8080ff80808080ffffffa0f6b30d35deef5ab9503d1a1bcb8f925ab1ab696c1b44b74136065bfa28ecb414ff840160b0c580ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586380ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586280ffff3cffa0f706128ab3f7c7b6e99604efaa1bc5020e80d4326dfc4531370d7e82c66b0de28080ff80808080ffffffa0d4f5edffd3d87bf6d422549ba6cfe1e5899cecb4ec4b4fff1c9d7e30062d1f12ff83582c2e80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff83582c2e80ffff3cffa054f5e652e955b8b3e574935ddb05f8d30d00a2a79d8c84a903c13139d859ea968080ff80808080ffffffa01be3b16b78bd104b7041e0b5b5dd8c78530a75e25c1e8203c27792a7ddaeb898ff8400b0585c80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585c80ffff3cffa07a4a0e1fb4e7aab302aaa8ab77189ce4d3b19541a57cc956f5171fbb4d81edf98080ff80808080ffffffa0ca133f82e1486028f4b0d009874a7f8b21d9406e919b7bb47a9da1f668a526deff8447a3e40680ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20280ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20480ffff3cffa0447f9cadb29e315913e25fc18152165b0ea7893d19dcf3f309910917e15aee008080ff80808080ffffffa0ffbd848c0b66c50dbab2122f1b15d1e9771b7654b33d379345b194ac30478293ff8400b0585b80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585b80ffff3cffa0e6de55e71d6f3d12998c71194e3f67a612442af04c435b1443ca846400aa8e1d8080ff80808080ffffffa01231b679fca8b6099278fcb76e6abb40bc4bc02a141a4e339afda2e2cf367e09ff840160b0c980ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586580ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586480ffff3cffa01ba4b383434e121ce6da848d34aa7d10af1d218a8923a5823885975248bb13ba8080ff80808080ffffffa0a01dc2e6b59ceb221acaf0ce1f719acdbe8a1a34f9e1e7d35f03c51ca096fbd8ff850e8d4a510080ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff850746a5287f80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff850746a5288180ffff3cffa0596918f869055a61c70e39efbb3aba08c36c0bd24aa222f1933c2ca8d44952a78080ff80808080ffffffa01bc83e0c24619f0db2d7262964e28676e800a41b3169f07844b4539b8b36cfdaff840160b0b680ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585a80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585c80ffff3cffa0da3a2120d276d345c94ef1dfb83bb73896c8315ed591fa9bf467e4888c403cd88080ff80808080ffffffa09e14b2a2df23d0ea06e65d52a7368a7b760f74119df0473154a704aac5d78bcbff8400b0585980ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585980ffff3cffa0a47934dfbf767c122decb6fe05bef5d756d3c6f65f62e8e6d60c73c30b47bbf78080ff80808080ffffffa0439f11e66c6d5be7f35b47a2d29916fbb621e54735b6da4c3238db2289c16ec7ff8447a3e40a80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20480ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20680ffff3cffa071a79d6350db438dfa9063ced51008fc6ad6f7effbb3be074a6c8b68655f99768080ff80808080ffffffa0b14f1786e14f3832df13ea13496a30f500fafd5e58634651f6c99120d08f9873ff8400b0585c80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585c80ffff3cffa0e890e6cc8f849254d8bf541393b083eebefe0a16813ed8d143bf84da0e842bf68080ff80808080ffffffa0e83094d4e20544cf848813ee1afe8d458d74485ccea2ad99522247a03097d415ff840160b0b980ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585d80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585c80ffff3cffa02c9ea15fc5012ff074eb271a911923ee3326a36d59a5da2518554011f2ad68778080ff80808080ffffffa08ec06aec6e484c71837571320f091925b321d9676952fc4b61e6b4e5ec07493aff8400b0585e80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585e80ffff3cffa0f76fb6c8764a0c28e7cdb729fc49d548d1a9f324894576f8ad616b9bffac65438080ff80808080ffffffa0a7cfdcaa046bdb370c91eb2e08f5d4ab298b1822bad738e73aeae0260a0c7350ff840160b0c680ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586280ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586480ffff3cffa0c161de0e48f6cae3011c17813b6f120d5f952051c049c2bf94bbee1b84710a5b8080ff80808080ffffffa018912c28d04dd493a8c42e1de4a03ae18b06c73425639672cca992ffe2e25787ff8402c14e9880ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160a74b80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160a74d80ffff3cffa0fb3cd61d6c4bf6bde020cbbab5d466935167bfbc477b224d1407d890d31f404d8080ff80808080ffffffa0c400d4cd2a513bebbfeaeffe94acc6befa6ba917b0e61ce6990cfbeeae310cd6ff8447a3e40a80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20480ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20680ffff3cffa0787a7157acb97a27d4d7a1514eba0887eb688bab42608c73da6652563b5f8d6f8080ff80808080ffffffa0fed732e29cd0ee7c84d99d7a3cf87880fbd3dc43eb038b1e7060351cc4226cdcff8400b0585b80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585b80ffff3cffa0c6f0704147a501ead4d1abe957a6d624a54ef2c5aee0ceaf503f1c848c9ce98b8080ff80808080ffffffa0a8d262c23d4a033d920d8b4f3de8ca84e13f48d72b50e1ef1b037e7e83ca22abff8400b0585880ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585880ffff3cffa0b1e59f62e99d6012933ec60fca06cb7c41cdcc540f6836fbca8420834a6765158080ff80808080ffffffa08eb822d03720b59d5d8b9629e0f777038dec18dd16d67dff795a2239080da8a0ff8447a3e40c80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20580ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20780ffff3cffa0641fa3c5acab642131a56ec31f4abd9bdec30bb32dc65255a9c92dab937c3b268080ff80808080ffffffa0ff55447a95be9e6c20e51dc55f1c4e9f2983fde80c602ba9bdcd383e3d270a37ff85008f47c80b80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40680ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40580ffff3cffa03a015a455fbb8f6a9fe99db942e3414879487ce8f64aae467afe3e80356ba5d58080ff80808080ffffffa03365470197804e8b61d41d662e76823f1c1f3c0d784d9bb62f9f405ba81808a4ff8400b0585a80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585a80ffff3cffa0f913fcd1452fb5ad2fca86cf9dda384f9daf9f9cebe9b93be2b605e60e3d0d618080ff80808080ffffffa0bcf7ac9ce2aa1d995aa7018ff09c125b25ed8b7117dbcf4f1a2ef163bcfe6695ff8447a3e40e80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20680ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20880ffff3cffa069d678495cfa2597dee3074049e8591683980c7d03cbd20444aac155f41bfec28080ff80808080ffffffa09727a0d2dfd420e50aca68c8dc661b600e1a97fb61d89f1ae8f254edee7b2766ff8400b0586080ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0586080ffff3cffa039975e939e0ef9d55f0ce4670a73b9747c264badc1e5cc7c42fd69268ab3687d8080ff80808080ffffffa0304322cc7b535a72f08d2d1055f6ee554e6a329ff528802946de6b72d548adc3ff8447a3e40a80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20480ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20680ffff3cffa0b0a170fe20a74d78da62ede99c77e19f4bdd9e2bb455d144c6a1dbc3b0bdf6898080ff80808080ffffffa0be517dac7174b4586e73dfd368663911ee2ab28111dafad104a9ab908aa6e230ff8400b0585b80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585b80ffff3cffa052d6abf03300765bf89ac6fd4ed1a09d3f1ce65405dc432b9da7cff8ec9b11cc8080ff80808080ffffffa0e90de870990f9122523a9494e22faa266a1620a04636bfb543368415802b9028ff83582c2c80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff83582c2c80ffff3cffa0bfb401668a12f0bdfc409d4463ee7881c84e426a50b3c2edbebc45787159d4f88080ff80808080ffffffa0f6b30d35deef5ab9503d1a1bcb8f925ab1ab696c1b44b74136065bfa28ecb414ff840160b0c380ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586280ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586180ffff3cffa08600a6c840ef6500bf5c4a9afbe10ae1eb13c75b2ced23db352f96a9e82289c28080ff80808080ffffffa05a4c4df59f4573165f115f7c17a0176467d4a0b6ec3d5a28936ac5825703d767ff8402c1616f80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b880ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b780ffff3cffa00c3a1b58314cc0057f5558590a6cd2a95114ea3cdc4fe8fdf965ca3aad36e8e98080ff80808080ffffffa058cd547bf98be402e820acfa1ff795f2df6f35b7879d4002d560ccc654e89bc8ff8402c1617780ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0bc80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0bb80ffff3cffa07e4f22fa64f1602b0addefe5f6a216a189e4041005b7901e8ab373cd35bf14878080ff80808080ffffffa03e6252e765eff7c1fcbc60a95f25baad541e834965c06aac7800472e0ec69133ff8400b0585980ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585980ffff3cffa0c01a4a71408b009c9d0bc048b9857684f04f865b320668ae20b06344f7d65f868080ff80808080ffffffa04631132d2c6c6bdc36f84d07807a8b3689646b7f8756dd62abf90ae09de0806fff8400b0585c80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585c80ffff3cffa074f9e549698fdfa59936f89d827ba224c1e9d58835da795c3d58f84b0e242a3e8080ff80808080ffffffa04ded3078963282c2b017519c6bd1680f9ac404431bd38bac3905bf6116ddf284ff85008f47c80b80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40680ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40580ffff3cffa0c2dd8a87614402b29ec10cad9ee4742db67af6cd17c6b3e5fcdeabd7a862f2458080ff80808080ffffffa096431fb64b9cbf358b157c77f744659c29b3b80e848e147abcf6062d0806e5bcff8402c1617480ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b980ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0bb80ffff3cffa0b8661f671eaa8c8e939bc7ca31ea76ca429a220586d7738ec0dadcf1d71464b28080ff80808080ffffffa06e0ac61695600226a12c6d0a45bd4c55ecaf4b1e17195467b21c78600d8c4e7dff8400b0585e80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585e80ffff3cffa0190d18c52e64f910553a5062da4013aa903bc641509a86d21a8ee4ed0d2416ed8080ff80808080ffffffa0698863aeda4e5aa76a86c57ea2b24bb61b526549876447c5b7d7423be031481aff8402c1617180ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b980ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b880ffff3cffa015c0ef75f019c19c798b7191124c39c2d7760c258449138174f0757cf8d2d6f28080ff80808080ffffffa0d3aee2b0bb51b5ec746cced743a9d35343e8cc7d91fd5400ad6490cd95a668ecff83582c2d80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff83582c2d80ffff3cffa0d4178daf452ea9c5de55fa6a6777de37787c22d777896bc69c8f16b10749471d8080ff80808080ffffffa079eafe56a9ffe9aec9e1c9a8cb2479d98dc6d64a2a51abdc96812dc4e1cea58cff8447a3e40c80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20580ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20780ffff3cffa01e6d668f513d5bc74b3048d27cbfaf8f4383a0c1187059a99bd781437cd842da8080ff80808080ffffffa0fe6a260aae06c22d2bbc48a7ea13ca4794e5948bed2db66887e1e6c19b4d8756ff8447a3e40c80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20580ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20780ffff3cffa0b97aebb5fa4c0aa7571956ae56bfbfa31dbc2d589b625b0feadb984c613e477a8080ff80808080ffffffa0f0e8cba1c5fe680a386c928473dbbdf898c0f80d8459e4c64bf38a3a7513fa78ff8400b0585d80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585d80ffff3cffa0294249da887e19992871083d843002044cb1f089f67eb881298c1509d8d2f4378080ff80808080ffffffa0ba1ffa93e3ffbf11b0c0b3a4553c0eaa8fe8a7a4079896e0821ed00e490ac18fff8447a3e41180ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20980ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20880ffff3cffa0266dc7f210a336186cabe6d547d16982bdff514e30cff7b235ffe3af363b756a8080ff80808080ffffffa0aa710372b3ebdacb6957a816d4b68f78d3eac19473d0f501488140d0ba0a2e71ff8447a3e40980ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8447a3e40980ffff3cffa0a58e99b897e41b6f0a6e26f9c1f03a9d1184d002d9be2a053538d1307042765f8080ff80808080ffffffa08a51b1f077c57a6ec7c6151183fca41e4618b74c8b0922524395412236da39feff8447a3e40e80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20680ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20880ffff3cffa02a16584e4e464e4b93595f9e8adbf1f40bc27e08ef15ba521b18fa95b908fa178080ff80808080ffffffa070c49a93454a72dd1ae467b5cdc1b0dcaeeffb120e67c866489da2e58944a376ff840160b0bd80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585f80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585e80ffff3cffa054f5ae09003f141bd59525b9ef7fccf12d19ad010f57361d27674d7b99b193ae8080ff80808080ffffffa0f96e90c51faaf4d760948ef829adcdf9582859ed558932a106330f0375b883cfff8400b0585f80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585f80ffff3cffa08450e463e4822ea8647fa69df92370bcfe6dca62d4156e59646e8248692f9ebf8080ff80808080ffffffa088ab3d5709bca5de64e92392dc5e491caa8eb708e68b99e28a27ee41583a26aaff8423d1f20480ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20480ffff3cffa0467c76369105e2215895a6146b460d397a7c782fe735023eb5042f295d63e6828080ff80808080ffffffa0f880e075500dba69409eddfc251fa4ec4ddd025f326cbe14f9083882a7abdaa1ff8447a3e40480ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20180ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20380ffff3cffa0ed86b89cb7966299de397b2bb423366b0a99d29b7e666a78c5e7cd431b3ecbb68080ff80808080ffffffa072ceb8e2eb1811953798158c3d989d825f78b396daa08bed2981c28d6da2e0e2ff840160b0b780ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585c80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585b80ffff3cffa00336ed07d69a78092a99d7f729047580d5661ecb9c32c6155eb75aed2837ac408080ff80808080ffffffa069ef351bd8912e4822f9e0bafdf224778968e5901a97fd1b342f0c7f4018e900ff8400b0585b80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585b80ffff3cffa073ccf4e89352c1a01b15ffd23721822ee8886795c63f404905aabf14121d91988080ff80808080ffffffa0511c34a82b3df6458f3bbfbcc76554d32c17129964e16a085d1f4995caec430bff8423d1f20580ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20580ffff3cffa0147c6589da044dd4430c1911e70663a7c8f1db2f3de93975cc5eecfb905c26c48080ff80808080ffffffa01651d7460018e0b4c32dd1745b35ba80f5b8002bc21d3f39339d8c83412ab80fff83582c2f80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff83582c2f80ffff3cffa040c73a614e3655694427f8079397fb81253ebadf2b95e34557df225b0a7e2ed28080ff80808080ffffffa03b16c9e966ee02504a74a6f34a6445385343682ed79792fb3efaf11c033cca5eff8400b0585b80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585b80ffff3cffa04f5162fea15fb14e1839f7e01518c90185e80a4aa5d6bd6a6b740231000c901b8080ff80808080ffffffa0c53a246fee97129c653004ed22b38062dfe9d6e3ba6443a031124633b0fa66faff8400b0585980ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585980ffff3cffa0896563682be22f3b5928ee85713546f42af52f08f490641123519acc77ee92df8080ff80808080ffffffa02f51dd89503e7692a5b90bfa15fbff81bdb6a280f0dae4df1c11c5133e9b9ea3ff8400b0585d80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585d80ffff3cffa04a7c480a72a7e924aa7d1fbe678d01588dae61a1e4e631c1c84e5c1a92490d988080ff80808080ffffffa0354f78f0a664cfe0b411af56ad0a14a6e701f3246f1a8d3f02a558fdc81fee36ff83582c2d80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff83582c2d80ffff3cffa0ec84117cb6f6a4f9f685bbd65de6f57729673bab5815756463efa68ab3fa860d8080ff80808080ffffffa0aa8e2b75e24fd416f72ab330c392a49f02e770c4cf26f56695c992be76d49cdcff8400b0585d80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585d80ffff3cffa0f591ef90ec65f2d9009c85ea1b216e18897ed4d28410bde179de836046324e5d8080ff80808080ffffffa0d4f9ac2cd2c8c6bfe36821df7cb888f1f5cbbeecbd991da5d9080ef86111597eff8400b0585980ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585980ffff3cffa0092d734f755fe961921241c4465a802ae0d3574fa32c2a68fe992f3b4cb36b888080ff80808080ffffffa04574c5b6015e07323c4a4f9c16d3c685ff0425a0d151550851836d532a0cc0bbff8400b0586080ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0586080ffff3cffa0638c2f2faaecd715e948d1d05554af3d357950cec8a937c1e7aa980a6d6ae0558080ff80808080ffffffa0e45a2947f03c62287b9d597019b2bac123a1c98d0a2ad91d5c68b6a184c9272fff8400b0585f80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585f80ffff3cffa0c4103af944705c169b2bcc450eae167ad4fb1f6093fe699ed6abb6ddccc68f8d8080ff80808080ffffffa0b3a2d8084a5eabec5719834f2a362c7c3a8ac0bbc8c70dac2be2e0e1b84e0910ff8400b0585c80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585c80ffff3cffa024b62842598d4c3b4b088dcfbf79650d8b3c9689cea85154e00af0babcb1515a8080ff80808080ffffffa0b1108346d500ed6249b1a63d71ebf6e78254532c2888b5889b7aa05019684442ff8400b0585d80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585d80ffff3cffa0a2bd6d4ff8348fda1fed50d66466158ce85a2541487b3064f4a71c3fedf7bf3f8080ff80808080ffffffa0014bc10ac158d5816ff5c594d8fb0306e731d37123cfd2b9c3176928fe69af9cff8400b0585980ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585980ffff3cffa0fa7adffd3a8ec91ddc13e2a7a788c314715a12563e10765254dc7d2465a868a58080ff80808080ffffffa0a549330def5c7e88ec2102716b7fb5580c7903ed8672c0237401b753c3fd6eb4ff8400b0585b80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585b80ffff3cffa07b43353f495585cf90a7151fbb2132c015d4b932bece7e449b0f818f76a4a10d8080ff80808080ffffffa0f25ece79d5f732a761ede397e561e6158d813dec430ac8cccccd6c5c1f869edbff8400b0585e80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585e80ffff3cffa0a175565be7fa96f71f3b9d8290cfc67538354a6e1fac3aa99d428f1d4bfef7dc8080ff80808080ffffffa0bc7bcbb15da96f354b73f48a8800500219c865b7d9f9e4ac1c1bc9b0756a61faff8400b0585880ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585880ffff3cffa0b84486d2549db9a0c9acd36f70a45d63bf78fb80b5ab73683f835c1e1a6deea28080ff80808080ffffffa0428d291dcbea8cdd94853b1e24ee9918be2ae4f5e902b4c7ebd798c695f863b1ff8423d1f20580ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20580ffff3cffa0ed7913262e435119220a01414cf6a300b77c28568d22479f9e31851dd0255b9a8080ff80808080ffffffa0556706ab8113d62e80304be3b519e8c50b6908d4efbe7208b60c56f3fe412bcbff8400b0585c80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585c80ffff3cffa03d4fae32f2516af6c2c3d3f09308c74921a330be979fbf2375455a3e3ad285f38080ff80808080ffffffa0aed493fa152db15e3306f7a0c31c8a2c779e9181df6173d747001459db9469f4ff8400b0585b80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585b80ffff3cffa0c4f6f5ab53afb6207ec45c33e4c887872a3df51cfa9dacfc0438b561221ac82d8080ff80808080ffffffa0dd6b3a1c11142870d96a7cb33f78fca24c9b4834e9de22da05e7b7e5fdad0464ff8423d1f20580ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20580ffff3cffa01d49ebbf058715ffcdf69b04a82ba25b1c8b58e8f1a14697b15196725c49d64f8080ff80808080ffffffa0f0bf69f9a2435f08117b1eb05c1f763bce2adc1ae7e9f729ae8e21f9421f3decff8400b0585880ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585880ffff3cffa0eb788e2d138d23552e82532bb4e61782109d4d9189f5ffa3fc338d81e797c2f58080ff80808080ffffffa03435d4af1fd3df675cf897224b674722fa4ff8f6de1329ffecf70d4919184f00ff8400b0585b80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585b80ffff3cffa0ec72a58055b8f76b5fbc82ec70d0fd886d6644297f27ee5d91299f6bfdb70dc98080ff80808080ffffffa079a0da7fe8cd264e2869e2c6602728b0e5fe2312e590bb85a0b3056329d4382aff8400b0585980ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585980ffff3cffa05869276d52c244a07dc29a8dafd65d3eb8f3fe61edaab4c3bb95f33a73c157e18080ff80808080ffffffa0f1b000e5dacd2f11bbc32d28ad319f5b905b88ab43c8689fe3becc23512a4b81ff8400b0585c80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585c80ffff3cffa00dd097521a23424e7977dc7896b7016589a3fed67d0d7d48df7b1f0a99fa0dfd8080ff80808080ffffffa0635b4fb1c2646821ca4c44176db18c3be13f30d45b7b8f97975662c6d6432638ff83582c2d80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff83582c2d80ffff3cffa01d7665c23a0a3773afdcda0463013b9f640893b007caa3fab19134c10265e1768080ff80808080ffffffa0c0c03161af9b1eb0ffc1b615aab56803d4238b719daa3bbc7c69d7337277129aff8423d1f20480ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20480ffff3cffa042270040eb9fe61fff747b753e3c9497c7248a1ab3f12224def3802fc6c2f2c58080ff80808080ffffffa073406ff9eccc0f6e1ce47212ea2fdb62a9155784fd757f4b5c804621c0076a1bff8400b0585f80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585f80ffff3cffa04a18cea46d1f60596d94e734efa18066f5c3b83c63867e48365d5d6466f6a1d08080ff80808080ffffffa0efbed0793e9bb2dd5e1b2ea74972d987615b468fbc47310cb3c7a09873cf3c1eff8423d1f20280ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20280ffff3cffa0d50341c6b01e84836402cae1f168b7d478e10ff332e380a74ef5b588ab2102678080ff80808080ffffffa01bdbfb38e623f6a6490c98e3a5549824ad9d8176a7cf0659db02598197a70bc1ff8400b0585f80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585f80ffff3cffa08e5e91ee485b51185a3b14ed4c1e70df19f6fac388ae2afbf8f3c1121d7c23728080ff80808080ffffffa057ceb15ba5355bb361404c8cc9d39f99ca3323eeac38cb955b53efbf3df06da1ff83582c2f80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff83582c2f80ffff3cffa06e668013f2f78f1dc4e0af7606ce7f9ffff346d4721e3b52d204ce846b163be58080ff80808080ffffffa0f315f3f7ee4d2d627d4d9ac002a59b2648074f8d9c1c680c3c6c3cb22f2d87d9ff83582c2f80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff83582c2f80ffff3cffa099ed622700dda213393d22cf5aef78e003fd2d08c3a841d5c41514d61d2f1fec8080ff80808080ffffffa080d40c34a51afe3553a3927f6f074fe58be29d8a6c8a8c3a56671c968ba0410eff8400b0585a80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585a80ffff3cffa06604ba0181fe1292954a5e974ea1bcbea92293b5ed8c6bf22de1c0673c45c4618080ff80808080ffffffa0b3f7ce079bb92c37163512fcf511b40319e0f03e0125a6a9a6723ff1dd0a39a4ff8400b0585980ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585980ffff3cffa006e5384df8c4e5dc6f0904019d8e75a4473dfd2efb10fcca534439ba6999dce18080ff80808080ffffffa057b08eda1ef67ab4cb61365564421be741c5a27af1f50019cfa5c992e2f27085ff8400b0585c80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585c80ffff3cffa06622342ca1690ee841e71e72b31f10bde52d1a6f6a326438bfaeb7df69ce85ac8080ff80808080ffffffa0917f98c2f876421014e8520d3a4304fcdb344e6dc02593c487f67671497b8336ff8400b0585b80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585b80ffff3cffa0b6f00404dfe46fd322b14792ccfa34934273b3a8ca5a43894991e7810021a17b8080ff80808080ffffffa05fc46097162c2fd340997ff97786e3be596db7c6928b01cf6f6407b493b6bdbbff8400b0586080ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0586080ffff3cffa0be02210752008d934b6bfff8d16477df8f87f19fa62fc40ae4f64a7d6c0392f68080ff80808080ffffffa087cb34ba5e62e28196ad8be49be6fc9b5787c5046c08cdd9b2179401e26c61bfff8400b0585c80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585c80ffff3cffa0cefbfcd8835de00a09b61ecda7f0d5aee3d294a2698fe5063d89e5c399e828f78080ff80808080ffffffa03118947b60deedf91fab9cc093f2ed6f10ca52a178318b8e1028bde1c42c339fff8400b0585a80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585a80ffff3cffa0af25d49cc0aa9b074e937b8d7333ab4e82cbed22a47ab928cec2cb4bce331bae8080ff80808080ffffffa0c2496c083038dbe6477addb05a7dd43a97158bd7adaa18f54375474b588c9009ff8400b0585f80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585f80ffff3cffa009ec8ae49e1a39bf0f3248aad24ac8d13df2a31ed86711f7308ba43feb88707b8080ff80808080ffffffa07b42ae908371b23fcbd469965e333fb3ca7d0bf6ccf218461aa36aa12d2091e0ff83582c2c80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff83582c2c80ffff3cffa0c2591edb3d1ceba2e6d18c1d85b45aff48baed24557f388c5b6044c9536bb70e8080ff80808080ffffffa08529131055a687b68626ddb2a092f97462354770148030c2045fea714d9290d2ff8423d1f20580ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20580ffff3cffa0648bbd7b4e963a8189fe9e20e4b97767d72c1dafe6739c38b1bdb723dd3dd7eb8080ff80808080ffffffa0ab6657daa1874674127bcdabedbc189a8d592076cc14d0611cd25fb48e59f53eff8400b0585d80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585d80ffff3cffa0e05fee5dbfa85ec5ff3da0213919bc5bbf8979881905a70ea408b844197765c98080ff80808080ffffffa08aca10b0de27ef32ba9cfbadafff68f1a9df45511aa0b8a55118338324711c0aff8400b0585a80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585a80ffff3cffa09083a200824786117cd658495c675a6c94134a7db6f57baa1124d95654b7e6928080ff80808080ffffffa0e7e5d7b3812dde461b268cef74dcc82613cc5de8fa8b017123213c88e0fdbc6fff840160b0b780ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585c80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585b80ffff3cffa05eaf7016845a07f145eb497e190287c03eca894b633880203ae09b7d34e6c00a8080ff80808080ffffffa0cb90bdc48a6728b1275591f867de60eeafec0c139e5b57705f214f05abde8f57ff8402c1616c80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b580ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b780ffff3cffa04f356d1b3866bc98e8c645c8a5d4af7db6e37b4286e4695fb3ee267ed2efc1118080ff80808080ffffffa0fdabfa097e2e9a5462cc069dd762df669ffb89913f1edd95642a446f387d57a8ff8447a3e40680ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20280ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20480ffff3cffa0db493296333c946c8f446c4481aa9894a25d2a198cb6f39162d9195dc1cf48938080ff80808080ffffffa0afeae06c828bdab74463d567ec58329dd547e450a70d1b24b72e468702f51f8cff8447a3e40e80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20680ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20880ffff3cffa0572a1fcd7e4e8da68964ca9f506f4a332edc6a79f3e143034c2ae61f0f238e348080ff80808080ffffffa0b9af218c9162fd1b9d37a662cc78f4b606a9a715d2f07591726a59b0a12168a0ff8400b0585b80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585b80ffff3cffa0acff85074c28fdc9047c3f15248cd4490cc36bc6426464080b267afe0fa02be68080ff80808080ffffffa0b16e0ee3ddb57ef3c79aa4dd1ced5c5a5db2050c20fbdc49b56a1a41871f0cccff840160b0bf80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586080ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585f80ffff3cffa00895dcb36d84df74a7e59b5958b9a5e0ca675f70331c1dac660c614e8b9479b38080ff80808080ffffffa0c4d58dff14e8ea000d16a95bf05b4c8cf0a3d424071f6528d1e6f9aee649bf27ff8400b0585b80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585b80ffff3cffa02524310a6de6f9974de5a68abd27df8f18495dbcb1ad50bc4a092b6a96b933f18080ff80808080ffffffa01d38d59793a8e6d3088bc72f7374c9d2530e1807f7966a51f7021e9a02db06b8ff840160b0b280ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585880ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585a80ffff3cffa09afc244bb8ed9c725a94269c611268d2bab8a8267cf84bbc4184207bcd2103768080ff80808080ffffffa042ae9de1892eb92c6ade4d58b3365bed2a25e3685a6cb110687bb36bb25c3963ff840160b0c580ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586380ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586280ffff3cffa0b0736d428d0fffaefb88e66d068c268513d1fa96e6a2ffd54de412f06ee84fab8080ff80808080ffffffa0b261b7c347dee6ec39f3873b12ea50c60f5a683999a052bc0aea380a895f8d14ff8447a3e40d80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20780ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20680ffff3cffa0523404cd598207bc4e41b2a0c649a4e6b951aa6b19215270f5423604476094138080ff80808080ffffffa05ddd6e29e2327bd9dfd14e83caf212ca058d0598faab82dce357f3cfecec3dd1ff8447a3e40c80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20580ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20780ffff3cffa09df7f4c92733a64000e602ff21f8ad7ff30891ea5d017ba895cbbabd9cc0ed0b8080ff80808080ffffffa0ab057e5708fed4502af0d4ccb66e2597db2d7147c0ebb5ef26d059674b5d0747ff8447a3e40d80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20780ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20680ffff3cffa0b181e325d434b2a0fb977a9053fe64e8a1fe06246bde74c22285ff2764ffdb128080ff80808080ffffffa073dcd49e0b459ebddef4bd3590fb6e0e9514482afe8ddc4988384bcc76ddc75eff8423d1f20380ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20380ffff3cffa0f67670ea53798168d76f4e3e1fe00dfb4ee5ad6c1bbe0981357f2645cfb2925c8080ff80808080ffffffa010d0d834d342a020831c7ad87a0fc4102927c237cd0b7fd20f540a149909e65aff840160b0b180ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585980ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585880ffff3cffa0feacf214efe5d89674159bdedf0059c0716eaf638535bfca7c6f6b4777e0664a8080ff80808080ffffffa0fc69fe96fbdf987048054f0218e78b6363c04eb46c7516f74f6cf19956f688d4ff8400b0585e80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585e80ffff3cffa0898b2e84605885194d43ac81dfe7a1a4b6b7c5ebb5812127fcd9bea7613be69e8080ff80808080ffffffa08a11466cfebb27b6757d97c0278d48f4739db02d391e8b5ebb1013159b778763ff8447a3e40780ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20480ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20380ffff3cffa015f714120e4343c04332523b7967ccadd6a4fe66c0f71e5d6c1c0c68b055f4128080ff80808080ffffffa03bffa17be6f964ef5fedab528833b35eb5d97bd199494993d5dd8bac827134a9ff8402c14e9680ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160a74a80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160a74c80ffff3cffa07e84d7d5bcb960166390abdd8ad9848b6b3d999e4463b77cebb51480adba78a78080ff80808080ffffffa0ef5592b47c2718c5c8be7865b27fdc4be60925269cf6feec0924a2dddc318f9fff8402c14ea680ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160a75280ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160a75480ffff3cffa0b4b66d5ff3c2d3023179b600ac07bfd645a42a90dcff6566a1f2eec0ad2d6cd58080ff80808080ffffffa002ec65b5ae0e3bde48c4f23c79ad4b214f9e6bd793a65c2625d857ff4ee7e230ff8402c1616b80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b680ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b580ffff3cffa020c0dd60d1527f2b78201ad10d351dab58f925b3397a75e790af64e5764011998080ff80808080ffffffa0cf7be53241a6df7154acb5ea699209b125eff3750940d43fd8194baec3e74db2ff8400b0585b80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585b80ffff3cffa07467fb103cfb4ae07589602fcea39349ea6863d346c9f3d9027c314ea87318c18080ff80808080ffffffa0e97a41789a748dd51f3370fe06b10b54b3b304f4877d794f1628f7ac4d3ec5adff840160b0c780ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586480ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586380ffff3cffa05bc86bc61d36afc81046776a973c88f7f39a04fdf2e7e04fb3c9ec7b770e5d1b8080ff80808080ffffffa047f37741c35cfa8cb54df75bdf69a22bd85b3f1ee3d2d250e2eb7e073e5790c8ff8400b0585f80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585f80ffff3cffa0920429e605d942138f7212d9646f8e53a543f79f28b762a70e98f80416df4eed8080ff80808080ffffffa0e1b9c8951e77246ba089a699afb6f1d0a3ca12669656b0b92335355900be4ac9ff8447a3e40880ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20380ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20580ffff3cffa06300476c967dc777a1bf90ed58f840960acd5c9092a3dae07208de4f9180d0d98080ff80808080ffffffa027dde671a3e38d01cf6bcc364182648c9a705b623b84b38d5ef81ebdeef1c5dcff8402c14ea480ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160a75180ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160a75380ffff3cffa00b36f5c1f08cbd967f7cdb4f28a2483b4d078f31867b7124272084f84e9b19058080ff80808080ffffffa049d4f3b8737625b0c326cd51fcf963d7206686557c5a0c3f249b73a8644e4137ff8402c1616280ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b080ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b280ffff3cffa0c3d3d625d13db0c6e5dd9a3102315582d5a3f290b3e8d007c8164a755ff5987f8080ff80808080ffffffa0cea1564ac2c1b13ba978bd666cfc0c4ddbe299457c03404316a9f33883f025e3ff8447a3e40c80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20580ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20780ffff3cffa0ce7c50de7405098624854b8ac9bffbd36d12bebe28e5492c5b260d1b737617668080ff80808080ffffffa0face3898ff3e472210b68a45c8f4c6cb8a8994b278fd3553d4b5c7e28639769aff8400b0585b80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585b80ffff3cffa0c3ee4a21615bb72679a97331693832313b3cc4dc38c4df229b2c28f2c784ed5b8080ff80808080ffffffa08f86f2b80cf1d7878aa90a0f9224bb2d98685393a0b334fa89d9b3e2b145ca08ff8402c14e9680ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160a74a80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160a74c80ffff3cffa00221055f03a05dd26cbf4349524d85f39f99a69c1bfce8f00fa94245dda789a98080ff80808080ffffffa03f81886329b5c5a4f05175949ae2e1d5546d459c84fe67d59b934e840e7010d9ff8423d1f20480ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20480ffff3cffa0ea1b70eb145d97cf6fba7b3fcb54b23c2a953f068a11fcead67667d400a993458080ff80808080ffffffa0b0145538c69b306b5387348121692305596927c79504e95ca2bd68921ad7ced9ff8447a3e40880ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20380ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20580ffff3cffa01b913fcd1fa254f98a0400071c66374c00c9c7f1d2d4058705491376dc16cf8b8080ff80808080ffffffa0617a306607c8eff3a51d0e05955c04e15f4ca86221406e823abe1c360d729fd3ff8447a3e40d80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20780ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20680ffff3cffa003c4595744e4bc5bac3ab3ea80ea02d1a92ec021d12e976f64844e11778f0fba8080ff80808080ffffffa0843c12de1f94a7655848371b2abb46ad945078688bcb58158783adb125ad65c0ff840160b0be80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585e80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586080ffff3cffa0ed24d419a193a8c552dca67724f1e41dd4eb9c4f4c04bd97c80d27a785f162e38080ff80808080ffffffa00390ddf35088e6dbd9e0d4fcde634e21d4bee21c7cf79648d7c7e6b8cfb0094cff8400b0585b80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585b80ffff3cffa037eb20f4d097730eb6c2139793d2dff2689a3c42a30965fb97a68c8878b0b5768080ff80808080ffffffa0fef2bf04f111c5bb9c377b2e7c354f790e4eeed53aad522d3c527a4318a13512ff8400b0585d80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585d80ffff3cffa059d1a78e27f18bb558a725b360fb5f930d68d29abd26604a2388d4c7ae6964da8080ff80808080ffffffa04e87d165d512a2ae389750ea8cf2e89d1835cac55a5a4e9bd65a07f7af098826ff8400b0585d80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585d80ffff3cffa01cbc15b17c032b0af08894db3ba1dfc861ccedc86cfc8c676b3a1fa5f1e2a0728080ff80808080ffffffa06bc65c37ebbae68e61803510f6dbbb98e7d0c024d404fb8d01e587362e4d13f4ff83582c2e80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff83582c2e80ffff3cffa0537c00d7149faec649f9c1a854a4d761086d33657d96f5b2e6061d8f66624aed8080ff80808080ffffffa0c1baa8dd1c69b5f86699fc8c67c046236109e3587030d40b27d98318c2dd6668ff8400b0585c80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585c80ffff3cffa0e04c219f9c91ea4114c41e59b6b69692979fbc559dcb6d71bfa771da19a5c2748080ff80808080ffffffa0a937747054cfe3879585c0ab183134a0eb3436453733bd6ddb1fd785939ad189ff8423d1f20580ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20580ffff3cffa0a1bd5fed1e088235126ada9e646cc340a83bcc59cd2d14bbf80ebae4181b471f8080ff80808080ffffffa0eddd01b7d0f6a2be54a4e3019dd4fe5c881c589dc383ae7a1d81af97a84708adff8400b0585b80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585b80ffff3cffa05334a253bd986b12847d0924437be4e735459628a1b3e24cf3a4cd802847d6c68080ff80808080ffffffa03a395ff80de226285a9017f077548888b4eb9e6d5f40b16f57bc0a50bdb3d5a0ff8400b0585c80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585c80ffff3cffa06b563c6ed37479960e1a72099fc4846f0330a4a6fc88aaaf0ea951cf76b024ae8080ff80808080ffffffa0c986df2fff4b863690fd3a4d7ca14425c98278e2ffe5859e772f9671c669ee8fff8400b0585e80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585e80ffff3cffa041ed39e98d91d602a07d02be4881dc7e7b67c0cd359b8c1d7f193e0302a470aa8080ff80808080ffffffa0793688b32ce7449dab96add743fc4c222dbdd386b5445c2eb7dbeed5d92fa21fff8400b0585d80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585d80ffff3cffa061b805cba34e53d3be323ce30f96c0f1107ecb135dea5481169e21e55b6963678080ff80808080ffffffa01bcb5130bbdab6aebfdb3ef2538f4883db00847f09d6ef403f4e2bdff74d1363ff8400b0585d80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585d80ffff3cffa060603991b5a89a3178fcb86b6204964480d45158d8a3591634b0396ca6a0a7428080ff80808080ffffffa0c8de7b7013a04cd2aa5e1769e9211b8b91d9123694e83bc6e7d10a69e6ab0086ff8400b0585980ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585980ffff3cffa002ce3044fec740c213353877030cfcea1dae03c08d1634ad2afed1243b904a6d8080ff80808080ffffffa0588671a1a4ebea3e9e32ad37ca336c02425c5501d7a7273651c5e35c1bc7c920ff8400b0585c80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585c80ffff3cffa09cbca907237019e42d1c07776edcebf845fcff0ed13fb22719a70f90ff40fb3b8080ff80808080ffffffa0d84dc27bbd104a8f44f455e8049c55d728a714eff6c093ae1ccfc15207c3ab34ff8400b0585d80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585d80ffff3cffa087f799f2ac2bd0c58e72cc44916e5ac61eb0022b09348c95c4a966b59ccce6d98080ff80808080ffffffa07e609109aec65ed40f222493cecc3ae1a97129b8d7429d2b8a82b39f0da35270ff8400b0585e80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585e80ffff3cffa07997d690bc22b4cc7760694b1797d96d5e852222dac786907e49af608aca26578080ff80808080ffffffa0f2fbb14a965ecf31354dc1dc2d69b8c0032c5aadd7d9f7749c795a9ff6ebd317ff8400b0585a80ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585a80ffff3cffa01ef431ceb848923a0a953c7bc1f87a085528a4b0a40434d47130c9fdd56fe72f8080ff80808080ffffffa0b72a621f08463d7a653f9a0113d6a6a6384f9a9aa8598c9af1266a9199f801eaff8423d1f20780ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8423d1f20780ffff3cffa00e9dfcc339ee88da37bf062775eb37b9f54f79225734382654a4857af8f6dc0d8080ff80808080ffffffa0935ee64b5bc00c408f63f4c1699cb9fd4b104e4361072312e7662aa169326217ff8400b0585980ffffb1b0abb1669111f258ca82016afc5859ee264f4c65c3b6c8ddbbd1dcee5a33fed983e66c74e4e197b1207738cd7cbcaf51d5ffff80ffff01ffff33ffa062485c17f937c92d0b9080ec0b522946b383945378ebb09bde7bee2799460f81ff8400b0585980ffff3cffa0b5901975457178a38a6d040d7a84db5050aea3fe5c35e0d9ac8dd04f59d497b18080ff80808080ffffffa095268cb9ee12a078cc85e82d3cab2dacf4e04acbaafb2d77715043ba50dcfdcdff85008f47c80e80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40680ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40880ffff3cffa0dafbbb0eb72cc349cc338861c86efdc8132b7b9e2f8914a86ab192fdab7204268080ff80808080ffffffa09a3ffa764a6df19a8c1df8b2a85a8fb98f4ae31ff71b2381e60c77fdb603933bff840160b0ba80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585c80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585e80ffff3cffa03d44bc6a45311ed3d083154ec36a1a1b65efb098bc7942c0e42ffac8d32e27898080ff80808080ffffffa0bf1cfb2640ee95b1bb7793af7895b41d25ebcf26bd39e6a382eae442bc9238f3ff85008f47c80d80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40780ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40680ffff3cffa055ba57e4ccfaf25cf3fca173d29f31eab541c036eb03174c9332dc4efbba4fb68080ff80808080ffffffa046a094cefe5ff01226aebc3ca085d115a5cbcdd0f650d3d0dba5bd58cd17e947ff840160b0c780ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586480ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586380ffff3cffa08b183229353adf442a4a859ad53b28c45897311a7c98e473d97479c87a24fab18080ff80808080ffffffa03f60c4819d8f2764c1c02f29d5401f9886c04a5d3e29ecffaad9295899acdeecff850e8d4a50ff80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff850746a5288080ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff850746a5287f80ffff3cffa020736aef971c62b5f7388c5b65bfcc5b130b5a9944783f498d9e7fca050698198080ff80808080ffffffa0dbb36ba5fcbcfda2d97667d2fd4ec97ac6ccc98626d884a5b11cf8bf2d9a5b7bff8447a3e40d80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20780ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20680ffff3cffa0baab766d86f090bb885ca7f5df44801d32cdda43a86a7a5f69dc05dc3ffe3ed28080ff80808080ffffffa0b2a48246ef6bba198be956d1e485f32b7432f54901c85eb8b5dc9f94413a3cd0ff8447a3e40d80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20780ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20680ffff3cffa0084fddc1b5cc98d4a1d1c4f92f804cf99bc919a75c2c575202a08a6adc2300c78080ff80808080ffffffa0e05584c170fac6f42ec6e45609a5ced165a8b7b8993a9373f6ae629f1c94dec5ff8447a3e40680ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20280ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20480ffff3cffa0f8b41ac20a3a13e18b1b59de1dd4792fad0298adb260716ce7c6715db31346de8080ff80808080ffffffa044c784e182fee49a01fdbd2e5c342a872ec24c189d8e8cd20ca7ccee55f205d5ff8447a3e40880ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20380ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20580ffff3cffa0e314a08b32347d4561b38d6a5d512ab62f390b845da388bac26dd4fd23e1e38d8080ff80808080ffffffa02ea606894cb6b1c07ec0f41295ccaba623914e3a76f7b7041e6ea909c5f83026ff8402c1616780ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b480ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b380ffff3cffa0de9dbf868733c749223a790a36cc994ec63ad2c00081f0f9ec2f9dbe37d82eea8080ff80808080ffffffa0d3770cd54c89ed9a6380a353c07bcc821b7f7ed159805c8c672cffab4100617aff8447a3e40680ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20280ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20480ffff3cffa06af7ea7705863e4fe6dda9580d564b986e73b6b740d0b4f781b8392dc7c8f6978080ff80808080ffffffa001726e54508acc896c475ef4b73e32667a96c0f035a9d46e5b0aa8a3a5e4e220ff8402c14eab80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160a75680ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160a75580ffff3cffa0ec527658759632aa294c76df2187f8cecf46ab8a21471f3fbaae74e64c95e2868080ff80808080ffffffa0447cc65d142ff42327a540bda45f384f9b15d22c3a77b917583f26fc7cc46277ff8447a3e40e80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20680ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20880ffff3cffa04070be27bcadf5949560ce33372f966fa021de07b94477d8df1d9824ce88bbbe8080ff80808080ffffffa02ea606894cb6b1c07ec0f41295ccaba623914e3a76f7b7041e6ea909c5f83026ff8402c1616980ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b580ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b480ffff3cffa0f6255499187555daa58242680b728ce444b11c671193695db147c64845596a4b8080ff80808080ffffffa04809ae72d2b84684e3f1f255af861d5e77e0a00f56df78b26b0d6618ea18d9a4ff8402c1617180ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b980ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b880ffff3cffa092680e31e27c8be6fcb9515bef032a4f640cafff0fb580d74af7bac5fca32ff28080ff80808080ffffffa04caf58c637f1954811c9e3ab7985af080e85c12947518c77f24f492df4843291ff8447a3e40580ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20380ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20280ffff3cffa048db2e872ce5f815780147fd286a61292c3bafe7153a708dfc2055062340c9e98080ff80808080ffffffa05ba21d0d6938dc35af0f9ed00cd2e69751150f75d9cb74ac126528559b5101c0ff8402c14eac80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160a75580ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160a75780ffff3cffa0bf6d807b3e44f6cc4db591acfdf062e177c8c31ccd6b13b59aa05b7b8450850b8080ff80808080ffffffa0eb892e69730c7ed7c507add9d7aee249c220035e379b2add4ed6257d1d61c1caff8402c1617480ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b980ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0bb80ffff3cffa02c48d33c8709f2f168fd76c896c5e0626e43f0289160dd75093f6a55893513c48080ff80808080ffffffa08113f4a31c22a5252e6ba52ad6deef53c950332ab72f3a0704eb8e5d001630dcff85008f47c81080ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40780ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40980ffff3cffa02eb74f10352a761081cfdff072aa7d0eff15e757c9897ba6f3dac3f0aebf52b68080ff80808080ffffffa0d066971ad9c5dd7bd521ef81e2691822f5f29cfa8ea6061da2574a665d7686a7ff8447a3e40d80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20780ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20680ffff3cffa0e1b1c218f3a322e3ad360b6ee5f88ca801000f16621609fede6c6a549810dc9b8080ff80808080ffffffa049206bb286d254cf50afbda4406b81182c48e26e24042b62c30185682539a3b7ff840160b0b680ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585a80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585c80ffff3cffa08aa1d6d4cea580a72bc1936a4af82336a81236031d0393acc20f4f68edf41b108080ff80808080ffffffa0d516bf9ff83bbdb9ba76e73c1c9db2e088a83cfc425f58835da5d7e0336204bbff840160b0bd80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585f80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585e80ffff3cffa031e91abfbba4ada34f660482c7c7715697c712f478dbd4bdc42539af760c74078080ff80808080ffffffa065d18dde2cf98ed722af2b1ed34ce924cd39ed12c95cc4c1bfc75ceb29a41d1cff8447a3e40a80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20480ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20680ffff3cffa0dbd080a26c9f951631be0b9c0259894900f101e821f6deab50817810917d5e278080ff80808080ffffffa002068c5a319c64a72fb6e9ea22114e8bdc017a070aedead504030a2bdd1b406cff8402c1617a80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0bc80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0be80ffff3cffa007f009ec742ebf3756470c922601f1e6b5f4ae1900963cf36866c4fba0294dad8080ff80808080ffffffa0b5738f477b77463faef1c31747a3f528a40b55f48c0fa83ce91c5a153df074d3ff8402c1617380ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0ba80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b980ffff3cffa0525ff2eb716a5ba46f14629270bdabaae059d22c58e53858937c8145b9d915ab8080ff80808080ffffffa065b50b5081107892a2ef219460afc0c2ff0b6b3b4c20f54ac7a45719e0b17465ff8447a3e40a80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20480ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20680ffff3cffa02adc087e2edfc610b0df8aec87b632da5988067321ef938040767460725bd1828080ff80808080ffffffa045b2a3cc78a6de5da1d0ff0f1d2125023394a5890162776c63c59537955c8412ff85008f47c81580ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40b80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40a80ffff3cffa028ff50f1c1ddff3d8e829a22d087bfcf7da3128242614dd5b781edb0ccfebb038080ff80808080ffffffa0d2e95afda7ed6dc02a02d5263bb4626cb4e3321db7196a868161d8fafa8cdbf8ff85008f47c81580ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40b80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40a80ffff3cffa02f8ea8d5ab7d0c089e34cd3b3620331deaa314e0ae3a59c268f380f1f9a7521c8080ff80808080ffffffa0cb3e2efaa7950e020eb2055963e3538ef876310d0e5ff00fa35dd3584dc63ef3ff8447a3e40d80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20780ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20680ffff3cffa039560703ee0f425248bd4f670033c308d575e9f9772cef62d1ce038a4f63f0b78080ff80808080ffffffa07f1f56e3ee6a4cca18e7b674c44bae20d6c6151e94bda5be7761051e6c0b4d16ff8402c14ea880ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160a75380ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160a75580ffff3cffa0db9764685fe12eb4e95b0e0ee15683c52cd1b4e742d4e653213bf717cc34b16f8080ff80808080ffffffa0fa0ae4627f657c913482a41651515316e2cd3edf7c0d5c4a330672e992b9f1c3ff8447a3e40580ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20380ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20280ffff3cffa0a090d0c0df1f6cac1a39e47157841b404f9750d38cb29110a160d41a8910009c8080ff80808080ffffffa061ea0d10cfa2b721007969b04c2ee443ee7f3753ecdf47c455e8af27b8d1fa4cff8402c1616a80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b480ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b680ffff3cffa0ddeb4889ebf686d64378553c76e79a3cd0773ed8f7730287ee366e977a704eb68080ff80808080ffffffa046d9b5ca6bc881380f17cd07f9a6c5588f343d8becc4344d6cb9df2303b230bfff840160b0c680ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586280ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586480ffff3cffa0f8a92a0738fd5ff599ff57a3b3d469d2fc8490374a894cd1eae0e71e1b4b95218080ff80808080ffffffa02413d23aa213e0458cb28f860b8a8e607ac68e614aca36a12268f64b5207b2f0ff840160b0b480ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585980ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585b80ffff3cffa039a7674aaaf450861f91c6f0f81458873303fa13983817d9d1f70ba6922ca3e38080ff80808080ffffffa065fe6a64d143c70229ed6c47a58726afde162b0328af772bd2d938213414578aff840160b0be80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585e80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586080ffff3cffa016be5cdebb2f2a49530ca88078ef8aed10edfd9d79c7acec2f8c15f7d1d2da9f8080ff80808080ffffffa07bdd1d9c1552cabfb9c6af230f455c1b26a76104df52411b26eb0e26fc743c90ff840160b0b480ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585980ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585b80ffff3cffa06611fd26904da0b6fdfab7633b7fde452f024f1f64cc7bad5acb660bedf2d4348080ff80808080ffffffa0efdf951ca7a6af5e99b3c8a8708a7de62856cf72b11020f0573b814e3f80b5d7ff840160b0c280ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586080ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586280ffff3cffa05a98610406994dedf53f3d3d6340fd63a7393c4163b6022ce429dabbcb7879a38080ff80808080ffffffa0da502d791dfb0b5549487bf8cb16a1e31103122995505a38cb12faf42aaf1c86ff840160b0c680ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586280ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586480ffff3cffa076d7e9fe08f26700e44fef4a0dd6d7c026499544a594b2e550afc22ca0350a448080ff80808080ffffffa0fc65f09b331191af9c1ce56afe9334c449c0fad5fe0f69e3f49f764a320100dfff8447a3e40b80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20680ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20580ffff3cffa02e12b5d53cc44e2277a18c346acbbd0be11ac9e1f2c9a7396a738cf5ca2f5ed78080ff80808080ffffffa0b261b7c347dee6ec39f3873b12ea50c60f5a683999a052bc0aea380a895f8d14ff8447a3e40e80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20680ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20880ffff3cffa0790303db735333947e6cfdc6d4d586282b108c4c6a4d41fa112accb280854b2c8080ff80808080ffffffa04ecab489faed3f8e2f7af2053bcc49452815a1e2732a74cbc2f6508c059b4ee1ff85008f47c80d80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40780ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40680ffff3cffa08bc7b7af9cad9225847119aad7958b60384c0cf16cd65d5c7d6cc262f4df00948080ff80808080ffffffa07b33fa770069faf0f4d42f22cc3c44121f4f297a5e406d20faaf96efc5de8d84ff85008f47c80b80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40680ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40580ffff3cffa08d268c2c814e480976b4a880eaafa4d85df1d45c90490f8c3fd636baf29cd2a58080ff80808080ffffffa0bf0dc20bc9b635895ab94b7083f6571f0fbc0bf4de683b2a4b430ca558750ef9ff8447a3e40b80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20680ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20580ffff3cffa0c4fea07e0dab44657f683823b76a61e1bd5521102698ff29da5d63e47cb9feeb8080ff80808080ffffffa02ec8d1901fe7b2335fb18fb59a625c1c023bd748e978596728e9fdceda3b9f3fff8447a3e40b80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20680ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20580ffff3cffa00bf3552d1870862ae7dc158a4cc5d67c76a078c268b5bd973a2df530894d5ff38080ff80808080ffffffa00926198872d32739e7baad86c8f4c36824fa5484df42b86b3f16c31bce37c917ff840160b0b180ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585980ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585880ffff3cffa00f7ee9164401d72d16ecb89f0e9638f586eb16c9425bc7001cc0b11f9888e0ba8080ff80808080ffffffa07bc905abc786185639090c6143197a7061ad5116a7ee93e484066f92d2f9087dff8447a3e41180ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20980ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20880ffff3cffa075bb52d833b8570d2ec3076f8052d3588389ffe581c9d8758dbf6eff430b0ea08080ff80808080ffffffa02779e8b7674600cadd7771aa033d2ae754dbf241c06c94eddfb2e188b5a6a43dff840160b0b180ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585980ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585880ffff3cffa00d8ddad8030240e5b651614f2aa6a6d5513e782289d95629e9c9a9b8c4b5819d8080ff80808080ffffffa099d8176abaf98f428308e2f76bd5d143f45d3554f5265c49586a07b5d1e7fc3cff8447a3e40880ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20380ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20580ffff3cffa03524d8c20c9506451a2c01d6d7b43ad084b9a2bb6d68c6994cdc37076909ac7c8080ff80808080ffffffa02a811bf4a1b311f8232914e0fa06399fbfe414a589b48752f14d7cefa0453171ff8402c1617a80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0bc80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0be80ffff3cffa0fa4408cc74d490f2e281df47066c0ca6a660dc16e442bbee36775b87ab3ee4248080ff80808080ffffffa002ec65b5ae0e3bde48c4f23c79ad4b214f9e6bd793a65c2625d857ff4ee7e230ff8402c1616d80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b780ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b680ffff3cffa086a1947dd0bac571053c5fe7c922eef5f3fc61bb210d711c3827bdf8bb4b88ac8080ff80808080ffffffa0b5a93c29b71ec8a5aefe46749e6856cbf2757822594fc9a1579168651e1f9859ff8447a3e40d80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20780ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20680ffff3cffa0cee7cab42a0a8440f8df2648c24f11dbfaf0ac90842c058251e2674c8bf935c38080ff80808080ffffffa0ed3eabe13c2223b3e972882fae00f238ac9b5a3193dde3a140e491196cdadfafff840160b0c980ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586580ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586480ffff3cffa09d4bbce41275ae2d8527ef8fa18c50bf16e1684077217c3d69d4afac27e6ad358080ff80808080ffffffa088be05b657b59ccb74347a814cdf80cc5e67bfd57c7731b38f0d563b3c4ed19dff840160b0be80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0585e80ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8400b0586080ffff3cffa0ef74eeb23c3bb5c60c9239ce176c2a850f28824882bf59f3a4e841a5b1ac23368080ff80808080ffffffa0ca5d13838f90ae7451c4cfecaada4425571a6e9e7387e28a449646ece360c6f4ff8447a3e41080ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20780ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20980ffff3cffa0a116f7c92bf696a1a2b733f04f745a74deb6f870db469a3a075ce80faa397cef8080ff80808080ffffffa0793b5738594febec98726a64be3f661fc4950e4e8d284673500603cecd64dbc8ff8402c1616680ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b280ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff840160b0b480ffff3cffa094eeb15925c8d366cbd7c432b89e1f99e1a7a2a70c06636392af926cd25104278080ff80808080ffffffa066329556d1f491d31c88d4ee804fb8eea51a19bb7e25ce3ef5539bc7979f6436ff85008f47c81080ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40780ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40980ffff3cffa09a15a7209c9ecb59bb8f05b1c753f611051207c22c55971653429f6958ba04628080ff80808080ffffffa000ef0ac249ee02e676fb008a477b69af037c716bc143a4326ef769493ac9cb62ff8447a3e40680ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20280ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20480ffff3cffa03e932ccac4376d3717ad54a706ad8a06ba55a6338ae48cb2344e0ce9a2ebc6b68080ff80808080ffffffa08a7294218fc543b5969323691ef2a359edb8ec1811d4f32831e9cbe590bd6804ff8447a3e40e80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20680ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20880ffff3cffa058fd0547e7f373ee231117cfc98a53d6f2a9b712a19f33be5e19ced509ef07068080ff80808080ffffffa0171f6aade114e337ced78310c5c6ecacc7ff4b16a267b87b8ea7eee4c8b37435ff85008f47c81180ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40980ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8447a3e40880ffff3cffa0648cf31c8ee4c907e2bf9ac941e77890edf6bd535add695e40d571da47c230ef8080ff80808080ffffffa04cb723549fb46375953b3e281611b71ba6f6ecb155126b3701e1738204729596ff8447a3e40580ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20380ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20280ffff3cffa0356166fd8b2e384082ca55a36d903db4b65ab7549ea982da55798b49afa22da68080ff80808080ffffffa06a4ba801089f26ad4aba06ec1c176cc19c6032f8c9e293f424333e1d3d0c821aff8447a3e40a80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20480ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20680ffff3cffa0e7c96be4f60facfd0db6ad8e4e9a5a3f243f2c0bee1c1a671f7b1b84a191accb8080ff80808080ffffffa029b9134e49e42405bf72c8ff5b47f06ab1424dcf5bbee4a4d7baee0d9fbb4d4eff8447a3e40a80ffffb1b08b9b6e9c5d3b901b884cc519393527c2aea61a9827044dbd4e6471b75da42132f117a4ddbe848f5d77ee0bc9f4b5ff2cffff80ffff01ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20480ffff33ffa04d7048ed8c813a636dcb53c02079627f274218ab31f56c2ffacb35d4597d86d9ff8423d1f20680ffff3cffa00eadbdfa570b1d9106d86307c5095300070badc5db36e9cf54076481faa6d7a68080ff8080808080ff01808080808080", + "transactions_generator_ref_list": [ + 442734 + ], + "transactions_info": { + "aggregated_signature": "0xb9986f0921371e18f0da7aa295d7c710b74944918f624147c2e796ae7d92ef35dfbc5ecb4d7ed5cfab4757ffdf5979d5056a1eff28dc3787402e22722114d310fa863c6b76a7f40040910b44bb429ab91b6fa5cb19a18803cf9db32c1accd35a", + "cost": 3912542704, + "fees": 0, + "generator_refs_root": "0x074828c624d23d64dfbb38b73bc30631025bae452bbc392778f3e8e16b49e2d2", + "generator_root": "0x905d255c68eb522961642e797bbe83d9422478646936ad091ac3d23cded11af2", + "reward_claims_incorporated": [ + { + "amount": 1750000000000, + "parent_coin_info": "0xae83525ba8d1dd3f09b277de18ca3e4300000000000000000000000000071d21", + "puzzle_hash": "0xa076a4cd8c39e4046f37c3df72c41b4589a737e54a0a8538c9e67b53739de992" + }, + { + "amount": 250000000000, + "parent_coin_info": "0xfc0af20d20c4b3e92ef2a48bd291ccb200000000000000000000000000071d21", + "puzzle_hash": "0xef4efadd838306f2240e6f782387604d2b20e5e692aba561ea31fe8b888e70bf" + } + ] + } + }, + "success": true +} diff --git a/tests/tools/test_run_block.py b/tests/tools/test_run_block.py index cc5b88873267..140f9c9521f5 100644 --- a/tests/tools/test_run_block.py +++ b/tests/tools/test_run_block.py @@ -106,3 +106,14 @@ def test_block_cat(): assert not cat_list[0].memo assert cat_list[0].npc.coin_name.hex() == "4314b142cecfd6121474116e5a690d6d9b2e8c374e1ebef15235b0f3de4e2508" assert cat_list[0].npc.puzzle_hash.hex() == "ddc37f3cbb49e3566b8638c5aaa93d5e10ee91dfd5d8ce37ad7175432d7209aa" + + +def test_generator_ref(): + """Run a block containing a back reference without error""" + dirname = Path(__file__).parent + with open(dirname / "466212.json") as f: + full_block = json.load(f) + + cat_list = run_json_block(full_block, dirname, constants) + + assert cat_list == [] diff --git a/tools/run_block.py b/tools/run_block.py index 16b96d9ea75c..90956da0ce90 100644 --- a/tools/run_block.py +++ b/tools/run_block.py @@ -46,6 +46,7 @@ from chia.consensus.constants import ConsensusConstants from chia.consensus.default_constants import DEFAULT_CONSTANTS +from chia.full_node.generator import create_generator_args from chia.types.blockchain_format.program import SerializedProgram from chia.types.blockchain_format.coin import Coin from chia.types.condition_opcodes import ConditionOpcode @@ -101,15 +102,15 @@ def run_generator( else: flags = 0 - _, result = block_generator.program.run_with_cost(max_cost, flags, block_generator.generator_refs) + args = create_generator_args(block_generator.generator_refs).first() + _, block_result = block_generator.program.run_with_cost(max_cost, flags, args) - coin_spends = result.first() + coin_spends = block_result.first() cat_list: List[CAT] = [] for spend in coin_spends.as_iter(): parent, puzzle, amount, solution = spend.as_iter() - matched, curried_args = match_cat_puzzle(puzzle) if not matched: @@ -118,11 +119,11 @@ def run_generator( _, asset_id, _ = curried_args memo = "" - result = puzzle.run(solution) + puzzle_result = puzzle.run(solution) conds: Dict[ConditionOpcode, List[ConditionWithArgs]] = {} - for condition in result.as_python(): + for condition in puzzle_result.as_python(): op = ConditionOpcode(condition[0]) if op not in conds: From 87aeadc34310eb86aa0bd56f4eb00f84cc9563b2 Mon Sep 17 00:00:00 2001 From: Yostra Date: Tue, 8 Feb 2022 01:13:42 +0100 Subject: [PATCH 019/378] Wallet improvements (#10077) * disonnect from untusted faster, fork point change, pool state handle * name conflict * deadlock * fix inclusion validation for first sub epoch, don't fetch header blocks on every new peak * lint * can be none * revert changes * stop wallet peers if trusted peer is connected and synced * remove pool changes * remove cononfusing log --- chia/wallet/wallet_node.py | 188 ++++++++++++++++++---------- chia/wallet/wallet_state_manager.py | 3 +- 2 files changed, 120 insertions(+), 71 deletions(-) diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index 0c9cefd18e7f..c0dc82d0cc89 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -364,7 +364,7 @@ def initialize_wallet_peers(self): connect_to_unknown_peers = self.config.get("connect_to_unknown_peers", True) testing = self.config.get("testing", False) - if connect_to_unknown_peers and not testing: + if self.wallet_peers is None and connect_to_unknown_peers and not testing: self.wallet_peers = WalletPeers( self.server, self.config["target_peer_count"], @@ -388,6 +388,7 @@ def initialize_wallet_peers(self): def on_disconnect(self, peer: WSChiaConnection): if self.is_trusted(peer): self.local_node_synced = False + self.initialize_wallet_peers() if peer.peer_node_id in self.untrusted_caches: self.untrusted_caches.pop(peer.peer_node_id) @@ -537,22 +538,32 @@ async def subscribe_to_coin_updates(self, coin_names, peer, height=uint32(0)): if all_coins_state is not None and self.is_trusted(peer): await self.wallet_state_manager.new_coin_state(all_coins_state.coin_states, peer) - async def get_coin_state(self, coin_names: List[bytes32]) -> List[CoinState]: + async def get_coin_state( + self, coin_names: List[bytes32], peer: Optional[WSChiaConnection] = None + ) -> List[CoinState]: assert self.server is not None all_nodes = self.server.connection_by_type[NodeType.FULL_NODE] if len(all_nodes.keys()) == 0: raise ValueError("Not connected to the full node") - first_node = list(all_nodes.values())[0] + + # Use supplied if provided, prioritize trusted otherwise + if peer is None: + for node in list(all_nodes.values()): + if self.is_trusted(node): + peer = node + break + if peer is None: + peer = list(all_nodes.values())[0] + + assert peer is not None msg = wallet_protocol.RegisterForCoinUpdates(coin_names, uint32(0)) - coin_state: Optional[RespondToCoinUpdates] = await first_node.register_interest_in_coin(msg) + coin_state: Optional[RespondToCoinUpdates] = await peer.register_interest_in_coin(msg) assert coin_state is not None - if not self.is_trusted(first_node): + if not self.is_trusted(peer): valid_list = [] for coin in coin_state.coin_states: - valid = await self.validate_received_state_from_peer( - coin, first_node, self.get_cache_for_peer(first_node) - ) + valid = await self.validate_received_state_from_peer(coin, peer, self.get_cache_for_peer(peer)) if valid: valid_list.append(coin) return valid_list @@ -604,6 +615,23 @@ def get_full_node_peer(self): else: return None + async def last_local_tx_block(self, header_hash: bytes32) -> Optional[BlockRecord]: + assert self.wallet_state_manager is not None + current_hash = header_hash + while True: + if self.wallet_state_manager.blockchain.contains_block(current_hash): + block = self.wallet_state_manager.blockchain.try_block_record(current_hash) + if block is None: + return None + if block.is_transaction_block: + return block + if block.prev_transaction_block_hash is None: + return None + current_hash = block.prev_transaction_block_hash + else: + break + return None + async def fetch_last_tx_from_peer(self, height: uint32, peer: WSChiaConnection) -> Optional[HeaderBlock]: request_height = height while True: @@ -619,6 +647,16 @@ async def fetch_last_tx_from_peer(self, height: uint32, peer: WSChiaConnection) request_height = uint32(request_height - 1) return None + async def disconnect_and_stop_wpeers(self): + if len(self.server.get_full_node_connections()) > 1: + for peer in self.server.get_full_node_connections(): + if not self.is_trusted(peer): + asyncio.create_task(peer.close()) + + if self.wallet_peers is not None: + await self.wallet_peers.ensure_is_closed() + self.wallet_peers = None + async def get_timestamp_for_height(self, height: uint32) -> uint64: """ Returns the timestamp for transaction block at h=height, if not transaction block, backtracks until it finds @@ -654,37 +692,31 @@ async def new_peak_wallet(self, peak: wallet_protocol.NewPeakWallet, peer: WSChi assert self.wallet_state_manager is not None assert self.server is not None request_time = int(time.time()) - async with self.new_peak_lock: - if self.wallet_state_manager is None: - # When logging out of wallet + + if self.wallet_state_manager is None: + # When logging out of wallet + return + if self.is_trusted(peer): + request = wallet_protocol.RequestBlockHeader(peak.height) + header_response: Optional[RespondBlockHeader] = await peer.request_block_header(request) + assert header_response is not None + + # Get last timestamp + last_tx: Optional[HeaderBlock] = await self.fetch_last_tx_from_peer(peak.height, peer) + latest_timestamp: Optional[uint64] = None + if last_tx is not None: + assert last_tx.foliage_transaction_block is not None + latest_timestamp = last_tx.foliage_transaction_block.timestamp + + # Ignore if not synced + if latest_timestamp is None or self.config["testing"] is False and latest_timestamp < request_time - 600: return - if self.is_trusted(peer): - async with self.wallet_state_manager.lock: - request = wallet_protocol.RequestBlockHeader(peak.height) - header_response: Optional[RespondBlockHeader] = await peer.request_block_header(request) - assert header_response is not None - - # Get last timestamp - last_tx: Optional[HeaderBlock] = await self.fetch_last_tx_from_peer(peak.height, peer) - latest_timestamp: Optional[uint64] = None - if last_tx is not None: - assert last_tx.foliage_transaction_block is not None - latest_timestamp = last_tx.foliage_transaction_block.timestamp - - # Ignore if not synced - if ( - latest_timestamp is None - or self.config["testing"] is False - and latest_timestamp < request_time - 600 - ): - return - # Disconnect from all untrusted peers if our local node is trusted and synced - if len(self.server.get_full_node_connections()) > 1: - for peer in self.server.get_full_node_connections(): - if not self.is_trusted(peer): - asyncio.create_task(peer.close()) + # Disconnect from all untrusted peers if our local node is trusted and synced + await self.disconnect_and_stop_wpeers() + async with self.new_peak_lock: + async with self.wallet_state_manager.lock: # Sync to trusted node self.local_node_synced = True if peer.peer_node_id not in self.synced_peers: @@ -696,7 +728,8 @@ async def new_peak_wallet(self, peak: wallet_protocol.NewPeakWallet, peer: WSChi self.wallet_state_manager.state_changed("new_block") self.wallet_state_manager.set_sync_mode(False) - else: + else: + async with self.new_peak_lock: request = wallet_protocol.RequestBlockHeader(peak.height) response: Optional[RespondBlockHeader] = await peer.request_block_header(request) if response is None or not isinstance(response, RespondBlockHeader) or response.header_block is None: @@ -718,19 +751,27 @@ async def new_peak_wallet(self, peak: wallet_protocol.NewPeakWallet, peer: WSChi # This block is after our peak, so we don't need to check if node is synced pass else: + tx_timestamp = None if not response.header_block.is_transaction_block: - last_tx_block = await self.fetch_last_tx_from_peer(response.header_block.height, peer) + last_tx_block = None + # Try local first + last_block_record = await self.last_local_tx_block(response.header_block.prev_header_hash) + if last_block_record is not None: + tx_timestamp = last_block_record.timestamp + else: + last_tx_block = await self.fetch_last_tx_from_peer(response.header_block.height, peer) + if last_tx_block is not None: + assert last_tx_block.foliage_transaction_block is not None + tx_timestamp = last_tx_block.foliage_transaction_block.timestamp else: last_tx_block = response.header_block + assert last_tx_block.foliage_transaction_block is not None + tx_timestamp = last_tx_block.foliage_transaction_block.timestamp - if last_tx_block is None: - return - assert last_tx_block is not None - assert last_tx_block.foliage_transaction_block is not None - if ( - self.config["testing"] is False - and last_tx_block.foliage_transaction_block.timestamp < request_time - 600 - ): + if tx_timestamp is None: + return None + + if self.config["testing"] is False and tx_timestamp < request_time - 600: # Full node not synced, don't sync to it self.log.info("Peer we connected to is not fully synced, dropping connection...") await peer.close() @@ -764,11 +805,17 @@ async def new_peak_wallet(self, peak: wallet_protocol.NewPeakWallet, peer: WSChi return assert weight_proof is not None old_proof = self.wallet_state_manager.blockchain.synced_weight_proof + curr_peak = await self.wallet_state_manager.blockchain.get_peak_block() fork_point = 0 + if curr_peak is not None: + fork_point = max(0, curr_peak.height - 32) + if old_proof is not None: - fork_point = self.wallet_state_manager.weight_proof_handler.get_fork_point( + wp_fork_point = self.wallet_state_manager.weight_proof_handler.get_fork_point( old_proof, weight_proof ) + if wp_fork_point != 0: + fork_point = wp_fork_point await self.wallet_state_manager.blockchain.new_weight_proof(weight_proof, block_records) if syncing: @@ -786,6 +833,7 @@ async def new_peak_wallet(self, peak: wallet_protocol.NewPeakWallet, peer: WSChi self.synced_peers.add(peer.peer_node_id) self.wallet_state_manager.state_changed("new_block") + self.wallet_state_manager.set_sync_mode(False) await self.update_ui() except Exception: if syncing: @@ -840,8 +888,6 @@ async def wallet_short_sync_backtrack(self, header_block: HeaderBlock, peer) -> fork_height = top.height - 1 blocks.reverse() - # Roll back coins and transactions - self.log.info(f"Rolling back to {fork_height}") await self.wallet_state_manager.reorg_rollback(fork_height) peak = await self.wallet_state_manager.blockchain.get_peak_block() self.rollback_request_caches(fork_height) @@ -1150,7 +1196,7 @@ async def validate_block_inclusion(self, block: HeaderBlock, peer, peer_request_ if stored_record.header_hash == block.header_hash: return True - weight_proof = self.wallet_state_manager.blockchain.synced_weight_proof + weight_proof: Optional[WeightProof] = self.wallet_state_manager.blockchain.synced_weight_proof if weight_proof is None: return False @@ -1171,26 +1217,30 @@ async def validate_block_inclusion(self, block: HeaderBlock, peer, peer_request_ compare_to_recent = True end = first_height_recent else: - request = RequestSESInfo(block.height, block.height + 32) - if block.height in peer_request_cache.ses_requests: - res_ses: RespondSESInfo = peer_request_cache.ses_requests[block.height] + if block.height < self.constants.SUB_EPOCH_BLOCKS: + inserted = weight_proof.sub_epochs[1] + end = self.constants.SUB_EPOCH_BLOCKS + inserted.num_blocks_overflow else: - res_ses = await peer.request_ses_hashes(request) - peer_request_cache.ses_requests[block.height] = res_ses - - ses_0 = res_ses.reward_chain_hash[0] - last_height = res_ses.heights[0][-1] # Last height in sub epoch - end = last_height - for idx, ses in enumerate(weight_proof.sub_epochs): - if idx > len(weight_proof.sub_epochs) - 3: - break - if ses.reward_chain_hash == ses_0: - current_ses = ses - inserted = weight_proof.sub_epochs[idx + 2] - break - if current_ses is None: - self.log.error("Failed validation 2") - return False + request = RequestSESInfo(block.height, block.height + 32) + if block.height in peer_request_cache.ses_requests: + res_ses: RespondSESInfo = peer_request_cache.ses_requests[block.height] + else: + res_ses = await peer.request_ses_hashes(request) + peer_request_cache.ses_requests[block.height] = res_ses + + ses_0 = res_ses.reward_chain_hash[0] + last_height = res_ses.heights[0][-1] # Last height in sub epoch + end = last_height + for idx, ses in enumerate(weight_proof.sub_epochs): + if idx > len(weight_proof.sub_epochs) - 3: + break + if ses.reward_chain_hash == ses_0: + current_ses = ses + inserted = weight_proof.sub_epochs[idx + 2] + break + if current_ses is None: + self.log.error("Failed validation 2") + return False blocks = [] diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index dadb6a74dca4..91cd6be97065 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -571,7 +571,7 @@ async def fetch_parent_and_check_for_cat(self, peer, coin_state) -> Tuple[Option ): return None, None - response: List[CoinState] = await self.wallet_node.get_coin_state([coin_state.coin.parent_coin_info]) + response: List[CoinState] = await self.wallet_node.get_coin_state([coin_state.coin.parent_coin_info], peer) if len(response) == 0: self.log.warning(f"Could not find a parent coin with ID: {coin_state.coin.parent_coin_info}") return None, None @@ -869,7 +869,6 @@ async def new_coin_state( except Exception as e: self.log.debug(f"Not a pool wallet launcher {e}") continue - # solution_to_pool_state may return None but this may not be an error if pool_state is None: self.log.debug("solution_to_pool_state returned None, ignore and continue") From 613cdc47f2236199a17b4f2b9e0fd5e8071e414e Mon Sep 17 00:00:00 2001 From: Mariano Sorgente <3069354+mariano54@users.noreply.github.com> Date: Mon, 7 Feb 2022 19:38:45 -0500 Subject: [PATCH 020/378] Ms.wallet fixes (#10094) * wallet fixes * Don't show false positive synched * Code cleanup and lint * Fixes * Revert issue * Fix reorg issue Co-authored-by: wjblanke --- chia/cmds/configure.py | 1 + chia/wallet/wallet_node.py | 138 +++++++++++++++++----------- chia/wallet/wallet_state_manager.py | 27 +++--- 3 files changed, 96 insertions(+), 70 deletions(-) diff --git a/chia/cmds/configure.py b/chia/cmds/configure.py index 4741862232fc..4c2848195698 100644 --- a/chia/cmds/configure.py +++ b/chia/cmds/configure.py @@ -105,6 +105,7 @@ def configure( config["introducer"]["port"] = int(testnet_port) config["full_node"]["introducer_peer"]["host"] = testnet_introducer config["full_node"]["dns_servers"] = [testnet_dns_introducer] + config["wallet"]["dns_servers"] = [testnet_dns_introducer] config["selected_network"] = testnet config["harvester"]["selected_network"] = testnet config["pool"]["selected_network"] = testnet diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index c0dc82d0cc89..483458e5b080 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -472,37 +472,54 @@ async def trusted_sync(self, full_node: WSChiaConnection): self.wallet_state_manager.state_changed("coin_added", wallet_id) self.synced_peers.add(full_node.peer_node_id) - async def receive_state_from_untrusted_peer(self, items: List[CoinState], peer, height: Optional[uint32]): + async def receive_state_from_peer( + self, items: List[CoinState], peer: WSChiaConnection, fork_height: Optional[uint32], height: Optional[uint32] + ): + assert self.wallet_state_manager is not None + trusted = self.is_trusted(peer) # Validate states in parallel, apply serial if self.validation_semaphore is None: self.validation_semaphore = asyncio.Semaphore(6) if self.new_state_lock is None: self.new_state_lock = asyncio.Lock() + # If there is a fork, we need to ensure that we roll back in trusted mode to properly handle reorgs + if trusted and fork_height is not None and height is not None and fork_height != height - 1: + await self.wallet_state_manager.reorg_rollback(fork_height) + all_tasks = [] - for idx, state in enumerate(items): + for idx, potential_state in enumerate(items): - async def receive_and_validate(state: CoinState, peer, height: Optional[uint32], idx): + async def receive_and_validate(inner_state: CoinState, inner_idx: int): assert self.wallet_state_manager is not None assert self.validation_semaphore is not None # if height is not None: async with self.validation_semaphore: - valid = await self.validate_received_state_from_peer(state, peer, self.get_cache_for_peer(peer)) - if valid: - self.log.info(f"new coin state received ({idx} / {len(items)})") - assert self.new_state_lock is not None - async with self.new_state_lock: - await self.wallet_state_manager.new_coin_state([state], peer) - elif height is not None: - self.add_state_to_race_cache(height, state) - else: - if state.created_height is not None: - self.add_state_to_race_cache(state.created_height, state) - if state.spent_height is not None: - self.add_state_to_race_cache(state.spent_height, state) + try: + if trusted: + valid = True + else: + valid = await self.validate_received_state_from_peer( + inner_state, peer, self.get_cache_for_peer(peer) + ) + if valid: + self.log.info(f"new coin state received ({inner_idx} / {len(items)})") + assert self.new_state_lock is not None + async with self.new_state_lock: + await self.wallet_state_manager.new_coin_state([inner_state], peer) + elif height is not None: + self.add_state_to_race_cache(height, inner_state) + else: + if inner_state.created_height is not None: + self.add_state_to_race_cache(inner_state.created_height, inner_state) + if inner_state.spent_height is not None: + self.add_state_to_race_cache(inner_state.spent_height, inner_state) + except Exception as e: + tb = traceback.format_exc() + self.log.error(f"Exception while adding state: {e} {tb}") - task = receive_and_validate(state, peer, height, idx) + task = receive_and_validate(potential_state, idx) all_tasks.append(task) while len(self.validation_semaphore._waiters) > 20: self.log.debug("sleeping 2 sec") @@ -518,25 +535,19 @@ async def subscribe_to_phs(self, puzzle_hashes: List[bytes32], peer: WSChiaConne assert self.wallet_state_manager is not None msg = wallet_protocol.RegisterForPhUpdates(puzzle_hashes, height) all_state: Optional[RespondToPhUpdates] = await peer.register_interest_in_puzzle_hash(msg) - # State for untrusted sync is processed only in wp sync | or short sync backwards if all_state is None: return + await self.receive_state_from_peer(all_state.coin_states, peer, None, None) - if self.is_trusted(peer): - await self.wallet_state_manager.new_coin_state(all_state.coin_states, peer) - else: - await self.receive_state_from_untrusted_peer(all_state.coin_states, peer, None) - - async def subscribe_to_coin_updates(self, coin_names, peer, height=uint32(0)): + async def subscribe_to_coin_updates(self, coin_names: List[bytes32], peer: WSChiaConnection, height=uint32(0)): """ Tell full nodes that we are interested in coin ids, and for trusted connections, add the new coin state for the coin changes. """ msg = wallet_protocol.RegisterForCoinUpdates(coin_names, height) all_coins_state: Optional[RespondToCoinUpdates] = await peer.register_interest_in_coin(msg) - # State for untrusted sync is processed only in wp sync | or short sync backwards if all_coins_state is not None and self.is_trusted(peer): - await self.wallet_state_manager.new_coin_state(all_coins_state.coin_states, peer) + await self.receive_state_from_peer(all_coins_state.coin_states, peer, None, None) async def get_coin_state( self, coin_names: List[bytes32], peer: Optional[WSChiaConnection] = None @@ -594,19 +605,13 @@ async def state_update_received(self, request: wallet_protocol.CoinStateUpdate, assert self.wallet_state_manager is not None assert self.server is not None - if self.is_trusted(peer): - async with self.new_peak_lock: - async with self.wallet_state_manager.lock: - self.log.debug(f"state_update_received is {request}") - await self.wallet_state_manager.new_coin_state( - request.items, peer, request.fork_height, request.height - ) - await self.update_ui() - else: - async with self.new_peak_lock: - async with self.wallet_state_manager.lock: - self.log.debug(f"state_update_received is {request}") - await self.receive_state_from_untrusted_peer(request.items, peer, request.height) + async with self.new_peak_lock: + async with self.wallet_state_manager.lock: + self.log.debug(f"state_update_received is {request}") + await self.receive_state_from_peer( + request.items, peer, request.fork_height if self.is_trusted(peer) else None, request.height + ) + await self.update_ui() def get_full_node_peer(self): nodes = self.server.get_full_node_connections() @@ -689,6 +694,7 @@ async def get_timestamp_for_height(self, height: uint32) -> uint64: curr_height = uint32(curr_height - 1) async def new_peak_wallet(self, peak: wallet_protocol.NewPeakWallet, peer: WSChiaConnection): + self.log.info(f"New peak wallet.. {peak.height} {peer.get_peer_info()}") assert self.wallet_state_manager is not None assert self.server is not None request_time = int(time.time()) @@ -846,11 +852,12 @@ async def new_peak_wallet(self, peak: wallet_protocol.NewPeakWallet, peer: WSChi self.wallet_state_manager.set_sync_mode(False) else: + self.log.info(f"Starting backtrack sync to {peer.get_peer_info()}") await self.wallet_short_sync_backtrack(peak_block, peer) if peer.peer_node_id not in self.synced_peers: # Edge case, we still want to subscribe for all phs # (Hints are not in filter) - await self.untrusted_subscribe_to_puzzle_hashes(peer, True, self.get_cache_for_peer(peer)) + await self.untrusted_subscribe_to_puzzle_hashes(peer, self.get_cache_for_peer(peer)) self.synced_peers.add(peer.peer_node_id) if peak_block.height in self.race_cache: @@ -860,10 +867,17 @@ async def new_peak_wallet(self, peak: wallet_protocol.NewPeakWallet, peer: WSChi ) if valid: await self.wallet_state_manager.new_coin_state([state], peer) + else: + self.log.warning(f"Invalid state from peer {peer}") + await peer.close(9999) + return self.wallet_state_manager.set_sync_mode(False) self.wallet_state_manager.state_changed("new_block") await self.wallet_state_manager.new_peak(peak) + + if peak.height > self.wallet_state_manager.finished_sync_up_to: + self.wallet_state_manager.finished_sync_up_to = uint32(peak.height) self._pending_tx_handler() async def wallet_short_sync_backtrack(self, header_block: HeaderBlock, peer) -> int: @@ -888,7 +902,13 @@ async def wallet_short_sync_backtrack(self, header_block: HeaderBlock, peer) -> fork_height = top.height - 1 blocks.reverse() - await self.wallet_state_manager.reorg_rollback(fork_height) + + # Roll back coins and transactions + peak_height = self.wallet_state_manager.blockchain.get_peak_height() + if fork_height < peak_height: + self.log.info(f"Rolling back to {fork_height}") + await self.wallet_state_manager.reorg_rollback(fork_height) + peak = await self.wallet_state_manager.blockchain.get_peak_block() self.rollback_request_caches(fork_height) @@ -957,7 +977,6 @@ async def get_puzzle_hashes_to_subscribe(self) -> List[bytes32]: async def untrusted_subscribe_to_puzzle_hashes( self, peer: WSChiaConnection, - save_state: bool, peer_request_cache: Optional[PeerRequestCache], ): assert self.wallet_state_manager is not None @@ -978,9 +997,8 @@ async def untrusted_subscribe_to_puzzle_hashes( all_state: Optional[RespondToPhUpdates] = await peer.register_interest_in_puzzle_hash(msg) assert all_state is not None - if save_state: - assert peer_request_cache is not None - await self.receive_state_from_untrusted_peer(all_state.coin_states, peer, None) + assert peer_request_cache is not None + await self.receive_state_from_peer(all_state.coin_states, peer, None, None) # Check if new puzzle hashed have been created check_again = await self.get_puzzle_hashes_to_subscribe() @@ -1005,12 +1023,12 @@ async def untrusted_sync_to_peer(self, peer: WSChiaConnection, syncing: bool, fo # Always sync fully from untrusted # Get state for puzzle hashes self.log.debug("Start untrusted_subscribe_to_puzzle_hashes ") - await self.untrusted_subscribe_to_puzzle_hashes(peer, True, peer_request_cache) + await self.untrusted_subscribe_to_puzzle_hashes(peer, peer_request_cache) self.log.debug("End untrusted_subscribe_to_puzzle_hashes ") - checked_call_coins = False + checked_all_coins = False checked_coins: Set[bytes32] = set() - while not checked_call_coins: + while not checked_all_coins: # Get state for coins ids all_coins = await self.wallet_state_manager.coin_store.get_coins_to_check(uint32(0)) all_coin_names = [coin_record.name() for coin_record in all_coins] @@ -1046,17 +1064,17 @@ async def untrusted_sync_to_peer(self, peer: WSChiaConnection, syncing: bool, fo if coin_state_entry.created_height <= fork_height: coin_state_before_fork.append(coin_state_entry) - await self.receive_state_from_untrusted_peer(coin_state_before_fork, peer, None) + await self.receive_state_from_peer(coin_state_before_fork, peer, None, None) all_coins = await self.wallet_state_manager.coin_store.get_coins_to_check(uint32(0)) all_coin_names = [coin_record.name() for coin_record in all_coins] removed_dict = await self.wallet_state_manager.trade_manager.get_coins_of_interest() all_coin_names.extend(removed_dict.keys()) - checked_call_coins = True + checked_all_coins = True for coin_name in all_coin_names: if coin_name not in checked_coins: - checked_call_coins = False + checked_all_coins = False break end_time = time.time() @@ -1066,7 +1084,7 @@ async def untrusted_sync_to_peer(self, peer: WSChiaConnection, syncing: bool, fo async def validate_received_state_from_peer( self, coin_state: CoinState, - peer, + peer: WSChiaConnection, peer_request_cache: PeerRequestCache, ) -> bool: """ @@ -1187,7 +1205,9 @@ async def validate_received_state_from_peer( peer_request_cache.states_validated[coin_state.coin.get_hash()] = coin_state return True - async def validate_block_inclusion(self, block: HeaderBlock, peer, peer_request_cache: PeerRequestCache) -> bool: + async def validate_block_inclusion( + self, block: HeaderBlock, peer: WSChiaConnection, peer_request_cache: PeerRequestCache + ) -> bool: assert self.wallet_state_manager is not None if self.wallet_state_manager.blockchain.contains_height(block.height): stored_hash = self.wallet_state_manager.blockchain.height_to_hash(block.height) @@ -1242,22 +1262,28 @@ async def validate_block_inclusion(self, block: HeaderBlock, peer, peer_request_ self.log.error("Failed validation 2") return False - blocks = [] - + blocks: List[HeaderBlock] = [] for i in range(start - (start % 32), end + 1, 32): request_start = min(uint32(i), end) request_end = min(uint32(i + 31), end) request_h_response = RequestHeaderBlocks(request_start, request_end) if (request_start, request_end) in peer_request_cache.block_requests: - res_h_blocks: RespondHeaderBlocks = peer_request_cache.block_requests[(request_start, request_end)] + self.log.info(f"Using cache for blocks {request_start} - {request_end}") + res_h_blocks: Optional[RespondHeaderBlocks] = peer_request_cache.block_requests[ + (request_start, request_end) + ] else: start_time = time.time() res_h_blocks = await peer.request_header_blocks(request_h_response) + if res_h_blocks is None: + self.log.error("Failed validation 2.5") + return False end_time = time.time() peer_request_cache.block_requests[(request_start, request_end)] = res_h_blocks self.log.info( f"Fetched blocks: {request_start} - {request_end} | duration: {end_time - start_time}" ) + assert res_h_blocks is not None blocks.extend([bl for bl in res_h_blocks.header_blocks if bl.height >= start]) if compare_to_recent and weight_proof.recent_chain_data[0].header_hash != blocks[-1].header_hash: diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 91cd6be97065..d3a060c1d986 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -16,6 +16,7 @@ from chia.pools.pool_wallet import PoolWallet from chia.protocols import wallet_protocol from chia.protocols.wallet_protocol import PuzzleSolutionResponse, RespondPuzzleSolution, CoinState +from chia.server.ws_connection import WSChiaConnection from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.program import Program from chia.types.blockchain_format.sized_bytes import bytes32 @@ -105,6 +106,7 @@ class WalletStateManager: blockchain: WalletBlockchain coin_store: WalletCoinStore sync_store: WalletSyncStore + finished_sync_up_to: uint32 interested_store: WalletInterestedStore weight_proof_handler: WalletWeightProofHandler server: ChiaServer @@ -155,6 +157,7 @@ async def create( self.wallet_node = wallet_node self.sync_mode = False + self.finished_sync_up_to = uint32(0) self.weight_proof_handler = WalletWeightProofHandler(self.constants) self.blockchain = await WalletBlockchain.create(self.basic_store, self.constants, self.weight_proof_handler) @@ -443,6 +446,9 @@ async def synced(self): if latest is None: return False + if latest.height - self.finished_sync_up_to > 2: + return False + latest_timestamp = self.blockchain.get_latest_timestamp() if latest_timestamp > int(time.time()) - 10 * 60: @@ -620,9 +626,7 @@ async def fetch_parent_and_check_for_cat(self, peer, coin_state) -> Tuple[Option async def new_coin_state( self, coin_states: List[CoinState], - peer, - fork_height: Optional[uint32] = None, - current_height: Optional[uint32] = None, + peer: WSChiaConnection, ): created_h_none = [] for coin_st in coin_states.copy(): @@ -636,12 +640,6 @@ async def new_coin_state( all_unconfirmed: List[TransactionRecord] = await self.tx_store.get_all_unconfirmed() trade_coin_removed: List[CoinState] = [] - if fork_height is not None and current_height is not None and fork_height != current_height - 1: - # This only applies to trusted mode - await self.reorg_rollback(fork_height) - - new_interested_coin_ids: List[bytes32] = [] - for coin_state_idx, coin_state in enumerate(coin_states): info = await self.get_wallet_id_for_puzzle_hash(coin_state.coin.puzzle_hash) local_record: Optional[WalletCoinRecord] = await self.coin_store.get_coin_record(coin_state.coin.name()) @@ -840,7 +838,8 @@ async def new_coin_state( uint32(record.wallet_id), record.wallet_type, ) - new_interested_coin_ids.append(new_singleton_coin.name()) + await self.coin_store.set_spent(curr_coin_state.coin.name(), curr_coin_state.spent_height) + await self.interested_store.add_interested_coin_id(new_singleton_coin.name(), True) new_coin_state: List[CoinState] = await self.wallet_node.get_coin_state( [new_singleton_coin.name()] ) @@ -888,12 +887,10 @@ async def new_coin_state( await self.coin_added( coin_added, coin_state.spent_height, [], pool_wallet.id(), WalletType(pool_wallet.type()) ) - new_interested_coin_ids.append(coin_added.name()) + await self.interested_store.add_interested_coin_id(coin_added.name(), True) else: raise RuntimeError("All cases already handled") # Logic error, all cases handled - for new_coin_id in new_interested_coin_ids: - await self.add_interested_coin_id(new_coin_id) for coin_state_removed in trade_coin_removed: await self.trade_manager.coins_of_interest_farmed(coin_state_removed) @@ -927,7 +924,7 @@ def is_farmer_reward(self, created_height, parent_id): return True return False - async def get_wallet_id_for_puzzle_hash(self, puzzle_hash) -> Optional[Tuple[uint32, WalletType]]: + async def get_wallet_id_for_puzzle_hash(self, puzzle_hash: bytes32) -> Optional[Tuple[uint32, WalletType]]: info = await self.puzzle_store.wallet_info_for_puzzle_hash(puzzle_hash) if info is not None: wallet_id, wallet_type = info @@ -936,6 +933,8 @@ async def get_wallet_id_for_puzzle_hash(self, puzzle_hash) -> Optional[Tuple[uin interested_wallet_id = await self.interested_store.get_interested_puzzle_hash_wallet_id(puzzle_hash=puzzle_hash) if interested_wallet_id is not None: wallet_id = uint32(interested_wallet_id) + if wallet_id not in self.wallets.keys(): + self.log.warning(f"Do not have wallet {wallet_id} for puzzle_hash {puzzle_hash}") wallet_type = WalletType(self.wallets[uint32(wallet_id)].type()) return wallet_id, wallet_type return None From 66fae97b761a410d5312995205929dc9f54db886 Mon Sep 17 00:00:00 2001 From: Chris Marslender Date: Tue, 8 Feb 2022 14:58:53 -0600 Subject: [PATCH 021/378] Don't return from sync_changed - create payloads list and pass it along (#10149) --- chia/rpc/wallet_rpc_api.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 9ae7ab2681fc..7a4949c6c211 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -125,12 +125,13 @@ async def _state_changed(self, *args) -> List[WsRpcMessage]: Called by the WalletNode or WalletStateManager when something has changed in the wallet. This gives us an opportunity to send notifications to all connected clients via WebSocket. """ + payloads = [] if args[0] is not None and args[0] == "sync_changed": # Metrics is the only current consumer for this event - return [create_payload_dict(args[0], {}, self.service_name, "metrics")] + payloads.append(create_payload_dict(args[0], {}, self.service_name, "metrics")) if len(args) < 2: - return [] + return payloads data = { "state": args[0], @@ -140,7 +141,7 @@ async def _state_changed(self, *args) -> List[WsRpcMessage]: if args[2] is not None: data["additional_data"] = args[2] - payloads = [create_payload_dict("state_changed", data, self.service_name, "wallet_ui")] + payloads.append(create_payload_dict("state_changed", data, self.service_name, "wallet_ui")) if args[0] == "coin_added": payloads.append(create_payload_dict(args[0], data, self.service_name, "metrics")) From 68bca61df0f142589a5c1ae6f821fb4a85e6e510 Mon Sep 17 00:00:00 2001 From: William Blanke Date: Tue, 8 Feb 2022 13:01:33 -0800 Subject: [PATCH 022/378] updated gui to 866077d96153fccfd21d86dfe7f9111efbdd5a48 --- chia-blockchain-gui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia-blockchain-gui b/chia-blockchain-gui index 26646cbba6c4..866077d96153 160000 --- a/chia-blockchain-gui +++ b/chia-blockchain-gui @@ -1 +1 @@ -Subproject commit 26646cbba6c4e04d390e1830e430252e81a33f87 +Subproject commit 866077d96153fccfd21d86dfe7f9111efbdd5a48 From 67705e09071ce1b306100d04e89e9090498dca35 Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 8 Feb 2022 16:51:30 -0800 Subject: [PATCH 023/378] Ensure wallet/db directory exists before attempting to migrate a wallet (#10151) db from standalone_wallet/wallet/db. --- chia/wallet/wallet_node.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index 483458e5b080..e86ae13eed4a 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -210,13 +210,14 @@ async def _start( .replace("KEY", db_path_key_suffix) ) path = path_from_root(self.root_path, db_path_replaced.replace("v1", "v2")) + mkdir(path.parent) + standalone_path = path_from_root(STANDALONE_ROOT_PATH, f"{db_path_replaced.replace('v2', 'v1')}_new") if not path.exists(): if standalone_path.exists(): self.log.info(f"Copying wallet db from {standalone_path} to {path}") path.write_bytes(standalone_path.read_bytes()) - mkdir(path.parent) self.new_peak_lock = asyncio.Lock() assert self.server is not None self.wallet_state_manager = await WalletStateManager.create( From 2314f47ecc1883cdf655e382a0cee7f501ce035c Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 8 Feb 2022 17:58:59 -0800 Subject: [PATCH 024/378] Post a state change when an offer is added or soft-cancelled. More offer (#10153) state changes may be needed in the future, but for now the GUI will be able to refresh wallet balances in response to offer soft-cancellation and addition. --- chia/wallet/trade_manager.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/chia/wallet/trade_manager.py b/chia/wallet/trade_manager.py index 7d1b22ae9e0a..52b2f0d0cb6f 100644 --- a/chia/wallet/trade_manager.py +++ b/chia/wallet/trade_manager.py @@ -168,6 +168,7 @@ async def get_trade_by_id(self, trade_id: bytes32) -> Optional[TradeRecord]: async def cancel_pending_offer(self, trade_id: bytes32): await self.trade_store.set_status(trade_id, TradeStatus.CANCELLED, False) + self.wallet_state_manager.state_changed("offer_cancelled") async def cancel_pending_offer_safely( self, trade_id: bytes32, fee: uint64 = uint64(0) @@ -220,6 +221,7 @@ async def cancel_pending_offer_safely( async def save_trade(self, trade: TradeRecord): await self.trade_store.add_trade_record(trade, False) + self.wallet_state_manager.state_changed("offer_added") async def create_offer_for_ids( self, offer: Dict[Union[int, bytes32], int], fee: uint64 = uint64(0), validate_only: bool = False From 3b160139f8447629f87508c4e5f656c6b3636f06 Mon Sep 17 00:00:00 2001 From: William Blanke Date: Tue, 8 Feb 2022 21:19:43 -0800 Subject: [PATCH 025/378] pinned to gui to df2fb1a8e24c55cbfbf7d4c5aeb462c09de2679b --- chia-blockchain-gui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia-blockchain-gui b/chia-blockchain-gui index 866077d96153..df2fb1a8e24c 160000 --- a/chia-blockchain-gui +++ b/chia-blockchain-gui @@ -1 +1 @@ -Subproject commit 866077d96153fccfd21d86dfe7f9111efbdd5a48 +Subproject commit df2fb1a8e24c55cbfbf7d4c5aeb462c09de2679b From 8b4ab4bfde41a0094f3a80343c57e1a5b1e22a2f Mon Sep 17 00:00:00 2001 From: Pavel Yakovlev Date: Wed, 9 Feb 2022 20:11:01 +0300 Subject: [PATCH 026/378] fix full sync on DB v2 (#10157) * Update hint_store.py * Update db_upgrade_func.py * Fix a hint by black. Co-authored-by: Amine Khaldi --- chia/cmds/db_upgrade_func.py | 4 +--- chia/full_node/hint_store.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/chia/cmds/db_upgrade_func.py b/chia/cmds/db_upgrade_func.py index 1fd9421c2987..0b1dd0354d38 100644 --- a/chia/cmds/db_upgrade_func.py +++ b/chia/cmds/db_upgrade_func.py @@ -245,9 +245,7 @@ async def convert_v1_to_v2(in_path: Path, out_path: Path) -> None: commit_in -= 1 if commit_in == 0: commit_in = HINT_COMMIT_RATE - await out_db.executemany( - "INSERT OR IGNORE INTO hints VALUES(?, ?) ON CONFLICT DO NOTHING", hint_values - ) + await out_db.executemany("INSERT OR IGNORE INTO hints VALUES(?, ?)", hint_values) await out_db.commit() await out_db.execute("begin transaction") hint_values = [] diff --git a/chia/full_node/hint_store.py b/chia/full_node/hint_store.py index e64316c50eed..422eb544e7cf 100644 --- a/chia/full_node/hint_store.py +++ b/chia/full_node/hint_store.py @@ -38,7 +38,7 @@ async def get_coin_ids(self, hint: bytes) -> List[bytes32]: async def add_hints(self, coin_hint_list: List[Tuple[bytes32, bytes]]) -> None: if self.db_wrapper.db_version == 2: cursor = await self.db_wrapper.db.executemany( - "INSERT INTO hints VALUES(?, ?) ON CONFLICT DO NOTHING", + "INSERT OR IGNORE INTO hints VALUES(?, ?)", coin_hint_list, ) else: From 6315cb4a4bc36db0a7cc55710142ca85ec1f0fc5 Mon Sep 17 00:00:00 2001 From: Yostra Date: Thu, 10 Feb 2022 01:19:02 +0100 Subject: [PATCH 027/378] Try all nodes when fetching a parent. (#10152) * try all nodes * lint --- chia/wallet/cat_wallet/cat_wallet.py | 46 ++++++++++++---------------- 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/chia/wallet/cat_wallet/cat_wallet.py b/chia/wallet/cat_wallet/cat_wallet.py index ba94d30bd94a..34f1445b9062 100644 --- a/chia/wallet/cat_wallet/cat_wallet.py +++ b/chia/wallet/cat_wallet/cat_wallet.py @@ -11,18 +11,18 @@ from chia.consensus.cost_calculator import NPCResult from chia.full_node.bundle_tools import simple_solution_generator from chia.full_node.mempool_check_conditions import get_name_puzzle_conditions -from chia.protocols.wallet_protocol import PuzzleSolutionResponse, CoinState +from chia.protocols.wallet_protocol import CoinState from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.program import Program from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.announcement import Announcement +from chia.types.coin_spend import CoinSpend from chia.types.generator_types import BlockGenerator from chia.types.spend_bundle import SpendBundle from chia.types.condition_opcodes import ConditionOpcode from chia.util.byte_types import hexstr_to_bytes from chia.util.condition_tools import conditions_dict_for_solution, pkm_pairs_for_conditions_dict from chia.util.ints import uint8, uint32, uint64, uint128 -from chia.util.json_util import dict_to_json_str from chia.wallet.cat_wallet.cat_constants import DEFAULT_CATS from chia.wallet.cat_wallet.cat_info import CATInfo from chia.wallet.cat_wallet.cat_utils import ( @@ -47,6 +47,7 @@ from chia.wallet.wallet_coin_record import WalletCoinRecord from chia.wallet.wallet_info import WalletInfo from chia.wallet.util.compute_memos import compute_memos +import traceback # This should probably not live in this file but it's for experimental right now @@ -280,31 +281,23 @@ async def coin_added(self, coin: Coin, height: uint32): break if search_for_parent: - data: Dict[str, Any] = { - "data": { - "action_data": { - "api_name": "request_puzzle_solution", - "height": height, - "coin_name": coin.parent_coin_info, - "received_coin": coin.name(), - } - } - } - - data_str = dict_to_json_str(data) - await self.wallet_state_manager.create_action( - name="request_puzzle_solution", - wallet_id=self.id(), - wallet_type=self.type(), - callback="puzzle_solution_received", - done=False, - data=data_str, - in_transaction=True, - ) + for node_id, node in self.wallet_state_manager.wallet_node.server.all_connections.items(): + try: + coin_state = await self.wallet_state_manager.wallet_node.get_coin_state( + [coin.parent_coin_info], node + ) + assert coin_state[0].coin.name() == coin.parent_coin_info + coin_spend = await self.wallet_state_manager.wallet_node.fetch_puzzle_solution( + node, coin_state[0].spent_height, coin_state[0].coin + ) + await self.puzzle_solution_received(coin_spend) + break + except Exception as e: + self.log.debug(f"Exception: {e}, traceback: {traceback.format_exc()}") - async def puzzle_solution_received(self, response: PuzzleSolutionResponse, action_id: int): - coin_name = response.coin_name - puzzle: Program = response.puzzle + async def puzzle_solution_received(self, coin_spend: CoinSpend): + coin_name = coin_spend.coin.name() + puzzle: Program = Program.from_bytes(bytes(coin_spend.puzzle_reveal)) matched, curried_args = match_cat_puzzle(puzzle) if matched: mod_hash, genesis_coin_checker_hash, inner_puzzle = curried_args @@ -324,7 +317,6 @@ async def puzzle_solution_received(self, response: PuzzleSolutionResponse, actio await self.add_lineage( coin_name, LineageProof(parent_coin.parent_coin_info, inner_puzzle.get_tree_hash(), parent_coin.amount) ) - await self.wallet_state_manager.action_store.action_done(action_id) else: # The parent is not a CAT which means we need to scrub all of its children from our DB child_coin_records = await self.wallet_state_manager.coin_store.get_coin_records_by_parent_id(coin_name) From 419f88f296b3e61d3af8fc070f7b31546f55bbe1 Mon Sep 17 00:00:00 2001 From: wjblanke Date: Wed, 9 Feb 2022 18:02:06 -0800 Subject: [PATCH 028/378] updated soft fork to 2300000 (#10170) --- chia/consensus/default_constants.py | 2 +- tests/conftest.py | 2 +- tests/core/full_node/test_mempool.py | 16 ++++++++-------- tests/core/test_cost_calculation.py | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/chia/consensus/default_constants.py b/chia/consensus/default_constants.py index be9029ba1f9f..a6066fe4c1ce 100644 --- a/chia/consensus/default_constants.py +++ b/chia/consensus/default_constants.py @@ -54,7 +54,7 @@ "MAX_GENERATOR_SIZE": 1000000, "MAX_GENERATOR_REF_LIST_SIZE": 512, # Number of references allowed in the block generator ref list "POOL_SUB_SLOT_ITERS": 37600000000, # iters limit * NUM_SPS - "SOFT_FORK_HEIGHT": 2000000, + "SOFT_FORK_HEIGHT": 2300000, } diff --git a/tests/conftest.py b/tests/conftest.py index 620d35aa41e8..0e0b1de2de69 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -35,7 +35,7 @@ def db_version(request): return request.param -@pytest.fixture(scope="function", params=[1000000, 2000000]) +@pytest.fixture(scope="function", params=[1000000, 2300000]) def softfork_height(request): return request.param diff --git a/tests/core/full_node/test_mempool.py b/tests/core/full_node/test_mempool.py index f8458cd93818..a3a814711d3a 100644 --- a/tests/core/full_node/test_mempool.py +++ b/tests/core/full_node/test_mempool.py @@ -1796,10 +1796,10 @@ def test_invalid_condition_args_terminator(self, softfork_height): [ (True, 0, -1, Err.GENERATOR_RUNTIME_ERROR.value), (False, 1000000, -1, None), - (False, 2000000, -1, Err.GENERATOR_RUNTIME_ERROR.value), + (False, 2300000, -1, Err.GENERATOR_RUNTIME_ERROR.value), (True, 0, 1, None), (False, 1000000, 1, None), - (False, 2000000, 1, None), + (False, 2300000, 1, None), ], ) def test_div(self, mempool, height, operand, expected): @@ -2034,7 +2034,7 @@ def test_create_coin_with_hint(self, softfork_height): [ (True, None), (False, 1000000), - (False, 2000000), + (False, 2300000), ], ) def test_unknown_condition(self, mempool, height): @@ -2180,7 +2180,7 @@ def test_duplicate_large_integer_ladder(self, opcode, softfork_height): start_time = time() npc_result = generator_condition_tester(condition, quote=False, height=softfork_height) run_time = time() - start_time - if softfork_height >= 2000000: + if softfork_height >= 2300000: assert npc_result.error == error_for_condition(opcode) else: assert npc_result.error is None @@ -2208,7 +2208,7 @@ def test_duplicate_large_integer(self, opcode, softfork_height): start_time = time() npc_result = generator_condition_tester(condition, quote=False, height=softfork_height) run_time = time() - start_time - if softfork_height >= 2000000: + if softfork_height >= 2300000: assert npc_result.error == error_for_condition(opcode) else: assert npc_result.error is None @@ -2236,7 +2236,7 @@ def test_duplicate_large_integer_substr(self, opcode, softfork_height): start_time = time() npc_result = generator_condition_tester(condition, quote=False, height=softfork_height) run_time = time() - start_time - if softfork_height >= 2000000: + if softfork_height >= 2300000: assert npc_result.error == error_for_condition(opcode) else: assert npc_result.error is None @@ -2266,7 +2266,7 @@ def test_duplicate_large_integer_substr_tail(self, opcode, softfork_height): start_time = time() npc_result = generator_condition_tester(condition, quote=False, height=softfork_height) run_time = time() - start_time - if softfork_height >= 2000000: + if softfork_height >= 2300000: assert npc_result.error == error_for_condition(opcode) else: assert npc_result.error is None @@ -2303,7 +2303,7 @@ def test_duplicate_reserve_fee(self, softfork_height): start_time = time() npc_result = generator_condition_tester(condition, quote=False, height=softfork_height) run_time = time() - start_time - if softfork_height >= 2000000: + if softfork_height >= 2300000: assert npc_result.error == error_for_condition(opcode) else: assert npc_result.error is None diff --git a/tests/core/test_cost_calculation.py b/tests/core/test_cost_calculation.py index fbdac073eab8..bbd018b6d4a5 100644 --- a/tests/core/test_cost_calculation.py +++ b/tests/core/test_cost_calculation.py @@ -246,7 +246,7 @@ async def test_clvm_max_cost(self, softfork_height): # raise the max cost to make sure this passes # ensure we pass if the program does not exceeds the cost npc_result = get_name_puzzle_conditions( - generator, 20000000, cost_per_byte=0, mempool_mode=False, height=softfork_height + generator, 23000000, cost_per_byte=0, mempool_mode=False, height=softfork_height ) assert npc_result.error is None From 231ef6faf20fba7463831a5015edee649a1d5d49 Mon Sep 17 00:00:00 2001 From: wjblanke Date: Thu, 10 Feb 2022 14:26:30 -0800 Subject: [PATCH 029/378] updated changelog (#10184) * updated changelog --- CHANGELOG.md | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11391ff7349c..18f350ab8961 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,79 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project does not yet adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) for setuptools_scm/PEP 440 reasons. +## 1.3.0 Beta Chia blockchain 2021-2-10 + +We at Chia have been working hard to bring all of our new features together into one easy-to-use release. Today, we’re proud to announce the beta release of our 1.3 client. Because this release is still in beta, we recommend that you only install it on non mission-critical systems. If you are running a large farm, you should wait for the full 1.3 release before upgrading. When will the full version of 1.3 be released? Soon. + +### Added: + +- CAT wallet support - add wallets for your favorite CATs +- Offers - make, take, and share your offers +- Integrated light wallet sync - to get you synced up faster while your full node syncs +- Wallet mode - Access just the wallet features to make and receive transactions +- Farmer mode - All your farming tools, and full node, while getting all the benefits of the upgraded wallet features +- New v2 DB - improved compression for smaller footprint +- Key derivation tool via CLI - lets you derive wallet addresses, child keys, and also search your keys for arbitrary wallet addresses/keys +- Light wallet data migration - CAT wallets you set up and your offer history will be carried over +- The farmer will report version info in User-Agent field for pool protocol (Thanks @FazendaPool) +- Added new RPC, get_version, to the daemon to return the version of Chia (Thanks @dkackman) +- Added new config.yaml setting, reserved_cores, to specify how many cores Chia will not use when launching process pools. Using 0 will allow Chia to use all cores for process pools. Set the default to 0 to allow Chia to use all cores. This can result in faster syncing and better performance overall especially on lower-end CPUs like the Raspberry Pi4. +- Added new RPC, get_logged_in_fingerprint, to the wallet to return the currently logged in fingerprint. +- Added new CLI option, chia keys derive, to allow deriving any number of keys in various ways. This is particularly useful to do an exhaustive search for a given address using chia keys derive search. +- Div soft fork block height set to 2,300,000 + +### Changed: + +- Light wallet client sync updated to only require 3 peers instead of 5 +- Only CATs from the default CAT list will be automatically added, all other unknown CATs will need to be manually added +- New sorting pattern for offer history - Open/pending offers sorted on top ordered by creation date > confirmation block height > trade id, and then Confirmed and Cancelled offers sorted by the same order +- When plotting multiple plots with the GUI, new items are taken from the top of the list instead of the bottom +- CA certificate store update +- VDF, chiapos, and blspy workflows updated to support python 3.10 wheels +- We now store peers and peer information in a serialized format instead of sqlite. The new files are called peers.dat and wallet_peers.dat. New settings peers_file_path and wallet_peers_file_path added to config.yaml. +- CLI option chia show will display the currently selected network (mainnet or testnet) +- CLI option chia plots check will display the Pool Contract Address for Portable (PlotNFT) plots +- Thanks to @cross for adding the ability to resolve IPv6 from hostnames in config.yaml. Added new config option prefer_ipv6 to toggle whether to resolve to IPv6 or IPv4. Default is false (IPv4) +- The default timeout when syncing the node was increased from 10 seconds to 30 seconds to avoid timing out when syncing from slower peers. +- TLS 1.2 is now the minimum required for all communication including peer-to-peer. The TLS 1.2 allowed cipher list is set to: "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256" +- In a future release the minimum TLS version will be set to TLS 1.3. A warning in the log will be emitted if the version of openssl in use does not support TLS 1.3. If supported, all local connections will be restricted to TLS 1.3. +- The new testnet is testnet10 +- Switch to using npm ci from npm install in the GUI install scripts +- Improved sync performance of the full node by doing BLS validation in separate processes. +- Default log rotation was changed to 50MiB from 20MiB - added config.yaml setting log_maxbytesrotation to configure this. +- Thanks to @cross for an optimization to chiapos to use rename instead of copy if the tmp2 and final files are on the same filesystem. +- Updated to use chiapos 1.0.9 +- Updated to use blspy 1.0.8 +- Implemented a limit to the number of PlotNFTs a user can create - with the limit set to 20. This is to prevent users from incorrectly creating multiple PlotNFTs. This limit can be overridden for those users who have specific use cases that require more than 20 PlotNFTs + +### Fixed: + +- Offer history limit has been fixed to show all offers now instead of limiting to just 49 offers +- Fixed issues with using madmax CLI options -w, -G, -2, -t and -d (Issue 9163) (thanks @randomisresistance and @lasers8oclockday1) +- Fixed issues with CLI option –passhrase-file (Issue 9032) (thanks @moonlitbugs) +- Fixed issues with displaying IPv6 address in CLI with chia show -c +- Thanks to @chuwt for fix to looping logic during node synching +- Fixed the chia-blockchain RPM to set the permission of chrome-sandbox properly +- Fixed issues where the wallet code would not generate enough addresses when looking for coins, which can result in missed coins due to the address not being checked. Deprecated the config setting initial_num_public_keys_new_wallet. The config setting initial_num_public_keys is now used in all cases. +- Thanks to @risner for fixes related to using colorlog +- Fixed issues in reading the pool_list from config if set to null +- Fixed display info in CLI chia show -c when No Info should be displayed +- Thanks to @madMAx42v3r for fixes in chiapos related to a possible race condition when multiple threads call Verifier::ValidateProof +- Thanks to @PastaPastaPasta for some compiler warning fixes in bls-signatures +- Thanks to @random-zebra for fixing a bug in the bls-signature copy assignment operator +- Thanks to @lourkeur for fixes in blspy related to pybind11 2.8+ +- Thanks to @nioos-ledger with a fix to the python implementation of bls-signatures + +### Known Issues: + +- When you are adding plots and you choose the option to “create a Plot NFT”, you will get an error message “Initial_target_state” and the plots will not get created + - Workaround: Create the Plot NFT first in the “Pool” tab, and then add your plots and choose the created plot NFT in the drop down. +- If you are installing on a machine for the first time, when the GUI loads and you don’t have any pre-existing wallet keys, the GUI will flicker and not load anything. + - Workaround: close and relaunch the GUI +- When you close the Chia app, regardless if you are in farmer mode or wallet, the content on the exit dialog isn’t correct +- If you start with wallet mode and then switch to farmer mode and back to wallet mode, the full node will continue to sync in the background. To get the full node to stop syncing after switching to wallet mode, you will need to close the Chia and relaunch the Chia app. +- Wallets with large number of transactions or large number of coins will take longer to sync (more than a few minutes), but should take less time than a full node sync. It could fail in some cases. + ## 1.2.11 Chia blockchain 2021-11-4 Farmers rejoice: today's release integrates two plotters in broad use in the Chia community: Bladebit, created by @harold-b, and Madmax, created by @madMAx43v3r. Both of these plotters bring significant improvements in plotting time. More plotting info [here](https://github.com/Chia-Network/chia-blockchain/wiki/Alternative--Plotters). From ed70c2663ae0f98e8e237870d53f6de3afab7c13 Mon Sep 17 00:00:00 2001 From: dustinface <35775977+xdustinface@users.noreply.github.com> Date: Fri, 11 Feb 2022 21:33:53 +0100 Subject: [PATCH 030/378] farmer: Wait until `xch_target_address` is in the config in `setup_keys` (#10185) * farmer: Wait until `xch_target_address` is in the config in `setup_keys` * farmer: Reload config in `setup_keys` before `xch_target_address` checks --- chia/farmer/farmer.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/chia/farmer/farmer.py b/chia/farmer/farmer.py index 77f8d0037a6c..21ceadbb299b 100644 --- a/chia/farmer/farmer.py +++ b/chia/farmer/farmer.py @@ -153,6 +153,15 @@ async def setup_keys(self) -> bool: log.warning(no_keys_error_str) return False + config = load_config(self._root_path, "config.yaml") + if "xch_target_address" not in self.config: + self.config = config["farmer"] + if "xch_target_address" not in self.pool_config: + self.pool_config = config["pool"] + if "xch_target_address" not in self.config or "xch_target_address" not in self.pool_config: + log.debug("xch_target_address missing in the config") + return False + # This is the farmer configuration self.farmer_target_encoded = self.config["xch_target_address"] self.farmer_target = decode_puzzle_hash(self.farmer_target_encoded) From b229211ea5cb1f1b8463ebf41c3280283442d332 Mon Sep 17 00:00:00 2001 From: dustinface <35775977+xdustinface@users.noreply.github.com> Date: Fri, 11 Feb 2022 21:34:34 +0100 Subject: [PATCH 031/378] farmer: Move some member instantiations into `Farmer.__init__` (#10179) --- chia/farmer/farmer.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/chia/farmer/farmer.py b/chia/farmer/farmer.py index 21ceadbb299b..fe09489cc65a 100644 --- a/chia/farmer/farmer.py +++ b/chia/farmer/farmer.py @@ -128,6 +128,17 @@ def __init__( self.started = False self.harvester_handshake_task: Optional[asyncio.Task] = None + # From p2_singleton_puzzle_hash to pool state dict + self.pool_state: Dict[bytes32, Dict] = {} + + # From p2_singleton to auth PrivateKey + self.authentication_keys: Dict[bytes32, PrivateKey] = {} + + # Last time we updated pool_state based on the config file + self.last_config_access_time: uint64 = uint64(0) + + self.harvester_cache: Dict[str, Dict[str, HarvesterCacheEntry]] = {} + async def ensure_keychain_proxy(self) -> KeychainProxy: if not self.keychain_proxy: if self.local_keychain: @@ -181,19 +192,6 @@ async def setup_keys(self) -> bool: log.warning(no_keys_error_str) return False - # The variables below are for use with an actual pool - - # From p2_singleton_puzzle_hash to pool state dict - self.pool_state: Dict[bytes32, Dict] = {} - - # From p2_singleton to auth PrivateKey - self.authentication_keys: Dict[bytes32, PrivateKey] = {} - - # Last time we updated pool_state based on the config file - self.last_config_access_time: uint64 = uint64(0) - - self.harvester_cache: Dict[str, Dict[str, HarvesterCacheEntry]] = {} - return True async def _start(self): From b091a528d0fc9213c9dd53ab39739e19a4368ef0 Mon Sep 17 00:00:00 2001 From: William Blanke Date: Fri, 11 Feb 2022 12:36:29 -0800 Subject: [PATCH 032/378] updated to 8365b7c89bedc98ecc785161e07be440413e62ba --- chia-blockchain-gui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia-blockchain-gui b/chia-blockchain-gui index df2fb1a8e24c..8365b7c89bed 160000 --- a/chia-blockchain-gui +++ b/chia-blockchain-gui @@ -1 +1 @@ -Subproject commit df2fb1a8e24c55cbfbf7d4c5aeb462c09de2679b +Subproject commit 8365b7c89bedc98ecc785161e07be440413e62ba From 4494c66cf395c8063c2f7cbc0194cf409d8392bb Mon Sep 17 00:00:00 2001 From: ChiaMineJP Date: Sat, 12 Feb 2022 06:04:45 +0900 Subject: [PATCH 033/378] Update version prop in package.json of gui (#10192) * Update version prop in package.json of gui * Added type hints * Fixed a lint error --- installhelper.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/installhelper.py b/installhelper.py index bb0d05658432..c5da3a174d6e 100644 --- a/installhelper.py +++ b/installhelper.py @@ -6,6 +6,7 @@ # import json import os +from os.path import exists import subprocess from pkg_resources import parse_version @@ -15,7 +16,7 @@ # https://github.com/inveniosoftware/invenio-assets/blob/maint-1.0/invenio_assets/npm.py # Copyright (C) 2015-2018 CERN. # -def make_semver(version_str): +def make_semver(version_str: str) -> str: v = parse_version(version_str) major = v._version.release[0] try: @@ -32,34 +33,40 @@ def make_semver(version_str): prerelease.append("".join(str(x) for x in v._version.pre)) if v._version.dev: prerelease.append("".join(str(x) for x in v._version.dev)) - prerelease = ".".join(prerelease) local = v.local version = "{0}.{1}.{2}".format(major, minor, patch) if prerelease: - version += "-{0}".format(prerelease) + version += "-{0}".format(".".join(prerelease)) if local: version += "+{0}".format(local) return version -def update_version(): - with open(f"{os.path.dirname(__file__)}/chia-blockchain-gui/package.json") as f: - data = json.load(f) - +def get_chia_version() -> str: version: str = "0.0" output = subprocess.run(["chia", "version"], capture_output=True) if output.returncode == 0: version = str(output.stdout.strip(), "utf-8").splitlines()[-1] + return make_semver(version) + + +def update_version(package_json_path: str): + if not exists(package_json_path): + return + + with open(package_json_path) as f: + data = json.load(f) - data["version"] = make_semver(version) + data["version"] = get_chia_version() - with open(f"{os.path.dirname(__file__)}/chia-blockchain-gui/package.json", "w") as w: + with open(package_json_path, "w") as w: json.dump(data, indent=4, fp=w) if __name__ == "__main__": - update_version() + update_version(f"{os.path.dirname(__file__)}/chia-blockchain-gui/package.json") + update_version(f"{os.path.dirname(__file__)}/chia-blockchain-gui/packages/gui/package.json") From 54053178bf9d5eca7d28c4738b16205a1c792146 Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Fri, 11 Feb 2022 22:05:13 +0100 Subject: [PATCH 034/378] remove constraint on v1 database 'peak'-index. Constraints like this require at least sqlite 3.9 (2015) (#10199) --- chia/full_node/block_store.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia/full_node/block_store.py b/chia/full_node/block_store.py index bb820fff7e10..3f865e065988 100644 --- a/chia/full_node/block_store.py +++ b/chia/full_node/block_store.py @@ -97,7 +97,7 @@ async def create(cls, db_wrapper: DBWrapper): await self.db.execute("CREATE INDEX IF NOT EXISTS height on block_records(height)") - await self.db.execute("CREATE INDEX IF NOT EXISTS peak on block_records(is_peak) where is_peak = 1") + await self.db.execute("CREATE INDEX IF NOT EXISTS peak on block_records(is_peak)") await self.db.commit() self.block_cache = LRUCache(1000) From fc95c638a3feda9f57717066bcb64c96b3db8492 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 11 Feb 2022 21:30:19 -0500 Subject: [PATCH 035/378] Update to handle the NodeJS 16 dependency (#9921) * preliminary testing of in-directory n on ubuntu always * Use more n chiaminejp (#9971) * Added n as a local npm dependency * Fixed an issue where `install.sh` always tried to run `sudo apt install bc -y` even if `bc` is installed already * Added validations and useful outputs for `start-gui.sh` * Fixed lint error and use shell functions for readability * Replace tags with spaces * Skip installing python39 on RH like OS if it is already installed * Fixed an issue where start-gui.sh failed silently if venv is not activated * Suppressed message from pacman * Support CentOS7 * Fixed typo * Reduced unnecessary install messages * Fixed end of file * Added npm_global/__init__.py to pass CI * Fixed lint errors * Install python/sqlite from source on AMZN2. Clear old venv when changing python version on install * Suppress unnecessary command outputs * Suppress outputs * Added centos7/8 to install test * A minor fix * Fixed yaml syntax error * Fixed an issue where test-install-scripts failed in CentOS Co-authored-by: ChiaMineJP --- .github/workflows/test-install-scripts.yml | 26 +- .gitignore | 3 + build_scripts/npm_global/__init__.py | 0 build_scripts/npm_global/package-lock.json | 13 + build_scripts/npm_global/package.json | 15 ++ install-gui.sh | 219 +++++++++++------ install.sh | 271 +++++++++++++-------- start-gui.sh | 52 ++++ 8 files changed, 424 insertions(+), 175 deletions(-) create mode 100644 build_scripts/npm_global/__init__.py create mode 100644 build_scripts/npm_global/package-lock.json create mode 100644 build_scripts/npm_global/package.json create mode 100755 start-gui.sh diff --git a/.github/workflows/test-install-scripts.yml b/.github/workflows/test-install-scripts.yml index f52d1d77d121..6ac141058e16 100644 --- a/.github/workflows/test-install-scripts.yml +++ b/.github/workflows/test-install-scripts.yml @@ -64,7 +64,12 @@ jobs: - name: arch:latest type: arch url: "docker://archlinux:latest" - # TODO: what CentOS version provides Python3.7-3.9? + - name: centos:7 + type: centos + url: "docker://centos:7" + - name: centos:8 + type: centos + url: "docker://centos:8" - name: debian:buster type: debian # https://packages.debian.org/buster/python/python3 (3.7) @@ -117,6 +122,25 @@ jobs: run: | pacman --noconfirm --refresh base --sync git sudo + - name: Prepare CentOS + if: ${{ matrix.distribution.type == 'centos' }} + # Installing Git from yum brings git@1.x which doesn't work on actions/checkout. + # So install git@2.x from source + run: | + if [ "$(rpm --eval %{centos_ver})" = "8" ]; then + sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-Linux-*; + fi + yum update -y + yum install -y sudo gcc autoconf make wget curl-devel expat-devel gettext-devel openssl-devel perl-devel zlib-devel + wget https://mirrors.edge.kernel.org/pub/software/scm/git/git-2.29.0.tar.gz + tar zxf git-2.29.0.tar.gz + pushd git-2.29.0 + make configure + ./configure --prefix=/usr/local + make all + make install + popd + - name: Prepare Debian if: ${{ matrix.distribution.type == 'debian' }} env: diff --git a/.gitignore b/.gitignore index 683611dc87d2..c5082e8b5f64 100644 --- a/.gitignore +++ b/.gitignore @@ -90,6 +90,9 @@ win_code_sign_cert.p12 # chia-blockchain wheel build folder build/ +# Temporal `n` (node version manager) directory +.n/ + # pytest-monitor # https://pytest-monitor.readthedocs.io/en/latest/operating.html?highlight=.pymon#storage .pymon diff --git a/build_scripts/npm_global/__init__.py b/build_scripts/npm_global/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/build_scripts/npm_global/package-lock.json b/build_scripts/npm_global/package-lock.json new file mode 100644 index 000000000000..31ea8280cd10 --- /dev/null +++ b/build_scripts/npm_global/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "npm_global", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "n": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/n/-/n-8.0.2.tgz", + "integrity": "sha512-IvKMeWenkEntHnktypexqIi1BCTQc0Po1+zBanui+flF4dwHtsV+B2WNkx6KAMCqlTHyIisSddj1Y7EbnKRgXQ==" + } + } +} diff --git a/build_scripts/npm_global/package.json b/build_scripts/npm_global/package.json new file mode 100644 index 000000000000..346c484aaa87 --- /dev/null +++ b/build_scripts/npm_global/package.json @@ -0,0 +1,15 @@ +{ + "name": "npm_global", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "n": "^8.0.2" + } +} diff --git a/install-gui.sh b/install-gui.sh index 7e30316ef332..89cb6bbba7c7 100755 --- a/install-gui.sh +++ b/install-gui.sh @@ -2,115 +2,176 @@ set -e export NODE_OPTIONS="--max-old-space-size=3000" +SCRIPT_DIR=$(cd -- "$(dirname -- "$0")"; pwd) + +if [ "${SCRIPT_DIR}" != "$(pwd)" ]; then + echo "Please change working directory by the command below" + echo " cd ${SCRIPT_DIR}" + exit 1 +fi if [ -z "$VIRTUAL_ENV" ]; then echo "This requires the chia python virtual environment." echo "Execute '. ./activate' before running." - exit 1 + exit 1 fi if [ "$(id -u)" = 0 ]; then echo "The Chia Blockchain GUI can not be installed or run by the root user." - exit 1 + exit 1 fi # Allows overriding the branch or commit to build in chia-blockchain-gui SUBMODULE_BRANCH=$1 -UBUNTU=false +nodejs_is_installed(){ + if ! npm version >/dev/null 2>&1; then + return 1 + fi + return 0 +} + +do_install_npm_locally(){ + NPM_VERSION="$(npm -v | cut -d'.' -f 1)" + if [ "$NPM_VERSION" -lt "7" ]; then + echo "Current npm version($(npm -v)) is less than 7. GUI app requires npm>=7." + + if [ "$(uname)" = "OpenBSD" ] || [ "$(uname)" = "FreeBSD" ]; then + # `n` package does not support OpenBSD/FreeBSD + echo "Please install npm>=7 manually" + exit 1 + fi + + NPM_GLOBAL="${SCRIPT_DIR}/build_scripts/npm_global" + # install-gui.sh can be executed + echo "cd ${NPM_GLOBAL}" + cd "${NPM_GLOBAL}" + if [ "$NPM_VERSION" -lt "6" ]; then + # Ubuntu image of Amazon ec2 instance surprisingly uses nodejs@3.5.2 + # which doesn't support `npm ci` as of 27th Jan, 2022 + echo "npm install" + npm install + else + echo "npm ci" + npm ci + fi + export N_PREFIX=${SCRIPT_DIR}/.n + PATH="${N_PREFIX}/bin:$(npm bin):${PATH}" + export PATH + # `n 16` here installs nodejs@16 under $N_PREFIX directory + echo "n 16" + n 16 + echo "Current npm version: $(npm -v)" + if [ "$(npm -v | cut -d'.' -f 1)" -lt "7" ]; then + echo "Error: Failed to install npm>=7" + exit 1 + fi + cd "${SCRIPT_DIR}" + else + echo "Found npm $(npm -v)" + fi +} + # Manage npm and other install requirements on an OS specific basis if [ "$(uname)" = "Linux" ]; then - #LINUX=1 - if type apt-get; then - # Debian/Ubuntu - UBUNTU=true - - # Check if we are running a Raspberry PI 4 - if [ "$(uname -m)" = "aarch64" ] \ - && [ "$(uname -n)" = "raspberrypi" ]; then - # Check if NodeJS & NPM is installed - type npm >/dev/null 2>&1 || { - echo >&2 "Please install NODEJS&NPM manually" - } - else - sudo apt-get install -y npm nodejs libxss1 - fi - elif type yum && [ ! -f "/etc/redhat-release" ] && [ ! -f "/etc/centos-release" ] && [ ! -f /etc/rocky-release ] && [ ! -f /etc/fedora-release ]; then - # AMZN 2 - echo "Installing on Amazon Linux 2." - curl -sL https://rpm.nodesource.com/setup_12.x | sudo bash - - sudo yum install -y nodejs - elif type yum && [ ! -f /etc/rocky-release ] && [ ! -f /etc/fedora-release ] && [ -f /etc/redhat-release ] || [ -f /etc/centos-release ]; then - # CentOS or Redhat - echo "Installing on CentOS/Redhat." - curl -sL https://rpm.nodesource.com/setup_12.x | sudo bash - - sudo yum install -y nodejs - elif type yum && [ -f /etc/rocky-release ] || [ -f /etc/fedora-release ]; then - # RockyLinux - echo "Installing on RockyLinux/Fedora" - sudo dnf module enable nodejs:12 - sudo dnf install -y nodejs - fi - -elif [ "$(uname)" = "Darwin" ] && type brew && ! npm version >/dev/null 2>&1; then - # Install npm if not installed - brew install npm + #LINUX=1 + if type apt-get >/dev/null 2>&1; then + # Debian/Ubuntu + + # Check if we are running a Raspberry PI 4 + if [ "$(uname -m)" = "aarch64" ] \ + && [ "$(uname -n)" = "raspberrypi" ]; then + # Check if NodeJS & NPM is installed + type npm >/dev/null 2>&1 || { + echo >&2 "Please install NODEJS&NPM manually" + } + else + if ! nodejs_is_installed; then + echo "nodejs is not installed. Installing..." + echo "sudo apt-get install -y npm nodejs libxss1" + sudo apt-get install -y npm nodejs libxss1 + fi + do_install_npm_locally + fi + elif type yum >/dev/null 2>&1 && [ ! -f "/etc/redhat-release" ] && [ ! -f "/etc/centos-release" ] && [ ! -f /etc/rocky-release ] && [ ! -f /etc/fedora-release ]; then + # AMZN 2 + if ! nodejs_is_installed; then + echo "Installing nodejs on Amazon Linux 2." + curl -sL https://rpm.nodesource.com/setup_12.x | sudo bash - + sudo yum install -y nodejs + fi + do_install_npm_locally + elif type yum >/dev/null 2>&1 && [ ! -f /etc/rocky-release ] && [ ! -f /etc/fedora-release ] && [ -f /etc/redhat-release ] || [ -f /etc/centos-release ]; then + # CentOS or Redhat + if ! nodejs_is_installed; then + echo "Installing nodejs on CentOS/Redhat." + curl -sL https://rpm.nodesource.com/setup_12.x | sudo bash - + sudo yum install -y nodejs + fi + do_install_npm_locally + elif type yum >/dev/null 2>&1 && [ -f /etc/rocky-release ] || [ -f /etc/fedora-release ]; then + # RockyLinux + if ! nodejs_is_installed; then + echo "Installing nodejs on RockyLinux/Fedora" + sudo dnf module enable nodejs:12 + sudo dnf install -y nodejs + fi + do_install_npm_locally + fi +elif [ "$(uname)" = "Darwin" ] && type brew >/dev/null 2>&1; then + # MacOS + if ! nodejs_is_installed; then + echo "Installing nodejs on MacOS" + brew install npm + fi + do_install_npm_locally elif [ "$(uname)" = "OpenBSD" ]; then - pkg_add node + if ! nodejs_is_installed; then + echo "Installing nodejs" + pkg_add node + fi + do_install_npm_locally elif [ "$(uname)" = "FreeBSD" ]; then - pkg install node + if ! nodejs_is_installed; then + echo "Installing nodejs" + pkg install node + fi + do_install_npm_locally fi -# Ubuntu before 20.04LTS has an ancient node.js echo "" -UBUNTU_PRE_2004=false -if $UBUNTU; then - UBUNTU_PRE_2004=$(python -c 'import subprocess; process = subprocess.run(["lsb_release", "-rs"], stdout=subprocess.PIPE); print(float(process.stdout) < float(20.04))') -fi - -if [ "$UBUNTU_PRE_2004" = "True" ]; then - echo "Installing on Ubuntu older than 20.04 LTS: Ugrading node.js to stable." - UBUNTU_PRE_2004=true # Unfortunately Python returns True when shell expects true - sudo npm install -g n - sudo n stable - export PATH="$PATH" -fi - -if [ "$UBUNTU" = "true" ] && [ "$UBUNTU_PRE_2004" = "False" ]; then - echo "Installing on Ubuntu 20.04 LTS or newer: Using installed node.js version." -fi # For Mac and Windows, we will set up node.js on GitHub Actions and Azure # Pipelines directly, so skip unless you are completing a source/developer install. # Ubuntu special cases above. if [ ! "$CI" ]; then - echo "Running git submodule update --init --recursive." - echo "" - git submodule update --init --recursive - echo "Running git submodule update." - echo "" - git submodule update - cd chia-blockchain-gui - - if [ "$SUBMODULE_BRANCH" ]; - then + echo "Running git submodule update --init --recursive." + echo "" + git submodule update --init --recursive + echo "Running git submodule update." + echo "" + git submodule update + cd chia-blockchain-gui + + if [ "$SUBMODULE_BRANCH" ]; + then git fetch - git checkout "$SUBMODULE_BRANCH" + git checkout "$SUBMODULE_BRANCH" git pull - echo "" - echo "Building the GUI with branch $SUBMODULE_BRANCH" - echo "" - fi - - npm ci - npm audit fix || true - npm run build - python ../installhelper.py + echo "" + echo "Building the GUI with branch $SUBMODULE_BRANCH" + echo "" + fi + + npm ci + npm audit fix || true + npm run build + python ../installhelper.py else - echo "Skipping node.js in install.sh on MacOS ci." + echo "Skipping node.js in install.sh on MacOS ci." fi echo "" echo "Chia blockchain install-gui.sh completed." echo "" -echo "Type 'cd chia-blockchain-gui' and then 'npm run electron &' to start the GUI." +echo "Type 'bash start-gui.sh &' to start the GUI." diff --git a/install.sh b/install.sh index 416f25c11e0a..5fc39444a633 100644 --- a/install.sh +++ b/install.sh @@ -31,123 +31,176 @@ done UBUNTU=false DEBIAN=false if [ "$(uname)" = "Linux" ]; then - #LINUX=1 - if command -v apt-get &> /dev/null; then - OS_ID=$(lsb_release -is) - if [ "$OS_ID" = "Debian" ]; then - DEBIAN=true - else - UBUNTU=true - fi - fi + #LINUX=1 + if command -v apt-get >/dev/null; then + OS_ID=$(lsb_release -is) + if [ "$OS_ID" = "Debian" ]; then + DEBIAN=true + else + UBUNTU=true + fi + fi fi # Check for non 64 bit ARM64/Raspberry Pi installs if [ "$(uname -m)" = "armv7l" ]; then echo "" - echo "WARNING:" - echo "The Chia Blockchain requires a 64 bit OS and this is 32 bit armv7l" - echo "For more information, see" - echo "https://github.com/Chia-Network/chia-blockchain/wiki/Raspberry-Pi" - echo "Exiting." - exit 1 + echo "WARNING:" + echo "The Chia Blockchain requires a 64 bit OS and this is 32 bit armv7l" + echo "For more information, see" + echo "https://github.com/Chia-Network/chia-blockchain/wiki/Raspberry-Pi" + echo "Exiting." + exit 1 fi # Get submodules git submodule update --init mozilla-ca UBUNTU_PRE_2004=false if $UBUNTU; then - LSB_RELEASE=$(lsb_release -rs) - # In case Ubuntu minimal does not come with bc - if ! command -v bc &> /dev/null; then - sudo apt install bc -y - fi - # Mint 20.04 repsonds with 20 here so 20 instead of 20.04 - UBUNTU_PRE_2004=$(echo "$LSB_RELEASE<20" | bc) - UBUNTU_2100=$(echo "$LSB_RELEASE>=21" | bc) + LSB_RELEASE=$(lsb_release -rs) + # In case Ubuntu minimal does not come with bc + if ! command -v bc > /dev/null 2>&1; then + sudo apt install bc -y + fi + # Mint 20.04 responds with 20 here so 20 instead of 20.04 + UBUNTU_PRE_2004=$(echo "$LSB_RELEASE<20" | bc) + UBUNTU_2100=$(echo "$LSB_RELEASE>=21" | bc) fi +install_python3_and_sqlite3_from_source_with_yum() { + CURRENT_WD=$(pwd) + TMP_PATH=/tmp + + # Preparing installing Python + echo 'yum groupinstall -y "Development Tools"' + sudo yum groupinstall -y "Development Tools" + echo "sudo yum install -y openssl-devel libffi-devel bzip2-devel wget" + sudo yum install -y openssl-devel libffi-devel bzip2-devel wget + + echo "cd $TMP_PATH" + cd "$TMP_PATH" + # Install sqlite>=3.37 + # yum install sqlite-devel brings sqlite3.7 which is not compatible with chia + echo "wget https://www.sqlite.org/2022/sqlite-autoconf-3370200.tar.gz" + wget https://www.sqlite.org/2022/sqlite-autoconf-3370200.tar.gz + tar xf sqlite-autoconf-3370200.tar.gz + echo "cd sqlite-autoconf-3370200" + cd sqlite-autoconf-3370200 + echo "./configure --prefix=/usr/local" + # '| stdbuf ...' seems weird but this makes command outputs stay in single line. + ./configure --prefix=/usr/local | stdbuf -o0 cut -b1-"$(tput cols)" | sed -u 'i\\o033[2K' | stdbuf -o0 tr '\n' '\r'; echo + echo "make -j$(nproc)" + make -j"$(nproc)" | stdbuf -o0 cut -b1-"$(tput cols)" | sed -u 'i\\o033[2K' | stdbuf -o0 tr '\n' '\r'; echo + echo "sudo make install" + sudo make install | stdbuf -o0 cut -b1-"$(tput cols)" | sed -u 'i\\o033[2K' | stdbuf -o0 tr '\n' '\r'; echo + # yum install python3 brings Python3.6 which is not supported by chia + cd .. + echo "wget https://www.python.org/ftp/python/3.9.9/Python-3.9.9.tgz" + wget https://www.python.org/ftp/python/3.9.9/Python-3.9.9.tgz + tar xf Python-3.9.9.tgz + echo "cd Python-3.9.9" + cd Python-3.9.9 + echo "LD_RUN_PATH=/usr/local/lib ./configure --prefix=/usr/local" + # '| stdbuf ...' seems weird but this makes command outputs stay in single line. + LD_RUN_PATH=/usr/local/lib ./configure --prefix=/usr/local | stdbuf -o0 cut -b1-"$(tput cols)" | sed -u 'i\\o033[2K' | stdbuf -o0 tr '\n' '\r'; echo + echo "LD_RUN_PATH=/usr/local/lib make -j$(nproc)" + LD_RUN_PATH=/usr/local/lib make -j"$(nproc)" | stdbuf -o0 cut -b1-"$(tput cols)" | sed -u 'i\\o033[2K' | stdbuf -o0 tr '\n' '\r'; echo + echo "LD_RUN_PATH=/usr/local/lib sudo make altinstall" + LD_RUN_PATH=/usr/local/lib sudo make altinstall | stdbuf -o0 cut -b1-"$(tput cols)" | sed -u 'i\\o033[2K' | stdbuf -o0 tr '\n' '\r'; echo + cd "$CURRENT_WD" +} + + # Manage npm and other install requirements on an OS specific basis if [ "$(uname)" = "Linux" ]; then - #LINUX=1 - if [ "$UBUNTU" = "true" ] && [ "$UBUNTU_PRE_2004" = "1" ]; then - # Ubuntu - echo "Installing on Ubuntu pre 20.04 LTS." - sudo apt-get update - sudo apt-get install -y python3.7-venv python3.7-distutils - elif [ "$UBUNTU" = "true" ] && [ "$UBUNTU_PRE_2004" = "0" ] && [ "$UBUNTU_2100" = "0" ]; then - echo "Installing on Ubuntu 20.04 LTS." - sudo apt-get update - sudo apt-get install -y python3.8-venv python3-distutils - elif [ "$UBUNTU" = "true" ] && [ "$UBUNTU_2100" = "1" ]; then - echo "Installing on Ubuntu 21.04 or newer." - sudo apt-get update - sudo apt-get install -y python3.9-venv python3-distutils - elif [ "$DEBIAN" = "true" ]; then - echo "Installing on Debian." - sudo apt-get update - sudo apt-get install -y python3-venv - elif type pacman && [ -f "/etc/arch-release" ]; then - # Arch Linux - echo "Installing on Arch Linux." - echo "Python <= 3.9.9 is required. Installing python-3.9.9-1" - case $(uname -m) in - x86_64) - sudo pacman ${PACMAN_AUTOMATED} -U --needed https://archive.archlinux.org/packages/p/python/python-3.9.9-1-x86_64.pkg.tar.zst - ;; - aarch64) - sudo pacman ${PACMAN_AUTOMATED} -U --needed http://tardis.tiny-vps.com/aarm/packages/p/python/python-3.9.9-1-aarch64.pkg.tar.xz - ;; - *) - echo "Incompatible CPU architecture. Must be x86_64 or aarch64." - exit 1 - ;; - esac - sudo pacman ${PACMAN_AUTOMATED} -S --needed git - elif type yum && [ ! -f "/etc/redhat-release" ] && [ ! -f "/etc/centos-release" ] && [ ! -f "/etc/fedora-release" ]; then - # AMZN 2 - echo "Installing on Amazon Linux 2." - AMZN2_PY_LATEST=$(yum --showduplicates list python3 | expand | grep -P '(?!.*3.10.*)x86_64|(?!.*3.10.*)aarch64' | tail -n 1 | awk '{print $2}') - AMZN2_ARCH=$(uname -m) - sudo yum install -y python3-"$AMZN2_PY_LATEST"."$AMZN2_ARCH" git - elif type yum && [ -f "/etc/redhat-release" ] || [ -f "/etc/centos-release" ] || [ -f "/etc/fedora-release" ]; then - # CentOS or Redhat or Fedora - echo "Installing on CentOS/Redhat/Fedora." - fi + #LINUX=1 + if [ "$UBUNTU" = "true" ] && [ "$UBUNTU_PRE_2004" = "1" ]; then + # Ubuntu + echo "Installing on Ubuntu pre 20.04 LTS." + sudo apt-get update + sudo apt-get install -y python3.7-venv python3.7-distutils + elif [ "$UBUNTU" = "true" ] && [ "$UBUNTU_PRE_2004" = "0" ] && [ "$UBUNTU_2100" = "0" ]; then + echo "Installing on Ubuntu 20.04 LTS." + sudo apt-get update + sudo apt-get install -y python3.8-venv python3-distutils + elif [ "$UBUNTU" = "true" ] && [ "$UBUNTU_2100" = "1" ]; then + echo "Installing on Ubuntu 21.04 or newer." + sudo apt-get update + sudo apt-get install -y python3.9-venv python3-distutils + elif [ "$DEBIAN" = "true" ]; then + echo "Installing on Debian." + sudo apt-get update + sudo apt-get install -y python3-venv + elif type pacman >/dev/null 2>&1 && [ -f "/etc/arch-release" ]; then + # Arch Linux + echo "Installing on Arch Linux." + echo "Python <= 3.9.9 is required. Installing python-3.9.9-1" + case $(uname -m) in + x86_64) + sudo pacman ${PACMAN_AUTOMATED} -U --needed https://archive.archlinux.org/packages/p/python/python-3.9.9-1-x86_64.pkg.tar.zst + ;; + aarch64) + sudo pacman ${PACMAN_AUTOMATED} -U --needed http://tardis.tiny-vps.com/aarm/packages/p/python/python-3.9.9-1-aarch64.pkg.tar.xz + ;; + *) + echo "Incompatible CPU architecture. Must be x86_64 or aarch64." + exit 1 + ;; + esac + sudo pacman ${PACMAN_AUTOMATED} -S --needed git + elif type yum >/dev/null 2>&1 && [ ! -f "/etc/redhat-release" ] && [ ! -f "/etc/centos-release" ] && [ ! -f "/etc/fedora-release" ]; then + # AMZN 2 + echo "Installing on Amazon Linux 2." + if ! command -v python3.9 >/dev/null 2>&1; then + install_python3_and_sqlite3_from_source_with_yum + fi + elif type yum >/dev/null 2>&1 && [ -f "/etc/centos-release" ]; then + # CentOS + echo "Install on CentOS." + if ! command -v python3.9 >/dev/null 2>&1; then + install_python3_and_sqlite3_from_source_with_yum + fi + elif type yum >/dev/null 2>&1 && [ -f "/etc/redhat-release" ] || [ -f "/etc/fedora-release" ]; then + # Redhat or Fedora + echo "Installing on Redhat/Fedora." + if ! command -v python3.9 >/dev/null 2>&1; then + sudo yum install -y python39 + fi + fi elif [ "$(uname)" = "Darwin" ] && ! type brew >/dev/null 2>&1; then - echo "Installation currently requires brew on MacOS - https://brew.sh/" + echo "Installation currently requires brew on MacOS - https://brew.sh/" elif [ "$(uname)" = "OpenBSD" ]; then - export MAKE=${MAKE:-gmake} - export BUILD_VDF_CLIENT=${BUILD_VDF_CLIENT:-N} + export MAKE=${MAKE:-gmake} + export BUILD_VDF_CLIENT=${BUILD_VDF_CLIENT:-N} elif [ "$(uname)" = "FreeBSD" ]; then - export MAKE=${MAKE:-gmake} - export BUILD_VDF_CLIENT=${BUILD_VDF_CLIENT:-N} + export MAKE=${MAKE:-gmake} + export BUILD_VDF_CLIENT=${BUILD_VDF_CLIENT:-N} fi find_python() { - set +e - unset BEST_VERSION - for V in 39 3.9 38 3.8 37 3.7 3; do - if command -v python$V >/dev/null; then - if [ "$BEST_VERSION" = "" ]; then - BEST_VERSION=$V - if [ "$BEST_VERSION" = "3" ]; then - PY3_VERSION=$(python$BEST_VERSION --version | cut -d ' ' -f2) - if [[ "$PY3_VERSION" =~ 3.10.* ]]; then - echo "Chia requires Python version <= 3.9.9" - echo "Current Python version = $PY3_VERSION" - exit 1 - fi - fi - fi - fi - done - echo $BEST_VERSION - set -e + set +e + unset BEST_VERSION + for V in 39 3.9 38 3.8 37 3.7 3; do + if command -v python$V >/dev/null; then + if [ "$BEST_VERSION" = "" ]; then + BEST_VERSION=$V + if [ "$BEST_VERSION" = "3" ]; then + PY3_VERSION=$(python$BEST_VERSION --version | cut -d ' ' -f2) + if [[ "$PY3_VERSION" =~ 3.10.* ]]; then + echo "Chia requires Python version <= 3.9.9" + echo "Current Python version = $PY3_VERSION" + exit 1 + fi + fi + fi + fi + done + echo $BEST_VERSION + set -e } if [ "$INSTALL_PYTHON_VERSION" = "" ]; then - INSTALL_PYTHON_VERSION=$(find_python) + INSTALL_PYTHON_VERSION=$(find_python) fi # This fancy syntax sets INSTALL_PYTHON_PATH to "python3.7", unless @@ -156,10 +209,38 @@ fi INSTALL_PYTHON_PATH=python${INSTALL_PYTHON_VERSION:-3.7} +if ! command -v "$INSTALL_PYTHON_PATH" >/dev/null; then + echo "${INSTALL_PYTHON_PATH} was not found" + exit 1 +fi + echo "Python version is $INSTALL_PYTHON_VERSION" -$INSTALL_PYTHON_PATH -m venv venv + +# Check sqlite3 version bound to python +SQLITE_VERSION=$($INSTALL_PYTHON_PATH -c 'import sqlite3; print(sqlite3.sqlite_version)') +SQLITE_MAJOR_VER=$(echo "$SQLITE_VERSION" | cut -d'.' -f1) +SQLITE_MINOR_VER=$(echo "$SQLITE_VERSION" | cut -d'.' -f2) +echo "SQLite version of the Python is ${SQLITE_VERSION}" +if [ "$SQLITE_MAJOR_VER" -lt "3" ] || [ "$SQLITE_MAJOR_VER" = "3" ] && [ "$SQLITE_MINOR_VER" -lt "8" ]; then + echo "Only sqlite>=3.8 is supported" + exit 1 +fi + +# If version of `python` and "$INSTALL_PYTHON_VERSION" does not match, clear old version +VENV_CLEAR="" +if [ -e venv/bin/python ]; then + VENV_PYTHON_VER=$(venv/bin/python -V) + TARGET_PYTHON_VER=$($INSTALL_PYTHON_PATH -V) + if [ "$VENV_PYTHON_VER" != "$TARGET_PYTHON_VER" ]; then + echo "existing python version in venv is $VENV_PYTHON_VER while target python version is $TARGET_PYTHON_VER" + echo "Refreshing venv modules..." + VENV_CLEAR="--clear" + fi +fi + +$INSTALL_PYTHON_PATH -m venv venv $VENV_CLEAR if [ ! -f "activate" ]; then - ln -s venv/bin/activate . + ln -s venv/bin/activate . fi EXTRAS=${EXTRAS%,} diff --git a/start-gui.sh b/start-gui.sh new file mode 100755 index 000000000000..0a035898969b --- /dev/null +++ b/start-gui.sh @@ -0,0 +1,52 @@ +#!/bin/bash +set -e +export NODE_OPTIONS="--max-old-space-size=3000" + +SCRIPT_DIR=$(cd -- "$(dirname -- "$0")"; pwd) + +echo "### Checking GUI dependencies" + +if [ -d "${SCRIPT_DIR}/.n" ]; then + export N_PREFIX="${SCRIPT_DIR}/.n" + export PATH="${N_PREFIX}/bin:${PATH}" + echo "Loading nodejs/npm from" + echo " ${N_PREFIX}" +fi + +if [ -z "$VIRTUAL_ENV" ]; then + echo "This requires the chia python virtual environment." + echo "Execute '. ./activate' before running." + exit 1 +fi + +if ! npm version >/dev/null 2>&1; then + echo "Please install GUI dependencies by:" + echo " sh install-gui.sh" + echo "on ${SCRIPT_DIR}" + exit 1 +fi + +NPM_VERSION="$(npm -v | cut -d'.' -f 1)" +if [ "$NPM_VERSION" -lt "7" ]; then + echo "Current npm version($(npm -v)) is less than 7. GUI app requires npm>=7." + exit 1 +else + echo "Found npm $(npm -v)" +fi + +echo "### Checking GUI build" +GUI_BUILD_PATH="${SCRIPT_DIR}/chia-blockchain-gui/packages/gui/build/electron/main.js" +if [ ! -e "$GUI_BUILD_PATH" ]; then + echo "Error: GUI build was not found" + echo "It is expected at $GUI_BUILD_PATH" + echo "Please build GUI software by:" + echo " sh install-gui.sh" + exit 1 +else + echo "Found $GUI_BUILD_PATH" +fi + +echo "### Starting GUI" +cd "${SCRIPT_DIR}/chia-blockchain-gui/" +echo "npm run electron" +npm run electron From e47958605a8ed360f07591d14b59a468f26f00ad Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Sat, 12 Feb 2022 03:30:56 +0100 Subject: [PATCH 036/378] Tolerate missing hints (#10207) * update db-upgrade test to run faster, and also parameterized on whether the hints table is present * tolerate missing hints table in db conversion function --- chia/cmds/db_upgrade_func.py | 28 ++++--- tests/core/test_db_conversion.py | 133 ++++++++++++++++++------------- 2 files changed, 95 insertions(+), 66 deletions(-) diff --git a/chia/cmds/db_upgrade_func.py b/chia/cmds/db_upgrade_func.py index 0b1dd0354d38..e69a8dbc399a 100644 --- a/chia/cmds/db_upgrade_func.py +++ b/chia/cmds/db_upgrade_func.py @@ -1,4 +1,5 @@ from typing import Dict, Optional +import sqlite3 from pathlib import Path import sys from time import time @@ -237,18 +238,21 @@ async def convert_v1_to_v2(in_path: Path, out_path: Path) -> None: hint_values = [] await out_db.execute("CREATE TABLE hints(coin_id blob, hint blob, UNIQUE (coin_id, hint))") await out_db.commit() - async with in_db.execute("SELECT coin_id, hint FROM hints") as cursor: - count = 0 - await out_db.execute("begin transaction") - async for row in cursor: - hint_values.append((row[0], row[1])) - commit_in -= 1 - if commit_in == 0: - commit_in = HINT_COMMIT_RATE - await out_db.executemany("INSERT OR IGNORE INTO hints VALUES(?, ?)", hint_values) - await out_db.commit() - await out_db.execute("begin transaction") - hint_values = [] + try: + async with in_db.execute("SELECT coin_id, hint FROM hints") as cursor: + count = 0 + await out_db.execute("begin transaction") + async for row in cursor: + hint_values.append((row[0], row[1])) + commit_in -= 1 + if commit_in == 0: + commit_in = HINT_COMMIT_RATE + await out_db.executemany("INSERT OR IGNORE INTO hints VALUES(?, ?)", hint_values) + await out_db.commit() + await out_db.execute("begin transaction") + hint_values = [] + except sqlite3.OperationalError: + print(" no hints table, skipping") await out_db.executemany("INSERT OR IGNORE INTO hints VALUES (?, ?)", hint_values) await out_db.commit() diff --git a/tests/core/test_db_conversion.py b/tests/core/test_db_conversion.py index 8a8dfd415888..6a12c3ae0fb4 100644 --- a/tests/core/test_db_conversion.py +++ b/tests/core/test_db_conversion.py @@ -2,26 +2,28 @@ import aiosqlite import tempfile import random +import asyncio from pathlib import Path from typing import List, Tuple -from tests.blockchain.blockchain_test_utils import _validate_and_add_block -from tests.setup_nodes import bt, test_constants +from tests.setup_nodes import test_constants from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.util.ints import uint32, uint64 from chia.cmds.db_upgrade_func import convert_v1_to_v2 from chia.util.db_wrapper import DBWrapper from chia.full_node.block_store import BlockStore from chia.full_node.coin_store import CoinStore from chia.full_node.hint_store import HintStore from chia.consensus.blockchain import Blockchain +from chia.consensus.multiprocess_validation import PreValidationResult class TempFile: def __init__(self): self.path = Path(tempfile.NamedTemporaryFile().name) - def __enter__(self) -> DBWrapper: + def __enter__(self) -> Path: if self.path.exists(): self.path.unlink() return self.path @@ -37,11 +39,18 @@ def rand_bytes(num) -> bytes: return bytes(ret) +@pytest.fixture(scope="session") +def event_loop(): + loop = asyncio.get_event_loop() + yield loop + + class TestDbUpgrade: @pytest.mark.asyncio - async def test_blocks(self): + @pytest.mark.parametrize("with_hints", [True, False]) + async def test_blocks(self, default_1000_blocks, with_hints: bool): - blocks = bt.get_consecutive_blocks(758) + blocks = default_1000_blocks hints: List[Tuple[bytes32, bytes]] = [] for i in range(351): @@ -66,13 +75,20 @@ async def test_blocks(self): with TempFile() as in_file, TempFile() as out_file: async with aiosqlite.connect(in_file) as conn: + + await conn.execute("pragma journal_mode=OFF") + await conn.execute("pragma synchronous=OFF") + await conn.execute("pragma locking_mode=exclusive") + db_wrapper1 = DBWrapper(conn, 1) block_store1 = await BlockStore.create(db_wrapper1) - coin_store1 = await CoinStore.create(db_wrapper1, 0) - hint_store1 = await HintStore.create(db_wrapper1) - - for hint in hints: - await hint_store1.add_hints([(hint[0], hint[1])]) + coin_store1 = await CoinStore.create(db_wrapper1, uint32(0)) + if with_hints: + hint_store1 = await HintStore.create(db_wrapper1) + for h in hints: + await hint_store1.add_hints([(h[0], h[1])]) + else: + hint_store1 = None bc = await Blockchain.create( coin_store1, block_store1, test_constants, hint_store1, Path("."), reserved_cores=0 @@ -80,51 +96,60 @@ async def test_blocks(self): await db_wrapper1.commit_transaction() for block in blocks: - await _validate_and_add_block(bc, block) + # await _validate_and_add_block(bc, block) + results = PreValidationResult(None, uint64(1), None, False) + result, err, _, _ = await bc.receive_block(block, results) + assert err is None - # now, convert v1 in_file to v2 out_file - await convert_v1_to_v2(in_file, out_file) + # now, convert v1 in_file to v2 out_file + await convert_v1_to_v2(in_file, out_file) - async with aiosqlite.connect(out_file) as conn2: - db_wrapper2 = DBWrapper(conn2, 2) - block_store2 = await BlockStore.create(db_wrapper2) - coin_store2 = await CoinStore.create(db_wrapper2, 0) - hint_store2 = await HintStore.create(db_wrapper2) + async with aiosqlite.connect(in_file) as conn, aiosqlite.connect(out_file) as conn2: + db_wrapper1 = DBWrapper(conn, 1) + block_store1 = await BlockStore.create(db_wrapper1) + coin_store1 = await CoinStore.create(db_wrapper1, uint32(0)) + if with_hints: + hint_store1 = await HintStore.create(db_wrapper1) + else: + hint_store1 = None + + db_wrapper2 = DBWrapper(conn2, 2) + block_store2 = await BlockStore.create(db_wrapper2) + coin_store2 = await CoinStore.create(db_wrapper2, uint32(0)) + hint_store2 = await HintStore.create(db_wrapper2) + + if with_hints: # check hints - for hint in hints: - assert hint[0] in await hint_store1.get_coin_ids(hint[1]) - assert hint[0] in await hint_store2.get_coin_ids(hint[1]) - - # check peak - assert await block_store1.get_peak() == await block_store2.get_peak() - - # check blocks - for block in blocks: - hh = block.header_hash - height = block.height - assert await block_store1.get_full_block(hh) == await block_store2.get_full_block(hh) - assert await block_store1.get_full_block_bytes(hh) == await block_store2.get_full_block_bytes( - hh - ) - assert await block_store1.get_full_blocks_at([height]) == await block_store2.get_full_blocks_at( - [height] - ) - assert await block_store1.get_block_records_by_hash( - [hh] - ) == await block_store2.get_block_records_by_hash([hh]) - assert await block_store1.get_block_record(hh) == await block_store2.get_block_record(hh) - assert await block_store1.is_fully_compactified(hh) == await block_store2.is_fully_compactified( - hh - ) - - # check coins - for block in blocks: - coins = await coin_store1.get_coins_added_at_height(block.height) - assert await coin_store2.get_coins_added_at_height(block.height) == coins - assert await coin_store1.get_coins_removed_at_height( - block.height - ) == await coin_store2.get_coins_removed_at_height(block.height) - for c in coins: - n = c.coin.name() - assert await coin_store1.get_coin_record(n) == await coin_store2.get_coin_record(n) + for h in hints: + assert h[0] in await hint_store1.get_coin_ids(h[1]) + assert h[0] in await hint_store2.get_coin_ids(h[1]) + + # check peak + assert await block_store1.get_peak() == await block_store2.get_peak() + + # check blocks + for block in blocks: + hh = block.header_hash + height = block.height + assert await block_store1.get_full_block(hh) == await block_store2.get_full_block(hh) + assert await block_store1.get_full_block_bytes(hh) == await block_store2.get_full_block_bytes(hh) + assert await block_store1.get_full_blocks_at([height]) == await block_store2.get_full_blocks_at( + [height] + ) + assert await block_store1.get_block_records_by_hash( + [hh] + ) == await block_store2.get_block_records_by_hash([hh]) + assert await block_store1.get_block_record(hh) == await block_store2.get_block_record(hh) + assert await block_store1.is_fully_compactified(hh) == await block_store2.is_fully_compactified(hh) + + # check coins + for block in blocks: + coins = await coin_store1.get_coins_added_at_height(block.height) + assert await coin_store2.get_coins_added_at_height(block.height) == coins + assert await coin_store1.get_coins_removed_at_height( + block.height + ) == await coin_store2.get_coins_removed_at_height(block.height) + for c in coins: + n = c.coin.name() + assert await coin_store1.get_coin_record(n) == await coin_store2.get_coin_record(n) From 7fa1861def5797f63edfd2ad54f3efb77b52695b Mon Sep 17 00:00:00 2001 From: Mariano Sorgente <3069354+mariano54@users.noreply.github.com> Date: Mon, 14 Feb 2022 14:28:36 -0500 Subject: [PATCH 037/378] Ms.wallet refactor (#10146) * wallet fixes * Don't show false positive synched * Code cleanup and lint * Fixes * Revert issue * Fix reorg issue * WIP wallet node * More wallet refactor * More wallet changes * More * Fix subscription bugs * Fix some tests * Fix pool tests * More tweaks * Lint and small issues * call update_ui at the correct points * Small changes * new peak queue * Fix peer height issue * Rollback more for safety, and tweak logging * Small WSM style fixes * Change fork point in long_sync * More fixes with real world testing * Fix reversed filter * Fix function name * Fix coin store bug properly * Raise CancelledError * Fix rollback issue * Lint * Small fix * Fix CAT issue * Fix test trades race condition * Fix test trades race condition * Try to reduce flakiness * Test coin store and fix additional method * Improve flakiness --- chia/cmds/show.py | 109 +-- chia/cmds/wallet_funcs.py | 6 + chia/full_node/coin_store.py | 16 +- chia/full_node/full_node.py | 2 +- chia/full_node/full_node_api.py | 10 +- chia/pools/pool_wallet.py | 2 +- chia/util/merkle_set.py | 10 +- chia/util/network.py | 14 +- chia/wallet/cat_wallet/cat_wallet.py | 2 +- chia/wallet/key_val_store.py | 5 +- chia/wallet/rl_wallet/rl_wallet.py | 3 +- chia/wallet/trade_manager.py | 4 +- chia/wallet/util/new_peak_queue.py | 70 ++ chia/wallet/util/peer_request_cache.py | 51 ++ chia/wallet/util/wallet_sync_utils.py | 76 +- chia/wallet/wallet_blockchain.py | 17 +- chia/wallet/wallet_node.py | 938 ++++++++++----------- chia/wallet/wallet_node_api.py | 6 +- chia/wallet/wallet_puzzle_store.py | 2 +- chia/wallet/wallet_state_manager.py | 91 +- tests/core/full_node/test_coin_store.py | 42 + tests/core/full_node/test_full_node.py | 30 +- tests/core/server/test_rate_limits.py | 8 +- tests/pools/test_pool_rpc.py | 44 +- tests/wallet/cat_wallet/test_cat_wallet.py | 2 + tests/wallet/cat_wallet/test_trades.py | 5 + tests/wallet/rpc/test_wallet_rpc.py | 2 + tests/wallet/sync/test_wallet_sync.py | 10 +- 28 files changed, 932 insertions(+), 645 deletions(-) create mode 100644 chia/wallet/util/new_peak_queue.py create mode 100644 chia/wallet/util/peer_request_cache.py diff --git a/chia/cmds/show.py b/chia/cmds/show.py index c28197292411..20e47d33959a 100644 --- a/chia/cmds/show.py +++ b/chia/cmds/show.py @@ -1,8 +1,58 @@ -from typing import Any, Optional, Union +from typing import Any, Optional, Union, Dict from chia.types.blockchain_format.sized_bytes import bytes32 import click +from chia.util.network import is_trusted_inner + + +async def print_connections(client, time, NodeType, trusted_peers: Dict): + connections = await client.get_connections() + print("Connections:") + print("Type IP Ports NodeID Last Connect" + " MiB Up|Dwn") + for con in connections: + last_connect_tuple = time.struct_time(time.localtime(con["last_message_time"])) + last_connect = time.strftime("%b %d %T", last_connect_tuple) + mb_down = con["bytes_read"] / (1024 * 1024) + mb_up = con["bytes_written"] / (1024 * 1024) + + host = con["peer_host"] + # Strip IPv6 brackets + host = host.strip("[]") + + trusted: bool = is_trusted_inner(host, con["node_id"], trusted_peers, False) + # Nodetype length is 9 because INTRODUCER will be deprecated + if NodeType(con["type"]) is NodeType.FULL_NODE: + peak_height = con.get("peak_height", None) + connection_peak_hash = con.get("peak_hash", None) + if connection_peak_hash is None: + connection_peak_hash = "No Info" + else: + if connection_peak_hash.startswith(("0x", "0X")): + connection_peak_hash = connection_peak_hash[2:] + connection_peak_hash = f"{connection_peak_hash[:8]}..." + con_str = ( + f"{NodeType(con['type']).name:9} {host:38} " + f"{con['peer_port']:5}/{con['peer_server_port']:<5}" + f" {con['node_id'].hex()[:8]}... " + f"{last_connect} " + f"{mb_up:7.1f}|{mb_down:<7.1f}" + f"\n " + ) + if peak_height is not None: + con_str += f"-Height: {peak_height:8.0f} -Hash: {connection_peak_hash} -Trusted: {trusted}" + else: + con_str += f"-Height: No Info -Hash: {connection_peak_hash} -Trusted: {trusted}" + else: + con_str = ( + f"{NodeType(con['type']).name:9} {host:38} " + f"{con['peer_port']:5}/{con['peer_server_port']:<5}" + f" {con['node_id'].hex()[:8]}... " + f"{last_connect} " + f"{mb_up:7.1f}|{mb_down:<7.1f}" + ) + print(con_str) + async def show_async( rpc_port: Optional[int], @@ -15,10 +65,8 @@ async def show_async( block_by_header_hash: str, ) -> None: import aiohttp - import time import traceback - - from time import localtime, struct_time + import time from typing import List, Optional from chia.consensus.block_record import BlockRecord from chia.rpc.full_node_rpc_client import FullNodeRpcClient @@ -84,7 +132,7 @@ async def show_async( while curr is not None and not curr.is_transaction_block: curr = await client.get_block_record(curr.prev_hash) peak_time = curr.timestamp - peak_time_struct = struct_time(localtime(peak_time)) + peak_time_struct = time.struct_time(time.localtime(peak_time)) print( " Time:", @@ -115,51 +163,8 @@ async def show_async( if show_connections: print("") if show_connections: - connections = await client.get_connections() - print("Connections:") - print( - "Type IP Ports NodeID Last Connect" - + " MiB Up|Dwn" - ) - for con in connections: - last_connect_tuple = struct_time(localtime(con["last_message_time"])) - last_connect = time.strftime("%b %d %T", last_connect_tuple) - mb_down = con["bytes_read"] / (1024 * 1024) - mb_up = con["bytes_written"] / (1024 * 1024) - - host = con["peer_host"] - # Strip IPv6 brackets - host = host.strip("[]") - # Nodetype length is 9 because INTRODUCER will be deprecated - if NodeType(con["type"]) is NodeType.FULL_NODE: - peak_height = con["peak_height"] - connection_peak_hash = con["peak_hash"] - if connection_peak_hash is None: - connection_peak_hash = "No Info" - else: - if connection_peak_hash.startswith(("0x", "0X")): - connection_peak_hash = connection_peak_hash[2:] - connection_peak_hash = f"{connection_peak_hash[:8]}..." - if peak_height is None: - peak_height = 0 - con_str = ( - f"{NodeType(con['type']).name:9} {host:38} " - f"{con['peer_port']:5}/{con['peer_server_port']:<5}" - f" {con['node_id'].hex()[:8]}... " - f"{last_connect} " - f"{mb_up:7.1f}|{mb_down:<7.1f}" - f"\n " - f"-SB Height: {peak_height:8.0f} -Hash: {connection_peak_hash}" - ) - else: - con_str = ( - f"{NodeType(con['type']).name:9} {host:38} " - f"{con['peer_port']:5}/{con['peer_server_port']:<5}" - f" {con['node_id'].hex()[:8]}... " - f"{last_connect} " - f"{mb_up:7.1f}|{mb_down:<7.1f}" - ) - print(con_str) + trusted_peers: Dict = config["full_node"].get("trusted_peers", {}) + await print_connections(client, time, NodeType, trusted_peers) # if called together with state, leave a blank line if state: print("") @@ -217,8 +222,8 @@ async def show_async( difficulty = block.weight if block.is_transaction_block: assert full_block.transactions_info is not None - block_time = struct_time( - localtime( + block_time = time.struct_time( + time.localtime( full_block.foliage_transaction_block.timestamp if full_block.foliage_transaction_block else None diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index b9987d1c42cb..b468b8531edb 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -8,8 +8,10 @@ import aiohttp +from chia.cmds.show import print_connections from chia.cmds.units import units from chia.rpc.wallet_rpc_client import WalletRpcClient +from chia.server.outbound_message import NodeType from chia.server.start_wallet import SERVICE_NAME from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.bech32m import encode_puzzle_hash @@ -471,6 +473,10 @@ async def print_balances(args: dict, wallet_client: WalletRpcClient, fingerprint ) print(f" -Spendable: {print_balance(balances['spendable_balance'], scale, address_prefix)}") + print(" ") + trusted_peers: Dict = config.get("trusted_peers", {}) + await print_connections(wallet_client, time, NodeType, trusted_peers) + async def get_wallet(wallet_client: WalletRpcClient, fingerprint: int = None) -> Optional[Tuple[WalletRpcClient, int]]: if fingerprint is not None: diff --git a/chia/full_node/coin_store.py b/chia/full_node/coin_store.py index 25affe3e0b70..6b4e25a4f70c 100644 --- a/chia/full_node/coin_store.py +++ b/chia/full_node/coin_store.py @@ -305,8 +305,7 @@ async def get_coin_states_by_puzzle_hashes( self, include_spent_coins: bool, puzzle_hashes: List[bytes32], - start_height: uint32 = uint32(0), - end_height: uint32 = uint32((2 ** 32) - 1), + min_height: uint32 = uint32(0), ) -> List[CoinState]: if len(puzzle_hashes) == 0: return [] @@ -321,9 +320,9 @@ async def get_coin_states_by_puzzle_hashes( f"SELECT confirmed_index, spent_index, coinbase, puzzle_hash, " f"coin_parent, amount, timestamp FROM coin_record INDEXED BY coin_puzzle_hash " f'WHERE puzzle_hash in ({"?," * (len(puzzle_hashes) - 1)}?) ' - f"AND confirmed_index>=? AND confirmed_index=? OR spent_index>=?)" f"{'' if include_spent_coins else 'AND spent_index=0'}", - puzzle_hashes_db + (start_height, end_height), + puzzle_hashes_db + (min_height, min_height), ) as cursor: for row in await cursor.fetchall(): @@ -360,12 +359,11 @@ async def get_coin_records_by_parent_ids( coins.add(CoinRecord(coin, row[0], row[1], row[2], row[6])) return list(coins) - async def get_coin_state_by_ids( + async def get_coin_states_by_ids( self, include_spent_coins: bool, coin_ids: List[bytes32], - start_height: uint32 = uint32(0), - end_height: uint32 = uint32((2 ** 32) - 1), + min_height: uint32 = uint32(0), ) -> List[CoinState]: if len(coin_ids) == 0: return [] @@ -379,9 +377,9 @@ async def get_coin_state_by_ids( async with self.coin_record_db.execute( f"SELECT confirmed_index, spent_index, coinbase, puzzle_hash, " f'coin_parent, amount, timestamp FROM coin_record WHERE coin_name in ({"?," * (len(coin_ids) - 1)}?) ' - f"AND confirmed_index>=? AND confirmed_index=? OR spent_index>=?)" f"{'' if include_spent_coins else 'AND spent_index=0'}", - coin_ids_db + (start_height, end_height), + coin_ids_db + (min_height, min_height), ) as cursor: for row in await cursor.fetchall(): diff --git a/chia/full_node/full_node.py b/chia/full_node/full_node.py index 97f3cdebc26e..d0ac3fe2a713 100644 --- a/chia/full_node/full_node.py +++ b/chia/full_node/full_node.py @@ -939,10 +939,10 @@ async def validate_block_batches(batch_queue): await peer.close(600) raise ValueError(f"Failed to validate block batch {start_height} to {end_height}") self.log.info(f"Added blocks {start_height} to {end_height}") - await self.send_peak_to_wallets() peak = self.blockchain.get_peak() if len(coin_states) > 0 and fork_height is not None: await self.update_wallets(peak.height, fork_height, peak.header_hash, coin_states) + await self.send_peak_to_wallets() self.blockchain.clean_block_record(end_height - self.constants.BLOCKS_CACHE_SIZE) loop = asyncio.get_event_loop() diff --git a/chia/full_node/full_node_api.py b/chia/full_node/full_node_api.py index 9f746ea78653..c660210a3aaf 100644 --- a/chia/full_node/full_node_api.py +++ b/chia/full_node/full_node_api.py @@ -1436,12 +1436,12 @@ async def register_interest_in_puzzle_hash( # Send all coins with requested puzzle hash that have been created after the specified height states: List[CoinState] = await self.full_node.coin_store.get_coin_states_by_puzzle_hashes( - include_spent_coins=True, puzzle_hashes=request.puzzle_hashes, start_height=request.min_height + include_spent_coins=True, puzzle_hashes=request.puzzle_hashes, min_height=request.min_height ) if len(hint_coin_ids) > 0: - hint_states = await self.full_node.coin_store.get_coin_state_by_ids( - include_spent_coins=True, coin_ids=hint_coin_ids, start_height=request.min_height + hint_states = await self.full_node.coin_store.get_coin_states_by_ids( + include_spent_coins=True, coin_ids=hint_coin_ids, min_height=request.min_height ) states.extend(hint_states) @@ -1471,8 +1471,8 @@ async def register_interest_in_coin( self.full_node.peer_coin_ids[peer.peer_node_id].add(coin_id) self.full_node.peer_sub_counter[peer.peer_node_id] += 1 - states: List[CoinState] = await self.full_node.coin_store.get_coin_state_by_ids( - include_spent_coins=True, coin_ids=request.coin_ids, start_height=request.min_height + states: List[CoinState] = await self.full_node.coin_store.get_coin_states_by_ids( + include_spent_coins=True, coin_ids=request.coin_ids, min_height=request.min_height ) response = wallet_protocol.RespondToCoinUpdates(request.coin_ids, request.min_height, states) diff --git a/chia/pools/pool_wallet.py b/chia/pools/pool_wallet.py index 595db32838f7..e45c03886b3b 100644 --- a/chia/pools/pool_wallet.py +++ b/chia/pools/pool_wallet.py @@ -350,7 +350,7 @@ async def create( p2_puzzle_hash: bytes32 = (await self.get_current_state()).p2_singleton_puzzle_hash await self.wallet_state_manager.add_new_wallet(self, self.wallet_info.id, create_puzzle_hashes=False) - await self.wallet_state_manager.add_interested_puzzle_hash(p2_puzzle_hash, self.wallet_id, False) + await self.wallet_state_manager.add_interested_puzzle_hashes([p2_puzzle_hash], [self.wallet_id], False) return self @staticmethod diff --git a/chia/util/merkle_set.py b/chia/util/merkle_set.py index 7f16fa1dd7da..857b8c31d4d1 100644 --- a/chia/util/merkle_set.py +++ b/chia/util/merkle_set.py @@ -349,23 +349,23 @@ class SetError(Exception): pass -def confirm_included(root: Node, val: bytes, proof: bytes32) -> bool: +def confirm_included(root: bytes32, val: bytes, proof: bytes32) -> bool: return confirm_not_included_already_hashed(root, sha256(val).digest(), proof) -def confirm_included_already_hashed(root: Node, val: bytes, proof: bytes) -> bool: +def confirm_included_already_hashed(root: bytes32, val: bytes, proof: bytes) -> bool: return _confirm(root, val, proof, True) -def confirm_not_included(root: Node, val: bytes, proof: bytes32) -> bool: +def confirm_not_included(root: bytes32, val: bytes, proof: bytes32) -> bool: return confirm_not_included_already_hashed(root, sha256(val).digest(), proof) -def confirm_not_included_already_hashed(root: Node, val: bytes, proof: bytes) -> bool: +def confirm_not_included_already_hashed(root: bytes32, val: bytes, proof: bytes) -> bool: return _confirm(root, val, proof, False) -def _confirm(root: Node, val: bytes, proof: bytes, expected: bool) -> bool: +def _confirm(root: bytes32, val: bytes, proof: bytes, expected: bool) -> bool: try: p = deserialize_proof(proof) if p.get_root() != root: diff --git a/chia/util/network.py b/chia/util/network.py index 2985505a40fc..3bce97ac5fef 100644 --- a/chia/util/network.py +++ b/chia/util/network.py @@ -1,7 +1,8 @@ import socket from ipaddress import ip_address, IPv4Network, IPv6Network -from typing import Iterable, List, Tuple, Union, Any, Optional +from typing import Iterable, List, Tuple, Union, Any, Optional, Dict from chia.server.outbound_message import NodeType +from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.peer_info import PeerInfo from chia.util.ints import uint16 @@ -72,3 +73,14 @@ def get_host_addr(host: Union[PeerInfo, str], prefer_ipv6: Optional[bool]) -> st return t[4][0] # If neither matched preference, just return the first available return addrset[0][4][0] + + +def is_trusted_inner(peer_host: str, peer_node_id: bytes32, trusted_peers: Dict, testing: bool) -> bool: + if trusted_peers is None: + return False + if not testing and peer_host == "127.0.0.1": + return True + if peer_node_id.hex() not in trusted_peers: + return False + + return True diff --git a/chia/wallet/cat_wallet/cat_wallet.py b/chia/wallet/cat_wallet/cat_wallet.py index 34f1445b9062..aa338b87824e 100644 --- a/chia/wallet/cat_wallet/cat_wallet.py +++ b/chia/wallet/cat_wallet/cat_wallet.py @@ -284,7 +284,7 @@ async def coin_added(self, coin: Coin, height: uint32): for node_id, node in self.wallet_state_manager.wallet_node.server.all_connections.items(): try: coin_state = await self.wallet_state_manager.wallet_node.get_coin_state( - [coin.parent_coin_info], node + [coin.parent_coin_info], None, node ) assert coin_state[0].coin.name() == coin.parent_coin_info coin_spend = await self.wallet_state_manager.wallet_node.fetch_puzzle_solution( diff --git a/chia/wallet/key_val_store.py b/chia/wallet/key_val_store.py index 3a2a509429d7..1fbdd05a37eb 100644 --- a/chia/wallet/key_val_store.py +++ b/chia/wallet/key_val_store.py @@ -3,7 +3,6 @@ import aiosqlite from chia.util.db_wrapper import DBWrapper -from chia.util.streamable import Streamable class KeyValStore: @@ -47,9 +46,9 @@ async def get_object(self, key: str, object_type: Any) -> Any: return object_type.from_bytes(row[1]) - async def set_object(self, key: str, obj: Streamable): + async def set_object(self, key: str, obj: Any): """ - Adds object to key val store + Adds object to key val store. Obj MUST support __bytes__ and bytes() methods. """ async with self.db_wrapper.lock: cursor = await self.db_connection.execute( diff --git a/chia/wallet/rl_wallet/rl_wallet.py b/chia/wallet/rl_wallet/rl_wallet.py index b18e0372cc04..abfd7126f9b1 100644 --- a/chia/wallet/rl_wallet/rl_wallet.py +++ b/chia/wallet/rl_wallet/rl_wallet.py @@ -1,5 +1,4 @@ # RLWallet is subclass of Wallet -import asyncio import json import time from dataclasses import dataclass @@ -324,7 +323,7 @@ async def aggregate_this_coin(self, coin: Coin): memos=list(compute_memos(spend_bundle).items()), ) - asyncio.create_task(self.push_transaction(tx_record)) + await self.push_transaction(tx_record) async def rl_available_balance(self) -> uint64: self.rl_coin_record = await self._get_rl_coin_record() diff --git a/chia/wallet/trade_manager.py b/chia/wallet/trade_manager.py index 52b2f0d0cb6f..6c8303ce3126 100644 --- a/chia/wallet/trade_manager.py +++ b/chia/wallet/trade_manager.py @@ -81,7 +81,7 @@ async def get_trade_by_coin(self, coin: Coin) -> Optional[TradeRecord]: return trade return None - async def coins_of_interest_farmed(self, coin_state: CoinState): + async def coins_of_interest_farmed(self, coin_state: CoinState, fork_height: Optional[uint32]): """ If both our coins and other coins in trade got removed that means that trade was successfully executed If coins from other side of trade got farmed without ours, that means that trade failed because either someone @@ -110,7 +110,7 @@ async def coins_of_interest_farmed(self, coin_state: CoinState): our_settlement_ids: List[bytes32] = [c.name() for c in our_settlement_payments] # And get all relevant coin states - coin_states = await self.wallet_state_manager.wallet_node.get_coin_state(our_settlement_ids) + coin_states = await self.wallet_state_manager.wallet_node.get_coin_state(our_settlement_ids, fork_height) assert coin_states is not None coin_state_names: List[bytes32] = [cs.coin.name() for cs in coin_states] diff --git a/chia/wallet/util/new_peak_queue.py b/chia/wallet/util/new_peak_queue.py new file mode 100644 index 000000000000..846ccf814395 --- /dev/null +++ b/chia/wallet/util/new_peak_queue.py @@ -0,0 +1,70 @@ +import asyncio +import dataclasses +from enum import IntEnum +from typing import Any, List + +from chia.protocols.wallet_protocol import CoinStateUpdate, NewPeakWallet +from chia.server.ws_connection import WSChiaConnection +from chia.types.blockchain_format.sized_bytes import bytes32 + + +class NewPeakQueueTypes(IntEnum): + # Lower number means higher priority in the queue + COIN_ID_SUBSCRIPTION = 1 + PUZZLE_HASH_SUBSCRIPTION = 2 + FULL_NODE_STATE_UPDATED = 3 + NEW_PEAK_WALLET = 4 + + +@dataclasses.dataclass +class NewPeakItem: + item_type: NewPeakQueueTypes + data: Any + + def __lt__(self, other): + if self.item_type != other.item_type: + return self.item_type < other.item_type + if self.item_type in {NewPeakQueueTypes.COIN_ID_SUBSCRIPTION, NewPeakQueueTypes.PUZZLE_HASH_SUBSCRIPTION}: + return False # All subscriptions are equal + return self.data[0].height < other.data[0].height + + def __le__(self, other): + if self.item_type != other.item_type: + return self.item_type < other.item_type + if self.item_type in {NewPeakQueueTypes.COIN_ID_SUBSCRIPTION, NewPeakQueueTypes.PUZZLE_HASH_SUBSCRIPTION}: + return True # All subscriptions are equal + return self.data[0].height <= other.data[0].height + + def __gt__(self, other): + if self.item_type != other.item_type: + return self.item_type > other.item_type + if self.item_type in {NewPeakQueueTypes.COIN_ID_SUBSCRIPTION, NewPeakQueueTypes.PUZZLE_HASH_SUBSCRIPTION}: + return False # All subscriptions are equal + return self.data[0].height > other.data[0].height + + def __ge__(self, other): + if self.item_type != other.item_type: + return self.item_type > other.item_type + if self.item_type in {NewPeakQueueTypes.COIN_ID_SUBSCRIPTION, NewPeakQueueTypes.PUZZLE_HASH_SUBSCRIPTION}: + return True # All subscriptions are equal + return self.data[0].height >= other.data[0].height + + +class NewPeakQueue: + def __init__(self, inner_queue: asyncio.PriorityQueue): + self._inner_queue: asyncio.PriorityQueue = inner_queue + + async def subscribe_to_coin_ids(self, coin_ids: List[bytes32]): + await self._inner_queue.put(NewPeakItem(NewPeakQueueTypes.COIN_ID_SUBSCRIPTION, coin_ids)) + + async def subscribe_to_puzzle_hashes(self, puzzle_hashes: List[bytes32]): + await self._inner_queue.put(NewPeakItem(NewPeakQueueTypes.PUZZLE_HASH_SUBSCRIPTION, puzzle_hashes)) + + async def full_node_state_updated(self, coin_state_update: CoinStateUpdate, peer: WSChiaConnection): + await self._inner_queue.put(NewPeakItem(NewPeakQueueTypes.FULL_NODE_STATE_UPDATED, (coin_state_update, peer))) + + async def new_peak_wallet(self, new_peak: NewPeakWallet, peer: WSChiaConnection): + await self._inner_queue.put(NewPeakItem(NewPeakQueueTypes.NEW_PEAK_WALLET, (new_peak, peer))) + + async def get(self) -> NewPeakItem: + return await self._inner_queue.get() diff --git a/chia/wallet/util/peer_request_cache.py b/chia/wallet/util/peer_request_cache.py new file mode 100644 index 000000000000..12869ba04f53 --- /dev/null +++ b/chia/wallet/util/peer_request_cache.py @@ -0,0 +1,51 @@ +from typing import Dict, Tuple, Any, Optional, List + +from chia.protocols.wallet_protocol import CoinState +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.header_block import HeaderBlock +from chia.util.ints import uint32 + + +class PeerRequestCache: + blocks: Dict[uint32, HeaderBlock] + block_requests: Dict[Tuple[int, int], Any] + ses_requests: Dict[int, Any] + states_validated: Dict[bytes32, CoinState] + + def __init__(self): + self.blocks = {} + self.ses_requests = {} + self.block_requests = {} + self.states_validated = {} + + def clear_after_height(self, height: int): + # Remove any cached item which relates to an event that happened at a height above height. + self.blocks = {k: v for k, v in self.blocks.items() if k <= height} + self.block_requests = {k: v for k, v in self.block_requests.items() if k[0] <= height and k[1] <= height} + self.ses_requests = {k: v for k, v in self.ses_requests.items() if k <= height} + + remove_keys_states: List[bytes32] = [] + for k4, coin_state in self.states_validated.items(): + if coin_state.created_height is not None and coin_state.created_height > height: + remove_keys_states.append(k4) + elif coin_state.spent_height is not None and coin_state.spent_height > height: + remove_keys_states.append(k4) + for k5 in remove_keys_states: + self.states_validated.pop(k5) + + +async def can_use_peer_request_cache( + coin_state: CoinState, peer_request_cache: PeerRequestCache, fork_height: Optional[uint32] +): + if coin_state.get_hash() not in peer_request_cache.states_validated: + return False + if fork_height is None: + return True + if coin_state.created_height is None and coin_state.spent_height is None: + # Performing a reorg + return False + if coin_state.created_height is not None and coin_state.created_height > fork_height: + return False + if coin_state.spent_height is not None and coin_state.spent_height > fork_height: + return False + return True diff --git a/chia/wallet/util/wallet_sync_utils.py b/chia/wallet/util/wallet_sync_utils.py index 8a8e5090ce1e..bce674b07400 100644 --- a/chia/wallet/util/wallet_sync_utils.py +++ b/chia/wallet/util/wallet_sync_utils.py @@ -1,6 +1,8 @@ +import logging from typing import List, Optional, Tuple, Union, Dict from chia.consensus.constants import ConsensusConstants +from chia.protocols import wallet_protocol from chia.protocols.wallet_protocol import ( RequestAdditions, RespondAdditions, @@ -8,17 +10,73 @@ RejectRemovalsRequest, RespondRemovals, RequestRemovals, + RespondBlockHeader, + CoinState, + RespondToPhUpdates, + RespondToCoinUpdates, ) +from chia.server.ws_connection import WSChiaConnection from chia.types.blockchain_format.coin import hash_coin_list, Coin from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.full_block import FullBlock +from chia.types.header_block import HeaderBlock +from chia.util.ints import uint32 from chia.util.merkle_set import confirm_not_included_already_hashed, confirm_included_already_hashed, MerkleSet +log = logging.getLogger(__name__) + + +async def fetch_last_tx_from_peer(height: uint32, peer: WSChiaConnection) -> Optional[HeaderBlock]: + request_height: int = height + while True: + if request_height == -1: + return None + request = wallet_protocol.RequestBlockHeader(uint32(request_height)) + response: Optional[RespondBlockHeader] = await peer.request_block_header(request) + if response is not None and isinstance(response, RespondBlockHeader): + if response.header_block.is_transaction_block: + return response.header_block + else: + break + request_height = request_height - 1 + return None + + +async def subscribe_to_phs( + puzzle_hashes: List[bytes32], + peer: WSChiaConnection, + min_height: int, +) -> List[CoinState]: + """ + Tells full nodes that we are interested in puzzle hashes, and returns the response. + """ + msg = wallet_protocol.RegisterForPhUpdates(puzzle_hashes, uint32(max(min_height, uint32(0)))) + all_coins_state: Optional[RespondToPhUpdates] = await peer.register_interest_in_puzzle_hash(msg) + if all_coins_state is not None: + return all_coins_state.coin_states + return [] + + +async def subscribe_to_coin_updates( + coin_names: List[bytes32], + peer: WSChiaConnection, + min_height: int, +) -> List[CoinState]: + """ + Tells full nodes that we are interested in coin ids, and returns the response. + """ + msg = wallet_protocol.RegisterForCoinUpdates(coin_names, uint32(max(0, min_height))) + all_coins_state: Optional[RespondToCoinUpdates] = await peer.register_interest_in_coin(msg) + if all_coins_state is not None: + return all_coins_state.coin_states + return [] + + def validate_additions( coins: List[Tuple[bytes32, List[Coin]]], proofs: Optional[List[Tuple[bytes32, bytes, Optional[bytes]]]], - root, + root: bytes32, ): if proofs is None: # Verify root @@ -77,7 +135,9 @@ def validate_additions( return True -def validate_removals(coins, proofs, root): +def validate_removals( + coins: List[Tuple[bytes32, Optional[Coin]]], proofs: Optional[List[Tuple[bytes32, bytes]]], root: bytes32 +): if proofs is None: # If there are no proofs, it means all removals were returned in the response. # we must find the ones relevant to our wallets. @@ -124,7 +184,9 @@ def validate_removals(coins, proofs, root): return True -async def request_and_validate_removals(peer, height, header_hash, coin_name, removals_root) -> bool: +async def request_and_validate_removals( + peer: WSChiaConnection, height: uint32, header_hash: bytes32, coin_name: bytes32, removals_root: bytes32 +) -> bool: removals_request = RequestRemovals(height, header_hash, [coin_name]) removals_res: Optional[Union[RespondRemovals, RejectRemovalsRequest]] = await peer.request_removals( @@ -132,10 +194,13 @@ async def request_and_validate_removals(peer, height, header_hash, coin_name, re ) if removals_res is None or isinstance(removals_res, RejectRemovalsRequest): return False + assert removals_res.proofs is not None return validate_removals(removals_res.coins, removals_res.proofs, removals_root) -async def request_and_validate_additions(peer, height, header_hash, puzzle_hash, additions_root): +async def request_and_validate_additions( + peer: WSChiaConnection, height: uint32, header_hash: bytes32, puzzle_hash: bytes32, additions_root: bytes32 +): additions_request = RequestAdditions(height, header_hash, [puzzle_hash]) additions_res: Optional[Union[RespondAdditions, RejectAdditionsRequest]] = await peer.request_additions( additions_request @@ -143,12 +208,11 @@ async def request_and_validate_additions(peer, height, header_hash, puzzle_hash, if additions_res is None or isinstance(additions_res, RejectAdditionsRequest): return False - validated = validate_additions( + return validate_additions( additions_res.coins, additions_res.proofs, additions_root, ) - return validated def get_block_challenge( diff --git a/chia/wallet/wallet_blockchain.py b/chia/wallet/wallet_blockchain.py index 6b55efbb6b86..cf5c6da1ad81 100644 --- a/chia/wallet/wallet_blockchain.py +++ b/chia/wallet/wallet_blockchain.py @@ -24,6 +24,7 @@ class WalletBlockchain(BlockchainInterface): _weight_proof_handler: WalletWeightProofHandler synced_weight_proof: Optional[WeightProof] + _finished_sync_up_to: uint32 _peak: Optional[HeaderBlock] _height_to_hash: Dict[uint32, bytes32] @@ -48,6 +49,9 @@ async def create( self.CACHE_SIZE = constants.SUB_EPOCH_BLOCKS + 100 self._weight_proof_handler = weight_proof_handler self.synced_weight_proof = await self._basic_store.get_object("SYNCED_WEIGHT_PROOF", WeightProof) + self._finished_sync_up_to = await self._basic_store.get_object("FINISHED_SYNC_UP_TO", uint32) + if self._finished_sync_up_to is None: + self._finished_sync_up_to = uint32(0) self._peak = None self._peak = await self.get_peak_block() self._latest_timestamp = uint64(0) @@ -156,6 +160,8 @@ async def _rollback_to_height(self, height: int): await self._basic_store.remove_object("PEAK_BLOCK") def get_peak_height(self) -> uint32: + # The peak height is the latest height that we know of in the blockchain, it does not mean + # that we have downloaded all transactions up to that height. if self._peak is None: return uint32(0) return self._peak.height @@ -167,13 +173,22 @@ async def set_peak_block(self, block: HeaderBlock, timestamp: Optional[uint64] = self._latest_timestamp = timestamp elif block.foliage_transaction_block is not None: self._latest_timestamp = block.foliage_transaction_block.timestamp - log.info(f"Peak set to : {self._peak.height} timestamp: {self._latest_timestamp}") + log.info(f"Peak set to: {self._peak.height} timestamp: {self._latest_timestamp}") async def get_peak_block(self) -> Optional[HeaderBlock]: if self._peak is not None: return self._peak return await self._basic_store.get_object("PEAK_BLOCK", HeaderBlock) + async def set_finished_sync_up_to(self, height: uint32): + await self._basic_store.set_object("FINISHED_SYNC_UP_TO", height) + + async def get_finished_sync_up_to(self): + h: Optional[uint32] = await self._basic_store.get_object("FINISHED_SYNC_UP_TO", uint32) + if h is None: + return uint32(0) + return h + def get_latest_timestamp(self) -> uint64: return self._latest_timestamp diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index e86ae13eed4a..e76e28576908 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -3,6 +3,7 @@ import logging import time import traceback +from asyncio import CancelledError from pathlib import Path from typing import Callable, Dict, List, Optional, Set, Tuple, Any @@ -52,9 +53,14 @@ from chia.util.ints import uint32, uint64 from chia.util.keychain import KeyringIsLocked, Keychain from chia.util.path import mkdir, path_from_root +from chia.wallet.util.new_peak_queue import NewPeakQueue, NewPeakQueueTypes, NewPeakItem +from chia.wallet.util.peer_request_cache import PeerRequestCache, can_use_peer_request_cache from chia.wallet.util.wallet_sync_utils import ( request_and_validate_removals, request_and_validate_additions, + fetch_last_tx_from_peer, + subscribe_to_phs, + subscribe_to_coin_updates, ) from chia.wallet.wallet_coin_record import WalletCoinRecord from chia.wallet.wallet_state_manager import WalletStateManager @@ -63,34 +69,6 @@ from chia.util.profiler import profile_task -class PeerRequestCache: - blocks: Dict[uint32, HeaderBlock] - block_requests: Dict[Tuple[int, int], Any] - ses_requests: Dict[int, Any] - states_validated: Dict[bytes32, CoinState] - - def __init__(self): - self.blocks = {} - self.ses_requests = {} - self.block_requests = {} - self.states_validated = {} - - def clear_after_height(self, height: int): - # Remove any cached item which relates to an event that happened at a height above height. - self.blocks = {k: v for k, v in self.blocks.items() if k <= height} - self.block_requests = {k: v for k, v in self.block_requests.items() if k[0] <= height and k[1] <= height} - self.ses_requests = {k: v for k, v in self.ses_requests.items() if k <= height} - - remove_keys_states: List[bytes32] = [] - for k4, coin_state in self.states_validated.items(): - if coin_state.created_height is not None and coin_state.created_height > height: - remove_keys_states.append(k4) - elif coin_state.spent_height is not None and coin_state.spent_height > height: - remove_keys_states.append(k4) - for k5 in remove_keys_states: - self.states_validated.pop(k5) - - class WalletNode: key_config: Dict config: Dict @@ -109,7 +87,12 @@ class WalletNode: wallet_peers_initialized: bool keychain_proxy: Optional[KeychainProxy] wallet_peers: Optional[WalletPeers] - race_cache: Dict[uint32, Set[CoinState]] + race_cache: Dict[bytes32, Set[CoinState]] + race_cache_hashes: List[Tuple[uint32, bytes32]] + new_peak_queue: NewPeakQueue + _process_new_subscriptions_task: Optional[asyncio.Task] + _secondary_peer_sync_task: Optional[asyncio.Task] + node_peaks: Dict[bytes32, Tuple[uint32, bytes32]] validation_semaphore: Optional[asyncio.Semaphore] local_node_synced: bool new_state_lock: Optional[asyncio.Lock] @@ -145,14 +128,19 @@ def __init__( self.keychain_proxy = None self.local_keychain = local_keychain self.height_to_time: Dict[uint32, uint64] = {} - self.synced_peers: Set[bytes32] = set() + self.synced_peers: Set[bytes32] = set() # Peers that we have long synced to self.wallet_peers = None self.wallet_peers_initialized = False self.valid_wp_cache: Dict[bytes32, Any] = {} self.untrusted_caches: Dict[bytes32, Any] = {} self.race_cache = {} # in Untrusted mode wallet might get the state update before receiving the block + self.race_cache_hashes = [] + self._process_new_subscriptions_task = None + self._secondary_peer_sync_task = None + self.node_peaks = {} self.validation_semaphore = None self.local_node_synced = False + self.LONG_SYNC_THRESHOLD = 200 async def ensure_keychain_proxy(self) -> KeychainProxy: if not self.keychain_proxy: @@ -194,6 +182,9 @@ async def _start( self, fingerprint: Optional[int] = None, ) -> bool: + # Makes sure the coin_state_updates get higher priority than new_peak messages + self.new_peak_queue = NewPeakQueue(asyncio.PriorityQueue()) + self.synced_peers = set() private_key = await self.get_key_for_fingerprint(fingerprint) if private_key is None: @@ -242,6 +233,7 @@ async def _start( 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()) self.sync_event = asyncio.Event() if fingerprint is None: @@ -258,21 +250,19 @@ async def _start( self.wsm_close_task = None return True - async def new_puzzle_hash_created(self, puzzle_hashes: List[bytes32]): - if len(puzzle_hashes) == 0: - return - assert self.server is not None - full_nodes: Dict[bytes32, WSChiaConnection] = self.server.connection_by_type.get(NodeType.FULL_NODE, {}) - for node_id, node in full_nodes.copy().items(): - asyncio.create_task(self.subscribe_to_phs(puzzle_hashes, node)) - def _close(self): self.log.info("self._close") self.logged_in_fingerprint = None self._shut_down = True + if self._process_new_subscriptions_task is not None: + self._process_new_subscriptions_task.cancel() + if self._secondary_peer_sync_task is not None: + self._secondary_peer_sync_task.cancel() + async def _await_closed(self): self.log.info("self._await_closed") + if self.server is not None: await self.server.close_all_connections() if self.wallet_peers is not None: @@ -355,6 +345,60 @@ async def _messages_to_resend(self) -> List[Tuple[Message, Set[bytes32]]]: return messages + async def _process_new_subscriptions(self): + while not self._shut_down: + # Here we process four types of messages in the queue, where the first one has higher priority (lower + # number in the queue), and priority decreases for each type. + peer: Optional[WSChiaConnection] = None + item: Optional[NewPeakItem] = None + try: + peer, item = None, None + item = await self.new_peak_queue.get() + self.log.info(f"Pulled from queue: {item}") + assert item is not None + if item.item_type == NewPeakQueueTypes.COIN_ID_SUBSCRIPTION: + # Subscriptions are the highest priority, because we don't want to process any more peaks or + # state updates until we are sure that we subscribed to everything that we need to. Otherwise, + # we might not be able to process some state. + coin_ids: List[bytes32] = item.data + for peer in self.server.get_full_node_connections(): + coin_states: List[CoinState] = await subscribe_to_coin_updates(coin_ids, peer, uint32(0)) + if len(coin_states) > 0: + async with self.wallet_state_manager.lock: + await self.receive_state_from_peer(coin_states, peer) + elif item.item_type == NewPeakQueueTypes.PUZZLE_HASH_SUBSCRIPTION: + puzzle_hashes: List[bytes32] = item.data + for peer in self.server.get_full_node_connections(): + # Puzzle hash subscription + coin_states: List[CoinState] = await subscribe_to_phs(puzzle_hashes, peer, uint32(0)) + if len(coin_states) > 0: + async with self.wallet_state_manager.lock: + await self.receive_state_from_peer(coin_states, peer) + elif item.item_type == NewPeakQueueTypes.FULL_NODE_STATE_UPDATED: + # Note: this can take a while when we have a lot of transactions. We want to process these + # before new_peaks, since new_peak_wallet requires that we first obtain the state for that peak. + request: wallet_protocol.CoinStateUpdate = item.data[0] + peer = item.data[1] + assert peer is not None + await self.state_update_received(request, peer) + elif item.item_type == NewPeakQueueTypes.NEW_PEAK_WALLET: + # This can take a VERY long time, because it might trigger a long sync. It is OK if we miss some + # subscriptions or state updates, since all subscriptions and state updates will be handled by + # long_sync (up to the target height). + request: wallet_protocol.NewPeakWallet = item.data[0] + peer = item.data[1] + assert peer is not None + await self.new_peak_wallet(request, peer) + else: + assert False + except CancelledError: + self.log.info("Queue task cancelled, exiting.") + raise + except Exception as e: + self.log.error(f"Exception handling {item}, {e} {traceback.format_exc()}") + if peer is not None: + await peer.close(9999) + def set_server(self, server: ChiaServer): self.server = server self.initialize_wallet_peers() @@ -393,6 +437,10 @@ def on_disconnect(self, peer: WSChiaConnection): if peer.peer_node_id in self.untrusted_caches: self.untrusted_caches.pop(peer.peer_node_id) + if peer.peer_node_id in self.synced_peers: + self.synced_peers.remove(peer.peer_node_id) + if peer.peer_node_id in self.node_peaks: + self.node_peaks.pop(peer.peer_node_id) async def on_connect(self, peer: WSChiaConnection): if self.wallet_state_manager is None: @@ -406,7 +454,10 @@ async def on_connect(self, peer: WSChiaConnection): if not trusted and self.local_node_synced: await peer.close() - self.log.info(f"Connected peer {peer} is {trusted}") + if peer.peer_node_id in self.synced_peers: + self.synced_peers.remove(peer.peer_node_id) + + self.log.info(f"Connected peer {peer.get_peer_info()} is trusted: {trusted}") messages_peer_ids = await self._messages_to_resend() self.wallet_state_manager.state_changed("add_connection") for msg, peer_ids in messages_peer_ids: @@ -415,67 +466,114 @@ async def on_connect(self, peer: WSChiaConnection): await peer.send_message(msg) if self.wallet_peers is not None: - asyncio.create_task(self.wallet_peers.on_connect(peer)) + await self.wallet_peers.on_connect(peer) - async def trusted_sync(self, full_node: WSChiaConnection): + async def long_sync( + self, + target_height: uint32, + full_node: WSChiaConnection, + fork_height: int, + *, + rollback: bool, + ): """ - Performs a one-time sync with each trusted peer, subscribing to interested puzzle hashes and coin ids. + Sync algorithm: + - Download and verify weight proof (if not trusted) + - Roll back anything after the fork point (if rollback=True) + - Subscribe to all puzzle_hashes over and over until there are no more updates + - Subscribe to all coin_ids over and over until there are no more updates + - rollback=False means that we are just double-checking with this peer to make sure we don't have any + missing transactions, so we don't need to rollback """ - self.log.info("Starting trusted sync") + + def is_new_state_update(cs: CoinState) -> bool: + if cs.spent_height is None and cs.created_height is None: + return True + if cs.spent_height is not None and cs.spent_height >= fork_height: + return True + if cs.created_height is not None and cs.created_height >= fork_height: + return True + return False + + trusted: bool = self.is_trusted(full_node) + self.log.info(f"Starting sync trusted: {trusted} to peer {full_node.peer_host}") assert self.wallet_state_manager is not None - self.wallet_state_manager.set_sync_mode(True) start_time = time.time() - current_height: uint32 = self.wallet_state_manager.blockchain.get_peak_height() - request_height: uint32 = uint32(max(0, current_height - 1000)) - already_checked: Set[bytes32] = set() + if rollback: + await self.wallet_state_manager.reorg_rollback(fork_height) + self.rollback_request_caches(fork_height) + await self.update_ui() + + already_checked_ph: Set[bytes32] = set() continue_while: bool = True + all_puzzle_hashes: List[bytes32] = await self.get_puzzle_hashes_to_subscribe() while continue_while: # Get all phs from puzzle store - all_puzzle_hashes: List[bytes32] = await self.get_puzzle_hashes_to_subscribe() - to_check: List[bytes32] = [] - for ph in all_puzzle_hashes: - if ph in already_checked: - continue - else: - to_check.append(ph) - already_checked.add(ph) - if len(to_check) == 1000: - break - - await self.subscribe_to_phs(to_check, full_node, request_height) + ph_chunks: List[List[bytes32]] = chunks(all_puzzle_hashes, 1000) + for chunk in ph_chunks: + ph_update_res: List[CoinState] = await subscribe_to_phs( + [p for p in chunk if p not in already_checked_ph], full_node, 0 + ) + ph_update_res = list(filter(is_new_state_update, ph_update_res)) + await self.receive_state_from_peer(ph_update_res, full_node) + already_checked_ph.update(chunk) # Check if new puzzle hashed have been created - check_again = await self.get_puzzle_hashes_to_subscribe() await self.wallet_state_manager.create_more_puzzle_hashes() + all_puzzle_hashes = await self.get_puzzle_hashes_to_subscribe() + continue_while = False + for ph in all_puzzle_hashes: + if ph not in already_checked_ph: + continue_while = True + break + self.log.info(f"Successfully subscribed and updated {len(already_checked_ph)} puzzle hashes") + continue_while = False + all_coin_ids: List[bytes32] = await self.get_coin_ids_to_subscribe(fork_height) + already_checked_coin_ids: Set[bytes32] = set() + while continue_while: + one_k_chunks = chunks(all_coin_ids, 1000) + for chunk in one_k_chunks: + c_update_res: List[CoinState] = await subscribe_to_coin_updates(chunk, full_node, 0) + c_update_res = list(filter(is_new_state_update, c_update_res)) + await self.receive_state_from_peer(c_update_res, full_node) + already_checked_coin_ids.update(chunk) + + all_coin_ids = await self.get_coin_ids_to_subscribe(fork_height) continue_while = False - for ph in check_again: - if ph not in already_checked: + for coin_id in all_coin_ids: + if coin_id not in already_checked_coin_ids: continue_while = True break + self.log.info(f"Successfully subscribed and updated {len(already_checked_coin_ids)} coin ids") - all_coins: Set[WalletCoinRecord] = await self.wallet_state_manager.coin_store.get_coins_to_check(request_height) - all_coin_names: List[bytes32] = [coin_record.name() for coin_record in all_coins] - removed_dict = await self.wallet_state_manager.trade_manager.get_coins_of_interest() - all_coin_names.extend(removed_dict.keys()) + if target_height > await self.wallet_state_manager.blockchain.get_finished_sync_up_to(): + await self.wallet_state_manager.blockchain.set_finished_sync_up_to(target_height) + + if trusted: + self.local_node_synced = True + + self.wallet_state_manager.state_changed("new_block") + + self.synced_peers.add(full_node.peer_node_id) + await self.update_ui() - one_k_chunks = chunks(all_coin_names, 1000) - for chunk in one_k_chunks: - await self.subscribe_to_coin_updates(chunk, full_node, request_height) - self.wallet_state_manager.set_sync_mode(False) end_time = time.time() duration = end_time - start_time - self.log.info(f"Trusted sync duration was: {duration}") - # Refresh wallets - for wallet_id, wallet in self.wallet_state_manager.wallets.items(): - self.wallet_state_manager.state_changed("coin_removed", wallet_id) - self.wallet_state_manager.state_changed("coin_added", wallet_id) - self.synced_peers.add(full_node.peer_node_id) + self.log.info(f"Sync (trusted: {trusted}) duration was: {duration}") async def receive_state_from_peer( - self, items: List[CoinState], peer: WSChiaConnection, fork_height: Optional[uint32], height: Optional[uint32] + self, + items: List[CoinState], + peer: WSChiaConnection, + fork_height: Optional[uint32] = None, + height: Optional[uint32] = None, + header_hash: Optional[bytes32] = None, ): + # Adds the state to the wallet state manager. If the peer is trusted, we do not validate. If the peer is + # untrusted we do, but we might not add the state, since we need to receive the new_peak message as well. + assert self.wallet_state_manager is not None trusted = self.is_trusted(peer) # Validate states in parallel, apply serial @@ -487,6 +585,9 @@ async def receive_state_from_peer( # If there is a fork, we need to ensure that we roll back in trusted mode to properly handle reorgs if trusted and fork_height is not None and height is not None and fork_height != height - 1: await self.wallet_state_manager.reorg_rollback(fork_height) + cache: PeerRequestCache = self.get_cache_for_peer(peer) + if fork_height is not None: + cache.clear_after_height(fork_height) all_tasks = [] @@ -498,24 +599,19 @@ async def receive_and_validate(inner_state: CoinState, inner_idx: int): # if height is not None: async with self.validation_semaphore: try: + if header_hash is not None: + assert height is not None + self.add_state_to_race_cache(header_hash, height, inner_state) + self.log.info(f"Added to race cache: {height}, {inner_state}") if trusted: valid = True else: - valid = await self.validate_received_state_from_peer( - inner_state, peer, self.get_cache_for_peer(peer) - ) + valid = await self.validate_received_state_from_peer(inner_state, peer, cache, fork_height) if valid: - self.log.info(f"new coin state received ({inner_idx} / {len(items)})") + self.log.info(f"new coin state received ({inner_idx + 1} / {len(items)})") assert self.new_state_lock is not None async with self.new_state_lock: - await self.wallet_state_manager.new_coin_state([inner_state], peer) - elif height is not None: - self.add_state_to_race_cache(height, inner_state) - else: - if inner_state.created_height is not None: - self.add_state_to_race_cache(inner_state.created_height, inner_state) - if inner_state.spent_height is not None: - self.add_state_to_race_cache(inner_state.spent_height, inner_state) + await self.wallet_state_manager.new_coin_state([inner_state], peer, fork_height) except Exception as e: tb = traceback.format_exc() self.log.error(f"Exception while adding state: {e} {tb}") @@ -527,60 +623,7 @@ async def receive_and_validate(inner_state: CoinState, inner_idx: int): await asyncio.sleep(2) await asyncio.gather(*all_tasks) - - async def subscribe_to_phs(self, puzzle_hashes: List[bytes32], peer: WSChiaConnection, height=uint32(0)): - """ - Tell full nodes that we are interested in puzzle hashes, and for trusted connections, add the new coin state - for the puzzle hashes. - """ - assert self.wallet_state_manager is not None - msg = wallet_protocol.RegisterForPhUpdates(puzzle_hashes, height) - all_state: Optional[RespondToPhUpdates] = await peer.register_interest_in_puzzle_hash(msg) - if all_state is None: - return - await self.receive_state_from_peer(all_state.coin_states, peer, None, None) - - async def subscribe_to_coin_updates(self, coin_names: List[bytes32], peer: WSChiaConnection, height=uint32(0)): - """ - Tell full nodes that we are interested in coin ids, and for trusted connections, add the new coin state - for the coin changes. - """ - msg = wallet_protocol.RegisterForCoinUpdates(coin_names, height) - all_coins_state: Optional[RespondToCoinUpdates] = await peer.register_interest_in_coin(msg) - if all_coins_state is not None and self.is_trusted(peer): - await self.receive_state_from_peer(all_coins_state.coin_states, peer, None, None) - - async def get_coin_state( - self, coin_names: List[bytes32], peer: Optional[WSChiaConnection] = None - ) -> List[CoinState]: - assert self.server is not None - all_nodes = self.server.connection_by_type[NodeType.FULL_NODE] - if len(all_nodes.keys()) == 0: - raise ValueError("Not connected to the full node") - - # Use supplied if provided, prioritize trusted otherwise - if peer is None: - for node in list(all_nodes.values()): - if self.is_trusted(node): - peer = node - break - if peer is None: - peer = list(all_nodes.values())[0] - - assert peer is not None - msg = wallet_protocol.RegisterForCoinUpdates(coin_names, uint32(0)) - coin_state: Optional[RespondToCoinUpdates] = await peer.register_interest_in_coin(msg) - assert coin_state is not None - - if not self.is_trusted(peer): - valid_list = [] - for coin in coin_state.coin_states: - valid = await self.validate_received_state_from_peer(coin, peer, self.get_cache_for_peer(peer)) - if valid: - valid_list.append(coin) - return valid_list - - return coin_state.coin_states + await self.update_ui() async def get_coins_with_puzzle_hash(self, puzzle_hash) -> List[CoinState]: assert self.wallet_state_manager is not None @@ -594,70 +637,70 @@ async def get_coins_with_puzzle_hash(self, puzzle_hash) -> List[CoinState]: assert coin_state is not None return coin_state.coin_states - def is_trusted(self, peer): + async def is_peer_synced( + self, peer: WSChiaConnection, header_block: HeaderBlock, request_time: uint64 + ) -> Optional[uint64]: + # Get last timestamp + last_tx: Optional[HeaderBlock] = await fetch_last_tx_from_peer(header_block.height, peer) + latest_timestamp: Optional[uint64] = None + if last_tx is not None: + assert last_tx.foliage_transaction_block is not None + latest_timestamp = last_tx.foliage_transaction_block.timestamp + + # Return None if not synced + if latest_timestamp is None or self.config["testing"] is False and latest_timestamp < request_time - 600: + return None + return latest_timestamp + + def is_trusted(self, peer) -> bool: + assert self.server is not None return self.server.is_trusted_peer(peer, self.config["trusted_peers"]) - def add_state_to_race_cache(self, height: uint32, coin_state: CoinState): - if height not in self.race_cache: - self.race_cache[height] = set() - self.race_cache[height].add(coin_state) + def add_state_to_race_cache(self, header_hash: bytes32, height: uint32, coin_state: CoinState) -> None: + # Clears old state that is no longer relevant + delete_threshold = 100 + for rc_height, rc_hh in self.race_cache_hashes: + if height - delete_threshold >= rc_height: + self.race_cache.pop(rc_hh) + self.race_cache_hashes = [ + (rc_height, rc_hh) for rc_height, rc_hh in self.race_cache_hashes if height - delete_threshold < rc_height + ] + + if header_hash not in self.race_cache: + self.race_cache[header_hash] = set() + self.race_cache[header_hash].add(coin_state) - async def state_update_received(self, request: wallet_protocol.CoinStateUpdate, peer: WSChiaConnection): + async def state_update_received(self, request: wallet_protocol.CoinStateUpdate, peer: WSChiaConnection) -> None: + # This gets called every time there is a new coin or puzzle hash change in the DB + # that is of interest to this wallet. It is not guaranteed to come for every height. This message is guaranteed + # to come before the corresponding new_peak for each height. We handle this differently for trusted and + # untrusted peers. For trusted, we always process the state, and we process reorgs as well. assert self.wallet_state_manager is not None assert self.server is not None - async with self.new_peak_lock: - async with self.wallet_state_manager.lock: - self.log.debug(f"state_update_received is {request}") - await self.receive_state_from_peer( - request.items, peer, request.fork_height if self.is_trusted(peer) else None, request.height - ) - await self.update_ui() + async with self.wallet_state_manager.lock: + await self.receive_state_from_peer( + request.items, + peer, + request.fork_height, + request.height, + request.peak_hash, + ) - def get_full_node_peer(self): + def get_full_node_peer(self) -> Optional[WSChiaConnection]: + assert self.server is not None nodes = self.server.get_full_node_connections() if len(nodes) > 0: return nodes[0] else: return None - async def last_local_tx_block(self, header_hash: bytes32) -> Optional[BlockRecord]: - assert self.wallet_state_manager is not None - current_hash = header_hash - while True: - if self.wallet_state_manager.blockchain.contains_block(current_hash): - block = self.wallet_state_manager.blockchain.try_block_record(current_hash) - if block is None: - return None - if block.is_transaction_block: - return block - if block.prev_transaction_block_hash is None: - return None - current_hash = block.prev_transaction_block_hash - else: - break - return None - - async def fetch_last_tx_from_peer(self, height: uint32, peer: WSChiaConnection) -> Optional[HeaderBlock]: - request_height = height - while True: - if request_height == 0: - return None - request = wallet_protocol.RequestBlockHeader(request_height) - response: Optional[RespondBlockHeader] = await peer.request_block_header(request) - if response is not None and isinstance(response, RespondBlockHeader): - if response.header_block.is_transaction_block: - return response.header_block - else: - break - request_height = uint32(request_height - 1) - return None - async def disconnect_and_stop_wpeers(self): + # Close connection of non trusted peers if len(self.server.get_full_node_connections()) > 1: for peer in self.server.get_full_node_connections(): if not self.is_trusted(peer): - asyncio.create_task(peer.close()) + await peer.close() if self.wallet_peers is not None: await self.wallet_peers.ensure_is_closed() @@ -680,209 +723,184 @@ async def get_timestamp_for_height(self, height: uint32) -> uint64: ): self.height_to_time[height] = block.foliage_transaction_block.timestamp return block.foliage_transaction_block.timestamp - - peer = self.get_full_node_peer() - assert peer is not None - curr_height: uint32 = height - while True: - request = wallet_protocol.RequestBlockHeader(curr_height) - response: Optional[RespondBlockHeader] = await peer.request_block_header(request) - if response is None or not isinstance(response, RespondBlockHeader): - raise ValueError(f"Invalid response from {peer}, {response}") - if response.header_block.foliage_transaction_block is not None: - self.height_to_time[height] = response.header_block.foliage_transaction_block.timestamp - return response.header_block.foliage_transaction_block.timestamp - curr_height = uint32(curr_height - 1) - - async def new_peak_wallet(self, peak: wallet_protocol.NewPeakWallet, peer: WSChiaConnection): - self.log.info(f"New peak wallet.. {peak.height} {peer.get_peer_info()}") - assert self.wallet_state_manager is not None - assert self.server is not None - request_time = int(time.time()) - + peer: Optional[WSChiaConnection] = self.get_full_node_peer() + if peer is None: + raise ValueError("Cannot fetch timestamp, no peers") + last_tx_block: Optional[HeaderBlock] = await fetch_last_tx_from_peer(height, peer) + if last_tx_block is None: + raise ValueError(f"Error fetching blocks from peer {peer.get_peer_info()}") + assert last_tx_block.foliage_transaction_block is not None + return last_tx_block.foliage_transaction_block.timestamp + + async def new_peak_wallet(self, new_peak: wallet_protocol.NewPeakWallet, peer: WSChiaConnection): if self.wallet_state_manager is None: # When logging out of wallet return - if self.is_trusted(peer): - request = wallet_protocol.RequestBlockHeader(peak.height) - header_response: Optional[RespondBlockHeader] = await peer.request_block_header(request) - assert header_response is not None - - # Get last timestamp - last_tx: Optional[HeaderBlock] = await self.fetch_last_tx_from_peer(peak.height, peer) - latest_timestamp: Optional[uint64] = None - if last_tx is not None: - assert last_tx.foliage_transaction_block is not None - latest_timestamp = last_tx.foliage_transaction_block.timestamp - - # Ignore if not synced - if latest_timestamp is None or self.config["testing"] is False and latest_timestamp < request_time - 600: - return - - # Disconnect from all untrusted peers if our local node is trusted and synced - await self.disconnect_and_stop_wpeers() + assert self.server is not None + request_time = uint64(int(time.time())) + trusted: bool = self.is_trusted(peer) + peak_hb: Optional[HeaderBlock] = await self.wallet_state_manager.blockchain.get_peak_block() + if peak_hb is not None and new_peak.weight < peak_hb.weight: + # Discards old blocks, but accepts blocks that are equal in weight to peak + return - async with self.new_peak_lock: - async with self.wallet_state_manager.lock: - # Sync to trusted node - self.local_node_synced = True - if peer.peer_node_id not in self.synced_peers: - await self.trusted_sync(peer) + request = wallet_protocol.RequestBlockHeader(new_peak.height) + response: Optional[RespondBlockHeader] = await peer.request_block_header(request) + if response is None: + self.log.warning(f"Peer {peer.get_peer_info()} did not respond in time.") + await peer.close(120) + return + header_block: HeaderBlock = response.header_block - await self.wallet_state_manager.blockchain.set_peak_block( - header_response.header_block, latest_timestamp - ) + latest_timestamp: Optional[uint64] = await self.is_peer_synced(peer, header_block, request_time) + if latest_timestamp is None: + if trusted: + self.log.debug(f"Trusted peer {peer.get_peer_info()} is not synced.") + return + else: + self.log.warning(f"Non-trusted peer {peer.get_peer_info()} is not synced, disconnecting") + await peer.close(120) + return - self.wallet_state_manager.state_changed("new_block") + current_height: uint32 = await self.wallet_state_manager.blockchain.get_finished_sync_up_to() + if self.is_trusted(peer): + async with self.wallet_state_manager.lock: + await self.wallet_state_manager.blockchain.set_peak_block(header_block, latest_timestamp) + # Disconnect from all untrusted peers if our local node is trusted and synced + await self.disconnect_and_stop_wpeers() + + # Sync to trusted node if we haven't done so yet. As long as we have synced once (and not + # disconnected), we assume that the full node will continue to give us state updates, so we do + # not need to resync. + if peer.peer_node_id not in self.synced_peers: + await self.long_sync(new_peak.height, peer, uint32(max(0, current_height - 256)), rollback=True) self.wallet_state_manager.set_sync_mode(False) else: - async with self.new_peak_lock: - request = wallet_protocol.RequestBlockHeader(peak.height) - response: Optional[RespondBlockHeader] = await peer.request_block_header(request) - if response is None or not isinstance(response, RespondBlockHeader) or response.header_block is None: - self.log.debug(f"bad peak response from peer {response}, perhaps connection was closed") - return - peak_block = response.header_block - current_peak: Optional[HeaderBlock] = await self.wallet_state_manager.blockchain.get_peak_block() - if current_peak is not None and peak_block.weight < current_peak.weight: - if peak_block.height < current_peak.height - 20: - await peer.close(120) - return - - # don't sync if full node is not synced it self, since we want to fully sync to a few peers - if ( - not response.header_block.is_transaction_block - and current_peak is not None - and peak_block.prev_header_hash == current_peak.header_hash - ): - # This block is after our peak, so we don't need to check if node is synced - pass - else: - tx_timestamp = None - if not response.header_block.is_transaction_block: - last_tx_block = None - # Try local first - last_block_record = await self.last_local_tx_block(response.header_block.prev_header_hash) - if last_block_record is not None: - tx_timestamp = last_block_record.timestamp - else: - last_tx_block = await self.fetch_last_tx_from_peer(response.header_block.height, peer) - if last_tx_block is not None: - assert last_tx_block.foliage_transaction_block is not None - tx_timestamp = last_tx_block.foliage_transaction_block.timestamp - else: - last_tx_block = response.header_block - assert last_tx_block.foliage_transaction_block is not None - tx_timestamp = last_tx_block.foliage_transaction_block.timestamp - - if tx_timestamp is None: - return None - - if self.config["testing"] is False and tx_timestamp < request_time - 600: - # Full node not synced, don't sync to it - self.log.info("Peer we connected to is not fully synced, dropping connection...") - await peer.close() - return - - long_sync_threshold = 200 - far_behind: bool = ( - peak.height - self.wallet_state_manager.blockchain.get_peak_height() > long_sync_threshold - ) - - # check if claimed peak is heavier or same as our current peak - # if we haven't synced fully to this peer sync again - if ( - peer.peer_node_id not in self.synced_peers or far_behind - ) and peak.height >= self.constants.WEIGHT_PROOF_RECENT_BLOCKS: - syncing = False - if far_behind or len(self.synced_peers) == 0: - syncing = True - self.wallet_state_manager.set_sync_mode(True) - try: - ( - valid_weight_proof, - weight_proof, - summaries, - block_records, - ) = await self.fetch_and_validate_the_weight_proof(peer, response.header_block) - if valid_weight_proof is False: - if syncing: - self.wallet_state_manager.set_sync_mode(False) - await peer.close() - return - assert weight_proof is not None - old_proof = self.wallet_state_manager.blockchain.synced_weight_proof - curr_peak = await self.wallet_state_manager.blockchain.get_peak_block() - fork_point = 0 - if curr_peak is not None: - fork_point = max(0, curr_peak.height - 32) - - if old_proof is not None: - wp_fork_point = self.wallet_state_manager.weight_proof_handler.get_fork_point( - old_proof, weight_proof - ) - if wp_fork_point != 0: - fork_point = wp_fork_point - - await self.wallet_state_manager.blockchain.new_weight_proof(weight_proof, block_records) - if syncing: - async with self.wallet_state_manager.lock: - await self.untrusted_sync_to_peer(peer, syncing, fork_point) - else: - await self.untrusted_sync_to_peer(peer, syncing, fork_point) - if ( - self.wallet_state_manager.blockchain.synced_weight_proof is None - or weight_proof.recent_chain_data[-1].weight - > self.wallet_state_manager.blockchain.synced_weight_proof.recent_chain_data[-1].weight - ): - await self.wallet_state_manager.blockchain.new_weight_proof(weight_proof, block_records) - - self.synced_peers.add(peer.peer_node_id) + far_behind: bool = ( + new_peak.height - self.wallet_state_manager.blockchain.get_peak_height() > self.LONG_SYNC_THRESHOLD + ) - self.wallet_state_manager.state_changed("new_block") - self.wallet_state_manager.set_sync_mode(False) - await self.update_ui() - except Exception: + # check if claimed peak is heavier or same as our current peak + # if we haven't synced fully to this peer sync again + if ( + peer.peer_node_id not in self.synced_peers or far_behind + ) and new_peak.height >= self.constants.WEIGHT_PROOF_RECENT_BLOCKS: + syncing = False + if far_behind or len(self.synced_peers) == 0: + syncing = True + self.wallet_state_manager.set_sync_mode(True) + try: + ( + valid_weight_proof, + weight_proof, + summaries, + block_records, + ) = await self.fetch_and_validate_the_weight_proof(peer, response.header_block) + if valid_weight_proof is False: if syncing: self.wallet_state_manager.set_sync_mode(False) - tb = traceback.format_exc() - self.log.error(f"Error syncing to {peer.get_peer_info()} {tb}") await peer.close() return + assert weight_proof is not None + old_proof = self.wallet_state_manager.blockchain.synced_weight_proof + if syncing: + fork_point: int = max(0, current_height - 32) + else: + fork_point = max(0, current_height - 50000) + if old_proof is not None: + # If the weight proof fork point is in the past, rollback more to ensure we don't have duplicate + # state + wp_fork_point = self.wallet_state_manager.weight_proof_handler.get_fork_point( + old_proof, weight_proof + ) + fork_point = min(fork_point, wp_fork_point) + + await self.wallet_state_manager.blockchain.new_weight_proof(weight_proof, block_records) + if syncing: + async with self.wallet_state_manager.lock: + self.log.info("Primary peer syncing") + await self.long_sync(new_peak.height, peer, fork_point, rollback=True) + else: + if self._secondary_peer_sync_task is None or self._secondary_peer_sync_task.done(): + self.log.info("Secondary peer syncing") + self._secondary_peer_sync_task = asyncio.create_task( + self.long_sync(new_peak.height, peer, fork_point, rollback=False) + ) + return + else: + self.log.info("Will not do secondary sync, there is already another sync task running.") + return + self.log.info(f"New peak wallet.. {new_peak.height} {peer.get_peer_info()} 12") + if ( + self.wallet_state_manager.blockchain.synced_weight_proof is None + or weight_proof.recent_chain_data[-1].weight + > self.wallet_state_manager.blockchain.synced_weight_proof.recent_chain_data[-1].weight + ): + await self.wallet_state_manager.blockchain.new_weight_proof(weight_proof, block_records) + except Exception as e: + tb = traceback.format_exc() + self.log.error(f"Error syncing to {peer.get_peer_info()} {e} {tb}") if syncing: self.wallet_state_manager.set_sync_mode(False) + tb = traceback.format_exc() + self.log.error(f"Error syncing to {peer.get_peer_info()} {tb}") + await peer.close() + return + if syncing: + self.wallet_state_manager.set_sync_mode(False) + + else: + # This is the (untrusted) case where we already synced and are not too far behind. Here we just + # fetch one by one. + async with self.wallet_state_manager.lock: + peak_hb = await self.wallet_state_manager.blockchain.get_peak_block() + if peak_hb is None or new_peak.weight > peak_hb.weight: + backtrack_fork_height: int = await self.wallet_short_sync_backtrack(header_block, peer) + else: + backtrack_fork_height = new_peak.height - 1 - else: - self.log.info(f"Starting backtrack sync to {peer.get_peer_info()}") - await self.wallet_short_sync_backtrack(peak_block, peer) if peer.peer_node_id not in self.synced_peers: - # Edge case, we still want to subscribe for all phs + # Edge case, this happens when the peak < WEIGHT_PROOF_RECENT_BLOCKS + # we still want to subscribe for all phs and coins. # (Hints are not in filter) - await self.untrusted_subscribe_to_puzzle_hashes(peer, self.get_cache_for_peer(peer)) + all_coin_ids: List[bytes32] = await self.get_coin_ids_to_subscribe(uint32(0)) + phs: List[bytes32] = await self.get_puzzle_hashes_to_subscribe() + ph_updates: List[CoinState] = await subscribe_to_phs(phs, peer, uint32(0)) + coin_updates: List[CoinState] = await subscribe_to_coin_updates(all_coin_ids, peer, uint32(0)) + peer_new_peak_height, peer_new_peak_hash = self.node_peaks[peer.peer_node_id] + await self.receive_state_from_peer( + ph_updates + coin_updates, + peer, + height=peer_new_peak_height, + header_hash=peer_new_peak_hash, + ) self.synced_peers.add(peer.peer_node_id) + else: + if peak_hb is not None and new_peak.weight <= peak_hb.weight: + # Don't process blocks at the same weight + return - if peak_block.height in self.race_cache: - for state in self.race_cache[peak_block.height]: - valid = await self.validate_received_state_from_peer( - state, peer, self.get_cache_for_peer(peer) - ) - if valid: - await self.wallet_state_manager.new_coin_state([state], peer) - else: - self.log.warning(f"Invalid state from peer {peer}") - await peer.close(9999) - return - self.wallet_state_manager.set_sync_mode(False) - self.wallet_state_manager.state_changed("new_block") + # For every block, we need to apply the cache from race_cache + for potential_height in range(backtrack_fork_height + 1, new_peak.height + 1): + header_hash = self.wallet_state_manager.blockchain.height_to_hash(uint32(potential_height)) + if header_hash in self.race_cache: + self.log.debug(f"Receiving race state: {self.race_cache[header_hash]}") + await self.receive_state_from_peer(list(self.race_cache[header_hash]), peer) - await self.wallet_state_manager.new_peak(peak) + self.wallet_state_manager.state_changed("new_block") + self.wallet_state_manager.set_sync_mode(False) + self.log.info(f"Finished processing new peak of {new_peak.height}") - if peak.height > self.wallet_state_manager.finished_sync_up_to: - self.wallet_state_manager.finished_sync_up_to = uint32(peak.height) - self._pending_tx_handler() + if ( + peer.peer_node_id in self.synced_peers + and new_peak.height > await self.wallet_state_manager.blockchain.get_finished_sync_up_to() + ): + await self.wallet_state_manager.blockchain.set_finished_sync_up_to(new_peak.height) + await self.wallet_state_manager.new_peak(new_peak) - async def wallet_short_sync_backtrack(self, header_block: HeaderBlock, peer) -> int: + async def wallet_short_sync_backtrack(self, header_block: HeaderBlock, peer: WSChiaConnection) -> int: assert self.wallet_state_manager is not None + peak: Optional[HeaderBlock] = await self.wallet_state_manager.blockchain.get_peak_block() top = header_block blocks = [top] @@ -903,14 +921,12 @@ async def wallet_short_sync_backtrack(self, header_block: HeaderBlock, peer) -> fork_height = top.height - 1 blocks.reverse() - # Roll back coins and transactions peak_height = self.wallet_state_manager.blockchain.get_peak_height() if fork_height < peak_height: self.log.info(f"Rolling back to {fork_height}") await self.wallet_state_manager.reorg_rollback(fork_height) - - peak = await self.wallet_state_manager.blockchain.get_peak_block() + await self.update_ui() self.rollback_request_caches(fork_height) if peak is not None: @@ -975,125 +991,31 @@ async def get_puzzle_hashes_to_subscribe(self) -> List[bytes32]: all_puzzle_hashes.extend(interested_puzzle_hashes) return all_puzzle_hashes - async def untrusted_subscribe_to_puzzle_hashes( - self, - peer: WSChiaConnection, - peer_request_cache: Optional[PeerRequestCache], - ): - assert self.wallet_state_manager is not None - already_checked = set() - continue_while = True - while continue_while: - all_puzzle_hashes = await self.get_puzzle_hashes_to_subscribe() - to_check = [] - for ph in all_puzzle_hashes: - if ph in already_checked: - continue - else: - to_check.append(ph) - already_checked.add(ph) - if len(to_check) == 1000: - break - msg = wallet_protocol.RegisterForPhUpdates(to_check, uint32(0)) - all_state: Optional[RespondToPhUpdates] = await peer.register_interest_in_puzzle_hash(msg) - assert all_state is not None - - assert peer_request_cache is not None - await self.receive_state_from_peer(all_state.coin_states, peer, None, None) - - # Check if new puzzle hashed have been created - check_again = await self.get_puzzle_hashes_to_subscribe() - - continue_while = False - for ph in check_again: - if ph not in already_checked: - continue_while = True - break - - async def untrusted_sync_to_peer(self, peer: WSChiaConnection, syncing: bool, fork_height: int): + async def get_coin_ids_to_subscribe(self, min_height: int) -> List[bytes32]: assert self.wallet_state_manager is not None - # If new weight proof is higher than the old one, rollback to the fork point and than apply new coin_states - self.log.info(f"Starting untrusted sync to: {peer.get_peer_info()}, syncing: {syncing}, fork at: {fork_height}") - if syncing: - self.log.info(f"Rollback for {fork_height}") - await self.wallet_state_manager.reorg_rollback(fork_height) - - start_time: float = time.time() - peer_request_cache: PeerRequestCache = self.get_cache_for_peer(peer) - self.untrusted_caches[peer.peer_node_id] = peer_request_cache - # Always sync fully from untrusted - # Get state for puzzle hashes - self.log.debug("Start untrusted_subscribe_to_puzzle_hashes ") - await self.untrusted_subscribe_to_puzzle_hashes(peer, peer_request_cache) - self.log.debug("End untrusted_subscribe_to_puzzle_hashes ") - - checked_all_coins = False - checked_coins: Set[bytes32] = set() - while not checked_all_coins: - # Get state for coins ids - all_coins = await self.wallet_state_manager.coin_store.get_coins_to_check(uint32(0)) - all_coin_names = [coin_record.name() for coin_record in all_coins] - removed_dict = await self.wallet_state_manager.trade_manager.get_coins_of_interest() - all_coin_names.extend(removed_dict.keys()) - - to_check: List[bytes32] = [] - for coin_name in all_coin_names: - if coin_name in checked_coins: - continue - else: - to_check.append(coin_name) - checked_coins.add(coin_name) - if len(to_check) == 1000: - break - - msg1 = wallet_protocol.RegisterForCoinUpdates(to_check, uint32(0)) - new_state: Optional[RespondToCoinUpdates] = await peer.register_interest_in_coin(msg1) - - assert new_state is not None - if syncing: - # If syncing, completely change over to this peer's information - coin_state_before_fork: List[CoinState] = new_state.coin_states - else: - # Otherwise, we only want to apply changes before the fork point, since we are synced to another peer - # We are just validating that there is no missing information - coin_state_before_fork = [] - for coin_state_entry in new_state.coin_states: - if coin_state_entry.spent_height is not None: - if coin_state_entry.spent_height <= fork_height: - coin_state_before_fork.append(coin_state_entry) - elif coin_state_entry.created_height is not None: - if coin_state_entry.created_height <= fork_height: - coin_state_before_fork.append(coin_state_entry) - - await self.receive_state_from_peer(coin_state_before_fork, peer, None, None) - - all_coins = await self.wallet_state_manager.coin_store.get_coins_to_check(uint32(0)) - all_coin_names = [coin_record.name() for coin_record in all_coins] - removed_dict = await self.wallet_state_manager.trade_manager.get_coins_of_interest() - all_coin_names.extend(removed_dict.keys()) - - checked_all_coins = True - for coin_name in all_coin_names: - if coin_name not in checked_coins: - checked_all_coins = False - break - - end_time = time.time() - duration = end_time - start_time - self.log.info(f"Sync duration was: {duration}") + all_coins: Set[WalletCoinRecord] = await self.wallet_state_manager.coin_store.get_coins_to_check(min_height) + all_coin_names: Set[bytes32] = {coin_record.name() for coin_record in all_coins} + removed_dict = await self.wallet_state_manager.trade_manager.get_coins_of_interest() + all_coin_names.update(removed_dict.keys()) + all_coin_names.update(await self.wallet_state_manager.interested_store.get_interested_coin_ids()) + return list(all_coin_names) async def validate_received_state_from_peer( self, coin_state: CoinState, peer: WSChiaConnection, peer_request_cache: PeerRequestCache, + fork_height: Optional[uint32], ) -> bool: """ Returns all state that is valid and included in the blockchain proved by the weight proof. If return_old_states is False, only new states that are not in the coin_store are returned. """ assert self.wallet_state_manager is not None - if coin_state.coin.get_hash() in peer_request_cache.states_validated: + + # Only use the cache if we are talking about states before the fork point. If we are evaluating something + # in a reorg, we cannot use the cache, since we don't know if it's actually in the new chain after the reorg. + if await can_use_peer_request_cache(coin_state, peer_request_cache, fork_height): return True spent_height = coin_state.spent_height @@ -1101,7 +1023,7 @@ async def validate_received_state_from_peer( current = await self.wallet_state_manager.coin_store.get_coin_record(coin_state.coin.name()) # if remote state is same as current local state we skip validation - # CoinRecord unspent = height 0, coin state = None. We adjust for comparison bellow + # CoinRecord unspent = height 0, coin state = None. We adjust for comparison below current_spent_height = None if current is not None and current.spent_block_height != 0: current_spent_height = current.spent_block_height @@ -1143,13 +1065,16 @@ async def validate_received_state_from_peer( ) if validate_additions_result is False: + self.log.warning("Validate false 1") await peer.close(9999) return False - # get blocks on top of this block - validated = await self.validate_block_inclusion(state_block, peer, peer_request_cache) - if not validated: - return False + # If spent_height is None, we need to validate that the creation block is actually in the longest blockchain. + # Otherwise, we don't have to, since we will validate the spent block later. + if coin_state.spent_height is None: + validated = await self.validate_block_inclusion(state_block, peer, peer_request_cache) + if not validated: + return False if spent_height is None and current is not None and current.spent_block_height != 0: # Peer is telling us that coin that was previously known to be spent is not spent anymore @@ -1173,6 +1098,7 @@ async def validate_received_state_from_peer( spent_state_block.foliage_transaction_block.removals_root, ) if validate_removals_result is False: + self.log.warning("Validate false 2") await peer.close(9999) return False validated = await self.validate_block_inclusion(spent_state_block, peer, peer_request_cache) @@ -1198,12 +1124,13 @@ async def validate_received_state_from_peer( spent_state_block.foliage_transaction_block.removals_root, ) if validate_removals_result is False: + self.log.warning("Validate false 3") await peer.close(9999) return False validated = await self.validate_block_inclusion(spent_state_block, peer, peer_request_cache) if not validated: return False - peer_request_cache.states_validated[coin_state.coin.get_hash()] = coin_state + peer_request_cache.states_validated[coin_state.get_hash()] = coin_state return True async def validate_block_inclusion( @@ -1224,6 +1151,8 @@ async def validate_block_inclusion( if block.height >= weight_proof.recent_chain_data[0].height: # this was already validated as part of the wp validation index = block.height - weight_proof.recent_chain_data[0].height + if index >= len(weight_proof.recent_chain_data): + return False if weight_proof.recent_chain_data[index].header_hash != block.header_hash: self.log.error("Failed validation 1") return False @@ -1354,7 +1283,42 @@ async def fetch_puzzle_solution(self, peer, height: uint32, coin: Coin) -> CoinS solution_response.response.solution.to_serialized_program(), ) - async def fetch_children(self, peer, coin_name) -> List[CoinState]: + async def get_coin_state( + self, coin_names: List[bytes32], fork_height: Optional[uint32] = None, peer: Optional[WSChiaConnection] = None + ) -> List[CoinState]: + assert self.server is not None + all_nodes = self.server.connection_by_type[NodeType.FULL_NODE] + if len(all_nodes.keys()) == 0: + raise ValueError("Not connected to the full node") + # Use supplied if provided, prioritize trusted otherwise + if peer is None: + for node in list(all_nodes.values()): + if self.is_trusted(node): + peer = node + break + if peer is None: + peer = list(all_nodes.values())[0] + + assert peer is not None + msg = wallet_protocol.RegisterForCoinUpdates(coin_names, uint32(0)) + coin_state: Optional[RespondToCoinUpdates] = await peer.register_interest_in_coin(msg) + assert coin_state is not None + + if not self.is_trusted(peer): + valid_list = [] + for coin in coin_state.coin_states: + valid = await self.validate_received_state_from_peer( + coin, peer, self.get_cache_for_peer(peer), fork_height + ) + if valid: + valid_list.append(coin) + return valid_list + + return coin_state.coin_states + + async def fetch_children( + self, peer: WSChiaConnection, coin_name: bytes32, fork_height: Optional[uint32] = None + ) -> List[CoinState]: response: Optional[wallet_protocol.RespondChildren] = await peer.request_children( wallet_protocol.RequestChildren(coin_name) ) @@ -1365,13 +1329,13 @@ async def fetch_children(self, peer, coin_name) -> List[CoinState]: request_cache = self.get_cache_for_peer(peer) validated = [] for state in response.coin_states: - valid = await self.validate_received_state_from_peer(state, peer, request_cache) + valid = await self.validate_received_state_from_peer(state, peer, request_cache, fork_height) if valid: validated.append(state) return validated return response.coin_states - # For RPC only. You should use wallet_state_manager.add_pending_transaction for normal wallet business. + # For RPC only. You should use wallet_state_manager.add_pending_transaction for normal wallet business. async def push_tx(self, spend_bundle): msg = make_msg( ProtocolMessageTypes.send_transaction, diff --git a/chia/wallet/wallet_node_api.py b/chia/wallet/wallet_node_api.py index 9e0c6617b142..ec16eef24dc7 100644 --- a/chia/wallet/wallet_node_api.py +++ b/chia/wallet/wallet_node_api.py @@ -46,7 +46,8 @@ async def new_peak_wallet(self, peak: wallet_protocol.NewPeakWallet, peer: WSChi """ The full node sent as a new peak """ - await self.wallet_node.new_peak_wallet(peak, peer) + self.wallet_node.node_peaks[peer.peer_node_id] = (peak.height, peak.header_hash) + await self.wallet_node.new_peak_queue.new_peak_wallet(peak, peer) @api_request async def reject_block_header(self, response: wallet_protocol.RejectHeaderRequest): @@ -135,10 +136,11 @@ async def respond_header_blocks(self, request: wallet_protocol.RespondHeaderBloc async def reject_header_blocks(self, request: wallet_protocol.RejectHeaderBlocks): self.log.warning(f"Reject header blocks: {request}") + @execute_task @peer_required @api_request async def coin_state_update(self, request: wallet_protocol.CoinStateUpdate, peer: WSChiaConnection): - await self.wallet_node.state_update_received(request, peer) + await self.wallet_node.new_peak_queue.full_node_state_updated(request, peer) @api_request async def respond_to_ph_update(self, request: wallet_protocol.RespondToPhUpdates): diff --git a/chia/wallet/wallet_puzzle_store.py b/chia/wallet/wallet_puzzle_store.py index 8034a7237810..c68df2898740 100644 --- a/chia/wallet/wallet_puzzle_store.py +++ b/chia/wallet/wallet_puzzle_store.py @@ -308,7 +308,7 @@ async def index_for_puzzle_hash_and_wallet(self, puzzle_hash: bytes32, wallet_id return None - async def wallet_info_for_puzzle_hash(self, puzzle_hash: bytes32) -> Optional[Tuple[uint32, WalletType]]: + async def wallet_info_for_puzzle_hash(self, puzzle_hash: bytes32) -> Optional[Tuple[int, WalletType]]: """ Returns the derivation path for the puzzle_hash. Returns None if not present. diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index d3a060c1d986..504318b4b423 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -326,9 +326,12 @@ async def create_more_puzzle_hashes(self, from_zero: bool = False, in_transactio False, ) ) - puzzle_hashes = [record.puzzle_hash for record in derivation_paths] await self.puzzle_store.add_derivation_paths(derivation_paths, in_transaction) - await self.wallet_node.new_puzzle_hash_created(puzzle_hashes) + await self.add_interested_puzzle_hashes( + [record.puzzle_hash for record in derivation_paths], + [record.wallet_id for record in derivation_paths], + in_transaction, + ) if unused > 0: await self.puzzle_store.set_used_up_to(uint32(unused - 1), in_transaction) @@ -446,7 +449,7 @@ async def synced(self): if latest is None: return False - if latest.height - self.finished_sync_up_to > 2: + if latest.height - await self.blockchain.get_finished_sync_up_to() > 1: return False latest_timestamp = self.blockchain.get_latest_timestamp() @@ -571,13 +574,17 @@ async def unconfirmed_removals_for_wallet(self, wallet_id: int) -> Dict[bytes32, removals[coin.name()] = coin return removals - async def fetch_parent_and_check_for_cat(self, peer, coin_state) -> Tuple[Optional[uint32], Optional[WalletType]]: + async def fetch_parent_and_check_for_cat( + self, peer: WSChiaConnection, coin_state: CoinState, fork_height: Optional[uint32] + ) -> Tuple[Optional[uint32], Optional[WalletType]]: if self.is_pool_reward(coin_state.created_height, coin_state.coin.parent_coin_info) or self.is_farmer_reward( coin_state.created_height, coin_state.coin.parent_coin_info ): return None, None - response: List[CoinState] = await self.wallet_node.get_coin_state([coin_state.coin.parent_coin_info], peer) + response: List[CoinState] = await self.wallet_node.get_coin_state( + [coin_state.coin.parent_coin_info], fork_height, peer + ) if len(response) == 0: self.log.warning(f"Could not find a parent coin with ID: {coin_state.coin.parent_coin_info}") return None, None @@ -624,11 +631,12 @@ async def fetch_parent_and_check_for_cat(self, peer, coin_state) -> Tuple[Option return wallet_id, wallet_type async def new_coin_state( - self, - coin_states: List[CoinState], - peer: WSChiaConnection, - ): - created_h_none = [] + self, coin_states: List[CoinState], peer: WSChiaConnection, fork_height: Optional[uint32] + ) -> None: + # TODO: add comment about what this method does + + # Sort by created height, then add the reorg states (created_height is None) to the end + created_h_none: List[CoinState] = [] for coin_st in coin_states.copy(): if coin_st.created_height is None: coin_states.remove(coin_st) @@ -641,10 +649,13 @@ async def new_coin_state( trade_coin_removed: List[CoinState] = [] for coin_state_idx, coin_state in enumerate(coin_states): - info = await self.get_wallet_id_for_puzzle_hash(coin_state.coin.puzzle_hash) + wallet_info: Optional[Tuple[uint32, WalletType]] = await self.get_wallet_id_for_puzzle_hash( + coin_state.coin.puzzle_hash + ) local_record: Optional[WalletCoinRecord] = await self.coin_store.get_coin_record(coin_state.coin.name()) self.log.debug(f"{coin_state.coin.name()}: {coin_state}") + # If we already have this coin, and it was spent and confirmed at the same heights, then we return (done) if local_record is not None: local_spent = None if local_record.spent_block_height != 0: @@ -655,15 +666,15 @@ async def new_coin_state( ): continue - wallet_id = None - wallet_type = None - if info is not None: - wallet_id, wallet_type = info + wallet_id: Optional[uint32] = None + wallet_type: Optional[WalletType] = None + if wallet_info is not None: + wallet_id, wallet_type = wallet_info elif local_record is not None: wallet_id = uint32(local_record.wallet_id) wallet_type = local_record.wallet_type elif coin_state.created_height is not None: - wallet_id, wallet_type = await self.fetch_parent_and_check_for_cat(peer, coin_state) + wallet_id, wallet_type = await self.fetch_parent_and_check_for_cat(peer, coin_state, fork_height) if wallet_id is None or wallet_type is None: self.log.info(f"No wallet for coin state: {coin_state}") @@ -683,6 +694,7 @@ async def new_coin_state( if coin_state.created_height is None: # TODO implements this coin got reorged + # TODO: we need to potentially roll back the pool wallet here pass elif coin_state.created_height is not None and coin_state.spent_height is None: await self.coin_added(coin_state.coin, coin_state.created_height, all_txs, wallet_id, wallet_type) @@ -745,7 +757,7 @@ async def new_coin_state( ) await self.tx_store.add_transaction_record(tx_record, False) - children = await self.wallet_node.fetch_children(peer, coin_state.coin.name()) + children = await self.wallet_node.fetch_children(peer, coin_state.coin.name(), fork_height) assert children is not None additions = [state.coin for state in children] if len(children) > 0: @@ -820,6 +832,7 @@ async def new_coin_state( if coin_state.spent_height is not None and coin_state.coin.amount == uint64(1): wallet = self.wallets[uint32(record.wallet_id)] curr_coin_state: CoinState = coin_state + while curr_coin_state.spent_height is not None: cs: CoinSpend = await self.wallet_node.fetch_puzzle_solution( peer, curr_coin_state.spent_height, curr_coin_state.coin @@ -839,16 +852,16 @@ async def new_coin_state( record.wallet_type, ) await self.coin_store.set_spent(curr_coin_state.coin.name(), curr_coin_state.spent_height) - await self.interested_store.add_interested_coin_id(new_singleton_coin.name(), True) + await self.add_interested_coin_ids([new_singleton_coin.name()], True) new_coin_state: List[CoinState] = await self.wallet_node.get_coin_state( - [new_singleton_coin.name()] + [new_singleton_coin.name()], fork_height, peer ) assert len(new_coin_state) == 1 curr_coin_state = new_coin_state[0] # Check if a child is a singleton launcher if children is None: - children = await self.wallet_node.fetch_children(peer, coin_state.coin.name()) + children = await self.wallet_node.fetch_children(peer, coin_state.coin.name(), fork_height) assert children is not None for child in children: if child.coin.puzzle_hash != SINGLETON_LAUNCHER_HASH: @@ -856,18 +869,19 @@ async def new_coin_state( if await self.have_a_pool_wallet_with_launched_id(child.coin.name()): continue if child.spent_height is None: + # TODO handle spending launcher later block continue launcher_spend: Optional[CoinSpend] = await self.wallet_node.fetch_puzzle_solution( peer, coin_state.spent_height, child.coin ) if launcher_spend is None: continue - pool_state = None try: pool_state = solution_to_pool_state(launcher_spend) except Exception as e: self.log.debug(f"Not a pool wallet launcher {e}") continue + # solution_to_pool_state may return None but this may not be an error if pool_state is None: self.log.debug("solution_to_pool_state returned None, ignore and continue") @@ -887,12 +901,12 @@ async def new_coin_state( await self.coin_added( coin_added, coin_state.spent_height, [], pool_wallet.id(), WalletType(pool_wallet.type()) ) - await self.interested_store.add_interested_coin_id(coin_added.name(), True) + await self.add_interested_coin_ids([coin_added.name()], True) else: raise RuntimeError("All cases already handled") # Logic error, all cases handled for coin_state_removed in trade_coin_removed: - await self.trade_manager.coins_of_interest_farmed(coin_state_removed) + await self.trade_manager.coins_of_interest_farmed(coin_state_removed, fork_height) async def have_a_pool_wallet_with_launched_id(self, launcher_id: bytes32) -> bool: for wallet_id, wallet in self.wallets.items(): @@ -928,15 +942,16 @@ async def get_wallet_id_for_puzzle_hash(self, puzzle_hash: bytes32) -> Optional[ info = await self.puzzle_store.wallet_info_for_puzzle_hash(puzzle_hash) if info is not None: wallet_id, wallet_type = info - return wallet_id, wallet_type + return uint32(wallet_id), wallet_type interested_wallet_id = await self.interested_store.get_interested_puzzle_hash_wallet_id(puzzle_hash=puzzle_hash) if interested_wallet_id is not None: wallet_id = uint32(interested_wallet_id) if wallet_id not in self.wallets.keys(): self.log.warning(f"Do not have wallet {wallet_id} for puzzle_hash {puzzle_hash}") + return None wallet_type = WalletType(self.wallets[uint32(wallet_id)].type()) - return wallet_id, wallet_type + return uint32(wallet_id), wallet_type return None async def coin_added( @@ -1052,9 +1067,7 @@ async def add_pending_transaction(self, tx_record: TransactionRecord): all_coins_names.extend([coin.name() for coin in tx_record.additions]) all_coins_names.extend([coin.name() for coin in tx_record.removals]) - nodes = self.server.get_full_node_connections() - for node in nodes: - await self.wallet_node.subscribe_to_coin_updates(all_coins_names, node) + await self.add_interested_coin_ids(all_coins_names) self.tx_pending_changed() self.state_changed("pending_transaction", tx_record.wallet_id) @@ -1124,7 +1137,6 @@ async def reorg_rollback(self, height: int): TransactionType.INCOMING_TRADE, ]: await self.tx_store.tx_reorged(record) - self.tx_pending_changed() # Removes wallets that were created from a blockchain transaction which got reorged. @@ -1247,16 +1259,19 @@ async def new_peak(self, peak: wallet_protocol.NewPeakWallet): if wallet.type() == uint8(WalletType.POOLING_WALLET): await wallet.new_peak(peak.height) - async def add_interested_puzzle_hash( - self, puzzle_hash: bytes32, wallet_id: int, in_transaction: bool = False + async def add_interested_puzzle_hashes( + self, puzzle_hashes: List[bytes32], wallet_ids: List[int], in_transaction: bool = False ) -> None: - await self.interested_store.add_interested_puzzle_hash(puzzle_hash, wallet_id, in_transaction) - await self.wallet_node.new_puzzle_hash_created([puzzle_hash]) - - async def add_interested_coin_id(self, coin_id: bytes32) -> None: - nodes = self.server.get_full_node_connections() - for node in nodes: - await self.wallet_node.subscribe_to_coin_updates([coin_id], node) + for puzzle_hash, wallet_id in zip(puzzle_hashes, wallet_ids): + await self.interested_store.add_interested_puzzle_hash(puzzle_hash, wallet_id, in_transaction) + if len(puzzle_hashes) > 0: + await self.wallet_node.new_peak_queue.subscribe_to_puzzle_hashes(puzzle_hashes) + + async def add_interested_coin_ids(self, coin_ids: List[bytes32], in_transaction: bool = False) -> None: + for coin_id in coin_ids: + await self.interested_store.add_interested_coin_id(coin_id, in_transaction) + if len(coin_ids) > 0: + await self.wallet_node.new_peak_queue.subscribe_to_coin_ids(coin_ids) async def delete_trade_transactions(self, trade_id: bytes32): txs: List[TransactionRecord] = await self.tx_store.get_transactions_by_trade_id(trade_id) diff --git a/tests/core/full_node/test_coin_store.py b/tests/core/full_node/test_coin_store.py index 9f4b3b8a6f60..ce8c8ef79bb9 100644 --- a/tests/core/full_node/test_coin_store.py +++ b/tests/core/full_node/test_coin_store.py @@ -16,6 +16,7 @@ from chia.types.full_block import FullBlock from chia.types.generator_types import BlockGenerator from chia.util.generator_tools import tx_removals_and_additions +from chia.util.hash import std_hash from chia.util.ints import uint64, uint32 from tests.blockchain.blockchain_test_utils import _validate_and_add_block from tests.wallet_tools import WalletTool @@ -368,3 +369,44 @@ async def test_get_puzzle_hash(self, cache_size: uint32, tmp_dir, db_version): assert len(coins_pool) == num_blocks - 2 b.shut_down() + + @pytest.mark.asyncio + @pytest.mark.parametrize("cache_size", [0, 10, 100000]) + async def test_get_coin_states(self, cache_size: uint32, tmp_dir, db_version): + async with DBConnection(db_version) as db_wrapper: + crs = [ + CoinRecord( + Coin(std_hash(i.to_bytes(4, byteorder="big")), std_hash(b"2"), uint64(100)), + uint32(i), + uint32(2 * i), + False, + uint64(12321312), + ) + for i in range(1, 301) + ] + crs += [ + CoinRecord( + Coin(std_hash(b"X" + i.to_bytes(4, byteorder="big")), std_hash(b"3"), uint64(100)), + uint32(i), + uint32(2 * i), + False, + uint64(12321312), + ) + for i in range(1, 301) + ] + coin_store = await CoinStore.create(db_wrapper, cache_size=uint32(cache_size)) + await coin_store._add_coin_records(crs) + + assert len(await coin_store.get_coin_states_by_puzzle_hashes(True, [std_hash(b"2")], 0)) == 300 + assert len(await coin_store.get_coin_states_by_puzzle_hashes(False, [std_hash(b"2")], 0)) == 0 + assert len(await coin_store.get_coin_states_by_puzzle_hashes(True, [std_hash(b"2")], 300)) == 151 + assert len(await coin_store.get_coin_states_by_puzzle_hashes(True, [std_hash(b"2")], 603)) == 0 + assert len(await coin_store.get_coin_states_by_puzzle_hashes(True, [std_hash(b"1")], 0)) == 0 + + coins = [cr.coin.name() for cr in crs] + bad_coins = [std_hash(cr.coin.name()) for cr in crs] + assert len(await coin_store.get_coin_states_by_ids(True, coins, 0)) == 600 + assert len(await coin_store.get_coin_states_by_ids(False, coins, 0)) == 0 + assert len(await coin_store.get_coin_states_by_ids(True, coins, 300)) == 302 + assert len(await coin_store.get_coin_states_by_ids(True, coins, 603)) == 0 + assert len(await coin_store.get_coin_states_by_ids(True, bad_coins, 0)) == 0 diff --git a/tests/core/full_node/test_full_node.py b/tests/core/full_node/test_full_node.py index 8f299ef1f24e..a4fbeef68a3e 100644 --- a/tests/core/full_node/test_full_node.py +++ b/tests/core/full_node/test_full_node.py @@ -44,6 +44,7 @@ _validate_and_add_block, _validate_and_add_block_no_error, ) +from tests.pools.test_pool_rpc import wallet_is_synced from tests.wallet_tools import WalletTool from chia.wallet.cat_wallet.cat_wallet import CATWallet from chia.wallet.transaction_record import TransactionRecord @@ -183,9 +184,10 @@ async def test_block_compression(self, setup_two_nodes_and_wallet, empty_blockch for i in range(4): await full_node_1.farm_new_transaction_block(FarmNewBlockProtocol(ph)) - await time_out_assert(10, wallet_height_at_least, True, wallet_node_1, 4) - await time_out_assert(10, node_height_at_least, True, full_node_1, 4) - await time_out_assert(10, node_height_at_least, True, full_node_2, 4) + await time_out_assert(30, wallet_height_at_least, True, wallet_node_1, 4) + await time_out_assert(30, node_height_at_least, True, full_node_1, 4) + await time_out_assert(30, node_height_at_least, True, full_node_2, 4) + await time_out_assert(30, wallet_is_synced, True, wallet_node_1, full_node_1) # Send a transaction to mempool tr: TransactionRecord = await wallet.generate_signed_transaction( @@ -202,16 +204,17 @@ async def test_block_compression(self, setup_two_nodes_and_wallet, empty_blockch # Farm a block await full_node_1.farm_new_transaction_block(FarmNewBlockProtocol(ph)) - await time_out_assert(10, node_height_at_least, True, full_node_1, 5) - await time_out_assert(10, node_height_at_least, True, full_node_2, 5) - await time_out_assert(10, wallet_height_at_least, True, wallet_node_1, 5) + await time_out_assert(30, node_height_at_least, True, full_node_1, 5) + await time_out_assert(30, node_height_at_least, True, full_node_2, 5) + await time_out_assert(30, wallet_height_at_least, True, wallet_node_1, 5) + await time_out_assert(30, wallet_is_synced, True, wallet_node_1, full_node_1) async def check_transaction_confirmed(transaction) -> bool: tx = await wallet_node_1.wallet_state_manager.get_transaction(transaction.name) return tx.confirmed - await time_out_assert(10, check_transaction_confirmed, True, tr) - await asyncio.sleep(0.5) + await time_out_assert(30, check_transaction_confirmed, True, tr) + await asyncio.sleep(2) # Confirm generator is not compressed program: Optional[SerializedProgram] = (await full_node_1.get_all_full_blocks())[-1].transactions_generator @@ -237,9 +240,10 @@ async def check_transaction_confirmed(transaction) -> bool: await time_out_assert(10, node_height_at_least, True, full_node_1, 6) await time_out_assert(10, node_height_at_least, True, full_node_2, 6) await time_out_assert(10, wallet_height_at_least, True, wallet_node_1, 6) + await time_out_assert(30, wallet_is_synced, True, wallet_node_1, full_node_1) await time_out_assert(10, check_transaction_confirmed, True, tr) - await asyncio.sleep(0.5) + await asyncio.sleep(2) # Confirm generator is compressed program: Optional[SerializedProgram] = (await full_node_1.get_all_full_blocks())[-1].transactions_generator @@ -253,6 +257,7 @@ async def check_transaction_confirmed(transaction) -> bool: await time_out_assert(10, node_height_at_least, True, full_node_1, 8) await time_out_assert(10, node_height_at_least, True, full_node_2, 8) await time_out_assert(10, wallet_height_at_least, True, wallet_node_1, 8) + await time_out_assert(30, wallet_is_synced, True, wallet_node_1, full_node_1) # Send another 2 tx tr: TransactionRecord = await wallet.generate_signed_transaction( @@ -307,9 +312,10 @@ async def check_transaction_confirmed(transaction) -> bool: await time_out_assert(10, node_height_at_least, True, full_node_1, 9) await time_out_assert(10, node_height_at_least, True, full_node_2, 9) await time_out_assert(10, wallet_height_at_least, True, wallet_node_1, 9) + await time_out_assert(30, wallet_is_synced, True, wallet_node_1, full_node_1) await time_out_assert(10, check_transaction_confirmed, True, tr) - await asyncio.sleep(0.5) + await asyncio.sleep(2) # Confirm generator is compressed program: Optional[SerializedProgram] = (await full_node_1.get_all_full_blocks())[-1].transactions_generator @@ -352,9 +358,10 @@ async def check_transaction_confirmed(transaction) -> bool: await time_out_assert(10, node_height_at_least, True, full_node_1, 10) await time_out_assert(10, node_height_at_least, True, full_node_2, 10) await time_out_assert(10, wallet_height_at_least, True, wallet_node_1, 10) + await time_out_assert(30, wallet_is_synced, True, wallet_node_1, full_node_1) await time_out_assert(10, check_transaction_confirmed, True, new_tr) - await asyncio.sleep(0.5) + await asyncio.sleep(2) # Confirm generator is not compressed, #CAT creation has a cat spend all_blocks = await full_node_1.get_all_full_blocks() @@ -397,6 +404,7 @@ async def check_transaction_confirmed(transaction) -> bool: await time_out_assert(10, node_height_at_least, True, full_node_1, 11) await time_out_assert(10, node_height_at_least, True, full_node_2, 11) await time_out_assert(10, wallet_height_at_least, True, wallet_node_1, 11) + await time_out_assert(30, wallet_is_synced, True, wallet_node_1, full_node_1) # Confirm generator is not compressed program: Optional[SerializedProgram] = (await full_node_1.get_all_full_blocks())[-1].transactions_generator diff --git a/tests/core/server/test_rate_limits.py b/tests/core/server/test_rate_limits.py index a1011fbd2172..7833a04ba2a5 100644 --- a/tests/core/server/test_rate_limits.py +++ b/tests/core/server/test_rate_limits.py @@ -23,11 +23,11 @@ async def test_too_many_messages(self): # Too many messages r = RateLimiter(incoming=True) new_tx_message = make_msg(ProtocolMessageTypes.new_transaction, bytes([1] * 40)) - for i in range(3000): + for i in range(4900): assert r.process_msg_and_check(new_tx_message) saw_disconnect = False - for i in range(3000): + for i in range(4900): response = r.process_msg_and_check(new_tx_message) if not response: saw_disconnect = True @@ -146,11 +146,11 @@ async def test_periodic_reset(self): # Counts reset also r = RateLimiter(True, 5) new_tx_message = make_msg(ProtocolMessageTypes.new_transaction, bytes([1] * 40)) - for i in range(3000): + for i in range(4900): assert r.process_msg_and_check(new_tx_message) saw_disconnect = False - for i in range(3000): + for i in range(4900): response = r.process_msg_and_check(new_tx_message) if not response: saw_disconnect = True diff --git a/tests/pools/test_pool_rpc.py b/tests/pools/test_pool_rpc.py index 4e053fa2608d..abae043baa5a 100644 --- a/tests/pools/test_pool_rpc.py +++ b/tests/pools/test_pool_rpc.py @@ -23,6 +23,7 @@ from chia.util.bech32m import encode_puzzle_hash from chia.util.byte_types import hexstr_to_bytes from chia.wallet.derive_keys import find_authentication_sk, find_owner_sk +from chia.wallet.wallet_node import WalletNode from tests.block_tools import get_plot_dir from chia.util.config import load_config from chia.util.ints import uint16, uint32 @@ -62,6 +63,14 @@ async def create_pool_plot(p2_singleton_puzzle_hash: bytes32) -> Optional[bytes3 return plot_id +async def wallet_is_synced(wallet_node: WalletNode, full_node_api): + 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() + ) + + @pytest.fixture(scope="session") def event_loop(): loop = asyncio.get_event_loop() @@ -186,7 +195,7 @@ async def test_create_new_pool_wallet_self_farm(self, one_wallet_node_and_rpc, f for summary in summaries_response: if WalletType(int(summary["type"])) == WalletType.POOLING_WALLET: assert False - + await time_out_assert(10, wallet_is_synced, True, wallet_node_0, full_node_api) creation_tx: TransactionRecord = await client.create_new_pool_wallet( our_ph, "", 0, "localhost:5000", "new", "SELF_POOLING", fee ) @@ -200,6 +209,7 @@ async def test_create_new_pool_wallet_self_farm(self, one_wallet_node_and_rpc, f await self.farm_blocks(full_node_api, our_ph, 6) assert full_node_api.full_node.mempool_manager.get_spendbundle(creation_tx.name) is None + await time_out_assert(10, wallet_is_synced, True, wallet_node_0, full_node_api) summaries_response = await client.get_wallets() wallet_id: Optional[int] = None for summary in summaries_response: @@ -257,6 +267,8 @@ async def test_create_new_pool_wallet_farm_to_pool(self, one_wallet_node_and_rpc await time_out_assert(10, wallet_0.get_confirmed_balance, total_block_rewards) + await time_out_assert(10, wallet_is_synced, True, wallet_node_0, full_node_api) + our_ph = await wallet_0.get_new_puzzlehash() summaries_response = await client.get_wallets() for summary in summaries_response: @@ -276,6 +288,7 @@ async def test_create_new_pool_wallet_farm_to_pool(self, one_wallet_node_and_rpc await self.farm_blocks(full_node_api, our_ph, 6) assert full_node_api.full_node.mempool_manager.get_spendbundle(creation_tx.name) is None + await time_out_assert(5, wallet_is_synced, True, wallet_node_0, full_node_api) summaries_response = await client.get_wallets() wallet_id: Optional[int] = None for summary in summaries_response: @@ -331,6 +344,7 @@ async def test_create_multiple_pool_wallets(self, one_wallet_node_and_rpc, fee, wallet_0 = wallet_node_0.wallet_state_manager.main_wallet await time_out_assert(10, wallet_0.get_confirmed_balance, total_block_rewards) await time_out_assert(10, wallet_node_0.wallet_state_manager.blockchain.get_peak_height, PREFARMED_BLOCKS) + await time_out_assert(10, wallet_is_synced, True, wallet_node_0, full_node_api) our_ph_1 = await wallet_0.get_new_puzzlehash() our_ph_2 = await wallet_0.get_new_puzzlehash() @@ -390,12 +404,6 @@ async def test_create_multiple_pool_wallets(self, one_wallet_node_and_rpc, fee, with pytest.raises(ValueError): await client.pw_status(3) - def wallet_is_synced(): - return ( - wallet_node_0.wallet_state_manager.blockchain.get_peak_height() - == full_node_api.full_node.blockchain.get_peak_height() - ) - # Create some CAT wallets to increase wallet IDs for i in range(5): await asyncio.sleep(2) @@ -407,7 +415,7 @@ def wallet_is_synced(): asset_id = bytes.fromhex(res["asset_id"]) assert len(asset_id) > 0 await self.farm_blocks(full_node_api, our_ph_2, 6) - await time_out_assert(20, wallet_is_synced) + await time_out_assert(20, wallet_is_synced, True, wallet_node_0, full_node_api) bal_0 = await client.get_wallet_balance(cat_0_id) assert bal_0["confirmed_wallet_balance"] == 20 @@ -415,6 +423,7 @@ def wallet_is_synced(): # run this code more than once, since it's slow. if fee == 0 and not trusted: for i in range(22): + await time_out_assert(10, wallet_is_synced, True, wallet_node_0, full_node_api) creation_tx_3: TransactionRecord = await client.create_new_pool_wallet( our_ph_1, "localhost", 5, "localhost:5000", "new", "FARMING_TO_POOL", fee ) @@ -425,7 +434,7 @@ def wallet_is_synced(): creation_tx_3.name, ) await self.farm_blocks(full_node_api, our_ph_2, 2) - await time_out_assert(20, wallet_is_synced) + await time_out_assert(20, wallet_is_synced, True, wallet_node_0, full_node_api) full_config: Dict = load_config(wallet_0.wallet_state_manager.root_path, "config.yaml") pool_list: List[Dict] = full_config["pool"]["pool_list"] @@ -447,7 +456,7 @@ def wallet_is_synced(): assert owner_sk != auth_sk @pytest.mark.asyncio - @pytest.mark.parametrize("trusted", [True]) + @pytest.mark.parametrize("trusted", [True, False]) @pytest.mark.parametrize("fee", [0]) async def test_absorb_self(self, one_wallet_node_and_rpc, fee, trusted): client, wallet_node_0, full_node_api = one_wallet_node_and_rpc @@ -472,6 +481,7 @@ async def test_absorb_self(self, one_wallet_node_and_rpc, fee, trusted): if WalletType(int(summary["type"])) == WalletType.POOLING_WALLET: assert False + await time_out_assert(10, wallet_is_synced, True, wallet_node_0, full_node_api) creation_tx: TransactionRecord = await client.create_new_pool_wallet( our_ph, "", 0, "localhost:5000", "new", "SELF_POOLING", fee ) @@ -577,6 +587,7 @@ async def test_absorb_pooling(self, one_wallet_node_and_rpc, fee, trusted): await time_out_assert(10, wallet_0.get_confirmed_balance, total_block_rewards) await time_out_assert(10, wallet_node_0.wallet_state_manager.blockchain.get_peak_height, PREFARMED_BLOCKS) + await time_out_assert(10, wallet_is_synced, True, wallet_node_0, full_node_api) our_ph = await wallet_0.get_new_puzzlehash() summaries_response = await client.get_wallets() for summary in summaries_response: @@ -719,6 +730,7 @@ async def test_self_pooling_to_pooling(self, setup, fee, trusted): await time_out_assert(10, wallets[0].get_unconfirmed_balance, total_block_rewards) await time_out_assert(10, wallets[0].get_confirmed_balance, total_block_rewards) await time_out_assert(10, wallets[0].get_spendable_balance, total_block_rewards) + await time_out_assert(10, wallet_is_synced, True, wallet_node_0, full_node_api) assert total_block_rewards > 0 summaries_response = await client.get_wallets() @@ -748,6 +760,7 @@ async def test_self_pooling_to_pooling(self, setup, fee, trusted): await self.farm_blocks(full_node_api, our_ph, 6) assert full_node_api.full_node.mempool_manager.get_spendbundle(creation_tx.name) is None + await time_out_assert(10, wallet_is_synced, True, wallet_node_0, full_node_api) summaries_response = await client.get_wallets() wallet_id: Optional[int] = None @@ -857,6 +870,7 @@ async def have_chia(): return (await wallets[0].get_confirmed_balance()) > 0 await time_out_assert(timeout=WAIT_SECS, function=have_chia) + await time_out_assert(10, wallet_is_synced, True, wallet_nodes[0], full_node_api) creation_tx: TransactionRecord = await client.create_new_pool_wallet( our_ph, "", 0, "localhost:5000", "new", "SELF_POOLING", fee @@ -872,6 +886,8 @@ async def have_chia(): await self.farm_blocks(full_node_api, our_ph, 6) assert full_node_api.full_node.mempool_manager.get_spendbundle(creation_tx.name) is None + await time_out_assert(10, wallet_is_synced, True, wallet_nodes[0], full_node_api) + summaries_response = await client.get_wallets() wallet_id: Optional[int] = None for summary in summaries_response: @@ -912,6 +928,8 @@ async def status_is_farming_to_pool(): await time_out_assert(timeout=WAIT_SECS, function=status_is_farming_to_pool) + await time_out_assert(10, wallet_is_synced, True, wallet_nodes[0], full_node_api) + status: PoolWalletInfo = (await client.pw_status(wallet_id))[0] leave_pool_tx: TransactionRecord = await client.pw_self_pool(wallet_id, fee) @@ -974,6 +992,7 @@ async def have_chia(): return (await wallets[0].get_confirmed_balance()) > 0 await time_out_assert(timeout=WAIT_SECS, function=have_chia) + await time_out_assert(10, wallet_is_synced, True, wallet_nodes[0], full_node_api) creation_tx: TransactionRecord = await client.create_new_pool_wallet( pool_a_ph, "https://pool-a.org", 5, "localhost:5000", "new", "FARMING_TO_POOL", fee @@ -989,6 +1008,8 @@ async def have_chia(): await self.farm_blocks(full_node_api, our_ph, 6) assert full_node_api.full_node.mempool_manager.get_spendbundle(creation_tx.name) is None + await time_out_assert(10, wallet_is_synced, True, wallet_nodes[0], full_node_api) + summaries_response = await client.get_wallets() wallet_id: Optional[int] = None for summary in summaries_response: @@ -1074,6 +1095,7 @@ async def have_chia(): return (await wallets[0].get_confirmed_balance()) > 0 await time_out_assert(timeout=WAIT_SECS, function=have_chia) + await time_out_assert(10, wallet_is_synced, True, wallet_nodes[0], full_node_api) creation_tx: TransactionRecord = await client.create_new_pool_wallet( pool_a_ph, "https://pool-a.org", 5, "localhost:5000", "new", "FARMING_TO_POOL", fee @@ -1089,6 +1111,8 @@ async def have_chia(): await self.farm_blocks(full_node_api, our_ph, 6) assert full_node_api.full_node.mempool_manager.get_spendbundle(creation_tx.name) is None + await time_out_assert(5, wallet_is_synced, True, wallet_nodes[0], full_node_api) + summaries_response = await client.get_wallets() wallet_id: Optional[int] = None for summary in summaries_response: diff --git a/tests/wallet/cat_wallet/test_cat_wallet.py b/tests/wallet/cat_wallet/test_cat_wallet.py index 1afe5c16df27..8717ae14937a 100644 --- a/tests/wallet/cat_wallet/test_cat_wallet.py +++ b/tests/wallet/cat_wallet/test_cat_wallet.py @@ -15,6 +15,7 @@ from chia.wallet.cat_wallet.cat_constants import DEFAULT_CATS from chia.wallet.puzzles.cat_loader import CAT_MOD from chia.wallet.transaction_record import TransactionRecord +from tests.pools.test_pool_rpc import wallet_is_synced from tests.setup_nodes import setup_simulators_and_wallets from tests.time_out_assert import time_out_assert @@ -80,6 +81,7 @@ async def test_cat_creation(self, two_wallet_nodes, trusted): ) await time_out_assert(15, wallet.get_confirmed_balance, funds) + await time_out_assert(10, wallet_is_synced, True, wallet_node, full_node_api) async with wallet_node.wallet_state_manager.lock: cat_wallet: CATWallet = await CATWallet.create_new_cat_wallet( diff --git a/tests/wallet/cat_wallet/test_trades.py b/tests/wallet/cat_wallet/test_trades.py index e0f520d4f53e..20fffb1006ef 100644 --- a/tests/wallet/cat_wallet/test_trades.py +++ b/tests/wallet/cat_wallet/test_trades.py @@ -12,6 +12,7 @@ from chia.wallet.trading.offer import Offer from chia.wallet.trading.trade_status import TradeStatus from chia.wallet.transaction_record import TransactionRecord +from tests.pools.test_pool_rpc import wallet_is_synced from tests.setup_nodes import setup_simulators_and_wallets from tests.time_out_assert import time_out_assert @@ -75,6 +76,9 @@ async def wallets_prefarm(two_wallet_nodes, trusted): for i in range(0, buffer): await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(token_bytes())) + await time_out_assert(10, wallet_is_synced, True, wallet_node_0, full_node_api) + await time_out_assert(10, wallet_is_synced, True, wallet_node_1, full_node_api) + return wallet_node_0, wallet_node_1, full_node_api @@ -409,6 +413,7 @@ async def test_trade_cancellation(self, wallets_prefarm): cat_wallet_maker: CATWallet = await CATWallet.create_new_cat_wallet( wallet_node_maker.wallet_state_manager, wallet_maker, {"identifier": "genesis_by_id"}, uint64(100) ) + tx_queue: List[TransactionRecord] = await wallet_node_maker.wallet_state_manager.tx_store.get_not_sent() await time_out_assert( 15, tx_in_pool, True, full_node.full_node.mempool_manager, tx_queue[0].spend_bundle.name() diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index eb58c154f644..b840c9531b4d 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -32,6 +32,7 @@ from chia.wallet.transaction_record import TransactionRecord from chia.wallet.transaction_sorting import SortKey from chia.wallet.util.compute_memos import compute_memos +from tests.pools.test_pool_rpc import wallet_is_synced from tests.setup_nodes import bt, setup_simulators_and_wallets, self_hostname from tests.time_out_assert import time_out_assert @@ -168,6 +169,7 @@ async def eventual_balance(): async def eventual_balance_det(c, wallet_id: str): return (await c.get_wallet_balance(wallet_id))["confirmed_wallet_balance"] + await time_out_assert(5, wallet_is_synced, True, wallet_node, full_node_api) # Checks that the memo can be retrieved tx_confirmed = await client.get_transaction("1", transaction_id) assert tx_confirmed.confirmed diff --git a/tests/wallet/sync/test_wallet_sync.py b/tests/wallet/sync/test_wallet_sync.py index 48e6d472c246..bcc45db27530 100644 --- a/tests/wallet/sync/test_wallet_sync.py +++ b/tests/wallet/sync/test_wallet_sync.py @@ -11,6 +11,7 @@ from chia.util.ints import uint16, uint32 from chia.wallet.wallet_state_manager import WalletStateManager from tests.connection_utils import disconnect_all_and_reconnect +from tests.pools.test_pool_rpc import wallet_is_synced from tests.setup_nodes import bt, self_hostname, setup_node_and_wallet, setup_simulators_and_wallets, test_constants from tests.time_out_assert import time_out_assert @@ -261,7 +262,7 @@ async def get_tx_count(wallet_id): @pytest.mark.parametrize( "trusted", - [True, False], + [False], ) @pytest.mark.asyncio async def test_wallet_reorg_get_coinbase(self, wallet_node_simulator, default_400_blocks, trusted): @@ -296,6 +297,7 @@ async def get_tx_count(wallet_id): return len(txs) await time_out_assert(10, get_tx_count, 0, 1) + await time_out_assert(30, wallet_is_synced, True, wallet_node, full_node_api) num_blocks_reorg_1 = 40 blocks_reorg_1 = bt.get_consecutive_blocks( @@ -304,6 +306,7 @@ async def get_tx_count(wallet_id): blocks_reorg_2 = bt.get_consecutive_blocks(num_blocks_reorg_1, block_list_input=blocks_reorg_1) for block in blocks_reorg_2[-41:]: + await asyncio.sleep(0.4) await full_node_api.full_node.respond_block(full_node_protocol.RespondBlock(block)) await disconnect_all_and_reconnect(server_2, fn_server) @@ -313,5 +316,6 @@ async def get_tx_count(wallet_id): uint32(len(blocks_reorg_1)) ) - await time_out_assert(10, get_tx_count, 2, 1) - await time_out_assert(10, wallet.get_confirmed_balance, funds) + await time_out_assert(60, wallet_is_synced, True, wallet_node, full_node_api) + await time_out_assert(20, get_tx_count, 2, 1) + await time_out_assert(20, wallet.get_confirmed_balance, funds) From 366b6d090d9e701d85d428bf915cb4e5e3700c2d Mon Sep 17 00:00:00 2001 From: Chris Marslender Date: Mon, 14 Feb 2022 13:48:19 -0600 Subject: [PATCH 038/378] Update pyinstaller version (#10113) --- build_scripts/build_linux_deb.sh | 2 +- build_scripts/build_linux_rpm.sh | 2 +- build_scripts/build_macos.sh | 2 +- build_scripts/build_macos_m1.sh | 2 +- build_scripts/build_windows.ps1 | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/build_scripts/build_linux_deb.sh b/build_scripts/build_linux_deb.sh index f4840e08f6fc..b489d1e70f01 100644 --- a/build_scripts/build_linux_deb.sh +++ b/build_scripts/build_linux_deb.sh @@ -34,7 +34,7 @@ rm -rf dist mkdir dist echo "Create executables with pyinstaller" -pip install pyinstaller==4.5 +pip install pyinstaller==4.9 SPEC_FILE=$(python -c 'import chia; print(chia.PYINSTALLER_SPEC_PATH)') pyinstaller --log-level=INFO "$SPEC_FILE" LAST_EXIT_CODE=$? diff --git a/build_scripts/build_linux_rpm.sh b/build_scripts/build_linux_rpm.sh index a0504f30a52e..02641e2eb325 100644 --- a/build_scripts/build_linux_rpm.sh +++ b/build_scripts/build_linux_rpm.sh @@ -36,7 +36,7 @@ rm -rf dist mkdir dist echo "Create executables with pyinstaller" -pip install pyinstaller==4.5 +pip install pyinstaller==4.9 SPEC_FILE=$(python -c 'import chia; print(chia.PYINSTALLER_SPEC_PATH)') pyinstaller --log-level=INFO "$SPEC_FILE" LAST_EXIT_CODE=$? diff --git a/build_scripts/build_macos.sh b/build_scripts/build_macos.sh index e540469670f2..32617dda6ec6 100644 --- a/build_scripts/build_macos.sh +++ b/build_scripts/build_macos.sh @@ -25,7 +25,7 @@ sudo rm -rf dist mkdir dist echo "Create executables with pyinstaller" -pip install pyinstaller==4.5 +pip install pyinstaller==4.9 SPEC_FILE=$(python -c 'import chia; print(chia.PYINSTALLER_SPEC_PATH)') pyinstaller --log-level=INFO "$SPEC_FILE" LAST_EXIT_CODE=$? diff --git a/build_scripts/build_macos_m1.sh b/build_scripts/build_macos_m1.sh index e8871bc19617..a48ff267eba8 100644 --- a/build_scripts/build_macos_m1.sh +++ b/build_scripts/build_macos_m1.sh @@ -25,7 +25,7 @@ sudo rm -rf dist mkdir dist echo "Install pyinstaller and build bootloaders for M1" -pip install pyinstaller==4.5 +pip install pyinstaller==4.9 echo "Create executables with pyinstaller" SPEC_FILE=$(python -c 'import chia; print(chia.PYINSTALLER_SPEC_PATH)') diff --git a/build_scripts/build_windows.ps1 b/build_scripts/build_windows.ps1 index 1dd33157da33..58af0d585d2c 100644 --- a/build_scripts/build_windows.ps1 +++ b/build_scripts/build_windows.ps1 @@ -30,7 +30,7 @@ python -m venv venv python -m pip install --upgrade pip pip install wheel pep517 pip install pywin32 -pip install pyinstaller==4.5 +pip install pyinstaller==4.9 pip install setuptools_scm Write-Output " ---" From eb3ed142e040ed5b238d06f07ef86c635b78e74e Mon Sep 17 00:00:00 2001 From: Chris Marslender Date: Mon, 14 Feb 2022 13:48:35 -0600 Subject: [PATCH 039/378] Crawler RPC (#10141) * Add crawler RPC server * Generate private keypair for crawler * Bring over cleanup changes from the last closed PR * Update the crawler RPC information to be its own subsection within seeder * Add sleep before crawling to let the daemon connection get set up * Wait for the actual callback to not be None, instead of just a random sleep interval * Rework crawler/dns seeder to use the daemon + normal chia start process rather than the old system intended for the standalone repo * Update configure testnet to work with seeder config * Add back the crawler/seeder options from the standalone version * Remove the check for none/sleep. Not needed when this is started by the daemon * Add real data to the get_peer_counts endpoint * Lint * Fix calls to configure from init * Turns out we still might sometimes move too quick before daemon/state changed callback is ready * Add peer counts in the state_changed callback method * Add a setting for peer_connect_timeout in the seeder: section so we can control it just for crawler * start_seeder * Pass config/root_path to the DNSServer so it can also use the configured crawler DB Path * change in () instead of if/or * Remove unnecessary return --- chia/cmds/configure.py | 67 +++++++++- chia/cmds/init_funcs.py | 38 +++++- chia/cmds/seeder.py | 204 ----------------------------- chia/daemon/server.py | 5 +- chia/rpc/crawler_rpc_api.py | 51 ++++++++ chia/seeder/crawler.py | 61 ++++++--- chia/seeder/crawler_api.py | 2 +- chia/seeder/dns_server.py | 17 +-- chia/seeder/start_crawler.py | 4 + chia/seeder/util/__init__.py | 0 chia/seeder/util/config.py | 34 ----- chia/seeder/util/service.py | 103 --------------- chia/seeder/util/service_groups.py | 17 --- chia/server/server.py | 4 +- chia/util/initial-config.yaml | 13 +- chia/util/service_groups.py | 3 + setup.py | 6 +- 17 files changed, 224 insertions(+), 405 deletions(-) delete mode 100644 chia/cmds/seeder.py create mode 100644 chia/rpc/crawler_rpc_api.py delete mode 100644 chia/seeder/util/__init__.py delete mode 100644 chia/seeder/util/config.py delete mode 100644 chia/seeder/util/service.py delete mode 100644 chia/seeder/util/service_groups.py diff --git a/chia/cmds/configure.py b/chia/cmds/configure.py index 4c2848195698..e54da32234cc 100644 --- a/chia/cmds/configure.py +++ b/chia/cmds/configure.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import Dict +from typing import Dict, Optional import click @@ -19,6 +19,10 @@ def configure( set_peer_count: str, testnet: str, peer_connect_timeout: str, + crawler_db_path: str, + crawler_minimum_version_count: Optional[int], + seeder_domain_name: str, + seeder_nameserver: str, ): config: Dict = load_config(DEFAULT_ROOT_PATH, "config.yaml") change_made = False @@ -95,6 +99,7 @@ def configure( testnet_port = "58444" testnet_introducer = "introducer-testnet10.chia.net" testnet_dns_introducer = "dns-introducer-testnet10.chia.net" + bootstrap_peers = ["testnet10-node.chia.net"] testnet = "testnet10" config["full_node"]["port"] = int(testnet_port) config["full_node"]["introducer_peer"]["port"] = int(testnet_port) @@ -115,6 +120,13 @@ def configure( config["ui"]["selected_network"] = testnet config["introducer"]["selected_network"] = testnet config["wallet"]["selected_network"] = testnet + + if "seeder" in config: + config["seeder"]["port"] = int(testnet_port) + config["seeder"]["other_peers_port"] = int(testnet_port) + config["seeder"]["selected_network"] = testnet + config["seeder"]["bootstrap_peers"] = bootstrap_peers + print("Default full node port, introducer and network setting updated") change_made = True @@ -123,6 +135,7 @@ def configure( mainnet_port = "8444" mainnet_introducer = "introducer.chia.net" mainnet_dns_introducer = "dns-introducer.chia.net" + bootstrap_peers = ["node.chia.net"] net = "mainnet" config["full_node"]["port"] = int(mainnet_port) config["full_node"]["introducer_peer"]["port"] = int(mainnet_port) @@ -142,6 +155,13 @@ def configure( config["ui"]["selected_network"] = net config["introducer"]["selected_network"] = net config["wallet"]["selected_network"] = net + + if "seeder" in config: + config["seeder"]["port"] = int(mainnet_port) + config["seeder"]["other_peers_port"] = int(mainnet_port) + config["seeder"]["selected_network"] = net + config["seeder"]["bootstrap_peers"] = bootstrap_peers + print("Default full node port, introducer and network setting updated") change_made = True else: @@ -151,10 +171,25 @@ def configure( config["full_node"]["peer_connect_timeout"] = int(peer_connect_timeout) change_made = True + if crawler_db_path is not None and "seeder" in config: + config["seeder"]["crawler_db_path"] = crawler_db_path + change_made = True + + if crawler_minimum_version_count is not None and "seeder" in config: + config["seeder"]["minimum_version_count"] = crawler_minimum_version_count + change_made = True + + if seeder_domain_name is not None and "seeder" in config: + config["seeder"]["domain_name"] = seeder_domain_name + change_made = True + + if seeder_nameserver is not None and "seeder" in config: + config["seeder"]["nameserver"] = seeder_nameserver + change_made = True + if change_made: print("Restart any running chia services for changes to take effect") save_config(root_path, "config.yaml", config) - return 0 @click.command("configure", short_help="Modify configuration") @@ -197,6 +232,26 @@ def configure( ) @click.option("--set-peer-count", help="Update the target peer count (default 80)", type=str) @click.option("--set-peer-connect-timeout", help="Update the peer connect timeout (default 30)", type=str) +@click.option( + "--crawler-db-path", + help="configures the path to the crawler database", + type=str, +) +@click.option( + "--crawler-minimum-version-count", + help="configures how many of a particular version must be seen to be reported in logs", + type=int, +) +@click.option( + "--seeder-domain-name", + help="configures the seeder domain_name setting. Ex: `seeder.example.com.`", + type=str, +) +@click.option( + "--seeder-nameserver", + help="configures the seeder nameserver setting. Ex: `example.com.`", + type=str, +) @click.pass_context def configure_cmd( ctx, @@ -210,6 +265,10 @@ def configure_cmd( set_peer_count, testnet, set_peer_connect_timeout, + crawler_db_path, + crawler_minimum_version_count, + seeder_domain_name, + seeder_nameserver, ): configure( ctx.obj["root_path"], @@ -223,4 +282,8 @@ def configure_cmd( set_peer_count, testnet, set_peer_connect_timeout, + crawler_db_path, + crawler_minimum_version_count, + seeder_domain_name, + seeder_nameserver, ) diff --git a/chia/cmds/init_funcs.py b/chia/cmds/init_funcs.py index 0b3c05859292..3fa6702432be 100644 --- a/chia/cmds/init_funcs.py +++ b/chia/cmds/init_funcs.py @@ -41,7 +41,7 @@ ) from chia.cmds.configure import configure -private_node_names = {"full_node", "wallet", "farmer", "harvester", "timelord", "daemon"} +private_node_names = {"full_node", "wallet", "farmer", "harvester", "timelord", "crawler", "daemon"} public_node_names = {"full_node", "wallet", "farmer", "introducer", "timelord"} @@ -407,7 +407,23 @@ def chia_init( # This is reached if CHIA_ROOT is set, or if user has run chia init twice # before a new update. if testnet: - configure(root_path, "", "", "", "", "", "", "", "", testnet="true", peer_connect_timeout="") + configure( + root_path, + set_farmer_peer="", + set_node_introducer="", + set_fullnode_port="", + set_harvester_port="", + set_log_level="", + enable_upnp="", + set_outbound_peer_count="", + set_peer_count="", + testnet="true", + peer_connect_timeout="", + crawler_db_path="", + crawler_minimum_version_count=None, + seeder_domain_name="", + seeder_nameserver="", + ) if fix_ssl_permissions: fix_ssl(root_path) if should_check_keys: @@ -417,7 +433,23 @@ def chia_init( create_default_chia_config(root_path) if testnet: - configure(root_path, "", "", "", "", "", "", "", "", testnet="true", peer_connect_timeout="") + configure( + root_path, + set_farmer_peer="", + set_node_introducer="", + set_fullnode_port="", + set_harvester_port="", + set_log_level="", + enable_upnp="", + set_outbound_peer_count="", + set_peer_count="", + testnet="true", + peer_connect_timeout="", + crawler_db_path="", + crawler_minimum_version_count=None, + seeder_domain_name="", + seeder_nameserver="", + ) create_all_ssl(root_path) if fix_ssl_permissions: fix_ssl(root_path) diff --git a/chia/cmds/seeder.py b/chia/cmds/seeder.py deleted file mode 100644 index 271992d4e44c..000000000000 --- a/chia/cmds/seeder.py +++ /dev/null @@ -1,204 +0,0 @@ -import os -from pathlib import Path -from typing import Dict - -import click - -import chia.cmds.configure as chia_configure -from chia import __version__ -from chia.cmds.chia import monkey_patch_click -from chia.cmds.init_funcs import init -from chia.seeder.util.config import patch_default_seeder_config -from chia.seeder.util.service_groups import all_groups, services_for_groups -from chia.seeder.util.service import launch_service, kill_service -from chia.util.config import load_config, save_config -from chia.util.default_root import DEFAULT_ROOT_PATH - -CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) - - -@click.group( - help=f"\n Manage the Chia Seeder ({__version__})\n", - epilog="Try 'chia seeder start crawler' or 'chia seeder start server'", - context_settings=CONTEXT_SETTINGS, -) -@click.option("--root-path", default=DEFAULT_ROOT_PATH, help="Config file root", type=click.Path(), show_default=True) -@click.pass_context -def cli( - ctx: click.Context, - root_path: str, -) -> None: - from pathlib import Path - - ctx.ensure_object(dict) - ctx.obj["root_path"] = Path(root_path) - - -@cli.command("version", short_help="Show the Chia Seeder version") -def version_cmd() -> None: - print(__version__) - - -@click.command("init", short_help="Create or migrate the configuration") -@click.pass_context -def init_cmd(ctx: click.Context, **kwargs): - print("Calling Chia Seeder Init...") - init(None, ctx.obj["root_path"], True) - if os.environ.get("CHIA_ROOT", None) is not None: - print(f"warning, your CHIA_ROOT is set to {os.environ['CHIA_ROOT']}.") - root_path = ctx.obj["root_path"] - print(f"Chia directory {root_path}") - if root_path.is_dir() and not Path(root_path / "config" / "config.yaml").exists(): - # This is reached if CHIA_ROOT is set, but there is no config - # This really shouldn't happen, but if we dont have the base chia config, we can't continue - print("Config does not exist. Can't continue!") - return -1 - patch_default_seeder_config(root_path) - return 0 - - -@click.command("start", short_help="Start service groups") -@click.argument("group", type=click.Choice(all_groups()), nargs=-1, required=True) -@click.pass_context -def start_cmd(ctx: click.Context, group: str) -> None: - services = services_for_groups(group) - - for service in services: - print(f"Starting {service}") - launch_service(ctx.obj["root_path"], service) - - -@click.command("stop", short_help="Stop service groups") -@click.argument("group", type=click.Choice(all_groups()), nargs=-1, required=True) -@click.pass_context -def stop_cmd(ctx: click.Context, group: str) -> None: - services = services_for_groups(group) - - for service in services: - print(f"Stopping {service}") - kill_service(ctx.obj["root_path"], service) - - -def configure( - root_path: Path, - testnet: str, - crawler_db_path: str, - minimum_version_count: int, - domain_name: str, - nameserver: str, -): - # Run the parent config, in case anything there (testnet) needs to be run, THEN load the config for local changes - chia_configure.configure(root_path, "", "", "", "", "", "", "", "", testnet, "") - - config: Dict = load_config(DEFAULT_ROOT_PATH, "config.yaml") - change_made = False - if testnet is not None: - if testnet == "true" or testnet == "t": - print("Updating Chia Seeder to testnet settings") - port = 58444 - network = "testnet10" - bootstrap = ["testnet-node.chia.net"] - - config["seeder"]["port"] = port - config["seeder"]["other_peers_port"] = port - config["seeder"]["selected_network"] = network - config["seeder"]["bootstrap_peers"] = bootstrap - - change_made = True - - elif testnet == "false" or testnet == "f": - print("Updating Chia Seeder to mainnet settings") - port = 8444 - network = "mainnet" - bootstrap = ["node.chia.net"] - - config["seeder"]["port"] = port - config["seeder"]["other_peers_port"] = port - config["seeder"]["selected_network"] = network - config["seeder"]["bootstrap_peers"] = bootstrap - - change_made = True - else: - print("Please choose True or False") - - if crawler_db_path is not None: - config["seeder"]["crawler_db_path"] = crawler_db_path - change_made = True - - if minimum_version_count is not None: - config["seeder"]["minimum_version_count"] = minimum_version_count - change_made = True - - if domain_name is not None: - config["seeder"]["domain_name"] = domain_name - change_made = True - - if nameserver is not None: - config["seeder"]["nameserver"] = nameserver - change_made = True - - if change_made: - print("Restart any running Chia Seeder services for changes to take effect") - save_config(root_path, "config.yaml", config) - return 0 - - -@click.command("configure", short_help="Modify configuration") -@click.option( - "--testnet", - "-t", - help="configures for connection to testnet", - type=click.Choice(["true", "t", "false", "f"]), -) -@click.option( - "--crawler-db-path", - help="configures for path to the crawler database", - type=str, -) -@click.option( - "--minimum-version-count", - help="configures how many of a particular version must be seen to be reported in logs", - type=int, -) -@click.option( - "--domain-name", - help="configures the domain_name setting. Ex: `seeder.example.com.`", - type=str, -) -@click.option( - "--nameserver", - help="configures the nameserver setting. Ex: `example.com.`", - type=str, -) -@click.pass_context -def configure_cmd( - ctx, - testnet, - crawler_db_path, - minimum_version_count, - domain_name, - nameserver, -): - configure( - ctx.obj["root_path"], - testnet, - crawler_db_path, - minimum_version_count, - domain_name, - nameserver, - ) - - -cli.add_command(init_cmd) -cli.add_command(start_cmd) -cli.add_command(stop_cmd) -cli.add_command(configure_cmd) - - -def main() -> None: - monkey_patch_click() - cli() # pylint: disable=no-value-for-parameter - - -if __name__ == "__main__": - main() diff --git a/chia/daemon/server.py b/chia/daemon/server.py index bf3119c59d5d..ca03ef66116e 100644 --- a/chia/daemon/server.py +++ b/chia/daemon/server.py @@ -100,9 +100,8 @@ class PlotEvent(str, Enum): "chia_timelord": "start_timelord", "chia_timelord_launcher": "timelord_launcher", "chia_full_node_simulator": "start_simulator", - "chia_seeder": "chia_seeder", - "chia_seeder_crawler": "chia_seeder_crawler", - "chia_seeder_dns": "chia_seeder_dns", + "chia_seeder": "start_seeder", + "chia_crawler": "start_crawler", } def executable_for_service(service_name: str) -> str: diff --git a/chia/rpc/crawler_rpc_api.py b/chia/rpc/crawler_rpc_api.py new file mode 100644 index 000000000000..d063cb9a402d --- /dev/null +++ b/chia/rpc/crawler_rpc_api.py @@ -0,0 +1,51 @@ +import ipaddress +from typing import Any, Callable, Dict, List, Optional + +from chia.seeder.crawler import Crawler +from chia.util.ws_message import WsRpcMessage, create_payload_dict + + +class CrawlerRpcApi: + def __init__(self, crawler: Crawler): + self.service = crawler + self.service_name = "chia_crawler" + + def get_routes(self) -> Dict[str, Callable]: + return { + "/get_peer_counts": self.get_peer_counts, + } + + async def _state_changed(self, change: str, change_data: Optional[Dict[str, Any]] = None) -> List[WsRpcMessage]: + payloads = [] + + if change_data is None: + change_data = await self.get_peer_counts({}) + + if change in ("crawl_batch_completed", "loaded_initial_peers"): + payloads.append(create_payload_dict(change, change_data, self.service_name, "metrics")) + + return payloads + + async def get_peer_counts(self, _request: Dict) -> Dict[str, Any]: + ipv6_addresses_count = 0 + for host in self.service.best_timestamp_per_peer.keys(): + try: + ipaddress.IPv6Address(host) + ipv6_addresses_count += 1 + except ipaddress.AddressValueError: + continue + + reliable_peers = 0 + if self.service.crawl_store is not None: + reliable_peers = self.service.crawl_store.get_reliable_peers() + + data = { + "peer_counts": { + "total_last_5_days": len(self.service.best_timestamp_per_peer), + "reliable_nodes": reliable_peers, + "ipv4_last_5_days": len(self.service.best_timestamp_per_peer) - ipv6_addresses_count, + "ipv6_last_5_days": ipv6_addresses_count, + "versions": self.service.versions, + } + } + return data diff --git a/chia/seeder/crawler.py b/chia/seeder/crawler.py index 4ca8a86b4889..c8b2f93f2b6d 100644 --- a/chia/seeder/crawler.py +++ b/chia/seeder/crawler.py @@ -3,6 +3,7 @@ import time import traceback import ipaddress +from collections import defaultdict from pathlib import Path from typing import Any, Callable, Dict, List, Optional, Tuple @@ -27,13 +28,15 @@ class Crawler: coin_store: CoinStore connection: aiosqlite.Connection config: Dict - server: Any + server: Optional[ChiaServer] + crawl_store: Optional[CrawlStore] log: logging.Logger constants: ConsensusConstants _shut_down: bool root_path: Path peer_count: int with_peak: set + minimum_version_count: int def __init__( self, @@ -58,16 +61,19 @@ def __init__( self.version_cache: List[Tuple[str, str]] = [] self.handshake_time: Dict[str, int] = {} self.best_timestamp_per_peer: Dict[str, int] = {} - if "crawler_db_path" in config and config["crawler_db_path"] != "": - path = Path(config["crawler_db_path"]) - self.db_path = path.resolve() - else: - db_path_replaced: str = "crawler.db" - self.db_path = path_from_root(root_path, db_path_replaced) + crawler_db_path: str = config.get("crawler_db_path", "crawler.db") + self.db_path = path_from_root(root_path, crawler_db_path) mkdir(self.db_path.parent) self.bootstrap_peers = config["bootstrap_peers"] self.minimum_height = config["minimum_height"] self.other_peers_port = config["other_peers_port"] + self.versions: Dict[str, int] = defaultdict(lambda: 0) + self.minimum_version_count = self.config.get("minimum_version_count", 100) + if self.minimum_version_count < 1: + self.log.warning( + f"Crawler configuration minimum_version_count expected to be greater than zero: " + f"{self.minimum_version_count!r}" + ) def _set_state_changed_callback(self, callback: Callable): self.state_changed_callback = callback @@ -111,9 +117,20 @@ async def peer_action(peer: ws.WSChiaConnection): await self.crawl_store.peer_failed_to_connect(peer) async def _start(self): + # We override the default peer_connect_timeout when running from the crawler + crawler_peer_timeout = self.config.get("peer_connect_timeout", 2) + self.server.config["peer_connect_timeout"] = crawler_peer_timeout + self.task = asyncio.create_task(self.crawl()) async def crawl(self): + # Ensure the state_changed callback is set up before moving on + # Sometimes, the daemon connection + state changed callback isn't up and ready + # by the time we get to the first _state_changed call, so this just ensures it's there before moving on + while self.state_changed_callback is None: + self.log.info("Waiting for state changed callback...") + await asyncio.sleep(0.1) + try: self.connection = await aiosqlite.connect(self.db_path) self.crawl_store = await CrawlStore.create(self.connection) @@ -142,6 +159,12 @@ async def crawl(self): self.host_to_version, self.handshake_time = self.crawl_store.load_host_to_version() self.best_timestamp_per_peer = self.crawl_store.load_best_peer_reliability() + self.versions = defaultdict(lambda: 0) + for host, version in self.host_to_version.items(): + self.versions[version] += 1 + + self._state_changed("loaded_initial_peers") + while True: self.with_peak = set() peers_to_crawl = await self.crawl_store.get_peers_to_crawl(25000, 250000) @@ -217,11 +240,9 @@ async def crawl(self): for host, timestamp in self.best_timestamp_per_peer.items() if timestamp >= now - 5 * 24 * 3600 } - versions = {} + self.versions = defaultdict(lambda: 0) for host, version in self.host_to_version.items(): - if version not in versions: - versions[version] = 0 - versions[version] += 1 + self.versions[version] += 1 self.version_cache = [] self.peers_retrieved = [] @@ -263,9 +284,9 @@ async def crawl(self): ipv6_addresses_count = 0 for host in self.best_timestamp_per_peer.keys(): try: - _ = ipaddress.IPv6Address(host) + ipaddress.IPv6Address(host) ipv6_addresses_count += 1 - except ValueError: + except ipaddress.AddressValueError: continue self.log.error( "IPv4 addresses gossiped with timestamp in the last 5 days with respond_peers messages: " @@ -278,21 +299,17 @@ async def crawl(self): ipv6_available_peers = 0 for host in self.host_to_version.keys(): try: - _ = ipaddress.IPv6Address(host) + ipaddress.IPv6Address(host) ipv6_available_peers += 1 - except ValueError: + except ipaddress.AddressValueError: continue self.log.error( f"Total IPv4 nodes reachable in the last 5 days: {available_peers - ipv6_available_peers}." ) self.log.error(f"Total IPv6 nodes reachable in the last 5 days: {ipv6_available_peers}.") self.log.error("Version distribution among reachable in the last 5 days (at least 100 nodes):") - if "minimum_version_count" in self.config and self.config["minimum_version_count"] > 0: - minimum_version_count = self.config["minimum_version_count"] - else: - minimum_version_count = 100 - for version, count in sorted(versions.items(), key=lambda kv: kv[1], reverse=True): - if count >= minimum_version_count: + for version, count in sorted(self.versions.items(), key=lambda kv: kv[1], reverse=True): + if count >= self.minimum_version_count: self.log.error(f"Version: {version} - Count: {count}") self.log.error(f"Banned addresses in the DB: {banned_peers}") self.log.error(f"Temporary ignored addresses in the DB: {ignored_peers}") @@ -301,6 +318,8 @@ async def crawl(self): f"{total_records - banned_peers - ignored_peers}" ) self.log.error("***") + + self._state_changed("crawl_batch_completed") except Exception as e: self.log.error(f"Exception: {e}. Traceback: {traceback.format_exc()}.") diff --git a/chia/seeder/crawler_api.py b/chia/seeder/crawler_api.py index 7d9d1baf7e23..304be592d531 100644 --- a/chia/seeder/crawler_api.py +++ b/chia/seeder/crawler_api.py @@ -14,7 +14,7 @@ def __init__(self, crawler): self.crawler = crawler def _set_state_changed_callback(self, callback: Callable): - pass + self.crawler.state_changed_callback = callback def __getattr__(self, attr_name: str): async def invoke(*args, **kwargs): diff --git a/chia/seeder/dns_server.py b/chia/seeder/dns_server.py index 02c101c5e4cf..24dc5b7b10b1 100644 --- a/chia/seeder/dns_server.py +++ b/chia/seeder/dns_server.py @@ -4,7 +4,8 @@ import random import signal import traceback -from typing import Any, List +from pathlib import Path +from typing import Any, Dict, List import aiosqlite from dnslib import A, AAAA, SOA, NS, MX, CNAME, RR, DNSRecord, QTYPE, DNSHeader @@ -70,15 +71,15 @@ class DNSServer: pointer: int crawl_db: aiosqlite.Connection - def __init__(self): + def __init__(self, config: Dict, root_path: Path): self.reliable_peers_v4 = [] self.reliable_peers_v6 = [] self.lock = asyncio.Lock() self.pointer_v4 = 0 self.pointer_v6 = 0 - db_path_replaced: str = "crawler.db" - root_path = DEFAULT_ROOT_PATH - self.db_path = path_from_root(root_path, db_path_replaced) + + crawler_db_path: str = config.get("crawler_db_path", "crawler.db") + self.db_path = path_from_root(root_path, crawler_db_path) mkdir(self.db_path.parent) async def start(self): @@ -227,8 +228,8 @@ async def dns_response(self, data): log.error(f"Exception: {e}. Traceback: {traceback.format_exc()}.") -async def serve_dns(): - dns_server = DNSServer() +async def serve_dns(config: Dict, root_path: Path): + dns_server = DNSServer(config, root_path) await dns_server.start() # TODO: Make this cleaner? @@ -278,7 +279,7 @@ def signal_received(): log.info("signal handlers unsupported") try: - loop.run_until_complete(serve_dns()) + loop.run_until_complete(serve_dns(config, root_path)) finally: loop.close() diff --git a/chia/seeder/start_crawler.py b/chia/seeder/start_crawler.py index e0edf194f387..6c2abe714018 100644 --- a/chia/seeder/start_crawler.py +++ b/chia/seeder/start_crawler.py @@ -5,6 +5,7 @@ from chia.consensus.constants import ConsensusConstants from chia.consensus.default_constants import DEFAULT_CONSTANTS +from chia.rpc.crawler_rpc_api import CrawlerRpcApi from chia.seeder.crawler import Crawler from chia.seeder.crawler_api import CrawlerAPI from chia.server.outbound_message import NodeType @@ -43,6 +44,9 @@ def service_kwargs_for_full_node_crawler( network_id=network_id, ) + if config.get("crawler", {}).get("start_rpc_server", True): + kwargs["rpc_info"] = (CrawlerRpcApi, config.get("crawler", {}).get("crawler_rpc_port", 8561)) + return kwargs diff --git a/chia/seeder/util/__init__.py b/chia/seeder/util/__init__.py deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/chia/seeder/util/config.py b/chia/seeder/util/config.py deleted file mode 100644 index 187ff1d82a4c..000000000000 --- a/chia/seeder/util/config.py +++ /dev/null @@ -1,34 +0,0 @@ -from pathlib import Path - -import pkg_resources - -from chia.util.config import load_config, save_config - - -def patch_default_seeder_config(root_path: Path, filename="config.yaml") -> None: - """ - Checks if the seeder: section exists in the config. If not, the default seeder settings are appended to the file - """ - - existing_config = load_config(root_path, "config.yaml") - - if "seeder" in existing_config: - print("Chia Seeder section exists in config. No action required.") - return - - print("Chia Seeder section does not exist in config. Patching...") - config = load_config(root_path, "config.yaml") - # The following ignores root_path when the second param is absolute, which this will be - seeder_config = load_config(root_path, pkg_resources.resource_filename("chia.util", "initial-config.yaml")) - - # Patch in the values with anchors, since pyyaml tends to change - # the anchors to things like id001, etc - config["seeder"] = seeder_config["seeder"] - config["seeder"]["network_overrides"] = config["network_overrides"] - config["seeder"]["selected_network"] = config["selected_network"] - config["seeder"]["logging"] = config["logging"] - - # When running as crawler, we default to a much lower client timeout - config["full_node"]["peer_connect_timeout"] = 2 - - save_config(root_path, "config.yaml", config) diff --git a/chia/seeder/util/service.py b/chia/seeder/util/service.py deleted file mode 100644 index bb5ad35668f5..000000000000 --- a/chia/seeder/util/service.py +++ /dev/null @@ -1,103 +0,0 @@ -import os -import signal -import subprocess -import sys - -from pathlib import Path -from typing import Tuple - -from chia.daemon.server import pid_path_for_service -from chia.util.path import mkdir - - -def launch_service(root_path: Path, service_command) -> Tuple[subprocess.Popen, Path]: - """ - Launch a child process. - """ - # set up CHIA_ROOT - # invoke correct script - # save away PID - - # we need to pass on the possibly altered CHIA_ROOT - os.environ["CHIA_ROOT"] = str(root_path) - - print(f"Launching service with CHIA_ROOT: {os.environ['CHIA_ROOT']}") - - # Insert proper e - service_array = service_command.split() - service_executable = executable_for_service(service_array[0]) - service_array[0] = service_executable - - startupinfo = None - if os.name == "nt": - startupinfo = subprocess.STARTUPINFO() # type: ignore - startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW # type: ignore - - # CREATE_NEW_PROCESS_GROUP allows graceful shutdown on windows, by CTRL_BREAK_EVENT signal - if sys.platform == "win32" or sys.platform == "cygwin": - creationflags = subprocess.CREATE_NEW_PROCESS_GROUP - else: - creationflags = 0 - environ_copy = os.environ.copy() - process = subprocess.Popen( - service_array, shell=False, startupinfo=startupinfo, creationflags=creationflags, env=environ_copy - ) - pid_path = pid_path_for_service(root_path, service_command) - try: - mkdir(pid_path.parent) - with open(pid_path, "w") as f: - f.write(f"{process.pid}\n") - except Exception: - pass - return process, pid_path - - -def kill_service(root_path: Path, service_name: str) -> bool: - pid_path = pid_path_for_service(root_path, service_name) - - try: - with open(pid_path) as f: - pid = int(f.read()) - - # @TODO SIGKILL seems necessary right now for the DNS server, but not the crawler (fix that) - # @TODO Ensure processes stop before renaming the files and returning - os.kill(pid, signal.SIGKILL) - print("sent SIGKILL to process") - except Exception: - pass - - try: - pid_path_killed = pid_path.with_suffix(".pid-killed") - if pid_path_killed.exists(): - pid_path_killed.unlink() - os.rename(pid_path, pid_path_killed) - except Exception: - pass - - return True - - -# determine if application is a script file or frozen exe -if getattr(sys, "frozen", False): - name_map = { - "chia_seeder": "chia_seeder", - "chia_seeder_crawler": "chia_seeder_crawler", - "chia_seeder_server": "chia_seeder_server", - } - - def executable_for_service(service_name: str) -> str: - application_path = os.path.dirname(sys.executable) - if sys.platform == "win32" or sys.platform == "cygwin": - executable = name_map[service_name] - path = f"{application_path}/{executable}.exe" - return path - else: - path = f"{application_path}/{name_map[service_name]}" - return path - - -else: - application_path = os.path.dirname(__file__) - - def executable_for_service(service_name: str) -> str: - return service_name diff --git a/chia/seeder/util/service_groups.py b/chia/seeder/util/service_groups.py deleted file mode 100644 index c42ee4b05e50..000000000000 --- a/chia/seeder/util/service_groups.py +++ /dev/null @@ -1,17 +0,0 @@ -from typing import KeysView, Generator - -SERVICES_FOR_GROUP = { - "all": "chia_seeder_crawler chia_seeder_server".split(), - "crawler": "chia_seeder_crawler".split(), - "server": "chia_seeder_server".split(), -} - - -def all_groups() -> KeysView[str]: - return SERVICES_FOR_GROUP.keys() - - -def services_for_groups(groups) -> Generator[str, None, None]: - for group in groups: - for service in SERVICES_FOR_GROUP[group]: - yield service diff --git a/chia/server/server.py b/chia/server/server.py index b60b24be5454..2af0e1c63e63 100644 --- a/chia/server/server.py +++ b/chia/server/server.py @@ -406,9 +406,7 @@ async def start_client( connection: Optional[WSChiaConnection] = None try: # Crawler/DNS introducer usually uses a lower timeout than the default - timeout_value = ( - 30 if "peer_connect_timeout" not in self.config else float(self.config["peer_connect_timeout"]) - ) + timeout_value = float(self.config.get("peer_connect_timeout", 30)) timeout = ClientTimeout(total=timeout_value) session = ClientSession(timeout=timeout) diff --git a/chia/util/initial-config.yaml b/chia/util/initial-config.yaml index bae4c7fd8e1f..635ff02adab5 100644 --- a/chia/util/initial-config.yaml +++ b/chia/util/initial-config.yaml @@ -136,8 +136,10 @@ seeder: port: 8444 # Most full nodes on the network run on this port. (i.e. 8444 for mainnet, 58444 for testnet). other_peers_port: 8444 - # Path to crawler DB. If empty, will use $CHIA_ROOT/crawler.db - crawler_db_path: "" + # This will override the default full_node.peer_connect_timeout for the crawler full node + peer_connect_timeout: 2 + # Path to crawler DB. Defaults to $CHIA_ROOT/crawler.db + crawler_db_path: "crawler.db" # Peers used for the initial run. bootstrap_peers: - "node.chia.net" @@ -158,6 +160,13 @@ seeder: network_overrides: *network_overrides selected_network: *selected_network logging: *logging + # Crawler is its own standalone service within the seeder component + crawler: + start_rpc_server: True + rpc_port: 8561 + ssl: + private_crt: "config/ssl/crawler/private_crawler.crt" + private_key: "config/ssl/crawler/private_crawler.key" harvester: # The harvester server (if run) will run on this port diff --git a/chia/util/service_groups.py b/chia/util/service_groups.py index 1575fbfe361d..7ed895be68f9 100644 --- a/chia/util/service_groups.py +++ b/chia/util/service_groups.py @@ -13,6 +13,9 @@ "wallet": "chia_wallet".split(), "introducer": "chia_introducer".split(), "simulator": "chia_full_node_simulator".split(), + "crawler": "chia_crawler".split(), + "seeder": "chia_crawler chia_seeder".split(), + "seeder-only": "chia_seeder".split(), } diff --git a/setup.py b/setup.py index 93cfcf9daf03..c7d885a8c3d3 100644 --- a/setup.py +++ b/setup.py @@ -94,7 +94,6 @@ "chia.protocols", "chia.rpc", "chia.seeder", - "chia.seeder.util", "chia.server", "chia.simulator", "chia.types.blockchain_format", @@ -119,9 +118,8 @@ "chia_harvester = chia.server.start_harvester:main", "chia_farmer = chia.server.start_farmer:main", "chia_introducer = chia.server.start_introducer:main", - "chia_seeder = chia.cmds.seeder:main", - "chia_seeder_crawler = chia.seeder.start_crawler:main", - "chia_seeder_server = chia.seeder.dns_server:main", + "chia_crawler = chia.seeder.start_crawler:main", + "chia_seeder = chia.seeder.dns_server:main", "chia_timelord = chia.server.start_timelord:main", "chia_timelord_launcher = chia.timelord.timelord_launcher:main", "chia_full_node_simulator = chia.simulator.start_simulator:main", From 229eea9b551cbe02e838245c40979ebde806ed4f Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Mon, 14 Feb 2022 21:51:21 +0100 Subject: [PATCH 040/378] Log slow block height (#10160) * improve logging of blocks that are slow * add profiler to test_full_sync * Update tools/test_full_sync.py Co-authored-by: Kyle Altendorf Co-authored-by: Kyle Altendorf --- chia/full_node/coin_store.py | 8 +-- chia/full_node/full_node.py | 17 ++++--- tests/tools/test_full_sync.py | 2 +- tools/test_full_sync.py | 91 ++++++++++++++++++++++++++++++----- 4 files changed, 95 insertions(+), 23 deletions(-) diff --git a/chia/full_node/coin_store.py b/chia/full_node/coin_store.py index 6b4e25a4f70c..b46a951739e3 100644 --- a/chia/full_node/coin_store.py +++ b/chia/full_node/coin_store.py @@ -7,7 +7,7 @@ from chia.util.db_wrapper import DBWrapper from chia.util.ints import uint32, uint64 from chia.util.lru_cache import LRUCache -from time import time +import time import logging log = logging.getLogger(__name__) @@ -114,7 +114,7 @@ async def new_block( Returns a list of the CoinRecords that were added by this block """ - start = time() + start = time.monotonic() additions = [] @@ -146,10 +146,10 @@ async def new_block( await self._add_coin_records(additions) await self._set_spent(tx_removals, height) - end = time() + end = time.monotonic() log.log( logging.WARNING if end - start > 10 else logging.DEBUG, - f"It took {end - start:0.2f}s to apply {len(tx_additions)} additions and " + f"Height {height}: It took {end - start:0.2f}s to apply {len(tx_additions)} additions and " + f"{len(tx_removals)} removals to the coin store. Make sure " + "blockchain database is on a fast drive", ) diff --git a/chia/full_node/full_node.py b/chia/full_node/full_node.py index d0ac3fe2a713..72e181611f1e 100644 --- a/chia/full_node/full_node.py +++ b/chia/full_node/full_node.py @@ -1047,15 +1047,18 @@ async def receive_block_batch( # Validates signatures in multiprocessing since they take a while, and we don't have cached transactions # for these blocks (unlike during normal operation where we validate one at a time) - pre_validate_start = time.time() + pre_validate_start = time.monotonic() pre_validation_results: List[PreValidationResult] = await self.blockchain.pre_validate_blocks_multiprocessing( blocks_to_validate, {}, wp_summaries=wp_summaries, validate_signatures=True ) - pre_validate_end = time.time() - if pre_validate_end - pre_validate_start > 10: - self.log.warning(f"Block pre-validation time: {pre_validate_end - pre_validate_start:0.2f} seconds") - else: - self.log.debug(f"Block pre-validation time: {pre_validate_end - pre_validate_start:0.2f} seconds") + pre_validate_end = time.monotonic() + pre_validate_time = pre_validate_end - pre_validate_start + + self.log.log( + logging.WARNING if pre_validate_time > 10 else logging.DEBUG, + f"Block pre-validation time: {pre_validate_end - pre_validate_start:0.2f} seconds " + f"({len(blocks_to_validate)} blocks, start height: {blocks_to_validate[0].height})", + ) for i, block in enumerate(blocks_to_validate): if pre_validation_results[i].error is not None: self.log.error( @@ -1553,7 +1556,7 @@ async def respond_block( f"Block validation time: {validation_time:0.2f} seconds, " f"pre_validation time: {pre_validation_time:0.2f} seconds, " f"cost: {block.transactions_info.cost if block.transactions_info is not None else 'None'}" - f"{percent_full_str}", + f"{percent_full_str} header_hash: {header_hash} height: {block.height}", ) # This code path is reached if added == ADDED_AS_ORPHAN or NEW_TIP diff --git a/tests/tools/test_full_sync.py b/tests/tools/test_full_sync.py index 185b501d827f..a2616d4c0e2f 100644 --- a/tests/tools/test_full_sync.py +++ b/tests/tools/test_full_sync.py @@ -9,4 +9,4 @@ def test_full_sync_test(): file_path = os.path.realpath(__file__) db_file = Path(file_path).parent / "test-blockchain-db.sqlite" - asyncio.run(run_sync_test(db_file, db_version=2)) + asyncio.run(run_sync_test(db_file, db_version=2, profile=False)) diff --git a/tools/test_full_sync.py b/tools/test_full_sync.py index df0e077038a5..9387171d4e17 100755 --- a/tools/test_full_sync.py +++ b/tools/test_full_sync.py @@ -4,9 +4,14 @@ import aiosqlite import zstd import click +import logging +import cProfile +from typing import Iterator + from pathlib import Path -from time import time +import time import tempfile +from contextlib import contextmanager from chia.types.full_block import FullBlock from chia.consensus.default_constants import DEFAULT_CONSTANTS @@ -16,7 +21,46 @@ from chia.cmds.init_funcs import chia_init -async def run_sync_test(file: Path, db_version=2) -> None: +class ExitOnError(logging.Handler): + def __init__(self): + super().__init__() + self.exit_with_failure = False + + def emit(self, record): + if record.levelno != logging.ERROR: + return + self.exit_with_failure = True + + +@contextmanager +def enable_profiler(profile: bool, counter: int) -> Iterator[None]: + if not profile: + yield + return + + with cProfile.Profile() as pr: + receive_start_time = time.monotonic() + yield + + if time.monotonic() - receive_start_time > 10: + pr.create_stats() + pr.dump_stats(f"slow-batch-{counter:05d}.profile") + + +async def run_sync_test(file: Path, db_version, profile: bool) -> None: + + logger = logging.getLogger() + logger.setLevel(logging.WARNING) + handler = logging.FileHandler("test-full-sync.log") + handler.setFormatter( + logging.Formatter( + "%(levelname)-8s %(message)s", + datefmt="%Y-%m-%dT%H:%M:%S", + ) + ) + logger.addHandler(handler) + check_log = ExitOnError() + logger.addHandler(check_log) with tempfile.TemporaryDirectory() as root_dir: @@ -43,7 +87,7 @@ async def run_sync_test(file: Path, db_version=2) -> None: block_batch = [] - start_time = time() + start_time = time.monotonic() async for r in rows: block = FullBlock.from_bytes(zstd.decompress(r[2])) @@ -51,26 +95,51 @@ async def run_sync_test(file: Path, db_version=2) -> None: if len(block_batch) < 32: continue - success, advanced_peak, fork_height, coin_changes = await full_node.receive_block_batch( - block_batch, None, None # type: ignore[arg-type] - ) + with enable_profiler(profile, counter): + success, advanced_peak, fork_height, coin_changes = await full_node.receive_block_batch( + block_batch, None, None # type: ignore[arg-type] + ) + assert success assert advanced_peak counter += len(block_batch) - print(f"\rheight {counter} {counter/(time() - start_time):0.2f} blocks/s ", end="") + print(f"\rheight {counter} {counter/(time.monotonic() - start_time):0.2f} blocks/s ", end="") block_batch = [] + if check_log.exit_with_failure: + raise RuntimeError("error printed to log. exiting") finally: print("closing full node") full_node._close() await full_node._await_closed() -@click.command() +@click.group() +def main() -> None: + pass + + +@main.command("run", short_help="run simulated full sync from an existing blockchain db") @click.argument("file", type=click.Path(), required=True) -@click.argument("db-version", type=int, required=False, default=2) -def main(file: Path, db_version) -> None: - asyncio.run(run_sync_test(Path(file), db_version)) +@click.option("--db-version", type=int, required=False, default=2, help="the version of the specified db file") +@click.option("--profile", is_flag=True, required=False, default=False, help="dump CPU profiles for slow batches") +def run(file: Path, db_version: int, profile: bool) -> None: + asyncio.run(run_sync_test(Path(file), db_version, profile)) + + +@main.command("analyze", short_help="generate call stacks for all profiles dumped to current directory") +def analyze() -> None: + from shlex import quote + from glob import glob + from subprocess import check_call + + for input_file in glob("slow-batch-*.profile"): + output = input_file.replace(".profile", ".png") + print(f"{input_file}") + check_call(f"gprof2dot -f pstats {quote(input_file)} | dot -T png >{quote(output)}", shell=True) + +main.add_command(run) +main.add_command(analyze) if __name__ == "__main__": # pylint: disable = no-value-for-parameter From 884ebd902b9c43e4c7b85697c0d687e47260fabe Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Mon, 14 Feb 2022 15:52:14 -0500 Subject: [PATCH 041/378] remove chia.util.clvm pass-through file (#10130) * remove chia.util.clvm pass-through file * in tests too --- chia/full_node/mempool_manager.py | 2 +- chia/types/blockchain_format/coin.py | 3 ++- chia/util/clvm.py | 1 - chia/util/condition_tools.py | 3 ++- tests/core/full_node/test_full_node.py | 2 +- tests/core/full_node/test_mempool.py | 2 +- tests/core/full_node/test_performance.py | 2 +- tests/generator/test_rom.py | 2 +- tests/wallet_tools.py | 2 +- 9 files changed, 10 insertions(+), 9 deletions(-) delete mode 100644 chia/util/clvm.py diff --git a/chia/full_node/mempool_manager.py b/chia/full_node/mempool_manager.py index 952aee16fd6a..463e1b65cce5 100644 --- a/chia/full_node/mempool_manager.py +++ b/chia/full_node/mempool_manager.py @@ -7,6 +7,7 @@ from typing import Dict, List, Optional, Set, Tuple from blspy import GTElement from chiabip158 import PyBIP158 +from clvm.casts import int_from_bytes from chia.util import cached_bls from chia.consensus.block_record import BlockRecord @@ -27,7 +28,6 @@ from chia.types.mempool_item import MempoolItem from chia.types.spend_bundle import SpendBundle from chia.util.cached_bls import LOCAL_CACHE -from chia.util.clvm import int_from_bytes from chia.util.condition_tools import pkm_pairs from chia.util.errors import Err, ValidationError from chia.util.generator_tools import additions_for_npc diff --git a/chia/types/blockchain_format/coin.py b/chia/types/blockchain_format/coin.py index 456980fb9eb3..1ba4e0a04b48 100644 --- a/chia/types/blockchain_format/coin.py +++ b/chia/types/blockchain_format/coin.py @@ -1,8 +1,9 @@ from dataclasses import dataclass from typing import Any, List +from clvm.casts import int_to_bytes + from chia.types.blockchain_format.sized_bytes import bytes32 -from chia.util.clvm import int_to_bytes from chia.util.hash import std_hash from chia.util.ints import uint64 from chia.util.streamable import Streamable, streamable diff --git a/chia/util/clvm.py b/chia/util/clvm.py deleted file mode 100644 index 28d5e50ac416..000000000000 --- a/chia/util/clvm.py +++ /dev/null @@ -1 +0,0 @@ -from clvm.casts import int_from_bytes, int_to_bytes # noqa diff --git a/chia/util/condition_tools.py b/chia/util/condition_tools.py index 287edc3657e4..74a1e5fda7d9 100644 --- a/chia/util/condition_tools.py +++ b/chia/util/condition_tools.py @@ -1,5 +1,7 @@ from typing import Dict, List, Optional, Tuple, Set +from clvm.casts import int_from_bytes + from chia.types.announcement import Announcement from chia.types.name_puzzle_condition import NPC from chia.types.blockchain_format.coin import Coin @@ -7,7 +9,6 @@ from chia.types.blockchain_format.sized_bytes import bytes32, bytes48 from chia.types.condition_opcodes import ConditionOpcode from chia.types.condition_with_args import ConditionWithArgs -from chia.util.clvm import int_from_bytes from chia.util.errors import ConsensusError, Err from chia.util.ints import uint64 diff --git a/tests/core/full_node/test_full_node.py b/tests/core/full_node/test_full_node.py index a4fbeef68a3e..b30c9e334ac9 100644 --- a/tests/core/full_node/test_full_node.py +++ b/tests/core/full_node/test_full_node.py @@ -8,6 +8,7 @@ from typing import Dict, Optional, List from blspy import G2Element +from clvm.casts import int_to_bytes import pytest from chia.consensus.blockchain import ReceiveBlockResult @@ -34,7 +35,6 @@ from chia.types.spend_bundle import SpendBundle from chia.types.unfinished_block import UnfinishedBlock from tests.block_tools import get_signage_point -from chia.util.clvm import int_to_bytes from chia.util.errors import Err from chia.util.hash import std_hash from chia.util.ints import uint8, uint16, uint32, uint64 diff --git a/tests/core/full_node/test_mempool.py b/tests/core/full_node/test_mempool.py index a3a814711d3a..6d6526191c7d 100644 --- a/tests/core/full_node/test_mempool.py +++ b/tests/core/full_node/test_mempool.py @@ -5,6 +5,7 @@ from typing import Dict, List, Optional, Tuple, Callable +from clvm.casts import int_to_bytes import pytest import chia.server.ws_connection as ws @@ -23,7 +24,6 @@ from chia.types.condition_with_args import ConditionWithArgs from chia.types.spend_bundle import SpendBundle from chia.types.mempool_item import MempoolItem -from chia.util.clvm import int_to_bytes from chia.util.condition_tools import conditions_for_solution, pkm_pairs from chia.util.errors import Err from chia.util.ints import uint64, uint32 diff --git a/tests/core/full_node/test_performance.py b/tests/core/full_node/test_performance.py index 948d3e36a7af..0c0da13a038a 100644 --- a/tests/core/full_node/test_performance.py +++ b/tests/core/full_node/test_performance.py @@ -6,6 +6,7 @@ import time from typing import Dict +from clvm.casts import int_to_bytes import pytest import cProfile @@ -15,7 +16,6 @@ from chia.types.condition_opcodes import ConditionOpcode from chia.types.condition_with_args import ConditionWithArgs from chia.types.unfinished_block import UnfinishedBlock -from chia.util.clvm import int_to_bytes from chia.util.ints import uint64 from tests.wallet_tools import WalletTool diff --git a/tests/generator/test_rom.py b/tests/generator/test_rom.py index 868ca9774ff7..dce72b9183a9 100644 --- a/tests/generator/test_rom.py +++ b/tests/generator/test_rom.py @@ -1,3 +1,4 @@ +from clvm.casts import int_to_bytes from clvm_tools import binutils from clvm_tools.clvmc import compile_clvm_text @@ -8,7 +9,6 @@ from chia.types.condition_with_args import ConditionWithArgs from chia.types.name_puzzle_condition import NPC from chia.types.generator_types import BlockGenerator -from chia.util.clvm import int_to_bytes from chia.util.condition_tools import ConditionOpcode from chia.util.ints import uint32 from chia.wallet.puzzles.load_clvm import load_clvm diff --git a/tests/wallet_tools.py b/tests/wallet_tools.py index 3db5ddeca8d9..dc3aba79ec2d 100644 --- a/tests/wallet_tools.py +++ b/tests/wallet_tools.py @@ -1,6 +1,7 @@ from typing import Dict, List, Optional, Tuple, Any from blspy import AugSchemeMPL, G2Element, PrivateKey +from clvm.casts import int_from_bytes, int_to_bytes from chia.consensus.constants import ConsensusConstants from chia.util.hash import std_hash @@ -12,7 +13,6 @@ from chia.types.condition_opcodes import ConditionOpcode from chia.types.condition_with_args import ConditionWithArgs from chia.types.spend_bundle import SpendBundle -from chia.util.clvm import int_from_bytes, int_to_bytes from chia.util.condition_tools import conditions_by_opcode, conditions_for_solution from chia.util.ints import uint32, uint64 from chia.wallet.derive_keys import master_sk_to_wallet_sk From cc63c7017b5f9023da8cf8ab78c948d03209d245 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Mon, 14 Feb 2022 15:53:47 -0500 Subject: [PATCH 042/378] Remove deprecated initial_num_public_keys_new_wallet from initial-config.yaml (#10221) https://github.com/Chia-Network/chia-blockchain/blob/231ef6faf20fba7463831a5015edee649a1d5d49/CHANGELOG.md > Fixed issues where the wallet code would not generate enough addresses when looking for coins, which can result in missed coins due to the address not being checked. Deprecated the config setting initial_num_public_keys_new_wallet. The config setting initial_num_public_keys is now used in all cases. --- chia/util/initial-config.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/chia/util/initial-config.yaml b/chia/util/initial-config.yaml index 635ff02adab5..1b53f7e958cb 100644 --- a/chia/util/initial-config.yaml +++ b/chia/util/initial-config.yaml @@ -473,7 +473,6 @@ wallet: start_height_buffer: 100 # Wallet will stop fly sync at starting_height - buffer num_sync_batches: 50 initial_num_public_keys: 100 - initial_num_public_keys_new_wallet: 5 dns_servers: - "dns-introducer.chia.net" From 41717337e6b35f5a85433538b8d023bafad567f5 Mon Sep 17 00:00:00 2001 From: Adam Kelly <338792+aqk@users.noreply.github.com> Date: Mon, 14 Feb 2022 20:07:06 -0800 Subject: [PATCH 043/378] =?UTF-8?q?This=20patch=20enables=20fees=20for=20t?= =?UTF-8?q?he=20plotnft=20commands=20-=20create,=20claim,=20joi=E2=80=A6?= =?UTF-8?q?=20(#9968)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * This patch enables fees for the plotnft commands - create, claim, join, and leave. It also corrects a mismatch in the wallet code that appeared to expect Announcement objects, but actually required bytes. * Update to using Announcement class. Publish both claim tx and fee tx for correct wallet accounting. * Update keysearch for new wallet * Update test for fee, and update wallet_id invariant * Rename variable tracking coin in absorb SpendBundle * Update RPC failure cmdline diagnostic * Remove fee parameter in sign method. Add publish transaction method. Add entry in RPC API replies. * Fix fee tx in absorb. Update absorb tests to test for fees * conflict * lint Co-authored-by: William Blanke --- chia/cmds/plotnft.py | 14 ----- chia/cmds/plotnft_funcs.py | 2 +- chia/pools/pool_wallet.py | 105 +++++++++++++++++++++++---------- chia/pools/pool_wallet_info.py | 8 ++- chia/rpc/wallet_rpc_api.py | 12 ++-- chia/rpc/wallet_rpc_client.py | 22 ++++--- tests/pools/test_pool_rpc.py | 33 +++++++++-- 7 files changed, 129 insertions(+), 67 deletions(-) diff --git a/chia/cmds/plotnft.py b/chia/cmds/plotnft.py index 74c2b501bc75..1433f2c947d3 100644 --- a/chia/cmds/plotnft.py +++ b/chia/cmds/plotnft.py @@ -116,13 +116,6 @@ def create_cmd( required=True, callback=validate_fee, ) -@click.option( - "--fee", - help="Fee Per Transaction, in Mojos. Fee is used TWICE: once to leave pool, once to join.", - type=int, - callback=validate_fee, - default=0, -) @click.option( "-wp", "--wallet-rpc-port", @@ -153,13 +146,6 @@ def join_cmd(wallet_rpc_port: Optional[int], fingerprint: int, id: int, fee: int required=True, callback=validate_fee, ) -@click.option( - "--fee", - help="Transaction Fee, in Mojos. Fee is charged twice if already in a pool.", - type=int, - callback=validate_fee, - default=0, -) @click.option( "-wp", "--wallet-rpc-port", diff --git a/chia/cmds/plotnft_funcs.py b/chia/cmds/plotnft_funcs.py index b7ae0f1c9a70..5fa7d7c52d8b 100644 --- a/chia/cmds/plotnft_funcs.py +++ b/chia/cmds/plotnft_funcs.py @@ -104,7 +104,7 @@ async def create(args: dict, wallet_client: WalletRpcClient, fingerprint: int) - print(f"Do chia wallet get_transaction -f {fingerprint} -tx 0x{tx_record.name} to get status") return None except Exception as e: - print(f"Error creating plot NFT: {e}") + print(f"Error creating plot NFT: {e}\n Please start both farmer and wallet with: chia start -r farmer") return print("Aborting.") diff --git a/chia/pools/pool_wallet.py b/chia/pools/pool_wallet.py index e45c03886b3b..d3f30c57a989 100644 --- a/chia/pools/pool_wallet.py +++ b/chia/pools/pool_wallet.py @@ -1,3 +1,4 @@ +import dataclasses import logging import time from typing import Any, Optional, Set, Tuple, List, Dict @@ -413,6 +414,7 @@ async def create_new_pool_wallet_transaction( spend_bundle, singleton_puzzle_hash, launcher_coin_id = await PoolWallet.generate_launcher_spend( standard_wallet, uint64(1), + fee, initial_target_state, wallet_state_manager.constants.GENESIS_CHALLENGE, p2_singleton_delay_time, @@ -459,9 +461,15 @@ async def get_pool_wallet_index(self) -> uint32: async def sign(self, coin_spend: CoinSpend) -> SpendBundle: async def pk_to_sk(pk: G1Element) -> PrivateKey: - sk, _ = await self._get_owner_key_cache() - assert sk.get_g1() == pk - return sk + s = find_owner_sk([self.wallet_state_manager.private_key], pk) + if s is None: + return self.standard_wallet.secret_key_store.secret_key_for_public_key(pk) + else: + # Note that pool_wallet_index may be from another wallet than self.wallet_id + owner_sk, pool_wallet_index = s + if owner_sk is None: + return self.standard_wallet.secret_key_store.secret_key_for_public_key(pk) + return owner_sk return await sign_coin_spends( [coin_spend], @@ -470,7 +478,25 @@ async def pk_to_sk(pk: G1Element) -> PrivateKey: self.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM, ) - async def generate_travel_transaction(self, fee: uint64) -> TransactionRecord: + async def generate_fee_transaction(self, fee: uint64, coin_announcements=None) -> TransactionRecord: + fee_tx = await self.standard_wallet.generate_signed_transaction( + uint64(0), + (await self.standard_wallet.get_new_puzzlehash()), + fee=fee, + origin_id=None, + coins=None, + primaries=None, + ignore_max_send_amount=False, + coin_announcements_to_consume=coin_announcements, + ) + return fee_tx + + async def publish_transactions(self, travel_tx: TransactionRecord, fee_tx: Optional[TransactionRecord]): + await self.wallet_state_manager.add_pending_transaction(travel_tx) + if fee_tx is not None: + await self.wallet_state_manager.add_pending_transaction(dataclasses.replace(fee_tx, spend_bundle=None)) + + async def generate_travel_transactions(self, fee: uint64) -> Tuple[TransactionRecord, Optional[TransactionRecord]]: # target_state is contained within pool_wallet_state pool_wallet_info: PoolWalletInfo = await self.get_current_state() @@ -542,8 +568,11 @@ async def generate_travel_transaction(self, fee: uint64) -> TransactionRecord: else: raise RuntimeError("Invalid state") - signed_spend_bundle = await self.sign(outgoing_coin_spend) + fee_tx = None + if fee > 0: + fee_tx = await self.generate_fee_transaction(fee) + signed_spend_bundle = await self.sign(outgoing_coin_spend) assert signed_spend_bundle.removals()[0].puzzle_hash == singleton.puzzle_hash assert signed_spend_bundle.removals()[0].name() == singleton.name() assert signed_spend_bundle is not None @@ -566,12 +595,15 @@ async def generate_travel_transaction(self, fee: uint64) -> TransactionRecord: type=uint32(TransactionType.OUTGOING_TX.value), name=signed_spend_bundle.name(), ) - return tx_record + + await self.publish_transactions(tx_record, fee_tx) + return tx_record, fee_tx @staticmethod async def generate_launcher_spend( standard_wallet: Wallet, amount: uint64, + fee: uint64, initial_target_state: PoolState, genesis_challenge: bytes32, delay_time: uint64, @@ -629,7 +661,7 @@ async def generate_launcher_spend( create_launcher_tx_record: Optional[TransactionRecord] = await standard_wallet.generate_signed_transaction( amount, genesis_launcher_puz.get_tree_hash(), - uint64(0), + fee, None, coins, None, @@ -651,7 +683,9 @@ async def generate_launcher_spend( full_spend: SpendBundle = SpendBundle.aggregate([create_launcher_tx_record.spend_bundle, launcher_sb]) return full_spend, puzzle_hash, launcher_coin.name() - async def join_pool(self, target_state: PoolState, fee: uint64) -> Tuple[uint64, TransactionRecord]: + async def join_pool( + self, target_state: PoolState, fee: uint64 + ) -> Tuple[uint64, TransactionRecord, Optional[TransactionRecord]]: if target_state.state != FARMING_TO_POOL: raise ValueError(f"join_pool must be called with target_state={FARMING_TO_POOL} (FARMING_TO_POOL)") if self.target_state is not None: @@ -689,12 +723,10 @@ async def join_pool(self, target_state: PoolState, fee: uint64) -> Tuple[uint64, self.target_state = target_state self.next_transaction_fee = fee - tx_record: TransactionRecord = await self.generate_travel_transaction(fee) - await self.wallet_state_manager.add_pending_transaction(tx_record) - - return total_fee, tx_record + travel_tx, fee_tx = await self.generate_travel_transactions(fee) + return total_fee, travel_tx, fee_tx - async def self_pool(self, fee: uint64) -> Tuple[uint64, TransactionRecord]: + async def self_pool(self, fee: uint64) -> Tuple[uint64, TransactionRecord, Optional[TransactionRecord]]: if await self.have_unconfirmed_transaction(): raise ValueError( "Cannot self pool due to unconfirmed transaction. If this is stuck, delete the unconfirmed transaction." @@ -725,11 +757,10 @@ async def self_pool(self, fee: uint64) -> Tuple[uint64, TransactionRecord]: SELF_POOLING, owner_puzzlehash, owner_pubkey, pool_url=None, relative_lock_height=uint32(0) ) self.next_transaction_fee = fee - tx_record = await self.generate_travel_transaction(fee) - await self.wallet_state_manager.add_pending_transaction(tx_record) - return total_fee, tx_record + travel_tx, fee_tx = await self.generate_travel_transactions(fee) + return total_fee, travel_tx, fee_tx - async def claim_pool_rewards(self, fee: uint64) -> TransactionRecord: + async def claim_pool_rewards(self, fee: uint64) -> Tuple[TransactionRecord, Optional[TransactionRecord]]: # Search for p2_puzzle_hash coins, and spend them with the singleton if await self.have_unconfirmed_transaction(): raise ValueError( @@ -757,9 +788,12 @@ async def claim_pool_rewards(self, fee: uint64) -> TransactionRecord: all_spends: List[CoinSpend] = [] total_amount = 0 + + current_coin_record = None for coin_record in unspent_coin_records: if coin_record.coin not in coin_to_height_farmed: continue + current_coin_record = coin_record if len(all_spends) >= 100: # Limit the total number of spends, so it fits into the block break @@ -778,32 +812,44 @@ async def claim_pool_rewards(self, fee: uint64) -> TransactionRecord: self.log.info( f"Farmer coin: {coin_record.coin} {coin_record.coin.name()} {coin_to_height_farmed[coin_record.coin]}" ) - if len(all_spends) == 0: + if len(all_spends) == 0 or current_coin_record is None: raise ValueError("Nothing to claim, no unspent coinbase rewards") - # No signatures are required to absorb - spend_bundle: SpendBundle = SpendBundle(all_spends, G2Element()) + claim_spend: SpendBundle = SpendBundle(all_spends, G2Element()) + + # If fee is 0, no signatures are required to absorb + full_spend: SpendBundle = claim_spend + + fee_tx = None + if fee > 0: + absorb_announce = Announcement(current_coin_record.coin.name(), b"$") + fee_tx = await self.generate_fee_transaction(fee, coin_announcements=[absorb_announce]) + full_spend = SpendBundle.aggregate([fee_tx.spend_bundle, claim_spend]) + assert full_spend.fees() == fee + current_time = uint64(int(time.time())) + # The claim spend, minus the fee amount from the main wallet absorb_transaction: TransactionRecord = TransactionRecord( confirmed_at_height=uint32(0), - created_at_time=uint64(int(time.time())), + created_at_time=current_time, to_puzzle_hash=current_state.current.target_puzzle_hash, amount=uint64(total_amount), - fee_amount=fee, + fee_amount=fee, # This will not be double counted in self.standard_wallet confirmed=False, sent=uint32(0), - spend_bundle=spend_bundle, - additions=spend_bundle.additions(), - removals=spend_bundle.removals(), + spend_bundle=full_spend, + additions=full_spend.additions(), + removals=full_spend.removals(), wallet_id=uint32(self.wallet_id), sent_to=[], memos=[], trade_id=None, type=uint32(TransactionType.OUTGOING_TX.value), - name=spend_bundle.name(), + name=full_spend.name(), ) - await self.wallet_state_manager.add_pending_transaction(absorb_transaction) - return absorb_transaction + + await self.publish_transactions(absorb_transaction, fee_tx) + return absorb_transaction, fee_tx async def new_peak(self, peak_height: uint64) -> None: # This gets called from the WalletStateManager whenever there is a new peak @@ -847,8 +893,7 @@ async def new_peak(self, peak_height: uint64) -> None: assert self.target_state.relative_lock_height >= self.MINIMUM_RELATIVE_LOCK_HEIGHT assert self.target_state.pool_url is not None - tx_record = await self.generate_travel_transaction(self.next_transaction_fee) - await self.wallet_state_manager.add_pending_transaction(tx_record) + await self.generate_travel_transactions(self.next_transaction_fee) async def have_unconfirmed_transaction(self) -> bool: unconfirmed: List[TransactionRecord] = await self.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( diff --git a/chia/pools/pool_wallet_info.py b/chia/pools/pool_wallet_info.py index 42c5e4aebc9c..6437baf30e1a 100644 --- a/chia/pools/pool_wallet_info.py +++ b/chia/pools/pool_wallet_info.py @@ -1,6 +1,6 @@ from dataclasses import dataclass from enum import IntEnum -from typing import Optional, Dict +from typing import Optional, Dict, Any from blspy import G1Element @@ -10,7 +10,7 @@ from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.byte_types import hexstr_to_bytes from chia.util.ints import uint32, uint8 -from chia.util.streamable import streamable, Streamable +from chia.util.streamable import streamable, Streamable, dataclass_from_dict class PoolSingletonState(IntEnum): @@ -113,3 +113,7 @@ class PoolWalletInfo(Streamable): current_inner: Program # Inner puzzle in current singleton, not revealed yet tip_singleton_coin_id: bytes32 singleton_block_height: uint32 # Block height that current PoolState is from + + @classmethod + def from_json_dict(cls: Any, json_dict: Dict) -> Any: + return dataclass_from_dict(cls, json_dict) diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 7a4949c6c211..47434c189058 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -1321,8 +1321,8 @@ async def pw_join_pool(self, request) -> Dict: uint32(request["relative_lock_height"]), ) async with self.service.wallet_state_manager.lock: - total_fee, tx = await wallet.join_pool(new_target_state, fee) - return {"total_fee": total_fee, "transaction": tx} + total_fee, tx, fee_tx = await wallet.join_pool(new_target_state, fee) + return {"total_fee": total_fee, "transaction": tx, "fee_transaction": fee_tx} async def pw_self_pool(self, request) -> Dict: if self.service.wallet_state_manager is None: @@ -1338,8 +1338,8 @@ async def pw_self_pool(self, request) -> Dict: raise ValueError("Wallet needs to be fully synced.") async with self.service.wallet_state_manager.lock: - total_fee, tx = await wallet.self_pool(fee) # total_fee: uint64, tx: TransactionRecord - return {"total_fee": total_fee, "transaction": tx} + total_fee, tx, fee_tx = await wallet.self_pool(fee) + return {"total_fee": total_fee, "transaction": tx, "fee_transaction": fee_tx} async def pw_absorb_rewards(self, request) -> Dict: """Perform a sweep of the p2_singleton rewards controlled by the pool wallet singleton""" @@ -1352,9 +1352,9 @@ async def pw_absorb_rewards(self, request) -> Dict: wallet: PoolWallet = self.service.wallet_state_manager.wallets[wallet_id] async with self.service.wallet_state_manager.lock: - transaction: TransactionRecord = await wallet.claim_pool_rewards(fee) + transaction, fee_tx = await wallet.claim_pool_rewards(fee) state: PoolWalletInfo = await wallet.get_current_state() - return {"state": state.to_json_dict(), "transaction": transaction} + return {"state": state.to_json_dict(), "transaction": transaction, "fee_transaction": fee_tx} async def pw_status(self, request) -> Dict: """Return the complete state of the Pool wallet with id `request["wallet_id"]`""" diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index 7192a84707cb..08998b4befe4 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -328,7 +328,7 @@ async def pw_self_pool(self, wallet_id: str, fee: uint64) -> TransactionRecord: async def pw_join_pool( self, wallet_id: str, target_puzzlehash: bytes32, pool_url: str, relative_lock_height: uint32, fee: uint64 - ) -> TransactionRecord: + ) -> Dict: request = { "wallet_id": int(wallet_id), "target_puzzlehash": target_puzzlehash.hex(), @@ -337,13 +337,19 @@ async def pw_join_pool( "fee": fee, } - join_reply = await self.fetch("pw_join_pool", request) - return TransactionRecord.from_json_dict(join_reply["transaction"]) - - async def pw_absorb_rewards(self, wallet_id: str, fee: uint64 = uint64(0)) -> TransactionRecord: - return TransactionRecord.from_json_dict( - (await self.fetch("pw_absorb_rewards", {"wallet_id": wallet_id, "fee": fee}))["transaction"] - ) + reply = await self.fetch("pw_join_pool", request) + reply["transaction"] = TransactionRecord.from_json_dict(reply["transaction"]) + if reply["fee_transaction"]: + reply["fee_transaction"] = TransactionRecord.from_json_dict(reply["fee_transaction"]) + return reply["transaction"] + + async def pw_absorb_rewards(self, wallet_id: str, fee: uint64 = uint64(0)) -> Dict: + reply = await self.fetch("pw_absorb_rewards", {"wallet_id": wallet_id, "fee": fee}) + reply["state"] = PoolWalletInfo.from_json_dict(reply["state"]) + reply["transaction"] = TransactionRecord.from_json_dict(reply["transaction"]) + if reply["fee_transaction"]: + reply["fee_transaction"] = TransactionRecord.from_json_dict(reply["fee_transaction"]) + return reply async def pw_status(self, wallet_id: str) -> Tuple[PoolWalletInfo, List[TransactionRecord]]: json_dict = await self.fetch("pw_status", {"wallet_id": wallet_id}) diff --git a/tests/pools/test_pool_rpc.py b/tests/pools/test_pool_rpc.py index abae043baa5a..a54ef79c1e54 100644 --- a/tests/pools/test_pool_rpc.py +++ b/tests/pools/test_pool_rpc.py @@ -457,7 +457,7 @@ async def test_create_multiple_pool_wallets(self, one_wallet_node_and_rpc, fee, @pytest.mark.asyncio @pytest.mark.parametrize("trusted", [True, False]) - @pytest.mark.parametrize("fee", [0]) + @pytest.mark.parametrize("fee", [0, FEE_AMOUNT]) async def test_absorb_self(self, one_wallet_node_and_rpc, fee, trusted): client, wallet_node_0, full_node_api = one_wallet_node_and_rpc if trusted: @@ -516,7 +516,7 @@ async def test_absorb_self(self, one_wallet_node_and_rpc, fee, trusted): assert bal["confirmed_wallet_balance"] == 2 * 1750000000000 # Claim 2 * 1.75, and farm a new 1.75 - absorb_tx: TransactionRecord = await client.pw_absorb_rewards(2, fee) + absorb_tx: TransactionRecord = (await client.pw_absorb_rewards(2, fee))["transaction"] await time_out_assert( 5, full_node_api.full_node.mempool_manager.get_spendbundle, @@ -532,7 +532,7 @@ async def test_absorb_self(self, one_wallet_node_and_rpc, fee, trusted): assert bal["confirmed_wallet_balance"] == 1 * 1750000000000 # Claim another 1.75 - absorb_tx1: TransactionRecord = await client.pw_absorb_rewards(2, fee) + absorb_tx1: TransactionRecord = (await client.pw_absorb_rewards(2, fee))["transaction"] absorb_tx1.spend_bundle.debug() await time_out_assert( @@ -567,6 +567,10 @@ async def test_absorb_self(self, one_wallet_node_and_rpc, fee, trusted): with pytest.raises(ValueError): await client.pw_absorb_rewards(2, fee) + tx1 = await client.get_transactions(1) + assert (250000000000 + fee) in [tx.additions[0].amount for tx in tx1] + # await time_out_assert(10, wallet_0.get_confirmed_balance, total_block_rewards) + @pytest.mark.asyncio @pytest.mark.parametrize("trusted", [True, False]) @pytest.mark.parametrize("fee", [0, FEE_AMOUNT]) @@ -629,7 +633,7 @@ async def test_absorb_pooling(self, one_wallet_node_and_rpc, fee, trusted): assert bal["confirmed_wallet_balance"] == 0 # Claim 2 * 1.75, and farm a new 1.75 - absorb_tx: TransactionRecord = await client.pw_absorb_rewards(2, fee) + absorb_tx: TransactionRecord = (await client.pw_absorb_rewards(2, fee))["transaction"] await time_out_assert( 5, full_node_api.full_node.mempool_manager.get_spendbundle, @@ -645,7 +649,8 @@ async def test_absorb_pooling(self, one_wallet_node_and_rpc, fee, trusted): assert bal["confirmed_wallet_balance"] == 0 # Claim another 1.75 - absorb_tx: TransactionRecord = await client.pw_absorb_rewards(2, fee) + ret = await client.pw_absorb_rewards(2, fee) + absorb_tx: TransactionRecord = ret["transaction"] absorb_tx.spend_bundle.debug() await time_out_assert( 5, @@ -654,6 +659,12 @@ async def test_absorb_pooling(self, one_wallet_node_and_rpc, fee, trusted): absorb_tx.name, ) + if fee == 0: + assert ret["fee_transaction"] is None + else: + assert ret["fee_transaction"].fee_amount == fee + assert absorb_tx.fee_amount == fee + await self.farm_blocks(full_node_api, our_ph, 2) await asyncio.sleep(5) bal = await client.get_wallet_balance(2) @@ -684,7 +695,8 @@ async def test_absorb_pooling(self, one_wallet_node_and_rpc, fee, trusted): await full_node_api.full_node.respond_block(full_node_protocol.RespondBlock(block)) await asyncio.sleep(2) - absorb_tx: TransactionRecord = await client.pw_absorb_rewards(2, fee) + ret = await client.pw_absorb_rewards(2, fee) + absorb_tx: TransactionRecord = ret["transaction"] await time_out_assert( 5, full_node_api.full_node.mempool_manager.get_spendbundle, @@ -698,6 +710,14 @@ async def test_absorb_pooling(self, one_wallet_node_and_rpc, fee, trusted): assert status.current == new_status.current assert status.tip_singleton_coin_id != new_status.tip_singleton_coin_id status = new_status + assert ret["fee_transaction"] is None + + bal2 = await client.get_wallet_balance(2) + assert bal2["confirmed_wallet_balance"] == 0 + # Note: as written, confirmed balance will not reflect on absorbs, because the fee + # is paid back into the same client's wallet in this test. + tx1 = await client.get_transactions(1) + assert (250000000000 + fee) in [tx.additions[0].amount for tx in tx1] @pytest.mark.asyncio @pytest.mark.parametrize("trusted", [True]) @@ -916,6 +936,7 @@ async def have_chia(): assert status.current.state == 1 assert status.current.version == 1 + assert status.target assert status.target.pool_url == "https://pool.example.com" assert status.target.relative_lock_height == 5 assert status.target.state == 3 From ae9b7f4e7dd309d407ee74e6bcf944b8dfd9e8ba Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Tue, 15 Feb 2022 01:23:56 -0500 Subject: [PATCH 044/378] Disable CentOS 8 install script check (#10237) --- .github/workflows/test-install-scripts.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-install-scripts.yml b/.github/workflows/test-install-scripts.yml index 6ac141058e16..138c4a83c214 100644 --- a/.github/workflows/test-install-scripts.yml +++ b/.github/workflows/test-install-scripts.yml @@ -67,9 +67,10 @@ jobs: - name: centos:7 type: centos url: "docker://centos:7" - - name: centos:8 - type: centos - url: "docker://centos:8" +# commented out until we decide what to do with this, it fails consistently +# - name: centos:8 +# type: centos +# url: "docker://centos:8" - name: debian:buster type: debian # https://packages.debian.org/buster/python/python3 (3.7) From bd845f5d3acd9e6c7fb3f963c482d5c0b3da40cd Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Tue, 15 Feb 2022 01:24:19 -0500 Subject: [PATCH 045/378] Test CLI installer on Rocky (#10134) * Test CLI installer on Rocky * Add Rocky support to install.sh * Update test-install-scripts.yml * oops * also sudo for rocky install.sh testing * fix indentation * oops --- .github/workflows/test-install-scripts.yml | 8 ++++++++ install.sh | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/.github/workflows/test-install-scripts.yml b/.github/workflows/test-install-scripts.yml index 138c4a83c214..83c8696b7246 100644 --- a/.github/workflows/test-install-scripts.yml +++ b/.github/workflows/test-install-scripts.yml @@ -95,6 +95,9 @@ jobs: # type: fedora # # (35, 3.10) https://packages.fedoraproject.org/search?query=python3&releases=Fedora+35&start=0 # url: "docker://fedora:35" + - name: rockylinux:8 + type: rocky + url: "docker://rockylinux:8" - name: ubuntu:bionic (18.04) type: ubuntu # https://packages.ubuntu.com/bionic/python3.7 (18.04, 3.7) @@ -155,6 +158,11 @@ jobs: run: | yum install --assumeyes git + - name: Prepare Rocky + if: ${{ matrix.distribution.type == 'rocky' }} + run: | + yum install --assumeyes git sudo + - name: Prepare Ubuntu if: ${{ matrix.distribution.type == 'ubuntu' }} env: diff --git a/install.sh b/install.sh index 5fc39444a633..0850db57035a 100644 --- a/install.sh +++ b/install.sh @@ -160,6 +160,10 @@ if [ "$(uname)" = "Linux" ]; then if ! command -v python3.9 >/dev/null 2>&1; then install_python3_and_sqlite3_from_source_with_yum fi + elif type yum >/dev/null 2>&1 && [ -f "/etc/redhat-release" ] && grep Rocky /etc/redhat-release; then + echo "Installing on Rocky." + # TODO: make this smarter about getting the latest version + sudo yum install --assumeyes python39 elif type yum >/dev/null 2>&1 && [ -f "/etc/redhat-release" ] || [ -f "/etc/fedora-release" ]; then # Redhat or Fedora echo "Installing on Redhat/Fedora." From ff88892e7398dc299eda284032c77c9d1daaa808 Mon Sep 17 00:00:00 2001 From: William Allen Date: Tue, 15 Feb 2022 00:25:09 -0600 Subject: [PATCH 046/378] git short hash in dev artifact (#10195) * Adding build-linux-arm64-installer changes * Adding changes to build installers to include short hash in dev artifact * Fixing s3 dmg path for azure pipelines dev build * Testing shell: bash for win workflow * flipping slashes for bash env * Formatting build windows installer workflow * Reformatting build windows installer workflow --- .github/workflows/build-linux-arm64-installer.yml | 5 ++++- .github/workflows/build-linux-installer-deb.yml | 5 ++++- .github/workflows/build-linux-installer-rpm.yml | 5 ++++- .github/workflows/build-macos-m1-installer.yml | 6 +++++- .github/workflows/build-windows-installer.yml | 9 +++++++-- azure-pipelines.yml | 4 +++- 6 files changed, 27 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-linux-arm64-installer.yml b/.github/workflows/build-linux-arm64-installer.yml index e154ecc78e31..fd69f5a1dd48 100644 --- a/.github/workflows/build-linux-arm64-installer.yml +++ b/.github/workflows/build-linux-arm64-installer.yml @@ -137,7 +137,10 @@ jobs: CHIA_INSTALLER_VERSION: ${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }} if: steps.check_secrets.outputs.HAS_SECRET run: | - aws s3 cp "$GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_arm64.deb" s3://download-chia-net/builds/ + GIT_SHORT_HASH=$(echo "${GITHUB_SHA}" | cut -c1-8) + CHIA_DEV_BUILD=${CHIA_INSTALLER_VERSION}-$GIT_SHORT_HASH + echo "CHIA_DEV_BUILD=$CHIA_DEV_BUILD" >>$GITHUB_ENV + aws s3 cp "$GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_arm64.deb" "s3://download-chia-net/dev/chia-blockchain_${CHIA_DEV_BUILD}_arm64.deb" - name: Create Checksums if: startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main' diff --git a/.github/workflows/build-linux-installer-deb.yml b/.github/workflows/build-linux-installer-deb.yml index 60963186b7f8..76d020114c18 100644 --- a/.github/workflows/build-linux-installer-deb.yml +++ b/.github/workflows/build-linux-installer-deb.yml @@ -177,8 +177,11 @@ jobs: env: CHIA_INSTALLER_VERSION: ${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }} run: | + GIT_SHORT_HASH=$(echo "${GITHUB_SHA}" | cut -c1-8) + CHIA_DEV_BUILD=${CHIA_INSTALLER_VERSION}-$GIT_SHORT_HASH + echo "CHIA_DEV_BUILD=$CHIA_DEV_BUILD" >>$GITHUB_ENV ls ${{ github.workspace }}/build_scripts/final_installer/ - aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_amd64.deb s3://download-chia-net/builds/ + aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_amd64.deb s3://download-chia-net/dev/chia-blockchain_${CHIA_DEV_BUILD}_amd64.deb - name: Create Checksums if: startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main' diff --git a/.github/workflows/build-linux-installer-rpm.yml b/.github/workflows/build-linux-installer-rpm.yml index 5c4843e621cf..2eaa7e36be89 100644 --- a/.github/workflows/build-linux-installer-rpm.yml +++ b/.github/workflows/build-linux-installer-rpm.yml @@ -137,8 +137,11 @@ jobs: env: CHIA_INSTALLER_VERSION: ${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }} run: | + GIT_SHORT_HASH=$(echo "${GITHUB_SHA}" | cut -c1-8) + CHIA_DEV_BUILD=${CHIA_INSTALLER_VERSION}-$GIT_SHORT_HASH + echo "CHIA_DEV_BUILD=$CHIA_DEV_BUILD" >>$GITHUB_ENV ls $GITHUB_WORKSPACE/build_scripts/final_installer/ - aws s3 cp $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain-${CHIA_INSTALLER_VERSION}-1.x86_64.rpm s3://download-chia-net/builds/ + aws s3 cp $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain-${CHIA_INSTALLER_VERSION}-1.x86_64.rpm s3://download-chia-net/dev/chia-blockchain-${CHIA_DEV_BUILD}-1.x86_64.rpm - name: Create Checksums if: startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main' diff --git a/.github/workflows/build-macos-m1-installer.yml b/.github/workflows/build-macos-m1-installer.yml index 3b9aff5cec22..3f09e7de9960 100644 --- a/.github/workflows/build-macos-m1-installer.yml +++ b/.github/workflows/build-macos-m1-installer.yml @@ -144,8 +144,12 @@ jobs: AWS_ACCESS_KEY_ID: ${{ secrets.INSTALLER_UPLOAD_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.INSTALLER_UPLOAD_SECRET }} AWS_REGION: us-west-2 + CHIA_INSTALLER_VERSION: ${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }} run: | - arch -arm64 aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}-arm64.dmg s3://download-chia-net/builds/ + GIT_SHORT_HASH=$(echo "${GITHUB_SHA}" | cut -c1-8) + CHIA_DEV_BUILD=${CHIA_INSTALLER_VERSION}-$GIT_SHORT_HASH + echo "CHIA_DEV_BUILD=$CHIA_DEV_BUILD" >>$GITHUB_ENV + arch -arm64 aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/Chia-${CHIA_INSTALLER_VERSION}-arm64.dmg s3://download-chia-net/dev/Chia-${CHIA_DEV_BUILD}-arm64.dmg - name: Install py3createtorrent if: startsWith(github.ref, 'refs/tags/') diff --git a/.github/workflows/build-windows-installer.yml b/.github/workflows/build-windows-installer.yml index ecb16c4d0a2b..e5eb2bcb6de3 100644 --- a/.github/workflows/build-windows-installer.yml +++ b/.github/workflows/build-windows-installer.yml @@ -181,9 +181,14 @@ jobs: if: steps.check_secrets.outputs.HAS_AWS_SECRET env: CHIA_INSTALLER_VERSION: ${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }} + shell: bash run: | - ls ${{ github.workspace }}\chia-blockchain-gui\release-builds\windows-installer\ - aws s3 cp ${{ github.workspace }}\chia-blockchain-gui\release-builds\windows-installer\ChiaSetup-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.exe s3://download-chia-net/builds/ + GIT_SHORT_HASH=$(echo "${GITHUB_SHA}" | cut -c1-8) + CHIA_DEV_BUILD=${CHIA_INSTALLER_VERSION}-$GIT_SHORT_HASH + echo ::set-output name=CHIA_DEV_BUILD::${CHIA_DEV_BUILD} + echo ${CHIA_DEV_BUILD} + pwd + aws s3 cp chia-blockchain-gui/release-builds/windows-installer/ChiaSetup-${CHIA_INSTALLER_VERSION}.exe s3://download-chia-net/dev/ChiaSetup-${CHIA_DEV_BUILD}.exe - name: Create Checksums env: diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 76781bc665af..f9c401ce0b27 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -108,7 +108,9 @@ steps: export AWS_ACCESS_KEY_ID=$(AccessKey) export AWS_SECRET_ACCESS_KEY=$(SecretKey) export AWS_DEFAULT_REGION=us-west-2 - aws s3 cp $(System.DefaultWorkingDirectory)/build_scripts/final_installer/*.dmg s3://download-chia-net/builds/ + export GIT_SHORT_HASH=$(git rev-parse --short HEAD) + export CHIA_DEV_BUILD=$(basename $(ls $(System.DefaultWorkingDirectory)/build_scripts/final_installer/*.dmg | sed "s/.dmg/-$GIT_SHORT_HASH.dmg/")) + aws s3 cp $(System.DefaultWorkingDirectory)/build_scripts/final_installer/*.dmg s3://download-chia-net/dev/$CHIA_DEV_BUILD displayName: "Upload to S3" - bash: | From 65c0829f661316c622e1c3106af425f375f1ed1a Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Tue, 15 Feb 2022 01:28:42 -0500 Subject: [PATCH 047/378] madMAx executable discovery error handling cleanup (#9501) * Add missing version=None for * Log errors in get_madmax_install_info() instead of printing --- chia/plotters/madmax.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/chia/plotters/madmax.py b/chia/plotters/madmax.py index ca6560521a36..ce75682b08ab 100644 --- a/chia/plotters/madmax.py +++ b/chia/plotters/madmax.py @@ -45,6 +45,7 @@ def get_madmax_install_info(plotters_root_path: Path) -> Optional[Dict[str, Any] supported: bool = is_madmax_supported() if get_madmax_executable_path_for_ksize(plotters_root_path).exists(): + version = None try: proc = run_command( [os.fspath(get_madmax_executable_path_for_ksize(plotters_root_path)), "--version"], @@ -54,7 +55,8 @@ def get_madmax_install_info(plotters_root_path: Path) -> Optional[Dict[str, Any] ) version = proc.stdout.strip() except Exception as e: - print(f"Failed to determine madmax version: {e}") + tb = traceback.format_exc() + log.error(f"Failed to determine madmax version: {e} {tb}") if version is not None: installed = True From 98971de9d525e1de323ecb09f0777cca6286f932 Mon Sep 17 00:00:00 2001 From: dustinface <35775977+xdustinface@users.noreply.github.com> Date: Tue, 15 Feb 2022 16:28:53 +0100 Subject: [PATCH 048/378] pre-commit: Add a new hook to run `isort` (#8827) * pre-commit: Add a new hook to run `isort` * contributing: Add hint about `isort` * add isort to dev deps, ignore existing .py files, use black profile * long list to lines not comma delimited * isort: Update and sort ignore list to match latest `main` * add bash command line to generate isort extend skip list (#3) * add bash command line to generate extend skip list * tidy * isort: More files to ignore after rebase * tests: Fix `test_wallet_user_store.py` after rebase * Some fixes after rebase Co-authored-by: Kyle Altendorf --- .isort.cfg | 259 +++++++++++++++++++++++-- .pre-commit-config.yaml | 21 ++ CONTRIBUTING.md | 1 + benchmarks/block_ref.py | 14 +- chia/consensus/blockchain.py | 2 +- chia/wallet/util/peer_request_cache.py | 2 +- setup.py | 1 + tests/tools/test_full_sync.py | 3 +- tests/wallet/test_wallet_user_store.py | 2 +- tools/test_full_sync.py | 25 ++- 10 files changed, 288 insertions(+), 42 deletions(-) diff --git a/.isort.cfg b/.isort.cfg index d21b9f236f1b..c1991bb24e99 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -1,20 +1,243 @@ [settings] -profile= - -; vertical hanging indent mode also used in black configuration -multi_line_output = 3 - -; necessary because black expect the trailing comma -include_trailing_comma = true - -; black compatibility -force_grid_wrap = 0 - -; black compatibility -use_parentheses = True - -; black compatibility -ensure_newline_before_comments = True - -; we chose 120 as line length line_length = 120 +profile=black +skip_gitignore=true +# venv/bin/isort --check . |& sed -n "s;ERROR: ${PWD}/\(.*\) Imports are.*; \1;p" | sort | uniq +extend_skip= + benchmarks/block_store.py + benchmarks/coin_store.py + benchmarks/utils.py + chia/clvm/spend_sim.py + chia/cmds/chia.py + chia/cmds/db.py + chia/cmds/db_upgrade_func.py + chia/cmds/farm_funcs.py + chia/cmds/farm.py + chia/cmds/init_funcs.py + chia/cmds/init.py + chia/cmds/keys_funcs.py + chia/cmds/keys.py + chia/cmds/netspace.py + chia/cmds/passphrase_funcs.py + chia/cmds/passphrase.py + chia/cmds/plotnft_funcs.py + chia/cmds/plotnft.py + chia/cmds/plotters.py + chia/cmds/seeder.py + chia/cmds/show.py + chia/cmds/start_funcs.py + chia/cmds/start.py + chia/cmds/wallet_funcs.py + chia/cmds/wallet.py + chia/daemon/keychain_proxy.py + chia/daemon/keychain_server.py + chia/daemon/server.py + chia/farmer/farmer_api.py + chia/farmer/farmer.py + chia/full_node/block_height_map.py + chia/full_node/block_store.py + chia/full_node/bundle_tools.py + chia/full_node/coin_store.py + chia/full_node/full_node_api.py + chia/full_node/full_node.py + chia/full_node/generator.py + chia/full_node/hint_store.py + chia/full_node/lock_queue.py + chia/full_node/mempool_check_conditions.py + chia/full_node/mempool_manager.py + chia/full_node/weight_proof.py + chia/harvester/harvester_api.py + chia/harvester/harvester.py + chia/introducer/introducer.py + chia/plotters/bladebit.py + chia/plotters/chiapos.py + chia/plotters/install_plotter.py + chia/plotters/madmax.py + chia/plotters/plotters.py + chia/plotting/check_plots.py + chia/plotting/create_plots.py + chia/plotting/manager.py + chia/plotting/util.py + chia/pools/pool_puzzles.py + chia/pools/pool_wallet_info.py + chia/pools/pool_wallet.py + chia/protocols/harvester_protocol.py + chia/protocols/pool_protocol.py + chia/protocols/protocol_state_machine.py + chia/rpc/farmer_rpc_client.py + chia/rpc/full_node_rpc_client.py + chia/rpc/rpc_client.py + chia/rpc/wallet_rpc_api.py + chia/rpc/wallet_rpc_client.py + chia/seeder/crawler.py + chia/seeder/crawl_store.py + chia/seeder/dns_server.py + chia/seeder/util/service_groups.py + chia/seeder/util/service.py + chia/server/address_manager_sqlite_store.py + chia/server/address_manager_store.py + chia/server/introducer_peers.py + chia/server/node_discovery.py + chia/server/peer_store_resolver.py + chia/server/reconnect_task.py + chia/server/start_service.py + chia/server/start_wallet.py + chia/simulator/simulator_constants.py + chia/simulator/start_simulator.py + chia/ssl/create_ssl.py + chia/timelord/timelord_launcher.py + chia/types/blockchain_format/program.py + chia/types/blockchain_format/proof_of_space.py + chia/types/blockchain_format/vdf.py + chia/types/coin_solution.py + chia/types/coin_spend.py + chia/types/full_block.py + chia/types/generator_types.py + chia/types/name_puzzle_condition.py + chia/types/spend_bundle.py + chia/util/bech32m.py + chia/util/byte_types.py + chia/util/chain_utils.py + chia/util/check_fork_next_block.py + chia/util/chia_logging.py + chia/util/condition_tools.py + chia/util/dump_keyring.py + chia/util/file_keyring.py + chia/util/files.py + chia/util/generator_tools.py + chia/util/keychain.py + chia/util/keyring_wrapper.py + chia/util/log_exceptions.py + chia/util/network.py + chia/util/profiler.py + chia/util/service_groups.py + chia/util/ssl_check.py + chia/util/streamable.py + chia/util/ws_message.py + chia/wallet/cat_wallet/cat_info.py + chia/wallet/cat_wallet/cat_utils.py + chia/wallet/cat_wallet/cat_wallet.py + chia/wallet/derive_keys.py + chia/wallet/did_wallet/did_info.py + chia/wallet/did_wallet/did_wallet_puzzles.py + chia/wallet/did_wallet/did_wallet.py + chia/wallet/lineage_proof.py + chia/wallet/payment.py + chia/wallet/puzzles/genesis_checkers.py + chia/wallet/puzzles/load_clvm.py + chia/wallet/puzzles/prefarm/make_prefarm_ph.py + chia/wallet/puzzles/prefarm/spend_prefarm.py + chia/wallet/puzzles/puzzle_utils.py + chia/wallet/puzzles/singleton_top_layer.py + chia/wallet/puzzles/tails.py + chia/wallet/rl_wallet/rl_wallet.py + chia/wallet/sign_coin_spends.py + chia/wallet/trade_manager.py + chia/wallet/trade_record.py + chia/wallet/trading/offer.py + chia/wallet/trading/trade_store.py + chia/wallet/transaction_record.py + chia/wallet/util/compute_hints.py + chia/wallet/util/compute_memos.py + chia/wallet/util/debug_spend_bundle.py + chia/wallet/util/puzzle_compression.py + chia/wallet/util/wallet_sync_utils.py + chia/wallet/wallet_blockchain.py + chia/wallet/wallet_coin_store.py + chia/wallet/wallet_interested_store.py + chia/wallet/wallet_node_api.py + chia/wallet/wallet_node.py + chia/wallet/wallet_pool_store.py + chia/wallet/wallet.py + chia/wallet/wallet_state_manager.py + chia/wallet/wallet_weight_proof_handler.py + installhelper.py + tests/blockchain/test_blockchain.py + tests/blockchain/test_blockchain_transactions.py + tests/block_tools.py + tests/build-init-files.py + tests/build-workflows.py + tests/clvm/benchmark_costs.py + tests/clvm/coin_store.py + tests/clvm/test_chialisp_deserialization.py + tests/clvm/test_program.py + tests/clvm/test_puzzle_compression.py + tests/clvm/test_serialized_program.py + tests/clvm/test_singletons.py + tests/clvm/test_spend_sim.py + tests/conftest.py + tests/core/cmds/test_keys.py + tests/core/custom_types/test_coin.py + tests/core/custom_types/test_spend_bundle.py + tests/core/daemon/test_daemon.py + tests/core/full_node/full_sync/test_full_sync.py + tests/core/full_node/ram_db.py + tests/core/full_node/test_block_height_map.py + tests/core/full_node/test_block_store.py + tests/core/full_node/test_coin_store.py + tests/core/full_node/test_conditions.py + tests/core/full_node/test_full_node.py + tests/core/full_node/test_full_node_store.py + tests/core/full_node/test_hint_store.py + tests/core/full_node/test_mempool_performance.py + tests/core/full_node/test_mempool.py + tests/core/full_node/test_performance.py + tests/core/server/test_dos.py + tests/core/server/test_rate_limits.py + tests/core/ssl/test_ssl.py + tests/core/test_daemon_rpc.py + tests/core/test_db_conversion.py + tests/core/test_farmer_harvester_rpc.py + tests/core/test_filter.py + tests/core/test_full_node_rpc.py + tests/core/util/test_cached_bls.py + tests/core/util/test_config.py + tests/core/util/test_file_keyring_synchronization.py + tests/core/util/test_files.py + tests/core/util/test_keychain.py + tests/core/util/test_keyring_wrapper.py + tests/core/util/test_streamable.py + tests/generator/test_compression.py + tests/generator/test_generator_types.py + tests/generator/test_list_to_batches.py + tests/generator/test_rom.py + tests/generator/test_scan.py + tests/plotting/test_plot_manager.py + tests/plotting/util.py + tests/pools/test_pool_cmdline.py + tests/pools/test_pool_config.py + tests/pools/test_pool_puzzles_lifecycle.py + tests/pools/test_pool_rpc.py + tests/pools/test_wallet_pool_store.py + tests/setup_nodes.py + tests/simulation/test_simulation.py + tests/util/benchmark_cost.py + tests/util/blockchain.py + tests/util/build_network_protocol_files.py + tests/util/db_connection.py + tests/util/keyring.py + tests/util/key_tool.py + tests/util/misc.py + tests/util/network_protocol_data.py + tests/util/network.py + tests/util/test_lock_queue.py + tests/util/test_network_protocol_files.py + tests/util/test_struct_stream.py + tests/wallet/cat_wallet/test_cat_lifecycle.py + tests/wallet/cat_wallet/test_cat_wallet.py + tests/wallet/cat_wallet/test_offer_lifecycle.py + tests/wallet/did_wallet/test_did.py + tests/wallet/did_wallet/test_did_rpc.py + tests/wallet/rpc/test_wallet_rpc.py + tests/wallet/simple_sync/test_simple_sync_protocol.py + tests/wallet/test_singleton_lifecycle_fast.py + tests/wallet/test_singleton_lifecycle.py + tests/wallet/test_singleton.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.py + tests/wallet_tools.py + tests/weight_proof/test_weight_proof.py + tools/analyze-chain.py + tools/run_block.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4a75769c9331..253408f85bda 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,6 +14,27 @@ repos: language: python pass_filenames: false additional_dependencies: [click~=7.1] +# The following, commented hook is the usual way to add isort. However, it doesn't work in some environments. +# See https://github.com/PyCQA/isort/issues/1874#issuecomment-1002212936 +# ----------------------------------------------------- +# - repo: https://github.com/pycqa/isort +# rev: 5.9.3 +# hooks: +# - id: isort +# ----------------------------------------------------- +# The hook below is the workaround for the issue above. +- repo: local + hooks: + - id: isort + name: isort + entry: isort + require_serial: true + language: python + language_version: python3 + types_or: [cython, pyi, python] + args: ['--filter-files'] + minimum_pre_commit_version: '2.9.2' + additional_dependencies: [isort==5.10.1] - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.0.1 hooks: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d1b41b89bf19..96d847948702 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -58,6 +58,7 @@ py.test tests -v --durations 0 The [black library](https://black.readthedocs.io/en/stable/) is used as an automatic style formatter to make things easier. The [flake8 library](https://readthedocs.org/projects/flake8/) helps ensure consistent style. The [Mypy library](https://mypy.readthedocs.io/en/stable/) is very useful for ensuring objects are of the correct type, so try to always add the type of the return value, and the type of local variables. +The [isort library](https://isort.readthedocs.io) is used to sort, group and validate imports in all python files. If you want verbose logging for tests, edit the `tests/pytest.ini` file. diff --git a/benchmarks/block_ref.py b/benchmarks/block_ref.py index 84a80edc7245..91fb98c1c26e 100644 --- a/benchmarks/block_ref.py +++ b/benchmarks/block_ref.py @@ -1,13 +1,13 @@ -import click -import aiosqlite import asyncio -import time -import random import os - -from typing import Optional, List -from pathlib import Path +import random +import time from dataclasses import dataclass +from pathlib import Path +from typing import List, Optional + +import aiosqlite +import click from chia.consensus.blockchain import Blockchain from chia.consensus.default_constants import DEFAULT_CONSTANTS diff --git a/chia/consensus/blockchain.py b/chia/consensus/blockchain.py index d9fda3219177..ddfdc1ccc169 100644 --- a/chia/consensus/blockchain.py +++ b/chia/consensus/blockchain.py @@ -29,6 +29,7 @@ from chia.full_node.coin_store import CoinStore from chia.full_node.hint_store import HintStore from chia.full_node.mempool_check_conditions import get_name_puzzle_conditions +from chia.types.block_protocol import BlockInfo from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.program import SerializedProgram from chia.types.blockchain_format.sized_bytes import bytes32 @@ -47,7 +48,6 @@ from chia.util.generator_tools import get_block_header, tx_removals_and_additions from chia.util.ints import uint16, uint32, uint64, uint128 from chia.util.streamable import recurse_jsonify -from chia.types.block_protocol import BlockInfo log = logging.getLogger(__name__) diff --git a/chia/wallet/util/peer_request_cache.py b/chia/wallet/util/peer_request_cache.py index 12869ba04f53..c26f00842c36 100644 --- a/chia/wallet/util/peer_request_cache.py +++ b/chia/wallet/util/peer_request_cache.py @@ -1,4 +1,4 @@ -from typing import Dict, Tuple, Any, Optional, List +from typing import Any, Dict, List, Optional, Tuple from chia.protocols.wallet_protocol import CoinState from chia.types.blockchain_format.sized_bytes import bytes32 diff --git a/setup.py b/setup.py index c7d885a8c3d3..1764151e3452 100644 --- a/setup.py +++ b/setup.py @@ -46,6 +46,7 @@ "pytest-asyncio", "pytest-monitor; sys_platform == 'linux'", "pytest-xdist", + "isort", "flake8", "mypy", # TODO: black 22.1.0 requires click>=8, remove this pin after updating to click 8 diff --git a/tests/tools/test_full_sync.py b/tests/tools/test_full_sync.py index a2616d4c0e2f..e3d0842ce79e 100644 --- a/tests/tools/test_full_sync.py +++ b/tests/tools/test_full_sync.py @@ -1,10 +1,11 @@ #!/usr/bin/env python3 import asyncio -from tools.test_full_sync import run_sync_test import os from pathlib import Path +from tools.test_full_sync import run_sync_test + def test_full_sync_test(): file_path = os.path.realpath(__file__) diff --git a/tests/wallet/test_wallet_user_store.py b/tests/wallet/test_wallet_user_store.py index dadd8fb634c7..399557852bea 100644 --- a/tests/wallet/test_wallet_user_store.py +++ b/tests/wallet/test_wallet_user_store.py @@ -1,10 +1,10 @@ from pathlib import Path + import aiosqlite import pytest from chia.util.db_wrapper import DBWrapper from chia.wallet.util.wallet_types import WalletType - from chia.wallet.wallet_user_store import WalletUserStore diff --git a/tools/test_full_sync.py b/tools/test_full_sync.py index 9387171d4e17..3b8716aa215f 100755 --- a/tools/test_full_sync.py +++ b/tools/test_full_sync.py @@ -1,24 +1,23 @@ #!/usr/bin/env python3 import asyncio -import aiosqlite -import zstd -import click -import logging import cProfile -from typing import Iterator - -from pathlib import Path -import time +import logging import tempfile +import time from contextlib import contextmanager +from pathlib import Path +from typing import Iterator -from chia.types.full_block import FullBlock -from chia.consensus.default_constants import DEFAULT_CONSTANTS -from chia.util.config import load_config -from chia.full_node.full_node import FullNode +import aiosqlite +import click +import zstd from chia.cmds.init_funcs import chia_init +from chia.consensus.default_constants import DEFAULT_CONSTANTS +from chia.full_node.full_node import FullNode +from chia.types.full_block import FullBlock +from chia.util.config import load_config class ExitOnError(logging.Handler): @@ -128,8 +127,8 @@ def run(file: Path, db_version: int, profile: bool) -> None: @main.command("analyze", short_help="generate call stacks for all profiles dumped to current directory") def analyze() -> None: - from shlex import quote from glob import glob + from shlex import quote from subprocess import check_call for input_file in glob("slow-batch-*.profile"): From 605e1c9da4fc02fee62b93730575c1a986c4fed6 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Tue, 15 Feb 2022 10:38:57 -0500 Subject: [PATCH 049/378] Debug PEP 440 local versions all of a sudden (#10241) * empty * Ban setuptools 60.9.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a34b6b5e98c2..ff1b799bc7ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=4.1.2"] +requires = ["setuptools>=42,!=60.9.1", "wheel", "setuptools_scm[toml]>=4.1.2"] build-backend = "setuptools.build_meta" [tool.setuptools_scm] From c7ea56114d6302ff73b91a001be12147e3a6e03d Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Tue, 15 Feb 2022 11:23:21 -0700 Subject: [PATCH 050/378] Remove unnecessary uint64 cast (#10235) --- chia/full_node/mempool_manager.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/chia/full_node/mempool_manager.py b/chia/full_node/mempool_manager.py index 463e1b65cce5..2996f81b8b17 100644 --- a/chia/full_node/mempool_manager.py +++ b/chia/full_node/mempool_manager.py @@ -304,7 +304,7 @@ async def add_spendbundle( for add in additions: additions_dict[add.name()] = add - addition_amount = uint64(0) + addition_amount: int = 0 # Check additions for max coin amount for coin in additions: if coin.amount < 0: @@ -319,7 +319,7 @@ async def add_spendbundle( MempoolInclusionStatus.FAILED, Err.COIN_AMOUNT_EXCEEDS_MAXIMUM, ) - addition_amount = uint64(addition_amount + coin.amount) + addition_amount = addition_amount + coin.amount # Check for duplicate outputs addition_counter = collections.Counter(_.name() for _ in additions) for k, v in addition_counter.items(): @@ -336,7 +336,7 @@ async def add_spendbundle( removal_record_dict: Dict[bytes32, CoinRecord] = {} removal_coin_dict: Dict[bytes32, Coin] = {} - removal_amount = uint64(0) + removal_amount: int = 0 for name in removal_names: removal_record = await self.coin_store.get_coin_record(name) if removal_record is None and name not in additions_dict: @@ -359,7 +359,7 @@ async def add_spendbundle( ) assert removal_record is not None - removal_amount = uint64(removal_amount + removal_record.coin.amount) + removal_amount = removal_amount + removal_record.coin.amount removal_record_dict[name] = removal_record removal_coin_dict[name] = removal_record.coin From 220e845d51fb9dc143867cf53d8cb11529121ee3 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Tue, 15 Feb 2022 15:22:23 -0500 Subject: [PATCH 051/378] shutdown the weight proof process pool (#10163) * shutdown the weight proof process pool * use a context manager for the weight proof process pool executor * record of the debug code * mostly cleaned up * suppress sync task cancellation propagation when awaited while closing * breakup multi-second WeightProofHandler.validate_weight_proof with async sleeps * move awaiting of sync task until after existing cancellation * properly handle shutdown file with a new instance each time and a context manager * cleanup --- chia/full_node/full_node.py | 6 +- chia/full_node/weight_proof.py | 123 +++++++++++++++------ chia/server/start_service.py | 14 ++- chia/wallet/wallet_weight_proof_handler.py | 2 +- 4 files changed, 105 insertions(+), 40 deletions(-) diff --git a/chia/full_node/full_node.py b/chia/full_node/full_node.py index 72e181611f1e..244802b00f73 100644 --- a/chia/full_node/full_node.py +++ b/chia/full_node/full_node.py @@ -1,4 +1,5 @@ import asyncio +import contextlib import dataclasses import logging import random @@ -729,9 +730,9 @@ def _close(self): self._transaction_queue_task.cancel() if hasattr(self, "_blockchain_lock_queue"): self._blockchain_lock_queue.close() + cancel_task_safe(task=self._sync_task, log=self.log) async def _await_closed(self): - cancel_task_safe(self._sync_task, self.log) for task_id, task in list(self.full_node_store.tx_fetch_tasks.items()): cancel_task_safe(task, self.log) await self.connection.close() @@ -739,6 +740,9 @@ async def _await_closed(self): await asyncio.wait([self._init_weight_proof]) if hasattr(self, "_blockchain_lock_queue"): await self._blockchain_lock_queue.await_closed() + if self._sync_task is not None: + with contextlib.suppress(asyncio.CancelledError): + await self._sync_task async def _sync(self): """ diff --git a/chia/full_node/weight_proof.py b/chia/full_node/weight_proof.py index 0b09ce2f4a79..64a3c36c051f 100644 --- a/chia/full_node/weight_proof.py +++ b/chia/full_node/weight_proof.py @@ -5,7 +5,8 @@ import pathlib import random from concurrent.futures.process import ProcessPoolExecutor -from typing import Dict, List, Optional, Tuple +import tempfile +from typing import Dict, IO, List, Optional, Tuple from chia.consensus.block_header_validation import validate_finished_header_block from chia.consensus.block_record import BlockRecord @@ -43,6 +44,10 @@ log = logging.getLogger(__name__) +def _create_shutdown_file() -> IO: + return tempfile.NamedTemporaryFile(prefix="chia_full_node_weight_proof_handler_executor_shutdown_trigger") + + class WeightProofHandler: LAMBDA_L = 100 @@ -594,7 +599,13 @@ async def validate_weight_proof(self, weight_proof: WeightProof) -> Tuple[bool, peak_height = weight_proof.recent_chain_data[-1].reward_chain_block.height log.info(f"validate weight proof peak height {peak_height}") + # TODO: Consider if this can be spun off to a thread as an alternative to + # sprinkling async sleeps around. + + # timing reference: start summaries, sub_epoch_weight_list = _validate_sub_epoch_summaries(self.constants, weight_proof) + await asyncio.sleep(0) # break up otherwise multi-second sync code + # timing reference: 1 second if summaries is None: log.error("weight proof failed sub epoch data validation") return False, uint32(0), [] @@ -605,38 +616,65 @@ async def validate_weight_proof(self, weight_proof: WeightProof) -> Tuple[bool, log.error("failed weight proof sub epoch sample validation") return False, uint32(0), [] - executor = ProcessPoolExecutor(self._num_processes) - constants, summary_bytes, wp_segment_bytes, wp_recent_chain_bytes = vars_to_bytes( - self.constants, summaries, weight_proof - ) - - recent_blocks_validation_task = asyncio.get_running_loop().run_in_executor( - executor, _validate_recent_blocks, constants, wp_recent_chain_bytes, summary_bytes - ) - - segments_validated, vdfs_to_validate = _validate_sub_epoch_segments( - constants, rng, wp_segment_bytes, summary_bytes - ) - if not segments_validated: - return False, uint32(0), [] - - vdf_chunks = chunks(vdfs_to_validate, self._num_processes) - vdf_tasks = [] - for chunk in vdf_chunks: - byte_chunks = [] - for vdf_proof, classgroup, vdf_info in chunk: - byte_chunks.append((bytes(vdf_proof), bytes(classgroup), bytes(vdf_info))) - - vdf_task = asyncio.get_running_loop().run_in_executor(executor, _validate_vdf_batch, constants, byte_chunks) - vdf_tasks.append(vdf_task) - - for vdf_task in vdf_tasks: - validated = await vdf_task - if not validated: - return False, uint32(0), [] + # timing reference: 1 second + # TODO: Consider implementing an async polling closer for the executor. + with ProcessPoolExecutor(max_workers=self._num_processes) as executor: + # The shutdown file manager must be inside of the executor manager so that + # we request the workers close prior to waiting for them to close. + with _create_shutdown_file() as shutdown_file: + await asyncio.sleep(0) # break up otherwise multi-second sync code + # timing reference: 1.1 second + constants, summary_bytes, wp_segment_bytes, wp_recent_chain_bytes = vars_to_bytes( + self.constants, summaries, weight_proof + ) + await asyncio.sleep(0) # break up otherwise multi-second sync code + + # timing reference: 2 second + recent_blocks_validation_task = asyncio.get_running_loop().run_in_executor( + executor, + _validate_recent_blocks, + constants, + wp_recent_chain_bytes, + summary_bytes, + pathlib.Path(shutdown_file.name), + ) - valid_recent_blocks_task = recent_blocks_validation_task - valid_recent_blocks = await valid_recent_blocks_task + # timing reference: 2 second + segments_validated, vdfs_to_validate = _validate_sub_epoch_segments( + constants, rng, wp_segment_bytes, summary_bytes + ) + await asyncio.sleep(0) # break up otherwise multi-second sync code + if not segments_validated: + return False, uint32(0), [] + + # timing reference: 4 second + vdf_chunks = chunks(vdfs_to_validate, self._num_processes) + vdf_tasks = [] + # timing reference: 4 second + for chunk in vdf_chunks: + byte_chunks = [] + for vdf_proof, classgroup, vdf_info in chunk: + byte_chunks.append((bytes(vdf_proof), bytes(classgroup), bytes(vdf_info))) + + vdf_task = asyncio.get_running_loop().run_in_executor( + executor, + _validate_vdf_batch, + constants, + byte_chunks, + pathlib.Path(shutdown_file.name), + ) + vdf_tasks.append(vdf_task) + # give other stuff a turn + await asyncio.sleep(0) + + # timing reference: 4 second + for vdf_task in asyncio.as_completed(fs=vdf_tasks): + validated = await vdf_task + if not validated: + return False, uint32(0), [] + + valid_recent_blocks_task = recent_blocks_validation_task + valid_recent_blocks = await valid_recent_blocks_task if not valid_recent_blocks: log.error("failed validating weight proof recent blocks") return False, uint32(0), [] @@ -1294,10 +1332,20 @@ def validate_recent_blocks( return True, [bytes(sub) for sub in sub_blocks._block_records.values()] -def _validate_recent_blocks(constants_dict: Dict, recent_chain_bytes: bytes, summaries_bytes: List[bytes]) -> bool: +def _validate_recent_blocks( + constants_dict: Dict, + recent_chain_bytes: bytes, + summaries_bytes: List[bytes], + shutdown_file_path: Optional[pathlib.Path] = None, +) -> bool: constants, summaries = bytes_to_vars(constants_dict, summaries_bytes) recent_chain: RecentChainData = RecentChainData.from_bytes(recent_chain_bytes) - success, records = validate_recent_blocks(constants, recent_chain, summaries) + success, records = validate_recent_blocks( + constants=constants, + recent_chain=recent_chain, + summaries=summaries, + shutdown_file_path=shutdown_file_path, + ) return success @@ -1309,7 +1357,12 @@ def _validate_recent_blocks_and_get_records( ) -> Tuple[bool, List[bytes]]: constants, summaries = bytes_to_vars(constants_dict, summaries_bytes) recent_chain: RecentChainData = RecentChainData.from_bytes(recent_chain_bytes) - return validate_recent_blocks(constants, recent_chain, summaries, shutdown_file_path) + return validate_recent_blocks( + constants=constants, + recent_chain=recent_chain, + summaries=summaries, + shutdown_file_path=shutdown_file_path, + ) def _validate_pospace_recent_chain( diff --git a/chia/server/start_service.py b/chia/server/start_service.py index 8a2bb2bb78c6..68cfb5d7238a 100644 --- a/chia/server/start_service.py +++ b/chia/server/start_service.py @@ -1,4 +1,5 @@ import asyncio +import functools import os import logging import logging.config @@ -181,13 +182,20 @@ def _enable_signals(self) -> None: global main_pid main_pid = os.getpid() - signal.signal(signal.SIGINT, self._accept_signal) - signal.signal(signal.SIGTERM, self._accept_signal) + loop = asyncio.get_running_loop() + loop.add_signal_handler( + signal.SIGINT, + functools.partial(self._accept_signal, signal_number=signal.SIGINT), + ) + loop.add_signal_handler( + signal.SIGTERM, + functools.partial(self._accept_signal, signal_number=signal.SIGTERM), + ) if platform == "win32" or platform == "cygwin": # pylint: disable=E1101 signal.signal(signal.SIGBREAK, self._accept_signal) # type: ignore - def _accept_signal(self, signal_number: int, stack_frame): + def _accept_signal(self, signal_number: int, stack_frame=None): self._log.info(f"got signal {signal_number}") # we only handle signals in the main process. In the ProcessPoolExecutor diff --git a/chia/wallet/wallet_weight_proof_handler.py b/chia/wallet/wallet_weight_proof_handler.py index 7e3664644a1d..1a44ca1a2dcd 100644 --- a/chia/wallet/wallet_weight_proof_handler.py +++ b/chia/wallet/wallet_weight_proof_handler.py @@ -29,7 +29,7 @@ def _create_shutdown_file() -> IO: - return tempfile.NamedTemporaryFile(prefix="chia_executor_shutdown_trigger") + return tempfile.NamedTemporaryFile(prefix="chia_wallet_weight_proof_handler_executor_shutdown_trigger") class WalletWeightProofHandler: From 20a7078960a62c399229c0df60eda26e0bac3a8f Mon Sep 17 00:00:00 2001 From: Chris Marslender Date: Tue, 15 Feb 2022 16:27:31 -0600 Subject: [PATCH 052/378] Update the GHA build for intel mac to work as the primary release workflow (#10232) * Update the GHA build for intel mac to work as the primary release workflow * Add dev hash logic to GHA version of intel mac installer --- .github/workflows/build-macos-installer.yml | 110 +++++++++++++++++--- 1 file changed, 94 insertions(+), 16 deletions(-) diff --git a/.github/workflows/build-macos-installer.yml b/.github/workflows/build-macos-installer.yml index 99c53f7baf55..aa01c7f58b60 100644 --- a/.github/workflows/build-macos-installer.yml +++ b/.github/workflows/build-macos-installer.yml @@ -1,4 +1,4 @@ -name: MacOS installer on Catalina and Python 3.8 +name: MacOS Intel installer on Python 3.9 on: push: @@ -17,7 +17,7 @@ concurrency: jobs: build: - name: MacOS installer on Catalina and Python 3.8 + name: MacOS Intel Installer on Python 3.9 runs-on: ${{ matrix.os }} timeout-minutes: 40 strategy: @@ -39,6 +39,31 @@ jobs: - name: Cleanup any leftovers that exist from previous runs run: bash build_scripts/clean-runner.sh || true + - name: Test for secrets access + id: check_secrets + shell: bash + run: | + unset HAS_APPLE_SECRET + unset HAS_AWS_SECRET + + if [ -n "$APPLE_SECRET" ]; then HAS_APPLE_SECRET='true' ; fi + echo ::set-output name=HAS_APPLE_SECRET::${HAS_APPLE_SECRET} + + if [ -n "$AWS_SECRET" ]; then HAS_AWS_SECRET='true' ; fi + echo ::set-output name=HAS_AWS_SECRET::${HAS_AWS_SECRET} + env: + APPLE_SECRET: "${{ secrets.APPLE_DEV_ID_APP }}" + AWS_SECRET: "${{ secrets.INSTALLER_UPLOAD_KEY }}" + + - name: Create installer version number + id: version_number + run: | + python3 -m venv ../venv + . ../venv/bin/activate + pip install setuptools_scm + echo "::set-output name=CHIA_INSTALLER_VERSION::$(python3 ./build_scripts/installer-version.py)" + deactivate + - name: Setup Python environment uses: actions/setup-python@v2 with: @@ -71,18 +96,8 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Test for secrets access - id: check_secrets - shell: bash - run: | - unset HAS_SECRET - if [ -n "$SECRET" ]; then HAS_SECRET='true' ; fi - echo ::set-output name=HAS_SECRET::${HAS_SECRET} - env: - SECRET: "${{ secrets.APPLE_DEV_ID_APP }}" - - name: Import Apple app signing certificate - if: steps.check_secrets.outputs.HAS_SECRET + if: steps.check_secrets.outputs.HAS_APPLE_SECRET uses: Apple-Actions/import-codesign-certs@v1 with: p12-file-base64: ${{ secrets.APPLE_DEV_ID_APP }} @@ -121,9 +136,9 @@ jobs: with: node-version: '16.x' - - name: Build MacOS DMG in Catalina + - name: Build MacOS DMG env: - NOTARIZE: ${{ steps.check_secrets.outputs.HAS_SECRET }} + NOTARIZE: ${{ steps.check_secrets.outputs.HAS_APPLE_SECRET }} APPLE_NOTARIZE_USERNAME: "${{ secrets.APPLE_NOTARIZE_USERNAME }}" APPLE_NOTARIZE_PASSWORD: "${{ secrets.APPLE_NOTARIZE_PASSWORD }}" run: | @@ -136,5 +151,68 @@ jobs: - name: Upload MacOS artifacts uses: actions/upload-artifact@v2 with: - name: Chia-Installer-on-MacOS-10.15-dmg + name: Chia-Installer-MacOS-intel-dmg path: ${{ github.workspace }}/build_scripts/final_installer/ + + - name: Create Checksums + run: | + ls + shasum -a 256 ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg > ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg.sha256 + + - name: Upload to s3 + if: steps.check_secrets.outputs.HAS_AWS_SECRET + env: + AWS_ACCESS_KEY_ID: ${{ secrets.INSTALLER_UPLOAD_KEY }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.INSTALLER_UPLOAD_SECRET }} + AWS_REGION: us-west-2 + CHIA_INSTALLER_VERSION: ${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }} + run: | + GIT_SHORT_HASH=$(echo "${GITHUB_SHA}" | cut -c1-8) + CHIA_DEV_BUILD=${CHIA_INSTALLER_VERSION}-$GIT_SHORT_HASH + echo "CHIA_DEV_BUILD=$CHIA_DEV_BUILD" >>$GITHUB_ENV + aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg s3://download-chia-net/builds/Chia-${CHIA_DEV_BUILD}.dmg + + - name: Install py3createtorrent + if: startsWith(github.ref, 'refs/tags/') + run: | + pip install py3createtorrent + + - name: Create torrent + if: startsWith(github.ref, 'refs/tags/') + run: | + py3createtorrent -f -t udp://tracker.opentrackr.org:1337/announce ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg -o ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg.torrent --webseed https://download-chia-net.s3.us-west-2.amazonaws.com/install/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg + ls ${{ github.workspace }}/build_scripts/final_installer/ + + - name: Upload Beta Installer + if: steps.check_secrets.outputs.HAS_AWS_SECRET && github.ref == 'refs/heads/main' + env: + CHIA_INSTALLER_VERSION: ${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }} + AWS_ACCESS_KEY_ID: ${{ secrets.INSTALLER_UPLOAD_KEY }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.INSTALLER_UPLOAD_SECRET }} + AWS_REGION: us-west-2 + run: | + aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg s3://download-chia-net/beta/Chia_latest_beta.dmg + aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg.sha256 s3://download-chia-net/beta/Chia_latest_beta.dmg.sha256 + + - name: Upload Release Files + if: steps.check_secrets.outputs.HAS_AWS_SECRET && startsWith(github.ref, 'refs/tags/') + env: + AWS_ACCESS_KEY_ID: ${{ secrets.INSTALLER_UPLOAD_KEY }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.INSTALLER_UPLOAD_SECRET }} + AWS_REGION: us-west-2 + run: | + aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg s3://download-chia-net/install/ + aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg.sha256 s3://download-chia-net/install/ + aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg.torrent s3://download-chia-net/torrents/ + + - name: Get tag name + if: startsWith(github.ref, 'refs/tags/') + id: tag-name + run: | + echo "::set-output name=TAG_NAME::$(echo ${{ github.ref }} | cut -d'/' -f 3)" + echo "::set-output name=REPO_NAME::$(echo ${{ github.repository }} | cut -d'/' -f 2)" + + - name: Mark installer complete + if: startsWith(github.ref, 'refs/tags/') + run: | + curl -s -XPOST -H "Authorization: Bearer ${{ secrets.GLUE_ACCESS_TOKEN }}" --data '{"chia_ref": "${{ steps.tag-name.outputs.TAG_NAME }}"}' ${{ secrets.GLUE_API_URL }}/api/v1/${{ steps.tag-name.outputs.REPO_NAME }}/${{ steps.tag-name.outputs.TAG_NAME }}/success/build-macos From 7df694ffe39efcc19e4df76d25baf5f1a62e9720 Mon Sep 17 00:00:00 2001 From: Gene Hoffman <30377676+hoffmang9@users.noreply.github.com> Date: Tue, 15 Feb 2022 14:27:41 -0800 Subject: [PATCH 053/378] Remove Azure MacOS CI build (#10231) --- azure-pipelines.yml | 155 -------------------------------------------- 1 file changed, 155 deletions(-) delete mode 100644 azure-pipelines.yml diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index f9c401ce0b27..000000000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,155 +0,0 @@ -# Python package -# Create and test a Python package on multiple Python versions. -# Add steps that analyze code, save the dist with the build record, publish to a PyPI-compatible index, and more: -# https://docs.microsoft.com/azure/devops/pipelines/languages/python - -trigger: - branches: - include: - - "*" - tags: - include: - - "*" - -pool: - vmImage: "macOS-10.15" -strategy: - matrix: - Mojave DMG: - python.version: "3.9" - -variables: - - group: Apple-Notarize-Variables - - group: AWS-Upload-Credentials - - group: GlueVariables - - group: GitHub - -steps: - - checkout: self # self represents the repo where the initial Azure Pipelines YAML file was found - submodules: recursive - fetchDepth: 0 - displayName: "Checkout code" - - - script: | - git config --global url."https://github.com/".insteadOf ssh://git@github.com/ - displayName: "Use https for git instead of ssh" - - - task: UsePythonVersion@0 - inputs: - versionSpec: "$(python.version)" - displayName: "Use Python $(python.version)" - - # Install Apple certificate - # Install an Apple certificate required to build on a macOS agent machine - - task: InstallAppleCertificate@2 - inputs: - certSecureFile: 'chia-apple-dev-id-app.p12' - certPwd: $(CHIA_APPLE_DEV_ID_APP_PASS) - keychain: temp - deleteCert: true - - - script: | - python3 -m venv ../venv - . ../venv/bin/activate - pip install setuptools_scm - touch $(System.DefaultWorkingDirectory)/build_scripts/version.txt - python ./build_scripts/installer-version.py > $(System.DefaultWorkingDirectory)/build_scripts/version.txt - cat $(System.DefaultWorkingDirectory)/build_scripts/version.txt - deactivate - displayName: Create installer version number - - - script: | - set -o errexit -o pipefail - MADMAX_VERSION=$(curl -u "$(GithubUsername):$(GithubToken)" --fail --silent "https://api.github.com/repos/Chia-Network/chia-plotter-madmax/releases/latest" | jq -r '.tag_name') - - mkdir "$(System.DefaultWorkingDirectory)/madmax" - wget -O "$(System.DefaultWorkingDirectory)/madmax/chia_plot" https://github.com/Chia-Network/chia-plotter-madmax/releases/download/${MADMAX_VERSION}/chia_plot-${MADMAX_VERSION}-macos-intel - wget -O "$(System.DefaultWorkingDirectory)/madmax/chia_plot_k34" https://github.com/Chia-Network/chia-plotter-madmax/releases/download/${MADMAX_VERSION}/chia_plot_k34-${MADMAX_VERSION}-macos-intel - chmod +x "$(System.DefaultWorkingDirectory)/madmax/chia_plot" - chmod +x "$(System.DefaultWorkingDirectory)/madmax/chia_plot_k34" - displayName: "Get latest madmax release" - - - script: | - sh install.sh - displayName: "Install dependencies" - - - task: NodeTool@0 - inputs: - versionSpec: '16.x' - displayName: "Setup Node 16.x" - - - bash: | - . ./activate - APPLE_NOTARIZE_USERNAME="$(APPLE_NOTARIZE_USERNAME)" - export APPLE_NOTARIZE_USERNAME - APPLE_NOTARIZE_PASSWORD="$(APPLE_NOTARIZE_PASSWORD)" - export APPLE_NOTARIZE_PASSWORD - if [ "$(APPLE_NOTARIZE_PASSWORD)" ]; then NOTARIZE="true"; else NOTARIZE="false"; fi - export NOTARIZE - cd build_scripts || exit - sh build_macos.sh - displayName: "Build DMG with build_scripts/build_macos.sh" - - - task: PublishPipelineArtifact@1 - inputs: - targetPath: $(System.DefaultWorkingDirectory)/build_scripts/final_installer/ - artifactName: MacOS-DMG - displayName: "Upload MacOS DMG" - - - bash: | - ls $(System.DefaultWorkingDirectory)/build_scripts/final_installer/ - cd $(System.DefaultWorkingDirectory)/build_scripts/ - export CHIA_VERSION="Chia-"$( $(System.DefaultWorkingDirectory)/build_scripts/final_installer/$CHIA_VERSION.dmg.sha256 - ls $(System.DefaultWorkingDirectory)/build_scripts/final_installer/ - displayName: "Create Checksums" - - - bash: | - export AWS_ACCESS_KEY_ID=$(AccessKey) - export AWS_SECRET_ACCESS_KEY=$(SecretKey) - export AWS_DEFAULT_REGION=us-west-2 - export GIT_SHORT_HASH=$(git rev-parse --short HEAD) - export CHIA_DEV_BUILD=$(basename $(ls $(System.DefaultWorkingDirectory)/build_scripts/final_installer/*.dmg | sed "s/.dmg/-$GIT_SHORT_HASH.dmg/")) - aws s3 cp $(System.DefaultWorkingDirectory)/build_scripts/final_installer/*.dmg s3://download-chia-net/dev/$CHIA_DEV_BUILD - displayName: "Upload to S3" - - - bash: | - cd $(System.DefaultWorkingDirectory)/build_scripts/ - export CHIA_VERSION="Chia-"$( Date: Tue, 15 Feb 2022 15:42:07 -0700 Subject: [PATCH 054/378] Announcement hash should never be None (#8965) * Announcement hash should never be None * Allow origin ID to be anywhere in the coin set * mypy * Change to for/else format * Fix comment Co-authored-by: Kyle Altendorf * Style change to loop * tiny bad merge * Add test * Lint Co-authored-by: Kyle Altendorf --- chia/wallet/wallet.py | 25 +++++++++++---- tests/wallet/test_wallet.py | 61 +++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 6 deletions(-) diff --git a/chia/wallet/wallet.py b/chia/wallet/wallet.py index 795ed5100ffd..583707f35da4 100644 --- a/chia/wallet/wallet.py +++ b/chia/wallet/wallet.py @@ -361,10 +361,9 @@ async def _generate_unsigned_transaction( memos = [] assert memos is not None for coin in coins: - self.log.info(f"coin from coins: {coin.name()} {coin}") - puzzle: Program = await self.puzzle_for_puzzle_hash(coin.puzzle_hash) # Only one coin creates outputs - if primary_announcement_hash is None and origin_id in (None, coin.name()): + if origin_id in (None, coin.name()): + origin_id = coin.name() if primaries is None: if amount > 0: primaries = [{"puzzlehash": newpuzzlehash, "amount": uint64(amount), "memos": memos}] @@ -379,6 +378,7 @@ async def _generate_unsigned_transaction( for primary in primaries: message_list.append(Coin(coin.name(), primary["puzzlehash"], primary["amount"]).name()) message: bytes32 = std_hash(b"".join(message_list)) + puzzle: Program = await self.puzzle_for_puzzle_hash(coin.puzzle_hash) solution: Program = self.make_solution( primaries=primaries, fee=fee, @@ -387,10 +387,23 @@ async def _generate_unsigned_transaction( puzzle_announcements_to_assert=puzzle_announcements_bytes, ) primary_announcement_hash = Announcement(coin.name(), message).name() - else: - assert primary_announcement_hash is not None - solution = self.make_solution(coin_announcements_to_assert={primary_announcement_hash}, primaries=[]) + spends.append( + CoinSpend( + coin, SerializedProgram.from_bytes(bytes(puzzle)), SerializedProgram.from_bytes(bytes(solution)) + ) + ) + break + else: + raise ValueError("origin_id is not in the set of selected coins") + + # Process the non-origin coins now that we have the primary announcement hash + for coin in coins: + if coin.name() == origin_id: + continue + + puzzle = await self.puzzle_for_puzzle_hash(coin.puzzle_hash) + solution = self.make_solution(coin_announcements_to_assert={primary_announcement_hash}, primaries=[]) spends.append( CoinSpend( coin, SerializedProgram.from_bytes(bytes(puzzle)), SerializedProgram.from_bytes(bytes(solution)) diff --git a/tests/wallet/test_wallet.py b/tests/wallet/test_wallet.py index 130905652937..092a03aeffb2 100644 --- a/tests/wallet/test_wallet.py +++ b/tests/wallet/test_wallet.py @@ -774,3 +774,64 @@ async def test_address_sliding_window(self, wallet_node_100_pk, trusted): await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(puzzle_hashes[209])) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0")) await time_out_assert(15, wallet.get_confirmed_balance, 12 * 10 ** 12) + + @pytest.mark.parametrize( + "trusted", + [True, False], + ) + @pytest.mark.asyncio + async def test_wallet_transaction_options(self, two_wallet_nodes, trusted): + num_blocks = 5 + full_nodes, wallets = two_wallet_nodes + full_node_api = full_nodes[0] + server_1 = full_node_api.full_node.server + wallet_node, server_2 = wallets[0] + wallet_node_2, server_3 = wallets[1] + wallet = wallet_node.wallet_state_manager.main_wallet + ph = await wallet.get_new_puzzlehash() + if trusted: + wallet_node.config["trusted_peers"] = {server_1.node_id.hex(): server_1.node_id.hex()} + wallet_node_2.config["trusted_peers"] = {server_1.node_id.hex(): server_1.node_id.hex()} + else: + wallet_node.config["trusted_peers"] = {} + wallet_node_2.config["trusted_peers"] = {} + + await server_2.start_client(PeerInfo(self_hostname, uint16(server_1._port)), None) + + for i in range(0, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(bytes32([0] * 32))) + + funds = sum( + [ + calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i)) + for i in range(1, num_blocks + 1) + ] + ) + + await time_out_assert(5, wallet.get_confirmed_balance, funds) + await time_out_assert(5, wallet.get_unconfirmed_balance, funds) + + AMOUNT_TO_SEND = 4000000000000 + coins = list(await wallet.select_coins(AMOUNT_TO_SEND)) + + tx = await wallet.generate_signed_transaction( + AMOUNT_TO_SEND, + await wallet_node_2.wallet_state_manager.main_wallet.get_new_puzzlehash(), + 0, + coins=coins, + origin_id=coins[2].name(), + ) + paid_coin = [coin for coin in tx.spend_bundle.additions() if coin.amount == AMOUNT_TO_SEND][0] + assert paid_coin.parent_coin_info == coins[2].name() + await wallet.push_transaction(tx) + + await time_out_assert(5, wallet.get_confirmed_balance, funds) + await time_out_assert(5, wallet.get_unconfirmed_balance, funds - AMOUNT_TO_SEND) + await time_out_assert(5, full_node_api.full_node.mempool_manager.get_spendbundle, tx.spend_bundle, tx.name) + + for i in range(0, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(bytes32([0] * 32))) + + await time_out_assert(5, wallet.get_confirmed_balance, funds - AMOUNT_TO_SEND) + await time_out_assert(5, wallet.get_unconfirmed_balance, funds - AMOUNT_TO_SEND) From 0464265734561121cab27899fa95f7ccdfa1349e Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Tue, 15 Feb 2022 23:42:51 +0100 Subject: [PATCH 055/378] use monotonic clock in benchmarks (#10242) --- benchmarks/block_ref.py | 8 +++---- benchmarks/block_store.py | 46 +++++++++++++++++++-------------------- benchmarks/coin_store.py | 26 +++++++++++----------- 3 files changed, 40 insertions(+), 40 deletions(-) diff --git a/benchmarks/block_ref.py b/benchmarks/block_ref.py index 91fb98c1c26e..cc5833400c04 100644 --- a/benchmarks/block_ref.py +++ b/benchmarks/block_ref.py @@ -1,9 +1,9 @@ import asyncio import os import random -import time from dataclasses import dataclass from pathlib import Path +from time import monotonic from typing import List, Optional import aiosqlite @@ -62,7 +62,7 @@ async def main(db_path: Path): hint_store = await HintStore.create(db_wrapper) coin_store = await CoinStore.create(db_wrapper) - start_time = time.time() + start_time = monotonic() # make configurable reserved_cores = 4 blockchain = await Blockchain.create( @@ -78,9 +78,9 @@ async def main(db_path: Path): random_refs(), ) - start_time = time.time() + start_time = monotonic() gen = await blockchain.get_block_generator(block) - one_call = time.time() - start_time + one_call = monotonic() - start_time timing += one_call assert gen is not None diff --git a/benchmarks/block_store.py b/benchmarks/block_store.py index a4c5a6f86545..172e37816e8d 100644 --- a/benchmarks/block_store.py +++ b/benchmarks/block_store.py @@ -1,6 +1,6 @@ import asyncio import random -from time import time +from time import monotonic from pathlib import Path from chia.full_node.block_store import BlockStore import os @@ -224,14 +224,14 @@ async def run_add_block_benchmark(version: int): sub_epoch_summary_included, ) - start = time() + start = monotonic() await block_store.add_full_block(header_hash, full_block, record) await block_store.set_in_chain([(header_hash,)]) header_hashes.append(header_hash) await block_store.set_peak(header_hash) await db_wrapper.db.commit() - stop = time() + stop = monotonic() total_time += stop - start # 19 seconds per block @@ -268,12 +268,12 @@ async def run_add_block_benchmark(version: int): print("profiling get_full_block") random.shuffle(header_hashes) - start = time() + start = monotonic() for h in header_hashes: block = await block_store.get_full_block(h) assert block.header_hash == h - stop = time() + stop = monotonic() total_time += stop - start print(f"{total_time:0.4f}s, get_full_block") @@ -283,12 +283,12 @@ async def run_add_block_benchmark(version: int): if verbose: print("profiling get_full_block_bytes") - start = time() + start = monotonic() for h in header_hashes: block = await block_store.get_full_block_bytes(h) assert len(block) > 0 - stop = time() + stop = monotonic() total_time += stop - start print(f"{total_time:0.4f}s, get_full_block_bytes") @@ -298,13 +298,13 @@ async def run_add_block_benchmark(version: int): if verbose: print("profiling get_full_blocks_at") - start = time() + start = monotonic() for h in range(1, block_height): blocks = await block_store.get_full_blocks_at([h]) assert len(blocks) == 1 assert blocks[0].height == h - stop = time() + stop = monotonic() total_time += stop - start print(f"{total_time:0.4f}s, get_full_blocks_at") @@ -314,13 +314,13 @@ async def run_add_block_benchmark(version: int): if verbose: print("profiling get_block_records_by_hash") - start = time() + start = monotonic() for h in header_hashes: blocks = await block_store.get_block_records_by_hash([h]) assert len(blocks) == 1 assert blocks[0].header_hash == h - stop = time() + stop = monotonic() total_time += stop - start print(f"{total_time:0.4f}s, get_block_records_by_hash") @@ -330,13 +330,13 @@ async def run_add_block_benchmark(version: int): if verbose: print("profiling get_blocks_by_hash") - start = time() + start = monotonic() for h in header_hashes: blocks = await block_store.get_blocks_by_hash([h]) assert len(blocks) == 1 assert blocks[0].header_hash == h - stop = time() + stop = monotonic() total_time += stop - start print(f"{total_time:0.4f}s, get_blocks_by_hash") @@ -346,12 +346,12 @@ async def run_add_block_benchmark(version: int): if verbose: print("profiling get_block_record") - start = time() + start = monotonic() for h in header_hashes: blocks = await block_store.get_block_record(h) assert blocks.header_hash == h - stop = time() + stop = monotonic() total_time += stop - start print(f"{total_time:0.4f}s, get_block_record") @@ -361,13 +361,13 @@ async def run_add_block_benchmark(version: int): if verbose: print("profiling get_block_records_in_range") - start = time() + start = monotonic() for i in range(100): h = random.randint(1, block_height - 100) blocks = await block_store.get_block_records_in_range(h, h + 99) assert len(blocks) == 100 - stop = time() + stop = monotonic() total_time += stop - start print(f"{total_time:0.4f}s, get_block_records_in_range") @@ -377,11 +377,11 @@ async def run_add_block_benchmark(version: int): if verbose: print("profiling get_block_records_close_to_peak") - start = time() + start = monotonic() blocks, peak = await block_store.get_block_records_close_to_peak(99) assert len(blocks) == 100 - stop = time() + stop = monotonic() total_time += stop - start print(f"{total_time:0.4f}s, get_block_records_close_to_peak") @@ -391,12 +391,12 @@ async def run_add_block_benchmark(version: int): if verbose: print("profiling is_fully_compactified") - start = time() + start = monotonic() for h in header_hashes: compactified = await block_store.is_fully_compactified(h) assert compactified is False - stop = time() + stop = monotonic() total_time += stop - start print(f"{total_time:0.4f}s, get_block_record") @@ -406,11 +406,11 @@ async def run_add_block_benchmark(version: int): if verbose: print("profiling get_random_not_compactified") - start = time() + start = monotonic() for i in range(1, 5000): blocks = await block_store.get_random_not_compactified(100) assert len(blocks) == 100 - stop = time() + stop = monotonic() total_time += stop - start print(f"{total_time:0.4f}s, get_random_not_compactified") diff --git a/benchmarks/coin_store.py b/benchmarks/coin_store.py index 1736916ef730..068f704b8577 100644 --- a/benchmarks/coin_store.py +++ b/benchmarks/coin_store.py @@ -1,6 +1,6 @@ import asyncio import random -from time import time +from time import monotonic from pathlib import Path from chia.full_node.coin_store import CoinStore from typing import List, Tuple @@ -109,7 +109,7 @@ async def run_new_block_benchmark(version: int): all_unspent = all_unspent[100:] total_remove += 100 - start = time() + start = monotonic() await coin_store.new_block( height, timestamp, @@ -118,7 +118,7 @@ async def run_new_block_benchmark(version: int): removals, ) await db_wrapper.db.commit() - stop = time() + stop = monotonic() # 19 seconds per block timestamp += 19 @@ -160,7 +160,7 @@ async def run_new_block_benchmark(version: int): all_unspent = all_unspent[700:] total_remove += 700 - start = time() + start = monotonic() await coin_store.new_block( height, timestamp, @@ -170,7 +170,7 @@ async def run_new_block_benchmark(version: int): ) await db_wrapper.db.commit() - stop = time() + stop = monotonic() # 19 seconds per block timestamp += 19 @@ -210,7 +210,7 @@ async def run_new_block_benchmark(version: int): all_unspent = all_unspent[2000:] total_remove += 2000 - start = time() + start = monotonic() await coin_store.new_block( height, timestamp, @@ -219,7 +219,7 @@ async def run_new_block_benchmark(version: int): removals, ) await db_wrapper.db.commit() - stop = time() + stop = monotonic() # 19 seconds per block timestamp += 19 @@ -242,9 +242,9 @@ async def run_new_block_benchmark(version: int): found_coins = 0 for i in range(NUM_ITERS): lookup = random.sample(all_coins, 200) - start = time() + start = monotonic() records = await coin_store.get_coin_records_by_names(True, lookup) - total_time += time() - start + total_time += monotonic() - start assert len(records) == 200 found_coins += len(records) if verbose: @@ -265,9 +265,9 @@ async def run_new_block_benchmark(version: int): found_coins = 0 for i in range(NUM_ITERS): lookup = random.sample(all_coins, 200) - start = time() + start = monotonic() records = await coin_store.get_coin_records_by_names(False, lookup) - total_time += time() - start + total_time += monotonic() - start assert len(records) <= 200 found_coins += len(records) if verbose: @@ -287,9 +287,9 @@ async def run_new_block_benchmark(version: int): total_time = 0 found_coins = 0 for i in range(1, block_height): - start = time() + start = monotonic() records = await coin_store.get_coins_removed_at_height(i) - total_time += time() - start + total_time += monotonic() - start found_coins += len(records) if verbose: print(".", end="") From f8a6c54e1a5b0a4de0cc0c41ea1b093d1b8483c5 Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 15 Feb 2022 14:43:50 -0800 Subject: [PATCH 056/378] Fix to allow debugging services when launched alongside the GUI (#10233) * Daemon RPCs `start_service` and `is_running` will additionally check self.connections to determine if the service is running. When launching services manually (e.g. when debugging), the GUI was attempting to have the daemon relaunch an already-running service. * Update chia/daemon/server.py Co-authored-by: Kyle Altendorf * Updates per feedback Co-authored-by: Kyle Altendorf --- chia/daemon/server.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/chia/daemon/server.py b/chia/daemon/server.py index ca03ef66116e..9ccf680c2bf7 100644 --- a/chia/daemon/server.py +++ b/chia/daemon/server.py @@ -1071,6 +1071,7 @@ async def start_service(self, request: Dict[str, Any]): error = None success = False testing = False + already_running = False if "testing" in request: testing = request["testing"] @@ -1084,9 +1085,17 @@ async def start_service(self, request: Dict[str, Any]): self.services.pop(service_command) error = None else: - error = f"Service {service_command} already running" - - if error is None: + self.log.info(f"Service {service_command} already running") + already_running = True + elif service_command in self.connections: + # If the service was started manually (not launched by the daemon), we should + # have a connection to it. + self.log.info(f"Service {service_command} already registered") + already_running = True + + if already_running: + success = True + elif error is None: try: exe_command = service_command if testing is True: @@ -1121,6 +1130,12 @@ async def is_running(self, request: Dict[str, Any]) -> Dict[str, Any]: else: process = self.services.get(service_name) is_running = process is not None and process.poll() is None + if not is_running: + # Check if we have a connection to the requested service. This might be the + # case if the service was started manually (i.e. not started by the daemon). + service_connections = self.connections.get(service_name) + if service_connections is not None: + is_running = len(service_connections) > 0 response = { "success": True, "service_name": service_name, From ce533879e25a536b6414307c4e6d850c1da06d4f Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Wed, 16 Feb 2022 17:01:04 +0100 Subject: [PATCH 057/378] run pytest in strict asyncio mode. add missing @pytest.mark.asyncio (#10230) --- .../blockchain/test_blockchain_transactions.py | 3 ++- tests/conftest.py | 17 +++++++++-------- tests/core/daemon/test_daemon.py | 13 +++++++------ .../core/full_node/full_sync/test_full_sync.py | 9 +++++---- tests/core/full_node/test_full_node.py | 11 ++++++----- tests/core/full_node/test_full_node_store.py | 5 +++-- tests/core/full_node/test_hint_store.py | 1 + tests/core/full_node/test_mempool.py | 3 ++- .../core/full_node/test_mempool_performance.py | 3 ++- tests/core/full_node/test_node_load.py | 3 ++- tests/core/full_node/test_performance.py | 3 ++- tests/core/full_node/test_transactions.py | 7 ++++--- tests/core/server/test_dos.py | 3 ++- tests/core/ssl/test_ssl.py | 9 +++++---- tests/core/test_daemon_rpc.py | 3 ++- tests/core/test_db_conversion.py | 3 ++- tests/core/test_farmer_harvester_rpc.py | 5 +++-- tests/core/test_filter.py | 3 ++- tests/core/test_full_node_rpc.py | 3 ++- tests/farmer_harvester/test_farmer_harvester.py | 3 ++- tests/pools/test_pool_rpc.py | 7 ++++--- tests/pytest.ini | 1 + tests/simulation/test_simulation.py | 5 +++-- tests/wallet/cat_wallet/test_cat_lifecycle.py | 3 ++- tests/wallet/cat_wallet/test_cat_wallet.py | 7 ++++--- tests/wallet/cat_wallet/test_offer_lifecycle.py | 3 ++- tests/wallet/cat_wallet/test_trades.py | 5 +++-- tests/wallet/did_wallet/test_did.py | 11 ++++++----- tests/wallet/did_wallet/test_did_rpc.py | 3 ++- tests/wallet/rl_wallet/test_rl_rpc.py | 3 ++- tests/wallet/rl_wallet/test_rl_wallet.py | 3 ++- tests/wallet/rpc/test_wallet_rpc.py | 3 ++- .../simple_sync/test_simple_sync_protocol.py | 5 +++-- tests/wallet/sync/test_wallet_sync.py | 7 ++++--- tests/wallet/test_wallet.py | 11 ++++++----- tests/wallet/test_wallet_blockchain.py | 3 ++- 36 files changed, 113 insertions(+), 77 deletions(-) diff --git a/tests/blockchain/test_blockchain_transactions.py b/tests/blockchain/test_blockchain_transactions.py index b8c26ed4f7fb..449773a4e28d 100644 --- a/tests/blockchain/test_blockchain_transactions.py +++ b/tests/blockchain/test_blockchain_transactions.py @@ -2,6 +2,7 @@ import logging import pytest +import pytest_asyncio from clvm.casts import int_to_bytes from chia.protocols import full_node_protocol, wallet_protocol @@ -31,7 +32,7 @@ def event_loop(): class TestBlockchainTransactions: - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def two_nodes(self, db_version): async for _ in setup_two_nodes(test_constants, db_version=db_version): yield _ diff --git a/tests/conftest.py b/tests/conftest.py index 0e0b1de2de69..dcd91b3e8008 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ import pytest +import pytest_asyncio import tempfile from pathlib import Path @@ -14,7 +15,7 @@ # fixtures avoids the issue. -@pytest.fixture(scope="function", params=[1, 2]) +@pytest_asyncio.fixture(scope="function", params=[1, 2]) async def empty_blockchain(request): """ Provides a list of 10 valid blocks, as well as a blockchain with 9 blocks added to it. @@ -43,21 +44,21 @@ def softfork_height(request): block_format_version = "rc4" -@pytest.fixture(scope="session") +@pytest_asyncio.fixture(scope="session") async def default_400_blocks(): from tests.util.blockchain import persistent_blocks return persistent_blocks(400, f"test_blocks_400_{block_format_version}.db", seed=b"alternate2") -@pytest.fixture(scope="session") +@pytest_asyncio.fixture(scope="session") async def default_1000_blocks(): from tests.util.blockchain import persistent_blocks return persistent_blocks(1000, f"test_blocks_1000_{block_format_version}.db") -@pytest.fixture(scope="session") +@pytest_asyncio.fixture(scope="session") async def pre_genesis_empty_slots_1000_blocks(): from tests.util.blockchain import persistent_blocks @@ -66,21 +67,21 @@ async def pre_genesis_empty_slots_1000_blocks(): ) -@pytest.fixture(scope="session") +@pytest_asyncio.fixture(scope="session") async def default_10000_blocks(): from tests.util.blockchain import persistent_blocks return persistent_blocks(10000, f"test_blocks_10000_{block_format_version}.db") -@pytest.fixture(scope="session") +@pytest_asyncio.fixture(scope="session") async def default_20000_blocks(): from tests.util.blockchain import persistent_blocks return persistent_blocks(20000, f"test_blocks_20000_{block_format_version}.db") -@pytest.fixture(scope="session") +@pytest_asyncio.fixture(scope="session") async def default_10000_blocks_compact(): from tests.util.blockchain import persistent_blocks @@ -94,7 +95,7 @@ async def default_10000_blocks_compact(): ) -@pytest.fixture(scope="function") +@pytest_asyncio.fixture(scope="function") async def tmp_dir(): with tempfile.TemporaryDirectory() as folder: yield Path(folder) diff --git a/tests/core/daemon/test_daemon.py b/tests/core/daemon/test_daemon.py index 84ea0e5d9672..becb6956d6fa 100644 --- a/tests/core/daemon/test_daemon.py +++ b/tests/core/daemon/test_daemon.py @@ -15,10 +15,11 @@ import aiohttp import pytest +import pytest_asyncio class TestDaemon: - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def get_daemon(self, get_b_tools): async for _ in setup_daemon(btools=get_b_tools): yield _ @@ -27,23 +28,23 @@ async def get_daemon(self, get_b_tools): # fixture, to test all versions of the database schema. This doesn't work # because of a hack in shutting down the full node, which means you cannot run # more than one simulations per process. - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def simulation(self, get_b_tools, get_b_tools_1): async for _ in setup_full_system( test_constants_modified, b_tools=get_b_tools, b_tools_1=get_b_tools_1, connect_to_daemon=True, db_version=1 ): yield _ - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def get_temp_keyring(self): with TempKeyring() as keychain: yield keychain - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def get_b_tools_1(self, get_temp_keyring): return await create_block_tools_async(constants=test_constants_modified, keychain=get_temp_keyring) - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def get_b_tools(self, get_temp_keyring): local_b_tools = await create_block_tools_async(constants=test_constants_modified, keychain=get_temp_keyring) new_config = local_b_tools._config @@ -51,7 +52,7 @@ async def get_b_tools(self, get_temp_keyring): local_b_tools.change_config(new_config) return local_b_tools - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def get_daemon_with_temp_keyring(self, get_b_tools): async for _ in setup_daemon(btools=get_b_tools): yield get_b_tools diff --git a/tests/core/full_node/full_sync/test_full_sync.py b/tests/core/full_node/full_sync/test_full_sync.py index 4bf0ebba644b..30f05f289182 100644 --- a/tests/core/full_node/full_sync/test_full_sync.py +++ b/tests/core/full_node/full_sync/test_full_sync.py @@ -5,6 +5,7 @@ from typing import List import pytest +import pytest_asyncio from chia.full_node.weight_proof import _validate_sub_epoch_summaries from chia.protocols import full_node_protocol @@ -28,22 +29,22 @@ def event_loop(): class TestFullSync: - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def two_nodes(self, db_version): async for _ in setup_two_nodes(test_constants, db_version=db_version): yield _ - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def three_nodes(self, db_version): async for _ in setup_n_nodes(test_constants, 3, db_version=db_version): yield _ - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def four_nodes(self, db_version): async for _ in setup_n_nodes(test_constants, 4, db_version=db_version): yield _ - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def five_nodes(self, db_version): async for _ in setup_n_nodes(test_constants, 5, db_version=db_version): yield _ diff --git a/tests/core/full_node/test_full_node.py b/tests/core/full_node/test_full_node.py index b30c9e334ac9..39494eb7b19e 100644 --- a/tests/core/full_node/test_full_node.py +++ b/tests/core/full_node/test_full_node.py @@ -10,6 +10,7 @@ from clvm.casts import int_to_bytes import pytest +import pytest_asyncio from chia.consensus.blockchain import ReceiveBlockResult from chia.consensus.pot_iterations import is_overflow_block @@ -106,7 +107,7 @@ def event_loop(): yield loop -@pytest.fixture(scope="module") +@pytest_asyncio.fixture(scope="module") async def wallet_nodes(): async_gen = setup_simulators_and_wallets(2, 1, {"MEMPOOL_BLOCK_BUFFER": 2, "MAX_BLOCK_COST_CLVM": 400000000}) nodes, wallets = await async_gen.__anext__() @@ -122,25 +123,25 @@ async def wallet_nodes(): yield _ -@pytest.fixture(scope="function") +@pytest_asyncio.fixture(scope="function") async def setup_four_nodes(db_version): async for _ in setup_simulators_and_wallets(5, 0, {}, starting_port=51000, db_version=db_version): yield _ -@pytest.fixture(scope="function") +@pytest_asyncio.fixture(scope="function") async def setup_two_nodes(db_version): async for _ in setup_simulators_and_wallets(2, 0, {}, starting_port=51100, db_version=db_version): yield _ -@pytest.fixture(scope="function") +@pytest_asyncio.fixture(scope="function") async def setup_two_nodes_and_wallet(): async for _ in setup_simulators_and_wallets(2, 1, {}, starting_port=51200, db_version=2): yield _ -@pytest.fixture(scope="function") +@pytest_asyncio.fixture(scope="function") async def wallet_nodes_mainnet(db_version): async_gen = setup_simulators_and_wallets(2, 1, {"NETWORK_TYPE": 0}, starting_port=40000, db_version=db_version) nodes, wallets = await async_gen.__anext__() diff --git a/tests/core/full_node/test_full_node_store.py b/tests/core/full_node/test_full_node_store.py index 1bad1b3d9a4c..748bc33718d8 100644 --- a/tests/core/full_node/test_full_node_store.py +++ b/tests/core/full_node/test_full_node_store.py @@ -6,6 +6,7 @@ from typing import List, Optional import pytest +import pytest_asyncio from chia.consensus.block_record import BlockRecord from chia.consensus.blockchain import ReceiveBlockResult @@ -52,7 +53,7 @@ def event_loop(): log = logging.getLogger(__name__) -@pytest.fixture(scope="function", params=[1, 2]) +@pytest_asyncio.fixture(scope="function", params=[1, 2]) async def empty_blockchain(request): bc1, connection, db_path = await create_blockchain(test_constants, request.param) yield bc1 @@ -61,7 +62,7 @@ async def empty_blockchain(request): db_path.unlink() -@pytest.fixture(scope="function", params=[1, 2]) +@pytest_asyncio.fixture(scope="function", params=[1, 2]) async def empty_blockchain_original(request): bc1, connection, db_path = await create_blockchain(test_constants_original, request.param) yield bc1 diff --git a/tests/core/full_node/test_hint_store.py b/tests/core/full_node/test_hint_store.py index 3627ce01f167..8b21ea56da52 100644 --- a/tests/core/full_node/test_hint_store.py +++ b/tests/core/full_node/test_hint_store.py @@ -51,6 +51,7 @@ async def test_basic_store(self, db_version): coins_for_non_hint = await hint_store.get_coin_ids(not_existing_hint) assert coins_for_non_hint == [] + @pytest.mark.asyncio async def test_duplicate_coins(self, db_version): async with DBConnection(db_version) as db_wrapper: hint_store = await HintStore.create(db_wrapper) diff --git a/tests/core/full_node/test_mempool.py b/tests/core/full_node/test_mempool.py index 6d6526191c7d..721533878ca6 100644 --- a/tests/core/full_node/test_mempool.py +++ b/tests/core/full_node/test_mempool.py @@ -7,6 +7,7 @@ from clvm.casts import int_to_bytes import pytest +import pytest_asyncio import chia.server.ws_connection as ws @@ -86,7 +87,7 @@ def event_loop(): # The reason for this is that our simulators can't be destroyed correctly, which # means you can't instantiate more than one per process, so this is a hack until # that is fixed. For now, our tests are not independent -@pytest.fixture(scope="module") +@pytest_asyncio.fixture(scope="module") async def two_nodes(): async_gen = setup_simulators_and_wallets(2, 1, {}) nodes, _ = await async_gen.__anext__() diff --git a/tests/core/full_node/test_mempool_performance.py b/tests/core/full_node/test_mempool_performance.py index 4c3eb090cc33..3b438b3abe16 100644 --- a/tests/core/full_node/test_mempool_performance.py +++ b/tests/core/full_node/test_mempool_performance.py @@ -4,6 +4,7 @@ import time import pytest +import pytest_asyncio import logging from chia.protocols import full_node_protocol @@ -41,7 +42,7 @@ def event_loop(): class TestMempoolPerformance: - @pytest.fixture(scope="module") + @pytest_asyncio.fixture(scope="module") async def wallet_nodes(self): key_seed = bt.farmer_master_sk_entropy async for _ in setup_simulators_and_wallets(2, 1, {}, key_seed=key_seed): diff --git a/tests/core/full_node/test_node_load.py b/tests/core/full_node/test_node_load.py index 28f7f74fb9b8..f4bc3737f391 100644 --- a/tests/core/full_node/test_node_load.py +++ b/tests/core/full_node/test_node_load.py @@ -2,6 +2,7 @@ import time import pytest +import pytest_asyncio from chia.protocols import full_node_protocol from chia.types.peer_info import PeerInfo @@ -18,7 +19,7 @@ def event_loop(): class TestNodeLoad: - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def two_nodes(self, db_version): async for _ in setup_two_nodes(test_constants, db_version=db_version): yield _ diff --git a/tests/core/full_node/test_performance.py b/tests/core/full_node/test_performance.py index 0c0da13a038a..0b496737ecbe 100644 --- a/tests/core/full_node/test_performance.py +++ b/tests/core/full_node/test_performance.py @@ -8,6 +8,7 @@ from clvm.casts import int_to_bytes import pytest +import pytest_asyncio import cProfile from chia.consensus.block_record import BlockRecord @@ -44,7 +45,7 @@ def event_loop(): yield loop -@pytest.fixture(scope="module") +@pytest_asyncio.fixture(scope="module") async def wallet_nodes(): async_gen = setup_simulators_and_wallets(1, 1, {"MEMPOOL_BLOCK_BUFFER": 1, "MAX_BLOCK_COST_CLVM": 11000000000}) nodes, wallets = await async_gen.__anext__() diff --git a/tests/core/full_node/test_transactions.py b/tests/core/full_node/test_transactions.py index 05d77725f40e..e0a2f5a0f894 100644 --- a/tests/core/full_node/test_transactions.py +++ b/tests/core/full_node/test_transactions.py @@ -3,6 +3,7 @@ from typing import Optional import pytest +import pytest_asyncio from chia.consensus.block_record import BlockRecord from chia.consensus.block_rewards import calculate_base_farmer_reward, calculate_pool_reward @@ -22,17 +23,17 @@ def event_loop(): class TestTransactions: - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def wallet_node(self): async for _ in setup_simulators_and_wallets(1, 1, {}): yield _ - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def two_wallet_nodes(self): async for _ in setup_simulators_and_wallets(1, 2, {}): yield _ - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def three_nodes_two_wallets(self): async for _ in setup_simulators_and_wallets(3, 2, {}): yield _ diff --git a/tests/core/server/test_dos.py b/tests/core/server/test_dos.py index a9e5cb55cfb7..876da52e62c6 100644 --- a/tests/core/server/test_dos.py +++ b/tests/core/server/test_dos.py @@ -3,6 +3,7 @@ import logging import pytest +import pytest_asyncio from aiohttp import ClientSession, ClientTimeout, ServerDisconnectedError, WSCloseCode, WSMessage, WSMsgType from chia.full_node.full_node_api import FullNodeAPI @@ -38,7 +39,7 @@ def event_loop(): yield loop -@pytest.fixture(scope="function") +@pytest_asyncio.fixture(scope="function") async def setup_two_nodes(db_version): async for _ in setup_simulators_and_wallets(2, 0, {}, starting_port=60000, db_version=db_version): yield _ diff --git a/tests/core/ssl/test_ssl.py b/tests/core/ssl/test_ssl.py index 4ef86993af48..791dea7bae5a 100644 --- a/tests/core/ssl/test_ssl.py +++ b/tests/core/ssl/test_ssl.py @@ -2,6 +2,7 @@ import aiohttp import pytest +import pytest_asyncio from chia.protocols.shared_protocol import protocol_version from chia.server.outbound_message import NodeType @@ -51,22 +52,22 @@ async def establish_connection(server: ChiaServer, dummy_port: int, ssl_context) class TestSSL: - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def harvester_farmer(self): async for _ in setup_farmer_harvester(test_constants): yield _ - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def wallet_node(self): async for _ in setup_simulators_and_wallets(1, 1, {}): yield _ - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def introducer(self): async for _ in setup_introducer(21233): yield _ - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def timelord(self): async for _ in setup_timelord(21236, 21237, False, test_constants, bt): yield _ diff --git a/tests/core/test_daemon_rpc.py b/tests/core/test_daemon_rpc.py index af4e31b542f0..e17d1b1d91e8 100644 --- a/tests/core/test_daemon_rpc.py +++ b/tests/core/test_daemon_rpc.py @@ -1,4 +1,5 @@ import pytest +import pytest_asyncio from tests.setup_nodes import setup_daemon from chia.daemon.client import connect_to_daemon @@ -7,7 +8,7 @@ class TestDaemonRpc: - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def get_daemon(self): async for _ in setup_daemon(btools=bt): yield _ diff --git a/tests/core/test_db_conversion.py b/tests/core/test_db_conversion.py index 6a12c3ae0fb4..8e5ac311e863 100644 --- a/tests/core/test_db_conversion.py +++ b/tests/core/test_db_conversion.py @@ -1,4 +1,5 @@ import pytest +import pytest_asyncio import aiosqlite import tempfile import random @@ -39,7 +40,7 @@ def rand_bytes(num) -> bytes: return bytes(ret) -@pytest.fixture(scope="session") +@pytest_asyncio.fixture(scope="session") def event_loop(): loop = asyncio.get_event_loop() yield loop diff --git a/tests/core/test_farmer_harvester_rpc.py b/tests/core/test_farmer_harvester_rpc.py index 6a9ff3ee02e5..2d63bea53d35 100644 --- a/tests/core/test_farmer_harvester_rpc.py +++ b/tests/core/test_farmer_harvester_rpc.py @@ -2,6 +2,7 @@ import time import pytest +import pytest_asyncio from chia.consensus.coinbase import create_puzzlehash_for_pk from chia.protocols import farmer_protocol @@ -24,13 +25,13 @@ log = logging.getLogger(__name__) -@pytest.fixture(scope="function") +@pytest_asyncio.fixture(scope="function") async def simulation(): async for _ in setup_farmer_harvester(test_constants): yield _ -@pytest.fixture(scope="function") +@pytest_asyncio.fixture(scope="function") async def environment(simulation): harvester_service, farmer_service = simulation diff --git a/tests/core/test_filter.py b/tests/core/test_filter.py index 6b4b024f648d..abac26aa7c07 100644 --- a/tests/core/test_filter.py +++ b/tests/core/test_filter.py @@ -2,6 +2,7 @@ from typing import List import pytest +import pytest_asyncio from chiabip158 import PyBIP158 from tests.setup_nodes import setup_simulators_and_wallets, bt @@ -14,7 +15,7 @@ def event_loop(): class TestFilter: - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def wallet_and_node(self): async for _ in setup_simulators_and_wallets(1, 1, {}): yield _ diff --git a/tests/core/test_full_node_rpc.py b/tests/core/test_full_node_rpc.py index 9e79c1919cae..78f50892f14e 100644 --- a/tests/core/test_full_node_rpc.py +++ b/tests/core/test_full_node_rpc.py @@ -3,6 +3,7 @@ from typing import List import pytest +import pytest_asyncio from blspy import AugSchemeMPL from chia.consensus.pot_iterations import is_overflow_block @@ -27,7 +28,7 @@ class TestRpc: - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def two_nodes(self): async for _ in setup_simulators_and_wallets(2, 0, {}): yield _ diff --git a/tests/farmer_harvester/test_farmer_harvester.py b/tests/farmer_harvester/test_farmer_harvester.py index b35bb3aef127..645b645a1439 100644 --- a/tests/farmer_harvester/test_farmer_harvester.py +++ b/tests/farmer_harvester/test_farmer_harvester.py @@ -1,6 +1,7 @@ import asyncio import pytest +import pytest_asyncio from chia.farmer.farmer import Farmer from chia.util.keychain import generate_mnemonic @@ -12,7 +13,7 @@ def farmer_is_started(farmer): return farmer.started -@pytest.fixture(scope="function") +@pytest_asyncio.fixture(scope="function") async def environment(): async for _ in setup_farmer_harvester(test_constants, False): yield _ diff --git a/tests/pools/test_pool_rpc.py b/tests/pools/test_pool_rpc.py index a54ef79c1e54..b6fc9e05a9d1 100644 --- a/tests/pools/test_pool_rpc.py +++ b/tests/pools/test_pool_rpc.py @@ -6,6 +6,7 @@ from typing import Optional, List, Dict import pytest +import pytest_asyncio from blspy import G1Element from chia.consensus.block_rewards import calculate_base_farmer_reward, calculate_pool_reward @@ -81,12 +82,12 @@ def event_loop(): class TestPoolWalletRpc: - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def two_wallet_nodes(self): async for _ in setup_simulators_and_wallets(1, 2, {}): yield _ - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def one_wallet_node_and_rpc(self): rmtree(get_pool_plot_dir(), ignore_errors=True) async for nodes in setup_simulators_and_wallets(1, 1, {}): @@ -122,7 +123,7 @@ async def one_wallet_node_and_rpc(self): await client.await_closed() await rpc_cleanup() - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def setup(self, two_wallet_nodes): rmtree(get_pool_plot_dir(), ignore_errors=True) full_nodes, wallets = two_wallet_nodes diff --git a/tests/pytest.ini b/tests/pytest.ini index 5add7716876d..28d4756488ea 100644 --- a/tests/pytest.ini +++ b/tests/pytest.ini @@ -3,3 +3,4 @@ log_cli = 1 log_level = WARNING log_format = %(asctime)s %(name)s: %(levelname)s %(message)s +asyncio_mode = strict diff --git a/tests/simulation/test_simulation.py b/tests/simulation/test_simulation.py index 7f9d76960d02..aacdabce3ae3 100644 --- a/tests/simulation/test_simulation.py +++ b/tests/simulation/test_simulation.py @@ -1,4 +1,5 @@ import pytest +import pytest_asyncio from chia.types.peer_info import PeerInfo from tests.block_tools import create_block_tools_async @@ -30,7 +31,7 @@ class TestSimulation: # fixture, to test all versions of the database schema. This doesn't work # because of a hack in shutting down the full node, which means you cannot run # more than one simulations per process. - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def extra_node(self): with TempKeyring() as keychain: b_tools = await create_block_tools_async(constants=test_constants_modified, keychain=keychain) @@ -43,7 +44,7 @@ async def extra_node(self): ): yield _ - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def simulation(self): async for _ in setup_full_system(test_constants_modified, db_version=1): yield _ diff --git a/tests/wallet/cat_wallet/test_cat_lifecycle.py b/tests/wallet/cat_wallet/test_cat_lifecycle.py index 9fdcdf3063b1..fa1f708613a3 100644 --- a/tests/wallet/cat_wallet/test_cat_lifecycle.py +++ b/tests/wallet/cat_wallet/test_cat_lifecycle.py @@ -1,4 +1,5 @@ import pytest +import pytest_asyncio from typing import List, Tuple, Optional, Dict from blspy import PrivateKey, AugSchemeMPL, G2Element @@ -38,7 +39,7 @@ class TestCATLifecycle: cost: Dict[str, int] = {} - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def setup_sim(self): sim = await SpendSim.create() sim_client = SimClient(sim) diff --git a/tests/wallet/cat_wallet/test_cat_wallet.py b/tests/wallet/cat_wallet/test_cat_wallet.py index 8717ae14937a..9f8b42ea89c9 100644 --- a/tests/wallet/cat_wallet/test_cat_wallet.py +++ b/tests/wallet/cat_wallet/test_cat_wallet.py @@ -2,6 +2,7 @@ from typing import List import pytest +import pytest_asyncio from chia.consensus.block_rewards import calculate_base_farmer_reward, calculate_pool_reward from chia.full_node.mempool_manager import MempoolManager @@ -34,17 +35,17 @@ async def tx_in_pool(mempool: MempoolManager, tx_id: bytes32): class TestCATWallet: - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def wallet_node(self): async for _ in setup_simulators_and_wallets(1, 1, {}): yield _ - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def two_wallet_nodes(self): async for _ in setup_simulators_and_wallets(1, 2, {}): yield _ - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def three_wallet_nodes(self): async for _ in setup_simulators_and_wallets(1, 3, {}): yield _ diff --git a/tests/wallet/cat_wallet/test_offer_lifecycle.py b/tests/wallet/cat_wallet/test_offer_lifecycle.py index 4186ddf4ac3e..e63ce73754dd 100644 --- a/tests/wallet/cat_wallet/test_offer_lifecycle.py +++ b/tests/wallet/cat_wallet/test_offer_lifecycle.py @@ -1,4 +1,5 @@ import pytest +import pytest_asyncio from typing import Dict, Optional, List from blspy import G2Element @@ -43,7 +44,7 @@ def str_to_cat_hash(tail_str: str) -> bytes32: class TestOfferLifecycle: cost: Dict[str, int] = {} - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def setup_sim(self): sim = await SpendSim.create() sim_client = SimClient(sim) diff --git a/tests/wallet/cat_wallet/test_trades.py b/tests/wallet/cat_wallet/test_trades.py index 20fffb1006ef..a546003b2bd8 100644 --- a/tests/wallet/cat_wallet/test_trades.py +++ b/tests/wallet/cat_wallet/test_trades.py @@ -3,6 +3,7 @@ from typing import List import pytest +import pytest_asyncio from chia.full_node.mempool_manager import MempoolManager from chia.simulator.simulator_protocol import FarmNewBlockProtocol @@ -30,7 +31,7 @@ def event_loop(): yield loop -@pytest.fixture(scope="function") +@pytest_asyncio.fixture(scope="function") async def two_wallet_nodes(): async for _ in setup_simulators_and_wallets(1, 2, {}): yield _ @@ -39,7 +40,7 @@ async def two_wallet_nodes(): buffer_blocks = 4 -@pytest.fixture(scope="function") +@pytest_asyncio.fixture(scope="function") async def wallets_prefarm(two_wallet_nodes, trusted): """ Sets up the node with 10 blocks, and returns a payer and payee wallet. diff --git a/tests/wallet/did_wallet/test_did.py b/tests/wallet/did_wallet/test_did.py index 69723a7d222e..f613262a80c3 100644 --- a/tests/wallet/did_wallet/test_did.py +++ b/tests/wallet/did_wallet/test_did.py @@ -1,5 +1,6 @@ import asyncio import pytest +import pytest_asyncio from chia.simulator.simulator_protocol import FarmNewBlockProtocol from chia.types.peer_info import PeerInfo from chia.util.ints import uint16, uint32, uint64 @@ -21,27 +22,27 @@ def event_loop(): class TestDIDWallet: - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def wallet_node(self): async for _ in setup_simulators_and_wallets(1, 1, {}): yield _ - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def two_wallet_nodes(self): async for _ in setup_simulators_and_wallets(1, 2, {}): yield _ - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def three_wallet_nodes(self): async for _ in setup_simulators_and_wallets(1, 3, {}): yield _ - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def two_wallet_nodes_five_freeze(self): async for _ in setup_simulators_and_wallets(1, 2, {}): yield _ - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def three_sim_two_wallets(self): async for _ in setup_simulators_and_wallets(3, 2, {}): yield _ diff --git a/tests/wallet/did_wallet/test_did_rpc.py b/tests/wallet/did_wallet/test_did_rpc.py index dd578d798941..1d5d617b5e9b 100644 --- a/tests/wallet/did_wallet/test_did_rpc.py +++ b/tests/wallet/did_wallet/test_did_rpc.py @@ -1,6 +1,7 @@ import asyncio import logging import pytest +import pytest_asyncio from chia.rpc.rpc_server import start_rpc_server from chia.rpc.wallet_rpc_api import WalletRpcApi @@ -26,7 +27,7 @@ def event_loop(): class TestDIDWallet: - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def three_wallet_nodes(self): async for _ in setup_simulators_and_wallets(1, 3, {}): yield _ diff --git a/tests/wallet/rl_wallet/test_rl_rpc.py b/tests/wallet/rl_wallet/test_rl_rpc.py index 3ba07a2a84bd..d3aeceed7ed4 100644 --- a/tests/wallet/rl_wallet/test_rl_rpc.py +++ b/tests/wallet/rl_wallet/test_rl_rpc.py @@ -1,6 +1,7 @@ import asyncio import pytest +import pytest_asyncio from chia.rpc.wallet_rpc_api import WalletRpcApi from chia.simulator.simulator_protocol import FarmNewBlockProtocol @@ -52,7 +53,7 @@ async def check_balance(api, wallet_id): class TestRLWallet: - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def three_wallet_nodes(self): async for _ in setup_simulators_and_wallets(1, 3, {}): yield _ diff --git a/tests/wallet/rl_wallet/test_rl_wallet.py b/tests/wallet/rl_wallet/test_rl_wallet.py index 7a92cbe0eba2..e4dce95ce286 100644 --- a/tests/wallet/rl_wallet/test_rl_wallet.py +++ b/tests/wallet/rl_wallet/test_rl_wallet.py @@ -1,6 +1,7 @@ import asyncio import pytest +import pytest_asyncio from chia.simulator.simulator_protocol import FarmNewBlockProtocol from chia.types.peer_info import PeerInfo @@ -17,7 +18,7 @@ def event_loop(): class TestCATWallet: - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def two_wallet_nodes(self): async for _ in setup_simulators_and_wallets(1, 2, {}): yield _ diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index b840c9531b4d..c83ac2c9feb5 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -11,6 +11,7 @@ import logging import pytest +import pytest_asyncio from chia.consensus.block_rewards import calculate_base_farmer_reward, calculate_pool_reward from chia.rpc.full_node_rpc_api import FullNodeRpcApi @@ -40,7 +41,7 @@ class TestWalletRpc: - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def two_wallet_nodes(self): async for _ in setup_simulators_and_wallets(1, 2, {}): yield _ diff --git a/tests/wallet/simple_sync/test_simple_sync_protocol.py b/tests/wallet/simple_sync/test_simple_sync_protocol.py index b45ec1e136a8..d4ee7b073de4 100644 --- a/tests/wallet/simple_sync/test_simple_sync_protocol.py +++ b/tests/wallet/simple_sync/test_simple_sync_protocol.py @@ -3,6 +3,7 @@ from typing import List, Optional import pytest +import pytest_asyncio from clvm.casts import int_to_bytes from colorlog import getLogger @@ -46,12 +47,12 @@ def event_loop(): class TestSimpleSyncProtocol: - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def wallet_node_simulator(self): async for _ in setup_simulators_and_wallets(1, 1, {}): yield _ - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def wallet_two_node_simulator(self): async for _ in setup_simulators_and_wallets(2, 1, {}): yield _ diff --git a/tests/wallet/sync/test_wallet_sync.py b/tests/wallet/sync/test_wallet_sync.py index bcc45db27530..1b80a4bb79c8 100644 --- a/tests/wallet/sync/test_wallet_sync.py +++ b/tests/wallet/sync/test_wallet_sync.py @@ -2,6 +2,7 @@ import asyncio import pytest +import pytest_asyncio from colorlog import getLogger from chia.consensus.block_rewards import calculate_base_farmer_reward, calculate_pool_reward @@ -33,17 +34,17 @@ def event_loop(): class TestWalletSync: - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def wallet_node(self): async for _ in setup_node_and_wallet(test_constants): yield _ - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def wallet_node_simulator(self): async for _ in setup_simulators_and_wallets(1, 1, {}): yield _ - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def wallet_node_starting_height(self): async for _ in setup_node_and_wallet(test_constants, starting_height=100): yield _ diff --git a/tests/wallet/test_wallet.py b/tests/wallet/test_wallet.py index 092a03aeffb2..49e18499768b 100644 --- a/tests/wallet/test_wallet.py +++ b/tests/wallet/test_wallet.py @@ -1,5 +1,6 @@ import asyncio import pytest +import pytest_asyncio import time from chia.consensus.block_rewards import calculate_base_farmer_reward, calculate_pool_reward from chia.protocols.full_node_protocol import RespondBlock @@ -27,27 +28,27 @@ def event_loop(): class TestWalletSimulator: - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def wallet_node(self): async for _ in setup_simulators_and_wallets(1, 1, {}, True): yield _ - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def wallet_node_100_pk(self): async for _ in setup_simulators_and_wallets(1, 1, {}, initial_num_public_keys=100): yield _ - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def two_wallet_nodes(self): async for _ in setup_simulators_and_wallets(1, 2, {}, True): yield _ - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def two_wallet_nodes_five_freeze(self): async for _ in setup_simulators_and_wallets(1, 2, {}, True): yield _ - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def three_sim_two_wallets(self): async for _ in setup_simulators_and_wallets(3, 2, {}, True): yield _ diff --git a/tests/wallet/test_wallet_blockchain.py b/tests/wallet/test_wallet_blockchain.py index 39cbaec54030..c4737b984078 100644 --- a/tests/wallet/test_wallet_blockchain.py +++ b/tests/wallet/test_wallet_blockchain.py @@ -4,6 +4,7 @@ import aiosqlite import pytest +import pytest_asyncio from chia.consensus.blockchain import ReceiveBlockResult from chia.protocols import full_node_protocol @@ -23,7 +24,7 @@ def event_loop(): class TestWalletBlockchain: - @pytest.fixture(scope="function") + @pytest_asyncio.fixture(scope="function") async def wallet_node(self): async for _ in setup_node_and_wallet(test_constants): yield _ From df858701e43dafba91a37b66690cf4add7a8be01 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Wed, 16 Feb 2022 18:21:00 -0500 Subject: [PATCH 058/378] Ban setuptools 60.9.2 (#10269) --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ff1b799bc7ac..3078c1c0d640 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,6 @@ [build-system] -requires = ["setuptools>=42,!=60.9.1", "wheel", "setuptools_scm[toml]>=4.1.2"] +# setuptools !=60.9.1, !=60.9.2 for bug missing no-local-version +requires = ["setuptools>=42,!=60.9.1,!=60.9.2", "wheel", "setuptools_scm[toml]>=4.1.2"] build-backend = "setuptools.build_meta" [tool.setuptools_scm] From 767974cb935692e14a3846db99be6dd3f5f7e23f Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Wed, 16 Feb 2022 18:26:03 -0500 Subject: [PATCH 059/378] reinstate pre-existing signal configuration on windows for services (#10263) --- chia/server/start_service.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/chia/server/start_service.py b/chia/server/start_service.py index 68cfb5d7238a..0dc03a17dd92 100644 --- a/chia/server/start_service.py +++ b/chia/server/start_service.py @@ -182,18 +182,21 @@ def _enable_signals(self) -> None: global main_pid main_pid = os.getpid() - loop = asyncio.get_running_loop() - loop.add_signal_handler( - signal.SIGINT, - functools.partial(self._accept_signal, signal_number=signal.SIGINT), - ) - loop.add_signal_handler( - signal.SIGTERM, - functools.partial(self._accept_signal, signal_number=signal.SIGTERM), - ) if platform == "win32" or platform == "cygwin": # pylint: disable=E1101 signal.signal(signal.SIGBREAK, self._accept_signal) # type: ignore + signal.signal(signal.SIGINT, self._accept_signal) + signal.signal(signal.SIGTERM, self._accept_signal) + else: + loop = asyncio.get_running_loop() + loop.add_signal_handler( + signal.SIGINT, + functools.partial(self._accept_signal, signal_number=signal.SIGINT), + ) + loop.add_signal_handler( + signal.SIGTERM, + functools.partial(self._accept_signal, signal_number=signal.SIGTERM), + ) def _accept_signal(self, signal_number: int, stack_frame=None): self._log.info(f"got signal {signal_number}") From 4920a464c1b1d8e9cdd35d19a36a05d143e1bd47 Mon Sep 17 00:00:00 2001 From: Jeff Date: Wed, 16 Feb 2022 15:51:56 -0800 Subject: [PATCH 060/378] Remove the "Cleaning up temp keychain in dir" message to reduce noise. (#10264) * Remove the "Cleaning up temp keychain in dir" message to reduce noise. * Remove unnecessary temp variable --- tests/util/keyring.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/util/keyring.py b/tests/util/keyring.py index 22f60123da54..e25d73df6986 100644 --- a/tests/util/keyring.py +++ b/tests/util/keyring.py @@ -194,9 +194,7 @@ def cleanup(self): if self.delete_on_cleanup: self.keychain.keyring_wrapper.keyring.cleanup_keyring_file_watcher() - temp_dir = self.keychain._temp_dir - print(f"Cleaning up temp keychain in dir: {temp_dir}") - shutil.rmtree(temp_dir) + shutil.rmtree(self.keychain._temp_dir) self.keychain._mock_supports_keyring_passphrase_patch.stop() self.keychain._mock_supports_os_passphrase_storage_patch.stop() From a7c73e41251fcdcd0c9939183b9d8d48acca6da4 Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Thu, 17 Feb 2022 01:21:25 +0100 Subject: [PATCH 061/378] use synchronous sqlite3 library in db conversion function (#10259) --- chia/cmds/db_upgrade_func.py | 178 +++++++++++++++++-------------- chia/full_node/block_store.py | 4 + tests/core/test_db_conversion.py | 2 +- 3 files changed, 105 insertions(+), 79 deletions(-) diff --git a/chia/cmds/db_upgrade_func.py b/chia/cmds/db_upgrade_func.py index e69a8dbc399a..4b48ff2ec54e 100644 --- a/chia/cmds/db_upgrade_func.py +++ b/chia/cmds/db_upgrade_func.py @@ -1,17 +1,11 @@ from typing import Dict, Optional -import sqlite3 from pathlib import Path import sys from time import time -import asyncio -import zstd - from chia.util.config import load_config, save_config from chia.util.path import mkdir, path_from_root -from chia.full_node.block_store import BlockStore -from chia.full_node.coin_store import CoinStore -from chia.full_node.hint_store import HintStore +from chia.util.ints import uint32 from chia.types.blockchain_format.sized_bytes import bytes32 @@ -46,7 +40,7 @@ def db_upgrade_func( out_db_path = path_from_root(root_path, db_path_replaced) mkdir(out_db_path.parent) - asyncio.run(convert_v1_to_v2(in_db_path, out_db_path)) + convert_v1_to_v2(in_db_path, out_db_path) if update_config: print("updating config.yaml") @@ -65,40 +59,40 @@ def db_upgrade_func( COIN_COMMIT_RATE = 30000 -async def convert_v1_to_v2(in_path: Path, out_path: Path) -> None: - import aiosqlite - from chia.util.db_wrapper import DBWrapper +def convert_v1_to_v2(in_path: Path, out_path: Path) -> None: + import sqlite3 + import zstd + + from contextlib import closing if out_path.exists(): print(f"output file already exists. {out_path}") raise RuntimeError("already exists") print(f"opening file for reading: {in_path}") - async with aiosqlite.connect(in_path) as in_db: + with closing(sqlite3.connect(in_path)) as in_db: try: - async with in_db.execute("SELECT * from database_version") as cursor: - row = await cursor.fetchone() + with closing(in_db.execute("SELECT * from database_version")) as cursor: + row = cursor.fetchone() if row is not None and row[0] != 1: print(f"blockchain database already version {row[0]}\nDone") raise RuntimeError("already v2") - except aiosqlite.OperationalError: + except sqlite3.OperationalError: pass - store_v1 = await BlockStore.create(DBWrapper(in_db, db_version=1)) - print(f"opening file for writing: {out_path}") - async with aiosqlite.connect(out_path) as out_db: - await out_db.execute("pragma journal_mode=OFF") - await out_db.execute("pragma synchronous=OFF") - await out_db.execute("pragma cache_size=131072") - await out_db.execute("pragma locking_mode=exclusive") + with closing(sqlite3.connect(out_path)) as out_db: + out_db.execute("pragma journal_mode=OFF") + out_db.execute("pragma synchronous=OFF") + out_db.execute("pragma cache_size=131072") + out_db.execute("pragma locking_mode=exclusive") print("initializing v2 version") - await out_db.execute("CREATE TABLE database_version(version int)") - await out_db.execute("INSERT INTO database_version VALUES(?)", (2,)) + out_db.execute("CREATE TABLE database_version(version int)") + out_db.execute("INSERT INTO database_version VALUES(?)", (2,)) print("initializing v2 block store") - await out_db.execute( + out_db.execute( "CREATE TABLE full_blocks(" "header_hash blob PRIMARY KEY," "prev_hash blob," @@ -109,16 +103,22 @@ async def convert_v1_to_v2(in_path: Path, out_path: Path) -> None: "block blob," "block_record blob)" ) - await out_db.execute( + out_db.execute( "CREATE TABLE sub_epoch_segments_v3(" "ses_block_hash blob PRIMARY KEY," "challenge_segments blob)" ) - await out_db.execute("CREATE TABLE current_peak(key int PRIMARY KEY, hash blob)") - - peak_hash, peak_height = await store_v1.get_peak() + out_db.execute("CREATE TABLE current_peak(key int PRIMARY KEY, hash blob)") + + with closing(in_db.execute("SELECT header_hash, height from block_records WHERE is_peak = 1")) as cursor: + peak_row = cursor.fetchone() + if peak_row is None: + print("v1 database does not have a peak block, there is no blockchain to convert") + raise RuntimeError("no blockchain") + peak_hash = bytes32(bytes.fromhex(peak_row[0])) + peak_height = uint32(peak_row[1]) print(f"peak: {peak_hash.hex()} height: {peak_height}") - await out_db.execute("INSERT INTO current_peak VALUES(?, ?)", (0, peak_hash)) - await out_db.commit() + out_db.execute("INSERT INTO current_peak VALUES(?, ?)", (0, peak_hash)) + out_db.commit() print("[1/5] converting full_blocks") height = peak_height + 1 @@ -130,15 +130,19 @@ async def convert_v1_to_v2(in_path: Path, out_path: Path) -> None: block_start_time = start_time block_values = [] - async with in_db.execute( - "SELECT header_hash, prev_hash, block, sub_epoch_summary FROM block_records ORDER BY height DESC" + with closing( + in_db.execute( + "SELECT header_hash, prev_hash, block, sub_epoch_summary FROM block_records ORDER BY height DESC" + ) ) as cursor: - async with in_db.execute( - "SELECT header_hash, height, is_fully_compactified, block FROM full_blocks ORDER BY height DESC" + with closing( + in_db.execute( + "SELECT header_hash, height, is_fully_compactified, block FROM full_blocks ORDER BY height DESC" + ) ) as cursor_2: - await out_db.execute("begin transaction") - async for row in cursor: + out_db.execute("begin transaction") + for row in cursor: header_hash = bytes.fromhex(row[0]) if header_hash != hh: @@ -146,7 +150,7 @@ async def convert_v1_to_v2(in_path: Path, out_path: Path) -> None: # progress cursor_2 until we find the header hash while True: - row_2 = await cursor_2.fetchone() + row_2 = cursor_2.fetchone() if row_2 is None: print(f"ERROR: could not find block {hh.hex()}") raise RuntimeError(f"block {hh.hex()} not found") @@ -158,7 +162,7 @@ async def convert_v1_to_v2(in_path: Path, out_path: Path) -> None: is_fully_compactified = row_2[2] block_bytes = row_2[3] - prev_hash = bytes.fromhex(row[1]) + prev_hash = bytes32.fromhex(row[1]) block_record = row[2] ses = row[3] @@ -185,18 +189,18 @@ async def convert_v1_to_v2(in_path: Path, out_path: Path) -> None: commit_in -= 1 if commit_in == 0: commit_in = BLOCK_COMMIT_RATE - await out_db.executemany( + out_db.executemany( "INSERT OR REPLACE INTO full_blocks VALUES(?, ?, ?, ?, ?, ?, ?, ?)", block_values ) - await out_db.commit() - await out_db.execute("begin transaction") + out_db.commit() + out_db.execute("begin transaction") block_values = [] end_time = time() rate = BLOCK_COMMIT_RATE / (end_time - start_time) start_time = end_time - await out_db.executemany("INSERT OR REPLACE INTO full_blocks VALUES(?, ?, ?, ?, ?, ?, ?, ?)", block_values) - await out_db.commit() + out_db.executemany("INSERT OR REPLACE INTO full_blocks VALUES(?, ?, ?, ?, ?, ?, ?, ?)", block_values) + out_db.commit() end_time = time() print(f"\r {end_time - block_start_time:.2f} seconds ") @@ -205,10 +209,12 @@ async def convert_v1_to_v2(in_path: Path, out_path: Path) -> None: commit_in = SES_COMMIT_RATE ses_values = [] ses_start_time = time() - async with in_db.execute("SELECT ses_block_hash, challenge_segments FROM sub_epoch_segments_v3") as cursor: + with closing( + in_db.execute("SELECT ses_block_hash, challenge_segments FROM sub_epoch_segments_v3") + ) as cursor: count = 0 - await out_db.execute("begin transaction") - async for row in cursor: + out_db.execute("begin transaction") + for row in cursor: block_hash = bytes32.fromhex(row[0]) ses = row[1] ses_values.append((block_hash, ses)) @@ -220,13 +226,13 @@ async def convert_v1_to_v2(in_path: Path, out_path: Path) -> None: commit_in -= 1 if commit_in == 0: commit_in = SES_COMMIT_RATE - await out_db.executemany("INSERT INTO sub_epoch_segments_v3 VALUES (?, ?)", ses_values) - await out_db.commit() - await out_db.execute("begin transaction") + out_db.executemany("INSERT INTO sub_epoch_segments_v3 VALUES (?, ?)", ses_values) + out_db.commit() + out_db.execute("begin transaction") ses_values = [] - await out_db.executemany("INSERT INTO sub_epoch_segments_v3 VALUES (?, ?)", ses_values) - await out_db.commit() + out_db.executemany("INSERT INTO sub_epoch_segments_v3 VALUES (?, ?)", ses_values) + out_db.commit() end_time = time() print(f"\r {end_time - ses_start_time:.2f} seconds ") @@ -236,32 +242,32 @@ async def convert_v1_to_v2(in_path: Path, out_path: Path) -> None: commit_in = HINT_COMMIT_RATE hint_start_time = time() hint_values = [] - await out_db.execute("CREATE TABLE hints(coin_id blob, hint blob, UNIQUE (coin_id, hint))") - await out_db.commit() + out_db.execute("CREATE TABLE hints(coin_id blob, hint blob, UNIQUE (coin_id, hint))") + out_db.commit() try: - async with in_db.execute("SELECT coin_id, hint FROM hints") as cursor: + with closing(in_db.execute("SELECT coin_id, hint FROM hints")) as cursor: count = 0 - await out_db.execute("begin transaction") - async for row in cursor: + out_db.execute("begin transaction") + for row in cursor: hint_values.append((row[0], row[1])) commit_in -= 1 if commit_in == 0: commit_in = HINT_COMMIT_RATE - await out_db.executemany("INSERT OR IGNORE INTO hints VALUES(?, ?)", hint_values) - await out_db.commit() - await out_db.execute("begin transaction") + out_db.executemany("INSERT OR IGNORE INTO hints VALUES(?, ?)", hint_values) + out_db.commit() + out_db.execute("begin transaction") hint_values = [] except sqlite3.OperationalError: print(" no hints table, skipping") - await out_db.executemany("INSERT OR IGNORE INTO hints VALUES (?, ?)", hint_values) - await out_db.commit() + out_db.executemany("INSERT OR IGNORE INTO hints VALUES (?, ?)", hint_values) + out_db.commit() end_time = time() print(f"\r {end_time - hint_start_time:.2f} seconds ") print("[4/5] converting coin_store") - await out_db.execute( + out_db.execute( "CREATE TABLE coin_record(" "coin_name blob PRIMARY KEY," " confirmed_index bigint," @@ -272,21 +278,24 @@ async def convert_v1_to_v2(in_path: Path, out_path: Path) -> None: " amount blob," # we use a blob of 8 bytes to store uint64 " timestamp bigint)" ) - await out_db.commit() + out_db.commit() commit_in = COIN_COMMIT_RATE rate = 1.0 start_time = time() coin_values = [] coin_start_time = start_time - async with in_db.execute( - "SELECT coin_name, confirmed_index, spent_index, coinbase, puzzle_hash, coin_parent, amount, timestamp " - "FROM coin_record WHERE confirmed_index <= ?", - (peak_height,), + with closing( + in_db.execute( + "SELECT coin_name, confirmed_index, spent_index, coinbase, " + "puzzle_hash, coin_parent, amount, timestamp " + "FROM coin_record WHERE confirmed_index <= ?", + (peak_height,), + ) ) as cursor: count = 0 - await out_db.execute("begin transaction") - async for row in cursor: + out_db.execute("begin transaction") + for row in cursor: spent_index = row[2] # in order to convert a consistent snapshot of the @@ -314,26 +323,39 @@ async def convert_v1_to_v2(in_path: Path, out_path: Path) -> None: commit_in -= 1 if commit_in == 0: commit_in = COIN_COMMIT_RATE - await out_db.executemany("INSERT INTO coin_record VALUES(?, ?, ?, ?, ?, ?, ?, ?)", coin_values) - await out_db.commit() - await out_db.execute("begin transaction") + out_db.executemany("INSERT INTO coin_record VALUES(?, ?, ?, ?, ?, ?, ?, ?)", coin_values) + out_db.commit() + out_db.execute("begin transaction") coin_values = [] end_time = time() rate = COIN_COMMIT_RATE / (end_time - start_time) start_time = end_time - await out_db.executemany("INSERT INTO coin_record VALUES(?, ?, ?, ?, ?, ?, ?, ?)", coin_values) - await out_db.commit() + out_db.executemany("INSERT INTO coin_record VALUES(?, ?, ?, ?, ?, ?, ?, ?)", coin_values) + out_db.commit() end_time = time() print(f"\r {end_time - coin_start_time:.2f} seconds ") print("[5/5] build indices") index_start_time = time() print(" block store") - await BlockStore.create(DBWrapper(out_db, db_version=2)) + out_db.execute("CREATE INDEX height on full_blocks(height)") + out_db.execute( + "CREATE INDEX is_fully_compactified ON" + " full_blocks(is_fully_compactified, in_main_chain) WHERE in_main_chain=1" + ) + out_db.execute("CREATE INDEX main_chain ON full_blocks(height, in_main_chain) WHERE in_main_chain=1") + out_db.commit() print(" coin store") - await CoinStore.create(DBWrapper(out_db, db_version=2)) + + out_db.execute("CREATE INDEX IF NOT EXISTS coin_confirmed_index on coin_record(confirmed_index)") + out_db.execute("CREATE INDEX IF NOT EXISTS coin_spent_index on coin_record(spent_index)") + out_db.execute("CREATE INDEX IF NOT EXISTS coin_puzzle_hash on coin_record(puzzle_hash)") + out_db.execute("CREATE INDEX IF NOT EXISTS coin_parent_index on coin_record(coin_parent)") + out_db.commit() print(" hint store") - await HintStore.create(DBWrapper(out_db, db_version=2)) + + out_db.execute("CREATE TABLE IF NOT EXISTS hints(coin_id blob, hint blob, UNIQUE (coin_id, hint))") + out_db.commit() end_time = time() print(f"\r {end_time - index_start_time:.2f} seconds ") diff --git a/chia/full_node/block_store.py b/chia/full_node/block_store.py index 3f865e065988..b3c419fe31bd 100644 --- a/chia/full_node/block_store.py +++ b/chia/full_node/block_store.py @@ -52,6 +52,8 @@ async def create(cls, db_wrapper: DBWrapper): # peak. The "key" field is there to make update statements simple await self.db.execute("CREATE TABLE IF NOT EXISTS current_peak(key int PRIMARY KEY, hash blob)") + # If any of these indices are altered, they should also be altered + # in the chia/cmds/db_upgrade.py file await self.db.execute("CREATE INDEX IF NOT EXISTS height on full_blocks(height)") # Sub epoch segments for weight proofs @@ -61,6 +63,8 @@ async def create(cls, db_wrapper: DBWrapper): "challenge_segments blob)" ) + # If any of these indices are altered, they should also be altered + # in the chia/cmds/db_upgrade.py file await self.db.execute( "CREATE INDEX IF NOT EXISTS is_fully_compactified ON" " full_blocks(is_fully_compactified, in_main_chain) WHERE in_main_chain=1" diff --git a/tests/core/test_db_conversion.py b/tests/core/test_db_conversion.py index 8e5ac311e863..a60bdab41cba 100644 --- a/tests/core/test_db_conversion.py +++ b/tests/core/test_db_conversion.py @@ -103,7 +103,7 @@ async def test_blocks(self, default_1000_blocks, with_hints: bool): assert err is None # now, convert v1 in_file to v2 out_file - await convert_v1_to_v2(in_file, out_file) + convert_v1_to_v2(in_file, out_file) async with aiosqlite.connect(in_file) as conn, aiosqlite.connect(out_file) as conn2: From 5d2770cbddf8434b76725cb70acb1755a0754837 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Wed, 16 Feb 2022 22:10:37 -0500 Subject: [PATCH 062/378] extend pools test workflow timeout to 60 minutes (#10271) --- .github/workflows/build-test-macos-pools.yml | 2 +- .github/workflows/build-test-ubuntu-pools.yml | 2 +- tests/pools/config.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-test-macos-pools.yml b/.github/workflows/build-test-macos-pools.yml index 23eaadf03593..88745eb56c3f 100644 --- a/.github/workflows/build-test-macos-pools.yml +++ b/.github/workflows/build-test-macos-pools.yml @@ -22,7 +22,7 @@ jobs: build: name: MacOS pools Tests runs-on: ${{ matrix.os }} - timeout-minutes: 45 + timeout-minutes: 60 strategy: fail-fast: false max-parallel: 4 diff --git a/.github/workflows/build-test-ubuntu-pools.yml b/.github/workflows/build-test-ubuntu-pools.yml index 06ba5d077fca..4fa27f077c90 100644 --- a/.github/workflows/build-test-ubuntu-pools.yml +++ b/.github/workflows/build-test-ubuntu-pools.yml @@ -22,7 +22,7 @@ jobs: build: name: Ubuntu pools Test runs-on: ${{ matrix.os }} - timeout-minutes: 45 + timeout-minutes: 60 strategy: fail-fast: false max-parallel: 4 diff --git a/tests/pools/config.py b/tests/pools/config.py index 2c8b89d30fc2..d9b815b24cb2 100644 --- a/tests/pools/config.py +++ b/tests/pools/config.py @@ -1 +1 @@ -job_timeout = 45 +job_timeout = 60 From c50ac761fae67c68652e8283aed029e0c86b2c13 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 17 Feb 2022 10:27:22 -0500 Subject: [PATCH 063/378] do not generate __init__.py in .pytest_cache (#10270) --- tests/build-init-files.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/build-init-files.py b/tests/build-init-files.py index 9a5db271b651..61eacf8a9f32 100755 --- a/tests/build-init-files.py +++ b/tests/build-init-files.py @@ -23,6 +23,9 @@ } +ignores = {"__pycache__", ".pytest_cache"} + + @click.command() @click.option( "-r", "--root", "root_str", type=click.Path(dir_okay=True, file_okay=False, resolve_path=True), default="." @@ -42,7 +45,7 @@ def command(verbose, root_str): path for tree_root in tree_roots for path in root.joinpath(tree_root).rglob("**/") - if "__pycache__" not in path.parts + if all(part not in ignores for part in path.parts) ) for path in directories: From 1df6793d51a3ea25b6ba8b8a960e315cafb55528 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 17 Feb 2022 10:28:55 -0500 Subject: [PATCH 064/378] filterwarnings = error (#10240) * filterwarnings = error * ignore some warnings * Update pytest.ini --- tests/pytest.ini | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/pytest.ini b/tests/pytest.ini index 28d4756488ea..08d89a020a7d 100644 --- a/tests/pytest.ini +++ b/tests/pytest.ini @@ -4,3 +4,16 @@ log_cli = 1 log_level = WARNING log_format = %(asctime)s %(name)s: %(levelname)s %(message)s asyncio_mode = strict +filterwarnings = + error + ignore:ssl_context is deprecated:DeprecationWarning + ignore:Implicitly cleaning up:ResourceWarning + ignore:unclosed Date: Thu, 17 Feb 2022 10:29:42 -0500 Subject: [PATCH 065/378] Fix memory leak in sync_store.py (#10216) * switch to orderdict and use fifo * workaround for older python versions * oops * Update sync_store.py * fixed * move comment for clarity * Update sync_store.py --- chia/full_node/sync_store.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/chia/full_node/sync_store.py b/chia/full_node/sync_store.py index b2b0cdf6fa88..bf3fb80e55be 100644 --- a/chia/full_node/sync_store.py +++ b/chia/full_node/sync_store.py @@ -1,6 +1,7 @@ import asyncio import logging -from typing import Dict, List, Optional, Set, Tuple +from collections import OrderedDict as orderedDict +from typing import Dict, List, Optional, OrderedDict, Set, Tuple from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.ints import uint32, uint128 @@ -12,7 +13,7 @@ class SyncStore: # Whether or not we are syncing sync_mode: bool long_sync: bool - peak_to_peer: Dict[bytes32, Set[bytes32]] # Header hash : peer node id + peak_to_peer: OrderedDict[bytes32, Set[bytes32]] # Header hash : peer node id peer_to_peak: Dict[bytes32, Tuple[bytes32, uint32, uint128]] # peer node id : [header_hash, height, weight] sync_target_header_hash: Optional[bytes32] # Peak hash we are syncing towards sync_target_height: Optional[uint32] # Peak height we are syncing towards @@ -29,7 +30,7 @@ async def create(cls): self.sync_target_header_hash = None self.sync_target_height = None self.peak_fork_point = {} - self.peak_to_peer = {} + self.peak_to_peer = orderedDict() self.peer_to_peak = {} self.peers_changed = asyncio.Event() @@ -73,7 +74,12 @@ def peer_has_block(self, header_hash: bytes32, peer_id: bytes32, weight: uint128 self.peak_to_peer[header_hash].add(peer_id) else: self.peak_to_peer[header_hash] = {peer_id} - + if len(self.peak_to_peer) > 256: # nice power of two + item = self.peak_to_peer.popitem(last=False) # Remove the oldest entry + # sync target hash is used throughout the sync process and should not be deleted. + if item[0] == self.sync_target_header_hash: + self.peak_to_peer[item[0]] = item[1] # Put it back in if it was the sync target + self.peak_to_peer.popitem(last=False) # Remove the oldest entry again if new_peak: self.peer_to_peak[peer_id] = (header_hash, height, weight) @@ -126,7 +132,7 @@ async def clear_sync_info(self): """ Clears the peak_to_peer info which can get quite large. """ - self.peak_to_peer = {} + self.peak_to_peer = orderedDict() def peer_disconnected(self, node_id: bytes32): if node_id in self.peer_to_peak: From d1516f9e004c8a216ead43ccd25ca746386670ee Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 17 Feb 2022 10:30:36 -0500 Subject: [PATCH 066/378] import ConditionOpcode from actual definition (#10132) --- chia/full_node/mempool_check_conditions.py | 2 +- chia/wallet/puzzles/puzzle_utils.py | 2 +- tests/block_tools.py | 2 +- tests/clvm/test_puzzles.py | 2 +- tests/clvm/test_singletons.py | 2 +- tests/generator/test_rom.py | 2 +- tests/wallet/test_singleton_lifecycle.py | 2 +- tests/wallet/test_singleton_lifecycle_fast.py | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/chia/full_node/mempool_check_conditions.py b/chia/full_node/mempool_check_conditions.py index 4edca8ca84eb..d14500ffbbbd 100644 --- a/chia/full_node/mempool_check_conditions.py +++ b/chia/full_node/mempool_check_conditions.py @@ -8,10 +8,10 @@ from chia.consensus.default_constants import DEFAULT_CONSTANTS from chia.full_node.generator import create_generator_args, setup_generator_args from chia.types.coin_record import CoinRecord +from chia.types.condition_opcodes import ConditionOpcode from chia.types.condition_with_args import ConditionWithArgs from chia.types.generator_types import BlockGenerator from chia.types.name_puzzle_condition import NPC -from chia.util.condition_tools import ConditionOpcode from chia.util.errors import Err from chia.util.ints import uint32, uint64, uint16 from chia.wallet.puzzles.generator_loader import GENERATOR_FOR_SINGLE_COIN_MOD diff --git a/chia/wallet/puzzles/puzzle_utils.py b/chia/wallet/puzzles/puzzle_utils.py index 1d8b72b4b2d4..cc863e092c88 100644 --- a/chia/wallet/puzzles/puzzle_utils.py +++ b/chia/wallet/puzzles/puzzle_utils.py @@ -1,6 +1,6 @@ from typing import Optional, List -from chia.util.condition_tools import ConditionOpcode +from chia.types.condition_opcodes import ConditionOpcode def make_create_coin_condition(puzzle_hash, amount, memos: Optional[List[bytes]]) -> List: diff --git a/tests/block_tools.py b/tests/block_tools.py index fc4d5143c468..0c67f8b87363 100644 --- a/tests/block_tools.py +++ b/tests/block_tools.py @@ -66,6 +66,7 @@ ) from chia.types.blockchain_format.sub_epoch_summary import SubEpochSummary from chia.types.blockchain_format.vdf import VDFInfo, VDFProof +from chia.types.condition_opcodes import ConditionOpcode from chia.types.end_of_slot_bundle import EndOfSubSlotBundle from chia.types.full_block import FullBlock from chia.types.generator_types import BlockGenerator, CompressorArg @@ -73,7 +74,6 @@ 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.condition_tools import ConditionOpcode from chia.util.config import load_config, save_config from chia.util.hash import std_hash from chia.util.ints import uint8, uint16, uint32, uint64, uint128 diff --git a/tests/clvm/test_puzzles.py b/tests/clvm/test_puzzles.py index 8f508448c96c..bd1fc237e85c 100644 --- a/tests/clvm/test_puzzles.py +++ b/tests/clvm/test_puzzles.py @@ -6,8 +6,8 @@ from chia.types.blockchain_format.program import Program from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.coin_spend import CoinSpend +from chia.types.condition_opcodes import ConditionOpcode from chia.types.spend_bundle import SpendBundle -from chia.util.condition_tools import ConditionOpcode from chia.util.hash import std_hash from chia.wallet.puzzles import ( p2_conditions, diff --git a/tests/clvm/test_singletons.py b/tests/clvm/test_singletons.py index 4b9fddd8e84e..9e6434d3f62c 100644 --- a/tests/clvm/test_singletons.py +++ b/tests/clvm/test_singletons.py @@ -8,9 +8,9 @@ from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.blockchain_format.coin import Coin from chia.types.coin_spend import CoinSpend +from chia.types.condition_opcodes import ConditionOpcode from chia.types.spend_bundle import SpendBundle from chia.util.errors import Err -from chia.util.condition_tools import ConditionOpcode from chia.util.ints import uint64 from chia.consensus.default_constants import DEFAULT_CONSTANTS from chia.wallet.lineage_proof import LineageProof diff --git a/tests/generator/test_rom.py b/tests/generator/test_rom.py index dce72b9183a9..36e7b74a2944 100644 --- a/tests/generator/test_rom.py +++ b/tests/generator/test_rom.py @@ -6,10 +6,10 @@ from chia.full_node.mempool_check_conditions import get_name_puzzle_conditions from chia.types.blockchain_format.program import Program, SerializedProgram from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.condition_opcodes import ConditionOpcode from chia.types.condition_with_args import ConditionWithArgs from chia.types.name_puzzle_condition import NPC from chia.types.generator_types import BlockGenerator -from chia.util.condition_tools import ConditionOpcode from chia.util.ints import uint32 from chia.wallet.puzzles.load_clvm import load_clvm from chia.consensus.condition_costs import ConditionCost diff --git a/tests/wallet/test_singleton_lifecycle.py b/tests/wallet/test_singleton_lifecycle.py index f98c57d164ed..d332a20c1417 100644 --- a/tests/wallet/test_singleton_lifecycle.py +++ b/tests/wallet/test_singleton_lifecycle.py @@ -10,8 +10,8 @@ from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.coin_spend import CoinSpend +from chia.types.condition_opcodes import ConditionOpcode from chia.types.spend_bundle import SpendBundle -from chia.util.condition_tools import ConditionOpcode from chia.util.ints import uint64 from chia.wallet.puzzles.load_clvm import load_clvm diff --git a/tests/wallet/test_singleton_lifecycle_fast.py b/tests/wallet/test_singleton_lifecycle_fast.py index b848416605a5..79630205ce92 100644 --- a/tests/wallet/test_singleton_lifecycle_fast.py +++ b/tests/wallet/test_singleton_lifecycle_fast.py @@ -9,8 +9,8 @@ from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.coin_spend import CoinSpend +from chia.types.condition_opcodes import ConditionOpcode from chia.types.spend_bundle import SpendBundle -from chia.util.condition_tools import ConditionOpcode from chia.util.ints import uint64 from chia.wallet.puzzles.load_clvm import load_clvm From 34159d352903c9c64030384cc9dcbdc92e0f8517 Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Thu, 17 Feb 2022 16:32:29 +0100 Subject: [PATCH 067/378] optimize get_block_generator() (#10104) * optimize get_block_generator() * add a v1 compatible get_generator() to speed up get_block_generator() with v1 databases. Add test. Add error log in case generator_from_block() fails. * speed up test_full_block_utils --- benchmarks/block_store.py | 30 +-- benchmarks/utils.py | 22 +- chia/consensus/blockchain.py | 42 ++-- chia/full_node/block_store.py | 61 ++++++ chia/util/full_block_utils.py | 208 +++++++++++++++++++ tests/block_tools.py | 2 +- tests/core/full_node/test_block_store.py | 45 +++++ tests/util/test_full_block_utils.py | 245 +++++++++++++++++++++++ 8 files changed, 616 insertions(+), 39 deletions(-) create mode 100644 chia/util/full_block_utils.py create mode 100644 tests/util/test_full_block_utils.py diff --git a/benchmarks/block_store.py b/benchmarks/block_store.py index 172e37816e8d..2190412898a4 100644 --- a/benchmarks/block_store.py +++ b/benchmarks/block_store.py @@ -8,9 +8,17 @@ from chia.util.db_wrapper import DBWrapper from chia.util.ints import uint128, uint64, uint32, uint8 -from chia.types.blockchain_format.classgroup import ClassgroupElement -from utils import rewards, rand_hash, setup_db, rand_g1, rand_g2, rand_bytes -from chia.types.blockchain_format.vdf import VDFInfo, VDFProof +from utils import ( + rewards, + rand_hash, + setup_db, + rand_g1, + rand_g2, + rand_bytes, + rand_vdf, + rand_vdf_proof, + rand_class_group_element, +) from chia.types.full_block import FullBlock from chia.consensus.block_record import BlockRecord from chia.types.blockchain_format.proof_of_space import ProofOfSpace @@ -28,22 +36,6 @@ random.seed(123456789) -def rand_class_group_element() -> ClassgroupElement: - return ClassgroupElement(rand_bytes(100)) - - -def rand_vdf() -> VDFInfo: - return VDFInfo(rand_hash(), uint64(random.randint(100000, 1000000000)), rand_class_group_element()) - - -def rand_vdf_proof() -> VDFProof: - return VDFProof( - uint8(1), # witness_type - rand_hash(), # witness - bool(random.randint(0, 1)), # normalized_to_identity - ) - - with open("clvm_generator.bin", "rb") as f: clvm_generator = f.read() diff --git a/benchmarks/utils.py b/benchmarks/utils.py index be9a8639d985..4ecf9fb98939 100644 --- a/benchmarks/utils.py +++ b/benchmarks/utils.py @@ -1,8 +1,10 @@ from chia.consensus.default_constants import DEFAULT_CONSTANTS -from chia.util.ints import uint64, uint32 +from chia.util.ints import uint64, uint32, uint8 from chia.consensus.coinbase import create_farmer_coin, create_pool_coin +from chia.types.blockchain_format.classgroup import ClassgroupElement from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.blockchain_format.vdf import VDFInfo, VDFProof from chia.util.db_wrapper import DBWrapper from typing import Tuple from pathlib import Path @@ -46,6 +48,24 @@ def rand_g2() -> G2Element: return AugSchemeMPL.sign(sk, b"foobar") +def rand_class_group_element() -> ClassgroupElement: + # TODO: address hint errors and remove ignores + # error: Argument 1 to "ClassgroupElement" has incompatible type "bytes"; expected "bytes100" [arg-type] + return ClassgroupElement(rand_bytes(100)) # type: ignore[arg-type] + + +def rand_vdf() -> VDFInfo: + return VDFInfo(rand_hash(), uint64(random.randint(100000, 1000000000)), rand_class_group_element()) + + +def rand_vdf_proof() -> VDFProof: + return VDFProof( + uint8(1), # witness_type + rand_hash(), # witness + bool(random.randint(0, 1)), # normalized_to_identity + ) + + async def setup_db(name: str, db_version: int) -> DBWrapper: db_filename = Path(name) try: diff --git a/chia/consensus/blockchain.py b/chia/consensus/blockchain.py index ddfdc1ccc169..5bde34139c6e 100644 --- a/chia/consensus/blockchain.py +++ b/chia/consensus/blockchain.py @@ -881,18 +881,22 @@ async def get_block_generator( ): # We are not in a reorg, no need to look up alternate header hashes # (we can get them from height_to_hash) - for ref_height in block.transactions_generator_ref_list: - header_hash = self.height_to_hash(ref_height) + if self.block_store.db_wrapper.db_version == 2: + # in the v2 database, we can look up blocks by height directly + # (as long as we're in the main chain) + result = await self.block_store.get_generators_at(block.transactions_generator_ref_list) + else: + for ref_height in block.transactions_generator_ref_list: + header_hash = self.height_to_hash(ref_height) - # if ref_height is invalid, this block should have failed with - # FUTURE_GENERATOR_REFS before getting here - assert header_hash is not None + # if ref_height is invalid, this block should have failed with + # FUTURE_GENERATOR_REFS before getting here + assert header_hash is not None - ref_block = await self.block_store.get_full_block(header_hash) - assert ref_block is not None - if ref_block.transactions_generator is None: - raise ValueError(Err.GENERATOR_REF_HAS_NO_GENERATOR) - result.append(ref_block.transactions_generator) + ref_gen = await self.block_store.get_generator(header_hash) + if ref_gen is None: + raise ValueError(Err.GENERATOR_REF_HAS_NO_GENERATOR) + result.append(ref_gen) else: # First tries to find the blocks in additional_blocks reorg_chain: Dict[uint32, FullBlock] = {} @@ -933,15 +937,17 @@ async def get_block_generator( else: if ref_height in additional_height_dict: ref_block = additional_height_dict[ref_height] + assert ref_block is not None + if ref_block.transactions_generator is None: + raise ValueError(Err.GENERATOR_REF_HAS_NO_GENERATOR) + result.append(ref_block.transactions_generator) else: header_hash = self.height_to_hash(ref_height) - # TODO: address hint error and remove ignore - # error: Argument 1 to "get_full_block" of "Blockchain" has incompatible type - # "Optional[bytes32]"; expected "bytes32" [arg-type] - ref_block = await self.get_full_block(header_hash) # type: ignore[arg-type] - assert ref_block is not None - if ref_block.transactions_generator is None: - raise ValueError(Err.GENERATOR_REF_HAS_NO_GENERATOR) - result.append(ref_block.transactions_generator) + if header_hash is None: + raise ValueError(Err.GENERATOR_REF_HAS_NO_GENERATOR) + gen = await self.block_store.get_generator(header_hash) + if gen is None: + raise ValueError(Err.GENERATOR_REF_HAS_NO_GENERATOR) + result.append(gen) assert len(result) == len(ref_list) return BlockGenerator(block.transactions_generator, result, []) diff --git a/chia/full_node/block_store.py b/chia/full_node/block_store.py index b3c419fe31bd..1bb180f00b67 100644 --- a/chia/full_node/block_store.py +++ b/chia/full_node/block_store.py @@ -7,10 +7,13 @@ from chia.consensus.block_record import BlockRecord from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.full_block import FullBlock +from chia.types.blockchain_format.program import SerializedProgram from chia.types.weight_proof import SubEpochChallengeSegment, SubEpochSegments +from chia.util.errors import Err from chia.util.db_wrapper import DBWrapper from chia.util.ints import uint32 from chia.util.lru_cache import LRUCache +from chia.util.full_block_utils import generator_from_block log = logging.getLogger(__name__) @@ -298,6 +301,64 @@ async def get_full_blocks_at(self, heights: List[uint32]) -> List[FullBlock]: ret.append(self.maybe_decompress(row[0])) return ret + async def get_generator(self, header_hash: bytes32) -> Optional[SerializedProgram]: + + cached = self.block_cache.get(header_hash) + if cached is not None: + log.debug(f"cache hit for block {header_hash.hex()}") + return cached.transactions_generator + + formatted_str = "SELECT block, height from full_blocks WHERE header_hash=?" + async with self.db.execute(formatted_str, (self.maybe_to_hex(header_hash),)) as cursor: + row = await cursor.fetchone() + if row is None: + return None + if self.db_wrapper.db_version == 2: + block_bytes = zstd.decompress(row[0]) + else: + block_bytes = row[0] + + try: + return generator_from_block(block_bytes) + except Exception as e: + log.error(f"cheap parser failed for block at height {row[1]}: {e}") + # this is defensive, on the off-chance that + # generator_from_block() fails, fall back to the reliable + # definition of parsing a block + b = FullBlock.from_bytes(block_bytes) + return b.transactions_generator + + async def get_generators_at(self, heights: List[uint32]) -> List[SerializedProgram]: + assert self.db_wrapper.db_version == 2 + + if len(heights) == 0: + return [] + + generators: Dict[uint32, SerializedProgram] = {} + heights_db = tuple(heights) + formatted_str = ( + f"SELECT block, height from full_blocks " + f'WHERE in_main_chain=1 AND height in ({"?," * (len(heights_db) - 1)}?)' + ) + async with self.db.execute(formatted_str, heights_db) as cursor: + async for row in cursor: + block_bytes = zstd.decompress(row[0]) + + try: + gen = generator_from_block(block_bytes) + except Exception as e: + log.error(f"cheap parser failed for block at height {row[1]}: {e}") + # this is defensive, on the off-chance that + # generator_from_block() fails, fall back to the reliable + # definition of parsing a block + b = FullBlock.from_bytes(block_bytes) + gen = b.transactions_generator + if gen is None: + raise ValueError(Err.GENERATOR_REF_HAS_NO_GENERATOR) + generators[uint32(row[1])] = gen + + return [generators[h] for h in heights] + async def get_block_records_by_hash(self, header_hashes: List[bytes32]): """ Returns a list of Block Records, ordered by the same order in which header_hashes are passed in. diff --git a/chia/util/full_block_utils.py b/chia/util/full_block_utils.py new file mode 100644 index 000000000000..85d2cbf3c246 --- /dev/null +++ b/chia/util/full_block_utils.py @@ -0,0 +1,208 @@ +from typing import Optional, Callable + +from chia.types.blockchain_format.program import SerializedProgram +from clvm_rs import serialized_length +from blspy import G1Element, G2Element + + +def skip_list(buf: memoryview, skip_item: Callable[[memoryview], memoryview]) -> memoryview: + n = int.from_bytes(buf[:4], "big", signed=False) + buf = buf[4:] + for i in range(n): + buf = skip_item(buf) + return buf + + +def skip_bytes(buf: memoryview) -> memoryview: + n = int.from_bytes(buf[:4], "big", signed=False) + buf = buf[4:] + assert n >= 0 + return buf[n:] + + +def skip_optional(buf: memoryview, skip_item: Callable[[memoryview], memoryview]) -> memoryview: + + if buf[0] == 0: + return buf[1:] + assert buf[0] == 1 + return skip_item(buf[1:]) + + +def skip_bytes32(buf: memoryview) -> memoryview: + return buf[32:] + + +def skip_uint32(buf: memoryview) -> memoryview: + return buf[4:] + + +def skip_uint64(buf: memoryview) -> memoryview: + return buf[8:] + + +def skip_uint128(buf: memoryview) -> memoryview: + return buf[16:] + + +def skip_uint8(buf: memoryview) -> memoryview: + return buf[1:] + + +def skip_bool(buf: memoryview) -> memoryview: + assert buf[0] in [0, 1] + return buf[1:] + + +# def skip_class_group_element(buf: memoryview) -> memoryview: +# return buf[100:] # bytes100 + + +def skip_vdf_info(buf: memoryview) -> memoryview: + # buf = skip_bytes32(buf) + # buf = skip_uint64(buf) + # return skip_class_group_element(buf) + return buf[32 + 8 + 100 :] + + +def skip_vdf_proof(buf: memoryview) -> memoryview: + buf = skip_uint8(buf) # witness_type + buf = skip_bytes(buf) # witness + return skip_bool(buf) # normalized_to_identity + + +def skip_challenge_chain_sub_slot(buf: memoryview) -> memoryview: + buf = skip_vdf_info(buf) + buf = skip_optional(buf, skip_bytes32) # infused challenge chain sub skit hash + buf = skip_optional(buf, skip_bytes32) # subepoch_summary_hash + buf = skip_optional(buf, skip_uint64) # new_sub_slot_iters + return skip_optional(buf, skip_uint64) # new_difficulty + + +def skip_infused_challenge_chain(buf: memoryview) -> memoryview: + return skip_vdf_info(buf) # infused_challenge_chain_end_of_slot_vdf + + +def skip_reward_chain_sub_slot(buf: memoryview) -> memoryview: + buf = skip_vdf_info(buf) # end_of_slot_vdf + buf = skip_bytes32(buf) # challenge_chain_sub_slot_hash + buf = skip_optional(buf, skip_bytes32) # infused_challenge_chain_sub_slot_hash + return skip_uint8(buf) + + +def skip_sub_slot_proofs(buf: memoryview) -> memoryview: + buf = skip_vdf_proof(buf) # challenge_chain_slot_proof + buf = skip_optional(buf, skip_vdf_proof) # infused_challenge_chain_slot_proof + return skip_vdf_proof(buf) # reward_chain_slot_proof + + +def skip_end_of_sub_slot_bundle(buf: memoryview) -> memoryview: + buf = skip_challenge_chain_sub_slot(buf) + buf = skip_optional(buf, skip_infused_challenge_chain) + buf = skip_reward_chain_sub_slot(buf) + return skip_sub_slot_proofs(buf) + + +def skip_g1_element(buf: memoryview) -> memoryview: + return buf[G1Element.SIZE :] + + +def skip_g2_element(buf: memoryview) -> memoryview: + return buf[G2Element.SIZE :] + + +def skip_proof_of_space(buf: memoryview) -> memoryview: + buf = skip_bytes32(buf) # challenge + buf = skip_optional(buf, skip_g1_element) # pool_public_key + buf = skip_optional(buf, skip_bytes32) # pool_contract_puzzle_hash + buf = skip_g1_element(buf) # plot_public_key + buf = skip_uint8(buf) # size + return skip_bytes(buf) # proof + + +def skip_reward_chain_block(buf: memoryview) -> memoryview: + buf = skip_uint128(buf) # weight + buf = skip_uint32(buf) # height + buf = skip_uint128(buf) # total_iters + buf = skip_uint8(buf) # signage_point_index + buf = skip_bytes32(buf) # pos_ss_cc_challenge_hash + + buf = skip_proof_of_space(buf) # proof_of_space + buf = skip_optional(buf, skip_vdf_info) # challenge_chain_sp_vdf + buf = skip_g2_element(buf) # challenge_chain_sp_signature + buf = skip_vdf_info(buf) # challenge_chain_ip_vdf + buf = skip_optional(buf, skip_vdf_info) # reward_chain_sp_vdf + buf = skip_g2_element(buf) # reward_chain_sp_signature + buf = skip_vdf_info(buf) # reward_chain_ip_vdf + buf = skip_optional(buf, skip_vdf_info) # infused_challenge_chain_ip_vdf + return skip_bool(buf) # is_transaction_block + + +def skip_pool_target(buf: memoryview) -> memoryview: + # buf = skip_bytes32(buf) # puzzle_hash + # return skip_uint32(buf) # max_height + return buf[32 + 4 :] + + +def skip_foliage_block_data(buf: memoryview) -> memoryview: + buf = skip_bytes32(buf) # unfinished_reward_block_hash + buf = skip_pool_target(buf) # pool_target + buf = skip_optional(buf, skip_g2_element) # pool_signature + buf = skip_bytes32(buf) # farmer_reward_puzzle_hash + return skip_bytes32(buf) # extension_data + + +def skip_foliage(buf: memoryview) -> memoryview: + buf = skip_bytes32(buf) # prev_block_hash + buf = skip_bytes32(buf) # reward_block_hash + buf = skip_foliage_block_data(buf) # foliage_block_data + buf = skip_g2_element(buf) # foliage_block_data_signature + buf = skip_optional(buf, skip_bytes32) # foliage_transaction_block_hash + return skip_optional(buf, skip_g2_element) # foliage_transaction_block_signature + + +def skip_foliage_transaction_block(buf: memoryview) -> memoryview: + # buf = skip_bytes32(buf) # prev_transaction_block_hash + # buf = skip_uint64(buf) # timestamp + # buf = skip_bytes32(buf) # filter_hash + # buf = skip_bytes32(buf) # additions_root + # buf = skip_bytes32(buf) # removals_root + # return skip_bytes32(buf) # transactions_info_hash + return buf[32 + 8 + 32 + 32 + 32 + 32 :] + + +def skip_coin(buf: memoryview) -> memoryview: + # buf = skip_bytes32(buf) # parent_coin_info + # buf = skip_bytes32(buf) # puzzle_hash + # return skip_uint64(buf) # amount + return buf[32 + 32 + 8 :] + + +def skip_transactions_info(buf: memoryview) -> memoryview: + # buf = skip_bytes32(buf) # generator_root + # buf = skip_bytes32(buf) # generator_refs_root + # buf = skip_g2_element(buf) # aggregated_signature + # buf = skip_uint64(buf) # fees + # buf = skip_uint64(buf) # cost + buf = buf[32 + 32 + G2Element.SIZE + 8 + 8 :] + return skip_list(buf, skip_coin) + + +def generator_from_block(buf: memoryview) -> Optional[SerializedProgram]: + buf = skip_list(buf, skip_end_of_sub_slot_bundle) # finished_sub_slots + buf = skip_reward_chain_block(buf) # reward_chain_block + buf = skip_optional(buf, skip_vdf_proof) # challenge_chain_sp_proof + buf = skip_vdf_proof(buf) # challenge_chain_ip_proof + buf = skip_optional(buf, skip_vdf_proof) # reward_chain_sp_proof + buf = skip_vdf_proof(buf) # reward_chain_ip_proof + buf = skip_optional(buf, skip_vdf_proof) # infused_challenge_chain_ip_proof + buf = skip_foliage(buf) # foliage + buf = skip_optional(buf, skip_foliage_transaction_block) # foliage_transaction_block + buf = skip_optional(buf, skip_transactions_info) # transactions_info + + # this is the transactions_generator optional + if buf[0] == 0: + return None + + buf = buf[1:] + length = serialized_length(buf) + return SerializedProgram.from_bytes(bytes(buf[:length])) diff --git a/tests/block_tools.py b/tests/block_tools.py index 0c67f8b87363..7b9f95ae6a9c 100644 --- a/tests/block_tools.py +++ b/tests/block_tools.py @@ -2003,7 +2003,7 @@ def create_test_unfinished_block( foliage_transaction_block, transactions_info, block_generator.program if block_generator else None, - block_generator.block_height_list if block_generator else [], # TODO: can block_generator ever be None? + block_generator.block_height_list if block_generator else [], ) diff --git a/tests/core/full_node/test_block_store.py b/tests/core/full_node/test_block_store.py index 180865cc251b..b33f759d9211 100644 --- a/tests/core/full_node/test_block_store.py +++ b/tests/core/full_node/test_block_store.py @@ -5,13 +5,17 @@ import dataclasses import pytest +from clvm.casts import int_to_bytes from chia.consensus.blockchain import Blockchain +from chia.consensus.full_block_to_block_record import header_block_to_sub_block_record +from chia.consensus.default_constants import DEFAULT_CONSTANTS from chia.full_node.block_store import BlockStore from chia.full_node.coin_store import CoinStore from chia.full_node.hint_store import HintStore from chia.util.ints import uint8 from chia.types.blockchain_format.vdf import VDFProof +from chia.types.blockchain_format.program import SerializedProgram from tests.blockchain.blockchain_test_utils import _validate_and_add_block from tests.util.db_connection import DBConnection from tests.setup_nodes import bt, test_constants @@ -221,3 +225,44 @@ def rand_vdf_proof() -> VDFProof: block_store.rollback_cache_block(block.header_hash) b = await block_store.get_full_block(block.header_hash) assert b.challenge_chain_ip_proof == proof + + @pytest.mark.asyncio + async def test_get_generator(self, db_version): + blocks = bt.get_consecutive_blocks(10) + + def generator(i: int) -> SerializedProgram: + return SerializedProgram.from_bytes(int_to_bytes(i)) + + async with DBConnection(db_version) as db_wrapper: + store = await BlockStore.create(db_wrapper) + + new_blocks = [] + for i, block in enumerate(blocks): + block = dataclasses.replace(block, transactions_generator=generator(i)) + block_record = header_block_to_sub_block_record( + DEFAULT_CONSTANTS, 0, block, 0, False, 0, max(0, block.height - 1), None + ) + await store.add_full_block(block.header_hash, block, block_record) + await store.set_in_chain([(block_record.header_hash,)]) + await store.set_peak(block_record.header_hash) + new_blocks.append(block) + + if db_version == 2: + expected_generators = list(map(lambda x: x.transactions_generator, new_blocks[1:10])) + generators = await store.get_generators_at(range(1, 10)) + assert generators == expected_generators + + # test out-of-order heights + expected_generators = list( + map(lambda x: x.transactions_generator, [new_blocks[i] for i in [4, 8, 3, 9]]) + ) + generators = await store.get_generators_at([4, 8, 3, 9]) + assert generators == expected_generators + + with pytest.raises(KeyError): + await store.get_generators_at([100]) + + assert await store.get_generator(blocks[2].header_hash) == new_blocks[2].transactions_generator + assert await store.get_generator(blocks[4].header_hash) == new_blocks[4].transactions_generator + assert await store.get_generator(blocks[6].header_hash) == new_blocks[6].transactions_generator + assert await store.get_generator(blocks[7].header_hash) == new_blocks[7].transactions_generator diff --git a/tests/util/test_full_block_utils.py b/tests/util/test_full_block_utils.py new file mode 100644 index 000000000000..b51170c3f019 --- /dev/null +++ b/tests/util/test_full_block_utils.py @@ -0,0 +1,245 @@ +import random +import pytest + +from chia.util.full_block_utils import generator_from_block +from chia.types.full_block import FullBlock +from chia.util.ints import uint128, uint64, uint32, uint8 +from chia.types.blockchain_format.pool_target import PoolTarget +from chia.types.blockchain_format.foliage import Foliage, FoliageTransactionBlock, TransactionsInfo, FoliageBlockData +from chia.types.blockchain_format.proof_of_space import ProofOfSpace +from chia.types.blockchain_format.reward_chain_block import RewardChainBlock +from chia.types.blockchain_format.program import SerializedProgram +from chia.types.blockchain_format.slots import ( + ChallengeChainSubSlot, + InfusedChallengeChainSubSlot, + RewardChainSubSlot, + SubSlotProofs, +) +from chia.types.end_of_slot_bundle import EndOfSubSlotBundle + +from benchmarks.utils import rand_hash, rand_bytes, rewards, rand_g1, rand_g2, rand_vdf, rand_vdf_proof + +test_g2s = [rand_g2() for _ in range(10)] +test_g1s = [rand_g1() for _ in range(10)] +test_hashes = [rand_hash() for _ in range(100)] +test_vdfs = [rand_vdf() for _ in range(100)] +test_vdf_proofs = [rand_vdf_proof() for _ in range(100)] + + +def g2(): + return random.sample(test_g2s, 1)[0] + + +def g1(): + return random.sample(test_g1s, 1)[0] + + +def hsh(): + return random.sample(test_hashes, 1)[0] + + +def vdf(): + return random.sample(test_vdfs, 1)[0] + + +def vdf_proof(): + return random.sample(test_vdf_proofs, 1)[0] + + +def get_proof_of_space(): + for pool_pk in [g1(), None]: + for plot_hash in [hsh(), None]: + yield ProofOfSpace( + hsh(), # challenge + pool_pk, + plot_hash, + g1(), # plot_public_key + uint8(32), + rand_bytes(8 * 32), + ) + + +def get_reward_chain_block(height): + for has_transactions in [True, False]: + for challenge_chain_sp_vdf in [vdf(), None]: + for reward_chain_sp_vdf in [vdf(), None]: + for infused_challenge_chain_ip_vdf in [vdf(), None]: + for proof_of_space in get_proof_of_space(): + weight = uint128(random.randint(0, 1000000000)) + iters = uint128(123456) + sp_index = uint8(0) + yield RewardChainBlock( + weight, + uint32(height), + iters, + sp_index, + hsh(), # pos_ss_cc_challenge_hash + proof_of_space, + challenge_chain_sp_vdf, + g2(), # challenge_chain_sp_signature + vdf(), # challenge_chain_ip_vdf + reward_chain_sp_vdf, + g2(), # reward_chain_sp_signature + vdf(), # reward_chain_ip_vdf + infused_challenge_chain_ip_vdf, + has_transactions, + ) + + +def get_foliage_block_data(): + for pool_signature in [g2(), None]: + pool_target = PoolTarget( + hsh(), # puzzle_hash + uint32(0), # max_height + ) + + yield FoliageBlockData( + hsh(), # unfinished_reward_block_hash + pool_target, + pool_signature, # pool_signature + hsh(), # farmer_reward_puzzle_hash + hsh(), # extension_data + ) + + +def get_foliage(): + for foliage_block_data in get_foliage_block_data(): + for foliage_transaction_block_hash in [hsh(), None]: + for foliage_transaction_block_signature in [g2(), None]: + yield Foliage( + hsh(), # prev_block_hash + hsh(), # reward_block_hash + foliage_block_data, + g2(), # foliage_block_data_signature + foliage_transaction_block_hash, + foliage_transaction_block_signature, + ) + + +def get_foliage_transaction_block(): + yield None + timestamp = uint64(1631794488) + yield FoliageTransactionBlock( + hsh(), # prev_transaction_block + timestamp, + hsh(), # filter_hash + hsh(), # additions_root + hsh(), # removals_root + hsh(), # transactions_info_hash + ) + + +def get_transactions_info(height): + yield None + farmer_coin, pool_coin = rewards(uint32(height)) + reward_claims_incorporated = [farmer_coin, pool_coin] + fees = uint64(random.randint(0, 150000)) + + yield TransactionsInfo( + hsh(), # generator_root + hsh(), # generator_refs_root + g2(), # aggregated_signature + fees, + uint64(random.randint(0, 12000000000)), # cost + reward_claims_incorporated, + ) + + +def get_challenge_chain_sub_slot(): + for infused_chain_sub_slot_hash in [hsh(), None]: + for sub_epoch_summary_hash in [hsh(), None]: + for new_sub_slot_iters in [uint64(random.randint(0, 4000000000)), None]: + for new_difficulty in [uint64(random.randint(1, 30)), None]: + yield ChallengeChainSubSlot( + vdf(), # challenge_chain_end_of_slot_vdf + infused_chain_sub_slot_hash, + sub_epoch_summary_hash, + new_sub_slot_iters, + new_difficulty, + ) + + +def get_reward_chain_sub_slot(): + for infused_challenge_chain_sub_slot_hash in [hsh(), None]: + yield RewardChainSubSlot( + vdf(), # end_of_slot_vdf + hsh(), # challenge_chain_sub_slot_hash + infused_challenge_chain_sub_slot_hash, + uint8(random.randint(0, 255)), # deficit + ) + + +def get_sub_slot_proofs(): + for infused_challenge_chain_slot_proof in [vdf_proof(), None]: + yield SubSlotProofs( + vdf_proof(), # challenge_chain_slot_proof + infused_challenge_chain_slot_proof, + vdf_proof(), # reward_chain_slot_proof + ) + + +def get_end_of_sub_slot(): + for challenge_chain in get_challenge_chain_sub_slot(): + for infused_challenge_chain in [InfusedChallengeChainSubSlot(vdf()), None]: + for reward_chain in get_reward_chain_sub_slot(): + for proofs in get_sub_slot_proofs(): + yield EndOfSubSlotBundle( + challenge_chain, + infused_challenge_chain, + reward_chain, + proofs, + ) + + +def get_finished_sub_slots(): + yield [] + yield [s for s in get_end_of_sub_slot()] + + +def get_full_blocks(): + + random.seed(123456789) + + generator = SerializedProgram.from_bytes(bytes.fromhex("ff01820539")) + + for foliage in get_foliage(): + for foliage_transaction_block in get_foliage_transaction_block(): + height = random.randint(0, 1000000) + for reward_chain_block in get_reward_chain_block(height): + for transactions_info in get_transactions_info(height): + for challenge_chain_sp_proof in [vdf_proof(), None]: + for reward_chain_sp_proof in [vdf_proof(), None]: + for infused_challenge_chain_ip_proof in [vdf_proof(), None]: + for finished_sub_slots in get_finished_sub_slots(): + + yield FullBlock( + finished_sub_slots, + reward_chain_block, + challenge_chain_sp_proof, + vdf_proof(), # challenge_chain_ip_proof + reward_chain_sp_proof, + vdf_proof(), # reward_chain_ip_proof + infused_challenge_chain_ip_proof, + foliage, + foliage_transaction_block, + transactions_info, + generator, # transactions_generator + [], # transactions_generator_ref_list + ) + + +class TestFullBlockParser: + @pytest.mark.asyncio + async def test_parser(self): + + # loop over every combination of Optionals being set and not set + # along with random values for the FullBlock fields. Ensure + # generator_from_block() successfully parses out the generator object + # correctly + for block in get_full_blocks(): + + block_bytes = bytes(block) + gen = generator_from_block(block_bytes) + assert gen == block.transactions_generator + # this doubles the run-time of this test, with questionable utility + # assert gen == FullBlock.from_bytes(block_bytes).transactions_generator From f6e479cc0784a67f37d4e61df5f1c8497278e4d3 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 17 Feb 2022 10:35:13 -0500 Subject: [PATCH 068/378] Improve StructStream hints to support decimal and other parameters like int itself (#9949) * Improve StructStream hints to support decimal and other parameters like int itself * get SupportsIndex from typing_extensions --- chia/util/struct_stream.py | 18 ++++++++++++++++-- tests/util/test_struct_stream.py | 13 +++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/chia/util/struct_stream.py b/chia/util/struct_stream.py index c62e008564f1..e1c6ae42cefe 100644 --- a/chia/util/struct_stream.py +++ b/chia/util/struct_stream.py @@ -1,6 +1,16 @@ import io import struct -from typing import Any, BinaryIO +from typing import Any, BinaryIO, SupportsInt, Type, TypeVar, Union +from typing_extensions import SupportsIndex, Protocol + + +_T_StructStream = TypeVar("_T_StructStream", bound="StructStream") + + +# https://github.com/python/typeshed/blob/c2182fdd3e572a1220c70ad9c28fd908b70fb19b/stdlib/_typeshed/__init__.pyi#L68-L69 +class SupportsTrunc(Protocol): + def __trunc__(self) -> int: + ... class StructStream(int): @@ -10,7 +20,11 @@ class StructStream(int): Create a class that can parse and stream itself based on a struct.pack template string. """ - def __new__(cls: Any, value: int): + # This is just a partial exposure of the underlying int constructor. Liskov... + # https://github.com/python/typeshed/blob/5d07ebc864577c04366fcc46b84479dbec033921/stdlib/builtins.pyi#L181-L185 + def __new__( + cls: Type[_T_StructStream], value: Union[str, bytes, SupportsInt, SupportsIndex, SupportsTrunc] + ) -> _T_StructStream: value = int(value) try: v1 = struct.unpack(cls.PACK, struct.pack(cls.PACK, value))[0] diff --git a/tests/util/test_struct_stream.py b/tests/util/test_struct_stream.py index d56afc7a82a6..59a7fd368688 100644 --- a/tests/util/test_struct_stream.py +++ b/tests/util/test_struct_stream.py @@ -1,3 +1,4 @@ +from decimal import Decimal import pytest import io @@ -106,3 +107,15 @@ def roundtrip(v): roundtrip(int8(0x7F)) roundtrip(int8(-0x80)) + + def test_uint32_from_decimal(self) -> None: + assert uint32(Decimal("137")) == 137 + + def test_uint32_from_float(self) -> None: + assert uint32(4.0) == 4 + + def test_uint32_from_str(self) -> None: + assert uint32("43") == 43 + + def test_uint32_from_bytes(self) -> None: + assert uint32(b"273") == 273 From 974e29c0dd6db892eecaf06b93094714fdaffc5b Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Thu, 17 Feb 2022 09:10:17 -0700 Subject: [PATCH 069/378] Create a CAT lineage store (#10181) * Create a CAT lineage store * Lint * Fix migration * Forgot await * Bad SQL statement * fix precommit Co-authored-by: wjblanke --- chia/wallet/cat_wallet/cat_info.py | 9 +++ chia/wallet/cat_wallet/cat_wallet.py | 59 +++++++------- chia/wallet/cat_wallet/lineage_store.py | 91 ++++++++++++++++++++++ chia/wallet/puzzles/genesis_checkers.py | 2 +- chia/wallet/puzzles/tails.py | 2 +- tests/wallet/cat_wallet/test_cat_wallet.py | 16 ++++ 6 files changed, 149 insertions(+), 30 deletions(-) create mode 100644 chia/wallet/cat_wallet/lineage_store.py diff --git a/chia/wallet/cat_wallet/cat_info.py b/chia/wallet/cat_wallet/cat_info.py index 6fa33af89528..9e2d6eadf5cf 100644 --- a/chia/wallet/cat_wallet/cat_info.py +++ b/chia/wallet/cat_wallet/cat_info.py @@ -12,4 +12,13 @@ class CATInfo(Streamable): limitations_program_hash: bytes32 my_tail: Optional[Program] # this is the program + + +# We used to store all of the lineage proofs here but it was very slow to serialize for a lot of transactions +# so we moved it to CATLineageStore. We keep this around for migration purposes. +@dataclass(frozen=True) +@streamable +class LegacyCATInfo(Streamable): + limitations_program_hash: bytes32 + my_tail: Optional[Program] # this is the program lineage_proofs: List[Tuple[bytes32, Optional[LineageProof]]] # {coin.name(): lineage_proof} diff --git a/chia/wallet/cat_wallet/cat_wallet.py b/chia/wallet/cat_wallet/cat_wallet.py index aa338b87824e..c70be0de951d 100644 --- a/chia/wallet/cat_wallet/cat_wallet.py +++ b/chia/wallet/cat_wallet/cat_wallet.py @@ -24,7 +24,7 @@ from chia.util.condition_tools import conditions_dict_for_solution, pkm_pairs_for_conditions_dict from chia.util.ints import uint8, uint32, uint64, uint128 from chia.wallet.cat_wallet.cat_constants import DEFAULT_CATS -from chia.wallet.cat_wallet.cat_info import CATInfo +from chia.wallet.cat_wallet.cat_info import CATInfo, LegacyCATInfo from chia.wallet.cat_wallet.cat_utils import ( CAT_MOD, SpendableCAT, @@ -33,6 +33,7 @@ match_cat_puzzle, ) from chia.wallet.derivation_record import DerivationRecord +from chia.wallet.cat_wallet.lineage_store import CATLineageStore from chia.wallet.lineage_proof import LineageProof from chia.wallet.payment import Payment from chia.wallet.puzzles.genesis_checkers import ALL_LIMITATIONS_PROGRAMS @@ -60,6 +61,7 @@ class CATWallet: cat_info: CATInfo standard_wallet: Wallet cost_of_single_tx: Optional[int] + lineage_store: CATLineageStore @staticmethod async def create_new_cat_wallet( @@ -77,12 +79,14 @@ async def create_new_cat_wallet( # We use 00 bytes because it's not optional. We must check this is overidden during issuance. empty_bytes = bytes32(32 * b"\0") - self.cat_info = CATInfo(empty_bytes, None, []) + self.cat_info = CATInfo(empty_bytes, None) info_as_string = bytes(self.cat_info).hex() self.wallet_info = await wallet_state_manager.user_store.create_wallet(name, WalletType.CAT, info_as_string) if self.wallet_info is None: raise ValueError("Internal Error") + self.lineage_store = await CATLineageStore.create(self.wallet_state_manager.db_wrapper, self.get_asset_id()) + try: chia_tx, spend_bundle = await ALL_LIMITATIONS_PROGRAMS[ cat_tail_info["identifier"] @@ -92,7 +96,6 @@ async def create_new_cat_wallet( amount, ) assert self.cat_info.limitations_program_hash != empty_bytes - assert self.cat_info.lineage_proofs != [] except Exception: await wallet_state_manager.user_store.delete_wallet(self.id(), False) raise @@ -162,12 +165,13 @@ async def create_wallet_for_cat( name = cat_info["name"] limitations_program_hash = bytes32(hexstr_to_bytes(limitations_program_hash_hex)) - self.cat_info = CATInfo(limitations_program_hash, None, []) + self.cat_info = CATInfo(limitations_program_hash, None) info_as_string = bytes(self.cat_info).hex() self.wallet_info = await wallet_state_manager.user_store.create_wallet(name, WalletType.CAT, info_as_string) if self.wallet_info is None: raise Exception("wallet_info is None") + self.lineage_store = await CATLineageStore.create(self.wallet_state_manager.db_wrapper, self.get_asset_id()) await self.wallet_state_manager.add_new_wallet(self, self.id()) return self @@ -185,7 +189,18 @@ async def create( self.wallet_state_manager = wallet_state_manager self.wallet_info = wallet_info self.standard_wallet = wallet - self.cat_info = CATInfo.from_bytes(hexstr_to_bytes(self.wallet_info.data)) + try: + self.cat_info = CATInfo.from_bytes(hexstr_to_bytes(self.wallet_info.data)) + self.lineage_store = await CATLineageStore.create(self.wallet_state_manager.db_wrapper, self.get_asset_id()) + except AssertionError: + # Do a migration of the lineage proofs + cat_info = LegacyCATInfo.from_bytes(hexstr_to_bytes(self.wallet_info.data)) + self.cat_info = CATInfo(cat_info.limitations_program_hash, cat_info.my_tail) + self.lineage_store = await CATLineageStore.create(self.wallet_state_manager.db_wrapper, self.get_asset_id()) + for coin_id, lineage in cat_info.lineage_proofs: + await self.add_lineage(coin_id, lineage) + await self.save_info(self.cat_info, False) + return self @classmethod @@ -261,7 +276,8 @@ async def set_tail_program(self, tail_program: str): assert Program.fromhex(tail_program).get_tree_hash() == self.cat_info.limitations_program_hash await self.save_info( CATInfo( - self.cat_info.limitations_program_hash, Program.fromhex(tail_program), self.cat_info.lineage_proofs + self.cat_info.limitations_program_hash, + Program.fromhex(tail_program), ), False, ) @@ -269,18 +285,14 @@ async def set_tail_program(self, tail_program: str): async def coin_added(self, coin: Coin, height: uint32): """Notification from wallet state manager that wallet has been received.""" self.log.info(f"CC wallet has been notified that {coin} was added") - search_for_parent: bool = True inner_puzzle = await self.inner_puzzle_for_cc_puzhash(coin.puzzle_hash) lineage_proof = LineageProof(coin.parent_coin_info, inner_puzzle.get_tree_hash(), coin.amount) - await self.add_lineage(coin.name(), lineage_proof, True) + await self.add_lineage(coin.name(), lineage_proof) - for name, lineage_proofs in self.cat_info.lineage_proofs: - if coin.parent_coin_info == name: - search_for_parent = False - break + lineage = await self.get_lineage_proof_for_coin(coin) - if search_for_parent: + if lineage is None: for node_id, node in self.wallet_state_manager.wallet_node.server.all_connections.items(): try: coin_state = await self.wallet_state_manager.wallet_node.get_coin_state( @@ -480,10 +492,7 @@ async def convert_puzzle_hash(self, puzzle_hash: bytes32) -> bytes32: return (await self.inner_puzzle_for_cc_puzhash(puzzle_hash)).get_tree_hash() async def get_lineage_proof_for_coin(self, coin) -> Optional[LineageProof]: - for name, proof in self.cat_info.lineage_proofs: - if name == coin.parent_coin_info: - return proof - return None + return await self.lineage_store.get_lineage_proof(coin.parent_coin_info) async def create_tandem_xch_tx( self, @@ -740,24 +749,18 @@ async def generate_signed_transaction( return tx_list - async def add_lineage(self, name: bytes32, lineage: Optional[LineageProof], in_transaction=False): + async def add_lineage(self, name: bytes32, lineage: Optional[LineageProof]): """ Lineage proofs are stored as a list of parent coins and the lineage proof you will need if they are the parent of the coin you are trying to spend. 'If I'm your parent, here's the info you need to spend yourself' """ self.log.info(f"Adding parent {name}: {lineage}") - current_list = self.cat_info.lineage_proofs.copy() - if (name, lineage) not in current_list: - current_list.append((name, lineage)) - cat_info: CATInfo = CATInfo(self.cat_info.limitations_program_hash, self.cat_info.my_tail, current_list) - await self.save_info(cat_info, in_transaction) + if lineage is not None: + await self.lineage_store.add_lineage_proof(name, lineage) - async def remove_lineage(self, name: bytes32, in_transaction=False): + async def remove_lineage(self, name: bytes32): self.log.info(f"Removing parent {name} (probably had a non-CAT parent)") - current_list = self.cat_info.lineage_proofs.copy() - current_list = list(filter(lambda tup: tup[0] != name, current_list)) - cat_info: CATInfo = CATInfo(self.cat_info.limitations_program_hash, self.cat_info.my_tail, current_list) - await self.save_info(cat_info, in_transaction) + await self.lineage_store.remove_lineage_proof(name) async def save_info(self, cat_info: CATInfo, in_transaction): self.cat_info = cat_info diff --git a/chia/wallet/cat_wallet/lineage_store.py b/chia/wallet/cat_wallet/lineage_store.py new file mode 100644 index 000000000000..b5eceee1dcbd --- /dev/null +++ b/chia/wallet/cat_wallet/lineage_store.py @@ -0,0 +1,91 @@ +import asyncio +import logging +from typing import Dict, Optional + +import aiosqlite + +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.util.db_wrapper import DBWrapper +from chia.wallet.lineage_proof import LineageProof + +log = logging.getLogger(__name__) + + +class CATLineageStore: + """ + WalletPuzzleStore keeps track of all generated puzzle_hashes and their derivation path / wallet. + This is only used for HD wallets where each address is derived from a public key. Otherwise, use the + WalletInterestedStore to keep track of puzzle hashes which we are interested in. + """ + + db_connection: aiosqlite.Connection + lock: asyncio.Lock + db_wrapper: DBWrapper + table_name: str + + @classmethod + async def create(cls, db_wrapper: DBWrapper, asset_id: str): + self = cls() + self.table_name = f"lineage_proofs_{asset_id}" + self.db_wrapper = db_wrapper + self.db_connection = self.db_wrapper.db + await self.db_connection.execute( + (f"CREATE TABLE IF NOT EXISTS {self.table_name}(" " coin_id text PRIMARY_KEY," " lineage blob)") + ) + + await self.db_connection.commit() + # Lock + self.lock = asyncio.Lock() # external + return self + + async def close(self): + await self.db_connection.close() + + async def _clear_database(self): + cursor = await self.db_connection.execute(f"DELETE FROM {self.table_name}") + await cursor.close() + await self.db_connection.commit() + + async def add_lineage_proof(self, coin_id: bytes32, lineage: LineageProof) -> None: + cursor = await self.db_connection.execute( + f"INSERT OR REPLACE INTO {self.table_name} VALUES(?, ?)", + (coin_id.hex(), bytes(lineage)), + ) + + await cursor.close() + await self.db_connection.commit() + + async def remove_lineage_proof(self, coin_id: bytes32) -> None: + cursor = await self.db_connection.execute( + f"DELETE FROM {self.table_name} WHERE coin_id=?;", + (coin_id.hex(),), + ) + + await cursor.close() + await self.db_connection.commit() + + async def get_lineage_proof(self, coin_id: bytes32) -> Optional[LineageProof]: + + cursor = await self.db_connection.execute( + f"SELECT * FROM {self.table_name} WHERE coin_id=?;", + (coin_id.hex(),), + ) + row = await cursor.fetchone() + await cursor.close() + + if row is not None and row[0] is not None: + return LineageProof.from_bytes(row[1]) + + return None + + async def get_all_lineage_proofs(self) -> Dict[bytes32, LineageProof]: + cursor = await self.db_connection.execute(f"SELECT * FROM {self.table_name}") + rows = await cursor.fetchall() + await cursor.close() + + lineage_dict = {} + + for row in rows: + lineage_dict[bytes32.from_hexstr(row[0])] = LineageProof.from_bytes(row[1]) + + return lineage_dict diff --git a/chia/wallet/puzzles/genesis_checkers.py b/chia/wallet/puzzles/genesis_checkers.py index 7ba72af62a53..cbb02b6f9f56 100644 --- a/chia/wallet/puzzles/genesis_checkers.py +++ b/chia/wallet/puzzles/genesis_checkers.py @@ -106,7 +106,7 @@ async def generate_issuance_bundle(cls, wallet, _: Dict, amount: uint64) -> Tupl if wallet.cat_info.my_tail is None: await wallet.save_info( - CATInfo(genesis_coin_checker.get_tree_hash(), genesis_coin_checker, wallet.cat_info.lineage_proofs), + CATInfo(genesis_coin_checker.get_tree_hash(), genesis_coin_checker), False, ) diff --git a/chia/wallet/puzzles/tails.py b/chia/wallet/puzzles/tails.py index f6321e67b8ef..b2c9fa960197 100644 --- a/chia/wallet/puzzles/tails.py +++ b/chia/wallet/puzzles/tails.py @@ -104,7 +104,7 @@ async def generate_issuance_bundle(cls, wallet, _: Dict, amount: uint64) -> Tupl if wallet.cat_info.my_tail is None: await wallet.save_info( - CATInfo(tail.get_tree_hash(), tail, wallet.cat_info.lineage_proofs), + CATInfo(tail.get_tree_hash(), tail), False, ) diff --git a/tests/wallet/cat_wallet/test_cat_wallet.py b/tests/wallet/cat_wallet/test_cat_wallet.py index 9f8b42ea89c9..5700cc8f6ccc 100644 --- a/tests/wallet/cat_wallet/test_cat_wallet.py +++ b/tests/wallet/cat_wallet/test_cat_wallet.py @@ -14,8 +14,10 @@ from chia.wallet.cat_wallet.cat_utils import construct_cat_puzzle from chia.wallet.cat_wallet.cat_wallet import CATWallet from chia.wallet.cat_wallet.cat_constants import DEFAULT_CATS +from chia.wallet.cat_wallet.cat_info import LegacyCATInfo from chia.wallet.puzzles.cat_loader import CAT_MOD from chia.wallet.transaction_record import TransactionRecord +from chia.wallet.wallet_info import WalletInfo from tests.pools.test_pool_rpc import wallet_is_synced from tests.setup_nodes import setup_simulators_and_wallets from tests.time_out_assert import time_out_assert @@ -105,6 +107,20 @@ async def test_cat_creation(self, two_wallet_nodes, trusted): await time_out_assert(15, cat_wallet.get_spendable_balance, 100) await time_out_assert(15, cat_wallet.get_unconfirmed_balance, 100) + # Test migration + all_lineage = await cat_wallet.lineage_store.get_all_lineage_proofs() + current_info = cat_wallet.wallet_info + data_str = bytes( + LegacyCATInfo( + cat_wallet.cat_info.limitations_program_hash, cat_wallet.cat_info.my_tail, list(all_lineage.items()) + ) + ).hex() + wallet_info = WalletInfo(current_info.id, current_info.name, current_info.type, data_str) + new_cat_wallet = await CATWallet.create(wallet_node.wallet_state_manager, wallet, wallet_info) + assert new_cat_wallet.cat_info.limitations_program_hash == cat_wallet.cat_info.limitations_program_hash + assert new_cat_wallet.cat_info.my_tail == cat_wallet.cat_info.my_tail + assert await cat_wallet.lineage_store.get_all_lineage_proofs() == all_lineage + @pytest.mark.parametrize( "trusted", [True, False], From 2c4cbb91271fa21b45560f4218c21771055c5e62 Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Thu, 17 Feb 2022 10:52:17 -0700 Subject: [PATCH 070/378] Do our best to only show inner puzzle hashes (#10015) * Do our best to only show inner puzzle hashes * Add to get_transaction(s) API * Unrelated minor fix * black --- chia/rpc/wallet_rpc_api.py | 17 +++++++++++++++-- chia/wallet/cat_wallet/cat_wallet.py | 4 +++- chia/wallet/wallet_state_manager.py | 16 ++++++++++++---- tests/wallet/cat_wallet/test_cat_wallet.py | 2 ++ 4 files changed, 32 insertions(+), 7 deletions(-) diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 47434c189058..92add3863d0b 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -1,4 +1,5 @@ import asyncio +import dataclasses import logging from pathlib import Path from typing import Callable, Dict, List, Optional, Tuple, Set, Any @@ -159,6 +160,15 @@ async def _stop_wallet(self): if peers_close_task is not None: await peers_close_task + async def _convert_tx_puzzle_hash(self, tx: TransactionRecord) -> TransactionRecord: + assert self.service.wallet_state_manager is not None + return dataclasses.replace( + tx, + to_puzzle_hash=( + await self.service.wallet_state_manager.convert_puzzle_hash(tx.wallet_id, tx.to_puzzle_hash) + ), + ) + ########################################################################################## # Key management ########################################################################################## @@ -656,7 +666,7 @@ async def get_transaction(self, request: Dict) -> Dict: raise ValueError(f"Transaction 0x{transaction_id.hex()} not found") return { - "transaction": tr.to_json_dict_convenience(self.service.config), + "transaction": (await self._convert_tx_puzzle_hash(tr)).to_json_dict_convenience(self.service.config), "transaction_id": tr.name, } @@ -674,7 +684,10 @@ async def get_transactions(self, request: Dict) -> Dict: wallet_id, start, end, sort_key=sort_key, reverse=reverse ) return { - "transactions": [tr.to_json_dict_convenience(self.service.config) for tr in transactions], + "transactions": [ + (await self._convert_tx_puzzle_hash(tr)).to_json_dict_convenience(self.service.config) + for tr in transactions + ], "wallet_id": wallet_id, } diff --git a/chia/wallet/cat_wallet/cat_wallet.py b/chia/wallet/cat_wallet/cat_wallet.py index c70be0de951d..14d68d971c85 100644 --- a/chia/wallet/cat_wallet/cat_wallet.py +++ b/chia/wallet/cat_wallet/cat_wallet.py @@ -124,7 +124,7 @@ async def create_new_cat_wallet( cc_record = TransactionRecord( confirmed_at_height=uint32(0), created_at_time=uint64(int(time.time())), - to_puzzle_hash=cc_coin.puzzle_hash, + to_puzzle_hash=(await self.convert_puzzle_hash(cc_coin.puzzle_hash)), amount=uint64(cc_coin.amount), fee_amount=uint64(0), confirmed=False, @@ -153,6 +153,8 @@ async def create_wallet_for_cat( self.standard_wallet = wallet self.log = logging.getLogger(__name__) + limitations_program_hash_hex = bytes32.from_hexstr(limitations_program_hash_hex).hex() # Normalize the format + for id, wallet in wallet_state_manager.wallets.items(): if wallet.type() == CATWallet.type(): if wallet.get_asset_id() == limitations_program_hash_hex: # type: ignore diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 504318b4b423..6d65c8f49eb0 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -740,7 +740,7 @@ async def new_coin_state( tx_record = TransactionRecord( confirmed_at_height=coin_state.created_height, created_at_time=uint64(created_timestamp), - to_puzzle_hash=coin_state.coin.puzzle_hash, + to_puzzle_hash=(await self.convert_puzzle_hash(wallet_id, coin_state.coin.puzzle_hash)), amount=uint64(coin_state.coin.amount), fee_amount=uint64(0), confirmed=True, @@ -794,7 +794,7 @@ async def new_coin_state( tx_record = TransactionRecord( confirmed_at_height=coin_state.spent_height, created_at_time=uint64(spent_timestamp), - to_puzzle_hash=to_puzzle_hash, + to_puzzle_hash=(await self.convert_puzzle_hash(wallet_id, to_puzzle_hash)), amount=uint64(int(amount)), fee_amount=uint64(fee), confirmed=True, @@ -995,7 +995,7 @@ async def coin_added( tx_record = TransactionRecord( confirmed_at_height=uint32(height), created_at_time=timestamp, - to_puzzle_hash=coin.puzzle_hash, + to_puzzle_hash=(await self.convert_puzzle_hash(wallet_id, coin.puzzle_hash)), amount=coin.amount, fee_amount=uint64(0), confirmed=True, @@ -1027,7 +1027,7 @@ async def coin_added( tx_record = TransactionRecord( confirmed_at_height=uint32(height), created_at_time=timestamp, - to_puzzle_hash=coin.puzzle_hash, + to_puzzle_hash=(await self.convert_puzzle_hash(wallet_id, coin.puzzle_hash)), amount=coin.amount, fee_amount=uint64(0), confirmed=True, @@ -1277,3 +1277,11 @@ async def delete_trade_transactions(self, trade_id: bytes32): txs: List[TransactionRecord] = await self.tx_store.get_transactions_by_trade_id(trade_id) for tx in txs: await self.tx_store.delete_transaction_record(tx.name) + + async def convert_puzzle_hash(self, wallet_id: uint32, puzzle_hash: bytes32) -> bytes32: + wallet = self.wallets[wallet_id] + # This should be general to wallets but for right now this is just for CATs so we'll add this if + if wallet.type() == WalletType.CAT.value: + return await wallet.convert_puzzle_hash(puzzle_hash) + + return puzzle_hash diff --git a/tests/wallet/cat_wallet/test_cat_wallet.py b/tests/wallet/cat_wallet/test_cat_wallet.py index 5700cc8f6ccc..af3a5112a662 100644 --- a/tests/wallet/cat_wallet/test_cat_wallet.py +++ b/tests/wallet/cat_wallet/test_cat_wallet.py @@ -192,6 +192,8 @@ async def test_cat_spend(self, two_wallet_nodes, trusted): await time_out_assert( 15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name() ) + if tx_record.wallet_id is cat_wallet.id(): + assert tx_record.to_puzzle_hash == cat_2_hash await time_out_assert(15, cat_wallet.get_pending_change_balance, 40) From e61c0f477b230583b8df6a934ed23f4f791760fe Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Thu, 17 Feb 2022 12:20:19 -0700 Subject: [PATCH 071/378] Accommodate multiple coin fees (#10251) --- chia/pools/pool_wallet.py | 8 +++----- tests/pools/test_pool_rpc.py | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/chia/pools/pool_wallet.py b/chia/pools/pool_wallet.py index d3f30c57a989..c676420177c5 100644 --- a/chia/pools/pool_wallet.py +++ b/chia/pools/pool_wallet.py @@ -405,7 +405,7 @@ async def create_new_pool_wallet_transaction( balance = await standard_wallet.get_confirmed_balance(unspent_records) if balance < PoolWallet.MINIMUM_INITIAL_BALANCE: raise ValueError("Not enough balance in main wallet to create a managed plotting pool.") - if balance < fee: + if balance < PoolWallet.MINIMUM_INITIAL_BALANCE + fee: raise ValueError("Not enough balance in main wallet to create a managed plotting pool with fee {fee}.") # Verify Parameters - raise if invalid @@ -613,12 +613,10 @@ async def generate_launcher_spend( Creates the initial singleton, which includes spending an origin coin, the launcher, and creating a singleton with the "pooling" inner state, which can be either self pooling or using a pool """ - coins: Set[Coin] = await standard_wallet.select_coins(amount) + coins: Set[Coin] = await standard_wallet.select_coins(uint64(amount + fee)) if coins is None: raise ValueError("Not enough coins to create pool wallet") - assert len(coins) == 1 - launcher_parent: Coin = coins.copy().pop() genesis_launcher_puz: Program = SINGLETON_LAUNCHER launcher_coin: Coin = Coin(launcher_parent.name(), genesis_launcher_puz.get_tree_hash(), amount) @@ -662,7 +660,7 @@ async def generate_launcher_spend( amount, genesis_launcher_puz.get_tree_hash(), fee, - None, + launcher_parent.name(), coins, None, False, diff --git a/tests/pools/test_pool_rpc.py b/tests/pools/test_pool_rpc.py index b6fc9e05a9d1..51e595d29474 100644 --- a/tests/pools/test_pool_rpc.py +++ b/tests/pools/test_pool_rpc.py @@ -35,7 +35,7 @@ # TODO: Compare deducted fees in all tests against reported total_fee log = logging.getLogger(__name__) -FEE_AMOUNT = 10 +FEE_AMOUNT = 2000000000000 def get_pool_plot_dir(): From fad3f88b13bd334b7be23ea5ce3b760b865ae96b Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Thu, 17 Feb 2022 12:44:28 -0700 Subject: [PATCH 072/378] Bundle CATs with announcements (#10260) * Bundle CATs with announcements * Make the announcement deterministic to the coins being spent --- chia/wallet/cat_wallet/cat_wallet.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/chia/wallet/cat_wallet/cat_wallet.py b/chia/wallet/cat_wallet/cat_wallet.py index 14d68d971c85..9f2040d40381 100644 --- a/chia/wallet/cat_wallet/cat_wallet.py +++ b/chia/wallet/cat_wallet/cat_wallet.py @@ -22,6 +22,7 @@ from chia.types.condition_opcodes import ConditionOpcode from chia.util.byte_types import hexstr_to_bytes from chia.util.condition_tools import conditions_dict_for_solution, pkm_pairs_for_conditions_dict +from chia.util.hash import std_hash from chia.util.ints import uint8, uint32, uint64, uint128 from chia.wallet.cat_wallet.cat_constants import DEFAULT_CATS from chia.wallet.cat_wallet.cat_info import CATInfo, LegacyCATInfo @@ -609,12 +610,13 @@ async def generate_unsigned_spendbundle( spendable_cc_list = [] chia_tx = None first = True + announcement: Announcement for coin in cat_coins: if first: first = False + announcement = Announcement(coin.name(), std_hash(b"".join([c.name() for c in cat_coins])), b"\xca") if need_chia_transaction: if fee > regular_chia_to_claim: - announcement = Announcement(coin.name(), b"$", b"\xca") chia_tx, _ = await self.create_tandem_xch_tx( fee, uint64(regular_chia_to_claim), announcement_to_assert=announcement ) @@ -627,16 +629,22 @@ async def generate_unsigned_spendbundle( elif regular_chia_to_claim > fee: chia_tx, _ = await self.create_tandem_xch_tx(fee, uint64(regular_chia_to_claim)) innersol = self.standard_wallet.make_solution( - primaries=primaries, coin_announcements_to_assert={announcement.name()} + primaries=primaries, + coin_announcements={announcement.message}, + coin_announcements_to_assert={announcement.name()}, ) else: innersol = self.standard_wallet.make_solution( primaries=primaries, + coin_announcements={announcement.message}, coin_announcements_to_assert=coin_announcements_bytes, puzzle_announcements_to_assert=puzzle_announcements_bytes, ) else: - innersol = self.standard_wallet.make_solution(primaries=[]) + innersol = self.standard_wallet.make_solution( + primaries=[], + coin_announcements_to_assert={announcement.name()}, + ) inner_puzzle = await self.inner_puzzle_for_cc_puzhash(coin.puzzle_hash) lineage_proof = await self.get_lineage_proof_for_coin(coin) assert lineage_proof is not None From 52e439ccbea2fc2e36f8f7a6a4ec04bd4a37c758 Mon Sep 17 00:00:00 2001 From: Mariano Sorgente <3069354+mariano54@users.noreply.github.com> Date: Thu, 17 Feb 2022 16:35:41 -0500 Subject: [PATCH 073/378] Ms.sync cancel (#10244) * Start fixing other issues * Fork point, sync cancelling, random peer * Reduce logging * Improve performance and have a fallback peer for fetching * Disconnect not synced peers * Make sure try catch doesn't fail * Fix lint * Update chia/wallet/wallet_node.py Co-authored-by: Kyle Altendorf * Pylint has a bug so ignore pylint for this line Co-authored-by: Kyle Altendorf --- chia/protocols/protocol_state_machine.py | 1 + chia/rpc/wallet_rpc_api.py | 2 +- chia/server/start_service.py | 4 +- chia/wallet/util/wallet_sync_utils.py | 79 +++++++++++- chia/wallet/wallet_blockchain.py | 3 +- chia/wallet/wallet_node.py | 158 +++++++++++++---------- 6 files changed, 171 insertions(+), 76 deletions(-) diff --git a/chia/protocols/protocol_state_machine.py b/chia/protocols/protocol_state_machine.py index 740c87c42bdc..1be6bba9c335 100644 --- a/chia/protocols/protocol_state_machine.py +++ b/chia/protocols/protocol_state_machine.py @@ -30,6 +30,7 @@ pmt.request_signage_point_or_end_of_sub_slot: [pmt.respond_signage_point, pmt.respond_end_of_sub_slot], pmt.request_compact_vdf: [pmt.respond_compact_vdf], pmt.request_peers: [pmt.respond_peers], + pmt.request_header_blocks: [pmt.respond_header_blocks, pmt.reject_header_blocks], } diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 92add3863d0b..ba6f4f54409b 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -383,7 +383,7 @@ async def get_sync_status(self, request: Dict): async def get_height_info(self, request: Dict): assert self.service.wallet_state_manager is not None - height = self.service.wallet_state_manager.blockchain.get_peak_height() + height = await self.service.wallet_state_manager.blockchain.get_finished_sync_up_to() return {"height": height} async def get_network_info(self, request: Dict): diff --git a/chia/server/start_service.py b/chia/server/start_service.py index 0dc03a17dd92..c7314b5a7b66 100644 --- a/chia/server/start_service.py +++ b/chia/server/start_service.py @@ -80,9 +80,9 @@ def __init__( chia_ca_crt, chia_ca_key = chia_ssl_ca_paths(root_path, self.config) inbound_rlp = self.config.get("inbound_rate_limit_percent") outbound_rlp = self.config.get("outbound_rate_limit_percent") - if NodeType == NodeType.WALLET: + if node_type == NodeType.WALLET: inbound_rlp = service_config.get("inbound_rate_limit_percent", inbound_rlp) - outbound_rlp = service_config.get("outbound_rate_limit_percent", 60) + outbound_rlp = 60 assert inbound_rlp and outbound_rlp self._server = ChiaServer( diff --git a/chia/wallet/util/wallet_sync_utils.py b/chia/wallet/util/wallet_sync_utils.py index bce674b07400..0dcf2d4b5602 100644 --- a/chia/wallet/util/wallet_sync_utils.py +++ b/chia/wallet/util/wallet_sync_utils.py @@ -1,4 +1,6 @@ +import asyncio import logging +import random from typing import List, Optional, Tuple, Union, Dict from chia.consensus.constants import ConsensusConstants @@ -14,6 +16,9 @@ CoinState, RespondToPhUpdates, RespondToCoinUpdates, + RespondHeaderBlocks, + RequestHeaderBlocks, + RejectHeaderBlocks, ) from chia.server.ws_connection import WSChiaConnection from chia.types.blockchain_format.coin import hash_coin_list, Coin @@ -22,7 +27,7 @@ from chia.types.header_block import HeaderBlock from chia.util.ints import uint32 from chia.util.merkle_set import confirm_not_included_already_hashed, confirm_included_already_hashed, MerkleSet - +from chia.wallet.util.peer_request_cache import PeerRequestCache log = logging.getLogger(__name__) @@ -270,3 +275,75 @@ def get_block_challenge( curr = all_blocks.get(curr.prev_header_hash, None) challenge = reversed_challenge_hashes[challenges_to_look_for - 1] return challenge + + +def last_change_height_cs(cs: CoinState) -> uint32: + if cs.spent_height is not None: + return cs.spent_height + if cs.created_height is not None: + return cs.created_height + return uint32(0) + + +async def _fetch_header_blocks_inner( + all_peers: List[WSChiaConnection], selected_peer_node_id: bytes32, request: RequestHeaderBlocks +) -> Optional[RespondHeaderBlocks]: + if len(all_peers) == 0: + return None + random_peer: WSChiaConnection = random.choice(all_peers) + res = await random_peer.request_header_blocks(request) + if isinstance(res, RespondHeaderBlocks): + return res + elif isinstance(res, RejectHeaderBlocks): + # Peer is not synced, close connection + await random_peer.close() + + bad_peer_id = random_peer.peer_node_id + if len(all_peers) == 1: + # No more peers to fetch from + return None + else: + if selected_peer_node_id == bad_peer_id: + # Select another peer fallback + while random_peer != bad_peer_id and len(all_peers) > 1: + random_peer = random.choice(all_peers) + else: + # Use the selected peer instead + random_peer = [p for p in all_peers if p.peer_node_id == selected_peer_node_id][0] + # Retry + res = await random_peer.request_header_blocks(request) + if isinstance(res, RespondHeaderBlocks): + return res + else: + return None + + +async def fetch_header_blocks_in_range( + start: uint32, + end: uint32, + peer_request_cache: PeerRequestCache, + all_peers: List[WSChiaConnection], + selected_peer_id: bytes32, +) -> Optional[List[HeaderBlock]]: + blocks: List[HeaderBlock] = [] + for i in range(start - (start % 32), end + 1, 32): + request_start = min(uint32(i), end) + request_end = min(uint32(i + 31), end) + if (request_start, request_end) in peer_request_cache.block_requests: + res_h_blocks_task: asyncio.Task = peer_request_cache.block_requests[(request_start, request_end)] + if res_h_blocks_task.done(): + res_h_blocks: Optional[RespondHeaderBlocks] = res_h_blocks_task.result() + else: + res_h_blocks = await res_h_blocks_task + else: + request_header_blocks = RequestHeaderBlocks(request_start, request_end) + res_h_blocks_task = asyncio.create_task( + _fetch_header_blocks_inner(all_peers, selected_peer_id, request_header_blocks) + ) + peer_request_cache.block_requests[(request_start, request_end)] = res_h_blocks_task + res_h_blocks = await res_h_blocks_task + if res_h_blocks is None: + return None + assert res_h_blocks is not None + blocks.extend([bl for bl in res_h_blocks.header_blocks if bl.height >= start]) + return blocks diff --git a/chia/wallet/wallet_blockchain.py b/chia/wallet/wallet_blockchain.py index cf5c6da1ad81..ae9881d7c743 100644 --- a/chia/wallet/wallet_blockchain.py +++ b/chia/wallet/wallet_blockchain.py @@ -181,7 +181,8 @@ async def get_peak_block(self) -> Optional[HeaderBlock]: return await self._basic_store.get_object("PEAK_BLOCK", HeaderBlock) async def set_finished_sync_up_to(self, height: uint32): - await self._basic_store.set_object("FINISHED_SYNC_UP_TO", height) + if height > await self.get_finished_sync_up_to(): + await self._basic_store.set_object("FINISHED_SYNC_UP_TO", height) async def get_finished_sync_up_to(self): h: Optional[uint32] = await self._basic_store.get_object("FINISHED_SYNC_UP_TO", uint32) diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index e76e28576908..70dbf56d38e9 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -1,6 +1,7 @@ import asyncio import json import logging +import random import time import traceback from asyncio import CancelledError @@ -32,7 +33,6 @@ RequestSESInfo, RespondSESInfo, RequestHeaderBlocks, - RespondHeaderBlocks, ) from chia.server.node_discovery import WalletPeers from chia.server.outbound_message import Message, NodeType, make_msg @@ -61,6 +61,8 @@ fetch_last_tx_from_peer, subscribe_to_phs, subscribe_to_coin_updates, + last_change_height_cs, + fetch_header_blocks_in_range, ) from chia.wallet.wallet_coin_record import WalletCoinRecord from chia.wallet.wallet_state_manager import WalletStateManager @@ -505,6 +507,8 @@ def is_new_state_update(cs: CoinState) -> bool: self.rollback_request_caches(fork_height) await self.update_ui() + # We only process new state updates to avoid slow reprocessing. We set the sync height after adding + # Things, so we don't have to reprocess these later. There can be many things in ph_update_res. already_checked_ph: Set[bytes32] = set() continue_while: bool = True all_puzzle_hashes: List[bytes32] = await self.get_puzzle_hashes_to_subscribe() @@ -516,7 +520,10 @@ def is_new_state_update(cs: CoinState) -> bool: [p for p in chunk if p not in already_checked_ph], full_node, 0 ) ph_update_res = list(filter(is_new_state_update, ph_update_res)) - await self.receive_state_from_peer(ph_update_res, full_node) + ph_update_res.sort(key=last_change_height_cs) + if not await self.receive_state_from_peer(ph_update_res, full_node, update_finished_height=True): + # If something goes wrong, abort sync + return already_checked_ph.update(chunk) # Check if new puzzle hashed have been created @@ -529,18 +536,23 @@ def is_new_state_update(cs: CoinState) -> bool: break self.log.info(f"Successfully subscribed and updated {len(already_checked_ph)} puzzle hashes") + # The number of coin id updates are usually going to be significantly less than ph updates, so we can + # sync from 0 every time. continue_while = False - all_coin_ids: List[bytes32] = await self.get_coin_ids_to_subscribe(fork_height) + all_coin_ids: List[bytes32] = await self.get_coin_ids_to_subscribe(0) already_checked_coin_ids: Set[bytes32] = set() while continue_while: one_k_chunks = chunks(all_coin_ids, 1000) for chunk in one_k_chunks: c_update_res: List[CoinState] = await subscribe_to_coin_updates(chunk, full_node, 0) c_update_res = list(filter(is_new_state_update, c_update_res)) - await self.receive_state_from_peer(c_update_res, full_node) + c_update_res.sort(key=last_change_height_cs) + if not await self.receive_state_from_peer(c_update_res, full_node): + # If something goes wrong, abort sync + return already_checked_coin_ids.update(chunk) - all_coin_ids = await self.get_coin_ids_to_subscribe(fork_height) + all_coin_ids = await self.get_coin_ids_to_subscribe(0) continue_while = False for coin_id in all_coin_ids: if coin_id not in already_checked_coin_ids: @@ -548,8 +560,8 @@ def is_new_state_update(cs: CoinState) -> bool: break self.log.info(f"Successfully subscribed and updated {len(already_checked_coin_ids)} coin ids") - if target_height > await self.wallet_state_manager.blockchain.get_finished_sync_up_to(): - await self.wallet_state_manager.blockchain.set_finished_sync_up_to(target_height) + # Only update this fully when the entire sync has completed + await self.wallet_state_manager.blockchain.set_finished_sync_up_to(target_height) if trusted: self.local_node_synced = True @@ -570,13 +582,14 @@ async def receive_state_from_peer( fork_height: Optional[uint32] = None, height: Optional[uint32] = None, header_hash: Optional[bytes32] = None, - ): + update_finished_height: bool = False, + ) -> bool: # Adds the state to the wallet state manager. If the peer is trusted, we do not validate. If the peer is # untrusted we do, but we might not add the state, since we need to receive the new_peak message as well. - assert self.wallet_state_manager is not None trusted = self.is_trusted(peer) # Validate states in parallel, apply serial + # TODO: optimize fetching if self.validation_semaphore is None: self.validation_semaphore = asyncio.Semaphore(6) if self.new_state_lock is None: @@ -589,41 +602,54 @@ async def receive_state_from_peer( if fork_height is not None: cache.clear_after_height(fork_height) - all_tasks = [] - - for idx, potential_state in enumerate(items): + all_tasks: List[asyncio.Task] = [] + target_concurrent_tasks: int = 20 + num_concurrent_tasks: int = 0 - async def receive_and_validate(inner_state: CoinState, inner_idx: int): - assert self.wallet_state_manager is not None + async def receive_and_validate(inner_state: CoinState, inner_idx: int): + try: assert self.validation_semaphore is not None - # if height is not None: async with self.validation_semaphore: - try: - if header_hash is not None: - assert height is not None - self.add_state_to_race_cache(header_hash, height, inner_state) - self.log.info(f"Added to race cache: {height}, {inner_state}") - if trusted: - valid = True - else: - valid = await self.validate_received_state_from_peer(inner_state, peer, cache, fork_height) - if valid: - self.log.info(f"new coin state received ({inner_idx + 1} / {len(items)})") - assert self.new_state_lock is not None - async with self.new_state_lock: - await self.wallet_state_manager.new_coin_state([inner_state], peer, fork_height) - except Exception as e: - tb = traceback.format_exc() - self.log.error(f"Exception while adding state: {e} {tb}") - - task = receive_and_validate(potential_state, idx) - all_tasks.append(task) - while len(self.validation_semaphore._waiters) > 20: - self.log.debug("sleeping 2 sec") - await asyncio.sleep(2) + assert self.wallet_state_manager is not None + if header_hash is not None: + assert height is not None + self.add_state_to_race_cache(header_hash, height, inner_state) + self.log.info(f"Added to race cache: {height}, {inner_state}") + if trusted: + valid = True + else: + valid = await self.validate_received_state_from_peer(inner_state, peer, cache, fork_height) + if valid: + self.log.info(f"new coin state received ({inner_idx + 1} / {len(items)})") + assert self.new_state_lock is not None + async with self.new_state_lock: + await self.wallet_state_manager.new_coin_state([inner_state], peer, fork_height) + if update_finished_height: + await self.wallet_state_manager.blockchain.set_finished_sync_up_to( + last_change_height_cs(inner_state) + ) + except Exception as e: + tb = traceback.format_exc() + self.log.error(f"Exception while adding state: {e} {tb}") + finally: + nonlocal num_concurrent_tasks + num_concurrent_tasks -= 1 # pylint: disable=E0602 + + for idx, potential_state in enumerate(items): + if self.server is None: + self.log.error("No server") + return False + if peer.peer_node_id not in self.server.all_connections: + self.log.error(f"Disconnected from peer {peer.peer_node_id} host {peer.peer_host}") + return False + while num_concurrent_tasks >= target_concurrent_tasks: + await asyncio.sleep(1) + all_tasks.append(asyncio.create_task(receive_and_validate(potential_state, idx))) + num_concurrent_tasks += 1 await asyncio.gather(*all_tasks) await self.update_ui() + return True async def get_coins_with_puzzle_hash(self, puzzle_hash) -> List[CoinState]: assert self.wallet_state_manager is not None @@ -691,7 +717,7 @@ def get_full_node_peer(self) -> Optional[WSChiaConnection]: assert self.server is not None nodes = self.server.get_full_node_connections() if len(nodes) > 0: - return nodes[0] + return random.choice(nodes) else: return None @@ -804,12 +830,16 @@ async def new_peak_wallet(self, new_peak: wallet_protocol.NewPeakWallet, peer: W assert weight_proof is not None old_proof = self.wallet_state_manager.blockchain.synced_weight_proof if syncing: - fork_point: int = max(0, current_height - 32) + # This usually happens the first time we start up the wallet. We roll back slightly to be + # safe, but we don't want to rollback too much (hence 16) + fork_point: int = max(0, current_height - 16) else: - fork_point = max(0, current_height - 50000) + # In this case we will not rollback so it's OK to check some older updates as well, to ensure + # that no recent transactions are being hidden. + fork_point = 0 if old_proof is not None: # If the weight proof fork point is in the past, rollback more to ensure we don't have duplicate - # state + # state. wp_fork_point = self.wallet_state_manager.weight_proof_handler.get_fork_point( old_proof, weight_proof ) @@ -868,13 +898,14 @@ async def new_peak_wallet(self, new_peak: wallet_protocol.NewPeakWallet, peer: W ph_updates: List[CoinState] = await subscribe_to_phs(phs, peer, uint32(0)) coin_updates: List[CoinState] = await subscribe_to_coin_updates(all_coin_ids, peer, uint32(0)) peer_new_peak_height, peer_new_peak_hash = self.node_peaks[peer.peer_node_id] - await self.receive_state_from_peer( + success = await self.receive_state_from_peer( ph_updates + coin_updates, peer, height=peer_new_peak_height, header_hash=peer_new_peak_hash, ) - self.synced_peers.add(peer.peer_node_id) + if success: + self.synced_peers.add(peer.peer_node_id) else: if peak_hb is not None and new_peak.weight <= peak_hb.weight: # Don't process blocks at the same weight @@ -891,10 +922,7 @@ async def new_peak_wallet(self, new_peak: wallet_protocol.NewPeakWallet, peer: W self.wallet_state_manager.set_sync_mode(False) self.log.info(f"Finished processing new peak of {new_peak.height}") - if ( - peer.peer_node_id in self.synced_peers - and new_peak.height > await self.wallet_state_manager.blockchain.get_finished_sync_up_to() - ): + 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) @@ -1034,6 +1062,7 @@ async def validate_received_state_from_peer( and current_spent_height == spent_height and current.confirmed_block_height == confirmed_height ): + peer_request_cache.states_validated[coin_state.get_hash()] = coin_state return True reorg_mode = False @@ -1051,6 +1080,8 @@ async def validate_received_state_from_peer( else: request = RequestHeaderBlocks(confirmed_height, confirmed_height) res = await peer.request_header_blocks(request) + if res is None: + return False state_block = res.header_blocks[0] peer_request_cache.blocks[confirmed_height] = state_block @@ -1137,6 +1168,7 @@ async def validate_block_inclusion( self, block: HeaderBlock, peer: WSChiaConnection, peer_request_cache: PeerRequestCache ) -> bool: assert self.wallet_state_manager is not None + assert self.server is not None if self.wallet_state_manager.blockchain.contains_height(block.height): stored_hash = self.wallet_state_manager.blockchain.height_to_hash(block.height) stored_record = self.wallet_state_manager.blockchain.try_block_record(stored_hash) @@ -1192,29 +1224,13 @@ async def validate_block_inclusion( self.log.error("Failed validation 2") return False - blocks: List[HeaderBlock] = [] - for i in range(start - (start % 32), end + 1, 32): - request_start = min(uint32(i), end) - request_end = min(uint32(i + 31), end) - request_h_response = RequestHeaderBlocks(request_start, request_end) - if (request_start, request_end) in peer_request_cache.block_requests: - self.log.info(f"Using cache for blocks {request_start} - {request_end}") - res_h_blocks: Optional[RespondHeaderBlocks] = peer_request_cache.block_requests[ - (request_start, request_end) - ] - else: - start_time = time.time() - res_h_blocks = await peer.request_header_blocks(request_h_response) - if res_h_blocks is None: - self.log.error("Failed validation 2.5") - return False - end_time = time.time() - peer_request_cache.block_requests[(request_start, request_end)] = res_h_blocks - self.log.info( - f"Fetched blocks: {request_start} - {request_end} | duration: {end_time - start_time}" - ) - assert res_h_blocks is not None - blocks.extend([bl for bl in res_h_blocks.header_blocks if bl.height >= start]) + all_peers = self.server.get_full_node_connections() + blocks: Optional[List[HeaderBlock]] = await fetch_header_blocks_in_range( + start, end, peer_request_cache, all_peers, peer.peer_node_id + ) + if blocks is None: + self.log.error(f"Error fetching blocks {start} {end}") + return False if compare_to_recent and weight_proof.recent_chain_data[0].header_hash != blocks[-1].header_hash: self.log.error("Failed validation 3") From 47a0a9101cdbf01c50a02c5d6501f5e26bbd5a21 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 17 Feb 2022 22:46:51 -0500 Subject: [PATCH 074/378] apply isort (#10280) --- chia/util/full_block_utils.py | 7 ++++--- chia/util/struct_stream.py | 2 +- tests/util/test_full_block_utils.py | 14 +++++++------- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/chia/util/full_block_utils.py b/chia/util/full_block_utils.py index 85d2cbf3c246..bc41e03f8f2a 100644 --- a/chia/util/full_block_utils.py +++ b/chia/util/full_block_utils.py @@ -1,8 +1,9 @@ -from typing import Optional, Callable +from typing import Callable, Optional -from chia.types.blockchain_format.program import SerializedProgram -from clvm_rs import serialized_length from blspy import G1Element, G2Element +from clvm_rs import serialized_length + +from chia.types.blockchain_format.program import SerializedProgram def skip_list(buf: memoryview, skip_item: Callable[[memoryview], memoryview]) -> memoryview: diff --git a/chia/util/struct_stream.py b/chia/util/struct_stream.py index e1c6ae42cefe..a674910fbb29 100644 --- a/chia/util/struct_stream.py +++ b/chia/util/struct_stream.py @@ -1,8 +1,8 @@ import io import struct from typing import Any, BinaryIO, SupportsInt, Type, TypeVar, Union -from typing_extensions import SupportsIndex, Protocol +from typing_extensions import Protocol, SupportsIndex _T_StructStream = TypeVar("_T_StructStream", bound="StructStream") diff --git a/tests/util/test_full_block_utils.py b/tests/util/test_full_block_utils.py index b51170c3f019..2d6c4e8fd872 100644 --- a/tests/util/test_full_block_utils.py +++ b/tests/util/test_full_block_utils.py @@ -1,14 +1,13 @@ import random + import pytest -from chia.util.full_block_utils import generator_from_block -from chia.types.full_block import FullBlock -from chia.util.ints import uint128, uint64, uint32, uint8 +from benchmarks.utils import rand_bytes, rand_g1, rand_g2, rand_hash, rand_vdf, rand_vdf_proof, rewards +from chia.types.blockchain_format.foliage import Foliage, FoliageBlockData, FoliageTransactionBlock, TransactionsInfo from chia.types.blockchain_format.pool_target import PoolTarget -from chia.types.blockchain_format.foliage import Foliage, FoliageTransactionBlock, TransactionsInfo, FoliageBlockData +from chia.types.blockchain_format.program import SerializedProgram from chia.types.blockchain_format.proof_of_space import ProofOfSpace from chia.types.blockchain_format.reward_chain_block import RewardChainBlock -from chia.types.blockchain_format.program import SerializedProgram from chia.types.blockchain_format.slots import ( ChallengeChainSubSlot, InfusedChallengeChainSubSlot, @@ -16,8 +15,9 @@ SubSlotProofs, ) from chia.types.end_of_slot_bundle import EndOfSubSlotBundle - -from benchmarks.utils import rand_hash, rand_bytes, rewards, rand_g1, rand_g2, rand_vdf, rand_vdf_proof +from chia.types.full_block import FullBlock +from chia.util.full_block_utils import generator_from_block +from chia.util.ints import uint8, uint32, uint64, uint128 test_g2s = [rand_g2() for _ in range(10)] test_g1s = [rand_g1() for _ in range(10)] From ffe4e7ca3772828983873bd27527c662f0016a7b Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 18 Feb 2022 10:29:55 -0500 Subject: [PATCH 075/378] Fix setuptools_scm local_scheme configuration (#10296) * only configure setuptools_scm in pyproject.toml * unban setuptools 60.9.1 and 60.9.2 --- pyproject.toml | 4 ++-- setup.py | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3078c1c0d640..9e550c7af6b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,9 @@ [build-system] -# setuptools !=60.9.1, !=60.9.2 for bug missing no-local-version -requires = ["setuptools>=42,!=60.9.1,!=60.9.2", "wheel", "setuptools_scm[toml]>=4.1.2"] +requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=4.1.2"] build-backend = "setuptools.build_meta" [tool.setuptools_scm] +fallback_version = "unknown-no-.git-directory" local_scheme = "no-local-version" [tool.black] diff --git a/setup.py b/setup.py index 1764151e3452..e35ca50a3c69 100644 --- a/setup.py +++ b/setup.py @@ -71,7 +71,6 @@ python_requires=">=3.7, <4", keywords="chia blockchain node", install_requires=dependencies, - setup_requires=["setuptools_scm"], extras_require=dict( uvloop=["uvloop"], dev=dev_dependencies, @@ -133,7 +132,6 @@ "chia.ssl": ["chia_ca.crt", "chia_ca.key", "dst_root_ca.pem"], "mozilla-ca": ["cacert.pem"], }, - use_scm_version={"fallback_version": "unknown-no-.git-directory"}, long_description=open("README.md").read(), long_description_content_type="text/markdown", zip_safe=False, From 5e4c1a1f62c10b141b25b30e715ee993eac03bb8 Mon Sep 17 00:00:00 2001 From: Chris Marslender Date: Fri, 18 Feb 2022 09:43:52 -0600 Subject: [PATCH 076/378] Timelord RPC + Misc Metrics Updates/Fixes (#10255) * Add mempool_max_total_cost to RPC * Add signage_point event * Fix incorrect crawler RPC port lookup * Set up initial timelord RPC server + finished_pot_challenge event * Add new compact proof event * Add skipping/new_peak to track when fastest or not * Check for None on change_data * Add skipping_peak + new_peak to changes for metrics * Convert chain to value * Rename iters * Timelord RPC to 8557 - 8556 is used in simulation tests * Make tests work with RPC server on timelord * Change event name to finished_pot * Use broadcast_farmer object * Move state changed for `finished_pot` after proofs_finished.append * Fix type on ips var + add vdf_info and vdf_proof * fix event name on the state_changed function --- chia/full_node/full_node.py | 2 ++ chia/rpc/full_node_rpc_api.py | 12 +++++++----- chia/rpc/timelord_rpc_api.py | 24 ++++++++++++++++++++++++ chia/seeder/start_crawler.py | 2 +- chia/server/start_timelord.py | 5 +++++ chia/timelord/timelord.py | 25 ++++++++++++++++++++++++- chia/timelord/timelord_api.py | 5 ++++- chia/util/initial-config.yaml | 3 +++ tests/core/ssl/test_ssl.py | 2 +- tests/setup_nodes.py | 8 +++++--- 10 files changed, 76 insertions(+), 12 deletions(-) create mode 100644 chia/rpc/timelord_rpc_api.py diff --git a/chia/full_node/full_node.py b/chia/full_node/full_node.py index 244802b00f73..5aeb608634e9 100644 --- a/chia/full_node/full_node.py +++ b/chia/full_node/full_node.py @@ -1210,6 +1210,8 @@ async def signage_point_post_processing( msg = make_msg(ProtocolMessageTypes.new_signage_point, broadcast_farmer) await self.server.send_to_all([msg], NodeType.FARMER) + self._state_changed("signage_point", {"broadcast_farmer": broadcast_farmer}) + async def peak_post_processing( self, block: FullBlock, diff --git a/chia/rpc/full_node_rpc_api.py b/chia/rpc/full_node_rpc_api.py index afed9ba31eec..beca3eacdb56 100644 --- a/chia/rpc/full_node_rpc_api.py +++ b/chia/rpc/full_node_rpc_api.py @@ -86,13 +86,11 @@ async def _state_changed(self, change: str, change_data: Dict[str, Any] = None) "metrics", ) ) - return payloads - if change == "block": - payloads.append(create_payload_dict("block", change_data, self.service_name, "metrics")) - return payloads + if change in ("block", "signage_point"): + payloads.append(create_payload_dict(change, change_data, self.service_name, "metrics")) - return [] + return payloads # this function is just here for backwards-compatibility. It will probably # be removed in the future @@ -123,6 +121,7 @@ async def get_blockchain_state(self, _request: Dict): "mempool_min_fees": { "cost_5000000": 0, }, + "mempool_max_total_cost": 0, "block_max_cost": 0, "node_id": node_id, }, @@ -171,10 +170,12 @@ async def get_blockchain_state(self, _request: Dict): mempool_size = len(self.service.mempool_manager.mempool.spends) mempool_cost = self.service.mempool_manager.mempool.total_mempool_cost mempool_min_fee_5m = self.service.mempool_manager.mempool.get_min_fee_rate(5000000) + mempool_max_total_cost = self.service.mempool_manager.mempool_max_total_cost else: mempool_size = 0 mempool_cost = 0 mempool_min_fee_5m = 0 + mempool_max_total_cost = 0 if self.service.server is not None: is_connected = len(self.service.server.get_full_node_connections()) > 0 else: @@ -202,6 +203,7 @@ async def get_blockchain_state(self, _request: Dict): # This Dict sets us up for that in the future "cost_5000000": mempool_min_fee_5m, }, + "mempool_max_total_cost": mempool_max_total_cost, "block_max_cost": self.service.constants.MAX_BLOCK_COST_CLVM, "node_id": node_id, }, diff --git a/chia/rpc/timelord_rpc_api.py b/chia/rpc/timelord_rpc_api.py new file mode 100644 index 000000000000..9eca2aea4d65 --- /dev/null +++ b/chia/rpc/timelord_rpc_api.py @@ -0,0 +1,24 @@ +from typing import Any, Callable, Dict, List, Optional + +from chia.timelord.timelord import Timelord +from chia.util.ws_message import WsRpcMessage, create_payload_dict + + +class TimelordRpcApi: + def __init__(self, timelord: Timelord): + self.service = timelord + self.service_name = "chia_timelord" + + def get_routes(self) -> Dict[str, Callable]: + return {} + + async def _state_changed(self, change: str, change_data: Optional[Dict[str, Any]] = None) -> List[WsRpcMessage]: + payloads = [] + + if change_data is None: + change_data = {} + + if change in ("finished_pot", "new_compact_proof", "skipping_peak", "new_peak"): + payloads.append(create_payload_dict(change, change_data, self.service_name, "metrics")) + + return payloads diff --git a/chia/seeder/start_crawler.py b/chia/seeder/start_crawler.py index 6c2abe714018..bd1062cf8b67 100644 --- a/chia/seeder/start_crawler.py +++ b/chia/seeder/start_crawler.py @@ -45,7 +45,7 @@ def service_kwargs_for_full_node_crawler( ) if config.get("crawler", {}).get("start_rpc_server", True): - kwargs["rpc_info"] = (CrawlerRpcApi, config.get("crawler", {}).get("crawler_rpc_port", 8561)) + kwargs["rpc_info"] = (CrawlerRpcApi, config.get("crawler", {}).get("rpc_port", 8561)) return kwargs diff --git a/chia/server/start_timelord.py b/chia/server/start_timelord.py index 53596c696113..dcb15deb4800 100644 --- a/chia/server/start_timelord.py +++ b/chia/server/start_timelord.py @@ -4,6 +4,7 @@ from chia.consensus.constants import ConsensusConstants from chia.consensus.default_constants import DEFAULT_CONSTANTS +from chia.rpc.timelord_rpc_api import TimelordRpcApi from chia.server.outbound_message import NodeType from chia.server.start_service import run_service from chia.timelord.timelord import Timelord @@ -46,6 +47,10 @@ def service_kwargs_for_timelord( auth_connect_peers=False, network_id=network_id, ) + + if config.get("start_rpc_server", True): + kwargs["rpc_info"] = (TimelordRpcApi, config.get("rpc_port", 8557)) + return kwargs diff --git a/chia/timelord/timelord.py b/chia/timelord/timelord.py index c7bb0df20586..f92d6acf0435 100644 --- a/chia/timelord/timelord.py +++ b/chia/timelord/timelord.py @@ -7,7 +7,7 @@ import time import traceback from concurrent.futures import ProcessPoolExecutor -from typing import Callable, Dict, List, Optional, Set, Tuple +from typing import Any, Callable, Dict, List, Optional, Set, Tuple from chiavdf import create_discriminant, prove @@ -152,6 +152,13 @@ def _close(self): async def _await_closed(self): pass + def _set_state_changed_callback(self, callback: Callable): + self.state_changed_callback = callback + + def state_changed(self, change: str, change_data: Optional[Dict[str, Any]] = None): + if self.state_changed_callback is not None: + self.state_changed_callback(change, change_data) + def set_server(self, server: ChiaServer): self.server = server @@ -986,6 +993,8 @@ async def _do_process_communication( # Verifies our own proof just in case form_size = ClassgroupElement.get_size(self.constants) output = ClassgroupElement.from_bytes(y_bytes[:form_size]) + # default value so that it's always set for state_changed later + ips: float = 0 if not self.bluebox_mode: time_taken = time.time() - self.chain_start_time[chain] ips = int(iterations_needed / time_taken * 10) / 10 @@ -1012,6 +1021,16 @@ async def _do_process_communication( async with self.lock: assert proof_label is not None self.proofs_finished.append((chain, vdf_info, vdf_proof, proof_label)) + self.state_changed( + "finished_pot", + { + "estimated_ips": ips, + "iterations_needed": iterations_needed, + "chain": chain.value, + "vdf_info": vdf_info, + "vdf_proof": vdf_proof, + }, + ) else: async with self.lock: writer.write(b"010") @@ -1025,6 +1044,10 @@ async def _do_process_communication( if self.server is not None: message = make_msg(ProtocolMessageTypes.respond_compact_proof_of_time, response) await self.server.send_to_all([message], NodeType.FULL_NODE) + self.state_changed( + "new_compact_proof", {"header_hash": header_hash, "height": height, "field_vdf": field_vdf} + ) + except ConnectionResetError as e: log.debug(f"Connection reset with VDF client {e}") diff --git a/chia/timelord/timelord_api.py b/chia/timelord/timelord_api.py index 9d5956969f58..e6d1e810c65a 100644 --- a/chia/timelord/timelord_api.py +++ b/chia/timelord/timelord_api.py @@ -17,7 +17,7 @@ def __init__(self, timelord) -> None: self.timelord = timelord def _set_state_changed_callback(self, callback: Callable): - pass + self.timelord.state_changed_callback = callback @api_request async def new_peak_timelord(self, new_peak: timelord_protocol.NewPeakTimelord): @@ -33,15 +33,18 @@ async def new_peak_timelord(self, new_peak: timelord_protocol.NewPeakTimelord): f"{new_peak.reward_chain_block.weight} " ) self.timelord.new_peak = new_peak + self.timelord.state_changed("new_peak", {"height": new_peak.reward_chain_block.height}) elif ( self.timelord.last_state.peak is not None and self.timelord.last_state.peak.reward_chain_block == new_peak.reward_chain_block ): log.info("Skipping peak, already have.") + self.timelord.state_changed("skipping_peak", {"height": new_peak.reward_chain_block.height}) return None else: log.warning("block that we don't have, changing to it.") self.timelord.new_peak = new_peak + self.timelord.state_changed("new_peak", {"height": new_peak.reward_chain_block.height}) self.timelord.new_subslot_end = None @api_request diff --git a/chia/util/initial-config.yaml b/chia/util/initial-config.yaml index 1b53f7e958cb..40ffb42aa8f5 100644 --- a/chia/util/initial-config.yaml +++ b/chia/util/initial-config.yaml @@ -305,6 +305,9 @@ timelord: # If `slow_bluebox` is True, launches `slow_bluebox_process_count` processes. slow_bluebox_process_count: 1 + start_rpc_server: True + rpc_port: 8557 + ssl: private_crt: "config/ssl/timelord/private_timelord.crt" private_key: "config/ssl/timelord/private_timelord.key" diff --git a/tests/core/ssl/test_ssl.py b/tests/core/ssl/test_ssl.py index 791dea7bae5a..34f0015ead62 100644 --- a/tests/core/ssl/test_ssl.py +++ b/tests/core/ssl/test_ssl.py @@ -69,7 +69,7 @@ async def introducer(self): @pytest_asyncio.fixture(scope="function") async def timelord(self): - async for _ in setup_timelord(21236, 21237, False, test_constants, bt): + async for _ in setup_timelord(21236, 21237, 0, False, test_constants, bt): yield _ @pytest.mark.asyncio diff --git a/tests/setup_nodes.py b/tests/setup_nodes.py index f5df5256f3e9..595cd6783f2e 100644 --- a/tests/setup_nodes.py +++ b/tests/setup_nodes.py @@ -311,7 +311,7 @@ def stop(): await kill_processes() -async def setup_timelord(port, full_node_port, sanitizer, consensus_constants: ConsensusConstants, b_tools): +async def setup_timelord(port, full_node_port, rpc_port, sanitizer, consensus_constants: ConsensusConstants, b_tools): config = b_tools.config["timelord"] config["port"] = port config["full_node_peer"]["port"] = full_node_port @@ -319,6 +319,8 @@ async def setup_timelord(port, full_node_port, sanitizer, consensus_constants: C config["fast_algorithm"] = False if sanitizer: config["vdf_server"]["port"] = 7999 + config["start_rpc_server"] = True + config["rpc_port"] = rpc_port kwargs = service_kwargs_for_timelord(b_tools.root_path, config, consensus_constants) kwargs.update( @@ -509,7 +511,7 @@ async def setup_full_system( setup_harvester(21234, 21235, consensus_constants, b_tools), setup_farmer(21235, consensus_constants, b_tools, uint16(21237)), setup_vdf_clients(8000), - setup_timelord(21236, 21237, False, consensus_constants, b_tools), + setup_timelord(21236, 21237, 21241, False, consensus_constants, b_tools), setup_full_node( consensus_constants, "blockchain_test.db", @@ -535,7 +537,7 @@ async def setup_full_system( db_version=db_version, ), setup_vdf_client(7999), - setup_timelord(21239, 1000, True, consensus_constants, b_tools_1), + setup_timelord(21239, 1000, 21242, True, consensus_constants, b_tools_1), ] introducer, introducer_server = await node_iters[0].__anext__() From 457812ac21022f8d6cc2ef37d6d03797ac20b9ce Mon Sep 17 00:00:00 2001 From: Chris Marslender Date: Fri, 18 Feb 2022 09:44:06 -0600 Subject: [PATCH 077/378] Crawler RPC IP endpoint (#10294) * Add endpoint to crawler to get IP addresses seen by the network after a particular timestamp * Add offset/limit for get_ips_after_timestamp --- chia/rpc/crawler_rpc_api.py | 21 +++++++++++ tests/core/test_crawler_rpc.py | 66 ++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 tests/core/test_crawler_rpc.py diff --git a/chia/rpc/crawler_rpc_api.py b/chia/rpc/crawler_rpc_api.py index d063cb9a402d..38173e49f75f 100644 --- a/chia/rpc/crawler_rpc_api.py +++ b/chia/rpc/crawler_rpc_api.py @@ -13,6 +13,7 @@ def __init__(self, crawler: Crawler): def get_routes(self) -> Dict[str, Callable]: return { "/get_peer_counts": self.get_peer_counts, + "/get_ips_after_timestamp": self.get_ips_after_timestamp, } async def _state_changed(self, change: str, change_data: Optional[Dict[str, Any]] = None) -> List[WsRpcMessage]: @@ -49,3 +50,23 @@ async def get_peer_counts(self, _request: Dict) -> Dict[str, Any]: } } return data + + async def get_ips_after_timestamp(self, _request: Dict) -> Dict[str, Any]: + after = _request.get("after", None) + if after is None: + raise ValueError("`after` is required and must be a unix timestamp") + + offset = _request.get("offset", 0) + limit = _request.get("limit", 10000) + + matched_ips: List[str] = [] + for ip, timestamp in self.service.best_timestamp_per_peer.items(): + if timestamp > after: + matched_ips.append(ip) + + matched_ips.sort() + + return { + "ips": matched_ips[offset : (offset + limit)], + "total": len(matched_ips), + } diff --git a/tests/core/test_crawler_rpc.py b/tests/core/test_crawler_rpc.py new file mode 100644 index 000000000000..5fcde02874cb --- /dev/null +++ b/tests/core/test_crawler_rpc.py @@ -0,0 +1,66 @@ +import atexit + +import pytest + +from chia.rpc.crawler_rpc_api import CrawlerRpcApi +from chia.seeder.crawler import Crawler +from tests.block_tools import create_block_tools, test_constants +from tests.util.keyring import TempKeyring + + +def cleanup_keyring(keyring: TempKeyring): + keyring.cleanup() + + +temp_keyring = TempKeyring() +keychain = temp_keyring.get_keychain() +atexit.register(cleanup_keyring, temp_keyring) # Attempt to cleanup the temp keychain +bt = create_block_tools(constants=test_constants, keychain=keychain) + + +class TestCrawlerRpc: + @pytest.mark.asyncio + async def test_get_ips_after_timestamp(self): + crawler = Crawler(bt.config.get("seeder", {}), bt.root_path, consensus_constants=bt.constants) + crawler_rpc_api = CrawlerRpcApi(crawler) + + # Should raise ValueError when `after` is not supplied + with pytest.raises(ValueError): + await crawler_rpc_api.get_ips_after_timestamp({}) + + # Crawler isn't actually crawling, so this should return zero IPs + response = await crawler_rpc_api.get_ips_after_timestamp({"after": 0}) + assert len(response["ips"]) == 0 + + # Add some known data + # IPs are listed here out of order (by time) to test consistent sorting + # Timestamps increase as the IP value increases + crawler.best_timestamp_per_peer["0.0.0.0"] = 0 + crawler.best_timestamp_per_peer["2.2.2.2"] = 1644300000 + crawler.best_timestamp_per_peer["1.1.1.1"] = 1644213600 + crawler.best_timestamp_per_peer["7.7.7.7"] = 1644732000 + crawler.best_timestamp_per_peer["3.3.3.3"] = 1644386400 + crawler.best_timestamp_per_peer["4.4.4.4"] = 1644472800 + crawler.best_timestamp_per_peer["9.9.9.9"] = 1644904800 + crawler.best_timestamp_per_peer["5.5.5.5"] = 1644559200 + crawler.best_timestamp_per_peer["6.6.6.6"] = 1644645600 + crawler.best_timestamp_per_peer["8.8.8.8"] = 1644818400 + + response = await crawler_rpc_api.get_ips_after_timestamp({"after": 0}) + assert len(response["ips"]) == 9 + + response = await crawler_rpc_api.get_ips_after_timestamp({"after": 1644473000}) + assert len(response["ips"]) == 5 + + # Test offset/limit functionality + response = await crawler_rpc_api.get_ips_after_timestamp({"after": 0, "limit": 2}) + assert len(response["ips"]) == 2 + assert response["total"] == 9 + assert response["ips"][0] == "1.1.1.1" + assert response["ips"][1] == "2.2.2.2" + + response = await crawler_rpc_api.get_ips_after_timestamp({"after": 0, "offset": 2, "limit": 2}) + assert len(response["ips"]) == 2 + assert response["total"] == 9 + assert response["ips"][0] == "3.3.3.3" + assert response["ips"][1] == "4.4.4.4" From f8ee3d55995a642ecf7d20724c3f1e64f9c12200 Mon Sep 17 00:00:00 2001 From: Amine Khaldi Date: Fri, 18 Feb 2022 16:48:49 +0100 Subject: [PATCH 078/378] Fix fee validation in cat_spend wallet RPC API (#10284) * Fix fee validation in cat_spend wallet RPC API. * Linting suggested changes. --- chia/rpc/wallet_rpc_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index ba6f4f54409b..d6f601471be5 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -839,7 +839,7 @@ async def cat_spend(self, request): memos: List[bytes] = [] if "memos" in request: memos = [mem.encode("utf-8") for mem in request["memos"]] - if not isinstance(request["amount"], int) or not isinstance(request["amount"], int): + if not isinstance(request["amount"], int) or not isinstance(request["fee"], int): raise ValueError("An integer amount or fee is required (too many decimals)") amount: uint64 = uint64(request["amount"]) if "fee" in request: From 9a3194599dedf59bb27df637cd8c0848ab1907b8 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 18 Feb 2022 10:49:19 -0500 Subject: [PATCH 079/378] do not enable signals on services in tests (#10290) --- chia/server/start_service.py | 5 ++++- tests/setup_nodes.py | 12 ++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/chia/server/start_service.py b/chia/server/start_service.py index c7314b5a7b66..4a8aac70b514 100644 --- a/chia/server/start_service.py +++ b/chia/server/start_service.py @@ -51,6 +51,7 @@ def __init__( rpc_info: Optional[Tuple[type, int]] = None, parse_cli_args=True, connect_to_daemon=True, + handle_signals=True, ) -> None: self.root_path = root_path self.config = load_config(root_path, "config.yaml") @@ -64,6 +65,7 @@ def __init__( self._rpc_task: Optional[asyncio.Task] = None self._rpc_close_task: Optional[asyncio.Task] = None self._network_id: str = network_id + self._handle_signals = handle_signals proctitle_name = f"chia_{service_name}" setproctitle(proctitle_name) @@ -135,7 +137,8 @@ async def start(self, **kwargs) -> None: self._did_start = True - self._enable_signals() + if self._handle_signals: + self._enable_signals() await self._node._start(**kwargs) self._node._shut_down = False diff --git a/tests/setup_nodes.py b/tests/setup_nodes.py index 595cd6783f2e..56d94453e88e 100644 --- a/tests/setup_nodes.py +++ b/tests/setup_nodes.py @@ -119,7 +119,7 @@ async def setup_full_node( connect_to_daemon=connect_to_daemon, ) - service = Service(**kwargs) + service = Service(**kwargs, handle_signals=False) await service.start() @@ -185,7 +185,7 @@ async def setup_wallet_node( connect_to_daemon=False, ) - service = Service(**kwargs) + service = Service(**kwargs, handle_signals=False) await service.start() @@ -210,7 +210,7 @@ async def setup_harvester( connect_to_daemon=False, ) - service = Service(**kwargs) + service = Service(**kwargs, handle_signals=False) if start_service: await service.start() @@ -250,7 +250,7 @@ async def setup_farmer( connect_to_daemon=False, ) - service = Service(**kwargs) + service = Service(**kwargs, handle_signals=False) if start_service: await service.start() @@ -272,7 +272,7 @@ async def setup_introducer(port): connect_to_daemon=False, ) - service = Service(**kwargs) + service = Service(**kwargs, handle_signals=False) await service.start() @@ -328,7 +328,7 @@ async def setup_timelord(port, full_node_port, rpc_port, sanitizer, consensus_co connect_to_daemon=False, ) - service = Service(**kwargs) + service = Service(**kwargs, handle_signals=False) await service.start() From c577b5a0a5b7aed615b345cba9e461872b4df54b Mon Sep 17 00:00:00 2001 From: Amine Khaldi Date: Fri, 18 Feb 2022 16:49:56 +0100 Subject: [PATCH 080/378] Fix lineage proofs primary key constraint. (#10282) * Fix lineage proofs primary key constraint. * Linting suggested changes. --- chia/wallet/cat_wallet/lineage_store.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia/wallet/cat_wallet/lineage_store.py b/chia/wallet/cat_wallet/lineage_store.py index b5eceee1dcbd..f73f4d08532f 100644 --- a/chia/wallet/cat_wallet/lineage_store.py +++ b/chia/wallet/cat_wallet/lineage_store.py @@ -30,7 +30,7 @@ async def create(cls, db_wrapper: DBWrapper, asset_id: str): self.db_wrapper = db_wrapper self.db_connection = self.db_wrapper.db await self.db_connection.execute( - (f"CREATE TABLE IF NOT EXISTS {self.table_name}(" " coin_id text PRIMARY_KEY," " lineage blob)") + (f"CREATE TABLE IF NOT EXISTS {self.table_name}(" " coin_id text PRIMARY KEY," " lineage blob)") ) await self.db_connection.commit() From a17e5fc100eea591df59a7c606de854ebcc0a5b5 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 18 Feb 2022 10:51:52 -0500 Subject: [PATCH 081/378] Split full node tests directory for parallelism (#10108) * Split full node tests directory for parallelism * reduce full node job timeout to 40, duplicate config to new directory * rebuild workflows * add missing __init__.py * update some imports * oops * isort * Revert "isort" This reverts commit 03d836034262ed5d86a71edb4a1a06fb19fb6014. * update isort exclude list --- ...build-test-macos-core-full_node-stores.yml | 100 ++++++++++++++++ .../build-test-macos-core-full_node.yml | 2 +- ...uild-test-ubuntu-core-full_node-stores.yml | 112 ++++++++++++++++++ .../build-test-ubuntu-core-full_node.yml | 2 +- .isort.cfg | 12 +- tests/core/full_node/config.py | 2 +- tests/core/full_node/stores/__init__.py | 0 tests/core/full_node/stores/config.py | 8 ++ .../{ => stores}/test_block_store.py | 0 .../full_node/{ => stores}/test_coin_store.py | 0 .../{ => stores}/test_full_node_store.py | 0 .../full_node/{ => stores}/test_hint_store.py | 0 .../full_node/{ => stores}/test_sync_store.py | 0 tests/core/full_node/test_full_node.py | 2 +- tests/core/full_node/test_performance.py | 2 +- 15 files changed, 229 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/build-test-macos-core-full_node-stores.yml create mode 100644 .github/workflows/build-test-ubuntu-core-full_node-stores.yml create mode 100644 tests/core/full_node/stores/__init__.py create mode 100644 tests/core/full_node/stores/config.py rename tests/core/full_node/{ => stores}/test_block_store.py (100%) rename tests/core/full_node/{ => stores}/test_coin_store.py (100%) rename tests/core/full_node/{ => stores}/test_full_node_store.py (100%) rename tests/core/full_node/{ => stores}/test_hint_store.py (100%) rename tests/core/full_node/{ => stores}/test_sync_store.py (100%) diff --git a/.github/workflows/build-test-macos-core-full_node-stores.yml b/.github/workflows/build-test-macos-core-full_node-stores.yml new file mode 100644 index 000000000000..27f54068e4d0 --- /dev/null +++ b/.github/workflows/build-test-macos-core-full_node-stores.yml @@ -0,0 +1,100 @@ +# +# THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme +# +name: MacOS core-full_node-stores Tests + +on: + push: + branches: + - main + tags: + - '**' + pull_request: + branches: + - '**' + +concurrency: + # SHA is added to the end if on `main` to let all main workflows run + group: ${{ github.ref }}-${{ github.workflow }}-${{ github.event_name }}-${{ github.ref == 'refs/heads/main' && github.sha || '' }} + cancel-in-progress: true + +jobs: + build: + name: MacOS core-full_node-stores Tests + runs-on: ${{ matrix.os }} + timeout-minutes: 40 + strategy: + fail-fast: false + max-parallel: 4 + matrix: + python-version: [3.8, 3.9] + os: [macOS-latest] + + steps: + - name: Checkout Code + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Setup Python environment + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Create keychain for CI use + run: | + security create-keychain -p foo chiachain + security default-keychain -s chiachain + security unlock-keychain -p foo chiachain + security set-keychain-settings -t 7200 -u chiachain + + - name: Get pip cache dir + id: pip-cache + run: | + echo "::set-output name=dir::$(pip cache dir)" + + - name: Cache pip + uses: actions/cache@v2.1.6 + with: + # Note that new runners may break this https://github.com/actions/cache/issues/292 + path: ${{ steps.pip-cache.outputs.dir }} + key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Checkout test blocks and plots + uses: actions/checkout@v2 + with: + repository: 'Chia-Network/test-cache' + path: '.chia' + ref: '0.28.0' + fetch-depth: 1 + + - name: Link home directory + run: | + cd $HOME + ln -s $GITHUB_WORKSPACE/.chia + echo "$HOME/.chia" + ls -al $HOME/.chia + + - name: Run install script + env: + INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} + BUILD_VDF_CLIENT: "N" + run: | + brew install boost + sh install.sh -d + + - name: Install timelord + run: | + . ./activate + sh install-timelord.sh + ./vdf_bench square_asm 400000 + + - name: Test core-full_node-stores code with pytest + run: | + . ./activate + ./venv/bin/py.test tests/core/full_node/stores/test_*.py -s -v --durations 0 +# +# THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme +# diff --git a/.github/workflows/build-test-macos-core-full_node.yml b/.github/workflows/build-test-macos-core-full_node.yml index bf74ecf6c961..e601da8605d5 100644 --- a/.github/workflows/build-test-macos-core-full_node.yml +++ b/.github/workflows/build-test-macos-core-full_node.yml @@ -22,7 +22,7 @@ jobs: build: name: MacOS core-full_node Tests runs-on: ${{ matrix.os }} - timeout-minutes: 80 + timeout-minutes: 40 strategy: fail-fast: false max-parallel: 4 diff --git a/.github/workflows/build-test-ubuntu-core-full_node-stores.yml b/.github/workflows/build-test-ubuntu-core-full_node-stores.yml new file mode 100644 index 000000000000..43377a417133 --- /dev/null +++ b/.github/workflows/build-test-ubuntu-core-full_node-stores.yml @@ -0,0 +1,112 @@ +# +# THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme +# +name: Ubuntu core-full_node-stores Test + +on: + push: + branches: + - main + tags: + - '**' + pull_request: + branches: + - '**' + +concurrency: + # SHA is added to the end if on `main` to let all main workflows run + group: ${{ github.ref }}-${{ github.workflow }}-${{ github.event_name }}-${{ github.ref == 'refs/heads/main' && github.sha || '' }} + cancel-in-progress: true + +jobs: + build: + name: Ubuntu core-full_node-stores Test + runs-on: ${{ matrix.os }} + timeout-minutes: 40 + strategy: + fail-fast: false + max-parallel: 4 + matrix: + python-version: [3.7, 3.8, 3.9] + os: [ubuntu-latest] + + steps: + - name: Checkout Code + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Setup Python environment + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Cache npm + uses: actions/cache@v2.1.6 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + + - name: Get pip cache dir + id: pip-cache + run: | + echo "::set-output name=dir::$(pip cache dir)" + + - name: Cache pip + uses: actions/cache@v2.1.6 + with: + path: ${{ steps.pip-cache.outputs.dir }} + key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Checkout test blocks and plots + uses: actions/checkout@v2 + with: + repository: 'Chia-Network/test-cache' + path: '.chia' + ref: '0.28.0' + fetch-depth: 1 + + - name: Link home directory + run: | + cd $HOME + ln -s $GITHUB_WORKSPACE/.chia + echo "$HOME/.chia" + ls -al $HOME/.chia + + - name: Install ubuntu dependencies + run: | + sudo apt-get install software-properties-common + sudo add-apt-repository ppa:deadsnakes/ppa + sudo apt-get update + sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y + + - name: Run install script + env: + INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} + run: | + sh install.sh -d + + - name: Install timelord + run: | + . ./activate + sh install-timelord.sh + ./vdf_bench square_asm 400000 + + - name: Test core-full_node-stores code with pytest + run: | + . ./activate + ./venv/bin/py.test tests/core/full_node/stores/test_*.py -s -v --durations 0 + + + - name: Check resource usage + run: | + sqlite3 -readonly -separator " " .pymon "select item,cpu_usage,total_time,mem_usage from TEST_METRICS order by mem_usage desc;" >metrics.out + ./tests/check_pytest_monitor_output.py metrics.out + ./tests/check_pytest_monitor_output.py Date: Fri, 18 Feb 2022 10:16:49 -0800 Subject: [PATCH 082/378] updated gui to 485c4eeb70cb5e1823051cf397c820d48d5b4df3 --- chia-blockchain-gui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia-blockchain-gui b/chia-blockchain-gui index 8365b7c89bed..485c4eeb70cb 160000 --- a/chia-blockchain-gui +++ b/chia-blockchain-gui @@ -1 +1 @@ -Subproject commit 8365b7c89bedc98ecc785161e07be440413e62ba +Subproject commit 485c4eeb70cb5e1823051cf397c820d48d5b4df3 From 9a0095b5aa3952136bde07e9cf4f6b875ac5910d Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Fri, 18 Feb 2022 19:19:40 +0100 Subject: [PATCH 083/378] improve checking arguments when converting the blockchain db (#10272) * improve checking arguments when converting the blockchain db * don't print stack traces on errors in chia db upgrade --- chia/cmds/db.py | 19 +++++++++++-------- chia/cmds/db_upgrade_func.py | 8 ++++++++ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/chia/cmds/db.py b/chia/cmds/db.py index e68c438f26a5..87e252b6959b 100644 --- a/chia/cmds/db.py +++ b/chia/cmds/db.py @@ -21,14 +21,17 @@ def db_cmd() -> None: @click.pass_context def db_upgrade_cmd(ctx: click.Context, no_update_config: bool, **kwargs) -> None: - in_db_path = kwargs.get("input") - out_db_path = kwargs.get("output") - db_upgrade_func( - Path(ctx.obj["root_path"]), - None if in_db_path is None else Path(in_db_path), - None if out_db_path is None else Path(out_db_path), - no_update_config, - ) + try: + in_db_path = kwargs.get("input") + out_db_path = kwargs.get("output") + db_upgrade_func( + Path(ctx.obj["root_path"]), + None if in_db_path is None else Path(in_db_path), + None if out_db_path is None else Path(out_db_path), + no_update_config, + ) + except RuntimeError as e: + print(f"FAILED: {e}") if __name__ == "__main__": diff --git a/chia/cmds/db_upgrade_func.py b/chia/cmds/db_upgrade_func.py index 4b48ff2ec54e..0a2f6a3a23ff 100644 --- a/chia/cmds/db_upgrade_func.py +++ b/chia/cmds/db_upgrade_func.py @@ -65,6 +65,14 @@ def convert_v1_to_v2(in_path: Path, out_path: Path) -> None: from contextlib import closing + if not in_path.exists(): + print(f"input file doesn't exist. {in_path}") + raise RuntimeError(f"can't find {in_path}") + + if in_path == out_path: + print(f"output file is the same as the input {in_path}") + raise RuntimeError("invalid conversion files") + if out_path.exists(): print(f"output file already exists. {out_path}") raise RuntimeError("already exists") From 095bb03b568c85b28d8b4d9cb300563c3dd9066e Mon Sep 17 00:00:00 2001 From: Mariano Sorgente <3069354+mariano54@users.noreply.github.com> Date: Fri, 18 Feb 2022 16:41:24 -0500 Subject: [PATCH 084/378] Fix the flaky test (#10304) * Fix the flaky test * Fix MacOS flaky test * Add missing import --- tests/wallet/simple_sync/test_simple_sync_protocol.py | 6 ++++++ tests/wallet/test_wallet.py | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/wallet/simple_sync/test_simple_sync_protocol.py b/tests/wallet/simple_sync/test_simple_sync_protocol.py index d4ee7b073de4..a9314a50e895 100644 --- a/tests/wallet/simple_sync/test_simple_sync_protocol.py +++ b/tests/wallet/simple_sync/test_simple_sync_protocol.py @@ -24,6 +24,7 @@ from chia.wallet.wallet import Wallet from chia.wallet.wallet_state_manager import WalletStateManager from tests.connection_utils import add_dummy_connection +from tests.pools.test_pool_rpc import wallet_is_synced from tests.setup_nodes import self_hostname, setup_simulators_and_wallets, bt from tests.time_out_assert import time_out_assert from tests.wallet.cat_wallet.test_cat_wallet import tx_in_pool @@ -196,6 +197,7 @@ async def test_subscribe_for_ph(self, wallet_node_simulator): data_response_1: RespondToPhUpdates = RespondToCoinUpdates.from_bytes(msg_response_1.data) assert len(data_response_1.coin_states) == 2 * num_blocks # 2 per height farmer / pool reward + await time_out_assert(10, wallet_is_synced, True, wallet_node, full_node_api) tx_record = await wallet.generate_signed_transaction(uint64(10), puzzle_hash, uint64(0)) assert len(tx_record.spend_bundle.removals()) == 1 spent_coin = tx_record.spend_bundle.removals()[0] @@ -213,6 +215,7 @@ async def test_subscribe_for_ph(self, wallet_node_simulator): # Let's make sure the wallet can handle a non ephemeral launcher from chia.wallet.puzzles.singleton_top_layer import SINGLETON_LAUNCHER_HASH + await time_out_assert(10, wallet_is_synced, True, wallet_node, full_node_api) tx_record = await wallet.generate_signed_transaction(uint64(10), SINGLETON_LAUNCHER_HASH, uint64(0)) await wallet.push_transaction(tx_record) @@ -313,6 +316,7 @@ async def test_subscribe_for_coin_id(self, wallet_node_simulator): assert notified_coins == coins # Test getting notification for coin that is about to be created + await time_out_assert(10, wallet_is_synced, True, wallet_node, full_node_api) tx_record = await standard_wallet.generate_signed_transaction(uint64(10), puzzle_hash, uint64(0)) tx_record.spend_bundle.additions() @@ -534,6 +538,7 @@ async def test_subscribe_for_hint(self, wallet_node_simulator): ConditionWithArgs(ConditionOpcode.CREATE_COIN, [hint_puzzle_hash, amount_bin, hint]) ] } + await time_out_assert(10, wallet_is_synced, True, wallet_node, full_node_api) tx: SpendBundle = wt.generate_signed_transaction( 10, wt.get_new_puzzlehash(), @@ -616,6 +621,7 @@ async def test_subscribe_for_hint_long_sync(self, wallet_two_node_simulator): ConditionWithArgs(ConditionOpcode.CREATE_COIN, [hint_puzzle_hash, amount_bin, hint]) ] } + await time_out_assert(10, wallet_is_synced, True, wallet_node, full_node_api) tx: SpendBundle = wt.generate_signed_transaction( 10, wt.get_new_puzzlehash(), diff --git a/tests/wallet/test_wallet.py b/tests/wallet/test_wallet.py index 49e18499768b..8d45d7e43418 100644 --- a/tests/wallet/test_wallet.py +++ b/tests/wallet/test_wallet.py @@ -493,8 +493,8 @@ async def test_wallet_create_hit_max_send_amount(self, two_wallet_nodes, trusted await time_out_assert(5, wallet.get_confirmed_balance, funds) primaries = [] - for i in range(0, 600): - primaries.append({"puzzlehash": ph, "amount": 100000000 + i}) + for i in range(0, 60): + primaries.append({"puzzlehash": ph, "amount": 1000000000 + i}) tx_split_coins = await wallet.generate_signed_transaction(1, ph, 0, primaries=primaries) From 53aaa823ee0d4bc3abb7cd695e89dbe8611e4cef Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 18 Feb 2022 21:57:40 -0500 Subject: [PATCH 085/378] add another wallet is synced assert to test_subscribe_for_ph (#10309) --- tests/wallet/simple_sync/test_simple_sync_protocol.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/wallet/simple_sync/test_simple_sync_protocol.py b/tests/wallet/simple_sync/test_simple_sync_protocol.py index a9314a50e895..c71c92ef592a 100644 --- a/tests/wallet/simple_sync/test_simple_sync_protocol.py +++ b/tests/wallet/simple_sync/test_simple_sync_protocol.py @@ -226,6 +226,8 @@ async def test_subscribe_for_ph(self, wallet_node_simulator): for i in range(0, num_blocks): await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(SINGLETON_LAUNCHER_HASH)) + await time_out_assert(10, wallet_is_synced, True, wallet_node, full_node_api) + # Send a transaction to make sure the wallet is still running tx_record = await wallet.generate_signed_transaction(uint64(10), junk_ph, uint64(0)) await wallet.push_transaction(tx_record) From bea709d979a4e7eb7cc2f0a075eac86cfaf6db23 Mon Sep 17 00:00:00 2001 From: Mariano Sorgente <3069354+mariano54@users.noreply.github.com> Date: Fri, 18 Feb 2022 22:52:00 -0500 Subject: [PATCH 086/378] Ms.memory leak2 (#10287) * Start fixing other issues * Fork point, sync cancelling, random peer * Reduce logging * Improve performance and have a fallback peer for fetching * Disconnect not synced peers * Make sure try catch doesn't fail * Fix memory leaks and keep track of sync target height * Fix lint * Update chia/wallet/wallet_node.py Co-authored-by: Kyle Altendorf * Pylint has a bug so ignore pylint for this line * Fix memory leaks * Increase cache size * Fix flaky test * Fix mistake * Spawn for memory * Revert spawn for now Co-authored-by: Kyle Altendorf Co-authored-by: wjblanke --- chia/wallet/util/peer_request_cache.py | 86 +++++++++++++++++------ chia/wallet/util/wallet_sync_utils.py | 9 ++- chia/wallet/wallet_blockchain.py | 15 ++-- chia/wallet/wallet_node.py | 96 +++++++++++++------------- chia/wallet/wallet_state_manager.py | 5 +- 5 files changed, 133 insertions(+), 78 deletions(-) diff --git a/chia/wallet/util/peer_request_cache.py b/chia/wallet/util/peer_request_cache.py index c26f00842c36..dacfe005880f 100644 --- a/chia/wallet/util/peer_request_cache.py +++ b/chia/wallet/util/peer_request_cache.py @@ -1,43 +1,85 @@ -from typing import Any, Dict, List, Optional, Tuple +import asyncio +from typing import Optional -from chia.protocols.wallet_protocol import CoinState +from chia.protocols.wallet_protocol import CoinState, RespondSESInfo from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.header_block import HeaderBlock from chia.util.ints import uint32 +from chia.util.lru_cache import LRUCache class PeerRequestCache: - blocks: Dict[uint32, HeaderBlock] - block_requests: Dict[Tuple[int, int], Any] - ses_requests: Dict[int, Any] - states_validated: Dict[bytes32, CoinState] + _blocks: LRUCache # height -> HeaderBlock + _block_requests: LRUCache # (start, end) -> RequestHeaderBlocks + _ses_requests: LRUCache # height -> Ses request + _states_validated: LRUCache # coin state hash -> last change height, or None for reorg def __init__(self): - self.blocks = {} - self.ses_requests = {} - self.block_requests = {} - self.states_validated = {} + self._blocks = LRUCache(100) + self._block_requests = LRUCache(100) + self._ses_requests = LRUCache(100) + self._states_validated = LRUCache(1000) + + def get_block(self, height: uint32) -> Optional[HeaderBlock]: + return self._blocks.get(height) + + def add_to_blocks(self, header_block: HeaderBlock) -> None: + self._blocks.put(header_block.height, header_block) + + def get_block_request(self, start: uint32, end: uint32) -> Optional[asyncio.Task]: + return self._block_requests.get((start, end)) + + def add_to_block_requests(self, start: uint32, end: uint32, request: asyncio.Task) -> None: + self._block_requests.put((start, end), request) + + def get_ses_request(self, height: uint32) -> Optional[RespondSESInfo]: + return self._ses_requests.get(height) + + def add_to_ses_requests(self, height: uint32, ses: RespondSESInfo) -> None: + self._ses_requests.put(height, ses) + + def in_states_validated(self, coin_state_hash: bytes32) -> bool: + return self._states_validated.get(coin_state_hash) is not None + + def add_to_states_validated(self, coin_state: CoinState) -> None: + cs_height: Optional[uint32] = None + if coin_state.spent_height is not None: + cs_height = coin_state.spent_height + elif coin_state.created_height is not None: + cs_height = coin_state.created_height + self._states_validated.put(coin_state.get_hash(), cs_height) def clear_after_height(self, height: int): # Remove any cached item which relates to an event that happened at a height above height. - self.blocks = {k: v for k, v in self.blocks.items() if k <= height} - self.block_requests = {k: v for k, v in self.block_requests.items() if k[0] <= height and k[1] <= height} - self.ses_requests = {k: v for k, v in self.ses_requests.items() if k <= height} + new_blocks = LRUCache(self._blocks.capacity) + for k, v in self._blocks.cache.items(): + if k <= height: + new_blocks.put(k, v) + self._blocks = new_blocks + + new_block_requests = LRUCache(self._block_requests.capacity) + for k, v in self._block_requests.cache.items(): + if k[0] <= height and k[1] <= height: + new_block_requests.put(k, v) + self._block_requests = new_block_requests + + new_ses_requests = LRUCache(self._ses_requests.capacity) + for k, v in self._ses_requests.cache.items(): + if k <= height: + new_ses_requests.put(k, v) + self._ses_requests = new_ses_requests - remove_keys_states: List[bytes32] = [] - for k4, coin_state in self.states_validated.items(): - if coin_state.created_height is not None and coin_state.created_height > height: - remove_keys_states.append(k4) - elif coin_state.spent_height is not None and coin_state.spent_height > height: - remove_keys_states.append(k4) - for k5 in remove_keys_states: - self.states_validated.pop(k5) + new_states_validated = LRUCache(self._states_validated.capacity) + for k, cs_height in self._states_validated.cache.items(): + if cs_height is not None: + new_states_validated.put(k, cs_height) + self._states_validated = new_states_validated async def can_use_peer_request_cache( coin_state: CoinState, peer_request_cache: PeerRequestCache, fork_height: Optional[uint32] ): - if coin_state.get_hash() not in peer_request_cache.states_validated: + if not peer_request_cache.in_states_validated(coin_state.get_hash()): return False if fork_height is None: return True diff --git a/chia/wallet/util/wallet_sync_utils.py b/chia/wallet/util/wallet_sync_utils.py index 0dcf2d4b5602..23fe175f81d2 100644 --- a/chia/wallet/util/wallet_sync_utils.py +++ b/chia/wallet/util/wallet_sync_utils.py @@ -329,18 +329,21 @@ async def fetch_header_blocks_in_range( for i in range(start - (start % 32), end + 1, 32): request_start = min(uint32(i), end) request_end = min(uint32(i + 31), end) - if (request_start, request_end) in peer_request_cache.block_requests: - res_h_blocks_task: asyncio.Task = peer_request_cache.block_requests[(request_start, request_end)] + res_h_blocks_task: Optional[asyncio.Task] = peer_request_cache.get_block_request(request_start, request_end) + + if res_h_blocks_task is not None: + log.info(f"Using cache for: {start}-{end}") if res_h_blocks_task.done(): res_h_blocks: Optional[RespondHeaderBlocks] = res_h_blocks_task.result() else: res_h_blocks = await res_h_blocks_task else: + log.info(f"Fetching: {start}-{end}") request_header_blocks = RequestHeaderBlocks(request_start, request_end) res_h_blocks_task = asyncio.create_task( _fetch_header_blocks_inner(all_peers, selected_peer_id, request_header_blocks) ) - peer_request_cache.block_requests[(request_start, request_end)] = res_h_blocks_task + peer_request_cache.add_to_block_requests(request_start, request_end, res_h_blocks_task) res_h_blocks = await res_h_blocks_task if res_h_blocks is None: return None diff --git a/chia/wallet/wallet_blockchain.py b/chia/wallet/wallet_blockchain.py index ae9881d7c743..b105d914df28 100644 --- a/chia/wallet/wallet_blockchain.py +++ b/chia/wallet/wallet_blockchain.py @@ -46,7 +46,7 @@ async def create( self = WalletBlockchain() self._basic_store = _basic_store self.constants = constants - self.CACHE_SIZE = constants.SUB_EPOCH_BLOCKS + 100 + self.CACHE_SIZE = constants.SUB_EPOCH_BLOCKS * 3 self._weight_proof_handler = weight_proof_handler self.synced_weight_proof = await self._basic_store.get_object("SYNCED_WEIGHT_PROOF", WeightProof) self._finished_sync_up_to = await self._basic_store.get_object("FINISHED_SYNC_UP_TO", uint32) @@ -92,7 +92,7 @@ async def new_weight_proof(self, weight_proof: WeightProof, records: Optional[Li self._sub_slot_iters = records[-1].sub_slot_iters self._difficulty = uint64(records[-1].weight - records[-2].weight) await self.set_peak_block(weight_proof.recent_chain_data[-1], latest_timestamp) - self.clean_block_records() + await self.clean_block_records() async def receive_block(self, block: HeaderBlock) -> Tuple[ReceiveBlockResult, Optional[Err]]: if self.contains_block(block.header_hash): @@ -109,6 +109,8 @@ async def receive_block(self, block: HeaderBlock) -> Tuple[ReceiveBlockResult, O else: sub_slot_iters = self._sub_slot_iters difficulty = self._difficulty + + # Validation requires a block cache (self) that goes back to a subepoch barrier required_iters, error = validate_finished_header_block( self.constants, self, block, False, difficulty, sub_slot_iters, False ) @@ -117,6 +119,8 @@ async def receive_block(self, block: HeaderBlock) -> Tuple[ReceiveBlockResult, O if required_iters is None: return ReceiveBlockResult.INVALID_BLOCK, Err.INVALID_POSPACE + # We are passing in sub_slot_iters here so we don't need to backtrack until the start of the epoch to find + # the sub slot iters and difficulty. This allows us to keep the cache small. block_record: BlockRecord = block_to_block_record( self.constants, self, required_iters, None, block, sub_slot_iters ) @@ -147,7 +151,7 @@ async def receive_block(self, block: HeaderBlock) -> Tuple[ReceiveBlockResult, O self._sub_slot_iters = block_record.sub_slot_iters self._difficulty = uint64(block_record.weight - self.block_record(block_record.prev_hash).weight) await self.set_peak_block(block, latest_timestamp) - self.clean_block_records() + await self.clean_block_records() return ReceiveBlockResult.NEW_PEAK, None return ReceiveBlockResult.ADDED_AS_ORPHAN, None @@ -183,6 +187,7 @@ async def get_peak_block(self) -> Optional[HeaderBlock]: async def set_finished_sync_up_to(self, height: uint32): if height > await self.get_finished_sync_up_to(): await self._basic_store.set_object("FINISHED_SYNC_UP_TO", height) + await self.clean_block_records() async def get_finished_sync_up_to(self): h: Optional[uint32] = await self._basic_store.get_object("FINISHED_SYNC_UP_TO", uint32) @@ -213,12 +218,12 @@ def block_record(self, header_hash: bytes32) -> BlockRecord: def add_block_record(self, block_record: BlockRecord): self._block_records[block_record.header_hash] = block_record - def clean_block_records(self): + async def clean_block_records(self): """ Cleans the cache so that we only maintain relevant blocks. This removes block records that have height < peak - CACHE_SIZE. """ - height_limit = max(0, self.get_peak_height() - self.CACHE_SIZE) + height_limit = max(0, (await self.get_finished_sync_up_to()) - self.CACHE_SIZE) if len(self._block_records) < self.CACHE_SIZE: return None diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index 70dbf56d38e9..6e46d29f3d60 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -134,7 +134,7 @@ def __init__( self.wallet_peers = None self.wallet_peers_initialized = False self.valid_wp_cache: Dict[bytes32, Any] = {} - self.untrusted_caches: Dict[bytes32, Any] = {} + self.untrusted_caches: Dict[bytes32, PeerRequestCache] = {} self.race_cache = {} # in Untrusted mode wallet might get the state update before receiving the block self.race_cache_hashes = [] self._process_new_subscriptions_task = None @@ -741,8 +741,8 @@ async def get_timestamp_for_height(self, height: uint32) -> uint64: return self.height_to_time[height] for cache in self.untrusted_caches.values(): - if height in cache.blocks: - block = cache.blocks[height] + block: Optional[HeaderBlock] = cache.get_block(height) + if block is not None: if ( block.foliage_transaction_block is not None and block.foliage_transaction_block.timestamp is not None @@ -1062,28 +1062,30 @@ async def validate_received_state_from_peer( and current_spent_height == spent_height and current.confirmed_block_height == confirmed_height ): - peer_request_cache.states_validated[coin_state.get_hash()] = coin_state + peer_request_cache.add_to_states_validated(coin_state) return True reorg_mode = False - if current is not None and confirmed_height is None: + + # If coin was removed from the blockchain + if confirmed_height is None: + if current is None: + # Coin does not exist in local DB, so no need to do anything + return False # This coin got reorged reorg_mode = True confirmed_height = current.confirmed_block_height - if confirmed_height is None: - return False - # request header block for created height - if confirmed_height in peer_request_cache.blocks and reorg_mode is False: - state_block: HeaderBlock = peer_request_cache.blocks[confirmed_height] - else: + state_block: Optional[HeaderBlock] = peer_request_cache.get_block(confirmed_height) + if state_block is None or reorg_mode: request = RequestHeaderBlocks(confirmed_height, confirmed_height) res = await peer.request_header_blocks(request) if res is None: return False state_block = res.header_blocks[0] - peer_request_cache.blocks[confirmed_height] = state_block + assert state_block is not None + peer_request_cache.add_to_blocks(state_block) # get proof of inclusion assert state_block.foliage_transaction_block is not None @@ -1107,46 +1109,45 @@ async def validate_received_state_from_peer( if not validated: return False - if spent_height is None and current is not None and current.spent_block_height != 0: - # Peer is telling us that coin that was previously known to be spent is not spent anymore - # Check old state - if current.spent_block_height != spent_height: - reorg_mode = True - if spent_height in peer_request_cache.blocks and reorg_mode is False: - spent_state_block: HeaderBlock = peer_request_cache.blocks[current.spent_block_height] - else: + # TODO: make sure all cases are covered + if current is not None: + if spent_height is None and current.spent_block_height != 0: + # Peer is telling us that coin that was previously known to be spent is not spent anymore + # Check old state + request = RequestHeaderBlocks(current.spent_block_height, current.spent_block_height) res = await peer.request_header_blocks(request) spent_state_block = res.header_blocks[0] assert spent_state_block.height == current.spent_block_height - peer_request_cache.blocks[current.spent_block_height] = spent_state_block - assert spent_state_block.foliage_transaction_block is not None - validate_removals_result: bool = await request_and_validate_removals( - peer, - current.spent_block_height, - spent_state_block.header_hash, - coin_state.coin.name(), - spent_state_block.foliage_transaction_block.removals_root, - ) - if validate_removals_result is False: - self.log.warning("Validate false 2") - await peer.close(9999) - return False - validated = await self.validate_block_inclusion(spent_state_block, peer, peer_request_cache) - if not validated: - return False + assert spent_state_block.foliage_transaction_block is not None + peer_request_cache.add_to_blocks(spent_state_block) + + validate_removals_result: bool = await request_and_validate_removals( + peer, + current.spent_block_height, + spent_state_block.header_hash, + coin_state.coin.name(), + spent_state_block.foliage_transaction_block.removals_root, + ) + if validate_removals_result is False: + self.log.warning("Validate false 2") + await peer.close(9999) + return False + validated = await self.validate_block_inclusion(spent_state_block, peer, peer_request_cache) + if not validated: + return False if spent_height is not None: # request header block for created height - if spent_height in peer_request_cache.blocks: - spent_state_block = peer_request_cache.blocks[spent_height] - else: + spent_state_block = peer_request_cache.get_block(spent_height) + if spent_state_block is None: request = RequestHeaderBlocks(spent_height, spent_height) res = await peer.request_header_blocks(request) spent_state_block = res.header_blocks[0] assert spent_state_block.height == spent_height - peer_request_cache.blocks[spent_height] = spent_state_block - assert spent_state_block.foliage_transaction_block is not None + assert spent_state_block.foliage_transaction_block is not None + peer_request_cache.add_to_blocks(spent_state_block) + assert spent_state_block is not None validate_removals_result = await request_and_validate_removals( peer, spent_state_block.height, @@ -1161,7 +1162,8 @@ async def validate_received_state_from_peer( validated = await self.validate_block_inclusion(spent_state_block, peer, peer_request_cache) if not validated: return False - peer_request_cache.states_validated[coin_state.get_hash()] = coin_state + peer_request_cache.add_to_states_validated(coin_state) + return True async def validate_block_inclusion( @@ -1204,11 +1206,11 @@ async def validate_block_inclusion( end = self.constants.SUB_EPOCH_BLOCKS + inserted.num_blocks_overflow else: request = RequestSESInfo(block.height, block.height + 32) - if block.height in peer_request_cache.ses_requests: - res_ses: RespondSESInfo = peer_request_cache.ses_requests[block.height] - else: + res_ses: Optional[RespondSESInfo] = peer_request_cache.get_ses_request(block.height) + if res_ses is None: res_ses = await peer.request_ses_hashes(request) - peer_request_cache.ses_requests[block.height] = res_ses + peer_request_cache.add_to_ses_requests(block.height, res_ses) + assert res_ses is not None ses_0 = res_ses.reward_chain_hash[0] last_height = res_ses.heights[0][-1] # Last height in sub epoch @@ -1284,7 +1286,7 @@ async def validate_block_inclusion( return False return True - async def fetch_puzzle_solution(self, peer, height: uint32, coin: Coin) -> CoinSpend: + async def fetch_puzzle_solution(self, peer: WSChiaConnection, height: uint32, coin: Coin) -> CoinSpend: solution_response = await peer.request_puzzle_solution( wallet_protocol.RequestPuzzleSolution(coin.name(), height) ) diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 6d65c8f49eb0..dac2dfbd45d1 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -87,6 +87,7 @@ class WalletStateManager: # TODO Don't allow user to send tx until wallet is synced sync_mode: bool + sync_target: uint32 genesis: FullBlock state_changed_callback: Optional[Callable] @@ -157,6 +158,7 @@ async def create( self.wallet_node = wallet_node self.sync_mode = False + self.sync_target = uint32(0) self.finished_sync_up_to = uint32(0) self.weight_proof_handler = WalletWeightProofHandler(self.constants) self.blockchain = await WalletBlockchain.create(self.basic_store, self.constants, self.weight_proof_handler) @@ -458,11 +460,12 @@ async def synced(self): return True return False - def set_sync_mode(self, mode: bool): + def set_sync_mode(self, mode: bool, sync_height: uint32 = uint32(0)): """ Sets the sync mode. This changes the behavior of the wallet node. """ self.sync_mode = mode + self.sync_target = sync_height self.state_changed("sync_changed") async def get_confirmed_spendable_balance_for_wallet(self, wallet_id: int, unspent_records=None) -> uint128: From 3f94eae9e1339a135c1ac3533bb04702f1d02582 Mon Sep 17 00:00:00 2001 From: William Blanke Date: Fri, 18 Feb 2022 19:53:50 -0800 Subject: [PATCH 087/378] updated gui to 800a0f6556b89e928b1cf027c996b5ed010a7799 --- chia-blockchain-gui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia-blockchain-gui b/chia-blockchain-gui index 485c4eeb70cb..800a0f6556b8 160000 --- a/chia-blockchain-gui +++ b/chia-blockchain-gui @@ -1 +1 @@ -Subproject commit 485c4eeb70cb5e1823051cf397c820d48d5b4df3 +Subproject commit 800a0f6556b89e928b1cf027c996b5ed010a7799 From 17fcd08914a553beed5f3b0abea1ba96499fe9da Mon Sep 17 00:00:00 2001 From: Chris Marslender Date: Sat, 19 Feb 2022 11:28:37 -0600 Subject: [PATCH 088/378] Add trigger workflow on PR for dev docker images (#10305) --- .github/workflows/trigger-docker-dev.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/workflows/trigger-docker-dev.yml diff --git a/.github/workflows/trigger-docker-dev.yml b/.github/workflows/trigger-docker-dev.yml new file mode 100644 index 000000000000..4a35efe9dc82 --- /dev/null +++ b/.github/workflows/trigger-docker-dev.yml @@ -0,0 +1,19 @@ +name: Trigger Dev Docker Build + +on: + pull_request: + +concurrency: + # SHA is added to the end if on `main` to let all main workflows run + group: ${{ github.ref }}-${{ github.workflow }}-${{ github.event_name }}-${{ github.ref == 'refs/heads/main' && github.sha || '' }} + cancel-in-progress: true + +jobs: + trigger: + name: Trigger building a new dev tag for the chia-docker image + runs-on: ubuntu-latest + steps: + - name: Trigger docker dev workflow via github-glue + run: | + curl -s -XPOST -H "Authorization: Bearer ${{ secrets.GLUE_ACCESS_TOKEN }}" --data '{"sha":"${{ github.sha }}"}' ${{ secrets.GLUE_API_URL }}/api/v1/docker-build-dev/${{ github.sha }}/start + curl -s -XPOST -H "Authorization: Bearer ${{ secrets.GLUE_ACCESS_TOKEN }}" --data '{"sha":"${{ github.sha }}"}' ${{ secrets.GLUE_API_URL }}/api/v1/docker-build-dev/${{ github.sha }}/success/build-dev From 29bd13623839eab29ce7e9445ccc91f95e6f5935 Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Sat, 19 Feb 2022 21:39:17 +0100 Subject: [PATCH 089/378] name processes we spawn in tests with a test_ prefix, to make it easier and find and kill them in case they're left running (#10318) --- chia/server/start_service.py | 3 ++- tests/setup_nodes.py | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/chia/server/start_service.py b/chia/server/start_service.py index 4a8aac70b514..80c46edaa912 100644 --- a/chia/server/start_service.py +++ b/chia/server/start_service.py @@ -52,6 +52,7 @@ def __init__( parse_cli_args=True, connect_to_daemon=True, handle_signals=True, + service_name_prefix="", ) -> None: self.root_path = root_path self.config = load_config(root_path, "config.yaml") @@ -67,7 +68,7 @@ def __init__( self._network_id: str = network_id self._handle_signals = handle_signals - proctitle_name = f"chia_{service_name}" + proctitle_name = f"chia_{service_name_prefix}{service_name}" setproctitle(proctitle_name) self._log = logging.getLogger(service_name) diff --git a/tests/setup_nodes.py b/tests/setup_nodes.py index 56d94453e88e..d45aa4b02bdd 100644 --- a/tests/setup_nodes.py +++ b/tests/setup_nodes.py @@ -117,6 +117,7 @@ async def setup_full_node( kwargs.update( parse_cli_args=False, connect_to_daemon=connect_to_daemon, + service_name_prefix="test_", ) service = Service(**kwargs, handle_signals=False) @@ -183,6 +184,7 @@ async def setup_wallet_node( kwargs.update( parse_cli_args=False, connect_to_daemon=False, + service_name_prefix="test_", ) service = Service(**kwargs, handle_signals=False) @@ -208,6 +210,7 @@ async def setup_harvester( connect_peers=[PeerInfo(self_hostname, farmer_port)], parse_cli_args=False, connect_to_daemon=False, + service_name_prefix="test_", ) service = Service(**kwargs, handle_signals=False) @@ -248,6 +251,7 @@ async def setup_farmer( kwargs.update( parse_cli_args=False, connect_to_daemon=False, + service_name_prefix="test_", ) service = Service(**kwargs, handle_signals=False) @@ -270,6 +274,7 @@ async def setup_introducer(port): advertised_port=port, parse_cli_args=False, connect_to_daemon=False, + service_name_prefix="test_", ) service = Service(**kwargs, handle_signals=False) @@ -326,6 +331,7 @@ async def setup_timelord(port, full_node_port, rpc_port, sanitizer, consensus_co kwargs.update( parse_cli_args=False, connect_to_daemon=False, + service_name_prefix="test_", ) service = Service(**kwargs, handle_signals=False) From b6b57aa2ab9f67f30f21b246b943d11fbf16ffff Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Sat, 19 Feb 2022 21:40:26 +0100 Subject: [PATCH 090/378] reduce output from running tests (#10281) * reduce output from running tests to mostly include logs from failed tests * back-paddle on the log level --- tests/pytest.ini | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/pytest.ini b/tests/pytest.ini index 08d89a020a7d..d0b86d3b8f2b 100644 --- a/tests/pytest.ini +++ b/tests/pytest.ini @@ -1,7 +1,9 @@ [pytest] ; logging options -log_cli = 1 +log_cli = False +addopts = --verbose --tb=short log_level = WARNING +console_output_style = count log_format = %(asctime)s %(name)s: %(levelname)s %(message)s asyncio_mode = strict filterwarnings = From 9358a7b3e19cfc04b92c3dd0db4416a79f9cd35d Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Sat, 19 Feb 2022 15:57:07 -0500 Subject: [PATCH 091/378] fix service restart (#10312) https://github.com/Chia-Network/chia-blockchain/pull/10233/files --- chia/daemon/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia/daemon/server.py b/chia/daemon/server.py index 9ccf680c2bf7..197480dfe1d8 100644 --- a/chia/daemon/server.py +++ b/chia/daemon/server.py @@ -1087,7 +1087,7 @@ async def start_service(self, request: Dict[str, Any]): else: self.log.info(f"Service {service_command} already running") already_running = True - elif service_command in self.connections: + elif len(self.connections.get(service_command, [])) > 0: # If the service was started manually (not launched by the daemon), we should # have a connection to it. self.log.info(f"Service {service_command} already registered") From 45c39f235b5ffa34398449913446656bf3a07e4e Mon Sep 17 00:00:00 2001 From: Mariano Sorgente <3069354+mariano54@users.noreply.github.com> Date: Sat, 19 Feb 2022 21:38:46 -0500 Subject: [PATCH 092/378] Fix infinite loop (#10324) --- chia/wallet/util/wallet_sync_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia/wallet/util/wallet_sync_utils.py b/chia/wallet/util/wallet_sync_utils.py index 23fe175f81d2..1a6871f1bdd3 100644 --- a/chia/wallet/util/wallet_sync_utils.py +++ b/chia/wallet/util/wallet_sync_utils.py @@ -305,7 +305,7 @@ async def _fetch_header_blocks_inner( else: if selected_peer_node_id == bad_peer_id: # Select another peer fallback - while random_peer != bad_peer_id and len(all_peers) > 1: + while random_peer.peer_node_id == bad_peer_id and len(all_peers) > 1: random_peer = random.choice(all_peers) else: # Use the selected peer instead From 71406efe813bfbfcd91707073d371ee535c52c07 Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Sun, 20 Feb 2022 16:40:46 +0100 Subject: [PATCH 093/378] make blockchain.height_to_hash() return None on failure, rather than assert (#10331) --- chia/consensus/blockchain.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/chia/consensus/blockchain.py b/chia/consensus/blockchain.py index 5bde34139c6e..b57ae02a717a 100644 --- a/chia/consensus/blockchain.py +++ b/chia/consensus/blockchain.py @@ -678,6 +678,8 @@ def get_ses(self, height: uint32) -> SubEpochSummary: return self.__height_map.get_ses(height) def height_to_hash(self, height: uint32) -> Optional[bytes32]: + if not self.__height_map.contains_height(height): + return None return self.__height_map.get_hash(height) def contains_height(self, height: uint32) -> bool: From 7ab1152a171ff2c4062f514f116dc753846f5e25 Mon Sep 17 00:00:00 2001 From: Chris Marslender Date: Sun, 20 Feb 2022 09:49:39 -0600 Subject: [PATCH 094/378] Check if secrets are available for triggering dev docker builds (#10337) --- .github/workflows/trigger-docker-dev.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/trigger-docker-dev.yml b/.github/workflows/trigger-docker-dev.yml index 4a35efe9dc82..48507e14bcb9 100644 --- a/.github/workflows/trigger-docker-dev.yml +++ b/.github/workflows/trigger-docker-dev.yml @@ -13,7 +13,19 @@ jobs: name: Trigger building a new dev tag for the chia-docker image runs-on: ubuntu-latest steps: + - name: Test for secrets access + id: check_secrets + shell: bash + run: | + unset HAS_SECRET + + if [ -n "$GLUE_ACCESS_TOKEN" ]; then HAS_SECRET='true' ; fi + echo ::set-output name=HAS_SECRET::${HAS_SECRET} + env: + GLUE_ACCESS_TOKEN: "${{ secrets.GLUE_ACCESS_TOKEN }}" + - name: Trigger docker dev workflow via github-glue + if: steps.check_secrets.outputs.HAS_SECRET run: | curl -s -XPOST -H "Authorization: Bearer ${{ secrets.GLUE_ACCESS_TOKEN }}" --data '{"sha":"${{ github.sha }}"}' ${{ secrets.GLUE_API_URL }}/api/v1/docker-build-dev/${{ github.sha }}/start curl -s -XPOST -H "Authorization: Bearer ${{ secrets.GLUE_ACCESS_TOKEN }}" --data '{"sha":"${{ github.sha }}"}' ${{ secrets.GLUE_API_URL }}/api/v1/docker-build-dev/${{ github.sha }}/success/build-dev From b544e75cd7cc126b2631f69c9948d2aa51161465 Mon Sep 17 00:00:00 2001 From: Amine Khaldi Date: Mon, 21 Feb 2022 06:28:43 +0100 Subject: [PATCH 095/378] Align the wallet node's weight proof timeout with the full node's value. (#10341) * Align the wallet node's weight proof timeout with the full node's value. * Give the wallet config its own weight_proof_timeout value. * Apply Kyle's cleaner version. Co-authored-by: Kyle Altendorf * Apply Kyle's anchors suggestion. Co-authored-by: Kyle Altendorf --- chia/util/initial-config.yaml | 5 ++++- chia/wallet/wallet_node.py | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/chia/util/initial-config.yaml b/chia/util/initial-config.yaml index 40ffb42aa8f5..222eb7fb4972 100644 --- a/chia/util/initial-config.yaml +++ b/chia/util/initial-config.yaml @@ -384,7 +384,7 @@ full_node: # Default is set to False, as the network needs only one or two blueboxes like this. sanitize_weight_proof_only: False # timeout for weight proof request - weight_proof_timeout: 360 + weight_proof_timeout: &weight_proof_timeout 360 # when enabled, the full node will print a pstats profile to the root_dir/profile every second # analyze with chia/utils/profiler.py @@ -519,3 +519,6 @@ wallet: # wallet overrides for limits inbound_rate_limit_percent: 100 outbound_rate_limit_percent: 60 + + # timeout for weight proof request + weight_proof_timeout: *weight_proof_timeout diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index 6e46d29f3d60..259ee60061b7 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -979,7 +979,11 @@ async def fetch_and_validate_the_weight_proof( assert self.wallet_state_manager.weight_proof_handler is not None weight_request = RequestProofOfWeight(peak.height, peak.header_hash) - weight_proof_response: RespondProofOfWeight = await peer.request_proof_of_weight(weight_request, timeout=60) + wp_timeout = self.config.get("weight_proof_timeout", 360) + self.log.debug(f"weight proof timeout is {wp_timeout} sec") + weight_proof_response: RespondProofOfWeight = await peer.request_proof_of_weight( + weight_request, timeout=wp_timeout + ) if weight_proof_response is None: return False, None, [], [] From a59979b8c8ea539c5ec5146196f0c903fa1323ab Mon Sep 17 00:00:00 2001 From: wjblanke Date: Sun, 20 Feb 2022 21:29:16 -0800 Subject: [PATCH 096/378] unused locks (#10320) * unused locks * unused locks * unused locks * unused locks * unused locks * unused locks * removed unused import --- chia/wallet/cat_wallet/lineage_store.py | 4 ---- chia/wallet/wallet_node.py | 1 - chia/wallet/wallet_state_manager.py | 2 -- 3 files changed, 7 deletions(-) diff --git a/chia/wallet/cat_wallet/lineage_store.py b/chia/wallet/cat_wallet/lineage_store.py index f73f4d08532f..7d50e009be2a 100644 --- a/chia/wallet/cat_wallet/lineage_store.py +++ b/chia/wallet/cat_wallet/lineage_store.py @@ -1,4 +1,3 @@ -import asyncio import logging from typing import Dict, Optional @@ -19,7 +18,6 @@ class CATLineageStore: """ db_connection: aiosqlite.Connection - lock: asyncio.Lock db_wrapper: DBWrapper table_name: str @@ -34,8 +32,6 @@ async def create(cls, db_wrapper: DBWrapper, asset_id: str): ) await self.db_connection.commit() - # Lock - self.lock = asyncio.Lock() # external return self async def close(self): diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index 259ee60061b7..7f3d09693ab4 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -211,7 +211,6 @@ async def _start( self.log.info(f"Copying wallet db from {standalone_path} to {path}") path.write_bytes(standalone_path.read_bytes()) - self.new_peak_lock = asyncio.Lock() assert self.server is not None self.wallet_state_manager = await WalletStateManager.create( private_key, diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index dac2dfbd45d1..b20c080f2afe 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -81,8 +81,6 @@ class WalletStateManager: # Makes sure only one asyncio thread is changing the blockchain state at one time lock: asyncio.Lock - tx_lock: asyncio.Lock - log: logging.Logger # TODO Don't allow user to send tx until wallet is synced From 48a1c26232a05f005a61dfbe695f0f688f8ee639 Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Mon, 21 Feb 2022 06:30:07 +0100 Subject: [PATCH 097/378] make mempool tests independent, i.e. possible to run individually (#10317) --- tests/core/full_node/test_mempool.py | 184 +++++++++++++-------------- 1 file changed, 88 insertions(+), 96 deletions(-) diff --git a/tests/core/full_node/test_mempool.py b/tests/core/full_node/test_mempool.py index 721533878ca6..2f5d491bb012 100644 --- a/tests/core/full_node/test_mempool.py +++ b/tests/core/full_node/test_mempool.py @@ -95,7 +95,21 @@ async def two_nodes(): full_node_2 = nodes[1] server_1 = full_node_1.full_node.server server_2 = full_node_2.full_node.server - yield full_node_1, full_node_2, server_1, server_2 + + reward_ph = WALLET_A.get_new_puzzlehash() + blocks = bt.get_consecutive_blocks( + 3, + 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, blocks[-1].height) + + yield full_node_1, full_node_2, server_1, server_2, blocks async for _ in async_gen: yield _ @@ -179,19 +193,8 @@ def test_cost(self): class TestMempool: @pytest.mark.asyncio async def test_basic_mempool(self, two_nodes): - reward_ph = WALLET_A.get_new_puzzlehash() - blocks = bt.get_consecutive_blocks( - 3, - guarantee_transaction_block=True, - farmer_reward_puzzle_hash=reward_ph, - pool_reward_puzzle_hash=reward_ph, - ) - full_node_1, _, server_1, _ = two_nodes - - 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, blocks[-1].height) + full_node_1, full_node_2, server_1, server_2, blocks = two_nodes max_mempool_cost = 40000000 * 5 mempool = Mempool(max_mempool_cost) @@ -230,20 +233,9 @@ async def respond_transaction( class TestMempoolManager: @pytest.mark.asyncio async def test_basic_mempool_manager(self, two_nodes): - reward_ph = WALLET_A.get_new_puzzlehash() - blocks = bt.get_consecutive_blocks( - 5, - guarantee_transaction_block=True, - farmer_reward_puzzle_hash=reward_ph, - pool_reward_puzzle_hash=reward_ph, - ) - full_node_1, full_node_2, server_1, server_2 = two_nodes - peer = await connect_and_get_peer(server_1, server_2) + full_node_1, full_node_2, server_1, server_2, blocks = two_nodes - 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_2, blocks[-1].height) + peer = await connect_and_get_peer(server_1, server_2) spend_bundle = generate_test_spend_bundle(list(blocks[-1].get_included_reward_coins())[0]) assert spend_bundle is not None @@ -299,7 +291,7 @@ def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: bundle = SpendBundle.aggregate([tx1, tx2]) return bundle - full_node_1, _, server_1, _ = two_nodes + full_node_1, _, server_1, _, _ = two_nodes blocks, bundle, status, err = await self.condition_tester2(two_nodes, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) @@ -330,7 +322,7 @@ def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: bundle = SpendBundle.aggregate([spend_bundle1, spend_bundle2]) return bundle - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes blocks, bundle, status, err = await self.condition_tester2(two_nodes, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) @@ -354,7 +346,7 @@ def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: bundle = SpendBundle.aggregate([spend_bundle1, spend_bundle2]) return bundle - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes blocks, bundle, status, err = await self.condition_tester2(two_nodes, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) @@ -365,7 +357,7 @@ def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: @pytest.mark.asyncio async def test_double_spend(self, two_nodes): reward_ph = WALLET_A.get_new_puzzlehash() - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes blocks = await full_node_1.get_all_full_blocks() start_height = blocks[-1].height blocks = bt.get_consecutive_blocks( @@ -426,7 +418,7 @@ def assert_sb_not_in_pool(self, node, sb): async def test_double_spend_with_higher_fee(self, two_nodes): reward_ph = WALLET_A.get_new_puzzlehash() - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes 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( @@ -503,7 +495,7 @@ async def test_double_spend_with_higher_fee(self, two_nodes): async def test_invalid_signature(self, two_nodes): reward_ph = WALLET_A.get_new_puzzlehash() - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes 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( @@ -540,7 +532,7 @@ async def condition_tester( coin: Optional[Coin] = None, ): reward_ph = WALLET_A.get_new_puzzlehash() - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes blocks = await full_node_1.get_all_full_blocks() start_height = blocks[-1].height blocks = bt.get_consecutive_blocks( @@ -571,7 +563,7 @@ async def condition_tester( @pytest.mark.asyncio async def condition_tester2(self, two_nodes, test_fun: Callable[[Coin, Coin], SpendBundle]): reward_ph = WALLET_A.get_new_puzzlehash() - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes 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( @@ -601,7 +593,7 @@ async def condition_tester2(self, two_nodes, test_fun: Callable[[Coin, Coin], Sp @pytest.mark.asyncio async def test_invalid_block_index(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes blocks = await full_node_1.get_all_full_blocks() start_height = blocks[-1].height cvp = ConditionWithArgs( @@ -619,7 +611,7 @@ async def test_invalid_block_index(self, two_nodes): @pytest.mark.asyncio async def test_block_index_missing_arg(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes blocks = await full_node_1.get_all_full_blocks() cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, []) dic = {ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE: [cvp]} @@ -633,7 +625,7 @@ async def test_block_index_missing_arg(self, two_nodes): @pytest.mark.asyncio async def test_correct_block_index(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, [int_to_bytes(1)]) dic = {ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) @@ -645,7 +637,7 @@ async def test_correct_block_index(self, two_nodes): @pytest.mark.asyncio async def test_block_index_garbage(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes # garbage at the end of the argument list is ignored cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, [int_to_bytes(1), b"garbage"]) dic = {ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE: [cvp]} @@ -658,7 +650,7 @@ async def test_block_index_garbage(self, two_nodes): @pytest.mark.asyncio async def test_negative_block_index(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, [int_to_bytes(-1)]) dic = {ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) @@ -670,7 +662,7 @@ async def test_negative_block_index(self, two_nodes): @pytest.mark.asyncio async def test_invalid_block_age(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_RELATIVE, [int_to_bytes(5)]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) @@ -683,7 +675,7 @@ async def test_invalid_block_age(self, two_nodes): @pytest.mark.asyncio async def test_block_age_missing_arg(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_RELATIVE, []) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) @@ -696,7 +688,7 @@ async def test_block_age_missing_arg(self, two_nodes): @pytest.mark.asyncio async def test_correct_block_age(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_RELATIVE, [int_to_bytes(1)]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, num_blocks=4) @@ -709,7 +701,7 @@ async def test_correct_block_age(self, two_nodes): @pytest.mark.asyncio async def test_block_age_garbage(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes # garbage at the end of the argument list is ignored cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_RELATIVE, [int_to_bytes(1), b"garbage"]) dic = {cvp.opcode: [cvp]} @@ -723,7 +715,7 @@ async def test_block_age_garbage(self, two_nodes): @pytest.mark.asyncio async def test_negative_block_age(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_RELATIVE, [int_to_bytes(-1)]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, num_blocks=4) @@ -736,7 +728,7 @@ async def test_negative_block_age(self, two_nodes): @pytest.mark.asyncio async def test_correct_my_id(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes blocks = await full_node_1.get_all_full_blocks() coin = list(blocks[-1].get_included_reward_coins())[0] cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_COIN_ID, [coin.name()]) @@ -751,7 +743,7 @@ async def test_correct_my_id(self, two_nodes): @pytest.mark.asyncio async def test_my_id_garbage(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes blocks = await full_node_1.get_all_full_blocks() coin = list(blocks[-1].get_included_reward_coins())[0] # garbage at the end of the argument list is ignored @@ -767,7 +759,7 @@ async def test_my_id_garbage(self, two_nodes): @pytest.mark.asyncio async def test_invalid_my_id(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes blocks = await full_node_1.get_all_full_blocks() coin = list(blocks[-1].get_included_reward_coins())[0] coin_2 = list(blocks[-2].get_included_reward_coins())[0] @@ -783,7 +775,7 @@ async def test_invalid_my_id(self, two_nodes): @pytest.mark.asyncio async def test_my_id_missing_arg(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes blocks = await full_node_1.get_all_full_blocks() cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_COIN_ID, []) dic = {cvp.opcode: [cvp]} @@ -797,7 +789,7 @@ async def test_my_id_missing_arg(self, two_nodes): @pytest.mark.asyncio async def test_assert_time_exceeds(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes # 5 seconds should be before the next block time_now = full_node_1.full_node.blockchain.get_peak().timestamp + 5 @@ -812,7 +804,7 @@ async def test_assert_time_exceeds(self, two_nodes): @pytest.mark.asyncio async def test_assert_time_fail(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes time_now = full_node_1.full_node.blockchain.get_peak().timestamp + 1000 cvp = ConditionWithArgs(ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, [int_to_bytes(time_now)]) @@ -826,7 +818,7 @@ async def test_assert_time_fail(self, two_nodes): @pytest.mark.asyncio async def test_assert_height_pending(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes print(full_node_1.full_node.blockchain.get_peak()) current_height = full_node_1.full_node.blockchain.get_peak().height @@ -841,7 +833,7 @@ async def test_assert_height_pending(self, two_nodes): @pytest.mark.asyncio async def test_assert_time_negative(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes time_now = -1 cvp = ConditionWithArgs(ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, [int_to_bytes(time_now)]) @@ -855,7 +847,7 @@ async def test_assert_time_negative(self, two_nodes): @pytest.mark.asyncio async def test_assert_time_missing_arg(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes cvp = ConditionWithArgs(ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, []) dic = {cvp.opcode: [cvp]} @@ -868,7 +860,7 @@ async def test_assert_time_missing_arg(self, two_nodes): @pytest.mark.asyncio async def test_assert_time_garbage(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes time_now = full_node_1.full_node.blockchain.get_peak().timestamp + 5 # garbage at the end of the argument list is ignored @@ -883,7 +875,7 @@ async def test_assert_time_garbage(self, two_nodes): @pytest.mark.asyncio async def test_assert_time_relative_exceeds(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes time_relative = 3 cvp = ConditionWithArgs(ConditionOpcode.ASSERT_SECONDS_RELATIVE, [int_to_bytes(time_relative)]) @@ -910,7 +902,7 @@ async def test_assert_time_relative_exceeds(self, two_nodes): @pytest.mark.asyncio async def test_assert_time_relative_garbage(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes time_relative = 0 # garbage at the end of the arguments is ignored @@ -926,7 +918,7 @@ async def test_assert_time_relative_garbage(self, two_nodes): @pytest.mark.asyncio async def test_assert_time_relative_missing_arg(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes cvp = ConditionWithArgs(ConditionOpcode.ASSERT_SECONDS_RELATIVE, []) dic = {cvp.opcode: [cvp]} @@ -940,7 +932,7 @@ async def test_assert_time_relative_missing_arg(self, two_nodes): @pytest.mark.asyncio async def test_assert_time_relative_negative(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes time_relative = -3 cvp = ConditionWithArgs(ConditionOpcode.ASSERT_SECONDS_RELATIVE, [int_to_bytes(time_relative)]) @@ -967,7 +959,7 @@ def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: bundle = SpendBundle.aggregate([spend_bundle1, spend_bundle2]) return bundle - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes blocks, bundle, status, err = await self.condition_tester2(two_nodes, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) @@ -993,7 +985,7 @@ def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: bundle = SpendBundle.aggregate([spend_bundle1, spend_bundle2]) return bundle - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes blocks, bundle, status, err = await self.condition_tester2(two_nodes, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) @@ -1003,7 +995,7 @@ def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: @pytest.mark.asyncio async def test_coin_announcement_missing_arg(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes def test_fun(coin_1: Coin, coin_2: Coin): # missing arg here @@ -1024,7 +1016,7 @@ def test_fun(coin_1: Coin, coin_2: Coin): @pytest.mark.asyncio async def test_coin_announcement_missing_arg2(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes def test_fun(coin_1: Coin, coin_2: Coin): announce = Announcement(coin_2.name(), b"test") @@ -1046,7 +1038,7 @@ def test_fun(coin_1: Coin, coin_2: Coin): @pytest.mark.asyncio async def test_coin_announcement_too_big(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes def test_fun(coin_1: Coin, coin_2: Coin): announce = Announcement(coin_2.name(), bytes([1] * 10000)) @@ -1081,7 +1073,7 @@ def test_fun(coin_1: Coin, coin_2: Coin): # create announcement @pytest.mark.asyncio async def test_invalid_coin_announcement_rejected(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes def test_fun(coin_1: Coin, coin_2: Coin): announce = Announcement(coin_2.name(), b"test") @@ -1110,7 +1102,7 @@ def test_fun(coin_1: Coin, coin_2: Coin): @pytest.mark.asyncio async def test_invalid_coin_announcement_rejected_two(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes def test_fun(coin_1: Coin, coin_2: Coin): announce = Announcement(coin_1.name(), b"test") @@ -1136,7 +1128,7 @@ def test_fun(coin_1: Coin, coin_2: Coin): @pytest.mark.asyncio async def test_correct_puzzle_announcement(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes def test_fun(coin_1: Coin, coin_2: Coin): announce = Announcement(coin_2.puzzle_hash, bytes(0x80)) @@ -1162,7 +1154,7 @@ def test_fun(coin_1: Coin, coin_2: Coin): @pytest.mark.asyncio async def test_puzzle_announcement_garbage(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes def test_fun(coin_1: Coin, coin_2: Coin): announce = Announcement(coin_2.puzzle_hash, bytes(0x80)) @@ -1187,7 +1179,7 @@ def test_fun(coin_1: Coin, coin_2: Coin): @pytest.mark.asyncio async def test_puzzle_announcement_missing_arg(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes def test_fun(coin_1: Coin, coin_2: Coin): # missing arg here @@ -1213,7 +1205,7 @@ def test_fun(coin_1: Coin, coin_2: Coin): @pytest.mark.asyncio async def test_puzzle_announcement_missing_arg2(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes def test_fun(coin_1: Coin, coin_2: Coin): announce = Announcement(coin_2.puzzle_hash, b"test") @@ -1241,7 +1233,7 @@ def test_fun(coin_1: Coin, coin_2: Coin): @pytest.mark.asyncio async def test_invalid_puzzle_announcement_rejected(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes def test_fun(coin_1: Coin, coin_2: Coin): announce = Announcement(coin_2.puzzle_hash, bytes("test", "utf-8")) @@ -1270,7 +1262,7 @@ def test_fun(coin_1: Coin, coin_2: Coin): @pytest.mark.asyncio async def test_invalid_puzzle_announcement_rejected_two(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes def test_fun(coin_1: Coin, coin_2: Coin): announce = Announcement(coin_2.puzzle_hash, b"test") @@ -1300,7 +1292,7 @@ def test_fun(coin_1: Coin, coin_2: Coin): @pytest.mark.asyncio async def test_assert_fee_condition(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes cvp = ConditionWithArgs(ConditionOpcode.RESERVE_FEE, [int_to_bytes(10)]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, fee=10) @@ -1313,7 +1305,7 @@ async def test_assert_fee_condition(self, two_nodes): @pytest.mark.asyncio async def test_assert_fee_condition_garbage(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes # garbage at the end of the arguments is ignored cvp = ConditionWithArgs(ConditionOpcode.RESERVE_FEE, [int_to_bytes(10), b"garbage"]) dic = {cvp.opcode: [cvp]} @@ -1326,7 +1318,7 @@ async def test_assert_fee_condition_garbage(self, two_nodes): @pytest.mark.asyncio async def test_assert_fee_condition_missing_arg(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes cvp = ConditionWithArgs(ConditionOpcode.RESERVE_FEE, []) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, fee=10) @@ -1335,7 +1327,7 @@ async def test_assert_fee_condition_missing_arg(self, two_nodes): @pytest.mark.asyncio async def test_assert_fee_condition_negative_fee(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes cvp = ConditionWithArgs(ConditionOpcode.RESERVE_FEE, [int_to_bytes(-1)]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, fee=10) @@ -1351,7 +1343,7 @@ async def test_assert_fee_condition_negative_fee(self, two_nodes): @pytest.mark.asyncio async def test_assert_fee_condition_fee_too_large(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes cvp = ConditionWithArgs(ConditionOpcode.RESERVE_FEE, [int_to_bytes(2 ** 64)]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, fee=10) @@ -1368,7 +1360,7 @@ async def test_assert_fee_condition_fee_too_large(self, two_nodes): @pytest.mark.asyncio async def test_assert_fee_condition_wrong_fee(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes cvp = ConditionWithArgs(ConditionOpcode.RESERVE_FEE, [int_to_bytes(10)]) dic = {cvp.opcode: [cvp]} @@ -1382,7 +1374,7 @@ async def test_assert_fee_condition_wrong_fee(self, two_nodes): @pytest.mark.asyncio async def test_stealing_fee(self, two_nodes): reward_ph = WALLET_A.get_new_puzzlehash() - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes blocks = await full_node_1.get_all_full_blocks() start_height = blocks[-1].height blocks = bt.get_consecutive_blocks( @@ -1393,7 +1385,7 @@ async def test_stealing_fee(self, two_nodes): pool_reward_puzzle_hash=reward_ph, ) - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes peer = await connect_and_get_peer(server_1, server_2) for block in blocks: @@ -1439,7 +1431,7 @@ async def test_stealing_fee(self, two_nodes): @pytest.mark.asyncio async def test_double_spend_same_bundle(self, two_nodes): reward_ph = WALLET_A.get_new_puzzlehash() - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes blocks = await full_node_1.get_all_full_blocks() start_height = blocks[-1].height blocks = bt.get_consecutive_blocks( @@ -1481,7 +1473,7 @@ async def test_double_spend_same_bundle(self, two_nodes): @pytest.mark.asyncio async def test_agg_sig_condition(self, two_nodes): reward_ph = WALLET_A.get_new_puzzlehash() - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes blocks = await full_node_1.get_all_full_blocks() start_height = blocks[-1].height blocks = bt.get_consecutive_blocks( @@ -1531,7 +1523,7 @@ async def test_agg_sig_condition(self, two_nodes): @pytest.mark.asyncio async def test_correct_my_parent(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes blocks = await full_node_1.get_all_full_blocks() coin = list(blocks[-1].get_included_reward_coins())[0] cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_PARENT_ID, [coin.parent_coin_info]) @@ -1547,7 +1539,7 @@ async def test_correct_my_parent(self, two_nodes): @pytest.mark.asyncio async def test_my_parent_garbage(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes blocks = await full_node_1.get_all_full_blocks() coin = list(blocks[-1].get_included_reward_coins())[0] # garbage at the end of the arguments list is allowed but stripped @@ -1564,7 +1556,7 @@ async def test_my_parent_garbage(self, two_nodes): @pytest.mark.asyncio async def test_my_parent_missing_arg(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes blocks = await full_node_1.get_all_full_blocks() cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_PARENT_ID, []) dic = {cvp.opcode: [cvp]} @@ -1579,7 +1571,7 @@ async def test_my_parent_missing_arg(self, two_nodes): @pytest.mark.asyncio async def test_invalid_my_parent(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes blocks = await full_node_1.get_all_full_blocks() coin = list(blocks[-1].get_included_reward_coins())[0] coin_2 = list(blocks[-2].get_included_reward_coins())[0] @@ -1596,7 +1588,7 @@ async def test_invalid_my_parent(self, two_nodes): @pytest.mark.asyncio async def test_correct_my_puzhash(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes blocks = await full_node_1.get_all_full_blocks() coin = list(blocks[-1].get_included_reward_coins())[0] cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_PUZZLEHASH, [coin.puzzle_hash]) @@ -1612,7 +1604,7 @@ async def test_correct_my_puzhash(self, two_nodes): @pytest.mark.asyncio async def test_my_puzhash_garbage(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes blocks = await full_node_1.get_all_full_blocks() coin = list(blocks[-1].get_included_reward_coins())[0] # garbage at the end of the arguments list is allowed but stripped @@ -1629,7 +1621,7 @@ async def test_my_puzhash_garbage(self, two_nodes): @pytest.mark.asyncio async def test_my_puzhash_missing_arg(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes blocks = await full_node_1.get_all_full_blocks() cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_PUZZLEHASH, []) dic = {cvp.opcode: [cvp]} @@ -1644,7 +1636,7 @@ async def test_my_puzhash_missing_arg(self, two_nodes): @pytest.mark.asyncio async def test_invalid_my_puzhash(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes blocks = await full_node_1.get_all_full_blocks() coin = list(blocks[-1].get_included_reward_coins())[0] cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_PUZZLEHASH, [Program.to([]).get_tree_hash()]) @@ -1660,7 +1652,7 @@ async def test_invalid_my_puzhash(self, two_nodes): @pytest.mark.asyncio async def test_correct_my_amount(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes blocks = await full_node_1.get_all_full_blocks() coin = list(blocks[-1].get_included_reward_coins())[0] cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_AMOUNT, [int_to_bytes(coin.amount)]) @@ -1676,7 +1668,7 @@ async def test_correct_my_amount(self, two_nodes): @pytest.mark.asyncio async def test_my_amount_garbage(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes blocks = await full_node_1.get_all_full_blocks() coin = list(blocks[-1].get_included_reward_coins())[0] # garbage at the end of the arguments list is allowed but stripped @@ -1693,7 +1685,7 @@ async def test_my_amount_garbage(self, two_nodes): @pytest.mark.asyncio async def test_my_amount_missing_arg(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes blocks = await full_node_1.get_all_full_blocks() cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_AMOUNT, []) dic = {cvp.opcode: [cvp]} @@ -1708,7 +1700,7 @@ async def test_my_amount_missing_arg(self, two_nodes): @pytest.mark.asyncio async def test_invalid_my_amount(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes blocks = await full_node_1.get_all_full_blocks() cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_AMOUNT, [int_to_bytes(1000)]) dic = {cvp.opcode: [cvp]} @@ -1723,7 +1715,7 @@ async def test_invalid_my_amount(self, two_nodes): @pytest.mark.asyncio async def test_negative_my_amount(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes blocks = await full_node_1.get_all_full_blocks() cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_AMOUNT, [int_to_bytes(-1)]) dic = {cvp.opcode: [cvp]} @@ -1738,7 +1730,7 @@ async def test_negative_my_amount(self, two_nodes): @pytest.mark.asyncio async def test_my_amount_too_large(self, two_nodes): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes blocks = await full_node_1.get_all_full_blocks() cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_AMOUNT, [int_to_bytes(2 ** 64)]) dic = {cvp.opcode: [cvp]} @@ -2387,7 +2379,7 @@ async def test_invalid_coin_spend_coin(self, two_nodes): farmer_reward_puzzle_hash=reward_ph, pool_reward_puzzle_hash=reward_ph, ) - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2, _ = two_nodes for block in blocks: await full_node_1.full_node.respond_block(full_node_protocol.RespondBlock(block)) From 3608d25c8059b0457471fd5396a0892a09b3fed3 Mon Sep 17 00:00:00 2001 From: dustinface <35775977+xdustinface@users.noreply.github.com> Date: Mon, 21 Feb 2022 06:30:57 +0100 Subject: [PATCH 098/378] type_checking: Drop some redundant `None` checks (#10334) --- chia/util/type_checking.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/chia/util/type_checking.py b/chia/util/type_checking.py index 267448a81733..df2239363f14 100644 --- a/chia/util/type_checking.py +++ b/chia/util/type_checking.py @@ -17,18 +17,18 @@ def get_origin(t: Type[Any]) -> Optional[Type[Any]]: def is_type_List(f_type: Type) -> bool: - return (get_origin(f_type) is not None and get_origin(f_type) == list) or f_type == list + return get_origin(f_type) == list or f_type == list def is_type_SpecificOptional(f_type) -> bool: """ Returns true for types such as Optional[T], but not Optional, or T. """ - return get_origin(f_type) is not None and f_type.__origin__ == Union and get_args(f_type)[1]() is None + return get_origin(f_type) == Union and get_args(f_type)[1]() is None def is_type_Tuple(f_type: Type) -> bool: - return (get_origin(f_type) is not None and get_origin(f_type) == tuple) or f_type == tuple + return get_origin(f_type) == tuple or f_type == tuple def strictdataclass(cls: Any): From 8a5c12541809526ef55995674595ed6485c6c330 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Mon, 21 Feb 2022 00:32:23 -0500 Subject: [PATCH 099/378] apply stricter mypy to all new files (#10133) * just put __init__.py files where we have .py files * and the rest * remove unused ignore * update mypy ignore list * remove chia.util.clvm pass-through file * import ConsensusConstants from actual definition * in tests too * import ConditionOpcode from actual definition * a little less ignore * update mypy reduced-strictness set * update mypy reduced-strictness set --- mypy.ini | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/mypy.ini b/mypy.ini index 2ae3429cb60f..9a671dd0ae35 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,8 +1,31 @@ [mypy] -files = benchmarks,build_scripts,chia,tests,*.py +files = benchmarks,build_scripts,chia,tests,tools,*.py ignore_missing_imports = True show_error_codes = True warn_unused_ignores = True -[mypy - lib] -ignore_errors = True +disallow_any_generics = True +disallow_subclassing_any = True +disallow_untyped_calls = True +disallow_untyped_defs = True +disallow_incomplete_defs = True +check_untyped_defs = True +disallow_untyped_decorators = True +no_implicit_optional = True +warn_return_any = True +no_implicit_reexport = True +strict_equality = True + +# list created by: venv/bin/mypy | sed -n 's/.py:.*//p' | sort | uniq | tr '/' '.' | tr '\n' ',' +[mypy-benchmarks.block_ref,benchmarks.block_store,benchmarks.coin_store,benchmarks.utils,build_scripts.installer-version,chia.clvm.spend_sim,chia.cmds.configure,chia.cmds.db,chia.cmds.db_upgrade_func,chia.cmds.farm_funcs,chia.cmds.init,chia.cmds.init_funcs,chia.cmds.keys,chia.cmds.keys_funcs,chia.cmds.passphrase,chia.cmds.passphrase_funcs,chia.cmds.plotnft,chia.cmds.plotnft_funcs,chia.cmds.plots,chia.cmds.plotters,chia.cmds.show,chia.cmds.start_funcs,chia.cmds.wallet,chia.cmds.wallet_funcs,chia.consensus.block_body_validation,chia.consensus.blockchain,chia.consensus.blockchain_interface,chia.consensus.block_creation,chia.consensus.block_header_validation,chia.consensus.block_record,chia.consensus.block_root_validation,chia.consensus.coinbase,chia.consensus.constants,chia.consensus.difficulty_adjustment,chia.consensus.get_block_challenge,chia.consensus.multiprocess_validation,chia.consensus.pos_quality,chia.consensus.vdf_info_computation,chia.daemon.client,chia.daemon.keychain_proxy,chia.daemon.keychain_server,chia.daemon.server,chia.farmer.farmer,chia.farmer.farmer_api,chia.full_node.block_height_map,chia.full_node.block_store,chia.full_node.bundle_tools,chia.full_node.coin_store,chia.full_node.full_node,chia.full_node.full_node_api,chia.full_node.full_node_store,chia.full_node.generator,chia.full_node.hint_store,chia.full_node.lock_queue,chia.full_node.mempool,chia.full_node.mempool_check_conditions,chia.full_node.mempool_manager,chia.full_node.pending_tx_cache,chia.full_node.sync_store,chia.full_node.weight_proof,chia.harvester.harvester,chia.harvester.harvester_api,chia.introducer.introducer,chia.introducer.introducer_api,chia.plotters.bladebit,chia.plotters.chiapos,chia.plotters.install_plotter,chia.plotters.madmax,chia.plotters.plotters,chia.plotters.plotters_util,chia.plotting.check_plots,chia.plotting.create_plots,chia.plotting.manager,chia.plotting.util,chia.pools.pool_config,chia.pools.pool_puzzles,chia.pools.pool_wallet,chia.pools.pool_wallet_info,chia.protocols.pool_protocol,chia.rpc.crawler_rpc_api,chia.rpc.farmer_rpc_api,chia.rpc.farmer_rpc_client,chia.rpc.full_node_rpc_api,chia.rpc.full_node_rpc_client,chia.rpc.harvester_rpc_api,chia.rpc.harvester_rpc_client,chia.rpc.rpc_client,chia.rpc.rpc_server,chia.rpc.timelord_rpc_api,chia.rpc.util,chia.rpc.wallet_rpc_api,chia.rpc.wallet_rpc_client,chia.seeder.crawler,chia.seeder.crawler_api,chia.seeder.crawl_store,chia.seeder.dns_server,chia.seeder.peer_record,chia.seeder.start_crawler,chia.server.address_manager,chia.server.address_manager_store,chia.server.connection_utils,chia.server.introducer_peers,chia.server.node_discovery,chia.server.peer_store_resolver,chia.server.rate_limits,chia.server.reconnect_task,chia.server.server,chia.server.ssl_context,chia.server.start_farmer,chia.server.start_full_node,chia.server.start_harvester,chia.server.start_introducer,chia.server.start_service,chia.server.start_timelord,chia.server.start_wallet,chia.server.upnp,chia.server.ws_connection,chia.simulator.full_node_simulator,chia.simulator.start_simulator,chia.ssl.create_ssl,chia.timelord.iters_from_block,chia.timelord.timelord,chia.timelord.timelord_api,chia.timelord.timelord_launcher,chia.timelord.timelord_state,chia.types.announcement,chia.types.blockchain_format.classgroup,chia.types.blockchain_format.coin,chia.types.blockchain_format.program,chia.types.blockchain_format.proof_of_space,chia.types.blockchain_format.tree_hash,chia.types.blockchain_format.vdf,chia.types.full_block,chia.types.header_block,chia.types.mempool_item,chia.types.name_puzzle_condition,chia.types.peer_info,chia.types.spend_bundle,chia.types.transaction_queue_entry,chia.types.unfinished_block,chia.types.unfinished_header_block,chia.util.api_decorators,chia.util.block_cache,chia.util.byte_types,chia.util.cached_bls,chia.util.check_fork_next_block,chia.util.chia_logging,chia.util.config,chia.util.db_wrapper,chia.util.dump_keyring,chia.util.file_keyring,chia.util.files,chia.util.hash,chia.util.ints,chia.util.json_util,chia.util.keychain,chia.util.keyring_wrapper,chia.util.log_exceptions,chia.util.lru_cache,chia.util.make_test_constants,chia.util.merkle_set,chia.util.network,chia.util.partial_func,chia.util.pip_import,chia.util.profiler,chia.util.safe_cancel_task,chia.util.service_groups,chia.util.ssl_check,chia.util.streamable,chia.util.struct_stream,chia.util.type_checking,chia.util.validate_alert,chia.wallet.block_record,chia.wallet.cat_wallet.cat_utils,chia.wallet.cat_wallet.cat_wallet,chia.wallet.cat_wallet.lineage_store,chia.wallet.chialisp,chia.wallet.did_wallet.did_wallet,chia.wallet.did_wallet.did_wallet_puzzles,chia.wallet.key_val_store,chia.wallet.lineage_proof,chia.wallet.payment,chia.wallet.puzzles.genesis_checkers,chia.wallet.puzzles.load_clvm,chia.wallet.puzzles.p2_conditions,chia.wallet.puzzles.p2_delegated_conditions,chia.wallet.puzzles.p2_delegated_puzzle,chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle,chia.wallet.puzzles.p2_m_of_n_delegate_direct,chia.wallet.puzzles.p2_puzzle_hash,chia.wallet.puzzles.prefarm.spend_prefarm,chia.wallet.puzzles.puzzle_utils,chia.wallet.puzzles.rom_bootstrap_generator,chia.wallet.puzzles.singleton_top_layer,chia.wallet.puzzles.tails,chia.wallet.rl_wallet.rl_wallet,chia.wallet.rl_wallet.rl_wallet_puzzles,chia.wallet.secret_key_store,chia.wallet.settings.user_settings,chia.wallet.trade_manager,chia.wallet.trade_record,chia.wallet.trading.offer,chia.wallet.trading.trade_store,chia.wallet.transaction_record,chia.wallet.util.backup_utils,chia.wallet.util.debug_spend_bundle,chia.wallet.util.new_peak_queue,chia.wallet.util.peer_request_cache,chia.wallet.util.wallet_sync_utils,chia.wallet.wallet,chia.wallet.wallet_action_store,chia.wallet.wallet_blockchain,chia.wallet.wallet_coin_store,chia.wallet.wallet_interested_store,chia.wallet.wallet_node,chia.wallet.wallet_node_api,chia.wallet.wallet_pool_store,chia.wallet.wallet_puzzle_store,chia.wallet.wallet_state_manager,chia.wallet.wallet_sync_store,chia.wallet.wallet_transaction_store,chia.wallet.wallet_user_store,chia.wallet.wallet_weight_proof_handler,installhelper,tests.blockchain.blockchain_test_utils,tests.blockchain.test_blockchain,tests.blockchain.test_blockchain_transactions,tests.block_tools,tests.build-init-files,tests.build-workflows,tests.clvm.coin_store,tests.clvm.test_chialisp_deserialization,tests.clvm.test_clvm_compilation,tests.clvm.test_program,tests.clvm.test_puzzle_compression,tests.clvm.test_puzzles,tests.clvm.test_serialized_program,tests.clvm.test_singletons,tests.clvm.test_spend_sim,tests.conftest,tests.connection_utils,tests.core.cmds.test_keys,tests.core.consensus.test_pot_iterations,tests.core.custom_types.test_coin,tests.core.custom_types.test_proof_of_space,tests.core.custom_types.test_spend_bundle,tests.core.daemon.test_daemon,tests.core.full_node.full_sync.test_full_sync,tests.core.full_node.stores.test_block_store,tests.core.full_node.stores.test_coin_store,tests.core.full_node.stores.test_full_node_store,tests.core.full_node.stores.test_hint_store,tests.core.full_node.stores.test_sync_store,tests.core.full_node.test_address_manager,tests.core.full_node.test_block_height_map,tests.core.full_node.test_conditions,tests.core.full_node.test_full_node,tests.core.full_node.test_mempool,tests.core.full_node.test_mempool_performance,tests.core.full_node.test_node_load,tests.core.full_node.test_peer_store_resolver,tests.core.full_node.test_performance,tests.core.full_node.test_transactions,tests.core.make_block_generator,tests.core.node_height,tests.core.server.test_dos,tests.core.server.test_rate_limits,tests.core.ssl.test_ssl,tests.core.test_cost_calculation,tests.core.test_crawler_rpc,tests.core.test_daemon_rpc,tests.core.test_db_conversion,tests.core.test_farmer_harvester_rpc,tests.core.test_filter,tests.core.test_full_node_rpc,tests.core.test_merkle_set,tests.core.test_setproctitle,tests.core.util.test_cached_bls,tests.core.util.test_config,tests.core.util.test_file_keyring_synchronization,tests.core.util.test_files,tests.core.util.test_keychain,tests.core.util.test_keyring_wrapper,tests.core.util.test_lru_cache,tests.core.util.test_significant_bits,tests.core.util.test_streamable,tests.core.util.test_type_checking,tests.farmer_harvester.test_farmer_harvester,tests.generator.test_compression,tests.generator.test_generator_types,tests.generator.test_list_to_batches,tests.generator.test_rom,tests.generator.test_scan,tests.plotting.test_plot_manager,tests.pools.test_pool_cmdline,tests.pools.test_pool_config,tests.pools.test_pool_puzzles_lifecycle,tests.pools.test_pool_rpc,tests.pools.test_wallet_pool_store,tests.setup_nodes,tests.simulation.test_simulation,tests.time_out_assert,tests.tools.test_full_sync,tests.tools.test_run_block,tests.util.alert_server,tests.util.benchmark_cost,tests.util.blockchain,tests.util.build_network_protocol_files,tests.util.db_connection,tests.util.generator_tools_testing,tests.util.keyring,tests.util.key_tool,tests.util.misc,tests.util.network,tests.util.rpc,tests.util.test_full_block_utils,tests.util.test_lock_queue,tests.util.test_network_protocol_files,tests.util.test_struct_stream,tests.wallet.cat_wallet.test_cat_lifecycle,tests.wallet.cat_wallet.test_cat_wallet,tests.wallet.cat_wallet.test_offer_lifecycle,tests.wallet.cat_wallet.test_trades,tests.wallet.did_wallet.test_did,tests.wallet.did_wallet.test_did_rpc,tests.wallet.rl_wallet.test_rl_rpc,tests.wallet.rl_wallet.test_rl_wallet,tests.wallet.rpc.test_wallet_rpc,tests.wallet.simple_sync.test_simple_sync_protocol,tests.wallet.sync.test_wallet_sync,tests.wallet.test_bech32m,tests.wallet.test_chialisp,tests.wallet.test_puzzle_store,tests.wallet.test_singleton,tests.wallet.test_singleton_lifecycle,tests.wallet.test_singleton_lifecycle_fast,tests.wallet.test_taproot,tests.wallet.test_wallet,tests.wallet.test_wallet_blockchain,tests.wallet.test_wallet_interested_store,tests.wallet.test_wallet_key_val_store,tests.wallet.test_wallet_user_store,tests.wallet_tools,tests.weight_proof.test_weight_proof,tools.analyze-chain,tools.run_block,tools.test_full_sync] +disallow_any_generics = False +disallow_subclassing_any = False +disallow_untyped_calls = False +disallow_untyped_defs = False +disallow_incomplete_defs = False +check_untyped_defs = False +disallow_untyped_decorators = False +no_implicit_optional = False +warn_return_any = False +no_implicit_reexport = False +strict_equality = False From e3fb3ce96e91380535fc48f8f7a03c43f540ced4 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Mon, 21 Feb 2022 00:33:09 -0500 Subject: [PATCH 100/378] Move black from SuperLinter to upload workflow, use 21.12b0 in pre-commit (#10103) * black==21.12b0 in pre-commit Match `setup.py`. * move black from super linter to upload workflow * black (updated) * configure so black . works * --check --diff for black --- .github/workflows/super-linter.yml | 2 -- .github/workflows/upload-pypi-source.yml | 4 ++++ .pre-commit-config.yaml | 2 +- chia/daemon/server.py | 1 - chia/util/streamable.py | 1 - chia/util/type_checking.py | 1 - pyproject.toml | 8 ++++++++ 7 files changed, 13 insertions(+), 6 deletions(-) diff --git a/.github/workflows/super-linter.yml b/.github/workflows/super-linter.yml index 7cfd47017140..255ae54c0f32 100644 --- a/.github/workflows/super-linter.yml +++ b/.github/workflows/super-linter.yml @@ -62,7 +62,6 @@ jobs: DEFAULT_BRANCH: main LINTER_RULES_PATH: . MARKDOWN_CONFIG_FILE: .markdown-lint.yml - PYTHON_BLACK_CONFIG_FILE: pyproject.toml PYTHON_FLAKE8_CONFIG_FILE: .flake8 PYTHON_ISORT_CONFIG_FILE: .isort.cfg PYTHON_PYLINT_CONFIG_FILE: pylintrc @@ -77,7 +76,6 @@ jobs: VALIDATE_POWERSHELL: true VALIDATE_PYTHON_PYLINT: true VALIDATE_PYTHON_FLAKE8: true - VALIDATE_PYTHON_BLACK: true # VALIDATE_PYTHON_ISORT: true VALIDATE_SHELL_SHFMT: true VALIDATE_TYPESCRIPT_ES: true diff --git a/.github/workflows/upload-pypi-source.yml b/.github/workflows/upload-pypi-source.yml index d2682e129457..b4e908de89d5 100644 --- a/.github/workflows/upload-pypi-source.yml +++ b/.github/workflows/upload-pypi-source.yml @@ -48,6 +48,10 @@ jobs: python -m pip install --upgrade pip setuptools wheel python -m pip install .[dev] + - name: Lint source with black + run: | + black --check --diff . + - name: Lint source with flake8 run: | flake8 chia tests diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 253408f85bda..fdaba8fc8346 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -46,7 +46,7 @@ repos: - id: check-ast - id: debug-statements - repo: https://github.com/psf/black - rev: 21.8b0 + rev: 21.12b0 hooks: - id: black - repo: https://gitlab.com/pycqa/flake8 diff --git a/chia/daemon/server.py b/chia/daemon/server.py index 197480dfe1d8..1216d66de4c1 100644 --- a/chia/daemon/server.py +++ b/chia/daemon/server.py @@ -114,7 +114,6 @@ def executable_for_service(service_name: str) -> str: path = f"{application_path}/{name_map[service_name]}" return path - else: application_path = os.path.dirname(__file__) diff --git a/chia/util/streamable.py b/chia/util/streamable.py index e4d6fdaeed48..d2281bbe961e 100644 --- a/chia/util/streamable.py +++ b/chia/util/streamable.py @@ -23,7 +23,6 @@ def get_args(t: Type[Any]) -> Tuple[Any, ...]: return getattr(t, "__args__", ()) - else: from typing import get_args diff --git a/chia/util/type_checking.py b/chia/util/type_checking.py index df2239363f14..8f9adf38e50d 100644 --- a/chia/util/type_checking.py +++ b/chia/util/type_checking.py @@ -10,7 +10,6 @@ def get_args(t: Type[Any]) -> Tuple[Any, ...]: def get_origin(t: Type[Any]) -> Optional[Type[Any]]: return getattr(t, "__origin__", None) - else: from typing import get_args, get_origin diff --git a/pyproject.toml b/pyproject.toml index 9e550c7af6b4..b6d308171269 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,3 +8,11 @@ local_scheme = "no-local-version" [tool.black] line-length = 120 +target-version = ['py37', 'py38', 'py39'] +include = ''' +^/( + [^/]*.py + | (benchmarks|build_scripts|chia|tests|tools)/.*\.pyi? +)$ +''' +exclude = '' From e35174c26827412fa8202a77dc8b9dea44b7f5c4 Mon Sep 17 00:00:00 2001 From: Patrick Maslana <79757486+pmaslana@users.noreply.github.com> Date: Mon, 21 Feb 2022 10:19:47 -0700 Subject: [PATCH 101/378] Fixed two typos in the initial-config.yaml (#10357) * Fixed two typos in the initial-config.yaml * Changed the letter case for a word. --- chia/util/initial-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chia/util/initial-config.yaml b/chia/util/initial-config.yaml index 222eb7fb4972..43bd7f154ca6 100644 --- a/chia/util/initial-config.yaml +++ b/chia/util/initial-config.yaml @@ -291,7 +291,7 @@ timelord: # without slowing down but running more than 1 with fast_algorithm will # run each vdf_client slower. fast_algorithm: False - # Bluebox (sanatizing Timelord): + # Bluebox (sanitizing timelord): # If set 'True', the timelord will create compact proofs of time, instead of # extending the chain. The attribute 'fast_algorithm' won't apply if timelord # is running in bluebox_mode. @@ -375,7 +375,7 @@ full_node: # Only connect to peers who we have heard about in the last recent_peer_threshold seconds recent_peer_threshold: 6000 - # Send to a Bluebox (sanatizing timelord) uncompact blocks once every + # Send to a Bluebox (sanitizing timelord) uncompact blocks once every # 'send_uncompact_interval' seconds. Set to 0 if you don't use this feature. send_uncompact_interval: 0 # At every 'send_uncompact_interval' seconds, send blueboxes 'target_uncompact_proofs' proofs to be normalized. From 58019afd5b5ed167dc30d389ff6ebe41d23f6663 Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Tue, 22 Feb 2022 05:07:05 +0100 Subject: [PATCH 102/378] chunk coin_store request into smaller sql queries (#10359) * chunk coin_store request into smaller sql queries, to not exceed the limit of 999 on old versions of sqlite * extend tests for chunks --- chia/full_node/coin_store.py | 105 +++++++++++++++++---------------- chia/full_node/weight_proof.py | 6 +- chia/util/chunks.py | 9 +++ chia/wallet/wallet_node.py | 6 +- tests/util/test_chunks.py | 16 +++++ 5 files changed, 84 insertions(+), 58 deletions(-) create mode 100644 chia/util/chunks.py create mode 100644 tests/util/test_chunks.py diff --git a/chia/full_node/coin_store.py b/chia/full_node/coin_store.py index b46a951739e3..8e6420f4849a 100644 --- a/chia/full_node/coin_store.py +++ b/chia/full_node/coin_store.py @@ -7,11 +7,14 @@ from chia.util.db_wrapper import DBWrapper from chia.util.ints import uint32, uint64 from chia.util.lru_cache import LRUCache +from chia.util.chunks import chunks import time import logging log = logging.getLogger(__name__) +MAX_SQLITE_PARAMETERS = 900 + class CoinStore: """ @@ -311,24 +314,25 @@ async def get_coin_states_by_puzzle_hashes( return [] coins = set() - puzzle_hashes_db: Tuple[Any, ...] - if self.db_wrapper.db_version == 2: - puzzle_hashes_db = tuple(puzzle_hashes) - else: - puzzle_hashes_db = tuple([ph.hex() for ph in puzzle_hashes]) - async with self.coin_record_db.execute( - f"SELECT confirmed_index, spent_index, coinbase, puzzle_hash, " - f"coin_parent, amount, timestamp FROM coin_record INDEXED BY coin_puzzle_hash " - f'WHERE puzzle_hash in ({"?," * (len(puzzle_hashes) - 1)}?) ' - f"AND (confirmed_index>=? OR spent_index>=?)" - f"{'' if include_spent_coins else 'AND spent_index=0'}", - puzzle_hashes_db + (min_height, min_height), - ) as cursor: + for puzzles in chunks(puzzle_hashes, MAX_SQLITE_PARAMETERS): + puzzle_hashes_db: Tuple[Any, ...] + if self.db_wrapper.db_version == 2: + puzzle_hashes_db = tuple(puzzles) + else: + puzzle_hashes_db = tuple([ph.hex() for ph in puzzles]) + async with self.coin_record_db.execute( + f"SELECT confirmed_index, spent_index, coinbase, puzzle_hash, " + f"coin_parent, amount, timestamp FROM coin_record INDEXED BY coin_puzzle_hash " + f'WHERE puzzle_hash in ({"?," * (len(puzzles) - 1)}?) ' + f"AND (confirmed_index>=? OR spent_index>=?)" + f"{'' if include_spent_coins else 'AND spent_index=0'}", + puzzle_hashes_db + (min_height, min_height), + ) as cursor: + + async for row in cursor: + coins.add(self.row_to_coin_state(row)) - for row in await cursor.fetchall(): - coins.add(self.row_to_coin_state(row)) - - return list(coins) + return list(coins) async def get_coin_records_by_parent_ids( self, @@ -341,23 +345,24 @@ async def get_coin_records_by_parent_ids( return [] coins = set() - parent_ids_db: Tuple[Any, ...] - if self.db_wrapper.db_version == 2: - parent_ids_db = tuple(parent_ids) - else: - parent_ids_db = tuple([pid.hex() for pid in parent_ids]) - async with self.coin_record_db.execute( - f"SELECT confirmed_index, spent_index, coinbase, puzzle_hash, " - f'coin_parent, amount, timestamp FROM coin_record WHERE coin_parent in ({"?," * (len(parent_ids) - 1)}?) ' - f"AND confirmed_index>=? AND confirmed_index=? AND confirmed_index=? OR spent_index>=?)" - f"{'' if include_spent_coins else 'AND spent_index=0'}", - coin_ids_db + (min_height, min_height), - ) as cursor: - - for row in await cursor.fetchall(): - coins.add(self.row_to_coin_state(row)) - return list(coins) + for ids in chunks(coin_ids, MAX_SQLITE_PARAMETERS): + coin_ids_db: Tuple[Any, ...] + if self.db_wrapper.db_version == 2: + coin_ids_db = tuple(ids) + else: + coin_ids_db = tuple([pid.hex() for pid in ids]) + async with self.coin_record_db.execute( + f"SELECT confirmed_index, spent_index, coinbase, puzzle_hash, " + f'coin_parent, amount, timestamp FROM coin_record WHERE coin_name in ({"?," * (len(ids) - 1)}?) ' + f"AND (confirmed_index>=? OR spent_index>=?)" + f"{'' if include_spent_coins else 'AND spent_index=0'}", + coin_ids_db + (min_height, min_height), + ) as cursor: + async for row in cursor: + coins.add(self.row_to_coin_state(row)) + return list(coins) async def rollback_to_block(self, block_index: int) -> List[CoinRecord]: """ diff --git a/chia/full_node/weight_proof.py b/chia/full_node/weight_proof.py index 64a3c36c051f..dfaabde55fcd 100644 --- a/chia/full_node/weight_proof.py +++ b/chia/full_node/weight_proof.py @@ -20,6 +20,7 @@ calculate_sp_iters, is_overflow_block, ) +from chia.util.chunks import chunks from chia.consensus.vdf_info_computation import get_signage_point_vdf_info from chia.types.blockchain_format.classgroup import ClassgroupElement from chia.types.blockchain_format.sized_bytes import bytes32 @@ -873,11 +874,6 @@ def handle_end_of_slot( ) -def chunks(some_list, chunk_size): - chunk_size = max(1, chunk_size) - return (some_list[i : i + chunk_size] for i in range(0, len(some_list), chunk_size)) - - def compress_segments(full_segment_index, segments: List[SubEpochChallengeSegment]) -> List[SubEpochChallengeSegment]: compressed_segments = [] compressed_segments.append(segments[0]) diff --git a/chia/util/chunks.py b/chia/util/chunks.py new file mode 100644 index 000000000000..b35b784cb6b1 --- /dev/null +++ b/chia/util/chunks.py @@ -0,0 +1,9 @@ +from typing import Iterator, List, TypeVar + +T = TypeVar("T") + + +def chunks(in_list: List[T], size: int) -> Iterator[List[T]]: + size = max(1, size) + for i in range(0, len(in_list), size): + yield in_list[i : i + size] diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index 7f3d09693ab4..43bdbde66591 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -6,7 +6,7 @@ import traceback from asyncio import CancelledError from pathlib import Path -from typing import Callable, Dict, List, Optional, Set, Tuple, Any +from typing import Callable, Dict, List, Optional, Set, Tuple, Any, Iterator from blspy import PrivateKey, AugSchemeMPL from packaging.version import Version @@ -21,7 +21,7 @@ KeychainProxy, KeyringIsEmpty, ) -from chia.full_node.weight_proof import chunks +from chia.util.chunks import chunks from chia.protocols import wallet_protocol from chia.protocols.full_node_protocol import RequestProofOfWeight, RespondProofOfWeight from chia.protocols.protocol_message_types import ProtocolMessageTypes @@ -513,7 +513,7 @@ def is_new_state_update(cs: CoinState) -> bool: all_puzzle_hashes: List[bytes32] = await self.get_puzzle_hashes_to_subscribe() while continue_while: # Get all phs from puzzle store - ph_chunks: List[List[bytes32]] = chunks(all_puzzle_hashes, 1000) + ph_chunks: Iterator[List[bytes32]] = chunks(all_puzzle_hashes, 1000) for chunk in ph_chunks: ph_update_res: List[CoinState] = await subscribe_to_phs( [p for p in chunk if p not in already_checked_ph], full_node, 0 diff --git a/tests/util/test_chunks.py b/tests/util/test_chunks.py new file mode 100644 index 000000000000..2c4383ab3a6f --- /dev/null +++ b/tests/util/test_chunks.py @@ -0,0 +1,16 @@ +from chia.util.chunks import chunks + + +def test_chunks() -> None: + + assert list(chunks([], 0)) == [] + assert list(chunks(["a"], 0)) == [["a"]] + assert list(chunks(["a", "b"], 0)) == [["a"], ["b"]] + + assert list(chunks(["a", "b", "c", "d"], -1)) == [["a"], ["b"], ["c"], ["d"]] + assert list(chunks(["a", "b", "c", "d"], 0)) == [["a"], ["b"], ["c"], ["d"]] + assert list(chunks(["a", "b", "c", "d"], 1)) == [["a"], ["b"], ["c"], ["d"]] + assert list(chunks(["a", "b", "c", "d"], 2)) == [["a", "b"], ["c", "d"]] + assert list(chunks(["a", "b", "c", "d"], 3)) == [["a", "b", "c"], ["d"]] + assert list(chunks(["a", "b", "c", "d"], 4)) == [["a", "b", "c", "d"]] + assert list(chunks(["a", "b", "c", "d"], 200)) == [["a", "b", "c", "d"]] From 6b8b4e41bf6bf2a197c58b4e319910396c90e9f8 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Mon, 21 Feb 2022 23:08:54 -0500 Subject: [PATCH 103/378] rework _fetch_header_blocks_inner() (#10326) * rework _fetch_header_blocks_inner() * flake8 * drop selected_peer_node_id * black * disconnect peer for None response --- chia/wallet/util/wallet_sync_utils.py | 54 ++++++++++----------------- chia/wallet/wallet_node.py | 2 +- 2 files changed, 21 insertions(+), 35 deletions(-) diff --git a/chia/wallet/util/wallet_sync_utils.py b/chia/wallet/util/wallet_sync_utils.py index 1a6871f1bdd3..e7d2d64d091c 100644 --- a/chia/wallet/util/wallet_sync_utils.py +++ b/chia/wallet/util/wallet_sync_utils.py @@ -18,7 +18,6 @@ RespondToCoinUpdates, RespondHeaderBlocks, RequestHeaderBlocks, - RejectHeaderBlocks, ) from chia.server.ws_connection import WSChiaConnection from chia.types.blockchain_format.coin import hash_coin_list, Coin @@ -286,36 +285,26 @@ def last_change_height_cs(cs: CoinState) -> uint32: async def _fetch_header_blocks_inner( - all_peers: List[WSChiaConnection], selected_peer_node_id: bytes32, request: RequestHeaderBlocks + all_peers: List[WSChiaConnection], + request: RequestHeaderBlocks, ) -> Optional[RespondHeaderBlocks]: - if len(all_peers) == 0: - return None - random_peer: WSChiaConnection = random.choice(all_peers) - res = await random_peer.request_header_blocks(request) - if isinstance(res, RespondHeaderBlocks): - return res - elif isinstance(res, RejectHeaderBlocks): - # Peer is not synced, close connection - await random_peer.close() - - bad_peer_id = random_peer.peer_node_id - if len(all_peers) == 1: - # No more peers to fetch from - return None - else: - if selected_peer_node_id == bad_peer_id: - # Select another peer fallback - while random_peer.peer_node_id == bad_peer_id and len(all_peers) > 1: - random_peer = random.choice(all_peers) - else: - # Use the selected peer instead - random_peer = [p for p in all_peers if p.peer_node_id == selected_peer_node_id][0] - # Retry - res = await random_peer.request_header_blocks(request) - if isinstance(res, RespondHeaderBlocks): - return res - else: - return None + # We will modify this list, don't modify passed parameters. + remaining_peers = list(all_peers) + + while len(remaining_peers) > 0: + peer = random.choice(remaining_peers) + + response = await peer.request_header_blocks(request) + + if isinstance(response, RespondHeaderBlocks): + return response + + # Request to peer failed in some way, close the connection and remove the peer + # from our local list. + await peer.close() + remaining_peers.remove(peer) + + return None async def fetch_header_blocks_in_range( @@ -323,7 +312,6 @@ async def fetch_header_blocks_in_range( end: uint32, peer_request_cache: PeerRequestCache, all_peers: List[WSChiaConnection], - selected_peer_id: bytes32, ) -> Optional[List[HeaderBlock]]: blocks: List[HeaderBlock] = [] for i in range(start - (start % 32), end + 1, 32): @@ -340,9 +328,7 @@ async def fetch_header_blocks_in_range( else: log.info(f"Fetching: {start}-{end}") request_header_blocks = RequestHeaderBlocks(request_start, request_end) - res_h_blocks_task = asyncio.create_task( - _fetch_header_blocks_inner(all_peers, selected_peer_id, request_header_blocks) - ) + res_h_blocks_task = asyncio.create_task(_fetch_header_blocks_inner(all_peers, request_header_blocks)) peer_request_cache.add_to_block_requests(request_start, request_end, res_h_blocks_task) res_h_blocks = await res_h_blocks_task if res_h_blocks is None: diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index 43bdbde66591..0a48a1d67b4f 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -1231,7 +1231,7 @@ async def validate_block_inclusion( all_peers = self.server.get_full_node_connections() blocks: Optional[List[HeaderBlock]] = await fetch_header_blocks_in_range( - start, end, peer_request_cache, all_peers, peer.peer_node_id + start, end, peer_request_cache, all_peers ) if blocks is None: self.log.error(f"Error fetching blocks {start} {end}") From 7232b1cd2df42210c1c8b2a03b73cdec9df6c2f3 Mon Sep 17 00:00:00 2001 From: Chris Marslender Date: Tue, 22 Feb 2022 09:55:26 -0600 Subject: [PATCH 104/378] Pin back to windows-2019 for now, since windows-latest is updated to 2022 (#10372) --- .github/workflows/build-windows-installer.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-windows-installer.yml b/.github/workflows/build-windows-installer.yml index e5eb2bcb6de3..3983e6247421 100644 --- a/.github/workflows/build-windows-installer.yml +++ b/.github/workflows/build-windows-installer.yml @@ -18,7 +18,7 @@ concurrency: jobs: build: name: Windows Installer on Windows 10 and Python 3.9 - runs-on: [windows-latest] + runs-on: [windows-2019] timeout-minutes: 40 steps: From 4ac563e0d0a3282c0e76562acd597ca073f25fd1 Mon Sep 17 00:00:00 2001 From: Chris Marslender Date: Tue, 22 Feb 2022 11:38:59 -0600 Subject: [PATCH 105/378] Add `to_address` param to get_transactions RPC (#10319) * Add `address` param to get_transactions to enable filtering transactions by the receiving address * move address where to the same line as the other where, so query segments are logically grouped * Update param to `to_address` and use timeout assert in the test --- chia/rpc/wallet_rpc_api.py | 7 ++++++- chia/wallet/wallet_transaction_store.py | 11 +++++++++-- tests/wallet/rpc/test_wallet_rpc.py | 13 ++++++++++++- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index d6f601471be5..e3257fb23dbc 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -680,8 +680,13 @@ async def get_transactions(self, request: Dict) -> Dict: sort_key = request.get("sort_key", None) reverse = request.get("reverse", False) + to_address = request.get("to_address", None) + to_puzzle_hash: Optional[bytes32] = None + if to_address is not None: + to_puzzle_hash = decode_puzzle_hash(to_address) + transactions = await self.service.wallet_state_manager.tx_store.get_transactions_between( - wallet_id, start, end, sort_key=sort_key, reverse=reverse + wallet_id, start, end, sort_key=sort_key, reverse=reverse, to_puzzle_hash=to_puzzle_hash ) return { "transactions": [ diff --git a/chia/wallet/wallet_transaction_store.py b/chia/wallet/wallet_transaction_store.py index d6caf776fb3e..4c44e26c26fe 100644 --- a/chia/wallet/wallet_transaction_store.py +++ b/chia/wallet/wallet_transaction_store.py @@ -349,13 +349,18 @@ async def get_unconfirmed_for_wallet(self, wallet_id: int) -> List[TransactionRe return [] async def get_transactions_between( - self, wallet_id: int, start, end, sort_key=None, reverse=False + self, wallet_id: int, start, end, sort_key=None, reverse=False, to_puzzle_hash: Optional[bytes32] = None ) -> List[TransactionRecord]: """Return a list of transaction between start and end index. List is in reverse chronological order. start = 0 is most recent transaction """ limit = end - start + if to_puzzle_hash is None: + puzz_hash_where = "" + else: + puzz_hash_where = f' and to_puzzle_hash="{to_puzzle_hash.hex()}"' + if sort_key is None: sort_key = "CONFIRMED_AT_HEIGHT" if sort_key not in SortKey.__members__: @@ -367,7 +372,9 @@ async def get_transactions_between( query_str = SortKey[sort_key].ascending() cursor = await self.db_connection.execute( - f"SELECT * from transaction_record where wallet_id=?" f" {query_str}, rowid" f" LIMIT {start}, {limit}", + f"SELECT * from transaction_record where wallet_id=?{puzz_hash_where}" + f" {query_str}, rowid" + f" LIMIT {start}, {limit}", (wallet_id,), ) rows = await cursor.fetchall() diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index c83ac2c9feb5..83567f68e5f0 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -23,7 +23,7 @@ from chia.types.announcement import Announcement from chia.types.blockchain_format.program import Program from chia.types.peer_info import PeerInfo -from chia.util.bech32m import encode_puzzle_hash +from chia.util.bech32m import decode_puzzle_hash, encode_puzzle_hash from chia.consensus.coinbase import create_puzzlehash_for_pk from chia.util.hash import std_hash from chia.wallet.derive_keys import master_sk_to_wallet_sk @@ -382,6 +382,17 @@ async def eventual_balance_det(c, wallet_id: str): assert list(memos.keys())[0] in [a.name() for a in send_tx_res.spend_bundle.additions()] assert list(memos.keys())[1] in [a.name() for a in send_tx_res.spend_bundle.additions()] + # Test get_transactions to address + ph_by_addr = await wallet.get_new_puzzlehash() + await client.send_transaction("1", 1, encode_puzzle_hash(ph_by_addr, "xch")) + await client.farm_block(encode_puzzle_hash(ph_by_addr, "xch")) + await time_out_assert(10, wallet_is_synced, True, wallet_node, full_node_api) + tx_for_address = await wallet_rpc_api.get_transactions( + {"wallet_id": "1", "to_address": encode_puzzle_hash(ph_by_addr, "xch")} + ) + assert len(tx_for_address["transactions"]) == 1 + assert decode_puzzle_hash(tx_for_address["transactions"][0]["to_address"]) == ph_by_addr + ############## # CATS # ############## From aa33d7e94b2f7e05d27895d44e8293b66e9326f9 Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Tue, 22 Feb 2022 18:39:38 +0100 Subject: [PATCH 106/378] use a proper temporary file for the blockchain database in tests, rather than files in the current directory with potentially colliding names (#10369) --- tests/util/blockchain.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/util/blockchain.py b/tests/util/blockchain.py index 3ea7b829738a..f18fcf8c8131 100644 --- a/tests/util/blockchain.py +++ b/tests/util/blockchain.py @@ -4,6 +4,7 @@ from typing import List import aiosqlite +import tempfile from chia.consensus.blockchain import Blockchain from chia.consensus.constants import ConsensusConstants @@ -16,15 +17,12 @@ from tests.setup_nodes import bt -blockchain_db_counter: int = 0 - async def create_blockchain(constants: ConsensusConstants, db_version: int): - global blockchain_db_counter - db_path = Path(f"blockchain_test-{blockchain_db_counter}.db") + db_path = Path(tempfile.NamedTemporaryFile().name) + if db_path.exists(): db_path.unlink() - blockchain_db_counter += 1 connection = await aiosqlite.connect(db_path) wrapper = DBWrapper(connection, db_version) coin_store = await CoinStore.create(wrapper) From 062dfe9feb9ffbc5cca5b891591912bc65f1e4b4 Mon Sep 17 00:00:00 2001 From: arty Date: Tue, 22 Feb 2022 10:24:40 -0800 Subject: [PATCH 107/378] Add info dump about how to use singletons (#9559) * Add info dump about how to use singletons * Add a comment re: matt howard pointing out that the eve spend can also be simulataneous * Fix commentary * Add commentary according to feedback --- chia/wallet/puzzles/singleton_top_layer.py | 137 +++++++++++++++++++++ 1 file changed, 137 insertions(+) diff --git a/chia/wallet/puzzles/singleton_top_layer.py b/chia/wallet/puzzles/singleton_top_layer.py index ab626582093b..420f3a896aba 100644 --- a/chia/wallet/puzzles/singleton_top_layer.py +++ b/chia/wallet/puzzles/singleton_top_layer.py @@ -20,6 +20,143 @@ MELT_CONDITION = [ConditionOpcode.CREATE_COIN, 0, ESCAPE_VALUE] +# +# An explanation of how this functions from a user's perspective +# +# Consider that you have some coin A that you want to create a singleton +# containing some inner puzzle I from with amount T. We'll call the Launcher +# coin, which is created from A "Launcher" and the first iteration of the +# singleton, called the "Eve" spend, Eve. When spent, I yields a coin +# running I' and so on in a singleton specific way described below. +# +# The structure of this on the blockchain when done looks like this +# +# ,------------. +# | Coin A | +# `------------' +# | +# ------------------ Atomic Transaction 1 ----------------- +# v +# .------------. .-------------------------------. +# | Launcher |------>| Eve Coin Containing Program I | +# `------------' `-------------------------------' +# | +# -------------------- End Transaction 1 ------------------\ +# | > The Eve coin +# --------------- (2) Transaction With I ------------------/ may also be +# | spent +# v simultaneously +# .-----------------------------------. +# | Running Singleton With Program I' | +# `-----------------------------------' +# | +# --------------------- End Transaction 2 ------------------ +# | +# --------------- (3) Transaction With I' ------------------ +# ... +# +# +# == Practial use of singleton_top_layer.py == +# +# 1) Designate some coin as coin A +# +# 2) call puzzle_for_singleton with that coin's name (it is the Parent of the +# Launch coin), and the initial inner puzzle I, curried as appropriate for +# its own purpose. Adaptations of the program I and its descendants are +# required as below. +# +# 3) call launch_conditions_and_coinsol to get a set of "launch_conditions", +# which will be used to spend standard coin A, and a "spend", which spends +# the Launcher created by the application of "launch_conditions" to A in a +# spend. These actions must be done in the same spend bundle. +# +# One can create a SpendBundle containing the spend of A giving it the +# argument list (() (q . launch_conditions) ()) and then append "spend" onto +# its .coin_spends to create a combined spend bundle. +# +# 4) submit the combine spend bundle. +# +# 5) Remember the identity of the Launcher coin: +# +# Coin(A.name(), SINGLETON_LAUNCHER_HASH, amount) +# +# A singleton has been created like this: +# +# Coin(Launcher.name(), puzzle_for_singleton(Launcher.name(), I), amount) +# +# +# == To spend the singleton requires some host side setup == +# +# The singleton adds an ASSERT_MY_COIN_ID to constrain it to the coin that +# matches its own conception of itself. It consumes a "LineageProof" object +# when spent that must be constructed so. We'll call the singleton we intend +# to spend "S". +# +# Specifically, the required puzzle is the Inner puzzle I for the parent of S +# unless S is the Eve coin, in which case it is None. +# So to spend S', the second singleton, I is used, and to spend S'', I' is used. +# We'll call this puzzle hash (or None) PH. +# +# If this is the Eve singleton: +# +# PH = None +# L = LineageProof(Launcher, PH, amount) +# +# - Note: the Eve singleton's .parent_coin_info should match Launcher here. +# +# Otherwise +# +# PH = ParentOf(S).inner_puzzle_hash +# L = LineageProof(ParentOf(S).name(), PH, amount) +# +# - Note: ParentOf(S).name is the .parent_coin_info member of the +# coin record for S. +# +# Now the coin S can be spent. +# The puzzle to use in the spend is given by +# +# puzzle_for_singleton(S.name(), I'.puzzle_hash()) +# +# and the arguments are given by (with the argument list to I designated AI) +# +# solution_for_singleton(L, amount, AI) +# +# Note that AI contains dynamic arguments to puzzle I _after_ the singleton +# truths. +# +# +# Adapting puzzles to the singleton +# +# 1) For the puzzle to create a coin from inside the singleton it will need the +# following values to be added to its curried in arguments: +# +# - A way to compute its own puzzle has for each of I' and so on. This can +# be accomplished by giving it its uncurried puzzle hash and using +# puzzle-hash-of-curried-function to compute it. Although full_puzzle_hash +# is used for some arguments, the inputs to all singleton_top_layer +# functions is the inner puzzle. +# +# - the name() of the Launcher coin (which you can compute from a Coin +# object) if you're not already using it in I puzzle for some other +# reason. +# +# 2) A non-curried argument called "singleton_truths" will be passed to your +# program. It is not required to use anything inside. +# +# There is little value in not receiving this argument via the adaptations +# below as a standard puzzle can't be used anyway. To work the result must +# be itself a singleton, and the singleton does not change the puzzle hash +# in an outgoing CREATE_COIN to cause it to be one. +# +# With this modification of the program I done, I and descendants will +# continue to produce I', I'' etc. +# +# The actual CREATE_COIN puzzle hash will be the result of +# this. The Launcher ID referred to here is the name() of +# the Launcher coin as above. +# + + # Given the parent and amount of the launcher coin, return the launcher coin def generate_launcher_coin(coin: Coin, amount: uint64) -> Coin: return Coin(coin.name(), SINGLETON_LAUNCHER_HASH, amount) From b895de80ebc80a2bfed80344af9f8d5f8dbcfd15 Mon Sep 17 00:00:00 2001 From: Chris Marslender Date: Tue, 22 Feb 2022 12:59:34 -0600 Subject: [PATCH 108/378] Update to download.chia.net bucket and use cached url for webseed (#10365) --- .github/workflows/build-linux-arm64-installer.yml | 14 +++++++------- .github/workflows/build-linux-installer-deb.yml | 14 +++++++------- .github/workflows/build-linux-installer-rpm.yml | 14 +++++++------- .github/workflows/build-macos-installer.yml | 14 +++++++------- .github/workflows/build-macos-m1-installer.yml | 14 +++++++------- .github/workflows/build-windows-installer.yml | 14 +++++++------- 6 files changed, 42 insertions(+), 42 deletions(-) diff --git a/.github/workflows/build-linux-arm64-installer.yml b/.github/workflows/build-linux-arm64-installer.yml index fd69f5a1dd48..5254306a4a96 100644 --- a/.github/workflows/build-linux-arm64-installer.yml +++ b/.github/workflows/build-linux-arm64-installer.yml @@ -140,7 +140,7 @@ jobs: GIT_SHORT_HASH=$(echo "${GITHUB_SHA}" | cut -c1-8) CHIA_DEV_BUILD=${CHIA_INSTALLER_VERSION}-$GIT_SHORT_HASH echo "CHIA_DEV_BUILD=$CHIA_DEV_BUILD" >>$GITHUB_ENV - aws s3 cp "$GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_arm64.deb" "s3://download-chia-net/dev/chia-blockchain_${CHIA_DEV_BUILD}_arm64.deb" + aws s3 cp "$GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_arm64.deb" "s3://download.chia.net/dev/chia-blockchain_${CHIA_DEV_BUILD}_arm64.deb" - name: Create Checksums if: startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main' @@ -161,7 +161,7 @@ jobs: env: CHIA_INSTALLER_VERSION: ${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }} run: | - py3createtorrent -f -t udp://tracker.opentrackr.org:1337/announce $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_arm64.deb -o $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_arm64.deb.torrent --webseed https://download-chia-net.s3.us-west-2.amazonaws.com/install/chia-blockchain_${CHIA_INSTALLER_VERSION}_arm64.deb + py3createtorrent -f -t udp://tracker.opentrackr.org:1337/announce $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_arm64.deb -o $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_arm64.deb.torrent --webseed https://download.chia.net/install/chia-blockchain_${CHIA_INSTALLER_VERSION}_arm64.deb ls $GITHUB_WORKSPACE/build_scripts/final_installer/ - name: Upload Beta Installer @@ -169,8 +169,8 @@ jobs: env: CHIA_INSTALLER_VERSION: ${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }} run: | - aws s3 cp $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_arm64.deb s3://download-chia-net/beta/chia-blockchain_arm64_latest_beta.deb - aws s3 cp $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_arm64.deb.sha256 s3://download-chia-net/beta/chia-blockchain_arm64_latest_beta.deb.sha256 + aws s3 cp $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_arm64.deb s3://download.chia.net/beta/chia-blockchain_arm64_latest_beta.deb + aws s3 cp $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_arm64.deb.sha256 s3://download.chia.net/beta/chia-blockchain_arm64_latest_beta.deb.sha256 - name: Upload Release Files if: steps.check_secrets.outputs.HAS_SECRET && startsWith(github.ref, 'refs/tags/') @@ -178,9 +178,9 @@ jobs: CHIA_INSTALLER_VERSION: ${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }} run: | ls $GITHUB_WORKSPACE/build_scripts/final_installer/ - aws s3 cp $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_arm64.deb s3://download-chia-net/install/ - aws s3 cp $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_arm64.deb.sha256 s3://download-chia-net/install/ - aws s3 cp $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_arm64.deb.torrent s3://download-chia-net/torrents/ + aws s3 cp $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_arm64.deb s3://download.chia.net/install/ + aws s3 cp $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_arm64.deb.sha256 s3://download.chia.net/install/ + aws s3 cp $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_arm64.deb.torrent s3://download.chia.net/torrents/ - name: Get tag name if: startsWith(github.ref, 'refs/tags/') diff --git a/.github/workflows/build-linux-installer-deb.yml b/.github/workflows/build-linux-installer-deb.yml index 76d020114c18..e3929e741157 100644 --- a/.github/workflows/build-linux-installer-deb.yml +++ b/.github/workflows/build-linux-installer-deb.yml @@ -181,7 +181,7 @@ jobs: CHIA_DEV_BUILD=${CHIA_INSTALLER_VERSION}-$GIT_SHORT_HASH echo "CHIA_DEV_BUILD=$CHIA_DEV_BUILD" >>$GITHUB_ENV ls ${{ github.workspace }}/build_scripts/final_installer/ - aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_amd64.deb s3://download-chia-net/dev/chia-blockchain_${CHIA_DEV_BUILD}_amd64.deb + aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_amd64.deb s3://download.chia.net/dev/chia-blockchain_${CHIA_DEV_BUILD}_amd64.deb - name: Create Checksums if: startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main' @@ -202,7 +202,7 @@ jobs: CHIA_INSTALLER_VERSION: ${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }} if: startsWith(github.ref, 'refs/tags/') run: | - py3createtorrent -f -t udp://tracker.opentrackr.org:1337/announce ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_amd64.deb -o ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_amd64.deb.torrent --webseed https://download-chia-net.s3.us-west-2.amazonaws.com/install/chia-blockchain_${CHIA_INSTALLER_VERSION}_amd64.deb + py3createtorrent -f -t udp://tracker.opentrackr.org:1337/announce ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_amd64.deb -o ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_amd64.deb.torrent --webseed https://download.chia.net/install/chia-blockchain_${CHIA_INSTALLER_VERSION}_amd64.deb ls - name: Upload Beta Installer @@ -210,17 +210,17 @@ jobs: env: CHIA_INSTALLER_VERSION: ${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }} run: | - aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_amd64.deb s3://download-chia-net/beta/chia-blockchain_amd64_latest_beta.deb - aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_amd64.deb.sha256 s3://download-chia-net/beta/chia-blockchain_amd64_latest_beta.deb.sha256 + aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_amd64.deb s3://download.chia.net/beta/chia-blockchain_amd64_latest_beta.deb + aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_amd64.deb.sha256 s3://download.chia.net/beta/chia-blockchain_amd64_latest_beta.deb.sha256 - name: Upload Release Files env: CHIA_INSTALLER_VERSION: ${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }} if: steps.check_secrets.outputs.HAS_SECRET && startsWith(github.ref, 'refs/tags/') run: | - aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_amd64.deb s3://download-chia-net/install/ - aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_amd64.deb.sha256 s3://download-chia-net/install/ - aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_amd64.deb.torrent s3://download-chia-net/torrents/ + aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_amd64.deb s3://download.chia.net/install/ + aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_amd64.deb.sha256 s3://download.chia.net/install/ + aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_amd64.deb.torrent s3://download.chia.net/torrents/ - name: Get tag name if: startsWith(github.ref, 'refs/tags/') diff --git a/.github/workflows/build-linux-installer-rpm.yml b/.github/workflows/build-linux-installer-rpm.yml index 2eaa7e36be89..96e580524e27 100644 --- a/.github/workflows/build-linux-installer-rpm.yml +++ b/.github/workflows/build-linux-installer-rpm.yml @@ -141,7 +141,7 @@ jobs: CHIA_DEV_BUILD=${CHIA_INSTALLER_VERSION}-$GIT_SHORT_HASH echo "CHIA_DEV_BUILD=$CHIA_DEV_BUILD" >>$GITHUB_ENV ls $GITHUB_WORKSPACE/build_scripts/final_installer/ - aws s3 cp $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain-${CHIA_INSTALLER_VERSION}-1.x86_64.rpm s3://download-chia-net/dev/chia-blockchain-${CHIA_DEV_BUILD}-1.x86_64.rpm + aws s3 cp $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain-${CHIA_INSTALLER_VERSION}-1.x86_64.rpm s3://download.chia.net/dev/chia-blockchain-${CHIA_DEV_BUILD}-1.x86_64.rpm - name: Create Checksums if: startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main' @@ -162,7 +162,7 @@ jobs: env: CHIA_INSTALLER_VERSION: ${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }} run: | - py3createtorrent -f -t udp://tracker.opentrackr.org:1337/announce $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain-${CHIA_INSTALLER_VERSION}-1.x86_64.rpm -o $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain-${CHIA_INSTALLER_VERSION}-1.x86_64.rpm.torrent --webseed https://download-chia-net.s3.us-west-2.amazonaws.com/install/chia-blockchain-${CHIA_INSTALLER_VERSION}-1.x86_64.rpm + py3createtorrent -f -t udp://tracker.opentrackr.org:1337/announce $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain-${CHIA_INSTALLER_VERSION}-1.x86_64.rpm -o $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain-${CHIA_INSTALLER_VERSION}-1.x86_64.rpm.torrent --webseed https://download.chia.net/install/chia-blockchain-${CHIA_INSTALLER_VERSION}-1.x86_64.rpm ls - name: Upload Beta Installer @@ -170,17 +170,17 @@ jobs: env: CHIA_INSTALLER_VERSION: ${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }} run: | - aws s3 cp $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain-${CHIA_INSTALLER_VERSION}-1.x86_64.rpm s3://download-chia-net/beta/chia-blockchain-1.x86_64_latest_beta.rpm - aws s3 cp $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain-${CHIA_INSTALLER_VERSION}-1.x86_64.rpm.sha256 s3://download-chia-net/beta/chia-blockchain-1.x86_64_latest_beta.rpm.sha256 + aws s3 cp $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain-${CHIA_INSTALLER_VERSION}-1.x86_64.rpm s3://download.chia.net/beta/chia-blockchain-1.x86_64_latest_beta.rpm + aws s3 cp $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain-${CHIA_INSTALLER_VERSION}-1.x86_64.rpm.sha256 s3://download.chia.net/beta/chia-blockchain-1.x86_64_latest_beta.rpm.sha256 - name: Upload Release Files if: steps.check_secrets.outputs.HAS_SECRET && startsWith(github.ref, 'refs/tags/') env: CHIA_INSTALLER_VERSION: ${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }} run: | - aws s3 cp $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain-${CHIA_INSTALLER_VERSION}-1.x86_64.rpm s3://download-chia-net/install/ - aws s3 cp $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain-${CHIA_INSTALLER_VERSION}-1.x86_64.rpm.sha256 s3://download-chia-net/install/ - aws s3 cp $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain-${CHIA_INSTALLER_VERSION}-1.x86_64.rpm.torrent s3://download-chia-net/torrents/ + aws s3 cp $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain-${CHIA_INSTALLER_VERSION}-1.x86_64.rpm s3://download.chia.net/install/ + aws s3 cp $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain-${CHIA_INSTALLER_VERSION}-1.x86_64.rpm.sha256 s3://download.chia.net/install/ + aws s3 cp $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain-${CHIA_INSTALLER_VERSION}-1.x86_64.rpm.torrent s3://download.chia.net/torrents/ - name: Get tag name if: startsWith(github.ref, 'refs/tags/') diff --git a/.github/workflows/build-macos-installer.yml b/.github/workflows/build-macos-installer.yml index aa01c7f58b60..4025a4424063 100644 --- a/.github/workflows/build-macos-installer.yml +++ b/.github/workflows/build-macos-installer.yml @@ -170,7 +170,7 @@ jobs: GIT_SHORT_HASH=$(echo "${GITHUB_SHA}" | cut -c1-8) CHIA_DEV_BUILD=${CHIA_INSTALLER_VERSION}-$GIT_SHORT_HASH echo "CHIA_DEV_BUILD=$CHIA_DEV_BUILD" >>$GITHUB_ENV - aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg s3://download-chia-net/builds/Chia-${CHIA_DEV_BUILD}.dmg + aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg s3://download.chia.net/builds/Chia-${CHIA_DEV_BUILD}.dmg - name: Install py3createtorrent if: startsWith(github.ref, 'refs/tags/') @@ -180,7 +180,7 @@ jobs: - name: Create torrent if: startsWith(github.ref, 'refs/tags/') run: | - py3createtorrent -f -t udp://tracker.opentrackr.org:1337/announce ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg -o ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg.torrent --webseed https://download-chia-net.s3.us-west-2.amazonaws.com/install/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg + py3createtorrent -f -t udp://tracker.opentrackr.org:1337/announce ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg -o ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg.torrent --webseed https://download.chia.net/install/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg ls ${{ github.workspace }}/build_scripts/final_installer/ - name: Upload Beta Installer @@ -191,8 +191,8 @@ jobs: AWS_SECRET_ACCESS_KEY: ${{ secrets.INSTALLER_UPLOAD_SECRET }} AWS_REGION: us-west-2 run: | - aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg s3://download-chia-net/beta/Chia_latest_beta.dmg - aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg.sha256 s3://download-chia-net/beta/Chia_latest_beta.dmg.sha256 + aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg s3://download.chia.net/beta/Chia_latest_beta.dmg + aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg.sha256 s3://download.chia.net/beta/Chia_latest_beta.dmg.sha256 - name: Upload Release Files if: steps.check_secrets.outputs.HAS_AWS_SECRET && startsWith(github.ref, 'refs/tags/') @@ -201,9 +201,9 @@ jobs: AWS_SECRET_ACCESS_KEY: ${{ secrets.INSTALLER_UPLOAD_SECRET }} AWS_REGION: us-west-2 run: | - aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg s3://download-chia-net/install/ - aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg.sha256 s3://download-chia-net/install/ - aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg.torrent s3://download-chia-net/torrents/ + aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg s3://download.chia.net/install/ + aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg.sha256 s3://download.chia.net/install/ + aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg.torrent s3://download.chia.net/torrents/ - name: Get tag name if: startsWith(github.ref, 'refs/tags/') diff --git a/.github/workflows/build-macos-m1-installer.yml b/.github/workflows/build-macos-m1-installer.yml index 3f09e7de9960..889e464e8ea3 100644 --- a/.github/workflows/build-macos-m1-installer.yml +++ b/.github/workflows/build-macos-m1-installer.yml @@ -149,7 +149,7 @@ jobs: GIT_SHORT_HASH=$(echo "${GITHUB_SHA}" | cut -c1-8) CHIA_DEV_BUILD=${CHIA_INSTALLER_VERSION}-$GIT_SHORT_HASH echo "CHIA_DEV_BUILD=$CHIA_DEV_BUILD" >>$GITHUB_ENV - arch -arm64 aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/Chia-${CHIA_INSTALLER_VERSION}-arm64.dmg s3://download-chia-net/dev/Chia-${CHIA_DEV_BUILD}-arm64.dmg + arch -arm64 aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/Chia-${CHIA_INSTALLER_VERSION}-arm64.dmg s3://download.chia.net/dev/Chia-${CHIA_DEV_BUILD}-arm64.dmg - name: Install py3createtorrent if: startsWith(github.ref, 'refs/tags/') @@ -159,7 +159,7 @@ jobs: - name: Create torrent if: startsWith(github.ref, 'refs/tags/') run: | - arch -arm64 py3createtorrent -f -t udp://tracker.opentrackr.org:1337/announce ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}-arm64.dmg -o ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}-arm64.dmg.torrent --webseed https://download-chia-net.s3.us-west-2.amazonaws.com/install/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}-arm64.dmg + arch -arm64 py3createtorrent -f -t udp://tracker.opentrackr.org:1337/announce ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}-arm64.dmg -o ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}-arm64.dmg.torrent --webseed https://download.chia.net/install/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}-arm64.dmg ls ${{ github.workspace }}/build_scripts/final_installer/ - name: Upload Beta Installer @@ -170,8 +170,8 @@ jobs: AWS_SECRET_ACCESS_KEY: ${{ secrets.INSTALLER_UPLOAD_SECRET }} AWS_REGION: us-west-2 run: | - arch -arm64 aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}-arm64.dmg s3://download-chia-net/beta/Chia-arm64_latest_beta.dmg - arch -arm64 aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}-arm64.dmg.sha256 s3://download-chia-net/beta/Chia-arm64_latest_beta.dmg.sha256 + arch -arm64 aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}-arm64.dmg s3://download.chia.net/beta/Chia-arm64_latest_beta.dmg + arch -arm64 aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}-arm64.dmg.sha256 s3://download.chia.net/beta/Chia-arm64_latest_beta.dmg.sha256 - name: Upload Release Files if: steps.check_secrets.outputs.HAS_AWS_SECRET && startsWith(github.ref, 'refs/tags/') @@ -180,9 +180,9 @@ jobs: AWS_SECRET_ACCESS_KEY: ${{ secrets.INSTALLER_UPLOAD_SECRET }} AWS_REGION: us-west-2 run: | - arch -arm64 aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}-arm64.dmg s3://download-chia-net/install/ - arch -arm64 aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}-arm64.dmg.sha256 s3://download-chia-net/install/ - arch -arm64 aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}-arm64.dmg.torrent s3://download-chia-net/torrents/ + arch -arm64 aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}-arm64.dmg s3://download.chia.net/install/ + arch -arm64 aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}-arm64.dmg.sha256 s3://download.chia.net/install/ + arch -arm64 aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}-arm64.dmg.torrent s3://download.chia.net/torrents/ - name: Get tag name if: startsWith(github.ref, 'refs/tags/') diff --git a/.github/workflows/build-windows-installer.yml b/.github/workflows/build-windows-installer.yml index 3983e6247421..65bb4eeeb266 100644 --- a/.github/workflows/build-windows-installer.yml +++ b/.github/workflows/build-windows-installer.yml @@ -188,7 +188,7 @@ jobs: echo ::set-output name=CHIA_DEV_BUILD::${CHIA_DEV_BUILD} echo ${CHIA_DEV_BUILD} pwd - aws s3 cp chia-blockchain-gui/release-builds/windows-installer/ChiaSetup-${CHIA_INSTALLER_VERSION}.exe s3://download-chia-net/dev/ChiaSetup-${CHIA_DEV_BUILD}.exe + aws s3 cp chia-blockchain-gui/release-builds/windows-installer/ChiaSetup-${CHIA_INSTALLER_VERSION}.exe s3://download.chia.net/dev/ChiaSetup-${CHIA_DEV_BUILD}.exe - name: Create Checksums env: @@ -205,7 +205,7 @@ jobs: - name: Create torrent if: startsWith(github.ref, 'refs/tags/') run: | - py3createtorrent -f -t udp://tracker.opentrackr.org:1337/announce ${{ github.workspace }}\chia-blockchain-gui\release-builds\windows-installer\ChiaSetup-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.exe -o ${{ github.workspace }}\chia-blockchain-gui\release-builds\windows-installer\ChiaSetup-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.exe.torrent --webseed https://download-chia-net.s3.us-west-2.amazonaws.com/install/ChiaSetup-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.exe + py3createtorrent -f -t udp://tracker.opentrackr.org:1337/announce ${{ github.workspace }}\chia-blockchain-gui\release-builds\windows-installer\ChiaSetup-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.exe -o ${{ github.workspace }}\chia-blockchain-gui\release-builds\windows-installer\ChiaSetup-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.exe.torrent --webseed https://download.chia.net/install/ChiaSetup-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.exe ls - name: Upload Beta Installer @@ -213,8 +213,8 @@ jobs: env: CHIA_INSTALLER_VERSION: ${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }} run: | - aws s3 cp ${{ github.workspace }}\chia-blockchain-gui\release-builds\windows-installer\ChiaSetup-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.exe s3://download-chia-net/beta/ChiaSetup-latest-beta.exe - aws s3 cp ${{ github.workspace }}\chia-blockchain-gui\release-builds\windows-installer\ChiaSetup-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.exe.sha256 s3://download-chia-net/beta/ChiaSetup-latest-beta.exe.sha256 + aws s3 cp ${{ github.workspace }}\chia-blockchain-gui\release-builds\windows-installer\ChiaSetup-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.exe s3://download.chia.net/beta/ChiaSetup-latest-beta.exe + aws s3 cp ${{ github.workspace }}\chia-blockchain-gui\release-builds\windows-installer\ChiaSetup-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.exe.sha256 s3://download.chia.net/beta/ChiaSetup-latest-beta.exe.sha256 - name: Upload Release Files if: steps.check_secrets.outputs.HAS_AWS_SECRET && startsWith(github.ref, 'refs/tags/') @@ -222,9 +222,9 @@ jobs: AWS_ACCESS_KEY_ID: ${{ secrets.INSTALLER_UPLOAD_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.INSTALLER_UPLOAD_SECRET }} run: | - aws s3 cp ${{ github.workspace }}\chia-blockchain-gui\release-builds\windows-installer\ChiaSetup-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.exe s3://download-chia-net/install/ - aws s3 cp ${{ github.workspace }}\chia-blockchain-gui\release-builds\windows-installer\ChiaSetup-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.exe.sha256 s3://download-chia-net/install/ - aws s3 cp ${{ github.workspace }}\chia-blockchain-gui\release-builds\windows-installer\ChiaSetup-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.exe.torrent s3://download-chia-net/torrents/ + aws s3 cp ${{ github.workspace }}\chia-blockchain-gui\release-builds\windows-installer\ChiaSetup-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.exe s3://download.chia.net/install/ + aws s3 cp ${{ github.workspace }}\chia-blockchain-gui\release-builds\windows-installer\ChiaSetup-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.exe.sha256 s3://download.chia.net/install/ + aws s3 cp ${{ github.workspace }}\chia-blockchain-gui\release-builds\windows-installer\ChiaSetup-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.exe.torrent s3://download.chia.net/torrents/ - name: Get tag name if: startsWith(github.ref, 'refs/tags/') From e1e779ee3c86dd047018ff1f420d1ec84432263e Mon Sep 17 00:00:00 2001 From: Mariano Sorgente <3069354+mariano54@users.noreply.github.com> Date: Tue, 22 Feb 2022 15:34:53 -0500 Subject: [PATCH 109/378] Add auth key back (#10374) * Add auth key back * PK not SK --- chia/farmer/farmer.py | 6 +++++- chia/pools/pool_config.py | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/chia/farmer/farmer.py b/chia/farmer/farmer.py index fe09489cc65a..1e1f8bb7a77a 100644 --- a/chia/farmer/farmer.py +++ b/chia/farmer/farmer.py @@ -18,7 +18,7 @@ connect_to_keychain_and_validate, wrap_local_keychain, ) -from chia.pools.pool_config import PoolWalletConfig, load_pool_config +from chia.pools.pool_config import PoolWalletConfig, load_pool_config, add_auth_key from chia.protocols import farmer_protocol, harvester_protocol from chia.protocols.pool_protocol import ( ErrorResponse, @@ -422,6 +422,7 @@ def get_authentication_sk(self, pool_config: PoolWalletConfig) -> Optional[Priva async def update_pool_state(self): config = load_config(self._root_path, "config.yaml") + pool_config_list: List[PoolWalletConfig] = load_pool_config(self._root_path) for pool_config in pool_config_list: p2_singleton_puzzle_hash = pool_config.p2_singleton_puzzle_hash @@ -432,6 +433,9 @@ async def update_pool_state(self): if authentication_sk is None: self.log.error(f"Could not find authentication sk for {p2_singleton_puzzle_hash}") continue + + add_auth_key(self._root_path, pool_config, authentication_sk.get_g1()) + if p2_singleton_puzzle_hash not in self.pool_state: self.pool_state[p2_singleton_puzzle_hash] = { "points_found_since_start": 0, diff --git a/chia/pools/pool_config.py b/chia/pools/pool_config.py index 8324a83e0d53..2fca70176870 100644 --- a/chia/pools/pool_config.py +++ b/chia/pools/pool_config.py @@ -58,6 +58,25 @@ def load_pool_config(root_path: Path) -> List[PoolWalletConfig]: return ret_list +# TODO: remove this a few versions after 1.3, since authentication_public_key is deprecated. This is here to support +# downgrading to versions older than 1.3. +def add_auth_key(root_path: Path, config_entry: PoolWalletConfig, auth_key: G1Element): + config = load_config(root_path, "config.yaml") + pool_list = config["pool"].get("pool_list", []) + if pool_list is not None: + for pool_config_dict in pool_list: + try: + if ( + G1Element.from_bytes(hexstr_to_bytes(pool_config_dict["owner_public_key"])) + == config_entry.owner_public_key + ): + pool_config_dict["authentication_public_key"] = bytes(auth_key).hex() + except Exception as e: + log.error(f"Exception updating config: {pool_config_dict} {e}") + config["pool"]["pool_list"] = pool_list + save_config(root_path, "config.yaml", config) + + async def update_pool_config(root_path: Path, pool_config_list: List[PoolWalletConfig]): full_config = load_config(root_path, "config.yaml") full_config["pool"]["pool_list"] = [c.to_json_dict() for c in pool_config_list] From d9154f62293b7e4401def0a9ada777c4b634689d Mon Sep 17 00:00:00 2001 From: dustinface <35775977+xdustinface@users.noreply.github.com> Date: Tue, 22 Feb 2022 21:41:45 +0100 Subject: [PATCH 110/378] tests: Use `pytest.raises` in some places (#10335) * tests: Use `pytest.raises` in some places * tests: Fix `test_StrictDataClassLists` --- tests/core/util/test_streamable.py | 5 +---- tests/core/util/test_type_checking.py | 22 +++++++++------------- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/tests/core/util/test_streamable.py b/tests/core/util/test_streamable.py index 2002153f6b05..dece7be79934 100644 --- a/tests/core/util/test_streamable.py +++ b/tests/core/util/test_streamable.py @@ -58,16 +58,13 @@ class TestClass2(Streamable): a = TestClass2(uint32(1), uint32(2), b"3") bytes(a) - try: + with raises(NotImplementedError): @dataclass(frozen=True) @streamable class TestClass3(Streamable): a: int - except NotImplementedError: - pass - def test_json(self): block = bt.create_genesis_block(test_constants, bytes([0] * 32), b"0") diff --git a/tests/core/util/test_type_checking.py b/tests/core/util/test_type_checking.py index acbd14acf633..775affbe66b4 100644 --- a/tests/core/util/test_type_checking.py +++ b/tests/core/util/test_type_checking.py @@ -2,6 +2,8 @@ from dataclasses import dataclass from typing import Dict, List, Optional, Tuple +from pytest import raises + from chia.util.ints import uint8 from chia.util.type_checking import is_type_List, is_type_SpecificOptional, strictdataclass @@ -53,11 +55,9 @@ class TestClass2: b = 0 assert TestClass2(25) - try: + + with raises(TypeError): TestClass2(1, 2) - assert False - except TypeError: - pass def test_StrictDataClassLists(self): @dataclass(frozen=True) @@ -67,16 +67,12 @@ class TestClass: b: List[List[uint8]] assert TestClass([1, 2, 3], [[uint8(200), uint8(25)], [uint8(25)]]) - try: - TestClass([1, 2, 3], [[uint8(200), uint8(25)], [uint8(25)]]) - assert False - except AssertionError: - pass - try: + + with raises(ValueError): + TestClass({"1": 1}, [[uint8(200), uint8(25)], [uint8(25)]]) + + with raises(ValueError): TestClass([1, 2, 3], [uint8(200), uint8(25)]) - assert False - except ValueError: - pass def test_StrictDataClassOptional(self): @dataclass(frozen=True) From 8eb2a29834d5e3c074b3a2b85f912082ea661d04 Mon Sep 17 00:00:00 2001 From: dustinface <35775977+xdustinface@users.noreply.github.com> Date: Tue, 22 Feb 2022 21:42:05 +0100 Subject: [PATCH 111/378] streamable: Enable flake8 and pylint (#10355) --- chia/util/streamable.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/chia/util/streamable.py b/chia/util/streamable.py index d2281bbe961e..baedc9c16461 100644 --- a/chia/util/streamable.py +++ b/chia/util/streamable.py @@ -1,5 +1,3 @@ -# flake8: noqa -# pylint: disable from __future__ import annotations import dataclasses @@ -7,7 +5,7 @@ import pprint import sys from enum import Enum -from typing import Any, BinaryIO, Dict, get_type_hints, List, Tuple, Type, Callable, Optional, Iterator +from typing import Any, BinaryIO, Dict, get_type_hints, List, Tuple, Type, TypeVar, Callable, Optional, Iterator from blspy import G1Element, G2Element, PrivateKey from typing_extensions import Literal @@ -47,6 +45,8 @@ def get_args(t: Type[Any]) -> Tuple[Any, ...]: # JSON does not support big ints, so these types must be serialized differently in JSON big_ints = [uint64, int64, uint128, int512] +_T_Streamable = TypeVar("_T_Streamable", bound="Streamable") + def dataclass_from_dict(klass, d): """ @@ -156,8 +156,10 @@ def streamable(cls: Any): 1. A tuple of x items is serialized by appending the serialization of each item. 2. A List is serialized into a 4 byte size prefix (number of items) and the serialization of each item. - 3. An Optional is serialized into a 1 byte prefix of 0x00 or 0x01, and if it's one, it's followed by the serialization of the item. - 4. A Custom item is serialized by calling the .parse method, passing in the stream of bytes into it. An example is a CLVM program. + 3. An Optional is serialized into a 1 byte prefix of 0x00 or 0x01, and if it's one, it's followed by the + serialization of the item. + 4. A Custom item is serialized by calling the .parse method, passing in the stream of bytes into it. An example is + a CLVM program. All of the constituents must have parse/from_bytes, and stream/__bytes__ and therefore be of fixed size. For example, int cannot be a constituent since it is not a fixed size, @@ -263,7 +265,7 @@ def parse_str(f: BinaryIO) -> str: class Streamable: @classmethod - def function_to_parse_one_item(cls: Type[cls.__name__], f_type: Type): # type: ignore + def function_to_parse_one_item(cls, f_type: Type) -> Callable[[BinaryIO], Any]: """ This function returns a function taking one argument `f: BinaryIO` that parses and returns a value of the given type. @@ -295,9 +297,9 @@ def function_to_parse_one_item(cls: Type[cls.__name__], f_type: Type): # type: raise NotImplementedError(f"Type {f_type} does not have parse") @classmethod - def parse(cls: Type[cls.__name__], f: BinaryIO) -> cls.__name__: # type: ignore + def parse(cls: Type[_T_Streamable], f: BinaryIO) -> _T_Streamable: # Create the object without calling __init__() to avoid unnecessary post-init checks in strictdataclass - obj: Streamable = object.__new__(cls) + obj: _T_Streamable = object.__new__(cls) fields: Iterator[str] = iter(FIELDS_FOR_STREAMABLE_CLASS.get(cls, {})) values: Iterator = (parse_f(f) for parse_f in PARSE_FUNCTIONS_FOR_STREAMABLE_CLASS[cls]) for field, value in zip(fields, values): From 97ef7b275a81f89044868c0e42c6411317511afb Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Wed, 23 Feb 2022 01:22:11 +0100 Subject: [PATCH 112/378] use free listen ports when running tests (#10307) --- tests/block_tools.py | 5 + tests/core/daemon/test_daemon.py | 16 ++-- tests/core/full_node/test_full_node.py | 8 +- tests/core/server/test_dos.py | 2 +- tests/core/ssl/test_ssl.py | 35 ++++--- tests/core/test_daemon_rpc.py | 6 ++ tests/core/test_farmer_harvester_rpc.py | 5 +- tests/core/test_full_node_rpc.py | 5 +- tests/pools/test_pool_rpc.py | 3 +- tests/setup_nodes.py | 122 ++++++++++++++++++------ tests/simulation/test_simulation.py | 19 ++-- tests/util/socket.py | 11 +++ tests/wallet/rpc/test_wallet_rpc.py | 7 +- 13 files changed, 169 insertions(+), 75 deletions(-) create mode 100644 tests/util/socket.py diff --git a/tests/block_tools.py b/tests/block_tools.py index 7b9f95ae6a9c..e667c50dc959 100644 --- a/tests/block_tools.py +++ b/tests/block_tools.py @@ -84,6 +84,7 @@ from chia.util.vdf_prover import get_vdf_info_and_proof from tests.time_out_assert import time_out_assert from tests.wallet_tools import WalletTool +from tests.util.socket import find_available_listen_port from chia.wallet.derive_keys import ( master_sk_to_farmer_sk, master_sk_to_local_sk, @@ -150,6 +151,10 @@ def __init__( self._config["selected_network"] = "testnet0" for service in ["harvester", "farmer", "full_node", "wallet", "introducer", "timelord", "pool"]: self._config[service]["selected_network"] = "testnet0" + + # some tests start the daemon, make sure it's on a free port + self._config["daemon_port"] = find_available_listen_port("daemon port") + save_config(self.root_path, "config.yaml", self._config) overrides = self._config["network_overrides"]["constants"][self._config["selected_network"]] updated_constants = constants.replace_str_to_bytes(**overrides) diff --git a/tests/core/daemon/test_daemon.py b/tests/core/daemon/test_daemon.py index becb6956d6fa..e277a0639eb7 100644 --- a/tests/core/daemon/test_daemon.py +++ b/tests/core/daemon/test_daemon.py @@ -19,10 +19,6 @@ class TestDaemon: - @pytest_asyncio.fixture(scope="function") - async def get_daemon(self, get_b_tools): - async for _ in setup_daemon(btools=get_b_tools): - yield _ # TODO: Ideally, the db_version should be the (parameterized) db_version # fixture, to test all versions of the database schema. This doesn't work @@ -48,7 +44,6 @@ async def get_b_tools_1(self, get_temp_keyring): async def get_b_tools(self, get_temp_keyring): local_b_tools = await create_block_tools_async(constants=test_constants_modified, keychain=get_temp_keyring) new_config = local_b_tools._config - new_config["daemon_port"] = 55401 local_b_tools.change_config(new_config) return local_b_tools @@ -58,9 +53,10 @@ async def get_daemon_with_temp_keyring(self, get_b_tools): yield get_b_tools @pytest.mark.asyncio - async def test_daemon_simulation(self, simulation, get_daemon, get_b_tools): - node1, node2, _, _, _, _, _, _, _, _, server1 = simulation - await server1.start_client(PeerInfo(self_hostname, uint16(21238))) + async def test_daemon_simulation(self, simulation, get_b_tools): + node1, node2, _, _, _, _, _, _, _, _, server1, daemon1 = simulation + node2_port = node2.full_node.config["port"] + await server1.start_client(PeerInfo(self_hostname, uint16(node2_port))) async def num_connections(): count = len(node2.server.connection_by_type[NodeType.FULL_NODE].items()) @@ -73,7 +69,7 @@ async def num_connections(): ssl_context = get_b_tools.get_daemon_ssl_context() ws = await session.ws_connect( - "wss://127.0.0.1:55401", + f"wss://127.0.0.1:{daemon1.daemon_port}", autoclose=True, autoping=True, heartbeat=60, @@ -176,7 +172,7 @@ async def check_empty_passphrase_case(response: aiohttp.http_websocket.WSMessage async with aiohttp.ClientSession() as session: async with session.ws_connect( - "wss://127.0.0.1:55401", + f"wss://127.0.0.1:{local_b_tools._config['daemon_port']}", autoclose=True, autoping=True, heartbeat=60, diff --git a/tests/core/full_node/test_full_node.py b/tests/core/full_node/test_full_node.py index 25a483575041..3e54c2e1015d 100644 --- a/tests/core/full_node/test_full_node.py +++ b/tests/core/full_node/test_full_node.py @@ -125,25 +125,25 @@ async def wallet_nodes(): @pytest_asyncio.fixture(scope="function") async def setup_four_nodes(db_version): - async for _ in setup_simulators_and_wallets(5, 0, {}, starting_port=51000, db_version=db_version): + async for _ in setup_simulators_and_wallets(5, 0, {}, db_version=db_version): yield _ @pytest_asyncio.fixture(scope="function") async def setup_two_nodes(db_version): - async for _ in setup_simulators_and_wallets(2, 0, {}, starting_port=51100, db_version=db_version): + async for _ in setup_simulators_and_wallets(2, 0, {}, db_version=db_version): yield _ @pytest_asyncio.fixture(scope="function") async def setup_two_nodes_and_wallet(): - async for _ in setup_simulators_and_wallets(2, 1, {}, starting_port=51200, db_version=2): + async for _ in setup_simulators_and_wallets(2, 1, {}, db_version=2): yield _ @pytest_asyncio.fixture(scope="function") async def wallet_nodes_mainnet(db_version): - async_gen = setup_simulators_and_wallets(2, 1, {"NETWORK_TYPE": 0}, starting_port=40000, db_version=db_version) + async_gen = setup_simulators_and_wallets(2, 1, {"NETWORK_TYPE": 0}, db_version=db_version) nodes, wallets = await async_gen.__anext__() full_node_1 = nodes[0] full_node_2 = nodes[1] diff --git a/tests/core/server/test_dos.py b/tests/core/server/test_dos.py index 876da52e62c6..59a31d3f7f5b 100644 --- a/tests/core/server/test_dos.py +++ b/tests/core/server/test_dos.py @@ -41,7 +41,7 @@ def event_loop(): @pytest_asyncio.fixture(scope="function") async def setup_two_nodes(db_version): - async for _ in setup_simulators_and_wallets(2, 0, {}, starting_port=60000, db_version=db_version): + async for _ in setup_simulators_and_wallets(2, 0, {}, db_version=db_version): yield _ diff --git a/tests/core/ssl/test_ssl.py b/tests/core/ssl/test_ssl.py index 34f0015ead62..b983fc0266fc 100644 --- a/tests/core/ssl/test_ssl.py +++ b/tests/core/ssl/test_ssl.py @@ -20,11 +20,13 @@ setup_simulators_and_wallets, setup_timelord, ) +from tests.util.socket import find_available_listen_port -async def establish_connection(server: ChiaServer, dummy_port: int, ssl_context) -> bool: +async def establish_connection(server: ChiaServer, ssl_context) -> bool: timeout = aiohttp.ClientTimeout(total=10) session = aiohttp.ClientSession(timeout=timeout) + dummy_port = 5 # this does not matter try: incoming_queue: asyncio.Queue = asyncio.Queue() url = f"wss://{self_hostname}:{server._port}/ws" @@ -64,12 +66,17 @@ async def wallet_node(self): @pytest_asyncio.fixture(scope="function") async def introducer(self): - async for _ in setup_introducer(21233): + introducer_port = find_available_listen_port("introducer") + async for _ in setup_introducer(introducer_port): yield _ @pytest_asyncio.fixture(scope="function") async def timelord(self): - async for _ in setup_timelord(21236, 21237, 0, False, test_constants, bt): + timelord_port = find_available_listen_port("timelord") + node_port = find_available_listen_port("node") + rpc_port = find_available_listen_port("rpc") + vdf_port = find_available_listen_port("vdf") + async for _ in setup_timelord(timelord_port, node_port, rpc_port, vdf_port, False, test_constants, bt): yield _ @pytest.mark.asyncio @@ -100,7 +107,7 @@ async def test_farmer(self, harvester_farmer): ssl_context = ssl_context_for_client( farmer_server.ca_private_crt_path, farmer_server.ca_private_key_path, priv_crt, priv_key ) - connected = await establish_connection(farmer_server, 12312, ssl_context) + connected = await establish_connection(farmer_server, ssl_context) assert connected is True # Create not authenticated cert @@ -112,12 +119,12 @@ async def test_farmer(self, harvester_farmer): ssl_context = ssl_context_for_client( farmer_server.chia_ca_crt_path, farmer_server.chia_ca_key_path, pub_crt, pub_key ) - connected = await establish_connection(farmer_server, 12312, ssl_context) + connected = await establish_connection(farmer_server, ssl_context) assert connected is False ssl_context = ssl_context_for_client( farmer_server.ca_private_crt_path, farmer_server.ca_private_key_path, pub_crt, pub_key ) - connected = await establish_connection(farmer_server, 12312, ssl_context) + connected = await establish_connection(farmer_server, ssl_context) assert connected is False @pytest.mark.asyncio @@ -138,7 +145,7 @@ async def test_full_node(self, wallet_node): ssl_context = ssl_context_for_client( full_node_server.chia_ca_crt_path, full_node_server.chia_ca_key_path, pub_crt, pub_key ) - connected = await establish_connection(full_node_server, 12312, ssl_context) + connected = await establish_connection(full_node_server, ssl_context) assert connected is True @pytest.mark.asyncio @@ -155,7 +162,7 @@ async def test_wallet(self, wallet_node): ssl_context = ssl_context_for_client( wallet_server.chia_ca_crt_path, wallet_server.chia_ca_key_path, pub_crt, pub_key ) - connected = await establish_connection(wallet_server, 12312, ssl_context) + connected = await establish_connection(wallet_server, ssl_context) assert connected is False # Not even signed by private cert @@ -170,7 +177,7 @@ async def test_wallet(self, wallet_node): ssl_context = ssl_context_for_client( wallet_server.ca_private_crt_path, wallet_server.ca_private_key_path, priv_crt, priv_key ) - connected = await establish_connection(wallet_server, 12312, ssl_context) + connected = await establish_connection(wallet_server, ssl_context) assert connected is False @pytest.mark.asyncio @@ -190,7 +197,7 @@ async def test_harvester(self, harvester_farmer): ssl_context = ssl_context_for_client( harvester_server.chia_ca_crt_path, harvester_server.chia_ca_key_path, pub_crt, pub_key ) - connected = await establish_connection(harvester_server, 12312, ssl_context) + connected = await establish_connection(harvester_server, ssl_context) assert connected is False # Not even signed by private cert @@ -205,7 +212,7 @@ async def test_harvester(self, harvester_farmer): ssl_context = ssl_context_for_client( harvester_server.ca_private_crt_path, harvester_server.ca_private_key_path, priv_crt, priv_key ) - connected = await establish_connection(harvester_server, 12312, ssl_context) + connected = await establish_connection(harvester_server, ssl_context) assert connected is False @pytest.mark.asyncio @@ -224,7 +231,7 @@ async def test_introducer(self, introducer): ssl_context = ssl_context_for_client( introducer_server.chia_ca_crt_path, introducer_server.chia_ca_key_path, pub_crt, pub_key ) - connected = await establish_connection(introducer_server, 12312, ssl_context) + connected = await establish_connection(introducer_server, ssl_context) assert connected is True @pytest.mark.asyncio @@ -243,7 +250,7 @@ async def test_timelord(self, timelord): ssl_context = ssl_context_for_client( timelord_server.chia_ca_crt_path, timelord_server.chia_ca_key_path, pub_crt, pub_key ) - connected = await establish_connection(timelord_server, 12312, ssl_context) + connected = await establish_connection(timelord_server, ssl_context) assert connected is False # Not even signed by private cert @@ -258,5 +265,5 @@ async def test_timelord(self, timelord): ssl_context = ssl_context_for_client( timelord_server.ca_private_crt_path, timelord_server.ca_private_key_path, priv_crt, priv_key ) - connected = await establish_connection(timelord_server, 12312, ssl_context) + connected = await establish_connection(timelord_server, ssl_context) assert connected is False diff --git a/tests/core/test_daemon_rpc.py b/tests/core/test_daemon_rpc.py index e17d1b1d91e8..e28814aa56ae 100644 --- a/tests/core/test_daemon_rpc.py +++ b/tests/core/test_daemon_rpc.py @@ -5,11 +5,17 @@ from chia.daemon.client import connect_to_daemon from tests.setup_nodes import bt from chia import __version__ +from tests.util.socket import find_available_listen_port +from chia.util.config import save_config class TestDaemonRpc: @pytest_asyncio.fixture(scope="function") async def get_daemon(self): + bt._config["daemon_port"] = find_available_listen_port() + # unfortunately, the daemon's WebSocketServer loads the configs from + # disk, so the only way to configure its port is to write it to disk + save_config(bt.root_path, "config.yaml", bt._config) async for _ in setup_daemon(btools=bt): yield _ diff --git a/tests/core/test_farmer_harvester_rpc.py b/tests/core/test_farmer_harvester_rpc.py index 2d63bea53d35..2401c9b79f58 100644 --- a/tests/core/test_farmer_harvester_rpc.py +++ b/tests/core/test_farmer_harvester_rpc.py @@ -21,6 +21,7 @@ from tests.setup_nodes import bt, self_hostname, setup_farmer_harvester, test_constants from tests.time_out_assert import time_out_assert, time_out_assert_custom_interval from tests.util.rpc import validate_get_routes +from tests.util.socket import find_available_listen_port log = logging.getLogger(__name__) @@ -45,8 +46,8 @@ def stop_node_cb(): farmer_rpc_api = FarmerRpcApi(farmer_service._api.farmer) harvester_rpc_api = HarvesterRpcApi(harvester_service._node) - rpc_port_farmer = uint16(21522) - rpc_port_harvester = uint16(21523) + rpc_port_farmer = uint16(find_available_listen_port("farmer rpc")) + rpc_port_harvester = uint16(find_available_listen_port("harvester rpc")) rpc_cleanup = await start_rpc_server( farmer_rpc_api, diff --git a/tests/core/test_full_node_rpc.py b/tests/core/test_full_node_rpc.py index 78f50892f14e..5ae06e37f0f7 100644 --- a/tests/core/test_full_node_rpc.py +++ b/tests/core/test_full_node_rpc.py @@ -25,6 +25,7 @@ from tests.setup_nodes import bt, self_hostname, setup_simulators_and_wallets, test_constants from tests.time_out_assert import time_out_assert from tests.util.rpc import validate_get_routes +from tests.util.socket import find_available_listen_port class TestRpc: @@ -36,7 +37,7 @@ async def two_nodes(self): @pytest.mark.asyncio async def test1(self, two_nodes): num_blocks = 5 - test_rpc_port = uint16(21522) + test_rpc_port = find_available_listen_port() nodes, _ = two_nodes full_node_api_1, full_node_api_2 = nodes server_1 = full_node_api_1.full_node.server @@ -231,7 +232,7 @@ async def num_connections(): @pytest.mark.asyncio async def test_signage_points(self, two_nodes, empty_blockchain): - test_rpc_port = uint16(21522) + test_rpc_port = find_available_listen_port() nodes, _ = two_nodes full_node_api_1, full_node_api_2 = nodes server_1 = full_node_api_1.full_node.server diff --git a/tests/pools/test_pool_rpc.py b/tests/pools/test_pool_rpc.py index 51e595d29474..99e175e08c08 100644 --- a/tests/pools/test_pool_rpc.py +++ b/tests/pools/test_pool_rpc.py @@ -32,6 +32,7 @@ from chia.wallet.util.wallet_types import WalletType from tests.setup_nodes import self_hostname, setup_simulators_and_wallets, bt from tests.time_out_assert import time_out_assert +from tests.util.socket import find_available_listen_port # TODO: Compare deducted fees in all tests against reported total_fee log = logging.getLogger(__name__) @@ -103,7 +104,7 @@ async def one_wallet_node_and_rpc(self): config = bt.config hostname = config["self_hostname"] daemon_port = config["daemon_port"] - test_rpc_port = uint16(21529) + test_rpc_port = find_available_listen_port() rpc_cleanup = await start_rpc_server( api_user, diff --git a/tests/setup_nodes.py b/tests/setup_nodes.py index d45aa4b02bdd..486987448f1d 100644 --- a/tests/setup_nodes.py +++ b/tests/setup_nodes.py @@ -22,6 +22,7 @@ from chia.util.bech32m import encode_puzzle_hash from tests.block_tools import create_block_tools, create_block_tools_async, test_constants from tests.util.keyring import TempKeyring +from tests.util.socket import find_available_listen_port from chia.util.hash import std_hash from chia.util.ints import uint16, uint32 from chia.util.keychain import bytes_to_mnemonic @@ -75,6 +76,7 @@ async def setup_full_node( consensus_constants: ConsensusConstants, db_name, port, + rpc_port, local_bt, introducer_port=None, simulator=False, @@ -106,7 +108,7 @@ async def setup_full_node( config["introducer_peer"] = None config["dns_servers"] = [] config["port"] = port - config["rpc_port"] = port + 1000 + config["rpc_port"] = rpc_port overrides = config["network_overrides"]["constants"][config["selected_network"]] updated_constants = consensus_constants.replace_str_to_bytes(**overrides) if simulator: @@ -134,6 +136,7 @@ async def setup_full_node( async def setup_wallet_node( port, + rpc_port, consensus_constants: ConsensusConstants, local_bt, full_node_port=None, @@ -145,7 +148,7 @@ async def setup_wallet_node( with TempKeyring() as keychain: config = bt.config["wallet"] config["port"] = port - config["rpc_port"] = port + 1000 + config["rpc_port"] = rpc_port if starting_height is not None: config["starting_height"] = starting_height config["initial_num_public_keys"] = initial_num_public_keys @@ -201,9 +204,13 @@ async def setup_wallet_node( async def setup_harvester( - port, farmer_port, consensus_constants: ConsensusConstants, b_tools, start_service: bool = True + port, rpc_port, farmer_port, consensus_constants: ConsensusConstants, b_tools, start_service: bool = True ): - kwargs = service_kwargs_for_harvester(b_tools.root_path, b_tools.config["harvester"], consensus_constants) + + config = bt.config["harvester"] + config["port"] = port + config["rpc_port"] = rpc_port + kwargs = service_kwargs_for_harvester(b_tools.root_path, config, consensus_constants) kwargs.update( server_listen_ports=[port], advertised_port=port, @@ -226,6 +233,7 @@ async def setup_harvester( async def setup_farmer( port, + rpc_port, consensus_constants: ConsensusConstants, b_tools, full_node_port: Optional[uint16] = None, @@ -237,6 +245,7 @@ async def setup_farmer( config["xch_target_address"] = encode_puzzle_hash(b_tools.farmer_ph, "xch") config["pool_public_keys"] = [bytes(pk).hex() for pk in b_tools.pool_pubkeys] config["port"] = port + config["rpc_port"] = rpc_port config_pool["xch_target_address"] = encode_puzzle_hash(b_tools.pool_ph, "xch") if full_node_port: @@ -316,14 +325,15 @@ def stop(): await kill_processes() -async def setup_timelord(port, full_node_port, rpc_port, sanitizer, consensus_constants: ConsensusConstants, b_tools): +async def setup_timelord( + port, full_node_port, rpc_port, vdf_port, sanitizer, consensus_constants: ConsensusConstants, b_tools +): config = b_tools.config["timelord"] config["port"] = port config["full_node_peer"]["port"] = full_node_port config["bluebox_mode"] = sanitizer config["fast_algorithm"] = False - if sanitizer: - config["vdf_server"]["port"] = 7999 + config["vdf_server"]["port"] = vdf_port config["start_rpc_server"] = True config["rpc_port"] = rpc_port @@ -354,7 +364,8 @@ async def setup_two_nodes(consensus_constants: ConsensusConstants, db_version: i setup_full_node( consensus_constants, "blockchain_test.db", - 21234, + find_available_listen_port("node1"), + find_available_listen_port("node1 rpc"), await create_block_tools_async(constants=test_constants, keychain=keychain1), simulator=False, db_version=db_version, @@ -362,7 +373,8 @@ async def setup_two_nodes(consensus_constants: ConsensusConstants, db_version: i setup_full_node( consensus_constants, "blockchain_test_2.db", - 21235, + find_available_listen_port("node2"), + find_available_listen_port("node2 rpc"), await create_block_tools_async(constants=test_constants, keychain=keychain2), simulator=False, db_version=db_version, @@ -381,7 +393,6 @@ async def setup_n_nodes(consensus_constants: ConsensusConstants, n: int, db_vers """ Setup and teardown of n full nodes, with blockchains and separate DBs. """ - port_start = 21244 node_iters = [] keyrings_to_cleanup = [] for i in range(n): @@ -391,7 +402,8 @@ async def setup_n_nodes(consensus_constants: ConsensusConstants, n: int, db_vers setup_full_node( consensus_constants, f"blockchain_test_{i}.db", - port_start + i, + find_available_listen_port(f"node{i}"), + find_available_listen_port(f"node{i} rpc"), await create_block_tools_async(constants=test_constants, keychain=keyring.get_keychain()), simulator=False, db_version=db_version, @@ -416,10 +428,22 @@ async def setup_node_and_wallet( btools = await create_block_tools_async(constants=test_constants, keychain=keychain) node_iters = [ setup_full_node( - consensus_constants, "blockchain_test.db", 21234, btools, simulator=False, db_version=db_version + consensus_constants, + "blockchain_test.db", + find_available_listen_port("node1"), + find_available_listen_port("node1 rpc"), + btools, + simulator=False, + db_version=db_version, ), setup_wallet_node( - 21235, consensus_constants, btools, None, starting_height=starting_height, key_seed=key_seed + find_available_listen_port("node2"), + find_available_listen_port("node2 rpc"), + consensus_constants, + btools, + None, + starting_height=starting_height, + key_seed=key_seed, ), ] @@ -437,7 +461,6 @@ async def setup_simulators_and_wallets( dic: Dict, starting_height=None, key_seed=None, - starting_port=50000, initial_num_public_keys=5, db_version=1, ): @@ -448,7 +471,8 @@ async def setup_simulators_and_wallets( consensus_constants = constants_for_dic(dic) for index in range(0, simulator_count): - port = starting_port + index + port = find_available_listen_port(f"node{index}") + 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 @@ -457,6 +481,7 @@ async def setup_simulators_and_wallets( bt_tools.constants, db_name, port, + rpc_port, bt_tools, simulator=True, db_version=db_version, @@ -469,12 +494,14 @@ async def setup_simulators_and_wallets( seed = std_hash(uint32(index)) else: seed = key_seed - port = starting_port + 5000 + index + 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 ) # block tools modifies constants wlt = setup_wallet_node( port, + rpc_port, bt_tools.constants, bt_tools, None, @@ -491,9 +518,13 @@ async def setup_simulators_and_wallets( async def setup_farmer_harvester(consensus_constants: ConsensusConstants, start_services: bool = True): + farmer_port = find_available_listen_port("farmer") + farmer_rpc_port = find_available_listen_port("farmer rpc") + harvester_port = find_available_listen_port("harvester") + harvester_rpc_port = find_available_listen_port("harvester rpc") node_iters = [ - setup_harvester(21234, 21235, consensus_constants, bt, start_services), - setup_farmer(21235, consensus_constants, bt, start_service=start_services), + setup_harvester(harvester_port, harvester_rpc_port, farmer_port, consensus_constants, bt, start_services), + setup_farmer(farmer_port, farmer_rpc_port, consensus_constants, bt, start_service=start_services), ] harvester_service = await node_iters[0].__anext__() @@ -512,18 +543,37 @@ async def setup_full_system( b_tools = await create_block_tools_async(constants=test_constants, keychain=keychain1) if b_tools_1 is None: b_tools_1 = await create_block_tools_async(constants=test_constants, keychain=keychain2) + introducer_port = find_available_listen_port("introducer") + farmer_port = find_available_listen_port("farmer") + farmer_rpc_port = find_available_listen_port("farmer rpc") + node1_port = find_available_listen_port("node1") + rpc1_port = find_available_listen_port("node1 rpc") + node2_port = find_available_listen_port("node2") + rpc2_port = find_available_listen_port("node2 rpc") + timelord1_port = find_available_listen_port("timelord1") + timelord1_rpc_port = find_available_listen_port("timelord1 rpc") + timelord2_port = find_available_listen_port("timelord2") + timelord2_rpc_port = find_available_listen_port("timelord2 rpc") + vdf1_port = find_available_listen_port("vdf1") + vdf2_port = find_available_listen_port("vdf2") + harvester_port = find_available_listen_port("harvester") + harvester_rpc_port = find_available_listen_port("harvester rpc") + node_iters = [ - setup_introducer(21233), - setup_harvester(21234, 21235, consensus_constants, b_tools), - setup_farmer(21235, consensus_constants, b_tools, uint16(21237)), - setup_vdf_clients(8000), - setup_timelord(21236, 21237, 21241, False, consensus_constants, b_tools), + setup_introducer(introducer_port), + setup_harvester(harvester_port, harvester_rpc_port, farmer_port, consensus_constants, b_tools), + setup_farmer(farmer_port, farmer_rpc_port, consensus_constants, b_tools, uint16(node1_port)), + setup_vdf_clients(vdf1_port), + setup_timelord( + timelord2_port, node1_port, timelord2_rpc_port, vdf1_port, False, consensus_constants, b_tools + ), setup_full_node( consensus_constants, "blockchain_test.db", - 21237, + node1_port, + rpc1_port, b_tools, - 21233, + introducer_port, False, 10, True, @@ -533,19 +583,23 @@ async def setup_full_system( setup_full_node( consensus_constants, "blockchain_test_2.db", - 21238, + node2_port, + rpc2_port, b_tools_1, - 21233, + introducer_port, False, 10, True, - connect_to_daemon, + False, # connect_to_daemon, db_version=db_version, ), - setup_vdf_client(7999), - setup_timelord(21239, 1000, 21242, True, consensus_constants, b_tools_1), + setup_vdf_client(vdf2_port), + setup_timelord(timelord1_port, 1000, timelord1_rpc_port, vdf2_port, True, consensus_constants, b_tools_1), ] + if connect_to_daemon: + node_iters.append(setup_daemon(btools=b_tools)) + introducer, introducer_server = await node_iters[0].__anext__() harvester_service = await node_iters[1].__anext__() harvester = harvester_service._node @@ -565,7 +619,7 @@ async def num_connections(): vdf_sanitizer = await node_iters[7].__anext__() sanitizer, sanitizer_server = await node_iters[8].__anext__() - yield ( + ret = ( node_api_1, node_api_2, harvester, @@ -579,4 +633,10 @@ async def num_connections(): node_api_1.full_node.server, ) + if connect_to_daemon: + daemon1 = await node_iters[9].__anext__() + yield ret + (daemon1,) + else: + yield ret + await _teardown_nodes(node_iters) diff --git a/tests/simulation/test_simulation.py b/tests/simulation/test_simulation.py index aacdabce3ae3..31d877db398f 100644 --- a/tests/simulation/test_simulation.py +++ b/tests/simulation/test_simulation.py @@ -8,6 +8,7 @@ from tests.setup_nodes import self_hostname, setup_full_node, setup_full_system, test_constants from tests.time_out_assert import time_out_assert from tests.util.keyring import TempKeyring +from tests.util.socket import find_available_listen_port test_constants_modified = test_constants.replace( **{ @@ -38,7 +39,8 @@ async def extra_node(self): async for _ in setup_full_node( test_constants_modified, "blockchain_test_3.db", - 21240, + find_available_listen_port(), + find_available_listen_port(), b_tools, db_version=1, ): @@ -52,10 +54,13 @@ async def simulation(self): @pytest.mark.asyncio async def test_simulation_1(self, simulation, extra_node): node1, node2, _, _, _, _, _, _, _, sanitizer_server, server1 = simulation - await server1.start_client(PeerInfo(self_hostname, uint16(21238))) + + node1_port = node1.full_node.config["port"] + node2_port = node2.full_node.config["port"] + await server1.start_client(PeerInfo(self_hostname, uint16(node2_port))) # Use node2 to test node communication, since only node1 extends the chain. - await time_out_assert(1500, node_height_at_least, True, node2, 7) - await sanitizer_server.start_client(PeerInfo(self_hostname, uint16(21238))) + await time_out_assert(600, node_height_at_least, True, node2, 7) + await sanitizer_server.start_client(PeerInfo(self_hostname, uint16(node2_port))) async def has_compact(node1, node2): peak_height_1 = node1.full_node.blockchain.get_peak_height() @@ -95,10 +100,10 @@ async def has_compact(node1, node2): # ) return has_compact == [True, True] - await time_out_assert(1500, has_compact, True, node1, node2) + await time_out_assert(600, has_compact, True, node1, node2) node3 = extra_node server3 = node3.full_node.server peak_height = max(node1.full_node.blockchain.get_peak_height(), node2.full_node.blockchain.get_peak_height()) - await server3.start_client(PeerInfo(self_hostname, uint16(21237))) - await server3.start_client(PeerInfo(self_hostname, uint16(21238))) + await server3.start_client(PeerInfo(self_hostname, uint16(node1_port))) + await server3.start_client(PeerInfo(self_hostname, uint16(node2_port))) await time_out_assert(600, node_height_at_least, True, node3, peak_height) diff --git a/tests/util/socket.py b/tests/util/socket.py new file mode 100644 index 000000000000..b4ff07d2757b --- /dev/null +++ b/tests/util/socket.py @@ -0,0 +1,11 @@ +import socket + + +def find_available_listen_port(name: str = "free") -> int: + s = socket.socket() + s.bind(("127.0.0.1", 0)) + addr = s.getsockname() + assert addr[1] > 0 + s.close() + print(f"{name} port: {addr[1]}") + return int(addr[1]) diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index 83567f68e5f0..aa861844e986 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -36,6 +36,7 @@ from tests.pools.test_pool_rpc import wallet_is_synced from tests.setup_nodes import bt, setup_simulators_and_wallets, self_hostname from tests.time_out_assert import time_out_assert +from tests.util.socket import find_available_listen_port log = logging.getLogger(__name__) @@ -52,9 +53,9 @@ async def two_wallet_nodes(self): ) @pytest.mark.asyncio async def test_wallet_rpc(self, two_wallet_nodes, trusted): - test_rpc_port = uint16(21529) - test_rpc_port_2 = uint16(21536) - test_rpc_port_node = uint16(21530) + test_rpc_port = find_available_listen_port() + test_rpc_port_2 = find_available_listen_port() + test_rpc_port_node = find_available_listen_port() num_blocks = 5 full_nodes, wallets = two_wallet_nodes full_node_api = full_nodes[0] From 001effae612a84eff4d2f9ccf7bb1391e3765048 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Tue, 22 Feb 2022 20:39:46 -0500 Subject: [PATCH 113/378] asyncify wallet weight proof validation (#10376) --- chia/full_node/weight_proof.py | 4 +++- chia/wallet/wallet_weight_proof_handler.py | 10 ++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/chia/full_node/weight_proof.py b/chia/full_node/weight_proof.py index dfaabde55fcd..0f20eb1d2b7a 100644 --- a/chia/full_node/weight_proof.py +++ b/chia/full_node/weight_proof.py @@ -601,7 +601,9 @@ async def validate_weight_proof(self, weight_proof: WeightProof) -> Tuple[bool, log.info(f"validate weight proof peak height {peak_height}") # TODO: Consider if this can be spun off to a thread as an alternative to - # sprinkling async sleeps around. + # sprinkling async sleeps around. Also see the corresponding comment + # in the wallet code. + # all instances tagged as: 098faior2ru08d08ufa # timing reference: start summaries, sub_epoch_weight_list = _validate_sub_epoch_summaries(self.constants, weight_proof) diff --git a/chia/wallet/wallet_weight_proof_handler.py b/chia/wallet/wallet_weight_proof_handler.py index 1a44ca1a2dcd..d8b03300b128 100644 --- a/chia/wallet/wallet_weight_proof_handler.py +++ b/chia/wallet/wallet_weight_proof_handler.py @@ -77,7 +77,13 @@ async def _validate_weight_proof_inner( peak_height = weight_proof.recent_chain_data[-1].reward_chain_block.height log.info(f"validate weight proof peak height {peak_height}") + # TODO: Consider if this can be spun off to a thread as an alternative to + # sprinkling async sleeps around. Also see the corresponding comment + # in the full node code. + # all instances tagged as: 098faior2ru08d08ufa + summaries, sub_epoch_weight_list = _validate_sub_epoch_summaries(self._constants, weight_proof) + await asyncio.sleep(0) # break up otherwise multi-second sync code if summaries is None: log.error("weight proof failed sub epoch data validation") return False, uint32(0), [], [] @@ -91,6 +97,7 @@ async def _validate_weight_proof_inner( constants, summary_bytes, wp_segment_bytes, wp_recent_chain_bytes = vars_to_bytes( self._constants, summaries, weight_proof ) + await asyncio.sleep(0) # break up otherwise multi-second sync code vdf_tasks: List[asyncio.Future] = [] recent_blocks_validation_task: asyncio.Future = asyncio.get_running_loop().run_in_executor( @@ -106,6 +113,7 @@ async def _validate_weight_proof_inner( segments_validated, vdfs_to_validate = _validate_sub_epoch_segments( constants, rng, wp_segment_bytes, summary_bytes ) + await asyncio.sleep(0) # break up otherwise multi-second sync code if not segments_validated: return False, uint32(0), [], [] @@ -124,6 +132,8 @@ async def _validate_weight_proof_inner( pathlib.Path(self._executor_shutdown_tempfile.name), ) vdf_tasks.append(vdf_task) + # give other stuff a turn + await asyncio.sleep(0) for vdf_task in vdf_tasks: validated = await vdf_task From c844016019ca496fb62b3d121444e8235425b8f0 Mon Sep 17 00:00:00 2001 From: Mariano Sorgente <3069354+mariano54@users.noreply.github.com> Date: Tue, 22 Feb 2022 21:37:51 -0500 Subject: [PATCH 114/378] Improve trusted sync performance (#10377) * Improve trusted sync performance * Fix broken CLI * Decrease timestamp cache size * Add all valid states at the right time --- chia/server/ws_connection.py | 2 +- chia/wallet/util/peer_request_cache.py | 17 +++++++- chia/wallet/util/wallet_sync_utils.py | 4 +- chia/wallet/wallet_node.py | 55 ++++++++++++++++---------- 4 files changed, 53 insertions(+), 25 deletions(-) diff --git a/chia/server/ws_connection.py b/chia/server/ws_connection.py index 7dbf8e98cd86..c627b7863c4d 100644 --- a/chia/server/ws_connection.py +++ b/chia/server/ws_connection.py @@ -459,7 +459,7 @@ async def _read_one_message(self) -> Optional[Message]: await asyncio.sleep(3) return None else: - self.log.warning( + self.log.debug( f"Peer surpassed rate limit {self.peer_host}, message: {message_type}, " f"port {self.peer_port} but not disconnecting" ) diff --git a/chia/wallet/util/peer_request_cache.py b/chia/wallet/util/peer_request_cache.py index dacfe005880f..4efc0c02db13 100644 --- a/chia/wallet/util/peer_request_cache.py +++ b/chia/wallet/util/peer_request_cache.py @@ -4,7 +4,7 @@ from chia.protocols.wallet_protocol import CoinState, RespondSESInfo from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.header_block import HeaderBlock -from chia.util.ints import uint32 +from chia.util.ints import uint32, uint64 from chia.util.lru_cache import LRUCache @@ -13,18 +13,24 @@ class PeerRequestCache: _block_requests: LRUCache # (start, end) -> RequestHeaderBlocks _ses_requests: LRUCache # height -> Ses request _states_validated: LRUCache # coin state hash -> last change height, or None for reorg + _timestamps: LRUCache # block height -> timestamp def __init__(self): self._blocks = LRUCache(100) self._block_requests = LRUCache(100) self._ses_requests = LRUCache(100) self._states_validated = LRUCache(1000) + self._timestamps = LRUCache(1000) def get_block(self, height: uint32) -> Optional[HeaderBlock]: return self._blocks.get(height) def add_to_blocks(self, header_block: HeaderBlock) -> None: self._blocks.put(header_block.height, header_block) + if header_block.is_transaction_block: + assert header_block.foliage_transaction_block is not None + if self._timestamps.get(header_block.height) is None: + self._timestamps.put(header_block.height, header_block.foliage_transaction_block.timestamp) def get_block_request(self, start: uint32, end: uint32) -> Optional[asyncio.Task]: return self._block_requests.get((start, end)) @@ -49,6 +55,9 @@ def add_to_states_validated(self, coin_state: CoinState) -> None: cs_height = coin_state.created_height self._states_validated.put(coin_state.get_hash(), cs_height) + def get_height_timestamp(self, height: uint32) -> Optional[uint64]: + return self._timestamps.get(height) + def clear_after_height(self, height: int): # Remove any cached item which relates to an event that happened at a height above height. new_blocks = LRUCache(self._blocks.capacity) @@ -75,6 +84,12 @@ def clear_after_height(self, height: int): new_states_validated.put(k, cs_height) self._states_validated = new_states_validated + new_timestamps = LRUCache(self._timestamps.capacity) + for h, ts in self._timestamps.cache.items(): + if h <= height: + new_timestamps.put(h, ts) + self._timestamps = new_timestamps + async def can_use_peer_request_cache( coin_state: CoinState, peer_request_cache: PeerRequestCache, fork_height: Optional[uint32] diff --git a/chia/wallet/util/wallet_sync_utils.py b/chia/wallet/util/wallet_sync_utils.py index e7d2d64d091c..9dcb7803d6c9 100644 --- a/chia/wallet/util/wallet_sync_utils.py +++ b/chia/wallet/util/wallet_sync_utils.py @@ -320,13 +320,13 @@ async def fetch_header_blocks_in_range( res_h_blocks_task: Optional[asyncio.Task] = peer_request_cache.get_block_request(request_start, request_end) if res_h_blocks_task is not None: - log.info(f"Using cache for: {start}-{end}") + log.debug(f"Using cache for: {start}-{end}") if res_h_blocks_task.done(): res_h_blocks: Optional[RespondHeaderBlocks] = res_h_blocks_task.result() else: res_h_blocks = await res_h_blocks_task else: - log.info(f"Fetching: {start}-{end}") + log.debug(f"Fetching: {start}-{end}") request_header_blocks = RequestHeaderBlocks(request_start, request_end) res_h_blocks_task = asyncio.create_task(_fetch_header_blocks_inner(all_peers, request_header_blocks)) peer_request_cache.add_to_block_requests(request_start, request_end, res_h_blocks_task) diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index 0a48a1d67b4f..d3c52863b5b9 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -605,27 +605,35 @@ async def receive_state_from_peer( target_concurrent_tasks: int = 20 num_concurrent_tasks: int = 0 - async def receive_and_validate(inner_state: CoinState, inner_idx: int): + async def receive_and_validate(inner_states: List[CoinState], inner_idx_start: int): try: assert self.validation_semaphore is not None async with self.validation_semaphore: assert self.wallet_state_manager is not None if header_hash is not None: assert height is not None - self.add_state_to_race_cache(header_hash, height, inner_state) - self.log.info(f"Added to race cache: {height}, {inner_state}") + for inner_state in inner_states: + self.add_state_to_race_cache(header_hash, height, inner_state) + self.log.info(f"Added to race cache: {height}, {inner_state}") if trusted: - valid = True + valid_states = inner_states else: - valid = await self.validate_received_state_from_peer(inner_state, peer, cache, fork_height) - if valid: - self.log.info(f"new coin state received ({inner_idx + 1} / {len(items)})") + valid_states = [ + inner_state + for inner_state in inner_states + if await self.validate_received_state_from_peer(inner_state, peer, cache, fork_height) + ] + if len(valid_states) > 0: + self.log.info( + f"new coin state received ({inner_idx_start}-" + f"{inner_idx_start + len(inner_states) - 1}/ {len(items) + 1})" + ) assert self.new_state_lock is not None async with self.new_state_lock: - await self.wallet_state_manager.new_coin_state([inner_state], peer, fork_height) + await self.wallet_state_manager.new_coin_state(valid_states, peer, fork_height) if update_finished_height: await self.wallet_state_manager.blockchain.set_finished_sync_up_to( - last_change_height_cs(inner_state) + last_change_height_cs(inner_states[-1]) ) except Exception as e: tb = traceback.format_exc() @@ -634,7 +642,11 @@ async def receive_and_validate(inner_state: CoinState, inner_idx: int): nonlocal num_concurrent_tasks num_concurrent_tasks -= 1 # pylint: disable=E0602 - for idx, potential_state in enumerate(items): + idx = 1 + # Keep chunk size below 1000 just in case, windows has sqlite limits of 999 per query + # Untrusted has a smaller batch size since validation has to happen which takes a while + chunk_size: int = 900 if trusted else 10 + for states in chunks(items, chunk_size): if self.server is None: self.log.error("No server") return False @@ -642,9 +654,10 @@ async def receive_and_validate(inner_state: CoinState, inner_idx: int): self.log.error(f"Disconnected from peer {peer.peer_node_id} host {peer.peer_host}") return False while num_concurrent_tasks >= target_concurrent_tasks: - await asyncio.sleep(1) - all_tasks.append(asyncio.create_task(receive_and_validate(potential_state, idx))) + await asyncio.sleep(0.1) + all_tasks.append(asyncio.create_task(receive_and_validate(states, idx))) num_concurrent_tasks += 1 + idx += len(states) await asyncio.gather(*all_tasks) await self.update_ui() @@ -740,21 +753,19 @@ async def get_timestamp_for_height(self, height: uint32) -> uint64: return self.height_to_time[height] for cache in self.untrusted_caches.values(): - block: Optional[HeaderBlock] = cache.get_block(height) - if block is not None: - if ( - block.foliage_transaction_block is not None - and block.foliage_transaction_block.timestamp is not None - ): - self.height_to_time[height] = block.foliage_transaction_block.timestamp - return block.foliage_transaction_block.timestamp + cache_ts: Optional[uint64] = cache.get_height_timestamp(height) + if cache_ts is not None: + return cache_ts + peer: Optional[WSChiaConnection] = self.get_full_node_peer() if peer is None: raise ValueError("Cannot fetch timestamp, no peers") + self.log.debug(f"Fetching block at height: {height}") last_tx_block: Optional[HeaderBlock] = await fetch_last_tx_from_peer(height, peer) if last_tx_block is None: raise ValueError(f"Error fetching blocks from peer {peer.get_peer_info()}") assert last_tx_block.foliage_transaction_block is not None + self.get_cache_for_peer(peer).add_to_blocks(last_tx_block) return last_tx_block.foliage_transaction_block.timestamp async def new_peak_wallet(self, new_peak: wallet_protocol.NewPeakWallet, peer: WSChiaConnection): @@ -798,6 +809,8 @@ async def new_peak_wallet(self, new_peak: wallet_protocol.NewPeakWallet, peer: W # disconnected), we assume that the full node will continue to give us state updates, so we do # not need to resync. if peer.peer_node_id not in self.synced_peers: + if new_peak.height - current_height > self.LONG_SYNC_THRESHOLD: + self.wallet_state_manager.set_sync_mode(True) await self.long_sync(new_peak.height, peer, uint32(max(0, current_height - 256)), rollback=True) self.wallet_state_manager.set_sync_mode(False) else: @@ -914,7 +927,7 @@ async def new_peak_wallet(self, new_peak: wallet_protocol.NewPeakWallet, peer: W for potential_height in range(backtrack_fork_height + 1, new_peak.height + 1): header_hash = self.wallet_state_manager.blockchain.height_to_hash(uint32(potential_height)) if header_hash in self.race_cache: - self.log.debug(f"Receiving race state: {self.race_cache[header_hash]}") + self.log.info(f"Receiving race state: {self.race_cache[header_hash]}") await self.receive_state_from_peer(list(self.race_cache[header_hash]), peer) self.wallet_state_manager.state_changed("new_block") From f6af0227284ea54eee28f0a5c92748f148a85409 Mon Sep 17 00:00:00 2001 From: Gene Hoffman <30377676+hoffmang9@users.noreply.github.com> Date: Wed, 23 Feb 2022 00:39:31 -0800 Subject: [PATCH 115/378] Improve SQLite version string (#10387) --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 0850db57035a..7d5ae61796d6 100644 --- a/install.sh +++ b/install.sh @@ -224,7 +224,7 @@ echo "Python version is $INSTALL_PYTHON_VERSION" SQLITE_VERSION=$($INSTALL_PYTHON_PATH -c 'import sqlite3; print(sqlite3.sqlite_version)') SQLITE_MAJOR_VER=$(echo "$SQLITE_VERSION" | cut -d'.' -f1) SQLITE_MINOR_VER=$(echo "$SQLITE_VERSION" | cut -d'.' -f2) -echo "SQLite version of the Python is ${SQLITE_VERSION}" +echo "SQLite version for Python is ${SQLITE_VERSION}" if [ "$SQLITE_MAJOR_VER" -lt "3" ] || [ "$SQLITE_MAJOR_VER" = "3" ] && [ "$SQLITE_MINOR_VER" -lt "8" ]; then echo "Only sqlite>=3.8 is supported" exit 1 From d0ed4fb0d532f75809d4a034751263c5f37a5baf Mon Sep 17 00:00:00 2001 From: Mariano Sorgente <3069354+mariano54@users.noreply.github.com> Date: Wed, 23 Feb 2022 08:28:03 -0500 Subject: [PATCH 116/378] Ms.fix get transactions (#10380) * Increase limit from 50 to unlimited * Increase limit from 50 to unlimited, and allow configuring from CLI call * Use max uint32 --- chia/cmds/wallet.py | 12 +++++++++++- chia/cmds/wallet_funcs.py | 9 ++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/chia/cmds/wallet.py b/chia/cmds/wallet.py index c1079f594e55..bd1dff88cb3e 100644 --- a/chia/cmds/wallet.py +++ b/chia/cmds/wallet.py @@ -48,6 +48,15 @@ def get_transaction_cmd(wallet_rpc_port: Optional[int], fingerprint: int, id: in show_default=True, required=True, ) +@click.option( + "-l", + "--limit", + help="Max number of transactions to return", + type=int, + default=(2 ** 32 - 1), + show_default=True, + required=False, +) @click.option("--verbose", "-v", count=True, type=int) @click.option( "--paginate/--no-paginate", @@ -59,10 +68,11 @@ def get_transactions_cmd( fingerprint: int, id: int, offset: int, + limit: int, verbose: bool, paginate: Optional[bool], ) -> None: - extra_params = {"id": id, "verbose": verbose, "offset": offset, "paginate": paginate} + extra_params = {"id": id, "verbose": verbose, "offset": offset, "paginate": paginate, "limit": limit} import asyncio from .wallet_funcs import execute_with_wallet, get_transactions diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index b468b8531edb..9e7e69562e5a 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -111,7 +111,11 @@ async def get_transactions(args: dict, wallet_client: WalletRpcClient, fingerpri paginate = args["paginate"] if paginate is None: paginate = sys.stdout.isatty() - txs: List[TransactionRecord] = await wallet_client.get_transactions(wallet_id) + offset = args["offset"] + limit = args["limit"] + txs: List[TransactionRecord] = await wallet_client.get_transactions( + wallet_id, start=offset, end=(offset + limit), reverse=True + ) config = load_config(DEFAULT_ROOT_PATH, "config.yaml", SERVICE_NAME) address_prefix = config["network_overrides"]["config"][config["selected_network"]]["address_prefix"] if len(txs) == 0: @@ -130,9 +134,8 @@ async def get_transactions(args: dict, wallet_client: WalletRpcClient, fingerpri print(e.args[0]) return - offset = args["offset"] num_per_screen = 5 if paginate else len(txs) - for i in range(offset, len(txs), num_per_screen): + for i in range(0, len(txs), num_per_screen): for j in range(0, num_per_screen): if i + j >= len(txs): break From d5aacd87f8ece1bbe4fbfdf1f715a886e5ea2c14 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Wed, 23 Feb 2022 08:31:32 -0500 Subject: [PATCH 117/378] Stop launching more wallet receipt and validation tasks during shut down (#10384) * Stop launching more wallet receipt and validation tasks during shut down * info not error * less f-string --- chia/wallet/wallet_node.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index d3c52863b5b9..be3bd3065449 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -655,6 +655,9 @@ async def receive_and_validate(inner_states: List[CoinState], inner_idx_start: i return False while num_concurrent_tasks >= target_concurrent_tasks: await asyncio.sleep(0.1) + if self._shut_down: + self.log.info("Terminating receipt and validation due to shut down request") + return False all_tasks.append(asyncio.create_task(receive_and_validate(states, idx))) num_concurrent_tasks += 1 idx += len(states) From 8a2e876e45b89c01489a8f8de18dd7b6d6de3a3a Mon Sep 17 00:00:00 2001 From: joshpainter Date: Wed, 23 Feb 2022 09:11:24 -0600 Subject: [PATCH 118/378] Added CAT wallet name parameter (#10381) The create_wallet_for_cat method call accepts a "name" parameter for the new CAT being created, but we weren't passing that "name" variable here so all new wallets created via this method end up being named "CAT WALLET" (the default for name parameter in create_wallet_for_cat) even if you specified a wallet name here. --- chia/rpc/wallet_rpc_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index e3257fb23dbc..13fbe4561493 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -446,7 +446,7 @@ async def create_new_wallet(self, request: Dict): elif request["mode"] == "existing": async with self.service.wallet_state_manager.lock: cat_wallet = await CATWallet.create_wallet_for_cat( - wallet_state_manager, main_wallet, request["asset_id"] + wallet_state_manager, main_wallet, request["asset_id"], name ) self.service.wallet_state_manager.state_changed("wallet_created") return {"type": cat_wallet.type(), "asset_id": request["asset_id"], "wallet_id": cat_wallet.id()} From d80293e39006d63aa37ee4b8de157969ed062502 Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Wed, 23 Feb 2022 16:51:18 +0100 Subject: [PATCH 119/378] more test fixes (#10395) * WIP: fix more hard coded listen sockets in tests * correct comment * remove seemingly redundant event_loop fixtures (that sometimes seem to cause problems) * when a test fails, the error is the most important thing to print * fix mempool tests to me truly independent, by giving each test case a fresh block, and fresh reward coins to use for the test --- tests/core/full_node/test_mempool.py | 385 ++++++++++----------- tests/pools/test_pool_rpc.py | 4 +- tests/wallet/cat_wallet/test_cat_wallet.py | 6 - tests/wallet/did_wallet/test_did_rpc.py | 3 +- tests/wallet/test_wallet.py | 6 - 5 files changed, 187 insertions(+), 217 deletions(-) diff --git a/tests/core/full_node/test_mempool.py b/tests/core/full_node/test_mempool.py index 2f5d491bb012..2b60c35e178c 100644 --- a/tests/core/full_node/test_mempool.py +++ b/tests/core/full_node/test_mempool.py @@ -73,6 +73,9 @@ def generate_test_spend_bundle( return transaction +# this is here to avoid this error: +# ScopeMismatch: You tried to access the 'function' scoped fixture 'event_loop' +# with a 'module' scoped request object, involved factories @pytest.fixture(scope="module") def event_loop(): loop = asyncio.get_event_loop() @@ -80,11 +83,8 @@ def event_loop(): # TODO: this fixture should really be at function scope, to make all tests -# independent. Right now, TestMempool::test_basic_mempool initializes these -# nodes, and the remaining tests just build on top of them. This means -# test_basic_mempool must alwasy be run in order to have most of the other tests -# pass. -# The reason for this is that our simulators can't be destroyed correctly, which +# independent. +# The reason it isn't is that our simulators can't be destroyed correctly, which # means you can't instantiate more than one per process, so this is a hack until # that is fixed. For now, our tests are not independent @pytest_asyncio.fixture(scope="module") @@ -109,7 +109,7 @@ async def two_nodes(): await time_out_assert(60, node_height_at_least, True, full_node_1, blocks[-1].height) - yield full_node_1, full_node_2, server_1, server_2, blocks + yield full_node_1, full_node_2, server_1, server_2 async for _ in async_gen: yield _ @@ -194,7 +194,7 @@ class TestMempool: @pytest.mark.asyncio async def test_basic_mempool(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, blocks = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes max_mempool_cost = 40000000 * 5 mempool = Mempool(max_mempool_cost) @@ -203,7 +203,8 @@ async def test_basic_mempool(self, two_nodes): with pytest.raises(ValueError): mempool.get_min_fee_rate(max_mempool_cost + 1) - spend_bundle = generate_test_spend_bundle(list(blocks[-1].get_included_reward_coins())[0]) + coin = await next_block(full_node_1, WALLET_A, bt) + spend_bundle = generate_test_spend_bundle(coin) assert spend_bundle is not None @@ -230,14 +231,36 @@ async def respond_transaction( return await node.full_node.respond_transaction(tx.transaction, spend_name, peer, test) +async def next_block(full_node_1, wallet_a, bt) -> Coin: + blocks = await full_node_1.get_all_full_blocks() + # we have to farm a new block here, to ensure every test has a unique coin to test spending. + # all this could be simplified if the tests did not share a simulation + start_height = blocks[-1].height + reward_ph = wallet_a.get_new_puzzlehash() + blocks = bt.get_consecutive_blocks( + 1, + 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 + 1) + return list(blocks[-1].get_included_reward_coins())[0] + + class TestMempoolManager: @pytest.mark.asyncio async def test_basic_mempool_manager(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, blocks = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes peer = await connect_and_get_peer(server_1, server_2) - spend_bundle = generate_test_spend_bundle(list(blocks[-1].get_included_reward_coins())[0]) + coin = await next_block(full_node_1, WALLET_A, bt) + spend_bundle = generate_test_spend_bundle(coin) assert spend_bundle is not None tx: full_node_protocol.RespondTransaction = full_node_protocol.RespondTransaction(spend_bundle) await full_node_1.respond_transaction(tx, peer) @@ -291,7 +314,7 @@ def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: bundle = SpendBundle.aggregate([tx1, tx2]) return bundle - full_node_1, _, server_1, _, _ = two_nodes + full_node_1, _, server_1, _ = two_nodes blocks, bundle, status, err = await self.condition_tester2(two_nodes, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) @@ -322,13 +345,13 @@ def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: bundle = SpendBundle.aggregate([spend_bundle1, spend_bundle2]) return bundle - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes blocks, bundle, status, err = await self.condition_tester2(two_nodes, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) + assert err is None assert mempool_bundle is bundle assert status == MempoolInclusionStatus.SUCCESS - assert err is None # this test makes sure that one spend successfully asserts the announce from # another spend, even though the create announcement is duplicated 100 times @@ -346,18 +369,18 @@ def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: bundle = SpendBundle.aggregate([spend_bundle1, spend_bundle2]) return bundle - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes blocks, bundle, status, err = await self.condition_tester2(two_nodes, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) + assert err is None assert mempool_bundle is bundle assert status == MempoolInclusionStatus.SUCCESS - assert err is None @pytest.mark.asyncio async def test_double_spend(self, two_nodes): reward_ph = WALLET_A.get_new_puzzlehash() - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes blocks = await full_node_1.get_all_full_blocks() start_height = blocks[-1].height blocks = bt.get_consecutive_blocks( @@ -378,8 +401,8 @@ async def test_double_spend(self, two_nodes): assert spend_bundle1 is not None tx1: full_node_protocol.RespondTransaction = full_node_protocol.RespondTransaction(spend_bundle1) status, err = await respond_transaction(full_node_1, tx1, peer) - assert status == MempoolInclusionStatus.SUCCESS assert err is None + assert status == MempoolInclusionStatus.SUCCESS spend_bundle2 = generate_test_spend_bundle( list(blocks[-1].get_included_reward_coins())[0], @@ -392,10 +415,10 @@ async def test_double_spend(self, two_nodes): sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) sb2 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle2.name()) + assert err == Err.MEMPOOL_CONFLICT assert sb1 == spend_bundle1 assert sb2 is None assert status == MempoolInclusionStatus.PENDING - assert err == Err.MEMPOOL_CONFLICT async def send_sb(self, node: FullNodeAPI, sb: SpendBundle) -> Optional[Message]: tx = wallet_protocol.SendTransaction(sb) @@ -418,7 +441,7 @@ def assert_sb_not_in_pool(self, node, sb): async def test_double_spend_with_higher_fee(self, two_nodes): reward_ph = WALLET_A.get_new_puzzlehash() - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes 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( @@ -495,7 +518,7 @@ async def test_double_spend_with_higher_fee(self, two_nodes): async def test_invalid_signature(self, two_nodes): reward_ph = WALLET_A.get_new_puzzlehash() - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes 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( @@ -532,7 +555,7 @@ async def condition_tester( coin: Optional[Coin] = None, ): reward_ph = WALLET_A.get_new_puzzlehash() - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes blocks = await full_node_1.get_all_full_blocks() start_height = blocks[-1].height blocks = bt.get_consecutive_blocks( @@ -563,7 +586,7 @@ async def condition_tester( @pytest.mark.asyncio async def condition_tester2(self, two_nodes, test_fun: Callable[[Coin, Coin], SpendBundle]): reward_ph = WALLET_A.get_new_puzzlehash() - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes 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( @@ -593,7 +616,7 @@ async def condition_tester2(self, two_nodes, test_fun: Callable[[Coin, Coin], Sp @pytest.mark.asyncio async def test_invalid_block_index(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes blocks = await full_node_1.get_all_full_blocks() start_height = blocks[-1].height cvp = ConditionWithArgs( @@ -605,13 +628,13 @@ async def test_invalid_block_index(self, two_nodes): sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert sb1 is None # the transaction may become valid later - assert status == MempoolInclusionStatus.PENDING assert err == Err.ASSERT_HEIGHT_ABSOLUTE_FAILED + assert status == MempoolInclusionStatus.PENDING @pytest.mark.asyncio async def test_block_index_missing_arg(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes blocks = await full_node_1.get_all_full_blocks() cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, []) dic = {ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE: [cvp]} @@ -619,177 +642,174 @@ async def test_block_index_missing_arg(self, two_nodes): sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert sb1 is None # the transaction may become valid later - assert status == MempoolInclusionStatus.FAILED assert err == Err.INVALID_CONDITION + assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio async def test_correct_block_index(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, [int_to_bytes(1)]) dic = {ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) + assert err is None assert sb1 is spend_bundle1 assert status == MempoolInclusionStatus.SUCCESS - assert err is None @pytest.mark.asyncio async def test_block_index_garbage(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes # garbage at the end of the argument list is ignored cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, [int_to_bytes(1), b"garbage"]) dic = {ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) + assert err is None assert sb1 is spend_bundle1 assert status == MempoolInclusionStatus.SUCCESS - assert err is None @pytest.mark.asyncio async def test_negative_block_index(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, [int_to_bytes(-1)]) dic = {ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) + assert err is None assert sb1 is spend_bundle1 assert status == MempoolInclusionStatus.SUCCESS - assert err is None @pytest.mark.asyncio async def test_invalid_block_age(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_RELATIVE, [int_to_bytes(5)]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) + assert err == Err.ASSERT_HEIGHT_RELATIVE_FAILED assert sb1 is None # the transaction may become valid later assert status == MempoolInclusionStatus.PENDING - assert err == Err.ASSERT_HEIGHT_RELATIVE_FAILED @pytest.mark.asyncio async def test_block_age_missing_arg(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_RELATIVE, []) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) + assert err == Err.INVALID_CONDITION assert sb1 is None # the transaction may become valid later assert status == MempoolInclusionStatus.FAILED - assert err == Err.INVALID_CONDITION @pytest.mark.asyncio async def test_correct_block_age(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_RELATIVE, [int_to_bytes(1)]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, num_blocks=4) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) + assert err is None assert sb1 is spend_bundle1 assert status == MempoolInclusionStatus.SUCCESS - assert err is None @pytest.mark.asyncio async def test_block_age_garbage(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes # garbage at the end of the argument list is ignored cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_RELATIVE, [int_to_bytes(1), b"garbage"]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, num_blocks=4) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) + assert err is None assert sb1 is spend_bundle1 assert status == MempoolInclusionStatus.SUCCESS - assert err is None @pytest.mark.asyncio async def test_negative_block_age(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_RELATIVE, [int_to_bytes(-1)]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, num_blocks=4) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) + assert err is None assert sb1 is spend_bundle1 assert status == MempoolInclusionStatus.SUCCESS - assert err is None @pytest.mark.asyncio async def test_correct_my_id(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes - blocks = await full_node_1.get_all_full_blocks() - coin = list(blocks[-1].get_included_reward_coins())[0] + full_node_1, full_node_2, server_1, server_2 = two_nodes + coin = await next_block(full_node_1, WALLET_A, bt) cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_COIN_ID, [coin.name()]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, coin=coin) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) + assert err is None assert sb1 is spend_bundle1 assert status == MempoolInclusionStatus.SUCCESS - assert err is None @pytest.mark.asyncio async def test_my_id_garbage(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes - blocks = await full_node_1.get_all_full_blocks() - coin = list(blocks[-1].get_included_reward_coins())[0] + full_node_1, full_node_2, server_1, server_2 = two_nodes + coin = await next_block(full_node_1, WALLET_A, bt) # garbage at the end of the argument list is ignored cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_COIN_ID, [coin.name(), b"garbage"]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, coin=coin) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) + assert err is None assert sb1 is spend_bundle1 assert status == MempoolInclusionStatus.SUCCESS - assert err is None @pytest.mark.asyncio async def test_invalid_my_id(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes - blocks = await full_node_1.get_all_full_blocks() - coin = list(blocks[-1].get_included_reward_coins())[0] - coin_2 = list(blocks[-2].get_included_reward_coins())[0] + full_node_1, full_node_2, server_1, server_2 = two_nodes + coin = await next_block(full_node_1, WALLET_A, bt) + coin_2 = await next_block(full_node_1, WALLET_A, bt) cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_COIN_ID, [coin_2.name()]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, coin=coin) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) + assert err == Err.ASSERT_MY_COIN_ID_FAILED assert sb1 is None assert status == MempoolInclusionStatus.FAILED - assert err == Err.ASSERT_MY_COIN_ID_FAILED @pytest.mark.asyncio async def test_my_id_missing_arg(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes blocks = await full_node_1.get_all_full_blocks() cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_COIN_ID, []) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) + assert err == Err.INVALID_CONDITION assert sb1 is None assert status == MempoolInclusionStatus.FAILED - assert err == Err.INVALID_CONDITION @pytest.mark.asyncio async def test_assert_time_exceeds(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes # 5 seconds should be before the next block time_now = full_node_1.full_node.blockchain.get_peak().timestamp + 5 @@ -797,28 +817,28 @@ async def test_assert_time_exceeds(self, two_nodes): dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) + assert err is None assert sb1 is spend_bundle1 assert status == MempoolInclusionStatus.SUCCESS - assert err is None @pytest.mark.asyncio async def test_assert_time_fail(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes time_now = full_node_1.full_node.blockchain.get_peak().timestamp + 1000 cvp = ConditionWithArgs(ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, [int_to_bytes(time_now)]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) + assert err == Err.ASSERT_SECONDS_ABSOLUTE_FAILED assert sb1 is None assert status == MempoolInclusionStatus.FAILED - assert err == Err.ASSERT_SECONDS_ABSOLUTE_FAILED @pytest.mark.asyncio async def test_assert_height_pending(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes print(full_node_1.full_node.blockchain.get_peak()) current_height = full_node_1.full_node.blockchain.get_peak().height @@ -826,41 +846,41 @@ async def test_assert_height_pending(self, two_nodes): dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) + assert err == Err.ASSERT_HEIGHT_ABSOLUTE_FAILED assert sb1 is None assert status == MempoolInclusionStatus.PENDING - assert err == Err.ASSERT_HEIGHT_ABSOLUTE_FAILED @pytest.mark.asyncio async def test_assert_time_negative(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes time_now = -1 cvp = ConditionWithArgs(ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, [int_to_bytes(time_now)]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) + assert err is None assert sb1 is spend_bundle1 assert status == MempoolInclusionStatus.SUCCESS - assert err is None @pytest.mark.asyncio async def test_assert_time_missing_arg(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes cvp = ConditionWithArgs(ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, []) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) + assert err == Err.INVALID_CONDITION assert sb1 is None assert status == MempoolInclusionStatus.FAILED - assert err == Err.INVALID_CONDITION @pytest.mark.asyncio async def test_assert_time_garbage(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes time_now = full_node_1.full_node.blockchain.get_peak().timestamp + 5 # garbage at the end of the argument list is ignored @@ -868,14 +888,14 @@ async def test_assert_time_garbage(self, two_nodes): dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) + assert err is None assert sb1 is spend_bundle1 assert status == MempoolInclusionStatus.SUCCESS - assert err is None @pytest.mark.asyncio async def test_assert_time_relative_exceeds(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes time_relative = 3 cvp = ConditionWithArgs(ConditionOpcode.ASSERT_SECONDS_RELATIVE, [int_to_bytes(time_relative)]) @@ -883,9 +903,9 @@ async def test_assert_time_relative_exceeds(self, two_nodes): blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) + assert err == Err.ASSERT_SECONDS_RELATIVE_FAILED assert sb1 is None assert status == MempoolInclusionStatus.FAILED - assert err == Err.ASSERT_SECONDS_RELATIVE_FAILED for i in range(0, 4): await full_node_1.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0")) @@ -895,14 +915,14 @@ async def test_assert_time_relative_exceeds(self, two_nodes): status, err = await respond_transaction(full_node_1, tx2, peer) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) + assert err is None assert sb1 is spend_bundle1 assert status == MempoolInclusionStatus.SUCCESS - assert err is None @pytest.mark.asyncio async def test_assert_time_relative_garbage(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes time_relative = 0 # garbage at the end of the arguments is ignored @@ -911,28 +931,28 @@ async def test_assert_time_relative_garbage(self, two_nodes): blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) + assert err is None assert sb1 is spend_bundle1 assert status == MempoolInclusionStatus.SUCCESS - assert err is None @pytest.mark.asyncio async def test_assert_time_relative_missing_arg(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes cvp = ConditionWithArgs(ConditionOpcode.ASSERT_SECONDS_RELATIVE, []) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) + assert err == Err.INVALID_CONDITION assert sb1 is None assert status == MempoolInclusionStatus.FAILED - assert err == Err.INVALID_CONDITION @pytest.mark.asyncio async def test_assert_time_relative_negative(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes time_relative = -3 cvp = ConditionWithArgs(ConditionOpcode.ASSERT_SECONDS_RELATIVE, [int_to_bytes(time_relative)]) @@ -940,9 +960,9 @@ async def test_assert_time_relative_negative(self, two_nodes): blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) + assert err is None assert sb1 is spend_bundle1 assert status == MempoolInclusionStatus.SUCCESS - assert err is None # ensure one spend can assert a coin announcement from another spend @pytest.mark.asyncio @@ -959,13 +979,13 @@ def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: bundle = SpendBundle.aggregate([spend_bundle1, spend_bundle2]) return bundle - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes blocks, bundle, status, err = await self.condition_tester2(two_nodes, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) + assert err is None assert mempool_bundle is bundle assert status == MempoolInclusionStatus.SUCCESS - assert err is None # ensure one spend can assert a coin announcement from another spend, even # though the conditions have garbage (ignored) at the end @@ -985,17 +1005,17 @@ def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: bundle = SpendBundle.aggregate([spend_bundle1, spend_bundle2]) return bundle - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes blocks, bundle, status, err = await self.condition_tester2(two_nodes, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) + assert err is None assert mempool_bundle is bundle assert status == MempoolInclusionStatus.SUCCESS - assert err is None @pytest.mark.asyncio async def test_coin_announcement_missing_arg(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes def test_fun(coin_1: Coin, coin_2: Coin): # missing arg here @@ -1010,13 +1030,13 @@ def test_fun(coin_1: Coin, coin_2: Coin): blocks, bundle, status, err = await self.condition_tester2(two_nodes, test_fun) + assert err == Err.INVALID_CONDITION assert full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) is None assert status == MempoolInclusionStatus.FAILED - assert err == Err.INVALID_CONDITION @pytest.mark.asyncio async def test_coin_announcement_missing_arg2(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes def test_fun(coin_1: Coin, coin_2: Coin): announce = Announcement(coin_2.name(), b"test") @@ -1032,13 +1052,13 @@ def test_fun(coin_1: Coin, coin_2: Coin): blocks, bundle, status, err = await self.condition_tester2(two_nodes, test_fun) + assert err == Err.INVALID_CONDITION assert full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) is None assert status == MempoolInclusionStatus.FAILED - assert err == Err.INVALID_CONDITION @pytest.mark.asyncio async def test_coin_announcement_too_big(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes def test_fun(coin_1: Coin, coin_2: Coin): announce = Announcement(coin_2.name(), bytes([1] * 10000)) @@ -1056,9 +1076,9 @@ def test_fun(coin_1: Coin, coin_2: Coin): blocks, bundle, status, err = await self.condition_tester2(two_nodes, test_fun) + assert err == Err.ASSERT_ANNOUNCE_CONSUMED_FAILED assert full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) is None assert status == MempoolInclusionStatus.FAILED - assert err == Err.ASSERT_ANNOUNCE_CONSUMED_FAILED blocks = bt.get_consecutive_blocks( 1, block_list_input=blocks, guarantee_transaction_block=True, transaction_data=bundle @@ -1073,7 +1093,7 @@ def test_fun(coin_1: Coin, coin_2: Coin): # create announcement @pytest.mark.asyncio async def test_invalid_coin_announcement_rejected(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes def test_fun(coin_1: Coin, coin_2: Coin): announce = Announcement(coin_2.name(), b"test") @@ -1096,13 +1116,13 @@ def test_fun(coin_1: Coin, coin_2: Coin): mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) + assert err == Err.ASSERT_ANNOUNCE_CONSUMED_FAILED assert mempool_bundle is None assert status == MempoolInclusionStatus.FAILED - assert err == Err.ASSERT_ANNOUNCE_CONSUMED_FAILED @pytest.mark.asyncio async def test_invalid_coin_announcement_rejected_two(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes def test_fun(coin_1: Coin, coin_2: Coin): announce = Announcement(coin_1.name(), b"test") @@ -1122,13 +1142,13 @@ def test_fun(coin_1: Coin, coin_2: Coin): blocks, bundle, status, err = await self.condition_tester2(two_nodes, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) + assert err == Err.ASSERT_ANNOUNCE_CONSUMED_FAILED assert mempool_bundle is None assert status == MempoolInclusionStatus.FAILED - assert err == Err.ASSERT_ANNOUNCE_CONSUMED_FAILED @pytest.mark.asyncio async def test_correct_puzzle_announcement(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes def test_fun(coin_1: Coin, coin_2: Coin): announce = Announcement(coin_2.puzzle_hash, bytes(0x80)) @@ -1148,13 +1168,13 @@ def test_fun(coin_1: Coin, coin_2: Coin): mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) + assert err is None assert mempool_bundle is bundle assert status == MempoolInclusionStatus.SUCCESS - assert err is None @pytest.mark.asyncio async def test_puzzle_announcement_garbage(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes def test_fun(coin_1: Coin, coin_2: Coin): announce = Announcement(coin_2.puzzle_hash, bytes(0x80)) @@ -1173,13 +1193,13 @@ def test_fun(coin_1: Coin, coin_2: Coin): blocks, bundle, status, err = await self.condition_tester2(two_nodes, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) + assert err is None assert mempool_bundle is bundle assert status == MempoolInclusionStatus.SUCCESS - assert err is None @pytest.mark.asyncio async def test_puzzle_announcement_missing_arg(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes def test_fun(coin_1: Coin, coin_2: Coin): # missing arg here @@ -1199,13 +1219,13 @@ def test_fun(coin_1: Coin, coin_2: Coin): mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) + assert err == Err.INVALID_CONDITION assert mempool_bundle is None assert status == MempoolInclusionStatus.FAILED - assert err == Err.INVALID_CONDITION @pytest.mark.asyncio async def test_puzzle_announcement_missing_arg2(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes def test_fun(coin_1: Coin, coin_2: Coin): announce = Announcement(coin_2.puzzle_hash, b"test") @@ -1227,13 +1247,13 @@ def test_fun(coin_1: Coin, coin_2: Coin): mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) + assert err == Err.INVALID_CONDITION assert mempool_bundle is None assert status == MempoolInclusionStatus.FAILED - assert err == Err.INVALID_CONDITION @pytest.mark.asyncio async def test_invalid_puzzle_announcement_rejected(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes def test_fun(coin_1: Coin, coin_2: Coin): announce = Announcement(coin_2.puzzle_hash, bytes("test", "utf-8")) @@ -1256,13 +1276,13 @@ def test_fun(coin_1: Coin, coin_2: Coin): mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) + assert err == Err.ASSERT_ANNOUNCE_CONSUMED_FAILED assert mempool_bundle is None assert status == MempoolInclusionStatus.FAILED - assert err == Err.ASSERT_ANNOUNCE_CONSUMED_FAILED @pytest.mark.asyncio async def test_invalid_puzzle_announcement_rejected_two(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes def test_fun(coin_1: Coin, coin_2: Coin): announce = Announcement(coin_2.puzzle_hash, b"test") @@ -1285,54 +1305,54 @@ def test_fun(coin_1: Coin, coin_2: Coin): mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) + assert err == Err.ASSERT_ANNOUNCE_CONSUMED_FAILED assert mempool_bundle is None assert status == MempoolInclusionStatus.FAILED - assert err == Err.ASSERT_ANNOUNCE_CONSUMED_FAILED @pytest.mark.asyncio async def test_assert_fee_condition(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes cvp = ConditionWithArgs(ConditionOpcode.RESERVE_FEE, [int_to_bytes(10)]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, fee=10) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) + assert err is None assert mempool_bundle is not None assert status == MempoolInclusionStatus.SUCCESS - assert err is None @pytest.mark.asyncio async def test_assert_fee_condition_garbage(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes # garbage at the end of the arguments is ignored cvp = ConditionWithArgs(ConditionOpcode.RESERVE_FEE, [int_to_bytes(10), b"garbage"]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, fee=10) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) + assert err is None assert mempool_bundle is not None assert status == MempoolInclusionStatus.SUCCESS - assert err is None @pytest.mark.asyncio async def test_assert_fee_condition_missing_arg(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes cvp = ConditionWithArgs(ConditionOpcode.RESERVE_FEE, []) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, fee=10) - assert status == MempoolInclusionStatus.FAILED assert err == Err.INVALID_CONDITION + assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio async def test_assert_fee_condition_negative_fee(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes cvp = ConditionWithArgs(ConditionOpcode.RESERVE_FEE, [int_to_bytes(-1)]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, fee=10) - assert status == MempoolInclusionStatus.FAILED assert err == Err.RESERVE_FEE_CONDITION_FAILED + assert status == MempoolInclusionStatus.FAILED blocks = bt.get_consecutive_blocks( 1, block_list_input=blocks, guarantee_transaction_block=True, transaction_data=spend_bundle1 ) @@ -1343,12 +1363,12 @@ async def test_assert_fee_condition_negative_fee(self, two_nodes): @pytest.mark.asyncio async def test_assert_fee_condition_fee_too_large(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes cvp = ConditionWithArgs(ConditionOpcode.RESERVE_FEE, [int_to_bytes(2 ** 64)]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, fee=10) - assert status == MempoolInclusionStatus.FAILED assert err == Err.RESERVE_FEE_CONDITION_FAILED + assert status == MempoolInclusionStatus.FAILED blocks = bt.get_consecutive_blocks( 1, block_list_input=blocks, guarantee_transaction_block=True, transaction_data=spend_bundle1 ) @@ -1360,21 +1380,21 @@ async def test_assert_fee_condition_fee_too_large(self, two_nodes): @pytest.mark.asyncio async def test_assert_fee_condition_wrong_fee(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes cvp = ConditionWithArgs(ConditionOpcode.RESERVE_FEE, [int_to_bytes(10)]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, fee=9) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) + assert err == Err.RESERVE_FEE_CONDITION_FAILED assert mempool_bundle is None assert status == MempoolInclusionStatus.FAILED - assert err == Err.RESERVE_FEE_CONDITION_FAILED @pytest.mark.asyncio async def test_stealing_fee(self, two_nodes): reward_ph = WALLET_A.get_new_puzzlehash() - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes blocks = await full_node_1.get_all_full_blocks() start_height = blocks[-1].height blocks = bt.get_consecutive_blocks( @@ -1385,7 +1405,7 @@ async def test_stealing_fee(self, two_nodes): pool_reward_puzzle_hash=reward_ph, ) - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes peer = await connect_and_get_peer(server_1, server_2) for block in blocks: @@ -1424,30 +1444,15 @@ async def test_stealing_fee(self, two_nodes): mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) + assert err == Err.RESERVE_FEE_CONDITION_FAILED assert mempool_bundle is None assert status == MempoolInclusionStatus.FAILED - assert err == Err.RESERVE_FEE_CONDITION_FAILED @pytest.mark.asyncio async def test_double_spend_same_bundle(self, two_nodes): - reward_ph = WALLET_A.get_new_puzzlehash() - full_node_1, full_node_2, server_1, server_2, _ = two_nodes - blocks = await full_node_1.get_all_full_blocks() - start_height = blocks[-1].height - 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, - ) - peer = await connect_and_get_peer(server_1, server_2) + full_node_1, full_node_2, server_1, server_2 = two_nodes - 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) - coin = list(blocks[-1].get_included_reward_coins())[0] + coin = await next_block(full_node_1, WALLET_A, bt) spend_bundle1 = generate_test_spend_bundle(coin) assert spend_bundle1 is not None @@ -1463,36 +1468,20 @@ async def test_double_spend_same_bundle(self, two_nodes): tx: full_node_protocol.RespondTransaction = full_node_protocol.RespondTransaction(spend_bundle_combined) + peer = await connect_and_get_peer(server_1, server_2) status, err = await respond_transaction(full_node_1, tx, peer) sb = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle_combined.name()) + assert err == Err.DOUBLE_SPEND assert sb is None assert status == MempoolInclusionStatus.FAILED - assert err == Err.DOUBLE_SPEND @pytest.mark.asyncio async def test_agg_sig_condition(self, two_nodes): - reward_ph = WALLET_A.get_new_puzzlehash() - full_node_1, full_node_2, server_1, server_2, _ = two_nodes - blocks = await full_node_1.get_all_full_blocks() - start_height = blocks[-1].height - 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) + full_node_1, full_node_2, server_1, server_2 = two_nodes - # this code has been changed to use generate_test_spend_bundle - # not quite sure why all the gymnastics are being performed + coin = await next_block(full_node_1, WALLET_A, bt) - coin = list(blocks[-1].get_included_reward_coins())[0] spend_bundle_0 = generate_test_spend_bundle(coin) unsigned: List[CoinSpend] = spend_bundle_0.coin_spends @@ -1523,25 +1512,23 @@ async def test_agg_sig_condition(self, two_nodes): @pytest.mark.asyncio async def test_correct_my_parent(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes - blocks = await full_node_1.get_all_full_blocks() - coin = list(blocks[-1].get_included_reward_coins())[0] + full_node_1, full_node_2, server_1, server_2 = two_nodes + coin = await next_block(full_node_1, WALLET_A, bt) cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_PARENT_ID, [coin.parent_coin_info]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, coin=coin) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) + assert err is None assert sb1 is spend_bundle1 assert status == MempoolInclusionStatus.SUCCESS - assert err is None @pytest.mark.asyncio async def test_my_parent_garbage(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes - blocks = await full_node_1.get_all_full_blocks() - coin = list(blocks[-1].get_included_reward_coins())[0] + full_node_1, full_node_2, server_1, server_2 = two_nodes + coin = await next_block(full_node_1, WALLET_A, bt) # garbage at the end of the arguments list is allowed but stripped cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_PARENT_ID, [coin.parent_coin_info, b"garbage"]) dic = {cvp.opcode: [cvp]} @@ -1549,14 +1536,14 @@ async def test_my_parent_garbage(self, two_nodes): sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) + assert err is None assert sb1 is spend_bundle1 assert status == MempoolInclusionStatus.SUCCESS - assert err is None @pytest.mark.asyncio async def test_my_parent_missing_arg(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes blocks = await full_node_1.get_all_full_blocks() cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_PARENT_ID, []) dic = {cvp.opcode: [cvp]} @@ -1564,49 +1551,46 @@ async def test_my_parent_missing_arg(self, two_nodes): sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) + assert err == Err.INVALID_CONDITION assert sb1 is None assert status == MempoolInclusionStatus.FAILED - assert err == Err.INVALID_CONDITION @pytest.mark.asyncio async def test_invalid_my_parent(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes - blocks = await full_node_1.get_all_full_blocks() - coin = list(blocks[-1].get_included_reward_coins())[0] - coin_2 = list(blocks[-2].get_included_reward_coins())[0] + full_node_1, full_node_2, server_1, server_2 = two_nodes + coin = await next_block(full_node_1, WALLET_A, bt) + coin_2 = await next_block(full_node_1, WALLET_A, bt) cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_PARENT_ID, [coin_2.parent_coin_info]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, coin=coin) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) + assert err == Err.ASSERT_MY_PARENT_ID_FAILED assert sb1 is None assert status == MempoolInclusionStatus.FAILED - assert err == Err.ASSERT_MY_PARENT_ID_FAILED @pytest.mark.asyncio async def test_correct_my_puzhash(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes - blocks = await full_node_1.get_all_full_blocks() - coin = list(blocks[-1].get_included_reward_coins())[0] + full_node_1, full_node_2, server_1, server_2 = two_nodes + coin = await next_block(full_node_1, WALLET_A, bt) cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_PUZZLEHASH, [coin.puzzle_hash]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, coin=coin) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) + assert err is None assert sb1 is spend_bundle1 assert status == MempoolInclusionStatus.SUCCESS - assert err is None @pytest.mark.asyncio async def test_my_puzhash_garbage(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes - blocks = await full_node_1.get_all_full_blocks() - coin = list(blocks[-1].get_included_reward_coins())[0] + full_node_1, full_node_2, server_1, server_2 = two_nodes + coin = await next_block(full_node_1, WALLET_A, bt) # garbage at the end of the arguments list is allowed but stripped cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_PUZZLEHASH, [coin.puzzle_hash, b"garbage"]) dic = {cvp.opcode: [cvp]} @@ -1614,14 +1598,14 @@ async def test_my_puzhash_garbage(self, two_nodes): sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) + assert err is None assert sb1 is spend_bundle1 assert status == MempoolInclusionStatus.SUCCESS - assert err is None @pytest.mark.asyncio async def test_my_puzhash_missing_arg(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes blocks = await full_node_1.get_all_full_blocks() cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_PUZZLEHASH, []) dic = {cvp.opcode: [cvp]} @@ -1629,48 +1613,45 @@ async def test_my_puzhash_missing_arg(self, two_nodes): sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) + assert err == Err.INVALID_CONDITION assert sb1 is None assert status == MempoolInclusionStatus.FAILED - assert err == Err.INVALID_CONDITION @pytest.mark.asyncio async def test_invalid_my_puzhash(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes - blocks = await full_node_1.get_all_full_blocks() - coin = list(blocks[-1].get_included_reward_coins())[0] + full_node_1, full_node_2, server_1, server_2 = two_nodes + coin = await next_block(full_node_1, WALLET_A, bt) cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_PUZZLEHASH, [Program.to([]).get_tree_hash()]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, coin=coin) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) + assert err == Err.ASSERT_MY_PUZZLEHASH_FAILED assert sb1 is None assert status == MempoolInclusionStatus.FAILED - assert err == Err.ASSERT_MY_PUZZLEHASH_FAILED @pytest.mark.asyncio async def test_correct_my_amount(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes - blocks = await full_node_1.get_all_full_blocks() - coin = list(blocks[-1].get_included_reward_coins())[0] + full_node_1, full_node_2, server_1, server_2 = two_nodes + coin = await next_block(full_node_1, WALLET_A, bt) cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_AMOUNT, [int_to_bytes(coin.amount)]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, coin=coin) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) + assert err is None assert sb1 is spend_bundle1 assert status == MempoolInclusionStatus.SUCCESS - assert err is None @pytest.mark.asyncio async def test_my_amount_garbage(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes - blocks = await full_node_1.get_all_full_blocks() - coin = list(blocks[-1].get_included_reward_coins())[0] + full_node_1, full_node_2, server_1, server_2 = two_nodes + coin = await next_block(full_node_1, WALLET_A, bt) # garbage at the end of the arguments list is allowed but stripped cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_AMOUNT, [int_to_bytes(coin.amount), b"garbage"]) dic = {cvp.opcode: [cvp]} @@ -1678,14 +1659,14 @@ async def test_my_amount_garbage(self, two_nodes): sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) + assert err is None assert sb1 is spend_bundle1 assert status == MempoolInclusionStatus.SUCCESS - assert err is None @pytest.mark.asyncio async def test_my_amount_missing_arg(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes blocks = await full_node_1.get_all_full_blocks() cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_AMOUNT, []) dic = {cvp.opcode: [cvp]} @@ -1693,14 +1674,14 @@ async def test_my_amount_missing_arg(self, two_nodes): sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) + assert err == Err.INVALID_CONDITION assert sb1 is None assert status == MempoolInclusionStatus.FAILED - assert err == Err.INVALID_CONDITION @pytest.mark.asyncio async def test_invalid_my_amount(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes blocks = await full_node_1.get_all_full_blocks() cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_AMOUNT, [int_to_bytes(1000)]) dic = {cvp.opcode: [cvp]} @@ -1708,14 +1689,14 @@ async def test_invalid_my_amount(self, two_nodes): sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) + assert err == Err.ASSERT_MY_AMOUNT_FAILED assert sb1 is None assert status == MempoolInclusionStatus.FAILED - assert err == Err.ASSERT_MY_AMOUNT_FAILED @pytest.mark.asyncio async def test_negative_my_amount(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes blocks = await full_node_1.get_all_full_blocks() cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_AMOUNT, [int_to_bytes(-1)]) dic = {cvp.opcode: [cvp]} @@ -1723,14 +1704,14 @@ async def test_negative_my_amount(self, two_nodes): sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) + assert err == Err.ASSERT_MY_AMOUNT_FAILED assert sb1 is None assert status == MempoolInclusionStatus.FAILED - assert err == Err.ASSERT_MY_AMOUNT_FAILED @pytest.mark.asyncio async def test_my_amount_too_large(self, two_nodes): - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes blocks = await full_node_1.get_all_full_blocks() cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_AMOUNT, [int_to_bytes(2 ** 64)]) dic = {cvp.opcode: [cvp]} @@ -1738,9 +1719,9 @@ async def test_my_amount_too_large(self, two_nodes): sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) + assert err == Err.ASSERT_MY_AMOUNT_FAILED assert sb1 is None assert status == MempoolInclusionStatus.FAILED - assert err == Err.ASSERT_MY_AMOUNT_FAILED # the following tests generate generator programs and run them through get_name_puzzle_conditions() @@ -2379,7 +2360,7 @@ async def test_invalid_coin_spend_coin(self, two_nodes): farmer_reward_puzzle_hash=reward_ph, pool_reward_puzzle_hash=reward_ph, ) - full_node_1, full_node_2, server_1, server_2, _ = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes for block in blocks: await full_node_1.full_node.respond_block(full_node_protocol.RespondBlock(block)) diff --git a/tests/pools/test_pool_rpc.py b/tests/pools/test_pool_rpc.py index 99e175e08c08..a5db79ca994f 100644 --- a/tests/pools/test_pool_rpc.py +++ b/tests/pools/test_pool_rpc.py @@ -104,7 +104,7 @@ async def one_wallet_node_and_rpc(self): config = bt.config hostname = config["self_hostname"] daemon_port = config["daemon_port"] - test_rpc_port = find_available_listen_port() + test_rpc_port = find_available_listen_port("rpc_port") rpc_cleanup = await start_rpc_server( api_user, @@ -138,7 +138,7 @@ async def setup(self, two_wallet_nodes): config = bt.config hostname = config["self_hostname"] daemon_port = config["daemon_port"] - test_rpc_port = uint16(21529) + test_rpc_port = find_available_listen_port("rpc_port") rpc_cleanup = await start_rpc_server( api_user, diff --git a/tests/wallet/cat_wallet/test_cat_wallet.py b/tests/wallet/cat_wallet/test_cat_wallet.py index af3a5112a662..3239f87dcafa 100644 --- a/tests/wallet/cat_wallet/test_cat_wallet.py +++ b/tests/wallet/cat_wallet/test_cat_wallet.py @@ -23,12 +23,6 @@ from tests.time_out_assert import time_out_assert -@pytest.fixture(scope="module") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - async def tx_in_pool(mempool: MempoolManager, tx_id: bytes32): tx = mempool.get_spendbundle(tx_id) if tx is None: diff --git a/tests/wallet/did_wallet/test_did_rpc.py b/tests/wallet/did_wallet/test_did_rpc.py index 1d5d617b5e9b..af5473fb01d3 100644 --- a/tests/wallet/did_wallet/test_did_rpc.py +++ b/tests/wallet/did_wallet/test_did_rpc.py @@ -13,6 +13,7 @@ from tests.setup_nodes import self_hostname, setup_simulators_and_wallets, bt from tests.time_out_assert import time_out_assert from chia.wallet.did_wallet.did_wallet import DIDWallet +from tests.util.socket import find_available_listen_port log = logging.getLogger(__name__) @@ -57,7 +58,7 @@ async def test_create_did(self, three_wallet_nodes): api_one = WalletRpcApi(wallet_node_0) config = bt.config daemon_port = config["daemon_port"] - test_rpc_port = uint16(21529) + test_rpc_port = uint16(find_available_listen_port("rpc_port")) await wallet_server_0.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) client = await WalletRpcClient.create(self_hostname, test_rpc_port, bt.root_path, bt.config) rpc_server_cleanup = await start_rpc_server( diff --git a/tests/wallet/test_wallet.py b/tests/wallet/test_wallet.py index 8d45d7e43418..887d74c4840c 100644 --- a/tests/wallet/test_wallet.py +++ b/tests/wallet/test_wallet.py @@ -21,12 +21,6 @@ from tests.wallet.cat_wallet.test_cat_wallet import tx_in_pool -@pytest.fixture(scope="module") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - class TestWalletSimulator: @pytest_asyncio.fixture(scope="function") async def wallet_node(self): From c82840eb6925b76a9714745f67df730e137ae3da Mon Sep 17 00:00:00 2001 From: Chris Marslender Date: Wed, 23 Feb 2022 17:44:59 -0600 Subject: [PATCH 120/378] Fix s3 destination for intel dev installers (#10403) --- .github/workflows/build-macos-installer.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-macos-installer.yml b/.github/workflows/build-macos-installer.yml index 4025a4424063..56ab55481224 100644 --- a/.github/workflows/build-macos-installer.yml +++ b/.github/workflows/build-macos-installer.yml @@ -170,7 +170,7 @@ jobs: GIT_SHORT_HASH=$(echo "${GITHUB_SHA}" | cut -c1-8) CHIA_DEV_BUILD=${CHIA_INSTALLER_VERSION}-$GIT_SHORT_HASH echo "CHIA_DEV_BUILD=$CHIA_DEV_BUILD" >>$GITHUB_ENV - aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg s3://download.chia.net/builds/Chia-${CHIA_DEV_BUILD}.dmg + aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg s3://download.chia.net/dev/Chia-${CHIA_DEV_BUILD}.dmg - name: Install py3createtorrent if: startsWith(github.ref, 'refs/tags/') From 1c96b21b7cf6ce3d1e76860fc771304c18782921 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Wed, 23 Feb 2022 21:20:52 -0500 Subject: [PATCH 121/378] Add pytest to mypy pre-commit environment (#10397) * Add pytest to mypy pre-commit environment * Update .pre-commit-config.yaml --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fdaba8fc8346..c0891660bd13 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -57,7 +57,7 @@ repos: rev: v0.930 hooks: - id: mypy - additional_dependencies: [types-aiofiles, types-click, types-setuptools, types-PyYAML] + additional_dependencies: [pytest, pytest-asyncio, types-aiofiles, types-click, types-setuptools, types-PyYAML] # This intentionally counters the settings in mypy.ini to allow a loose local # check and a strict CI check. This difference may or may not be retained long # term. From 2ba0b9f41db9c03b8811c4b4eadb1d6597c7d216 Mon Sep 17 00:00:00 2001 From: dustinface <35775977+xdustinface@users.noreply.github.com> Date: Thu, 24 Feb 2022 04:29:50 +0100 Subject: [PATCH 122/378] farmer: Log `POST`/`PUT` requests data with `DEBUG` log level (#10405) --- chia/farmer/farmer.py | 4 ++-- chia/farmer/farmer_api.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/chia/farmer/farmer.py b/chia/farmer/farmer.py index 1e1f8bb7a77a..1b3221ac2f83 100644 --- a/chia/farmer/farmer.py +++ b/chia/farmer/farmer.py @@ -347,7 +347,7 @@ async def _pool_post_farmer( assert owner_sk.get_g1() == pool_config.owner_public_key signature: G2Element = AugSchemeMPL.sign(owner_sk, post_farmer_payload.get_hash()) post_farmer_request = PostFarmerRequest(post_farmer_payload, signature) - + self.log.debug(f"POST /farmer request {post_farmer_request}") try: async with aiohttp.ClientSession() as session: async with session.post( @@ -387,7 +387,7 @@ async def _pool_put_farmer( assert owner_sk.get_g1() == pool_config.owner_public_key signature: G2Element = AugSchemeMPL.sign(owner_sk, put_farmer_payload.get_hash()) put_farmer_request = PutFarmerRequest(put_farmer_payload, signature) - + self.log.debug(f"PUT /farmer request {put_farmer_request}") try: async with aiohttp.ClientSession() as session: async with session.put( diff --git a/chia/farmer/farmer_api.py b/chia/farmer/farmer_api.py index ce323690e8ee..d0cb11577e15 100644 --- a/chia/farmer/farmer_api.py +++ b/chia/farmer/farmer_api.py @@ -230,7 +230,7 @@ async def new_proof_of_space( ) pool_state_dict["points_found_since_start"] += pool_state_dict["current_difficulty"] pool_state_dict["points_found_24h"].append((time.time(), pool_state_dict["current_difficulty"])) - + self.farmer.log.debug(f"POST /partial request {post_partial_request}") try: async with aiohttp.ClientSession() as session: async with session.post( From 55c4eb3d289bcb99f92da259ed9b1d1d5b8d589c Mon Sep 17 00:00:00 2001 From: dustinface <35775977+xdustinface@users.noreply.github.com> Date: Thu, 24 Feb 2022 14:46:28 +0100 Subject: [PATCH 123/378] farmer: Send a `PUT /farmer` if the signature verification failed (#10364) Due to the bug which was fixed in #9922 some pools obviously still have incorrect authentication public keys from their farmers. This PR is to make sure that the farmers update their authentication public key on the pool if they get an `INVALID_SIGNATURE` response error from a `GET /farmer`. --- chia/farmer/farmer.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/chia/farmer/farmer.py b/chia/farmer/farmer.py index 1b3221ac2f83..a540f06b9ff3 100644 --- a/chia/farmer/farmer.py +++ b/chia/farmer/farmer.py @@ -476,13 +476,13 @@ async def update_pool_state(self): if time.time() >= pool_state["next_farmer_update"]: authentication_token_timeout = pool_state["authentication_token_timeout"] - async def update_pool_farmer_info() -> Tuple[Optional[GetFarmerResponse], Optional[bool]]: + async def update_pool_farmer_info() -> Tuple[Optional[GetFarmerResponse], Optional[PoolErrorCode]]: # Run a GET /farmer to see if the farmer is already known by the pool response = await self._pool_get_farmer( pool_config, authentication_token_timeout, authentication_sk ) farmer_response: Optional[GetFarmerResponse] = None - farmer_known: Optional[bool] = None + error_code_response: Optional[PoolErrorCode] = None if response is not None: if "error_code" not in response: farmer_response = GetFarmerResponse.from_json_dict(response) @@ -491,17 +491,23 @@ async def update_pool_farmer_info() -> Tuple[Optional[GetFarmerResponse], Option pool_state["current_points"] = farmer_response.current_points pool_state["next_farmer_update"] = time.time() + UPDATE_POOL_FARMER_INFO_INTERVAL else: - farmer_known = response["error_code"] != PoolErrorCode.FARMER_NOT_KNOWN.value + try: + error_code_response = PoolErrorCode(response["error_code"]) + except ValueError: + self.log.error( + f"Invalid error code received from the pool: {response['error_code']}" + ) + self.log.error( "update_pool_farmer_info failed: " f"{response['error_code']}, {response['error_message']}" ) - return farmer_response, farmer_known + return farmer_response, error_code_response if authentication_token_timeout is not None: - farmer_info, farmer_is_known = await update_pool_farmer_info() - if farmer_info is None and farmer_is_known is not None and not farmer_is_known: + farmer_info, error_code = await update_pool_farmer_info() + if error_code == PoolErrorCode.FARMER_NOT_KNOWN: # Make the farmer known on the pool with a POST /farmer owner_sk_and_index: Optional[PrivateKey, uint32] = find_owner_sk( self.all_root_sks, pool_config.owner_public_key @@ -520,11 +526,13 @@ async def update_pool_farmer_info() -> Tuple[Optional[GetFarmerResponse], Option if farmer_info is None and not farmer_is_known: self.log.error("Failed to update farmer info after POST /farmer.") - # Update the payout instructions on the pool if required - if ( + # Update the farmer information on the pool if the payout instructions changed or if the + # signature is invalid (latter to make sure the pool has the correct authentication public key). + payout_instructions_update_required: bool = ( farmer_info is not None and pool_config.payout_instructions.lower() != farmer_info.payout_instructions.lower() - ): + ) + if payout_instructions_update_required or error_code == PoolErrorCode.INVALID_SIGNATURE: owner_sk_and_index: Optional[PrivateKey, uint32] = find_owner_sk( self.all_root_sks, pool_config.owner_public_key ) From cdf48f7b29ce915e43974baaaa71ce8bb4dcd650 Mon Sep 17 00:00:00 2001 From: dustinface <35775977+xdustinface@users.noreply.github.com> Date: Thu, 24 Feb 2022 19:19:57 +0100 Subject: [PATCH 124/378] benchmarks: Implement benchmarks for streamable (#10388) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * benchmarks: Implement benchmarks for streamable * benchmarks: Collect iterations per time instead of time per iterations * benchmarks: Add standard deviation to streamable benchs * benchmarks: Add ns/iteration to streamable benchs * benchmarks: Move object creation out or the runs loop * benchmarks: Use `click.Choice` for `--data` and `--mode` * benchmarks: Its µ * benchmarks: Improve logging * benchmarks: Drop unused code * benchmarks: Use `process_time` as clock * benchmarks: Add stdev `us/iterations %` + more precission * benchmarks: Add `--live/--no-live` option to enable live results --- benchmarks/block_store.py | 5 +- benchmarks/streamable.py | 228 ++++++++++++++++++++++++++++++++++++++ benchmarks/utils.py | 109 ++++++++++++++++++ 3 files changed, 338 insertions(+), 4 deletions(-) create mode 100644 benchmarks/streamable.py diff --git a/benchmarks/block_store.py b/benchmarks/block_store.py index 2190412898a4..4169bf50ab06 100644 --- a/benchmarks/block_store.py +++ b/benchmarks/block_store.py @@ -6,6 +6,7 @@ import os import sys +from benchmarks.utils import clvm_generator from chia.util.db_wrapper import DBWrapper from chia.util.ints import uint128, uint64, uint32, uint8 from utils import ( @@ -36,10 +37,6 @@ random.seed(123456789) -with open("clvm_generator.bin", "rb") as f: - clvm_generator = f.read() - - async def run_add_block_benchmark(version: int): verbose: bool = "--verbose" in sys.argv diff --git a/benchmarks/streamable.py b/benchmarks/streamable.py new file mode 100644 index 000000000000..135420202972 --- /dev/null +++ b/benchmarks/streamable.py @@ -0,0 +1,228 @@ +from dataclasses import dataclass +from enum import Enum +from statistics import stdev +from time import process_time as clock +from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union + +import click +from utils import EnumType, rand_bytes, rand_full_block, rand_hash + +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.full_block import FullBlock +from chia.util.ints import uint8, uint64 +from chia.util.streamable import Streamable, streamable + + +@dataclass(frozen=True) +@streamable +class BenchmarkInner(Streamable): + a: str + + +@dataclass(frozen=True) +@streamable +class BenchmarkMiddle(Streamable): + a: uint64 + b: List[bytes32] + c: Tuple[str, bool, uint8, List[bytes]] + d: Tuple[BenchmarkInner, BenchmarkInner] + e: BenchmarkInner + + +@dataclass(frozen=True) +@streamable +class BenchmarkClass(Streamable): + a: Optional[BenchmarkMiddle] + b: Optional[BenchmarkMiddle] + c: BenchmarkMiddle + d: List[BenchmarkMiddle] + e: Tuple[BenchmarkMiddle, BenchmarkMiddle, BenchmarkMiddle] + + +def get_random_inner() -> BenchmarkInner: + return BenchmarkInner(rand_bytes(20).hex()) + + +def get_random_middle() -> BenchmarkMiddle: + a: uint64 = uint64(10) + b: List[bytes32] = [rand_hash() for _ in range(a)] + c: Tuple[str, bool, uint8, List[bytes]] = ("benchmark", False, uint8(1), [rand_bytes(a) for _ in range(a)]) + d: Tuple[BenchmarkInner, BenchmarkInner] = (get_random_inner(), get_random_inner()) + e: BenchmarkInner = get_random_inner() + return BenchmarkMiddle(a, b, c, d, e) + + +def get_random_benchmark_object() -> BenchmarkClass: + a: Optional[BenchmarkMiddle] = None + b: Optional[BenchmarkMiddle] = get_random_middle() + c: BenchmarkMiddle = get_random_middle() + d: List[BenchmarkMiddle] = [get_random_middle() for _ in range(5)] + e: Tuple[BenchmarkMiddle, BenchmarkMiddle, BenchmarkMiddle] = ( + get_random_middle(), + get_random_middle(), + get_random_middle(), + ) + return BenchmarkClass(a, b, c, d, e) + + +def print_row( + *, + mode: str, + us_per_iteration: Union[str, float], + stdev_us_per_iteration: Union[str, float], + avg_iterations: Union[str, int], + stdev_iterations: Union[str, float], + end: str = "\n", +) -> None: + mode = "{0:<10}".format(f"{mode}") + us_per_iteration = "{0:<12}".format(f"{us_per_iteration}") + stdev_us_per_iteration = "{0:>20}".format(f"{stdev_us_per_iteration}") + avg_iterations = "{0:>18}".format(f"{avg_iterations}") + stdev_iterations = "{0:>22}".format(f"{stdev_iterations}") + print(f"{mode} | {us_per_iteration} | {stdev_us_per_iteration} | {avg_iterations} | {stdev_iterations}", end=end) + + +# The strings in this Enum are by purpose. See benchmark.utils.EnumType. +class Data(str, Enum): + all = "all" + benchmark = "benchmark" + full_block = "full_block" + + +# The strings in this Enum are by purpose. See benchmark.utils.EnumType. +class Mode(str, Enum): + all = "all" + creation = "creation" + to_bytes = "to_bytes" + from_bytes = "from_bytes" + to_json = "to_json" + from_json = "from_json" + + +def to_bytes(obj: Any) -> bytes: + return bytes(obj) + + +@dataclass +class ModeParameter: + conversion_cb: Callable[[Any], Any] + preparation_cb: Optional[Callable[[Any], Any]] = None + + +@dataclass +class BenchmarkParameter: + data_class: Type[Any] + object_creation_cb: Callable[[], Any] + mode_parameter: Dict[Mode, Optional[ModeParameter]] + + +benchmark_parameter: Dict[Data, BenchmarkParameter] = { + Data.benchmark: BenchmarkParameter( + BenchmarkClass, + get_random_benchmark_object, + { + Mode.creation: None, + Mode.to_bytes: ModeParameter(to_bytes), + Mode.from_bytes: ModeParameter(BenchmarkClass.from_bytes, to_bytes), + Mode.to_json: ModeParameter(BenchmarkClass.to_json_dict), + Mode.from_json: ModeParameter(BenchmarkClass.from_json_dict, BenchmarkClass.to_json_dict), + }, + ), + Data.full_block: BenchmarkParameter( + FullBlock, + rand_full_block, + { + Mode.creation: None, + Mode.to_bytes: ModeParameter(to_bytes), + Mode.from_bytes: ModeParameter(FullBlock.from_bytes, to_bytes), + Mode.to_json: ModeParameter(FullBlock.to_json_dict), + Mode.from_json: ModeParameter(FullBlock.from_json_dict, FullBlock.to_json_dict), + }, + ), +} + + +def run_for_ms(cb: Callable[[], Any], ms_to_run: int = 100) -> List[int]: + us_iteration_results: List[int] = [] + start = clock() + while int((clock() - start) * 1000) < ms_to_run: + start_iteration = clock() + cb() + stop_iteration = clock() + us_iteration_results.append(int((stop_iteration - start_iteration) * 1000 * 1000)) + return us_iteration_results + + +def calc_stdev_percent(iterations: List[int], avg: float) -> float: + deviation = 0 if len(iterations) < 2 else int(stdev(iterations) * 100) / 100 + return int((deviation / avg * 100) * 100) / 100 + + +@click.command() +@click.option("-d", "--data", default=Data.all, type=EnumType(Data)) +@click.option("-m", "--mode", default=Mode.all, type=EnumType(Mode)) +@click.option("-r", "--runs", default=100, help="Number of benchmark runs to average results") +@click.option("-t", "--ms", default=50, help="Milliseconds per run") +@click.option("--live/--no-live", default=False, help="Print live results (slower)") +def run(data: Data, mode: Mode, runs: int, ms: int, live: bool) -> None: + results: Dict[Data, Dict[Mode, List[List[int]]]] = {} + for current_data, parameter in benchmark_parameter.items(): + results[current_data] = {} + if data == Data.all or current_data == data: + print(f"\nruns: {runs}, ms/run: {ms}, benchmarks: {mode.name}, data: {parameter.data_class.__name__}") + print_row( + mode="mode", + us_per_iteration="µs/iteration", + stdev_us_per_iteration="stdev µs/iteration %", + avg_iterations="avg iterations/run", + stdev_iterations="stdev iterations/run %", + ) + for current_mode, current_mode_parameter in parameter.mode_parameter.items(): + results[current_data][current_mode] = [] + if mode == Mode.all or current_mode == mode: + us_iteration_results: List[int] + all_results: List[List[int]] = results[current_data][current_mode] + obj = parameter.object_creation_cb() + + def print_results(print_run: int, final: bool) -> None: + all_runtimes: List[int] = [x for inner in all_results for x in inner] + total_iterations: int = len(all_runtimes) + total_elapsed_us: int = sum(all_runtimes) + avg_iterations: float = total_iterations / print_run + stdev_iterations: float = calc_stdev_percent([len(x) for x in all_results], avg_iterations) + stdev_us_per_iteration: float = calc_stdev_percent( + all_runtimes, total_elapsed_us / total_iterations + ) + print_row( + mode=current_mode.name, + us_per_iteration=int(total_elapsed_us / total_iterations * 100) / 100, + stdev_us_per_iteration=stdev_us_per_iteration, + avg_iterations=int(avg_iterations), + stdev_iterations=stdev_iterations, + end="\n" if final else "\r", + ) + + current_run: int = 0 + while current_run < runs: + current_run += 1 + + if current_mode == Mode.creation: + cls = type(obj) + us_iteration_results = run_for_ms(lambda: cls(**obj.__dict__), ms) + else: + assert current_mode_parameter is not None + conversion_cb = current_mode_parameter.conversion_cb + assert conversion_cb is not None + prepared_obj = parameter.object_creation_cb() + if current_mode_parameter.preparation_cb is not None: + prepared_obj = current_mode_parameter.preparation_cb(obj) + us_iteration_results = run_for_ms(lambda: conversion_cb(prepared_obj), ms) + all_results.append(us_iteration_results) + if live: + print_results(current_run, False) + assert current_run == runs + print_results(runs, True) + + +if __name__ == "__main__": + run() # pylint: disable = no-value-for-parameter diff --git a/benchmarks/utils.py b/benchmarks/utils.py index 4ecf9fb98939..3f3604bd750a 100644 --- a/benchmarks/utils.py +++ b/benchmarks/utils.py @@ -5,11 +5,19 @@ from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.blockchain_format.vdf import VDFInfo, VDFProof +from chia.types.blockchain_format.foliage import Foliage, FoliageBlockData, FoliageTransactionBlock, TransactionsInfo +from chia.types.blockchain_format.pool_target import PoolTarget +from chia.types.blockchain_format.program import SerializedProgram +from chia.types.blockchain_format.proof_of_space import ProofOfSpace +from chia.types.blockchain_format.reward_chain_block import RewardChainBlock +from chia.types.full_block import FullBlock +from chia.util.ints import uint128 from chia.util.db_wrapper import DBWrapper from typing import Tuple from pathlib import Path from datetime import datetime import aiosqlite +import click import os import sys import random @@ -18,6 +26,20 @@ # farmer puzzle hash ph = bytes32(b"a" * 32) +with open(Path(os.path.realpath(__file__)).parent / "clvm_generator.bin", "rb") as f: + clvm_generator = f.read() + + +# Workaround to allow `Enum` with click.Choice: https://github.com/pallets/click/issues/605#issuecomment-901099036 +class EnumType(click.Choice): + def __init__(self, enum, case_sensitive=False): + self.__enum = enum + super().__init__(choices=[item.value for item in enum], case_sensitive=case_sensitive) + + def convert(self, value, param, ctx): + converted_str = super().convert(value, param, ctx) + return self.__enum(converted_str) + def rewards(height: uint32) -> Tuple[Coin, Coin]: farmer_coin = create_farmer_coin(height, ph, uint64(250000000), DEFAULT_CONSTANTS.GENESIS_CHALLENGE) @@ -66,6 +88,93 @@ def rand_vdf_proof() -> VDFProof: ) +def rand_full_block() -> FullBlock: + proof_of_space = ProofOfSpace( + rand_hash(), + rand_g1(), + None, + rand_g1(), + uint8(0), + rand_bytes(8 * 32), + ) + + reward_chain_block = RewardChainBlock( + uint128(1), + uint32(2), + uint128(3), + uint8(4), + rand_hash(), + proof_of_space, + None, + rand_g2(), + rand_vdf(), + None, + rand_g2(), + rand_vdf(), + rand_vdf(), + True, + ) + + pool_target = PoolTarget( + rand_hash(), + uint32(0), + ) + + foliage_block_data = FoliageBlockData( + rand_hash(), + pool_target, + rand_g2(), + rand_hash(), + rand_hash(), + ) + + foliage = Foliage( + rand_hash(), + rand_hash(), + foliage_block_data, + rand_g2(), + rand_hash(), + rand_g2(), + ) + + foliage_transaction_block = FoliageTransactionBlock( + rand_hash(), + uint64(0), + rand_hash(), + rand_hash(), + rand_hash(), + rand_hash(), + ) + + farmer_coin, pool_coin = rewards(uint32(0)) + + transactions_info = TransactionsInfo( + rand_hash(), + rand_hash(), + rand_g2(), + uint64(0), + uint64(1), + [farmer_coin, pool_coin], + ) + + full_block = FullBlock( + [], + reward_chain_block, + rand_vdf_proof(), + rand_vdf_proof(), + rand_vdf_proof(), + rand_vdf_proof(), + rand_vdf_proof(), + foliage, + foliage_transaction_block, + transactions_info, + SerializedProgram.from_bytes(clvm_generator), + [], + ) + + return full_block + + async def setup_db(name: str, db_version: int) -> DBWrapper: db_filename = Path(name) try: From 89740234e75ccdbe438b8c7c7546b7db1a4e831c Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 24 Feb 2022 23:08:06 -0500 Subject: [PATCH 125/378] spawn (not fork) for multiprocessing (#10322) * spawn (not fork) for multiprocessing * just append _worker to existing process names * return properly in getproctitle() * black * ignore for unhinted getproctitle() * Add comments about the setting of the multiprocessing start method --- chia/__init__.py | 19 +++++++++++++++++++ chia/consensus/blockchain.py | 7 ++++++- chia/full_node/mempool_manager.py | 3 ++- chia/full_node/weight_proof.py | 7 ++++++- chia/timelord/timelord.py | 7 ++++++- chia/util/setproctitle.py | 8 ++++++++ chia/wallet/wallet_weight_proof_handler.py | 7 ++++++- tests/conftest.py | 4 ++++ 8 files changed, 57 insertions(+), 5 deletions(-) diff --git a/chia/__init__.py b/chia/__init__.py index c136c9c3d63f..9c099f5d0b48 100644 --- a/chia/__init__.py +++ b/chia/__init__.py @@ -1,5 +1,24 @@ +import multiprocessing + from pkg_resources import DistributionNotFound, get_distribution, resource_filename +# The default multiprocessing start method on Linux has resulted in various issues. +# Several have been around resources being inherited by the worker processes resulting +# in ports, files, or streams, being held open unexpectedly. This can also affect +# memory used by the subprocesses and such. + +start_method = "spawn" +try: + # Set the start method. This may already have been done by the test suite. + multiprocessing.set_start_method(start_method) +except RuntimeError: + # Setting can fail if it has already been done. We do not care about the failure + # if the start method is what we want it to be anyways. + if multiprocessing.get_start_method(allow_none=True) != start_method: + # The start method is not what we wanted. We do not want to continue with + # this without further consideration. + raise + try: __version__ = get_distribution("chia-blockchain").version except DistributionNotFound: diff --git a/chia/consensus/blockchain.py b/chia/consensus/blockchain.py index b57ae02a717a..24780850c5fd 100644 --- a/chia/consensus/blockchain.py +++ b/chia/consensus/blockchain.py @@ -47,6 +47,7 @@ from chia.util.errors import ConsensusError, Err from chia.util.generator_tools import get_block_header, tx_removals_and_additions from chia.util.ints import uint16, uint32, uint64, uint128 +from chia.util.setproctitle import getproctitle, setproctitle from chia.util.streamable import recurse_jsonify log = logging.getLogger(__name__) @@ -117,7 +118,11 @@ async def create( if cpu_count > 61: cpu_count = 61 # Windows Server 2016 has an issue https://bugs.python.org/issue26903 num_workers = max(cpu_count - reserved_cores, 1) - self.pool = ProcessPoolExecutor(max_workers=num_workers) + self.pool = ProcessPoolExecutor( + max_workers=num_workers, + initializer=setproctitle, + initargs=(f"{getproctitle()}_worker",), + ) log.info(f"Started {num_workers} processes for block validation") self.constants = consensus_constants diff --git a/chia/full_node/mempool_manager.py b/chia/full_node/mempool_manager.py index 2996f81b8b17..048086939b8b 100644 --- a/chia/full_node/mempool_manager.py +++ b/chia/full_node/mempool_manager.py @@ -33,6 +33,7 @@ from chia.util.generator_tools import additions_for_npc from chia.util.ints import uint32, uint64 from chia.util.lru_cache import LRUCache +from chia.util.setproctitle import getproctitle, setproctitle from chia.util.streamable import recurse_jsonify log = logging.getLogger(__name__) @@ -98,7 +99,7 @@ def __init__(self, coin_store: CoinStore, consensus_constants: ConsensusConstant # Transactions that were unable to enter mempool, used for retry. (they were invalid) self.potential_cache = PendingTxCache(self.constants.MAX_BLOCK_COST_CLVM * 1) self.seen_cache_size = 10000 - self.pool = ProcessPoolExecutor(max_workers=2) + self.pool = ProcessPoolExecutor(max_workers=2, initializer=setproctitle, initargs=(f"{getproctitle()}_worker",)) # The mempool will correspond to a certain peak self.peak: Optional[BlockRecord] = None diff --git a/chia/full_node/weight_proof.py b/chia/full_node/weight_proof.py index 0f20eb1d2b7a..c6d1ad7ab6e7 100644 --- a/chia/full_node/weight_proof.py +++ b/chia/full_node/weight_proof.py @@ -40,6 +40,7 @@ from chia.util.block_cache import BlockCache from chia.util.hash import std_hash from chia.util.ints import uint8, uint32, uint64, uint128 +from chia.util.setproctitle import getproctitle, setproctitle from chia.util.streamable import dataclass_from_dict, recurse_jsonify log = logging.getLogger(__name__) @@ -621,7 +622,11 @@ async def validate_weight_proof(self, weight_proof: WeightProof) -> Tuple[bool, # timing reference: 1 second # TODO: Consider implementing an async polling closer for the executor. - with ProcessPoolExecutor(max_workers=self._num_processes) as executor: + with ProcessPoolExecutor( + max_workers=self._num_processes, + initializer=setproctitle, + initargs=(f"{getproctitle()}_worker",), + ) as executor: # The shutdown file manager must be inside of the executor manager so that # we request the workers close prior to waiting for them to close. with _create_shutdown_file() as shutdown_file: diff --git a/chia/timelord/timelord.py b/chia/timelord/timelord.py index f92d6acf0435..253b94408941 100644 --- a/chia/timelord/timelord.py +++ b/chia/timelord/timelord.py @@ -33,6 +33,7 @@ from chia.types.blockchain_format.vdf import VDFInfo, VDFProof from chia.types.end_of_slot_bundle import EndOfSubSlotBundle from chia.util.ints import uint8, uint16, uint32, uint64, uint128 +from chia.util.setproctitle import getproctitle, setproctitle from chia.util.streamable import Streamable, streamable log = logging.getLogger(__name__) @@ -132,7 +133,11 @@ async def _start(self): if os.name == "nt" or slow_bluebox: # `vdf_client` doesn't build on windows, use `prove()` from chiavdf. workers = self.config.get("slow_bluebox_process_count", 1) - self.bluebox_pool = ProcessPoolExecutor(max_workers=workers) + self.bluebox_pool = ProcessPoolExecutor( + max_workers=workers, + initializer=setproctitle, + initargs=(f"{getproctitle()}_worker",), + ) self.main_loop = asyncio.create_task( self._start_manage_discriminant_queue_sanitizer_slow(self.bluebox_pool, workers) ) diff --git a/chia/util/setproctitle.py b/chia/util/setproctitle.py index 6f4364f969ba..beb68c53225d 100644 --- a/chia/util/setproctitle.py +++ b/chia/util/setproctitle.py @@ -9,3 +9,11 @@ def setproctitle(ps_name: str) -> None: if no_setproctitle is False: pysetproctitle.setproctitle(ps_name) + + +def getproctitle() -> str: + if no_setproctitle is False: + # TODO: add type hints to setproctitle + return pysetproctitle.getproctitle() # type: ignore[no-any-return] + + return "" diff --git a/chia/wallet/wallet_weight_proof_handler.py b/chia/wallet/wallet_weight_proof_handler.py index d8b03300b128..6d62bad39e1a 100644 --- a/chia/wallet/wallet_weight_proof_handler.py +++ b/chia/wallet/wallet_weight_proof_handler.py @@ -24,6 +24,7 @@ ) from chia.util.ints import uint32 +from chia.util.setproctitle import getproctitle, setproctitle log = logging.getLogger(__name__) @@ -45,7 +46,11 @@ def __init__( self._constants = constants self._num_processes = 4 self._executor_shutdown_tempfile: IO = _create_shutdown_file() - self._executor: ProcessPoolExecutor = ProcessPoolExecutor(self._num_processes) + self._executor: ProcessPoolExecutor = ProcessPoolExecutor( + self._num_processes, + initializer=setproctitle, + initargs=(f"{getproctitle()}_worker",), + ) self._weight_proof_tasks: List[asyncio.Task] = [] def cancel_weight_proof_tasks(self): diff --git a/tests/conftest.py b/tests/conftest.py index dcd91b3e8008..e7bb78dad5e0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,9 +1,13 @@ +import multiprocessing import pytest import pytest_asyncio import tempfile from pathlib import Path +multiprocessing.set_start_method("spawn") + + # TODO: tests.setup_nodes (which is also imported by tests.util.blockchain) creates a # global BlockTools at tests.setup_nodes.bt. This results in an attempt to create # the chia root directory which the build scripts symlink to a sometimes-not-there From 0cf7dafbc12d441cec111c8c289cdafe5c251be9 Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Thu, 24 Feb 2022 21:56:00 -0700 Subject: [PATCH 126/378] Check that offer coins exist as well as not spent (#10409) * Check that offer coins exist as well as not spent * It shouldn't be an assertion * Remove assert --- chia/wallet/trade_manager.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/chia/wallet/trade_manager.py b/chia/wallet/trade_manager.py index 6c8303ce3126..b66acc9898bc 100644 --- a/chia/wallet/trade_manager.py +++ b/chia/wallet/trade_manager.py @@ -353,8 +353,7 @@ async def check_offer_validity(self, offer: Offer) -> bool: coin_states = await self.wallet_state_manager.wallet_node.get_coin_state( [c.name() for c in non_ephemeral_removals] ) - assert coin_states is not None - return not any([cs.spent_height is not None for cs in coin_states]) + return len(coin_states) == len(non_ephemeral_removals) and all([cs.spent_height is None for cs in coin_states]) async def calculate_tx_records_for_offer(self, offer: Offer, validate: bool) -> List[TransactionRecord]: if validate: From 0772f45c132bdf20cfd824b0ae3fefa4fe959a7c Mon Sep 17 00:00:00 2001 From: Gene Hoffman <30377676+hoffmang9@users.noreply.github.com> Date: Thu, 24 Feb 2022 20:56:23 -0800 Subject: [PATCH 127/378] Only show -Trusted when a node is trusted (#10404) --- chia/cmds/show.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/chia/cmds/show.py b/chia/cmds/show.py index 20e47d33959a..ed03ef72b7e7 100644 --- a/chia/cmds/show.py +++ b/chia/cmds/show.py @@ -40,9 +40,12 @@ async def print_connections(client, time, NodeType, trusted_peers: Dict): f"\n " ) if peak_height is not None: - con_str += f"-Height: {peak_height:8.0f} -Hash: {connection_peak_hash} -Trusted: {trusted}" + con_str += f"-Height: {peak_height:8.0f} -Hash: {connection_peak_hash}" else: - con_str += f"-Height: No Info -Hash: {connection_peak_hash} -Trusted: {trusted}" + con_str += f"-Height: No Info -Hash: {connection_peak_hash}" + # Only show when Trusted is True + if trusted: + con_str += f" -Trusted: {trusted}" else: con_str = ( f"{NodeType(con['type']).name:9} {host:38} " From a98d02d943250936e96c4017bdfb9c4c2e13c9b4 Mon Sep 17 00:00:00 2001 From: "xchdata.io" <93948614+xchdata1@users.noreply.github.com> Date: Fri, 25 Feb 2022 05:56:47 +0100 Subject: [PATCH 128/378] Fix pool puzzle comment typo (#10402) --- chia/pools/pool_puzzles.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chia/pools/pool_puzzles.py b/chia/pools/pool_puzzles.py index 0568e868d6f5..fb74c96503df 100644 --- a/chia/pools/pool_puzzles.py +++ b/chia/pools/pool_puzzles.py @@ -169,7 +169,7 @@ def create_travel_spend( if is_pool_member_inner_puzzle(inner_puzzle): # inner sol is key_value_list () # key_value_list is: - # "ps" -> poolstate as bytes + # "p" -> poolstate as bytes inner_sol: Program = Program.to([[("p", bytes(target))], 0]) elif is_pool_waitingroom_inner_puzzle(inner_puzzle): # inner sol is (spend_type, key_value_list, pool_reward_height) @@ -182,7 +182,7 @@ def create_travel_spend( f"hash:{Program.to(bytes(target)).get_tree_hash()}" ) # key_value_list is: - # "ps" -> poolstate as bytes + # "p" -> poolstate as bytes inner_sol = Program.to([1, [("p", bytes(target))], destination_inner.get_tree_hash()]) # current or target else: raise ValueError From b349c817fd5d563fb555233b444eff7917d6dc2b Mon Sep 17 00:00:00 2001 From: Mariano Sorgente <3069354+mariano54@users.noreply.github.com> Date: Thu, 24 Feb 2022 23:57:28 -0500 Subject: [PATCH 129/378] Log the correct error (#10399) --- chia/pools/pool_wallet.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/chia/pools/pool_wallet.py b/chia/pools/pool_wallet.py index c676420177c5..0df09ed9a592 100644 --- a/chia/pools/pool_wallet.py +++ b/chia/pools/pool_wallet.py @@ -271,9 +271,13 @@ async def apply_state_transition(self, new_state: CoinSpend, block_height: uint3 spent_coin_name: bytes32 = tip_coin.name() if spent_coin_name != new_state.coin.name(): - self.log.warning( - f"Failed to apply state transition. tip: {tip_coin} new_state: {new_state} height {block_height}" - ) + history: List[Tuple[uint32, CoinSpend]] = await self.get_spend_history() + if new_state.coin.name() in [sp.coin.name() for _, sp in history]: + self.log.info(f"Already have state transition: {new_state.coin.name()}") + else: + self.log.warning( + f"Failed to apply state transition. tip: {tip_coin} new_state: {new_state} height {block_height}" + ) return False await self.wallet_state_manager.pool_store.add_spend(self.wallet_id, new_state, block_height) From 7f3b61ddb46c1e5895ce685f38bcc44ba568ee4a Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Fri, 25 Feb 2022 05:58:06 +0100 Subject: [PATCH 130/378] slight simplification of interpreting the bytes received by the timelord. avoid redundant round-trips to strings (#10316) --- chia/timelord/timelord.py | 164 ++++++++++++++++++-------------------- 1 file changed, 79 insertions(+), 85 deletions(-) diff --git a/chia/timelord/timelord.py b/chia/timelord/timelord.py index 253b94408941..e74bd998b83c 100644 --- a/chia/timelord/timelord.py +++ b/chia/timelord/timelord.py @@ -958,100 +958,94 @@ async def _do_process_communication( self.vdf_failures_count += 1 break - msg = "" - try: - msg = data.decode() - except Exception: - pass - if msg == "STOP": + if data == b"STOP": log.debug(f"Stopped client running on ip {ip}.") async with self.lock: writer.write(b"ACK") await writer.drain() break - else: - try: - # This must be a proof, 4 bytes is length prefix - length = int.from_bytes(data, "big") - proof = await reader.readexactly(length) - stdout_bytes_io: io.BytesIO = io.BytesIO(bytes.fromhex(proof.decode())) - except ( - asyncio.IncompleteReadError, - ConnectionResetError, - Exception, - ) as e: - log.warning(f"{type(e)} {e}") - async with self.lock: - self.vdf_failures.append((chain, proof_label)) - self.vdf_failures_count += 1 - break + try: + # This must be a proof, 4 bytes is length prefix + length = int.from_bytes(data, "big") + proof = await reader.readexactly(length) + stdout_bytes_io: io.BytesIO = io.BytesIO(bytes.fromhex(proof.decode())) + except ( + asyncio.IncompleteReadError, + ConnectionResetError, + Exception, + ) as e: + log.warning(f"{type(e)} {e}") + async with self.lock: + self.vdf_failures.append((chain, proof_label)) + self.vdf_failures_count += 1 + break - iterations_needed = uint64(int.from_bytes(stdout_bytes_io.read(8), "big", signed=True)) - - y_size_bytes = stdout_bytes_io.read(8) - y_size = uint64(int.from_bytes(y_size_bytes, "big", signed=True)) - - y_bytes = stdout_bytes_io.read(y_size) - witness_type = uint8(int.from_bytes(stdout_bytes_io.read(1), "big", signed=True)) - proof_bytes: bytes = stdout_bytes_io.read() - - # Verifies our own proof just in case - form_size = ClassgroupElement.get_size(self.constants) - output = ClassgroupElement.from_bytes(y_bytes[:form_size]) - # default value so that it's always set for state_changed later - ips: float = 0 - if not self.bluebox_mode: - time_taken = time.time() - self.chain_start_time[chain] - ips = int(iterations_needed / time_taken * 10) / 10 - log.info( - f"Finished PoT chall:{challenge[:10].hex()}.. {iterations_needed}" - f" iters, " - f"Estimated IPS: {ips}, Chain: {chain}" - ) + iterations_needed = uint64(int.from_bytes(stdout_bytes_io.read(8), "big", signed=True)) - vdf_info: VDFInfo = VDFInfo( - challenge, - iterations_needed, - output, - ) - vdf_proof: VDFProof = VDFProof( - witness_type, - proof_bytes, - self.bluebox_mode, + y_size_bytes = stdout_bytes_io.read(8) + y_size = uint64(int.from_bytes(y_size_bytes, "big", signed=True)) + + y_bytes = stdout_bytes_io.read(y_size) + witness_type = uint8(int.from_bytes(stdout_bytes_io.read(1), "big", signed=True)) + proof_bytes: bytes = stdout_bytes_io.read() + + # Verifies our own proof just in case + form_size = ClassgroupElement.get_size(self.constants) + output = ClassgroupElement.from_bytes(y_bytes[:form_size]) + # default value so that it's always set for state_changed later + ips: float = 0 + if not self.bluebox_mode: + time_taken = time.time() - self.chain_start_time[chain] + ips = int(iterations_needed / time_taken * 10) / 10 + log.info( + f"Finished PoT chall:{challenge[:10].hex()}.. {iterations_needed}" + f" iters, " + f"Estimated IPS: {ips}, Chain: {chain}" ) - if not vdf_proof.is_valid(self.constants, initial_form, vdf_info): - log.error("Invalid proof of time!") - if not self.bluebox_mode: - async with self.lock: - assert proof_label is not None - self.proofs_finished.append((chain, vdf_info, vdf_proof, proof_label)) - self.state_changed( - "finished_pot", - { - "estimated_ips": ips, - "iterations_needed": iterations_needed, - "chain": chain.value, - "vdf_info": vdf_info, - "vdf_proof": vdf_proof, - }, - ) - else: - async with self.lock: - writer.write(b"010") - await writer.drain() - assert header_hash is not None - assert field_vdf is not None - assert height is not None - response = timelord_protocol.RespondCompactProofOfTime( - vdf_info, vdf_proof, header_hash, height, field_vdf - ) - if self.server is not None: - message = make_msg(ProtocolMessageTypes.respond_compact_proof_of_time, response) - await self.server.send_to_all([message], NodeType.FULL_NODE) - self.state_changed( - "new_compact_proof", {"header_hash": header_hash, "height": height, "field_vdf": field_vdf} - ) + vdf_info: VDFInfo = VDFInfo( + challenge, + iterations_needed, + output, + ) + vdf_proof: VDFProof = VDFProof( + witness_type, + proof_bytes, + self.bluebox_mode, + ) + + if not vdf_proof.is_valid(self.constants, initial_form, vdf_info): + log.error("Invalid proof of time!") + if not self.bluebox_mode: + async with self.lock: + assert proof_label is not None + self.proofs_finished.append((chain, vdf_info, vdf_proof, proof_label)) + self.state_changed( + "finished_pot", + { + "estimated_ips": ips, + "iterations_needed": iterations_needed, + "chain": chain.value, + "vdf_info": vdf_info, + "vdf_proof": vdf_proof, + }, + ) + else: + async with self.lock: + writer.write(b"010") + await writer.drain() + assert header_hash is not None + assert field_vdf is not None + assert height is not None + response = timelord_protocol.RespondCompactProofOfTime( + vdf_info, vdf_proof, header_hash, height, field_vdf + ) + if self.server is not None: + message = make_msg(ProtocolMessageTypes.respond_compact_proof_of_time, response) + await self.server.send_to_all([message], NodeType.FULL_NODE) + self.state_changed( + "new_compact_proof", {"header_hash": header_hash, "height": height, "field_vdf": field_vdf} + ) except ConnectionResetError as e: log.debug(f"Connection reset with VDF client {e}") From 43138bccf0685062c7ed6eac3d12f7cd6f3c6f2a Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 24 Feb 2022 23:58:28 -0500 Subject: [PATCH 131/378] move build and twine to be dev deps rather than workflow installs (#10291) --- .github/workflows/upload-pypi-source.yml | 4 ---- setup.py | 2 ++ 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/upload-pypi-source.yml b/.github/workflows/upload-pypi-source.yml index b4e908de89d5..c02d72f92c50 100644 --- a/.github/workflows/upload-pypi-source.yml +++ b/.github/workflows/upload-pypi-source.yml @@ -62,7 +62,6 @@ jobs: - name: Build source distribution run: | - pip install build python -m build --sdist --outdir dist . - name: Upload artifacts @@ -71,9 +70,6 @@ jobs: name: dist path: ./dist - - name: Install twine - run: pip install twine - - name: Publish distribution to Test PyPI if: steps.check_secrets.outputs.HAS_SECRET env: diff --git a/setup.py b/setup.py index e35ca50a3c69..cfca00d8326a 100644 --- a/setup.py +++ b/setup.py @@ -41,11 +41,13 @@ ] dev_dependencies = [ + "build", "pre-commit", "pytest", "pytest-asyncio", "pytest-monitor; sys_platform == 'linux'", "pytest-xdist", + "twine", "isort", "flake8", "mypy", From 874cc23c719e93572316be5726189d5fcb694288 Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Fri, 25 Feb 2022 17:00:16 +0100 Subject: [PATCH 132/378] add db validate function to check consistency of blockchain database (#10398) --- chia/cmds/db.py | 26 ++++- chia/cmds/db_validate_func.py | 187 +++++++++++++++++++++++++++++++ chia/util/db_wrapper.py | 2 +- tests/core/test_db_conversion.py | 15 +-- tests/core/test_db_validation.py | 176 +++++++++++++++++++++++++++++ tests/util/temp_file.py | 12 ++ 6 files changed, 398 insertions(+), 20 deletions(-) create mode 100644 chia/cmds/db_validate_func.py create mode 100644 tests/core/test_db_validation.py create mode 100644 tests/util/temp_file.py diff --git a/chia/cmds/db.py b/chia/cmds/db.py index 87e252b6959b..671254e68f57 100644 --- a/chia/cmds/db.py +++ b/chia/cmds/db.py @@ -1,6 +1,7 @@ from pathlib import Path import click from chia.cmds.db_upgrade_func import db_upgrade_func +from chia.cmds.db_validate_func import db_validate_func @click.group("db", short_help="Manage the blockchain database") @@ -8,7 +9,7 @@ def db_cmd() -> None: pass -@db_cmd.command("upgrade", short_help="EXPERIMENTAL: upgrade a v1 database to v2") +@db_cmd.command("upgrade", short_help="upgrade a v1 database to v2") @click.option("--input", default=None, type=click.Path(), help="specify input database file") @click.option("--output", default=None, type=click.Path(), help="specify output database file") @click.option( @@ -34,7 +35,22 @@ def db_upgrade_cmd(ctx: click.Context, no_update_config: bool, **kwargs) -> None print(f"FAILED: {e}") -if __name__ == "__main__": - from chia.util.default_root import DEFAULT_ROOT_PATH - - db_upgrade_func(DEFAULT_ROOT_PATH) +@db_cmd.command("validate", short_help="validate the (v2) blockchain database. Does not verify proofs") +@click.option("--db", default=None, type=click.Path(), help="Specifies which database file to validate") +@click.option( + "--validate-blocks", + default=False, + is_flag=True, + help="validate consistency of properties of the encoded blocks and block records", +) +@click.pass_context +def db_validate_cmd(ctx: click.Context, validate_blocks: bool, **kwargs) -> None: + try: + in_db_path = kwargs.get("input") + db_validate_func( + Path(ctx.obj["root_path"]), + None if in_db_path is None else Path(in_db_path), + validate_blocks=validate_blocks, + ) + except RuntimeError as e: + print(f"FAILED: {e}") diff --git a/chia/cmds/db_validate_func.py b/chia/cmds/db_validate_func.py new file mode 100644 index 000000000000..d5d3bdb3675b --- /dev/null +++ b/chia/cmds/db_validate_func.py @@ -0,0 +1,187 @@ +from pathlib import Path +from typing import Any, Dict, Optional + +from chia.consensus.block_record import BlockRecord +from chia.consensus.default_constants import DEFAULT_CONSTANTS +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.full_block import FullBlock +from chia.util.config import load_config +from chia.util.path import path_from_root + + +def db_validate_func( + root_path: Path, + in_db_path: Optional[Path] = None, + *, + validate_blocks: bool, +) -> None: + if in_db_path is None: + config: Dict[str, Any] = load_config(root_path, "config.yaml")["full_node"] + selected_network: str = config["selected_network"] + db_pattern: str = config["database_path"] + db_path_replaced: str = db_pattern.replace("CHALLENGE", selected_network) + in_db_path = path_from_root(root_path, db_path_replaced) + + validate_v2(in_db_path, validate_blocks=validate_blocks) + + print(f"\n\nDATABASE IS VALID: {in_db_path}\n") + + +def validate_v2(in_path: Path, *, validate_blocks: bool) -> None: + import sqlite3 + from contextlib import closing + + import zstd + + if not in_path.exists(): + print(f"input file doesn't exist. {in_path}") + raise RuntimeError(f"can't find {in_path}") + + print(f"opening file for reading: {in_path}") + with closing(sqlite3.connect(in_path)) as in_db: + + # read the database version + try: + with closing(in_db.execute("SELECT * FROM database_version")) as cursor: + row = cursor.fetchone() + if row is None or row == []: + raise RuntimeError("Database is missing version field") + if row[0] != 2: + raise RuntimeError(f"Database has the wrong version ({row[0]} expected 2)") + except sqlite3.OperationalError: + raise RuntimeError("Database is missing version table") + + try: + with closing(in_db.execute("SELECT hash FROM current_peak WHERE key = 0")) as cursor: + row = cursor.fetchone() + if row is None or row == []: + raise RuntimeError("Database is missing current_peak field") + peak = bytes32(row[0]) + except sqlite3.OperationalError: + raise RuntimeError("Database is missing current_peak table") + + print(f"peak hash: {peak}") + + with closing(in_db.execute("SELECT height FROM full_blocks WHERE header_hash = ?", (peak,))) as cursor: + peak_row = cursor.fetchone() + if peak_row is None or peak_row == []: + raise RuntimeError("Database is missing the peak block") + peak_height = peak_row[0] + + print(f"peak height: {peak_height}") + + print("traversing the full chain") + + current_height = peak_height + # we're looking for a block with this hash + expect_hash = peak + # once we find it, we know what the next block to look for is, which + # this is set to + next_hash = None + + num_orphans = 0 + height_to_hash = bytearray(peak_height * 32) + + with closing( + in_db.execute( + f"SELECT header_hash, prev_hash, height, in_main_chain" + f"{', block, block_record' if validate_blocks else ''} " + "FROM full_blocks ORDER BY height DESC" + ) + ) as cursor: + + for row in cursor: + + hh = row[0] + prev = row[1] + height = row[2] + in_main_chain = row[3] + + # if there are blocks being added to the database, just ignore + # the ones added since we picked the peak + if height > peak_height: + continue + + if validate_blocks: + block = FullBlock.from_bytes(zstd.decompress(row[4])) + block_record = BlockRecord.from_bytes(row[5]) + actual_header_hash = block.header_hash + actual_prev_hash = block.prev_header_hash + if actual_header_hash != hh: + raise RuntimeError( + f"Block {hh.hex()} has a blob with mismatching " f"hash: {actual_header_hash.hex()}" + ) + if block_record.header_hash != hh: + raise RuntimeError( + f"Block {hh.hex()} has a block record with mismatching " + f"hash: {block_record.header_hash.hex()}" + ) + if block_record.total_iters != block.total_iters: + raise RuntimeError( + f"Block {hh.hex()} has a block record with mismatching total " + f"iters: {block_record.total_iters} expected {block.total_iters}" + ) + if block_record.prev_hash != actual_prev_hash: + raise RuntimeError( + f"Block {hh.hex()} has a block record with mismatching " + f"prev_hash: {block_record.prev_hash} expected {actual_prev_hash.hex()}" + ) + if block.height != height: + raise RuntimeError( + f"Block {hh.hex()} has a mismatching " f"height: {block.height} expected {height}" + ) + + if height != current_height: + # we're moving to the next level. Make sure we found the block + # we were looking for at the previous level + if next_hash is None: + raise RuntimeError( + f"Database is missing the block with hash {expect_hash} at height {current_height}" + ) + expect_hash = next_hash + next_hash = None + current_height = height + + if hh == expect_hash: + if next_hash is not None: + raise RuntimeError(f"Database has multiple blocks with hash {hh.hex()}, " f"at height {height}") + if not in_main_chain: + raise RuntimeError( + f"block {hh.hex()} (height: {height}) is part of the main chain, " + f"but in_main_chain is not set" + ) + + if validate_blocks: + if actual_prev_hash != prev: + raise RuntimeError( + f"Block {hh.hex()} has a blob with mismatching " + f"prev-hash: {actual_prev_hash}, expected {prev}" + ) + + next_hash = prev + + height_to_hash[height * 32 : height * 32 + 32] = hh + + print(f"\r{height} orphaned blocks: {num_orphans} ", end="") + + else: + if in_main_chain: + raise RuntimeError( + f"block {hh.hex()} (height: {height}) is orphaned, " "but in_main_chain is set" + ) + num_orphans += 1 + print("") + + if current_height != 0: + raise RuntimeError(f"Database is missing blocks below height {current_height}") + + # make sure the prev_hash pointer of block height 0 is the genesis + # challenge + if next_hash != DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA: + raise RuntimeError( + f"Blockchain has invalid genesis challenge {next_hash}, expected " + f"{DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA.hex()}" + ) + + if num_orphans > 0: + print(f"{num_orphans} orphaned blocks") diff --git a/chia/util/db_wrapper.py b/chia/util/db_wrapper.py index 2d90b34a05bb..53af97f96022 100644 --- a/chia/util/db_wrapper.py +++ b/chia/util/db_wrapper.py @@ -27,5 +27,5 @@ async def rollback_transaction(self): cursor = await self.db.execute("ROLLBACK") await cursor.close() - async def commit_transaction(self): + async def commit_transaction(self) -> None: await self.db.commit() diff --git a/tests/core/test_db_conversion.py b/tests/core/test_db_conversion.py index a60bdab41cba..e6dd6e2d94cd 100644 --- a/tests/core/test_db_conversion.py +++ b/tests/core/test_db_conversion.py @@ -1,13 +1,13 @@ import pytest import pytest_asyncio import aiosqlite -import tempfile import random import asyncio from pathlib import Path from typing import List, Tuple from tests.setup_nodes import test_constants +from tests.util.temp_file import TempFile from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.ints import uint32, uint64 @@ -20,19 +20,6 @@ from chia.consensus.multiprocess_validation import PreValidationResult -class TempFile: - def __init__(self): - self.path = Path(tempfile.NamedTemporaryFile().name) - - def __enter__(self) -> Path: - if self.path.exists(): - self.path.unlink() - return self.path - - def __exit__(self, exc_t, exc_v, exc_tb): - self.path.unlink() - - def rand_bytes(num) -> bytes: ret = bytearray(num) for i in range(num): diff --git a/tests/core/test_db_validation.py b/tests/core/test_db_validation.py new file mode 100644 index 000000000000..64d5e1e79e6d --- /dev/null +++ b/tests/core/test_db_validation.py @@ -0,0 +1,176 @@ +import asyncio +import random +import sqlite3 +from asyncio.events import AbstractEventLoop +from contextlib import closing +from pathlib import Path +from typing import Iterator, List + +import aiosqlite +import pytest +import pytest_asyncio + +from chia.cmds.db_validate_func import validate_v2 +from chia.consensus.blockchain import Blockchain +from chia.consensus.default_constants import DEFAULT_CONSTANTS +from chia.consensus.multiprocess_validation import PreValidationResult +from chia.full_node.block_store import BlockStore +from chia.full_node.coin_store import CoinStore +from chia.full_node.hint_store import HintStore +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.full_block import FullBlock +from chia.util.db_wrapper import DBWrapper +from chia.util.ints import uint32, uint64 +from tests.setup_nodes import test_constants +from tests.util.temp_file import TempFile + + +@pytest_asyncio.fixture(scope="session") +def event_loop() -> Iterator[AbstractEventLoop]: + loop = asyncio.get_event_loop() + yield loop + + +def rand_hash() -> bytes32: + ret = bytearray(32) + for i in range(32): + ret[i] = random.getrandbits(8) + return bytes32(ret) + + +def make_version(conn: sqlite3.Connection, version: int) -> None: + conn.execute("CREATE TABLE database_version(version int)") + conn.execute("INSERT INTO database_version VALUES (?)", (version,)) + conn.commit() + + +def make_peak(conn: sqlite3.Connection, peak_hash: bytes32) -> None: + conn.execute("CREATE TABLE IF NOT EXISTS current_peak(key int PRIMARY KEY, hash blob)") + conn.execute("INSERT OR REPLACE INTO current_peak VALUES(?, ?)", (0, peak_hash)) + conn.commit() + + +def make_block_table(conn: sqlite3.Connection) -> None: + conn.execute( + "CREATE TABLE IF NOT EXISTS full_blocks(" + "header_hash blob PRIMARY KEY," + "prev_hash blob," + "height bigint," + "sub_epoch_summary blob," + "is_fully_compactified tinyint," + "in_main_chain tinyint," + "block blob," + "block_record blob)" + ) + + +def add_block( + conn: sqlite3.Connection, header_hash: bytes32, prev_hash: bytes32, height: int, in_main_chain: bool +) -> None: + conn.execute( + "INSERT INTO full_blocks VALUES(?, ?, ?, NULL, 0, ?, NULL, NULL)", + ( + header_hash, + prev_hash, + height, + in_main_chain, + ), + ) + + +def test_db_validate_wrong_version() -> None: + with TempFile() as db_file: + with closing(sqlite3.connect(db_file)) as conn: + make_version(conn, 3) + + with pytest.raises(RuntimeError) as execinfo: + validate_v2(db_file, validate_blocks=False) + assert "Database has the wrong version (3 expected 2)" in str(execinfo.value) + + +def test_db_validate_missing_peak_table() -> None: + with TempFile() as db_file: + with closing(sqlite3.connect(db_file)) as conn: + make_version(conn, 2) + + with pytest.raises(RuntimeError) as execinfo: + validate_v2(db_file, validate_blocks=False) + assert "Database is missing current_peak table" in str(execinfo.value) + + +def test_db_validate_missing_peak_block() -> None: + with TempFile() as db_file: + with closing(sqlite3.connect(db_file)) as conn: + make_version(conn, 2) + make_peak(conn, bytes32.fromhex("fafafafafafafafafafafafafafafafafafafafafafafafafafafafafafafafa")) + + make_block_table(conn) + + with pytest.raises(RuntimeError) as execinfo: + validate_v2(db_file, validate_blocks=False) + assert "Database is missing the peak block" in str(execinfo.value) + + +@pytest.mark.parametrize("invalid_in_chain", [True, False]) +def test_db_validate_in_main_chain(invalid_in_chain: bool) -> None: + with TempFile() as db_file: + with closing(sqlite3.connect(db_file)) as conn: + make_version(conn, 2) + make_block_table(conn) + + prev = bytes32(DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA) + for height in range(0, 100): + header_hash = rand_hash() + add_block(conn, header_hash, prev, height, True) + if height % 4 == 0: + # insert an orphaned block + add_block(conn, rand_hash(), prev, height, invalid_in_chain) + prev = header_hash + + make_peak(conn, header_hash) + + if invalid_in_chain: + with pytest.raises(RuntimeError) as execinfo: + validate_v2(db_file, validate_blocks=False) + assert " (height: 96) is orphaned, but in_main_chain is set" in str(execinfo.value) + else: + validate_v2(db_file, validate_blocks=False) + + +async def make_db(db_file: Path, blocks: List[FullBlock]) -> None: + async with aiosqlite.connect(db_file) as conn: + + await conn.execute("pragma journal_mode=OFF") + await conn.execute("pragma synchronous=OFF") + await conn.execute("pragma locking_mode=exclusive") + + # this is done by chia init normally + await conn.execute("CREATE TABLE database_version(version int)") + await conn.execute("INSERT INTO database_version VALUES (2)") + await conn.commit() + + db_wrapper = DBWrapper(conn, 2) + block_store = await BlockStore.create(db_wrapper) + coin_store = await CoinStore.create(db_wrapper, uint32(0)) + hint_store = await HintStore.create(db_wrapper) + + bc = await Blockchain.create(coin_store, block_store, test_constants, hint_store, Path("."), reserved_cores=0) + await db_wrapper.commit_transaction() + + for block in blocks: + results = PreValidationResult(None, uint64(1), None, False) + result, err, _, _ = await bc.receive_block(block, results) + assert err is None + + +@pytest.mark.asyncio +async def test_db_validate_default_1000_blocks(default_1000_blocks: List[FullBlock]) -> None: + + with TempFile() as db_file: + await make_db(db_file, default_1000_blocks) + + # we expect everything to be valid except this is a test chain, so it + # doesn't have the correct genesis challenge + with pytest.raises(RuntimeError) as execinfo: + validate_v2(db_file, validate_blocks=True) + assert "Blockchain has invalid genesis challenge" in str(execinfo.value) diff --git a/tests/util/temp_file.py b/tests/util/temp_file.py new file mode 100644 index 000000000000..e38906fa7150 --- /dev/null +++ b/tests/util/temp_file.py @@ -0,0 +1,12 @@ +import contextlib +import tempfile +from pathlib import Path +from typing import Iterator + + +@contextlib.contextmanager +def TempFile() -> Iterator[Path]: + path = Path(tempfile.NamedTemporaryFile().name) + yield path + if path.exists(): + path.unlink() From fdb993cb1049dacfa9db1edefd66a3d4dbba73c4 Mon Sep 17 00:00:00 2001 From: dustinface <35775977+xdustinface@users.noreply.github.com> Date: Fri, 25 Feb 2022 17:07:24 +0100 Subject: [PATCH 133/378] streamable: Cache stream functions (#10419) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply the same pattern as we have for deserialization to serialization. This avoids all those recursive runtime lookups for "how to stream this object" which brings a nice speedup: ``` compare: benchmark mode | µs/iteration old | µs/iteration new | diff % to_bytes | 447.57 | 193.56 | -56.75 compare: full_block mode | µs/iteration old | µs/iteration new | diff % to_bytes | 110.32 | 61.09 | -44.62 ``` --- chia/util/streamable.py | 85 ++++++++++++++++++++++++++++------------- 1 file changed, 58 insertions(+), 27 deletions(-) diff --git a/chia/util/streamable.py b/chia/util/streamable.py index baedc9c16461..73584bac7ed1 100644 --- a/chia/util/streamable.py +++ b/chia/util/streamable.py @@ -124,6 +124,7 @@ def recurse_jsonify(d): return d +STREAM_FUNCTIONS_FOR_STREAMABLE_CLASS = {} PARSE_FUNCTIONS_FOR_STREAMABLE_CLASS = {} FIELDS_FOR_STREAMABLE_CLASS = {} @@ -179,6 +180,7 @@ def streamable(cls: Any): cls1 = strictdataclass(cls) t = type(cls.__name__, (cls1, Streamable), {}) + stream_functions = [] parse_functions = [] try: hints = get_type_hints(t) @@ -189,8 +191,10 @@ def streamable(cls: Any): FIELDS_FOR_STREAMABLE_CLASS[t] = fields for _, f_type in fields.items(): + stream_functions.append(cls.function_to_stream_one_item(f_type)) parse_functions.append(cls.function_to_parse_one_item(f_type)) + STREAM_FUNCTIONS_FOR_STREAMABLE_CLASS[t] = stream_functions PARSE_FUNCTIONS_FOR_STREAMABLE_CLASS[t] = parse_functions return t @@ -263,6 +267,37 @@ def parse_str(f: BinaryIO) -> str: return bytes.decode(str_read_bytes, "utf-8") +def stream_optional(stream_inner_type_func: Callable[[Any, BinaryIO], None], item: Any, f: BinaryIO) -> None: + if item is None: + f.write(bytes([0])) + else: + f.write(bytes([1])) + stream_inner_type_func(item, f) + + +def stream_bytes(item: Any, f: BinaryIO) -> None: + write_uint32(f, uint32(len(item))) + f.write(item) + + +def stream_list(stream_inner_type_func: Callable[[Any, BinaryIO], None], item: Any, f: BinaryIO) -> None: + write_uint32(f, uint32(len(item))) + for element in item: + stream_inner_type_func(element, f) + + +def stream_tuple(stream_inner_type_funcs: List[Callable[[Any, BinaryIO], None]], item: Any, f: BinaryIO) -> None: + assert len(stream_inner_type_funcs) == len(item) + for i in range(len(item)): + stream_inner_type_funcs[i](item[i], f) + + +def stream_str(item: Any, f: BinaryIO) -> None: + str_bytes = item.encode("utf-8") + write_uint32(f, uint32(len(str_bytes))) + f.write(str_bytes) + + class Streamable: @classmethod def function_to_parse_one_item(cls, f_type: Type) -> Callable[[BinaryIO], Any]: @@ -312,51 +347,47 @@ def parse(cls: Type[_T_Streamable], f: BinaryIO) -> _T_Streamable: raise ValueError("Failed to parse unknown data in Streamable object") return obj - def stream_one_item(self, f_type: Type, item, f: BinaryIO) -> None: + @classmethod + def function_to_stream_one_item(cls, f_type: Type) -> Callable[[Any, BinaryIO], Any]: inner_type: Type if is_type_SpecificOptional(f_type): inner_type = get_args(f_type)[0] - if item is None: - f.write(bytes([0])) - else: - f.write(bytes([1])) - self.stream_one_item(inner_type, item, f) + stream_inner_type_func = cls.function_to_stream_one_item(inner_type) + return lambda item, f: stream_optional(stream_inner_type_func, item, f) elif f_type == bytes: - write_uint32(f, uint32(len(item))) - f.write(item) + return stream_bytes elif hasattr(f_type, "stream"): - item.stream(f) + return lambda item, f: item.stream(f) elif hasattr(f_type, "__bytes__"): - f.write(bytes(item)) + return lambda item, f: f.write(bytes(item)) elif is_type_List(f_type): - assert is_type_List(type(item)) - write_uint32(f, uint32(len(item))) inner_type = get_args(f_type)[0] - # wjb assert inner_type != get_args(List)[0] # type: ignore - for element in item: - self.stream_one_item(inner_type, element, f) + stream_inner_type_func = cls.function_to_stream_one_item(inner_type) + return lambda item, f: stream_list(stream_inner_type_func, item, f) elif is_type_Tuple(f_type): inner_types = get_args(f_type) - assert len(item) == len(inner_types) - for i in range(len(item)): - self.stream_one_item(inner_types[i], item[i], f) - + stream_inner_type_funcs = [] + for i in range(len(inner_types)): + stream_inner_type_funcs.append(cls.function_to_stream_one_item(inner_types[i])) + return lambda item, f: stream_tuple(stream_inner_type_funcs, item, f) elif f_type is str: - str_bytes = item.encode("utf-8") - write_uint32(f, uint32(len(str_bytes))) - f.write(str_bytes) + return stream_str elif f_type is bool: - f.write(int(item).to_bytes(1, "big")) + return lambda item, f: f.write(int(item).to_bytes(1, "big")) else: - raise NotImplementedError(f"can't stream {item}, {f_type}") + raise NotImplementedError(f"can't stream {f_type}") def stream(self, f: BinaryIO) -> None: + self_type = type(self) try: - fields = FIELDS_FOR_STREAMABLE_CLASS[type(self)] + fields = FIELDS_FOR_STREAMABLE_CLASS[self_type] + functions = STREAM_FUNCTIONS_FOR_STREAMABLE_CLASS[self_type] except Exception: fields = {} - for f_name, f_type in fields.items(): - self.stream_one_item(f_type, getattr(self, f_name), f) + functions = [] + + for field, stream_func in zip(fields, functions): + stream_func(getattr(self, field), f) def get_hash(self) -> bytes32: return bytes32(std_hash(bytes(self))) From ae61ed75a364a30b69c89fba7e0fe5fb778d03aa Mon Sep 17 00:00:00 2001 From: ChiaMineJP Date: Sat, 26 Feb 2022 01:08:50 +0900 Subject: [PATCH 134/378] Stop assuming `True == 1` (#10396) * Stop assuming `True == 1` * Removed unnecessary lines which confuses code readers --- chia/full_node/mempool_manager.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/chia/full_node/mempool_manager.py b/chia/full_node/mempool_manager.py index 048086939b8b..942ef1991188 100644 --- a/chia/full_node/mempool_manager.py +++ b/chia/full_node/mempool_manager.py @@ -336,7 +336,6 @@ async def add_spendbundle( return uint64(cost), MempoolInclusionStatus.SUCCESS, None removal_record_dict: Dict[bytes32, CoinRecord] = {} - removal_coin_dict: Dict[bytes32, Coin] = {} removal_amount: int = 0 for name in removal_names: removal_record = await self.coin_store.get_coin_record(name) @@ -362,9 +361,8 @@ async def add_spendbundle( assert removal_record is not None removal_amount = removal_amount + removal_record.coin.amount removal_record_dict[name] = removal_record - removal_coin_dict[name] = removal_record.coin - removals: List[Coin] = [coin for coin in removal_coin_dict.values()] + removals: List[Coin] = [record.coin for record in removal_record_dict.values()] if addition_amount > removal_amount: return None, MempoolInclusionStatus.FAILED, Err.MINTING_COIN @@ -401,7 +399,6 @@ async def add_spendbundle( # Use this information later when constructing a block fail_reason, conflicts = await self.check_removals(removal_record_dict) # If there is a mempool conflict check if this spendbundle has a higher fee per cost than all others - tmp_error: Optional[Err] = None conflicting_pool_items: Dict[bytes32, MempoolItem] = {} if fail_reason is Err.MEMPOOL_CONFLICT: for conflicting in conflicts: @@ -421,9 +418,6 @@ async def add_spendbundle( elif fail_reason: return None, MempoolInclusionStatus.FAILED, fail_reason - if tmp_error: - return None, MempoolInclusionStatus.FAILED, tmp_error - # Verify conditions, create hash_key list for aggsig check error: Optional[Err] = None for npc in npc_list: @@ -487,7 +481,7 @@ async def check_removals(self, removals: Dict[bytes32, CoinRecord]) -> Tuple[Opt for record in removals.values(): removal = record.coin # 1. Checks if it's been spent already - if record.spent == 1: + if record.spent: return Err.DOUBLE_SPEND, [] # 2. Checks if there's a mempool conflict if removal.name() in self.mempool.removals: From bf4a632e6babea9c7b05a3ecf64cc4b9e1498b86 Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Fri, 25 Feb 2022 17:09:46 +0100 Subject: [PATCH 135/378] simplify self_hostname. It doesn't need to depend on initial-config.yaml -> create_block_tools() -> global variable bt (#10371) --- tests/core/test_farmer_harvester_rpc.py | 2 +- tests/pools/test_pool_rpc.py | 30 ++++++++++------------ tests/setup_nodes.py | 4 ++- tests/wallet/cat_wallet/test_cat_wallet.py | 28 ++++++++++---------- tests/wallet/cat_wallet/test_trades.py | 6 ++--- tests/wallet/did_wallet/test_did.py | 20 +++++++-------- tests/wallet/rpc/test_wallet_rpc.py | 4 +-- 7 files changed, 47 insertions(+), 47 deletions(-) diff --git a/tests/core/test_farmer_harvester_rpc.py b/tests/core/test_farmer_harvester_rpc.py index 2401c9b79f58..a9bb0697701d 100644 --- a/tests/core/test_farmer_harvester_rpc.py +++ b/tests/core/test_farmer_harvester_rpc.py @@ -237,7 +237,7 @@ async def test_farmer_get_pool_state(environment): "launcher_id": "ae4ef3b9bfe68949691281a015a9c16630fc8f66d48c19ca548fb80768791afa", "owner_public_key": "aa11e92274c0f6a2449fd0c7cfab4a38f943289dbe2214c808b36390c34eacfaa1d4c8f3c6ec582ac502ff32228679a0", # noqa "payout_instructions": "c2b08e41d766da4116e388357ed957d04ad754623a915f3fd65188a8746cf3e8", - "pool_url": "localhost", + "pool_url": self_hostname, "p2_singleton_puzzle_hash": "16e4bac26558d315cded63d4c5860e98deb447cc59146dd4de06ce7394b14f17", "target_puzzle_hash": "344587cf06a39db471d2cc027504e8688a0a67cce961253500c956c73603fd58", } diff --git a/tests/pools/test_pool_rpc.py b/tests/pools/test_pool_rpc.py index a5db79ca994f..1a573cc4b728 100644 --- a/tests/pools/test_pool_rpc.py +++ b/tests/pools/test_pool_rpc.py @@ -102,13 +102,12 @@ async def one_wallet_node_and_rpc(self): api_user = WalletRpcApi(wallet_node_0) config = bt.config - hostname = config["self_hostname"] daemon_port = config["daemon_port"] test_rpc_port = find_available_listen_port("rpc_port") rpc_cleanup = await start_rpc_server( api_user, - hostname, + self_hostname, daemon_port, test_rpc_port, lambda x: None, @@ -136,13 +135,12 @@ async def setup(self, two_wallet_nodes): pool_ph = pool_ph_record.puzzle_hash api_user = WalletRpcApi(wallet_node_0) config = bt.config - hostname = config["self_hostname"] daemon_port = config["daemon_port"] test_rpc_port = find_available_listen_port("rpc_port") rpc_cleanup = await start_rpc_server( api_user, - hostname, + self_hostname, daemon_port, test_rpc_port, lambda x: None, @@ -199,7 +197,7 @@ async def test_create_new_pool_wallet_self_farm(self, one_wallet_node_and_rpc, f assert False await time_out_assert(10, wallet_is_synced, True, wallet_node_0, full_node_api) creation_tx: TransactionRecord = await client.create_new_pool_wallet( - our_ph, "", 0, "localhost:5000", "new", "SELF_POOLING", fee + our_ph, "", 0, f"{self_hostname}:5000", "new", "SELF_POOLING", fee ) await time_out_assert( 10, @@ -278,7 +276,7 @@ async def test_create_new_pool_wallet_farm_to_pool(self, one_wallet_node_and_rpc assert False creation_tx: TransactionRecord = await client.create_new_pool_wallet( - our_ph, "http://pool.example.com", 10, "localhost:5000", "new", "FARMING_TO_POOL", fee + our_ph, "http://pool.example.com", 10, f"{self_hostname}:5000", "new", "FARMING_TO_POOL", fee ) await time_out_assert( 10, @@ -356,10 +354,10 @@ async def test_create_multiple_pool_wallets(self, one_wallet_node_and_rpc, fee, assert False creation_tx: TransactionRecord = await client.create_new_pool_wallet( - our_ph_1, "", 0, "localhost:5000", "new", "SELF_POOLING", fee + our_ph_1, "", 0, f"{self_hostname}:5000", "new", "SELF_POOLING", fee ) creation_tx_2: TransactionRecord = await client.create_new_pool_wallet( - our_ph_1, "localhost", 12, "localhost:5000", "new", "FARMING_TO_POOL", fee + our_ph_1, self_hostname, 12, f"{self_hostname}:5000", "new", "FARMING_TO_POOL", fee ) await time_out_assert( @@ -427,7 +425,7 @@ async def test_create_multiple_pool_wallets(self, one_wallet_node_and_rpc, fee, for i in range(22): await time_out_assert(10, wallet_is_synced, True, wallet_node_0, full_node_api) creation_tx_3: TransactionRecord = await client.create_new_pool_wallet( - our_ph_1, "localhost", 5, "localhost:5000", "new", "FARMING_TO_POOL", fee + our_ph_1, self_hostname, 5, f"{self_hostname}:5000", "new", "FARMING_TO_POOL", fee ) await time_out_assert( 10, @@ -485,7 +483,7 @@ async def test_absorb_self(self, one_wallet_node_and_rpc, fee, trusted): await time_out_assert(10, wallet_is_synced, True, wallet_node_0, full_node_api) creation_tx: TransactionRecord = await client.create_new_pool_wallet( - our_ph, "", 0, "localhost:5000", "new", "SELF_POOLING", fee + our_ph, "", 0, f"{self_hostname}:5000", "new", "SELF_POOLING", fee ) await time_out_assert( @@ -602,7 +600,7 @@ async def test_absorb_pooling(self, one_wallet_node_and_rpc, fee, trusted): # Balance stars at 6 XCH assert (await wallet_0.get_confirmed_balance()) == 6000000000000 creation_tx: TransactionRecord = await client.create_new_pool_wallet( - our_ph, "http://123.45.67.89", 10, "localhost:5000", "new", "FARMING_TO_POOL", fee + our_ph, "http://123.45.67.89", 10, f"{self_hostname}:5000", "new", "FARMING_TO_POOL", fee ) await time_out_assert( @@ -761,10 +759,10 @@ async def test_self_pooling_to_pooling(self, setup, fee, trusted): assert False creation_tx: TransactionRecord = await client.create_new_pool_wallet( - our_ph, "", 0, "localhost:5000", "new", "SELF_POOLING", fee + our_ph, "", 0, f"{self_hostname}:5000", "new", "SELF_POOLING", fee ) creation_tx_2: TransactionRecord = await client.create_new_pool_wallet( - our_ph, "", 0, "localhost:5000", "new", "SELF_POOLING", fee + our_ph, "", 0, f"{self_hostname}:5000", "new", "SELF_POOLING", fee ) await time_out_assert( @@ -895,7 +893,7 @@ async def have_chia(): await time_out_assert(10, wallet_is_synced, True, wallet_nodes[0], full_node_api) creation_tx: TransactionRecord = await client.create_new_pool_wallet( - our_ph, "", 0, "localhost:5000", "new", "SELF_POOLING", fee + our_ph, "", 0, f"{self_hostname}:5000", "new", "SELF_POOLING", fee ) await time_out_assert( @@ -1018,7 +1016,7 @@ async def have_chia(): await time_out_assert(10, wallet_is_synced, True, wallet_nodes[0], full_node_api) creation_tx: TransactionRecord = await client.create_new_pool_wallet( - pool_a_ph, "https://pool-a.org", 5, "localhost:5000", "new", "FARMING_TO_POOL", fee + pool_a_ph, "https://pool-a.org", 5, f"{self_hostname}:5000", "new", "FARMING_TO_POOL", fee ) await time_out_assert( @@ -1121,7 +1119,7 @@ async def have_chia(): await time_out_assert(10, wallet_is_synced, True, wallet_nodes[0], full_node_api) creation_tx: TransactionRecord = await client.create_new_pool_wallet( - pool_a_ph, "https://pool-a.org", 5, "localhost:5000", "new", "FARMING_TO_POOL", fee + pool_a_ph, "https://pool-a.org", 5, f"{self_hostname}:5000", "new", "FARMING_TO_POOL", fee ) await time_out_assert( diff --git a/tests/setup_nodes.py b/tests/setup_nodes.py index 486987448f1d..3c65c834387a 100644 --- a/tests/setup_nodes.py +++ b/tests/setup_nodes.py @@ -38,7 +38,9 @@ def cleanup_keyring(keyring: TempKeyring): atexit.register(cleanup_keyring, temp_keyring) # Attempt to cleanup the temp keychain bt = create_block_tools(constants=test_constants, keychain=keychain) -self_hostname = bt.config["self_hostname"] +# if you have a system that has an unusual hostname for localhost and you want +# to run the tests, change this constant +self_hostname = "localhost" def constants_for_dic(dic): diff --git a/tests/wallet/cat_wallet/test_cat_wallet.py b/tests/wallet/cat_wallet/test_cat_wallet.py index 3239f87dcafa..4bb8c894bc0e 100644 --- a/tests/wallet/cat_wallet/test_cat_wallet.py +++ b/tests/wallet/cat_wallet/test_cat_wallet.py @@ -19,7 +19,7 @@ from chia.wallet.transaction_record import TransactionRecord from chia.wallet.wallet_info import WalletInfo from tests.pools.test_pool_rpc import wallet_is_synced -from tests.setup_nodes import setup_simulators_and_wallets +from tests.setup_nodes import self_hostname, setup_simulators_and_wallets from tests.time_out_assert import time_out_assert @@ -65,7 +65,7 @@ async def test_cat_creation(self, two_wallet_nodes, trusted): else: wallet_node.config["trusted_peers"] = {} - await server_2.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) + await server_2.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) for i in range(0, num_blocks): await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0")) @@ -137,8 +137,8 @@ async def test_cat_spend(self, two_wallet_nodes, trusted): else: wallet_node.config["trusted_peers"] = {} wallet_node_2.config["trusted_peers"] = {} - await server_2.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) - await server_3.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) + await server_2.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) + await server_3.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) for i in range(0, num_blocks): await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) @@ -234,7 +234,7 @@ async def test_get_wallet_for_asset_id(self, two_wallet_nodes, trusted): wallet_node.config["trusted_peers"] = {full_node_server.node_id.hex(): full_node_server.node_id.hex()} else: wallet_node.config["trusted_peers"] = {} - await server_2.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) + await server_2.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) for i in range(0, num_blocks): await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) @@ -291,8 +291,8 @@ async def test_cat_doesnt_see_eve(self, two_wallet_nodes, trusted): else: wallet_node.config["trusted_peers"] = {} wallet_node_2.config["trusted_peers"] = {} - await server_2.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) - await server_3.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) + await server_2.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) + await server_3.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) for i in range(0, num_blocks): await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) @@ -398,9 +398,9 @@ async def test_cat_spend_multiple(self, three_wallet_nodes, trusted): wallet_node_0.config["trusted_peers"] = {} wallet_node_1.config["trusted_peers"] = {} wallet_node_2.config["trusted_peers"] = {} - await wallet_server_0.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) - await wallet_server_1.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) - await wallet_server_2.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) + await wallet_server_0.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) + await wallet_server_1.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) + await wallet_server_2.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) for i in range(0, num_blocks): await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) @@ -534,8 +534,8 @@ async def test_cat_max_amount_send(self, two_wallet_nodes, trusted): else: wallet_node.config["trusted_peers"] = {} wallet_node_2.config["trusted_peers"] = {} - await server_2.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) - await server_3.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) + await server_2.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) + await server_3.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) for i in range(0, num_blocks): await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) @@ -663,8 +663,8 @@ async def test_cat_hint(self, two_wallet_nodes, trusted): else: wallet_node.config["trusted_peers"] = {} wallet_node_2.config["trusted_peers"] = {} - await server_2.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) - await server_3.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) + await server_2.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) + await server_3.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) for i in range(0, num_blocks): await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) diff --git a/tests/wallet/cat_wallet/test_trades.py b/tests/wallet/cat_wallet/test_trades.py index a546003b2bd8..cc191fc2db6d 100644 --- a/tests/wallet/cat_wallet/test_trades.py +++ b/tests/wallet/cat_wallet/test_trades.py @@ -14,7 +14,7 @@ from chia.wallet.trading.trade_status import TradeStatus from chia.wallet.transaction_record import TransactionRecord from tests.pools.test_pool_rpc import wallet_is_synced -from tests.setup_nodes import setup_simulators_and_wallets +from tests.setup_nodes import self_hostname, setup_simulators_and_wallets from tests.time_out_assert import time_out_assert @@ -65,8 +65,8 @@ async def wallets_prefarm(two_wallet_nodes, trusted): wallet_node_0.config["trusted_peers"] = {} wallet_node_1.config["trusted_peers"] = {} - await wallet_server_0.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) - await wallet_server_1.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) + await wallet_server_0.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) + await wallet_server_1.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) for i in range(0, farm_blocks): await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph0)) diff --git a/tests/wallet/did_wallet/test_did.py b/tests/wallet/did_wallet/test_did.py index f613262a80c3..f717ce924036 100644 --- a/tests/wallet/did_wallet/test_did.py +++ b/tests/wallet/did_wallet/test_did.py @@ -4,7 +4,7 @@ from chia.simulator.simulator_protocol import FarmNewBlockProtocol from chia.types.peer_info import PeerInfo from chia.util.ints import uint16, uint32, uint64 -from tests.setup_nodes import setup_simulators_and_wallets +from tests.setup_nodes import self_hostname, setup_simulators_and_wallets from chia.wallet.did_wallet.did_wallet import DIDWallet from chia.types.blockchain_format.program import Program from blspy import AugSchemeMPL @@ -64,9 +64,9 @@ async def test_creation_from_backup_file(self, three_wallet_nodes): ph1 = await wallet_1.get_new_puzzlehash() ph2 = await wallet_2.get_new_puzzlehash() - await server_0.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) - await server_1.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) - await server_2.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) + await server_0.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) + await server_1.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) + await server_2.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) for i in range(1, num_blocks): await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) @@ -185,8 +185,8 @@ async def test_did_recovery_with_multiple_backup_dids(self, two_wallet_nodes): ph = await wallet.get_new_puzzlehash() - await server_2.start_client(PeerInfo("localhost", uint16(server_1._port)), None) - await server_3.start_client(PeerInfo("localhost", uint16(server_1._port)), None) + await server_2.start_client(PeerInfo(self_hostname, uint16(server_1._port)), None) + await server_3.start_client(PeerInfo(self_hostname, uint16(server_1._port)), None) for i in range(1, num_blocks): await full_node_1.farm_new_transaction_block(FarmNewBlockProtocol(ph)) @@ -292,8 +292,8 @@ async def test_did_recovery_with_empty_set(self, two_wallet_nodes): ph = await wallet.get_new_puzzlehash() - await server_2.start_client(PeerInfo("localhost", uint16(server_1._port)), None) - await server_3.start_client(PeerInfo("localhost", uint16(server_1._port)), None) + await server_2.start_client(PeerInfo(self_hostname, uint16(server_1._port)), None) + await server_3.start_client(PeerInfo(self_hostname, uint16(server_1._port)), None) for i in range(1, num_blocks): await full_node_1.farm_new_transaction_block(FarmNewBlockProtocol(ph)) @@ -339,8 +339,8 @@ async def test_did_attest_after_recovery(self, two_wallet_nodes): wallet2 = wallet_node_2.wallet_state_manager.main_wallet ph = await wallet.get_new_puzzlehash() - await server_2.start_client(PeerInfo("localhost", uint16(server_1._port)), None) - await server_3.start_client(PeerInfo("localhost", uint16(server_1._port)), None) + await server_2.start_client(PeerInfo(self_hostname, uint16(server_1._port)), None) + await server_3.start_client(PeerInfo(self_hostname, uint16(server_1._port)), None) for i in range(1, num_blocks): await full_node_1.farm_new_transaction_block(FarmNewBlockProtocol(ph)) diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index aa861844e986..11b416be07b6 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -67,8 +67,8 @@ async def test_wallet_rpc(self, two_wallet_nodes, trusted): ph = await wallet.get_new_puzzlehash() ph_2 = await wallet_2.get_new_puzzlehash() - await server_2.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) - await server_3.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) + await server_2.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) + await server_3.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) if trusted: wallet_node.config["trusted_peers"] = {full_node_server.node_id.hex(): full_node_server.node_id.hex()} From d47119a773addbe186d19c5e972975e9f757acd4 Mon Sep 17 00:00:00 2001 From: Mariano Sorgente <3069354+mariano54@users.noreply.github.com> Date: Sat, 26 Feb 2022 14:55:19 -0500 Subject: [PATCH 136/378] Fix balance calculation so it works with future unconfirmed tx (#10447) --- chia/wallet/cat_wallet/cat_wallet.py | 2 +- chia/wallet/did_wallet/did_wallet.py | 4 +- chia/wallet/wallet_state_manager.py | 57 +++++----------------------- 3 files changed, 13 insertions(+), 50 deletions(-) diff --git a/chia/wallet/cat_wallet/cat_wallet.py b/chia/wallet/cat_wallet/cat_wallet.py index 9f2040d40381..de953bdde581 100644 --- a/chia/wallet/cat_wallet/cat_wallet.py +++ b/chia/wallet/cat_wallet/cat_wallet.py @@ -73,7 +73,7 @@ async def create_new_cat_wallet( self.standard_wallet = wallet self.log = logging.getLogger(__name__) std_wallet_id = self.standard_wallet.wallet_id - bal = await wallet_state_manager.get_confirmed_balance_for_wallet_already_locked(std_wallet_id) + bal = await wallet_state_manager.get_confirmed_balance_for_wallet(std_wallet_id) if amount > bal: raise ValueError("Not enough balance") self.wallet_state_manager = wallet_state_manager diff --git a/chia/wallet/did_wallet/did_wallet.py b/chia/wallet/did_wallet/did_wallet.py index 82bf8861859e..363cffe5e898 100644 --- a/chia/wallet/did_wallet/did_wallet.py +++ b/chia/wallet/did_wallet/did_wallet.py @@ -57,7 +57,7 @@ async def create_new_did_wallet( self.standard_wallet = wallet self.log = logging.getLogger(name if name else __name__) std_wallet_id = self.standard_wallet.wallet_id - bal = await wallet_state_manager.get_confirmed_balance_for_wallet_already_locked(std_wallet_id) + bal = await wallet_state_manager.get_confirmed_balance_for_wallet(std_wallet_id) if amount > bal: raise ValueError("Not enough balance") if amount & 1 == 0: @@ -76,7 +76,7 @@ async def create_new_did_wallet( raise ValueError("Internal Error") self.wallet_id = self.wallet_info.id std_wallet_id = self.standard_wallet.wallet_id - bal = await wallet_state_manager.get_confirmed_balance_for_wallet_already_locked(std_wallet_id) + bal = await wallet_state_manager.get_confirmed_balance_for_wallet(std_wallet_id) if amount > bal: raise ValueError("Not enough balance") diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index b20c080f2afe..c57c265ce11f 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -60,13 +60,6 @@ from chia.wallet.wallet_weight_proof_handler import WalletWeightProofHandler -def get_balance_from_coin_records(coin_records: Set[WalletCoinRecord]) -> uint128: - amount: uint128 = uint128(0) - for record in coin_records: - amount = uint128(amount + record.coin.amount) - return uint128(amount) - - class WalletStateManager: constants: ConsensusConstants config: Dict @@ -494,14 +487,6 @@ async def does_coin_belong_to_wallet(self, coin: Coin, wallet_id: int) -> bool: return False - async def get_confirmed_balance_for_wallet_already_locked(self, wallet_id: int) -> uint128: - # This is a workaround to be able to call la locking operation when already locked - # for example, in the create method of DID wallet - if self.lock.locked() is False: - raise AssertionError("expected wallet_state_manager to be locked") - unspent_coin_records = await self.coin_store.get_unspent_coins_for_wallet(wallet_id) - return get_balance_from_coin_records(unspent_coin_records) - async def get_confirmed_balance_for_wallet( self, wallet_id: int, @@ -513,13 +498,10 @@ async def get_confirmed_balance_for_wallet( # lock only if unspent_coin_records is None if unspent_coin_records is None: unspent_coin_records = await self.coin_store.get_unspent_coins_for_wallet(wallet_id) - amount: uint128 = uint128(0) - for record in unspent_coin_records: - amount = uint128(amount + record.coin.amount) - return uint128(amount) + return uint128(sum(cr.coin.amount for cr in unspent_coin_records)) async def get_unconfirmed_balance( - self, wallet_id, unspent_coin_records: Optional[Set[WalletCoinRecord]] = None + self, wallet_id: int, unspent_coin_records: Optional[Set[WalletCoinRecord]] = None ) -> uint128: """ Returns the balance, including coinbase rewards that are not spendable, and unconfirmed @@ -527,42 +509,23 @@ async def get_unconfirmed_balance( """ # This API should change so that get_balance_from_coin_records is called for Set[WalletCoinRecord] # and this method is called only for the unspent_coin_records==None case. - confirmed_amount = await self.get_confirmed_balance_for_wallet(wallet_id, unspent_coin_records) - return await self._get_unconfirmed_balance(wallet_id, confirmed_amount) - - async def get_unconfirmed_balance_already_locked(self, wallet_id) -> uint128: - confirmed_amount = await self.get_confirmed_balance_for_wallet_already_locked(wallet_id) - return await self._get_unconfirmed_balance(wallet_id, confirmed_amount) + if unspent_coin_records is None: + unspent_coin_records = await self.coin_store.get_unspent_coins_for_wallet(wallet_id) - async def _get_unconfirmed_balance(self, wallet_id, confirmed: uint128) -> uint128: unconfirmed_tx: List[TransactionRecord] = await self.tx_store.get_unconfirmed_for_wallet(wallet_id) - removal_amount: int = 0 - addition_amount: int = 0 + all_unspent_coins: Set[Coin] = {cr.coin for cr in unspent_coin_records} for record in unconfirmed_tx: - for removal in record.removals: - if await self.does_coin_belong_to_wallet(removal, wallet_id): - removal_amount += removal.amount for addition in record.additions: # This change or a self transaction if await self.does_coin_belong_to_wallet(addition, wallet_id): - addition_amount += addition.amount + all_unspent_coins.add(addition) - result = (confirmed + addition_amount) - removal_amount - return uint128(result) + for removal in record.removals: + if await self.does_coin_belong_to_wallet(removal, wallet_id) and removal in all_unspent_coins: + all_unspent_coins.remove(removal) - async def unconfirmed_additions_for_wallet(self, wallet_id: int) -> Dict[bytes32, Coin]: - """ - Returns change coins for the wallet_id. - (Unconfirmed addition transactions that have not been confirmed yet.) - """ - additions: Dict[bytes32, Coin] = {} - unconfirmed_tx = await self.tx_store.get_unconfirmed_for_wallet(wallet_id) - for record in unconfirmed_tx: - for coin in record.additions: - if await self.is_addition_relevant(coin): - additions[coin.name()] = coin - return additions + return uint128(sum(coin.amount for coin in all_unspent_coins)) async def unconfirmed_removals_for_wallet(self, wallet_id: int) -> Dict[bytes32, Coin]: """ From d245790a17de62cf3863fe0c9e8db625edbdf98e Mon Sep 17 00:00:00 2001 From: Mariano Sorgente <3069354+mariano54@users.noreply.github.com> Date: Sat, 26 Feb 2022 15:02:51 -0500 Subject: [PATCH 137/378] Only update height if we have processed everything before that height. (#10415) * Only update height if we have processed everything before that height. * Remove debug return * Forgot this file * Better sorting of coin states, and disconnect peer if we are connected to trusted * Fix disconnect, and don't mutate arguments * Fix comment * Update chia/wallet/wallet_node.py Co-authored-by: Kyle Altendorf * Update chia/wallet/wallet_state_manager.py Co-authored-by: Kyle Altendorf * Address comments Co-authored-by: Kyle Altendorf --- chia/wallet/util/wallet_sync_utils.py | 2 + chia/wallet/wallet_blockchain.py | 4 +- chia/wallet/wallet_node.py | 67 +++++++++++++++++++-------- chia/wallet/wallet_state_manager.py | 17 +++---- 4 files changed, 60 insertions(+), 30 deletions(-) diff --git a/chia/wallet/util/wallet_sync_utils.py b/chia/wallet/util/wallet_sync_utils.py index 9dcb7803d6c9..44675ee4d6b5 100644 --- a/chia/wallet/util/wallet_sync_utils.py +++ b/chia/wallet/util/wallet_sync_utils.py @@ -281,6 +281,8 @@ def last_change_height_cs(cs: CoinState) -> uint32: return cs.spent_height if cs.created_height is not None: return cs.created_height + + # Reorgs should be processed at the beginning return uint32(0) diff --git a/chia/wallet/wallet_blockchain.py b/chia/wallet/wallet_blockchain.py index b105d914df28..dcc24575881e 100644 --- a/chia/wallet/wallet_blockchain.py +++ b/chia/wallet/wallet_blockchain.py @@ -184,9 +184,9 @@ async def get_peak_block(self) -> Optional[HeaderBlock]: return self._peak return await self._basic_store.get_object("PEAK_BLOCK", HeaderBlock) - async def set_finished_sync_up_to(self, height: uint32): + async def set_finished_sync_up_to(self, height: int): if height > await self.get_finished_sync_up_to(): - await self._basic_store.set_object("FINISHED_SYNC_UP_TO", height) + await self._basic_store.set_object("FINISHED_SYNC_UP_TO", uint32(height)) await self.clean_block_records() async def get_finished_sync_up_to(self): diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index be3bd3065449..3822c390f817 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -519,7 +519,6 @@ def is_new_state_update(cs: CoinState) -> bool: [p for p in chunk if p not in already_checked_ph], full_node, 0 ) ph_update_res = list(filter(is_new_state_update, ph_update_res)) - ph_update_res.sort(key=last_change_height_cs) if not await self.receive_state_from_peer(ph_update_res, full_node, update_finished_height=True): # If something goes wrong, abort sync return @@ -545,7 +544,6 @@ def is_new_state_update(cs: CoinState) -> bool: for chunk in one_k_chunks: c_update_res: List[CoinState] = await subscribe_to_coin_updates(chunk, full_node, 0) c_update_res = list(filter(is_new_state_update, c_update_res)) - c_update_res.sort(key=last_change_height_cs) if not await self.receive_state_from_peer(c_update_res, full_node): # If something goes wrong, abort sync return @@ -576,7 +574,7 @@ def is_new_state_update(cs: CoinState) -> bool: async def receive_state_from_peer( self, - items: List[CoinState], + items_input: List[CoinState], peer: WSChiaConnection, fork_height: Optional[uint32] = None, height: Optional[uint32] = None, @@ -585,7 +583,9 @@ async def receive_state_from_peer( ) -> bool: # Adds the state to the wallet state manager. If the peer is trusted, we do not validate. If the peer is # untrusted we do, but we might not add the state, since we need to receive the new_peak message as well. - assert self.wallet_state_manager is not None + + if self.wallet_state_manager is None: + return False trusted = self.is_trusted(peer) # Validate states in parallel, apply serial # TODO: optimize fetching @@ -603,13 +603,15 @@ async def receive_state_from_peer( all_tasks: List[asyncio.Task] = [] target_concurrent_tasks: int = 20 - num_concurrent_tasks: int = 0 + concurrent_tasks_cs_heights: List[uint32] = [] + + # Ensure the list is sorted + items = sorted(items_input, key=last_change_height_cs) - async def receive_and_validate(inner_states: List[CoinState], inner_idx_start: int): + async def receive_and_validate(inner_states: List[CoinState], inner_idx_start: int, cs_heights: List[uint32]): try: assert self.validation_semaphore is not None async with self.validation_semaphore: - assert self.wallet_state_manager is not None if header_hash is not None: assert height is not None for inner_state in inner_states: @@ -626,21 +628,28 @@ async def receive_and_validate(inner_states: List[CoinState], inner_idx_start: i if len(valid_states) > 0: self.log.info( f"new coin state received ({inner_idx_start}-" - f"{inner_idx_start + len(inner_states) - 1}/ {len(items) + 1})" + f"{inner_idx_start + len(inner_states) - 1}/ {len(items)})" ) assert self.new_state_lock is not None async with self.new_state_lock: + if self.wallet_state_manager is None: + return await self.wallet_state_manager.new_coin_state(valid_states, peer, fork_height) - if update_finished_height: - await self.wallet_state_manager.blockchain.set_finished_sync_up_to( - last_change_height_cs(inner_states[-1]) - ) + + if update_finished_height: + if len(cs_heights) == 1: + # We have processed all past tasks, so we can increase the height safely + synced_up_to = last_change_height_cs(valid_states[-1]) - 1 + else: + # We know we have processed everything before this min height + synced_up_to = min(cs_heights) + await self.wallet_state_manager.blockchain.set_finished_sync_up_to(synced_up_to) + except Exception as e: tb = traceback.format_exc() self.log.error(f"Exception while adding state: {e} {tb}") finally: - nonlocal num_concurrent_tasks - num_concurrent_tasks -= 1 # pylint: disable=E0602 + cs_heights.remove(last_change_height_cs(inner_states[0])) idx = 1 # Keep chunk size below 1000 just in case, windows has sqlite limits of 999 per query @@ -653,13 +662,13 @@ async def receive_and_validate(inner_states: List[CoinState], inner_idx_start: i if peer.peer_node_id not in self.server.all_connections: self.log.error(f"Disconnected from peer {peer.peer_node_id} host {peer.peer_host}") return False - while num_concurrent_tasks >= target_concurrent_tasks: + while len(concurrent_tasks_cs_heights) >= target_concurrent_tasks: await asyncio.sleep(0.1) if self._shut_down: self.log.info("Terminating receipt and validation due to shut down request") return False - all_tasks.append(asyncio.create_task(receive_and_validate(states, idx))) - num_concurrent_tasks += 1 + concurrent_tasks_cs_heights.append(last_change_height_cs(states[0])) + all_tasks.append(asyncio.create_task(receive_and_validate(states, idx, concurrent_tasks_cs_heights))) idx += len(states) await asyncio.gather(*all_tasks) @@ -729,15 +738,20 @@ async def state_update_received(self, request: wallet_protocol.CoinStateUpdate, ) def get_full_node_peer(self) -> Optional[WSChiaConnection]: - assert self.server is not None + if self.server is None: + return None + nodes = self.server.get_full_node_connections() if len(nodes) > 0: return random.choice(nodes) else: return None - async def disconnect_and_stop_wpeers(self): - # Close connection of non trusted peers + async def disconnect_and_stop_wpeers(self) -> None: + if self.server is None: + return + + # Close connection of non-trusted peers if len(self.server.get_full_node_connections()) > 1: for peer in self.server.get_full_node_connections(): if not self.is_trusted(peer): @@ -747,6 +761,14 @@ async def disconnect_and_stop_wpeers(self): await self.wallet_peers.ensure_is_closed() self.wallet_peers = None + async def check_for_synced_trusted_peer(self, header_block: HeaderBlock, request_time: uint64) -> bool: + if self.server is None: + return False + for peer in self.server.get_full_node_connections(): + if self.is_trusted(peer) and await self.is_peer_synced(peer, header_block, request_time): + return True + return False + async def get_timestamp_for_height(self, height: uint32) -> uint64: """ Returns the timestamp for transaction block at h=height, if not transaction block, backtracks until it finds @@ -842,6 +864,11 @@ async def new_peak_wallet(self, new_peak: wallet_protocol.NewPeakWallet, peer: W self.wallet_state_manager.set_sync_mode(False) await peer.close() return + + if await self.check_for_synced_trusted_peer(header_block, request_time): + self.wallet_state_manager.set_sync_mode(False) + self.log.info("Cancelling untrusted sync, we are connected to a trusted peer") + return assert weight_proof is not None old_proof = self.wallet_state_manager.blockchain.synced_weight_proof if syncing: diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index c57c265ce11f..052be2af4761 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -41,6 +41,7 @@ from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.compute_hints import compute_coin_hints from chia.wallet.util.transaction_type import TransactionType +from chia.wallet.util.wallet_sync_utils import last_change_height_cs from chia.wallet.util.wallet_types import WalletType from chia.wallet.wallet import Wallet from chia.wallet.wallet_action import WalletAction @@ -599,14 +600,14 @@ async def new_coin_state( ) -> None: # TODO: add comment about what this method does - # Sort by created height, then add the reorg states (created_height is None) to the end - created_h_none: List[CoinState] = [] - for coin_st in coin_states.copy(): - if coin_st.created_height is None: - coin_states.remove(coin_st) - created_h_none.append(coin_st) - coin_states.sort(key=lambda x: x.created_height, reverse=False) # type: ignore - coin_states.extend(created_h_none) + # Input states should already be sorted by cs_height, with reorgs at the beginning + curr_h = -1 + for c_state in coin_states: + last_change_height = last_change_height_cs(c_state) + if last_change_height < curr_h: + raise ValueError("Input coin_states is not sorted properly") + curr_h = last_change_height + all_txs_per_wallet: Dict[int, List[TransactionRecord]] = {} trade_removals = await self.trade_manager.get_coins_of_interest() all_unconfirmed: List[TransactionRecord] = await self.tx_store.get_all_unconfirmed() From e8475fb1944affa301a22856b8c4b8217b062f6f Mon Sep 17 00:00:00 2001 From: Mariano Sorgente <3069354+mariano54@users.noreply.github.com> Date: Sat, 26 Feb 2022 18:31:10 -0500 Subject: [PATCH 138/378] GC connections in wallet (#10450) --- chia/server/server.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/chia/server/server.py b/chia/server/server.py index 2af0e1c63e63..6fc8f34912ec 100644 --- a/chia/server/server.py +++ b/chia/server/server.py @@ -208,7 +208,9 @@ async def garbage_collect_connections_task(self) -> None: await asyncio.sleep(600 if is_crawler is None else 2) to_remove: List[WSChiaConnection] = [] for connection in self.all_connections.values(): - if self._local_type == NodeType.FULL_NODE and connection.connection_type == NodeType.FULL_NODE: + if ( + self._local_type == NodeType.FULL_NODE or self._local_type == NodeType.WALLET + ) and connection.connection_type == NodeType.FULL_NODE: if is_crawler is not None: if time.time() - connection.creation_time > 5: to_remove.append(connection) From 0ba1347e6de49e105b8bb19538b8f8e07ce2b0f6 Mon Sep 17 00:00:00 2001 From: David Barratt Date: Sun, 27 Feb 2022 13:08:38 -0500 Subject: [PATCH 139/378] Fix the year from 2021 to 2022 (#10462) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18f350ab8961..3f140f5acdf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project does not yet adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) for setuptools_scm/PEP 440 reasons. -## 1.3.0 Beta Chia blockchain 2021-2-10 +## 1.3.0 Beta Chia blockchain 2022-2-10 We at Chia have been working hard to bring all of our new features together into one easy-to-use release. Today, we’re proud to announce the beta release of our 1.3 client. Because this release is still in beta, we recommend that you only install it on non mission-critical systems. If you are running a large farm, you should wait for the full 1.3 release before upgrading. When will the full version of 1.3 be released? Soon. From 7fb26a7a142e297e608c2f086fb57b450d7826e4 Mon Sep 17 00:00:00 2001 From: Mariano Sorgente <3069354+mariano54@users.noreply.github.com> Date: Sun, 27 Feb 2022 23:34:37 -0500 Subject: [PATCH 140/378] Ms.unused code (#10453) * Remove old backup stuff * Remove references to CC (couloured coins) which is deprecated terminology * removed no longer used import Co-authored-by: Kyle Altendorf --- chia/cmds/wallet_funcs.py | 34 +------ chia/rpc/wallet_rpc_client.py | 34 +------ chia/wallet/cat_wallet/cat_wallet.py | 46 ++++----- chia/wallet/puzzles/genesis_checkers.py | 12 +-- chia/wallet/util/backup_utils.py | 124 ------------------------ mypy.ini | 2 +- tests/wallet/rpc/test_wallet_rpc.py | 4 +- 7 files changed, 34 insertions(+), 222 deletions(-) delete mode 100644 chia/wallet/util/backup_utils.py diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index 9e7e69562e5a..c4759e232a92 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -516,39 +516,7 @@ async def get_wallet(wallet_client: WalletRpcClient, fingerprint: int = None) -> log_in_response = await wallet_client.log_in(fingerprint) if log_in_response["success"] is False: - if log_in_response["error"] == "not_initialized": - use_cloud = True - if "backup_path" in log_in_response: - path = log_in_response["backup_path"] - print(f"Backup file from backup.chia.net downloaded and written to: {path}") - val = input("Do you want to use this file to restore from backup? (Y/N) ") - if val.lower() == "y": - log_in_response = await wallet_client.log_in_and_restore(fingerprint, path) - else: - use_cloud = False - - if "backup_path" not in log_in_response or use_cloud is False: - if use_cloud is True: - val = input( - "No online backup file found,\n Press S to skip restore from backup" - "\n Press F to use your own backup file: " - ) - else: - val = input( - "Cloud backup declined,\n Press S to skip restore from backup" - "\n Press F to use your own backup file: " - ) - - if val.lower() == "s": - log_in_response = await wallet_client.log_in_and_skip(fingerprint) - elif val.lower() == "f": - val = input("Please provide the full path to your backup file: ") - log_in_response = await wallet_client.log_in_and_restore(fingerprint, val) - - if "success" not in log_in_response or log_in_response["success"] is False: - if "error" in log_in_response: - error = log_in_response["error"] - print(f"Error: {log_in_response[error]}") + print(f"Login failed: {log_in_response}") return None return wallet_client, fingerprint diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index 08998b4befe4..78c9e7839fe2 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -1,4 +1,3 @@ -from pathlib import Path from typing import Dict, List, Optional, Any, Tuple from chia.pools.pool_wallet_info import PoolWalletInfo @@ -27,35 +26,12 @@ async def log_in(self, fingerprint: int) -> Dict: try: return await self.fetch( "log_in", - {"host": "https://backup.chia.net", "fingerprint": fingerprint, "type": "start"}, + {"fingerprint": fingerprint, "type": "start"}, ) except ValueError as e: return e.args[0] - async def log_in_and_restore(self, fingerprint: int, file_path) -> Dict: - try: - return await self.fetch( - "log_in", - { - "host": "https://backup.chia.net", - "fingerprint": fingerprint, - "type": "restore_backup", - "file_path": file_path, - }, - ) - except ValueError as e: - return e.args[0] - - async def log_in_and_skip(self, fingerprint: int) -> Dict: - try: - return await self.fetch( - "log_in", - {"host": "https://backup.chia.net", "fingerprint": fingerprint, "type": "skip"}, - ) - except ValueError as e: - return e.args[0] - async def get_logged_in_fingerprint(self) -> int: return (await self.fetch("get_logged_in_fingerprint", {}))["fingerprint"] @@ -193,9 +169,6 @@ async def delete_unconfirmed_transactions(self, wallet_id: str) -> None: ) return None - async def create_backup(self, file_path: Path) -> None: - return await self.fetch("create_backup", {"file_path": str(file_path.resolve())}) - async def get_farmed_amount(self) -> Dict: return await self.fetch("get_farmed_amount", {}) @@ -254,7 +227,6 @@ async def create_new_did_wallet(self, amount): "backup_dids": [], "num_of_backup_ids_needed": 0, "amount": amount, - "host": f"{self.hostname}:{self.port}", } response = await self.fetch("create_new_wallet", request) return response @@ -264,7 +236,6 @@ async def create_new_did_wallet_from_recovery(self, filename): "wallet_type": "did_wallet", "did_type": "recovery", "filename": filename, - "host": f"{self.hostname}:{self.port}", } response = await self.fetch("create_new_wallet", request) return response @@ -305,7 +276,6 @@ async def create_new_pool_wallet( request: Dict[str, Any] = { "wallet_type": "pool_wallet", "mode": mode, - "host": backup_host, "initial_target_state": { "target_puzzle_hash": target_puzzlehash.hex() if target_puzzlehash else None, "relative_lock_height": relative_lock_height, @@ -364,7 +334,6 @@ async def create_new_cat_and_wallet(self, amount: uint64) -> Dict: "wallet_type": "cat_wallet", "mode": "new", "amount": amount, - "host": f"{self.hostname}:{self.port}", } return await self.fetch("create_new_wallet", request) @@ -373,7 +342,6 @@ async def create_wallet_for_existing_cat(self, asset_id: bytes) -> Dict: "wallet_type": "cat_wallet", "asset_id": asset_id.hex(), "mode": "existing", - "host": f"{self.hostname}:{self.port}", } return await self.fetch("create_new_wallet", request) diff --git a/chia/wallet/cat_wallet/cat_wallet.py b/chia/wallet/cat_wallet/cat_wallet.py index de953bdde581..eec51c2c8377 100644 --- a/chia/wallet/cat_wallet/cat_wallet.py +++ b/chia/wallet/cat_wallet/cat_wallet.py @@ -108,7 +108,7 @@ async def create_new_cat_wallet( # Change and actual CAT coin non_ephemeral_coins: List[Coin] = spend_bundle.not_ephemeral_additions() - cc_coin = None + cat_coin = None puzzle_store = self.wallet_state_manager.puzzle_store for c in non_ephemeral_coins: info = await puzzle_store.wallet_info_for_puzzle_hash(c.puzzle_hash) @@ -116,23 +116,23 @@ async def create_new_cat_wallet( raise ValueError("Internal Error") id, wallet_type = info if id == self.id(): - cc_coin = c + cat_coin = c - if cc_coin is None: + if cat_coin is None: raise ValueError("Internal Error, unable to generate new CAT coin") - cc_pid: bytes32 = cc_coin.parent_coin_info + cat_pid: bytes32 = cat_coin.parent_coin_info - cc_record = TransactionRecord( + cat_record = TransactionRecord( confirmed_at_height=uint32(0), created_at_time=uint64(int(time.time())), - to_puzzle_hash=(await self.convert_puzzle_hash(cc_coin.puzzle_hash)), - amount=uint64(cc_coin.amount), + to_puzzle_hash=(await self.convert_puzzle_hash(cat_coin.puzzle_hash)), + amount=uint64(cat_coin.amount), fee_amount=uint64(0), confirmed=False, sent=uint32(10), spend_bundle=None, - additions=[cc_coin], - removals=list(filter(lambda rem: rem.name() == cc_pid, spend_bundle.removals())), + additions=[cat_coin], + removals=list(filter(lambda rem: rem.name() == cat_pid, spend_bundle.removals())), wallet_id=self.id(), sent_to=[], trade_id=None, @@ -142,7 +142,7 @@ async def create_new_cat_wallet( ) chia_tx = dataclasses.replace(chia_tx, spend_bundle=spend_bundle) await self.standard_wallet.push_transaction(chia_tx) - await self.standard_wallet.push_transaction(cc_record) + await self.standard_wallet.push_transaction(cat_record) return self @staticmethod @@ -223,7 +223,7 @@ async def get_confirmed_balance(self, record_list: Optional[Set[WalletCoinRecord if lineage is not None: amount = uint64(amount + record.coin.amount) - self.log.info(f"Confirmed balance for cc wallet {self.id()} is {amount}") + self.log.info(f"Confirmed balance for cat wallet {self.id()} is {amount}") return uint64(amount) async def get_unconfirmed_balance(self, unspent_records=None) -> uint128: @@ -287,9 +287,9 @@ async def set_tail_program(self, tail_program: str): async def coin_added(self, coin: Coin, height: uint32): """Notification from wallet state manager that wallet has been received.""" - self.log.info(f"CC wallet has been notified that {coin} was added") + self.log.info(f"CAT wallet has been notified that {coin} was added") - inner_puzzle = await self.inner_puzzle_for_cc_puzhash(coin.puzzle_hash) + inner_puzzle = await self.inner_puzzle_for_cat_puzhash(coin.puzzle_hash) lineage_proof = LineageProof(coin.parent_coin_info, inner_puzzle.get_tree_hash(), coin.amount) await self.add_lineage(coin.name(), lineage_proof) @@ -355,8 +355,8 @@ async def get_new_puzzlehash(self) -> bytes32: def puzzle_for_pk(self, pubkey) -> Program: inner_puzzle = self.standard_wallet.puzzle_for_pk(bytes(pubkey)) - cc_puzzle: Program = construct_cat_puzzle(CAT_MOD, self.cat_info.limitations_program_hash, inner_puzzle) - return cc_puzzle + cat_puzzle: Program = construct_cat_puzzle(CAT_MOD, self.cat_info.limitations_program_hash, inner_puzzle) + return cat_puzzle async def get_new_cat_puzzle_hash(self): return (await self.wallet_state_manager.get_unused_derivation_record(self.id())).puzzle_hash @@ -478,9 +478,9 @@ async def sign(self, spend_bundle: SpendBundle) -> SpendBundle: agg_sig = AugSchemeMPL.aggregate(sigs) return SpendBundle.aggregate([spend_bundle, SpendBundle([], agg_sig)]) - async def inner_puzzle_for_cc_puzhash(self, cc_hash: bytes32) -> Program: + async def inner_puzzle_for_cat_puzhash(self, cat_hash: bytes32) -> Program: record: DerivationRecord = await self.wallet_state_manager.puzzle_store.get_derivation_record_for_puzzle_hash( - cc_hash + cat_hash ) inner_puzzle: Program = self.standard_wallet.puzzle_for_pk(bytes(record.pubkey)) return inner_puzzle @@ -492,7 +492,7 @@ async def convert_puzzle_hash(self, puzzle_hash: bytes32) -> bytes32: if record is None: return puzzle_hash else: - return (await self.inner_puzzle_for_cc_puzhash(puzzle_hash)).get_tree_hash() + return (await self.inner_puzzle_for_cat_puzhash(puzzle_hash)).get_tree_hash() async def get_lineage_proof_for_coin(self, coin) -> Optional[LineageProof]: return await self.lineage_store.get_lineage_proof(coin.parent_coin_info) @@ -607,7 +607,7 @@ async def generate_unsigned_spendbundle( limitations_program_reveal = self.cat_info.my_tail # Loop through the coins we've selected and gather the information we need to spend them - spendable_cc_list = [] + spendable_cat_list = [] chia_tx = None first = True announcement: Announcement @@ -645,10 +645,10 @@ async def generate_unsigned_spendbundle( primaries=[], coin_announcements_to_assert={announcement.name()}, ) - inner_puzzle = await self.inner_puzzle_for_cc_puzhash(coin.puzzle_hash) + inner_puzzle = await self.inner_puzzle_for_cat_puzhash(coin.puzzle_hash) lineage_proof = await self.get_lineage_proof_for_coin(coin) assert lineage_proof is not None - new_spendable_cc = SpendableCAT( + new_spendable_cat = SpendableCAT( coin, self.cat_info.limitations_program_hash, inner_puzzle, @@ -658,9 +658,9 @@ async def generate_unsigned_spendbundle( lineage_proof=lineage_proof, limitations_program_reveal=limitations_program_reveal, ) - spendable_cc_list.append(new_spendable_cc) + spendable_cat_list.append(new_spendable_cat) - cat_spend_bundle = unsigned_spend_bundle_for_spendable_cats(CAT_MOD, spendable_cc_list) + cat_spend_bundle = unsigned_spend_bundle_for_spendable_cats(CAT_MOD, spendable_cat_list) chia_spend_bundle = SpendBundle([], G2Element()) if chia_tx is not None and chia_tx.spend_bundle is not None: chia_spend_bundle = chia_tx.spend_bundle diff --git a/chia/wallet/puzzles/genesis_checkers.py b/chia/wallet/puzzles/genesis_checkers.py index cbb02b6f9f56..5f2bb8f871fb 100644 --- a/chia/wallet/puzzles/genesis_checkers.py +++ b/chia/wallet/puzzles/genesis_checkers.py @@ -71,23 +71,23 @@ async def generate_issuance_bundle(cls, wallet, _: Dict, amount: uint64) -> Tupl origin = coins.copy().pop() origin_id = origin.name() - cc_inner: Program = await wallet.get_new_inner_puzzle() + cat_inner: Program = await wallet.get_new_inner_puzzle() await wallet.add_lineage(origin_id, LineageProof()) genesis_coin_checker: Program = cls.construct([Program.to(origin_id)]) - minted_cc_puzzle_hash: bytes32 = construct_cat_puzzle( - CAT_MOD, genesis_coin_checker.get_tree_hash(), cc_inner + minted_cat_puzzle_hash: bytes32 = construct_cat_puzzle( + CAT_MOD, genesis_coin_checker.get_tree_hash(), cat_inner ).get_tree_hash() tx_record: TransactionRecord = await wallet.standard_wallet.generate_signed_transaction( - amount, minted_cc_puzzle_hash, uint64(0), origin_id, coins + amount, minted_cat_puzzle_hash, uint64(0), origin_id, coins ) assert tx_record.spend_bundle is not None inner_solution = wallet.standard_wallet.add_condition_to_solution( Program.to([51, 0, -113, genesis_coin_checker, []]), wallet.standard_wallet.make_solution( - primaries=[{"puzzlehash": cc_inner.get_tree_hash(), "amount": amount}], + primaries=[{"puzzlehash": cat_inner.get_tree_hash(), "amount": amount}], ), ) eve_spend = unsigned_spend_bundle_for_spendable_cats( @@ -96,7 +96,7 @@ async def generate_issuance_bundle(cls, wallet, _: Dict, amount: uint64) -> Tupl SpendableCAT( list(filter(lambda a: a.amount == amount, tx_record.additions))[0], genesis_coin_checker.get_tree_hash(), - cc_inner, + cat_inner, inner_solution, limitations_program_reveal=genesis_coin_checker, ) diff --git a/chia/wallet/util/backup_utils.py b/chia/wallet/util/backup_utils.py deleted file mode 100644 index 6836b8c85f4a..000000000000 --- a/chia/wallet/util/backup_utils.py +++ /dev/null @@ -1,124 +0,0 @@ -import base64 -import json -from typing import Any - -import aiohttp -from blspy import AugSchemeMPL, PrivateKey, PublicKeyMPL, SignatureMPL -from cryptography.fernet import Fernet - -from chia.server.server import ssl_context_for_root -from chia.ssl.create_ssl import get_mozilla_ca_crt -from chia.util.byte_types import hexstr_to_bytes -from chia.util.hash import std_hash -from chia.wallet.derive_keys import master_sk_to_backup_sk -from chia.wallet.util.wallet_types import WalletType - - -def open_backup_file(file_path, private_key): - backup_file_text = file_path.read_text() - backup_file_json = json.loads(backup_file_text) - meta_data = backup_file_json["meta_data"] - meta_data_bytes = json.dumps(meta_data).encode() - sig = backup_file_json["signature"] - - backup_pk = master_sk_to_backup_sk(private_key) - my_pubkey = backup_pk.get_g1() - key_base_64 = base64.b64encode(bytes(backup_pk)) - f = Fernet(key_base_64) - - encrypted_data = backup_file_json["data"].encode() - msg = std_hash(encrypted_data) + std_hash(meta_data_bytes) - - signature = SignatureMPL.from_bytes(hexstr_to_bytes(sig)) - pubkey = PublicKeyMPL.from_bytes(hexstr_to_bytes(meta_data["pubkey"])) - - sig_match_my = AugSchemeMPL.verify(my_pubkey, msg, signature) - sig_match_backup = AugSchemeMPL.verify(pubkey, msg, signature) - - assert sig_match_my is True - assert sig_match_backup is True - - data_bytes = f.decrypt(encrypted_data) - data_text = data_bytes.decode() - data_json = json.loads(data_text) - unencrypted = {} - unencrypted["data"] = data_json - unencrypted["meta_data"] = meta_data - return unencrypted - - -def get_backup_info(file_path, private_key): - json_dict = open_backup_file(file_path, private_key) - data = json_dict["data"] - wallet_list_json = data["wallet_list"] - - info_dict = {} - wallets = [] - for wallet_info in wallet_list_json: - wallet = {} - wallet["name"] = wallet_info["name"] - wallet["type"] = wallet_info["type"] - wallet["type_name"] = WalletType(wallet_info["type"]).name - wallet["id"] = wallet_info["id"] - wallet["data"] = wallet_info["data"] - wallets.append(wallet) - - info_dict["version"] = data["version"] - info_dict["fingerprint"] = data["fingerprint"] - info_dict["timestamp"] = data["timestamp"] - info_dict["wallets"] = wallets - - return info_dict - - -async def post(session: aiohttp.ClientSession, url: str, data: Any): - mozilla_root = get_mozilla_ca_crt() - ssl_context = ssl_context_for_root(mozilla_root) - response = await session.post(url, json=data, ssl=ssl_context) - return await response.json() - - -async def get(session: aiohttp.ClientSession, url: str): - response = await session.get(url) - return await response.text() - - -async def upload_backup(host: str, backup_text: str): - request = {"backup": backup_text} - session = aiohttp.ClientSession() - nonce_url = f"{host}/upload_backup" - upload_response = await post(session, nonce_url, request) - await session.close() - return upload_response - - -async def download_backup(host: str, private_key: PrivateKey): - session = aiohttp.ClientSession() - try: - backup_privkey = master_sk_to_backup_sk(private_key) - backup_pubkey = bytes(backup_privkey.get_g1()).hex() - # Get nonce - nonce_request = {"pubkey": backup_pubkey} - nonce_url = f"{host}/get_download_nonce" - nonce_response = await post(session, nonce_url, nonce_request) - nonce = nonce_response["nonce"] - - # Sign nonce - signature = bytes(AugSchemeMPL.sign(backup_privkey, std_hash(hexstr_to_bytes(nonce)))).hex() - # Request backup url - get_backup_url = f"{host}/download_backup" - backup_request = {"pubkey": backup_pubkey, "signature": signature} - backup_response = await post(session, get_backup_url, backup_request) - - if backup_response["success"] is False: - raise ValueError("No backup on backup service") - - # Download from s3 - backup_url = backup_response["url"] - backup_text = await get(session, backup_url) - await session.close() - return backup_text - except Exception as e: - await session.close() - # Pass exception - raise e diff --git a/mypy.ini b/mypy.ini index 9a671dd0ae35..8dd59a99a268 100644 --- a/mypy.ini +++ b/mypy.ini @@ -17,7 +17,7 @@ no_implicit_reexport = True strict_equality = True # list created by: venv/bin/mypy | sed -n 's/.py:.*//p' | sort | uniq | tr '/' '.' | tr '\n' ',' -[mypy-benchmarks.block_ref,benchmarks.block_store,benchmarks.coin_store,benchmarks.utils,build_scripts.installer-version,chia.clvm.spend_sim,chia.cmds.configure,chia.cmds.db,chia.cmds.db_upgrade_func,chia.cmds.farm_funcs,chia.cmds.init,chia.cmds.init_funcs,chia.cmds.keys,chia.cmds.keys_funcs,chia.cmds.passphrase,chia.cmds.passphrase_funcs,chia.cmds.plotnft,chia.cmds.plotnft_funcs,chia.cmds.plots,chia.cmds.plotters,chia.cmds.show,chia.cmds.start_funcs,chia.cmds.wallet,chia.cmds.wallet_funcs,chia.consensus.block_body_validation,chia.consensus.blockchain,chia.consensus.blockchain_interface,chia.consensus.block_creation,chia.consensus.block_header_validation,chia.consensus.block_record,chia.consensus.block_root_validation,chia.consensus.coinbase,chia.consensus.constants,chia.consensus.difficulty_adjustment,chia.consensus.get_block_challenge,chia.consensus.multiprocess_validation,chia.consensus.pos_quality,chia.consensus.vdf_info_computation,chia.daemon.client,chia.daemon.keychain_proxy,chia.daemon.keychain_server,chia.daemon.server,chia.farmer.farmer,chia.farmer.farmer_api,chia.full_node.block_height_map,chia.full_node.block_store,chia.full_node.bundle_tools,chia.full_node.coin_store,chia.full_node.full_node,chia.full_node.full_node_api,chia.full_node.full_node_store,chia.full_node.generator,chia.full_node.hint_store,chia.full_node.lock_queue,chia.full_node.mempool,chia.full_node.mempool_check_conditions,chia.full_node.mempool_manager,chia.full_node.pending_tx_cache,chia.full_node.sync_store,chia.full_node.weight_proof,chia.harvester.harvester,chia.harvester.harvester_api,chia.introducer.introducer,chia.introducer.introducer_api,chia.plotters.bladebit,chia.plotters.chiapos,chia.plotters.install_plotter,chia.plotters.madmax,chia.plotters.plotters,chia.plotters.plotters_util,chia.plotting.check_plots,chia.plotting.create_plots,chia.plotting.manager,chia.plotting.util,chia.pools.pool_config,chia.pools.pool_puzzles,chia.pools.pool_wallet,chia.pools.pool_wallet_info,chia.protocols.pool_protocol,chia.rpc.crawler_rpc_api,chia.rpc.farmer_rpc_api,chia.rpc.farmer_rpc_client,chia.rpc.full_node_rpc_api,chia.rpc.full_node_rpc_client,chia.rpc.harvester_rpc_api,chia.rpc.harvester_rpc_client,chia.rpc.rpc_client,chia.rpc.rpc_server,chia.rpc.timelord_rpc_api,chia.rpc.util,chia.rpc.wallet_rpc_api,chia.rpc.wallet_rpc_client,chia.seeder.crawler,chia.seeder.crawler_api,chia.seeder.crawl_store,chia.seeder.dns_server,chia.seeder.peer_record,chia.seeder.start_crawler,chia.server.address_manager,chia.server.address_manager_store,chia.server.connection_utils,chia.server.introducer_peers,chia.server.node_discovery,chia.server.peer_store_resolver,chia.server.rate_limits,chia.server.reconnect_task,chia.server.server,chia.server.ssl_context,chia.server.start_farmer,chia.server.start_full_node,chia.server.start_harvester,chia.server.start_introducer,chia.server.start_service,chia.server.start_timelord,chia.server.start_wallet,chia.server.upnp,chia.server.ws_connection,chia.simulator.full_node_simulator,chia.simulator.start_simulator,chia.ssl.create_ssl,chia.timelord.iters_from_block,chia.timelord.timelord,chia.timelord.timelord_api,chia.timelord.timelord_launcher,chia.timelord.timelord_state,chia.types.announcement,chia.types.blockchain_format.classgroup,chia.types.blockchain_format.coin,chia.types.blockchain_format.program,chia.types.blockchain_format.proof_of_space,chia.types.blockchain_format.tree_hash,chia.types.blockchain_format.vdf,chia.types.full_block,chia.types.header_block,chia.types.mempool_item,chia.types.name_puzzle_condition,chia.types.peer_info,chia.types.spend_bundle,chia.types.transaction_queue_entry,chia.types.unfinished_block,chia.types.unfinished_header_block,chia.util.api_decorators,chia.util.block_cache,chia.util.byte_types,chia.util.cached_bls,chia.util.check_fork_next_block,chia.util.chia_logging,chia.util.config,chia.util.db_wrapper,chia.util.dump_keyring,chia.util.file_keyring,chia.util.files,chia.util.hash,chia.util.ints,chia.util.json_util,chia.util.keychain,chia.util.keyring_wrapper,chia.util.log_exceptions,chia.util.lru_cache,chia.util.make_test_constants,chia.util.merkle_set,chia.util.network,chia.util.partial_func,chia.util.pip_import,chia.util.profiler,chia.util.safe_cancel_task,chia.util.service_groups,chia.util.ssl_check,chia.util.streamable,chia.util.struct_stream,chia.util.type_checking,chia.util.validate_alert,chia.wallet.block_record,chia.wallet.cat_wallet.cat_utils,chia.wallet.cat_wallet.cat_wallet,chia.wallet.cat_wallet.lineage_store,chia.wallet.chialisp,chia.wallet.did_wallet.did_wallet,chia.wallet.did_wallet.did_wallet_puzzles,chia.wallet.key_val_store,chia.wallet.lineage_proof,chia.wallet.payment,chia.wallet.puzzles.genesis_checkers,chia.wallet.puzzles.load_clvm,chia.wallet.puzzles.p2_conditions,chia.wallet.puzzles.p2_delegated_conditions,chia.wallet.puzzles.p2_delegated_puzzle,chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle,chia.wallet.puzzles.p2_m_of_n_delegate_direct,chia.wallet.puzzles.p2_puzzle_hash,chia.wallet.puzzles.prefarm.spend_prefarm,chia.wallet.puzzles.puzzle_utils,chia.wallet.puzzles.rom_bootstrap_generator,chia.wallet.puzzles.singleton_top_layer,chia.wallet.puzzles.tails,chia.wallet.rl_wallet.rl_wallet,chia.wallet.rl_wallet.rl_wallet_puzzles,chia.wallet.secret_key_store,chia.wallet.settings.user_settings,chia.wallet.trade_manager,chia.wallet.trade_record,chia.wallet.trading.offer,chia.wallet.trading.trade_store,chia.wallet.transaction_record,chia.wallet.util.backup_utils,chia.wallet.util.debug_spend_bundle,chia.wallet.util.new_peak_queue,chia.wallet.util.peer_request_cache,chia.wallet.util.wallet_sync_utils,chia.wallet.wallet,chia.wallet.wallet_action_store,chia.wallet.wallet_blockchain,chia.wallet.wallet_coin_store,chia.wallet.wallet_interested_store,chia.wallet.wallet_node,chia.wallet.wallet_node_api,chia.wallet.wallet_pool_store,chia.wallet.wallet_puzzle_store,chia.wallet.wallet_state_manager,chia.wallet.wallet_sync_store,chia.wallet.wallet_transaction_store,chia.wallet.wallet_user_store,chia.wallet.wallet_weight_proof_handler,installhelper,tests.blockchain.blockchain_test_utils,tests.blockchain.test_blockchain,tests.blockchain.test_blockchain_transactions,tests.block_tools,tests.build-init-files,tests.build-workflows,tests.clvm.coin_store,tests.clvm.test_chialisp_deserialization,tests.clvm.test_clvm_compilation,tests.clvm.test_program,tests.clvm.test_puzzle_compression,tests.clvm.test_puzzles,tests.clvm.test_serialized_program,tests.clvm.test_singletons,tests.clvm.test_spend_sim,tests.conftest,tests.connection_utils,tests.core.cmds.test_keys,tests.core.consensus.test_pot_iterations,tests.core.custom_types.test_coin,tests.core.custom_types.test_proof_of_space,tests.core.custom_types.test_spend_bundle,tests.core.daemon.test_daemon,tests.core.full_node.full_sync.test_full_sync,tests.core.full_node.stores.test_block_store,tests.core.full_node.stores.test_coin_store,tests.core.full_node.stores.test_full_node_store,tests.core.full_node.stores.test_hint_store,tests.core.full_node.stores.test_sync_store,tests.core.full_node.test_address_manager,tests.core.full_node.test_block_height_map,tests.core.full_node.test_conditions,tests.core.full_node.test_full_node,tests.core.full_node.test_mempool,tests.core.full_node.test_mempool_performance,tests.core.full_node.test_node_load,tests.core.full_node.test_peer_store_resolver,tests.core.full_node.test_performance,tests.core.full_node.test_transactions,tests.core.make_block_generator,tests.core.node_height,tests.core.server.test_dos,tests.core.server.test_rate_limits,tests.core.ssl.test_ssl,tests.core.test_cost_calculation,tests.core.test_crawler_rpc,tests.core.test_daemon_rpc,tests.core.test_db_conversion,tests.core.test_farmer_harvester_rpc,tests.core.test_filter,tests.core.test_full_node_rpc,tests.core.test_merkle_set,tests.core.test_setproctitle,tests.core.util.test_cached_bls,tests.core.util.test_config,tests.core.util.test_file_keyring_synchronization,tests.core.util.test_files,tests.core.util.test_keychain,tests.core.util.test_keyring_wrapper,tests.core.util.test_lru_cache,tests.core.util.test_significant_bits,tests.core.util.test_streamable,tests.core.util.test_type_checking,tests.farmer_harvester.test_farmer_harvester,tests.generator.test_compression,tests.generator.test_generator_types,tests.generator.test_list_to_batches,tests.generator.test_rom,tests.generator.test_scan,tests.plotting.test_plot_manager,tests.pools.test_pool_cmdline,tests.pools.test_pool_config,tests.pools.test_pool_puzzles_lifecycle,tests.pools.test_pool_rpc,tests.pools.test_wallet_pool_store,tests.setup_nodes,tests.simulation.test_simulation,tests.time_out_assert,tests.tools.test_full_sync,tests.tools.test_run_block,tests.util.alert_server,tests.util.benchmark_cost,tests.util.blockchain,tests.util.build_network_protocol_files,tests.util.db_connection,tests.util.generator_tools_testing,tests.util.keyring,tests.util.key_tool,tests.util.misc,tests.util.network,tests.util.rpc,tests.util.test_full_block_utils,tests.util.test_lock_queue,tests.util.test_network_protocol_files,tests.util.test_struct_stream,tests.wallet.cat_wallet.test_cat_lifecycle,tests.wallet.cat_wallet.test_cat_wallet,tests.wallet.cat_wallet.test_offer_lifecycle,tests.wallet.cat_wallet.test_trades,tests.wallet.did_wallet.test_did,tests.wallet.did_wallet.test_did_rpc,tests.wallet.rl_wallet.test_rl_rpc,tests.wallet.rl_wallet.test_rl_wallet,tests.wallet.rpc.test_wallet_rpc,tests.wallet.simple_sync.test_simple_sync_protocol,tests.wallet.sync.test_wallet_sync,tests.wallet.test_bech32m,tests.wallet.test_chialisp,tests.wallet.test_puzzle_store,tests.wallet.test_singleton,tests.wallet.test_singleton_lifecycle,tests.wallet.test_singleton_lifecycle_fast,tests.wallet.test_taproot,tests.wallet.test_wallet,tests.wallet.test_wallet_blockchain,tests.wallet.test_wallet_interested_store,tests.wallet.test_wallet_key_val_store,tests.wallet.test_wallet_user_store,tests.wallet_tools,tests.weight_proof.test_weight_proof,tools.analyze-chain,tools.run_block,tools.test_full_sync] +[mypy-benchmarks.block_ref,benchmarks.block_store,benchmarks.coin_store,benchmarks.utils,build_scripts.installer-version,chia.clvm.spend_sim,chia.cmds.configure,chia.cmds.db,chia.cmds.db_upgrade_func,chia.cmds.farm_funcs,chia.cmds.init,chia.cmds.init_funcs,chia.cmds.keys,chia.cmds.keys_funcs,chia.cmds.passphrase,chia.cmds.passphrase_funcs,chia.cmds.plotnft,chia.cmds.plotnft_funcs,chia.cmds.plots,chia.cmds.plotters,chia.cmds.show,chia.cmds.start_funcs,chia.cmds.wallet,chia.cmds.wallet_funcs,chia.consensus.block_body_validation,chia.consensus.blockchain,chia.consensus.blockchain_interface,chia.consensus.block_creation,chia.consensus.block_header_validation,chia.consensus.block_record,chia.consensus.block_root_validation,chia.consensus.coinbase,chia.consensus.constants,chia.consensus.difficulty_adjustment,chia.consensus.get_block_challenge,chia.consensus.multiprocess_validation,chia.consensus.pos_quality,chia.consensus.vdf_info_computation,chia.daemon.client,chia.daemon.keychain_proxy,chia.daemon.keychain_server,chia.daemon.server,chia.farmer.farmer,chia.farmer.farmer_api,chia.full_node.block_height_map,chia.full_node.block_store,chia.full_node.bundle_tools,chia.full_node.coin_store,chia.full_node.full_node,chia.full_node.full_node_api,chia.full_node.full_node_store,chia.full_node.generator,chia.full_node.hint_store,chia.full_node.lock_queue,chia.full_node.mempool,chia.full_node.mempool_check_conditions,chia.full_node.mempool_manager,chia.full_node.pending_tx_cache,chia.full_node.sync_store,chia.full_node.weight_proof,chia.harvester.harvester,chia.harvester.harvester_api,chia.introducer.introducer,chia.introducer.introducer_api,chia.plotters.bladebit,chia.plotters.chiapos,chia.plotters.install_plotter,chia.plotters.madmax,chia.plotters.plotters,chia.plotters.plotters_util,chia.plotting.check_plots,chia.plotting.create_plots,chia.plotting.manager,chia.plotting.util,chia.pools.pool_config,chia.pools.pool_puzzles,chia.pools.pool_wallet,chia.pools.pool_wallet_info,chia.protocols.pool_protocol,chia.rpc.crawler_rpc_api,chia.rpc.farmer_rpc_api,chia.rpc.farmer_rpc_client,chia.rpc.full_node_rpc_api,chia.rpc.full_node_rpc_client,chia.rpc.harvester_rpc_api,chia.rpc.harvester_rpc_client,chia.rpc.rpc_client,chia.rpc.rpc_server,chia.rpc.timelord_rpc_api,chia.rpc.util,chia.rpc.wallet_rpc_api,chia.rpc.wallet_rpc_client,chia.seeder.crawler,chia.seeder.crawler_api,chia.seeder.crawl_store,chia.seeder.dns_server,chia.seeder.peer_record,chia.seeder.start_crawler,chia.server.address_manager,chia.server.address_manager_store,chia.server.connection_utils,chia.server.introducer_peers,chia.server.node_discovery,chia.server.peer_store_resolver,chia.server.rate_limits,chia.server.reconnect_task,chia.server.server,chia.server.ssl_context,chia.server.start_farmer,chia.server.start_full_node,chia.server.start_harvester,chia.server.start_introducer,chia.server.start_service,chia.server.start_timelord,chia.server.start_wallet,chia.server.upnp,chia.server.ws_connection,chia.simulator.full_node_simulator,chia.simulator.start_simulator,chia.ssl.create_ssl,chia.timelord.iters_from_block,chia.timelord.timelord,chia.timelord.timelord_api,chia.timelord.timelord_launcher,chia.timelord.timelord_state,chia.types.announcement,chia.types.blockchain_format.classgroup,chia.types.blockchain_format.coin,chia.types.blockchain_format.program,chia.types.blockchain_format.proof_of_space,chia.types.blockchain_format.tree_hash,chia.types.blockchain_format.vdf,chia.types.full_block,chia.types.header_block,chia.types.mempool_item,chia.types.name_puzzle_condition,chia.types.peer_info,chia.types.spend_bundle,chia.types.transaction_queue_entry,chia.types.unfinished_block,chia.types.unfinished_header_block,chia.util.api_decorators,chia.util.block_cache,chia.util.byte_types,chia.util.cached_bls,chia.util.check_fork_next_block,chia.util.chia_logging,chia.util.config,chia.util.db_wrapper,chia.util.dump_keyring,chia.util.file_keyring,chia.util.files,chia.util.hash,chia.util.ints,chia.util.json_util,chia.util.keychain,chia.util.keyring_wrapper,chia.util.log_exceptions,chia.util.lru_cache,chia.util.make_test_constants,chia.util.merkle_set,chia.util.network,chia.util.partial_func,chia.util.pip_import,chia.util.profiler,chia.util.safe_cancel_task,chia.util.service_groups,chia.util.ssl_check,chia.util.streamable,chia.util.struct_stream,chia.util.type_checking,chia.util.validate_alert,chia.wallet.block_record,chia.wallet.cat_wallet.cat_utils,chia.wallet.cat_wallet.cat_wallet,chia.wallet.cat_wallet.lineage_store,chia.wallet.chialisp,chia.wallet.did_wallet.did_wallet,chia.wallet.did_wallet.did_wallet_puzzles,chia.wallet.key_val_store,chia.wallet.lineage_proof,chia.wallet.payment,chia.wallet.puzzles.genesis_checkers,chia.wallet.puzzles.load_clvm,chia.wallet.puzzles.p2_conditions,chia.wallet.puzzles.p2_delegated_conditions,chia.wallet.puzzles.p2_delegated_puzzle,chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle,chia.wallet.puzzles.p2_m_of_n_delegate_direct,chia.wallet.puzzles.p2_puzzle_hash,chia.wallet.puzzles.prefarm.spend_prefarm,chia.wallet.puzzles.puzzle_utils,chia.wallet.puzzles.rom_bootstrap_generator,chia.wallet.puzzles.singleton_top_layer,chia.wallet.puzzles.tails,chia.wallet.rl_wallet.rl_wallet,chia.wallet.rl_wallet.rl_wallet_puzzles,chia.wallet.secret_key_store,chia.wallet.settings.user_settings,chia.wallet.trade_manager,chia.wallet.trade_record,chia.wallet.trading.offer,chia.wallet.trading.trade_store,chia.wallet.transaction_record,chia.wallet.util.debug_spend_bundle,chia.wallet.util.new_peak_queue,chia.wallet.util.peer_request_cache,chia.wallet.util.wallet_sync_utils,chia.wallet.wallet,chia.wallet.wallet_action_store,chia.wallet.wallet_blockchain,chia.wallet.wallet_coin_store,chia.wallet.wallet_interested_store,chia.wallet.wallet_node,chia.wallet.wallet_node_api,chia.wallet.wallet_pool_store,chia.wallet.wallet_puzzle_store,chia.wallet.wallet_state_manager,chia.wallet.wallet_sync_store,chia.wallet.wallet_transaction_store,chia.wallet.wallet_user_store,chia.wallet.wallet_weight_proof_handler,installhelper,tests.blockchain.blockchain_test_utils,tests.blockchain.test_blockchain,tests.blockchain.test_blockchain_transactions,tests.block_tools,tests.build-init-files,tests.build-workflows,tests.clvm.coin_store,tests.clvm.test_chialisp_deserialization,tests.clvm.test_clvm_compilation,tests.clvm.test_program,tests.clvm.test_puzzle_compression,tests.clvm.test_puzzles,tests.clvm.test_serialized_program,tests.clvm.test_singletons,tests.clvm.test_spend_sim,tests.conftest,tests.connection_utils,tests.core.cmds.test_keys,tests.core.consensus.test_pot_iterations,tests.core.custom_types.test_coin,tests.core.custom_types.test_proof_of_space,tests.core.custom_types.test_spend_bundle,tests.core.daemon.test_daemon,tests.core.full_node.full_sync.test_full_sync,tests.core.full_node.stores.test_block_store,tests.core.full_node.stores.test_coin_store,tests.core.full_node.stores.test_full_node_store,tests.core.full_node.stores.test_hint_store,tests.core.full_node.stores.test_sync_store,tests.core.full_node.test_address_manager,tests.core.full_node.test_block_height_map,tests.core.full_node.test_conditions,tests.core.full_node.test_full_node,tests.core.full_node.test_mempool,tests.core.full_node.test_mempool_performance,tests.core.full_node.test_node_load,tests.core.full_node.test_peer_store_resolver,tests.core.full_node.test_performance,tests.core.full_node.test_transactions,tests.core.make_block_generator,tests.core.node_height,tests.core.server.test_dos,tests.core.server.test_rate_limits,tests.core.ssl.test_ssl,tests.core.test_cost_calculation,tests.core.test_crawler_rpc,tests.core.test_daemon_rpc,tests.core.test_db_conversion,tests.core.test_farmer_harvester_rpc,tests.core.test_filter,tests.core.test_full_node_rpc,tests.core.test_merkle_set,tests.core.test_setproctitle,tests.core.util.test_cached_bls,tests.core.util.test_config,tests.core.util.test_file_keyring_synchronization,tests.core.util.test_files,tests.core.util.test_keychain,tests.core.util.test_keyring_wrapper,tests.core.util.test_lru_cache,tests.core.util.test_significant_bits,tests.core.util.test_streamable,tests.core.util.test_type_checking,tests.farmer_harvester.test_farmer_harvester,tests.generator.test_compression,tests.generator.test_generator_types,tests.generator.test_list_to_batches,tests.generator.test_rom,tests.generator.test_scan,tests.plotting.test_plot_manager,tests.pools.test_pool_cmdline,tests.pools.test_pool_config,tests.pools.test_pool_puzzles_lifecycle,tests.pools.test_pool_rpc,tests.pools.test_wallet_pool_store,tests.setup_nodes,tests.simulation.test_simulation,tests.time_out_assert,tests.tools.test_full_sync,tests.tools.test_run_block,tests.util.alert_server,tests.util.benchmark_cost,tests.util.blockchain,tests.util.build_network_protocol_files,tests.util.db_connection,tests.util.generator_tools_testing,tests.util.keyring,tests.util.key_tool,tests.util.misc,tests.util.network,tests.util.rpc,tests.util.test_full_block_utils,tests.util.test_lock_queue,tests.util.test_network_protocol_files,tests.util.test_struct_stream,tests.wallet.cat_wallet.test_cat_lifecycle,tests.wallet.cat_wallet.test_cat_wallet,tests.wallet.cat_wallet.test_offer_lifecycle,tests.wallet.cat_wallet.test_trades,tests.wallet.did_wallet.test_did,tests.wallet.did_wallet.test_did_rpc,tests.wallet.rl_wallet.test_rl_rpc,tests.wallet.rl_wallet.test_rl_wallet,tests.wallet.rpc.test_wallet_rpc,tests.wallet.simple_sync.test_simple_sync_protocol,tests.wallet.sync.test_wallet_sync,tests.wallet.test_bech32m,tests.wallet.test_chialisp,tests.wallet.test_puzzle_store,tests.wallet.test_singleton,tests.wallet.test_singleton_lifecycle,tests.wallet.test_singleton_lifecycle_fast,tests.wallet.test_taproot,tests.wallet.test_wallet,tests.wallet.test_wallet_blockchain,tests.wallet.test_wallet_interested_store,tests.wallet.test_wallet_key_val_store,tests.wallet.test_wallet_user_store,tests.wallet_tools,tests.weight_proof.test_weight_proof,tools.analyze-chain,tools.run_block,tools.test_full_sync] disallow_any_generics = False disallow_subclassing_any = False disallow_untyped_calls = False diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index 11b416be07b6..79c10ed2cce8 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -585,7 +585,7 @@ async def tx_in_mempool_2(): pks = await client.get_public_keys() assert len(pks) == 2 - await client.log_in_and_skip(pks[1]) + await client.log_in(pks[1]) sk_dict = await client.get_private_key(pks[1]) assert sk_dict["fingerprint"] == pks[1] @@ -620,7 +620,7 @@ async def tx_in_mempool_2(): assert sk_dict["used_for_pool_rewards"] is False await client.delete_key(pks[0]) - await client.log_in_and_skip(pks[1]) + await client.log_in(pks[1]) assert len(await client.get_public_keys()) == 1 assert not (await client.get_sync_status()) From be2d8267aaa7bdc9c40115c1cd84b15146fdf0aa Mon Sep 17 00:00:00 2001 From: Chris Marslender Date: Mon, 28 Feb 2022 17:21:31 -0600 Subject: [PATCH 141/378] Don't load initial data older than 5 days (#10481) * When loading initial data, don't include IPs older than 5 days in the best timestamp dict * Don't load version data for hosts older than 5 days --- chia/seeder/crawl_store.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/chia/seeder/crawl_store.py b/chia/seeder/crawl_store.py index 8fd5496d9266..ccae0fc7fae4 100644 --- a/chia/seeder/crawl_store.py +++ b/chia/seeder/crawl_store.py @@ -359,6 +359,8 @@ def load_host_to_version(self): record = self.host_to_records[host] if record.version == "undefined": continue + if record.handshake_time < time.time() - 5 * 24 * 3600: + continue versions[host] = record.version handshake[host] = record.handshake_time @@ -367,7 +369,8 @@ def load_host_to_version(self): def load_best_peer_reliability(self): best_timestamp = {} for host, record in self.host_to_records.items(): - best_timestamp[host] = record.best_timestamp + if record.best_timestamp > time.time() - 5 * 24 * 3600: + best_timestamp[host] = record.best_timestamp return best_timestamp async def update_version(self, host, version, now): From 098cbf8b73f443f57053d3a8221b6eff099e4773 Mon Sep 17 00:00:00 2001 From: Jeff Date: Mon, 28 Feb 2022 15:21:53 -0800 Subject: [PATCH 142/378] Update the DMG background image (#10289) * Update the DMG background image * Updated with latest design. * Updated background.tiff with latest design * Fancier DMG customization support via build_dmg.js * npm_macos -> npm_macos_m1 * Pass in the app path to build_dmg.js * Peppering with __init__.py files to satisfy the precommit hook --- build_scripts/assets/__init__.py | 0 build_scripts/assets/dmg/README | 9 ++++ build_scripts/assets/dmg/__init__.py | 0 build_scripts/assets/dmg/background.tiff | Bin 0 -> 389022 bytes build_scripts/build_dmg.js | 66 +++++++++++++++++++++++ build_scripts/build_macos.sh | 3 +- build_scripts/build_macos_m1.sh | 3 +- 7 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 build_scripts/assets/__init__.py create mode 100644 build_scripts/assets/dmg/README create mode 100644 build_scripts/assets/dmg/__init__.py create mode 100644 build_scripts/assets/dmg/background.tiff create mode 100644 build_scripts/build_dmg.js diff --git a/build_scripts/assets/__init__.py b/build_scripts/assets/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/build_scripts/assets/dmg/README b/build_scripts/assets/dmg/README new file mode 100644 index 000000000000..6ae23efdb237 --- /dev/null +++ b/build_scripts/assets/dmg/README @@ -0,0 +1,9 @@ +To update the DMG background image, create 1x and 2x versions of a background: + +1x: background.png +2x: background@2x.png + +Create a single TIFF combining both 1x and 2x background bitmaps: +Run 'tiffutil -cathidpicheck background.png background@2x.png -out background.tiff' + +Use background.tiff as the DMG background diff --git a/build_scripts/assets/dmg/__init__.py b/build_scripts/assets/dmg/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/build_scripts/assets/dmg/background.tiff b/build_scripts/assets/dmg/background.tiff new file mode 100644 index 0000000000000000000000000000000000000000..1661c36475d1b9172afa0642a7426bbf654cbe68 GIT binary patch literal 389022 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%0D>itu{2xK9tc4PX_7i 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%0DxTspTjxT4Fw*2?!8Q?=pN7Y{r{7%zW-nZ0f0^2 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 KlP4Z0C;t!C_{@R; literal 0 HcmV?d00001 diff --git a/build_scripts/build_dmg.js b/build_scripts/build_dmg.js new file mode 100644 index 000000000000..baaccc91ab88 --- /dev/null +++ b/build_scripts/build_dmg.js @@ -0,0 +1,66 @@ +const createDMG = require('electron-installer-dmg'); + +// Return positioning params for the DMG contents. x,y coordinates represent the +// item's center point. +function getContents(opts) { + return [ + { + x: 466, + y: 344, + type: 'link', + path: '/Applications', + }, + { + x: 192, + y: 344, + type: 'file', + path: opts.appPath, + } + ] +} + +async function main(opts) { + console.log(`DMG creation options: ${JSON.stringify(opts, null, 2)}`); + + const { appPath, appName, dmgIcon, dmgBackground, outputDir, appVersion } = opts; + const dmgName = appName + (appVersion ? `-${appVersion}` : ''); + const dmgTitle = dmgName; + + console.log(`DMG name set to: ${dmgName}`); + console.log(`DMG title set to: ${dmgTitle}`); + + console.log('Creating DMG...'); + await createDMG({ + appPath: appPath, + name: dmgName, + title: dmgTitle, + icon: dmgIcon, + background: dmgBackground, + contents: getContents, + overwrite: true, + out: outputDir, + }); + + console.log('Finished'); +} + +const appName = 'Chia'; +const dmgIcon = '../chia-blockchain-gui/packages/gui/src/assets/img/Chia.icns'; +const dmgBackground = './assets/dmg/background.tiff'; +const outputDir = './final_installer'; +const appPath = process.argv[2]; // required +const appVersion = process.argv[3]; // undefined is ok + +if (!appPath) { + console.error('appPath is required'); + process.exit(1); +} + +main({ + appPath, + appName, + dmgIcon, + dmgBackground, + outputDir, + appVersion, +}); diff --git a/build_scripts/build_macos.sh b/build_scripts/build_macos.sh index 32617dda6ec6..38758c099fa1 100644 --- a/build_scripts/build_macos.sh +++ b/build_scripts/build_macos.sh @@ -88,8 +88,7 @@ cd ../../../build_scripts || exit DMG_NAME="Chia-$CHIA_INSTALLER_VERSION.dmg" echo "Create $DMG_NAME" mkdir final_installer -electron-installer-dmg dist/Chia-darwin-x64/Chia.app Chia-$CHIA_INSTALLER_VERSION \ ---overwrite --out final_installer +NODE_PATH=./npm_macos/node_modules node build_dmg.js dist/Chia-darwin-x64/Chia.app $CHIA_INSTALLER_VERSION LAST_EXIT_CODE=$? if [ "$LAST_EXIT_CODE" -ne 0 ]; then echo >&2 "electron-installer-dmg failed!" diff --git a/build_scripts/build_macos_m1.sh b/build_scripts/build_macos_m1.sh index a48ff267eba8..5e01580e4e29 100644 --- a/build_scripts/build_macos_m1.sh +++ b/build_scripts/build_macos_m1.sh @@ -90,8 +90,7 @@ cd ../../../build_scripts || exit DMG_NAME="Chia-$CHIA_INSTALLER_VERSION-arm64.dmg" echo "Create $DMG_NAME" mkdir final_installer -electron-installer-dmg dist/Chia-darwin-arm64/Chia.app Chia-$CHIA_INSTALLER_VERSION-arm64 \ ---overwrite --out final_installer +NODE_PATH=./npm_macos_m1/node_modules node build_dmg.js dist/Chia-darwin-arm64/Chia.app $CHIA_INSTALLER_VERSION-arm64 LAST_EXIT_CODE=$? if [ "$LAST_EXIT_CODE" -ne 0 ]; then echo >&2 "electron-installer-dmg failed!" From 941a91b36b5e0015e6a2611041ed40af5794bf90 Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Tue, 1 Mar 2022 09:39:00 -0700 Subject: [PATCH 143/378] Return the fees of an offer via RPC (#10480) --- chia/rpc/wallet_rpc_api.py | 2 +- tests/wallet/rpc/test_wallet_rpc.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 13fbe4561493..5e32623680d0 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -913,7 +913,7 @@ async def get_offer_summary(self, request): offer = Offer.from_bech32(offer_hex) offered, requested = offer.summary() - return {"summary": {"offered": offered, "requested": requested}} + return {"summary": {"offered": offered, "requested": requested, "fees": offer.bundle.fees()}} async def check_offer_validity(self, request): assert self.service.wallet_state_manager is not None diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index 79c10ed2cce8..630688e74007 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -475,7 +475,7 @@ async def eventual_balance_det(c, wallet_id: str): offer, trade_record = await client.create_offer_for_ids({uint32(1): -5, cat_0_id: 1}, fee=uint64(1)) summary = await client.get_offer_summary(offer) - assert summary == {"offered": {"xch": 5}, "requested": {col.hex(): 1}} + assert summary == {"offered": {"xch": 5}, "requested": {col.hex(): 1}, "fees": 1} assert await client.check_offer_validity(offer) From 695d9f7ef04156b058a9fe7be9fc3a3adff09807 Mon Sep 17 00:00:00 2001 From: dustinface <35775977+xdustinface@users.noreply.github.com> Date: Tue, 1 Mar 2022 19:11:21 +0100 Subject: [PATCH 144/378] farmer: Cleanup request retry and some logs (#10484) * farmer: Bump next update times regardless of the request results * farmer: Drop additional "success/failure" log logic We already print the PUT response in `_pool_put_farmer` the other parts just lead to confusion if the pool didn't implement the PUT correct. * farmer: Print error responses from the pool with `WARNING` log level --- chia/farmer/farmer.py | 47 ++++++++++++------------------------------- 1 file changed, 13 insertions(+), 34 deletions(-) diff --git a/chia/farmer/farmer.py b/chia/farmer/farmer.py index a540f06b9ff3..43ffeba13611 100644 --- a/chia/farmer/farmer.py +++ b/chia/farmer/farmer.py @@ -317,9 +317,11 @@ async def _pool_get_farmer( ) as resp: if resp.ok: response: Dict = json.loads(await resp.text()) - self.log.info(f"GET /farmer response: {response}") + log_level = logging.INFO if "error_code" in response: + log_level = logging.WARNING self.pool_state[pool_config.p2_singleton_puzzle_hash]["pool_errors_24h"].append(response) + self.log.log(log_level, f"GET /farmer response: {response}") return response else: self.handle_failed_pool_response( @@ -357,9 +359,11 @@ async def _pool_post_farmer( ) as resp: if resp.ok: response: Dict = json.loads(await resp.text()) - self.log.info(f"POST /farmer response: {response}") + log_level = logging.INFO if "error_code" in response: + log_level = logging.WARNING self.pool_state[pool_config.p2_singleton_puzzle_hash]["pool_errors_24h"].append(response) + self.log.log(log_level, f"POST /farmer response: {response}") return response else: self.handle_failed_pool_response( @@ -374,7 +378,7 @@ async def _pool_post_farmer( async def _pool_put_farmer( self, pool_config: PoolWalletConfig, authentication_token_timeout: uint8, owner_sk: PrivateKey - ) -> Optional[Dict]: + ) -> None: auth_sk: Optional[PrivateKey] = self.get_authentication_sk(pool_config) assert auth_sk is not None put_farmer_payload: PutFarmerPayload = PutFarmerPayload( @@ -397,10 +401,11 @@ async def _pool_put_farmer( ) as resp: if resp.ok: response: Dict = json.loads(await resp.text()) - self.log.info(f"PUT /farmer response: {response}") + log_level = logging.INFO if "error_code" in response: + log_level = logging.WARNING self.pool_state[pool_config.p2_singleton_puzzle_hash]["pool_errors_24h"].append(response) - return response + self.log.log(log_level, f"PUT /farmer response: {response}") else: self.handle_failed_pool_response( pool_config.p2_singleton_puzzle_hash, @@ -410,7 +415,6 @@ async def _pool_put_farmer( self.handle_failed_pool_response( pool_config.p2_singleton_puzzle_hash, f"Exception in PUT /farmer {pool_config.pool_url}, {e}" ) - return None def get_authentication_sk(self, pool_config: PoolWalletConfig) -> Optional[PrivateKey]: if pool_config.p2_singleton_puzzle_hash in self.authentication_keys: @@ -464,16 +468,17 @@ async def update_pool_state(self): # TODO: Improve error handling below, inform about unexpected failures if time.time() >= pool_state["next_pool_info_update"]: + pool_state["next_pool_info_update"] = time.time() + UPDATE_POOL_INFO_INTERVAL # Makes a GET request to the pool to get the updated information pool_info = await self._pool_get_pool_info(pool_config) if pool_info is not None and "error_code" not in pool_info: pool_state["authentication_token_timeout"] = pool_info["authentication_token_timeout"] - pool_state["next_pool_info_update"] = time.time() + UPDATE_POOL_INFO_INTERVAL # Only update the first time from GET /pool_info, gets updated from GET /farmer later if pool_state["current_difficulty"] is None: pool_state["current_difficulty"] = pool_info["minimum_difficulty"] if time.time() >= pool_state["next_farmer_update"]: + pool_state["next_farmer_update"] = time.time() + UPDATE_POOL_FARMER_INFO_INTERVAL authentication_token_timeout = pool_state["authentication_token_timeout"] async def update_pool_farmer_info() -> Tuple[Optional[GetFarmerResponse], Optional[PoolErrorCode]]: @@ -489,7 +494,6 @@ async def update_pool_farmer_info() -> Tuple[Optional[GetFarmerResponse], Option if farmer_response is not None: pool_state["current_difficulty"] = farmer_response.current_difficulty pool_state["current_points"] = farmer_response.current_points - pool_state["next_farmer_update"] = time.time() + UPDATE_POOL_FARMER_INFO_INTERVAL else: try: error_code_response = PoolErrorCode(response["error_code"]) @@ -498,11 +502,6 @@ async def update_pool_farmer_info() -> Tuple[Optional[GetFarmerResponse], Option f"Invalid error code received from the pool: {response['error_code']}" ) - self.log.error( - "update_pool_farmer_info failed: " - f"{response['error_code']}, {response['error_message']}" - ) - return farmer_response, error_code_response if authentication_token_timeout is not None: @@ -537,29 +536,9 @@ async def update_pool_farmer_info() -> Tuple[Optional[GetFarmerResponse], Option self.all_root_sks, pool_config.owner_public_key ) assert owner_sk_and_index is not None - put_farmer_response_dict = await self._pool_put_farmer( + await self._pool_put_farmer( pool_config, authentication_token_timeout, owner_sk_and_index[0] ) - try: - # put_farmer_response: PutFarmerResponse = PutFarmerResponse.from_json_dict( - # put_farmer_response_dict - # ) - # if put_farmer_response.payout_instructions: - # self.log.info( - # f"Farmer information successfully updated on the pool {pool_config.pool_url}" - # ) - # TODO: Fix Streamable implementation and recover the above. - if put_farmer_response_dict["payout_instructions"]: - self.log.info( - f"Farmer information successfully updated on the pool {pool_config.pool_url}" - ) - else: - raise Exception - except Exception: - self.log.error( - f"Failed to update farmer information on the pool {pool_config.pool_url}" - ) - else: self.log.warning( f"No pool specific authentication_token_timeout has been set for {p2_singleton_puzzle_hash}" From 90ebd287ab44d30ab6a823ca1d28bad7f4664da3 Mon Sep 17 00:00:00 2001 From: Mariano Sorgente <3069354+mariano54@users.noreply.github.com> Date: Tue, 1 Mar 2022 13:49:41 -0500 Subject: [PATCH 145/378] Fix method name (#10500) --- chia/protocols/protocol_state_machine.py | 1 + chia/wallet/wallet_node_api.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/chia/protocols/protocol_state_machine.py b/chia/protocols/protocol_state_machine.py index 1be6bba9c335..030c5da84d10 100644 --- a/chia/protocols/protocol_state_machine.py +++ b/chia/protocols/protocol_state_machine.py @@ -27,6 +27,7 @@ pmt.request_block: [pmt.respond_block, pmt.reject_block], pmt.request_blocks: [pmt.respond_blocks, pmt.reject_blocks], pmt.request_unfinished_block: [pmt.respond_unfinished_block], + pmt.request_block_header: [pmt.respond_block_header, pmt.reject_header_request], pmt.request_signage_point_or_end_of_sub_slot: [pmt.respond_signage_point, pmt.respond_end_of_sub_slot], pmt.request_compact_vdf: [pmt.respond_compact_vdf], pmt.request_peers: [pmt.respond_peers], diff --git a/chia/wallet/wallet_node_api.py b/chia/wallet/wallet_node_api.py index ec16eef24dc7..ddef660c4e95 100644 --- a/chia/wallet/wallet_node_api.py +++ b/chia/wallet/wallet_node_api.py @@ -50,7 +50,7 @@ async def new_peak_wallet(self, peak: wallet_protocol.NewPeakWallet, peer: WSChi await self.wallet_node.new_peak_queue.new_peak_wallet(peak, peer) @api_request - async def reject_block_header(self, response: wallet_protocol.RejectHeaderRequest): + async def reject_header_request(self, response: wallet_protocol.RejectHeaderRequest): """ The full node has rejected our request for a header. """ From 4681a57f48ab3a71a00974761a567271b752f449 Mon Sep 17 00:00:00 2001 From: William Blanke Date: Tue, 1 Mar 2022 10:52:42 -0800 Subject: [PATCH 146/378] updated gui to e2202874e1cb922a57370a47b2aec7bcf152b57d --- chia-blockchain-gui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia-blockchain-gui b/chia-blockchain-gui index 800a0f6556b8..e2202874e1cb 160000 --- a/chia-blockchain-gui +++ b/chia-blockchain-gui @@ -1 +1 @@ -Subproject commit 800a0f6556b89e928b1cf027c996b5ed010a7799 +Subproject commit e2202874e1cb922a57370a47b2aec7bcf152b57d From 13253def747f6def5955d76ef5f4481edc5a7e40 Mon Sep 17 00:00:00 2001 From: William Blanke Date: Tue, 1 Mar 2022 12:29:06 -0800 Subject: [PATCH 147/378] reverted to gui 800a0f6556b89e928b1cf027c996b5ed010a7799 --- chia-blockchain-gui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia-blockchain-gui b/chia-blockchain-gui index e2202874e1cb..800a0f6556b8 160000 --- a/chia-blockchain-gui +++ b/chia-blockchain-gui @@ -1 +1 @@ -Subproject commit e2202874e1cb922a57370a47b2aec7bcf152b57d +Subproject commit 800a0f6556b89e928b1cf027c996b5ed010a7799 From da5962cf2ea4768d326d58c059061cb74056cbee Mon Sep 17 00:00:00 2001 From: William Blanke Date: Tue, 1 Mar 2022 14:14:29 -0800 Subject: [PATCH 148/378] updated gui to 672cf2a74ade67a868df232772bd6358bce8dedf --- chia-blockchain-gui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia-blockchain-gui b/chia-blockchain-gui index 800a0f6556b8..672cf2a74ade 160000 --- a/chia-blockchain-gui +++ b/chia-blockchain-gui @@ -1 +1 @@ -Subproject commit 800a0f6556b89e928b1cf027c996b5ed010a7799 +Subproject commit 672cf2a74ade67a868df232772bd6358bce8dedf From 9bf80b49e73c07fda1ffe04789ff50ce12bf8f20 Mon Sep 17 00:00:00 2001 From: Mariano Sorgente <3069354+mariano54@users.noreply.github.com> Date: Wed, 2 Mar 2022 12:43:11 -0500 Subject: [PATCH 149/378] Only rewrite config when there is a difference (#10522) * Only rewrite config when there is a difference * Use variable --- chia/pools/pool_config.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/chia/pools/pool_config.py b/chia/pools/pool_config.py index 2fca70176870..07d2bd05f065 100644 --- a/chia/pools/pool_config.py +++ b/chia/pools/pool_config.py @@ -63,6 +63,7 @@ def load_pool_config(root_path: Path) -> List[PoolWalletConfig]: def add_auth_key(root_path: Path, config_entry: PoolWalletConfig, auth_key: G1Element): config = load_config(root_path, "config.yaml") pool_list = config["pool"].get("pool_list", []) + updated = False if pool_list is not None: for pool_config_dict in pool_list: try: @@ -70,11 +71,15 @@ def add_auth_key(root_path: Path, config_entry: PoolWalletConfig, auth_key: G1El G1Element.from_bytes(hexstr_to_bytes(pool_config_dict["owner_public_key"])) == config_entry.owner_public_key ): - pool_config_dict["authentication_public_key"] = bytes(auth_key).hex() + auth_key_hex = bytes(auth_key).hex() + if pool_config_dict.get("authentication_public_key", "") != auth_key_hex: + pool_config_dict["authentication_public_key"] = auth_key_hex + updated = True except Exception as e: log.error(f"Exception updating config: {pool_config_dict} {e}") - config["pool"]["pool_list"] = pool_list - save_config(root_path, "config.yaml", config) + if updated: + config["pool"]["pool_list"] = pool_list + save_config(root_path, "config.yaml", config) async def update_pool_config(root_path: Path, pool_config_list: List[PoolWalletConfig]): From 51dffd8501fd314ad4fb728887006968cea42217 Mon Sep 17 00:00:00 2001 From: Jeff Date: Wed, 2 Mar 2022 10:25:13 -0800 Subject: [PATCH 150/378] Preserve existing pool payout_instructions when creating a PoolWallet (#10507) * Preserve existing pool payout_instructions when creating a PoolWallet * Updated the logged string when payout_instructions needs to be generated. * Tests for update_pool_config * isort * Logging change, linter fixes, and more comments. --- chia/pools/pool_wallet.py | 16 +-- tests/pools/test_pool_wallet.py | 199 ++++++++++++++++++++++++++++++++ 2 files changed, 207 insertions(+), 8 deletions(-) create mode 100644 tests/pools/test_pool_wallet.py diff --git a/chia/pools/pool_wallet.py b/chia/pools/pool_wallet.py index 0df09ed9a592..881f049e1826 100644 --- a/chia/pools/pool_wallet.py +++ b/chia/pools/pool_wallet.py @@ -229,16 +229,16 @@ async def get_unconfirmed_transactions(self) -> List[TransactionRecord]: async def get_tip(self) -> Tuple[uint32, CoinSpend]: return self.wallet_state_manager.pool_store.get_spends_for_wallet(self.wallet_id)[-1] - async def update_pool_config(self, make_new_authentication_key: bool): + async def update_pool_config(self) -> None: current_state: PoolWalletInfo = await self.get_current_state() pool_config_list: List[PoolWalletConfig] = load_pool_config(self.wallet_state_manager.root_path) pool_config_dict: Dict[bytes32, PoolWalletConfig] = {c.launcher_id: c for c in pool_config_list} existing_config: Optional[PoolWalletConfig] = pool_config_dict.get(current_state.launcher_id, None) + payout_instructions: str = existing_config.payout_instructions if existing_config is not None else "" - if make_new_authentication_key or existing_config is None: - payout_instructions: str = (await self.standard_wallet.get_new_puzzlehash(in_transaction=True)).hex() - else: - payout_instructions = existing_config.payout_instructions + if len(payout_instructions) == 0: + payout_instructions = (await self.standard_wallet.get_new_puzzlehash(in_transaction=True)).hex() + self.log.info(f"New config entry. Generated payout_instructions puzzle hash: {payout_instructions}") new_config: PoolWalletConfig = PoolWalletConfig( current_state.launcher_id, @@ -293,7 +293,7 @@ async def apply_state_transition(self, new_state: CoinSpend, block_height: uint3 self.next_transaction_fee = uint64(0) break - await self.update_pool_config(False) + await self.update_pool_config() return True async def rewind(self, block_height: int) -> bool: @@ -312,7 +312,7 @@ async def rewind(self, block_height: int) -> bool: return True else: if await self.get_current_state() != prev_state: - await self.update_pool_config(False) + await self.update_pool_config() return False except Exception as e: self.log.error(f"Exception rewinding: {e}") @@ -351,7 +351,7 @@ async def create( launcher_spend = spend assert launcher_spend is not None await self.wallet_state_manager.pool_store.add_spend(self.wallet_id, launcher_spend, block_height) - await self.update_pool_config(True) + await self.update_pool_config() p2_puzzle_hash: bytes32 = (await self.get_current_state()).p2_singleton_puzzle_hash await self.wallet_state_manager.add_new_wallet(self, self.wallet_info.id, create_puzzle_hashes=False) diff --git a/tests/pools/test_pool_wallet.py b/tests/pools/test_pool_wallet.py new file mode 100644 index 000000000000..810eb614523a --- /dev/null +++ b/tests/pools/test_pool_wallet.py @@ -0,0 +1,199 @@ +from dataclasses import dataclass +from pathlib import Path +from typing import Any, List, Optional, cast +from unittest.mock import MagicMock + +import pytest +from blspy import G1Element + +from benchmarks.utils import rand_g1, rand_hash +from chia.pools.pool_wallet import PoolWallet +from chia.types.blockchain_format.sized_bytes import bytes32 + + +@dataclass +class MockStandardWallet: + canned_puzzlehash: bytes32 + + async def get_new_puzzlehash(self, in_transaction: bool = False) -> bytes32: + return self.canned_puzzlehash + + +@dataclass +class MockWalletStateManager: + root_path: Optional[Path] = None + + +@dataclass +class MockPoolWalletConfig: + launcher_id: bytes32 + pool_url: str + payout_instructions: str + target_puzzle_hash: bytes32 + p2_singleton_puzzle_hash: bytes32 + owner_public_key: G1Element + + +@dataclass +class MockPoolState: + pool_url: Optional[str] + target_puzzle_hash: bytes32 + owner_pubkey: G1Element + + +@dataclass +class MockPoolWalletInfo: + launcher_id: bytes32 + p2_singleton_puzzle_hash: bytes32 + current: MockPoolState + + +@pytest.mark.asyncio +async def test_update_pool_config_new_config(monkeypatch: Any) -> None: + """ + Test that PoolWallet can create a new pool config + """ + + updated_configs: List[MockPoolWalletConfig] = [] + payout_instructions_ph = rand_hash() + launcher_id: bytes32 = rand_hash() + p2_singleton_puzzle_hash: bytes32 = rand_hash() + pool_url: str = "" + target_puzzle_hash: bytes32 = rand_hash() + owner_pubkey: G1Element = rand_g1() + current: MockPoolState = MockPoolState( + pool_url=pool_url, + target_puzzle_hash=target_puzzle_hash, + owner_pubkey=owner_pubkey, + ) + current_state: MockPoolWalletInfo = MockPoolWalletInfo( + launcher_id=launcher_id, + p2_singleton_puzzle_hash=p2_singleton_puzzle_hash, + current=current, + ) + + # No config data + def mock_load_pool_config(root_path: Path) -> List[MockPoolWalletConfig]: + return [] + + monkeypatch.setattr("chia.pools.pool_wallet.load_pool_config", mock_load_pool_config) + + # Mock pool_config.update_pool_config to capture the updated configs + async def mock_pool_config_update_pool_config( + root_path: Path, pool_config_list: List[MockPoolWalletConfig] + ) -> None: + nonlocal updated_configs + updated_configs = pool_config_list + + monkeypatch.setattr("chia.pools.pool_wallet.update_pool_config", mock_pool_config_update_pool_config) + + # Mock PoolWallet.get_current_state to return our canned state + async def mock_get_current_state(self: Any) -> Any: + return current_state + + monkeypatch.setattr(PoolWallet, "get_current_state", mock_get_current_state) + + # Create an empty PoolWallet and populate only the required fields + wallet = PoolWallet() + # We need a standard wallet to provide a puzzlehash + wallet.standard_wallet = cast(Any, MockStandardWallet(canned_puzzlehash=payout_instructions_ph)) + # We need a wallet state manager to hold a root_path member + wallet.wallet_state_manager = MockWalletStateManager() + # We need a log object, but we don't care about how it's used + wallet.log = MagicMock() + + await wallet.update_pool_config() + + assert len(updated_configs) == 1 + assert updated_configs[0].launcher_id == launcher_id + assert updated_configs[0].pool_url == pool_url + assert updated_configs[0].payout_instructions == payout_instructions_ph.hex() + assert updated_configs[0].target_puzzle_hash == target_puzzle_hash + assert updated_configs[0].p2_singleton_puzzle_hash == p2_singleton_puzzle_hash + assert updated_configs[0].owner_public_key == owner_pubkey + + +@pytest.mark.asyncio +async def test_update_pool_config_existing_payout_instructions(monkeypatch: Any) -> None: + """ + Test that PoolWallet will retain existing payout_instructions when updating the pool config. + """ + + updated_configs: List[MockPoolWalletConfig] = [] + payout_instructions_ph = rand_hash() + launcher_id: bytes32 = rand_hash() + p2_singleton_puzzle_hash: bytes32 = rand_hash() + pool_url: str = "https://fake.pool.url" + target_puzzle_hash: bytes32 = rand_hash() + owner_pubkey: G1Element = rand_g1() + current: MockPoolState = MockPoolState( + pool_url=pool_url, + target_puzzle_hash=target_puzzle_hash, + owner_pubkey=owner_pubkey, + ) + current_state: MockPoolWalletInfo = MockPoolWalletInfo( + launcher_id=launcher_id, + p2_singleton_puzzle_hash=p2_singleton_puzzle_hash, + current=current, + ) + + # Existing config data with different values + # payout_instructions should _NOT_ be updated after calling update_pool_config + existing_launcher_id: bytes32 = launcher_id + existing_pool_url: str = "" + existing_payout_instructions_ph: bytes32 = rand_hash() + existing_target_puzzle_hash: bytes32 = rand_hash() + existing_p2_singleton_puzzle_hash: bytes32 = rand_hash() + existing_owner_pubkey: G1Element = rand_g1() + existing_config: MockPoolWalletConfig = MockPoolWalletConfig( + launcher_id=existing_launcher_id, + pool_url=existing_pool_url, + payout_instructions=existing_payout_instructions_ph.hex(), + target_puzzle_hash=existing_target_puzzle_hash, + p2_singleton_puzzle_hash=existing_p2_singleton_puzzle_hash, + owner_public_key=existing_owner_pubkey, + ) + + # No config data + def mock_load_pool_config(root_path: Path) -> List[MockPoolWalletConfig]: + nonlocal existing_config + return [existing_config] + + monkeypatch.setattr("chia.pools.pool_wallet.load_pool_config", mock_load_pool_config) + + # Mock pool_config.update_pool_config to capture the updated configs + async def mock_pool_config_update_pool_config( + root_path: Path, pool_config_list: List[MockPoolWalletConfig] + ) -> None: + nonlocal updated_configs + updated_configs = pool_config_list + + monkeypatch.setattr("chia.pools.pool_wallet.update_pool_config", mock_pool_config_update_pool_config) + + # Mock PoolWallet.get_current_state to return our canned state + async def mock_get_current_state(self: Any) -> Any: + return current_state + + monkeypatch.setattr(PoolWallet, "get_current_state", mock_get_current_state) + + # Create an empty PoolWallet and populate only the required fields + wallet = PoolWallet() + # We need a standard wallet to provide a puzzlehash + wallet.standard_wallet = cast(Any, MockStandardWallet(canned_puzzlehash=payout_instructions_ph)) + # We need a wallet state manager to hold a root_path member + wallet.wallet_state_manager = MockWalletStateManager() + # We need a log object, but we don't care about how it's used + wallet.log = MagicMock() + + await wallet.update_pool_config() + + assert len(updated_configs) == 1 + assert updated_configs[0].launcher_id == launcher_id + assert updated_configs[0].pool_url == pool_url + + # payout_instructions should still point to existing_payout_instructions_ph + assert updated_configs[0].payout_instructions == existing_payout_instructions_ph.hex() + + assert updated_configs[0].target_puzzle_hash == target_puzzle_hash + assert updated_configs[0].p2_singleton_puzzle_hash == p2_singleton_puzzle_hash + assert updated_configs[0].owner_public_key == owner_pubkey From 206205b254a59098e40feeab659191e3a0df91f8 Mon Sep 17 00:00:00 2001 From: Mariano Sorgente <3069354+mariano54@users.noreply.github.com> Date: Wed, 2 Mar 2022 13:41:13 -0500 Subject: [PATCH 151/378] Abort trusted sync if a state update fails (#10523) * Abort trusted sync if a state update fails * Fix lint --- chia/wallet/wallet_node.py | 50 +++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index 3822c390f817..dd636ac56118 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -600,6 +600,7 @@ async def receive_state_from_peer( cache: PeerRequestCache = self.get_cache_for_peer(peer) if fork_height is not None: cache.clear_after_height(fork_height) + self.log.info(f"Rolling back to {fork_height}") all_tasks: List[asyncio.Task] = [] target_concurrent_tasks: int = 20 @@ -617,21 +618,18 @@ async def receive_and_validate(inner_states: List[CoinState], inner_idx_start: i for inner_state in inner_states: self.add_state_to_race_cache(header_hash, height, inner_state) self.log.info(f"Added to race cache: {height}, {inner_state}") - if trusted: - valid_states = inner_states - else: - valid_states = [ - inner_state - for inner_state in inner_states - if await self.validate_received_state_from_peer(inner_state, peer, cache, fork_height) - ] + valid_states = [ + inner_state + for inner_state in inner_states + if await self.validate_received_state_from_peer(inner_state, peer, cache, fork_height) + ] if len(valid_states) > 0: - self.log.info( - f"new coin state received ({inner_idx_start}-" - f"{inner_idx_start + len(inner_states) - 1}/ {len(items)})" - ) assert self.new_state_lock is not None async with self.new_state_lock: + self.log.info( + f"new coin state received ({inner_idx_start}-" + f"{inner_idx_start + len(inner_states) - 1}/ {len(items)})" + ) if self.wallet_state_manager is None: return await self.wallet_state_manager.new_coin_state(valid_states, peer, fork_height) @@ -662,18 +660,31 @@ async def receive_and_validate(inner_states: List[CoinState], inner_idx_start: i if peer.peer_node_id not in self.server.all_connections: self.log.error(f"Disconnected from peer {peer.peer_node_id} host {peer.peer_host}") return False - while len(concurrent_tasks_cs_heights) >= target_concurrent_tasks: - await asyncio.sleep(0.1) - if self._shut_down: - self.log.info("Terminating receipt and validation due to shut down request") + if trusted: + try: + self.log.info(f"new coin state received ({idx}-" f"{idx + len(states) - 1}/ {len(items)})") + await self.wallet_state_manager.new_coin_state(states, peer, fork_height) + await self.wallet_state_manager.blockchain.set_finished_sync_up_to( + last_change_height_cs(states[-1]) - 1 + ) + except Exception as e: + tb = traceback.format_exc() + self.log.error(f"Error adding states.. {e} {tb}") return False - concurrent_tasks_cs_heights.append(last_change_height_cs(states[0])) - all_tasks.append(asyncio.create_task(receive_and_validate(states, idx, concurrent_tasks_cs_heights))) + else: + while len(concurrent_tasks_cs_heights) >= target_concurrent_tasks: + await asyncio.sleep(0.1) + if self._shut_down: + self.log.info("Terminating receipt and validation due to shut down request") + return False + concurrent_tasks_cs_heights.append(last_change_height_cs(states[0])) + all_tasks.append(asyncio.create_task(receive_and_validate(states, idx, concurrent_tasks_cs_heights))) idx += len(states) + still_connected = self.server is not None and peer.peer_node_id in self.server.all_connections await asyncio.gather(*all_tasks) await self.update_ui() - return True + return still_connected and self.server is not None and peer.peer_node_id in self.server.all_connections async def get_coins_with_puzzle_hash(self, puzzle_hash) -> List[CoinState]: assert self.wallet_state_manager is not None @@ -838,6 +849,7 @@ async def new_peak_wallet(self, new_peak: wallet_protocol.NewPeakWallet, peer: W self.wallet_state_manager.set_sync_mode(True) await self.long_sync(new_peak.height, peer, uint32(max(0, current_height - 256)), rollback=True) self.wallet_state_manager.set_sync_mode(False) + else: far_behind: bool = ( new_peak.height - self.wallet_state_manager.blockchain.get_peak_height() > self.LONG_SYNC_THRESHOLD From b76bf7b8a35e5fb3be7f7618809680ab5d3a498a Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Wed, 2 Mar 2022 21:50:06 +0100 Subject: [PATCH 152/378] fix updating of sub-epoch-summary map (part of BlockHeightMap) when the entirey change, including genesis changes (#10486) --- chia/full_node/block_height_map.py | 5 +++ tests/core/full_node/test_block_height_map.py | 40 +++++++++++++++++-- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/chia/full_node/block_height_map.py b/chia/full_node/block_height_map.py index 76f51dd2c62c..216e4d4a32d2 100644 --- a/chia/full_node/block_height_map.py +++ b/chia/full_node/block_height_map.py @@ -194,6 +194,11 @@ async def _load_blocks_from(self, height: uint32, prev_hash: bytes32): ): return self.__sub_epoch_summaries[height] = entry[2] + elif height in self.__sub_epoch_summaries: + # if the database file was swapped out and the existing + # cache doesn't represent any of it at all, a missing sub + # epoch summary needs to be removed from the cache too + del self.__sub_epoch_summaries[height] self.__set_hash(height, prev_hash) prev_hash = entry[1] diff --git a/tests/core/full_node/test_block_height_map.py b/tests/core/full_node/test_block_height_map.py index b7c02b47f976..38f2c21ede82 100644 --- a/tests/core/full_node/test_block_height_map.py +++ b/tests/core/full_node/test_block_height_map.py @@ -1,6 +1,6 @@ import pytest import struct -from chia.full_node.block_height_map import BlockHeightMap +from chia.full_node.block_height_map import BlockHeightMap, SesCache from chia.types.blockchain_format.sub_epoch_summary import SubEpochSummary from chia.util.db_wrapper import DBWrapper @@ -8,8 +8,7 @@ from chia.types.blockchain_format.sized_bytes import bytes32 from typing import Optional from chia.util.ints import uint8 - -# from tests.conftest import tmp_dir +from chia.util.files import write_file_async def gen_block_hash(height: int) -> bytes32: @@ -189,6 +188,41 @@ async def test_save_restore(self, tmp_dir, db_version): with pytest.raises(KeyError) as _: height_map.get_ses(height) + @pytest.mark.asyncio + async def test_restore_entire_chain(self, tmp_dir, db_version): + + # this is a test where the height-to-hash and height-to-ses caches are + # entirely unrelated to the database. Make sure they can both be fully + # replaced + async with DBConnection(db_version) as db_wrapper: + + heights = bytearray(900 * 32) + for i in range(900): + idx = i * 32 + heights[idx : idx + 32] = bytes([i % 256] * 32) + + await write_file_async(tmp_dir / "height-to-hash", heights) + + ses_cache = [] + for i in range(0, 900, 19): + ses_cache.append((i, gen_ses(i + 9999))) + + await write_file_async(tmp_dir / "sub-epoch-summaries", bytes(SesCache(ses_cache))) + + await setup_db(db_wrapper) + await setup_chain(db_wrapper, 10000, ses_every=20) + + height_map = await BlockHeightMap.create(tmp_dir, db_wrapper) + + for height in reversed(range(10000)): + assert height_map.contains_height(height) + assert height_map.get_hash(height) == gen_block_hash(height) + if (height % 20) == 0: + assert height_map.get_ses(height) == gen_ses(height) + else: + with pytest.raises(KeyError) as _: + height_map.get_ses(height) + @pytest.mark.asyncio async def test_restore_extend(self, tmp_dir, db_version): From 863036d03ab5e7062e7ac1af1fdd01fab06cb189 Mon Sep 17 00:00:00 2001 From: Jeff Date: Wed, 2 Mar 2022 13:46:23 -0800 Subject: [PATCH 153/378] Fix propagation of errors when adding a key with an invalid mnemonic (#10274) * Fix propagation of error messages originating from the keychain server * Test that adding key with an invalid mnemonic returns the expected error * Added daemon tests for the add_private_key RPC. Reverted wallet_rpc_client test as the daemon test is better suited for GUI testing. * Reformatting updates * Formatting change as requested by the pre-commit gods. --- chia/daemon/keychain_proxy.py | 8 +- chia/daemon/keychain_server.py | 40 +++++---- tests/core/daemon/test_daemon.py | 138 ++++++++++++++++++++++++++++++- 3 files changed, 165 insertions(+), 21 deletions(-) diff --git a/chia/daemon/keychain_proxy.py b/chia/daemon/keychain_proxy.py index 481fcf55a33a..abc24438e20f 100644 --- a/chia/daemon/keychain_proxy.py +++ b/chia/daemon/keychain_proxy.py @@ -108,9 +108,11 @@ def handle_error(self, response: WsRpcMessage): message = error_details.get("message", "") raise MalformedKeychainRequest(message) else: - err = f"{response['data'].get('command')} failed with error: {error}" - self.log.error(f"{err}") - raise Exception(f"{err}") + # Try to construct a more informative error message including the call that failed + if "command" in response["data"]: + err = f"{response['data'].get('command')} failed with error: {error}" + raise Exception(f"{err}") + raise Exception(f"{error}") async def add_private_key(self, mnemonic: str, passphrase: str) -> PrivateKey: """ diff --git a/chia/daemon/keychain_server.py b/chia/daemon/keychain_server.py index 75b51d850ce5..3c311655275d 100644 --- a/chia/daemon/keychain_server.py +++ b/chia/daemon/keychain_server.py @@ -56,21 +56,25 @@ def get_keychain_for_request(self, request: Dict[str, Any]): return keychain async def handle_command(self, command, data) -> Dict[str, Any]: - if command == "add_private_key": - return await self.add_private_key(cast(Dict[str, Any], data)) - elif command == "check_keys": - return await self.check_keys(cast(Dict[str, Any], data)) - elif command == "delete_all_keys": - return await self.delete_all_keys(cast(Dict[str, Any], data)) - elif command == "delete_key_by_fingerprint": - return await self.delete_key_by_fingerprint(cast(Dict[str, Any], data)) - elif command == "get_all_private_keys": - return await self.get_all_private_keys(cast(Dict[str, Any], data)) - elif command == "get_first_private_key": - return await self.get_first_private_key(cast(Dict[str, Any], data)) - elif command == "get_key_for_fingerprint": - return await self.get_key_for_fingerprint(cast(Dict[str, Any], data)) - return {} + try: + if command == "add_private_key": + return await self.add_private_key(cast(Dict[str, Any], data)) + elif command == "check_keys": + return await self.check_keys(cast(Dict[str, Any], data)) + elif command == "delete_all_keys": + return await self.delete_all_keys(cast(Dict[str, Any], data)) + elif command == "delete_key_by_fingerprint": + return await self.delete_key_by_fingerprint(cast(Dict[str, Any], data)) + elif command == "get_all_private_keys": + return await self.get_all_private_keys(cast(Dict[str, Any], data)) + elif command == "get_first_private_key": + return await self.get_first_private_key(cast(Dict[str, Any], data)) + elif command == "get_key_for_fingerprint": + return await self.get_key_for_fingerprint(cast(Dict[str, Any], data)) + return {} + except Exception as e: + log.exception(e) + return {"success": False, "error": str(e), "command": command} async def add_private_key(self, request: Dict[str, Any]) -> Dict[str, Any]: if self.get_keychain_for_request(request).is_keyring_locked(): @@ -93,6 +97,12 @@ async def add_private_key(self, request: Dict[str, Any]) -> Dict[str, Any]: "error": KEYCHAIN_ERR_KEYERROR, "error_details": {"message": f"The word '{e.args[0]}' is incorrect.'", "word": e.args[0]}, } + except ValueError as e: + log.exception(e) + return { + "success": False, + "error": str(e), + } return {"success": True} diff --git a/tests/core/daemon/test_daemon.py b/tests/core/daemon/test_daemon.py index e277a0639eb7..df59f519a0b8 100644 --- a/tests/core/daemon/test_daemon.py +++ b/tests/core/daemon/test_daemon.py @@ -1,3 +1,4 @@ +from chia.daemon.server import WebSocketServer from chia.server.outbound_message import NodeType from chia.types.peer_info import PeerInfo from tests.block_tools import BlockTools, create_block_tools_async @@ -49,8 +50,8 @@ async def get_b_tools(self, get_temp_keyring): @pytest_asyncio.fixture(scope="function") async def get_daemon_with_temp_keyring(self, get_b_tools): - async for _ in setup_daemon(btools=get_b_tools): - yield get_b_tools + async for daemon in setup_daemon(btools=get_b_tools): + yield get_b_tools, daemon @pytest.mark.asyncio async def test_daemon_simulation(self, simulation, get_b_tools): @@ -124,7 +125,7 @@ async def reader(ws, queue): @pytest.mark.filterwarnings("ignore::DeprecationWarning:websockets.*") @pytest.mark.asyncio async def test_validate_keyring_passphrase_rpc(self, get_daemon_with_temp_keyring): - local_b_tools: BlockTools = get_daemon_with_temp_keyring + local_b_tools: BlockTools = get_daemon_with_temp_keyring[0] keychain = local_b_tools.local_keychain # When: the keychain has a master passphrase set @@ -202,3 +203,134 @@ async def check_empty_passphrase_case(response: aiohttp.http_websocket.WSMessage await ws.send_str(create_payload("validate_keyring_passphrase", {"key": ""}, "test", "daemon")) # Expect: validation failure await check_empty_passphrase_case(await ws.receive()) + + # Suppress warning: "The explicit passing of coroutine objects to asyncio.wait() is deprecated since Python 3.8..." + # Can be removed when we upgrade to a newer version of websockets (9.1 works) + @pytest.mark.filterwarnings("ignore::DeprecationWarning:websockets.*") + @pytest.mark.asyncio + async def test_add_private_key(self, get_daemon_with_temp_keyring): + local_b_tools: BlockTools = get_daemon_with_temp_keyring[0] + daemon: WebSocketServer = get_daemon_with_temp_keyring[1] + keychain = daemon.keychain_server._default_keychain # Keys will be added here + test_mnemonic = ( + "grief lock ketchup video day owner torch young work " + "another venue evidence spread season bright private " + "tomato remind jaguar original blur embody project can" + ) + test_fingerprint = 2877570395 + mnemonic_with_typo = f"{test_mnemonic}xyz" # intentional typo: can -> canxyz + mnemonic_with_missing_word = " ".join(test_mnemonic.split(" ")[:-1]) # missing last word + + async def check_success_case(response: aiohttp.http_websocket.WSMessage): + nonlocal keychain + + # Expect: JSON response + assert response.type == aiohttp.WSMsgType.TEXT + message = json.loads(response.data.strip()) + # Expect: daemon handled the request + assert message["ack"] is True + # Expect: success flag is set to True + assert message["data"]["success"] is True + # Expect: the keychain has the new key + assert keychain.get_private_key_by_fingerprint(test_fingerprint) is not None + + async def check_missing_param_case(response: aiohttp.http_websocket.WSMessage): + # Expect: JSON response + assert response.type == aiohttp.WSMsgType.TEXT + message = json.loads(response.data.strip()) + # Expect: daemon handled the request + assert message["ack"] is True + # Expect: success flag is set to False + assert message["data"]["success"] is False + # Expect: error field is set to "malformed request" + assert message["data"]["error"] == "malformed request" + # Expect: error_details message is set to "missing mnemonic and/or passphrase" + assert message["data"]["error_details"]["message"] == "missing mnemonic and/or passphrase" + + async def check_mnemonic_with_typo_case(response: aiohttp.http_websocket.WSMessage): + # Expect: JSON response + assert response.type == aiohttp.WSMsgType.TEXT + message = json.loads(response.data.strip()) + # Expect: daemon handled the request + assert message["ack"] is True + # Expect: success flag is set to False + assert message["data"]["success"] is False + # Expect: error field is set to "'canxyz' is not in the mnemonic dictionary; may be misspelled" + assert message["data"]["error"] == "'canxyz' is not in the mnemonic dictionary; may be misspelled" + + async def check_invalid_mnemonic_length_case(response: aiohttp.http_websocket.WSMessage): + # Expect: JSON response + assert response.type == aiohttp.WSMsgType.TEXT + message = json.loads(response.data.strip()) + # Expect: daemon handled the request + assert message["ack"] is True + # Expect: success flag is set to False + assert message["data"]["success"] is False + # Expect: error field is set to "Invalid mnemonic length" + assert message["data"]["error"] == "Invalid mnemonic length" + + async def check_invalid_mnemonic_case(response: aiohttp.http_websocket.WSMessage): + # Expect: JSON response + assert response.type == aiohttp.WSMsgType.TEXT + message = json.loads(response.data.strip()) + # Expect: daemon handled the request + assert message["ack"] is True + # Expect: success flag is set to False + assert message["data"]["success"] is False + # Expect: error field is set to "Invalid order of mnemonic words" + assert message["data"]["error"] == "Invalid order of mnemonic words" + + async with aiohttp.ClientSession() as session: + async with session.ws_connect( + "wss://127.0.0.1:55401", + autoclose=True, + autoping=True, + heartbeat=60, + ssl=local_b_tools.get_daemon_ssl_context(), + max_msg_size=52428800, + ) as ws: + # Expect the key hasn't been added yet + assert keychain.get_private_key_by_fingerprint(test_fingerprint) is None + + await ws.send_str( + create_payload("add_private_key", {"mnemonic": test_mnemonic, "passphrase": ""}, "test", "daemon") + ) + # Expect: key was added successfully + await check_success_case(await ws.receive()) + + # When: missing mnemonic + await ws.send_str(create_payload("add_private_key", {"passphrase": ""}, "test", "daemon")) + # Expect: Failure due to missing mnemonic + await check_missing_param_case(await ws.receive()) + + # When: missing passphrase + await ws.send_str(create_payload("add_private_key", {"mnemonic": test_mnemonic}, "test", "daemon")) + # Expect: Failure due to missing passphrase + await check_missing_param_case(await ws.receive()) + + # When: using a mmnemonic with an incorrect word (typo) + await ws.send_str( + create_payload( + "add_private_key", {"mnemonic": mnemonic_with_typo, "passphrase": ""}, "test", "daemon" + ) + ) + # Expect: Failure due to misspelled mnemonic + await check_mnemonic_with_typo_case(await ws.receive()) + + # When: using a mnemonic with an incorrect word count + await ws.send_str( + create_payload( + "add_private_key", {"mnemonic": mnemonic_with_missing_word, "passphrase": ""}, "test", "daemon" + ) + ) + # Expect: Failure due to invalid mnemonic + await check_invalid_mnemonic_length_case(await ws.receive()) + + # When: using using an incorrect mnemnonic + await ws.send_str( + create_payload( + "add_private_key", {"mnemonic": " ".join(["abandon"] * 24), "passphrase": ""}, "test", "daemon" + ) + ) + # Expect: Failure due to checksum error + await check_invalid_mnemonic_case(await ws.receive()) From fc618ec1d22131c6ab16715e04cbd9db284c0b24 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Wed, 2 Mar 2022 19:48:08 -0500 Subject: [PATCH 154/378] catch up test_add_private_key() with dynamic ports (#10530) --- tests/core/daemon/test_daemon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/core/daemon/test_daemon.py b/tests/core/daemon/test_daemon.py index df59f519a0b8..d3486f895f18 100644 --- a/tests/core/daemon/test_daemon.py +++ b/tests/core/daemon/test_daemon.py @@ -282,7 +282,7 @@ async def check_invalid_mnemonic_case(response: aiohttp.http_websocket.WSMessage async with aiohttp.ClientSession() as session: async with session.ws_connect( - "wss://127.0.0.1:55401", + f"wss://127.0.0.1:{local_b_tools._config['daemon_port']}", autoclose=True, autoping=True, heartbeat=60, From 945bc5054c7d9f2e1426590a49c8ca0a2d67f5a7 Mon Sep 17 00:00:00 2001 From: Jeff Date: Wed, 2 Mar 2022 18:16:54 -0800 Subject: [PATCH 155/378] Updated background and icon positions. (#10531) --- build_scripts/assets/dmg/background.tiff | Bin 389022 -> 507222 bytes build_scripts/build_dmg.js | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build_scripts/assets/dmg/background.tiff b/build_scripts/assets/dmg/background.tiff index 1661c36475d1b9172afa0642a7426bbf654cbe68..6513049df56c4aec66d39b8951a7201537bfa824 100644 GIT binary patch delta 504439 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

Rss%J?)Qv#E5luhcr&ks-nU0xrB+m2pNjB z{YA4kzPAwzm!#h$`D!!G60~?^r;_Uv*6Cn+*Nd1>Y=pW^bxlPvgY!1~$mm|kU4O*R z6l06e3b89Us3p5~RNORjvK#7V6&lV;VkA|ql_ z-)PzSA-^6|8Yz&r$0)N*qor3XtG_$h0%@&F%)&}dV8C68lU zPO`ahVIgfYXsGxe1zNssxKe05X9f544oX7O5LF%`bkG``rl6@@*gRA5sqey8_YEz^ z69gzKu*v$_DJ{K^$w6ELg`u!YjzIO=S zO=l2KyR-XF@{+F>DW#k%s)O)l6UOIL{NJkllsIZ*5dZg%O45sA9`W-%*5JE}U}LN3 z=Lng2j2s#UCR>7CW~qc{KBk(aK|?m8{MIDhusfP>F4~=P%!~h4Aygsi4;)P_JkL!V zRwTeK9*tXEm>+SJR8KyCt*Sljn7ZQ+3;saXO|kTLO`P`UsKl;lT`VGt-``T6<63@t zdBb@9hb)o2O8YDPS9Dd8lVQVtE{;8uV$`*3P{YI0bq7@(nvP!;<7( zWK{%SQC4Rq>&HZW=T}l}N!u9z8gV+K(Q!HPJQE53Ia)$9Fr)df@j*zUZ9TuF2V*rXeg zAH%~4Nec<2)l%1XVc9hN;!afKg?iJ!m=oI{T=jHqAb3=gaREE!j@Li8L!^6= zi?R4MGGoelQnZGhx!kV4OtnPCwtz##AIJl6ELjM|QWcTOzxsG8KE7#$o+%P=tZgC9 z6JU*FF@ZOYO@&Mt(V&H^ZIpR^r=1Jsf87zd+_0F zjQ&8FAtWuZ+^t%FYSrOmaLDt*3ru-d)=y`KY%b|>uSX3U25hZKvwSK(^yotoED!8) zMYV9e>OuWyH#s!T`5K^5{n_3Lj%OBCIy*(Pg-o9~06MkDcy0#UG7kA0Xoni2BK?g_Rz(-2KH2B^)YsalC(ZykiU;SGyZ(MMY<>h8^Yw)|&-wCJe=hjS9do8r}QI zX8(?y#XD4%)hVAY1O9o~E|k_IbogFRD9bsQ?Ps)VDMxHcv5iw}vE#H-b^nk~`PfC^(vTll$bQ_9G zNxbFnMr~zOdHB>{holZt&M3Gotc{eMvDMxw4)Z^N+CO`RZgZ$)pS^%b+{ixPNsTL7 zGo_OqO}q^)8JXg`Qwa$M6iaGhml z^17$2s)lR2-bup#hh2)(!>9(8CZR(=ZfaTJ>Y^8&*Fm_3ir@h0c*9FA+cXP6Et%fr z$q~oK9AU$+n*GJZEj=sh!JLa8sujLcuv0=g`KRI^<6@l}TaK8ehzhlrGel?a3B&w8 z+Z^nVc=q=kI{dCZ2l^oO!AoufpiiP0y{*k$Qt15hEXoeDKz=$CrSQ^;PqngZq?OW+ zsZOZBn+&y+XR?J|1@`Co`)X{iw?`}ozgWDUCK>}Vb3%^ zyiC6+sqLt5wOBGdLg2Y9UaghEDh{Yy3q4v`=RR#Y4Yf#>pO!vZxas+^LX+ZfrG_2T zkifCh%#458DmNU~T6c|wG1 z>H1tarCjMeAHQqG&AOutJHO4EvBg7oJy=?=EOwqcETx!TBZjm7ih1Y#tq_Q`x=Mba zbvwP^by_lG@;%fpwefZQds7l|?B9ROF@MKNaa(e^SGs#v>TwZC^)wQN!jcTq|-5%IUVWOK#&Q>Bv(1|Ig49f4OR;TS{Budyj-%5}P~Be1|ww|;O=dr8neGg7yUm%I}>zfd6Yq%?I%eR}42_A!dMcc$$k*mfew$5wK{ zx2}J!nuqVG@$wjkO+dMUy2l%I!5+H6;<8*iMiX&RUm@_1&((e%{M5kP-7Y44g|Dvk zGFyRAgXX0z$u)iuNhk;Lb_>Kmi$H!Tairu{-5ukEB+`ZApE~tkWbBRx1ImRJYxwW= z2@jt0kAYF)i&4mp1KItE2u_(4$*8~!=OtopVgj7hY>k(=gg*{X@Uhj6>PE~6%W-jr z@8769;YT6Zmq*UXBQGz9S@8!HC{Ajz%HoHjfYdHI#65(Rqi-$o?B0wJa0bG+M~&zs zoXCnhsWj%NP69}OwuEU6kep0!6SiUz7VSs*(*5}Meup7(v=T=J`>jjfn-G)jQ+^~) z__NWz_x2}r1R3;UIGvg}GbbkNQQzJwM?({oz!NOJnfm*m>W+8;SERC$<3F@DkI{ zVy4kOEX1iBsq4tYvCLb1D;A>8_hZ-Zi8k1xBZP=W`-lz=h@?xjgxM(F_b@du0#zap>%)f=(GGU#G5cw_$2I@R;@*-&!7)n%;~9fndf@<|0P zi;%pyFAv4!9TutR4ko^3eQrVP=4u=$!9`S5&6_8U{zuK{ywpi55gh_KTM~`zgzt@#4@I$XG)N}pYF9R~^X6H7<$h`|FC_#@>s2t|l1?v7&U+;$HjtUw5t3jK2# z^{-24!^@%*B{O3H{HFoN9^fq7`dnWL0wp`+k_!%%*UXWR)(I(J!N*ur(13EEcL|rt z3TX{1bl)qL)fn>gE>)%h-7%0=m_ldXYKU=ix>9d7{=#?srBdS+4sd$a*fh!mm^hpU zLa(9Wj;k@U%-EqS8>^lSpHgsFW|N-+9TzOee*EFLpjk1wN|-O8%?gfbSEynX9?wyC z=Pe5csqdd@lt*ds&*f^?@KceJGBQIhsG{V+vaPZc0&=7v%D5Q0@n3<}2qnFX zR`%DrAYv+zHmQCtwhrHq4R=$$iBfQdf}Fd%WSlP!$(HsQkJ6r*qJWZUwTSxTFSY$Y z?dmXV_c)cfAcZIygDOH}dkxKtTD__;rb!ywA83`!r>X)2rd9n+)mm~ksV*p73+PM{LRd1ivYcH#Lda;zUOvl#x+nP{hIcPwj?wHw9C z{;syKBnpzQsZ~D@G9I*+jnz_S0ZLzPSX?II>pltLzd4z-u&s7*7Q9eJczkug3~~JaNHkI-0 zTujX2%1#&LMNa1)v*&(@_~)8CYF&h>>iOM#GC`dz#aYKiv{nmF=B_^_531>N9pADO zB6rZ$wV$$m@a*!b>9MNK{O?o?=h>6*t!)HfaX5+o`Ae@7QpCO{e;EW=Re#*?z@_n* zTaMaZmt}kQQ+n-WdOn?pBV2(oD%eL>iy=_%wCkrR3R@P^iF1da!lSP1wJVWN3N zuy8hdy=o`+(v9tp1|vz|U5l1R=oFjgZjPjqVuk+x#cuIy2h1Oxd)2`Lfdh`US{xIP zr5%-abrabYq6eHS@G3>eD^Wq(J%vo9JrOE`b|MU^Uh&xgtjnww`tg`gRU7X%K5Nxw znq|{6*t27s_00e+qVj3w^n7tbb9f{u`Im!`=ocKSS46GH2X7W0R_(-xM?P1Te7SP+ zh*7K6c2py$@j?TI5}Ir668>xtJmSvUPMzi#S63~Pw1Jh0o2GTz!I)d+Z*EHmp=0%x zcxywu;=`RSNv&bgsDZY@+u456GWtBMW_7php2{IU`4)a>gxLMlx=vso-JqAwxlI=)~mhjYT{YjG%I{VjV-+WeHpeWv>SVl#5l_d-KYbB znpTOEgc&EUFU8#Uk9DF(cl6>qbEmG;#k>(P{P?}Lj?_ax`EzpM7R?fVI!y$8fjp+n zM3gyc)>;Q%xHd&og9!Xg)nnZ3G{AI=n)=dem%P=I;Q0n{~+Ku40DQm8i zT?VDTHRsH~Z*yySP?KW~D6ZzNSpPd@9QO1F>Y%rEOZEu5h780eT&a-{!i1f1xQ{x% z%uBGsNF~gR=M-BC}WQT33#az z3Bo^QPu6+3jAMiAJN+*0Vd%uo7S(eBw~s0Q#aUr|o*ph=ozTLxAD88)6|5;kqU7(( z_{+TP=g(CG6|!`TjHWm)0(ofn8|BN){kC|nryLU7=^32Xar$*np>e^*y;l)2SHJ!S=NLlIqIfID~gE}mC0Gna+7L|fDsK1r+ zv*sVJXJTJHGy=mXrq^Nqbv# zQK6*C@(#R-Tu60G$5vw%QnAtIN~#QO))Mt*0N*S8immUT&=*kofQk5&elhDct(3!P zkVBgh6_VSl?>cFBzFz>REYilxm)XO`P2=(UpTV$Jy;RTDprgi2{}0&enZU;NflW@RauP{`7m zSs(8^V`ITlEC5RuXd{Ay;0`oxg;BXP1K3(x2T;Q5noTq4Yd1<(MyX=YMYNh7_{jFc ztrFrvDNvvT?V-4%FrToFak-Pt#ytH-FnhkBlJ(VI`@WXYw%WA5>q`k~NpiKjxpC-$ z=^@QgOiP-c|VC(3?*3e>AH4;s!2*7l>m1^eo8kb!fr;bf<6+ zf$3d9bQ!HkzsDJ?40W1U1&w&Oh-F3lUU`D+0<;_MA!oF&7I_*dZ2jg_;d=g`FK4v8 zJr!dz>q?>GoF0?|`ugK)s%uFlO`1aSl}uw_$I> z83R9#ihTJxMvd4?$$gQTU2mpklKK?EWSM}jZ^i zVoeP#v3>|2CU~}O*MM&>u$efaU#iP-@15fcM$b}ae6$!$;yT})>Lg9B-KDAgbA^{P z5q$-*FAA-^lo_9B=yab|v8X#0=%2iildvn=%XKr_o<>l(?oKY$SL9v}R#S3W>~So$ zc-An?8VXrCG@I3Oabcmo{X*C|nnONW9#5vcq;RnJD}Z~{t%};w>>f&WoeS>72uzVU z7WH+UZ0?l2IU+ye)NlSg%u|;_7hs=`t0TZHRp(WzxC5rkzLDHqHLO--5>|UdoKHus zF^}j_e2#c~WKdB)KV0gZt}ykNS$0kWKkX8%&A})CXCFot^RdNc&gIDRP?4?fiu2vC z#xFyA_vkWDqlFR?e5+%cwTde*`laZ|pw)*Ds-LzuW>2^RHeqwFS1ohvO+B%vEpFl` z&3>CVT!9A?#KWKGm6q><^5%^Aat|U^HmVWA7he!u)7P{zt77C4XGWF7H%`6>&4{Gc zd6n6}po`;X^4P(-#_~Jb<}=@k%p2dmXkVw`gH=TRt>N&ZgHys)>&uagR!>^b47u5s zQqoH)x`unsncG&T{YykL^dFZ-cdOdW-!D>!QhP1U=*8CL2dU2GHr|3(IfRt?6Dh6B zRw$xP%CE0_CcN2tC!zx9hh0I;M-q-3GJFERvXD6L#$pH0oJTgBUABm?)9$_l zMb!y;!7ffyKA9(AN#bvCheroI&e2({q=0PLa-{2X%BLLK|O ztqf9vMT$+T!~m2yGFA?7r_f>!l#A7BT$={6s{k-G03^ylRizHOo3@m#F23###F;3& z%Cp4f4D3oQ`V-wBH)7IjvA^E*@W5Z!?Ndta%LB7@0aRtJ=in}HI!SSRV5}H$z@vLy zQZ{By{@fZc?jY|Y(8F&FG-m_i$V!~UrL}H*NbGp{Ic4@gi^$DNz^&U;KXtFWE9@gW z;l7P`pWC6=ZTxV6U0Wx>wikl<4ov;tj^gi8e3z-4s0UtfP44&e^U(8u4j+$!vHjzf04kj><$CZ@w( zM3+xL$`Gxbva2|dtBP2!2T=#ai+8D@ARwGERaJ7i)NvGQaWHj$5WWoZ&+3iIR#KJj zG?(a@Ur;XE8APph*^hz8w^h%_K|VcT)Yu@&2Ds?+U|0vre@S(p9^91DY}N-tHmivr zD??gkwRqa{)k+OUNGK3N6uxEF`GPlJa*4l2drJdJJcsNN5#V{huJJ21)d{8SS#|j8 zu({I^pisOiaRhz`Hmp-OR98#Ge|-Pd!|_n4(_s<{^`?DQTnbr+oIVbs5nlqesN71GpMN8-1QTxh+uvW;cBzl$2yj(hYYEU#V1Aos)Kri#8IKcJ*LKxnG%wV$ZU8yz zge&(H&FL+~>biSF0)FZVxJ-apbXB8fBv@xupUs@x%wlRxcYRaI`J{2c-^9VATMRL9 zw;j)c&VnK*suB$7>%hLhXLC{v%)Jr%;U$_5?}h}zW}3f_3PcT)TJrv&pve}7VL6uCq;r$N`H{hO*oKJ_hr1Cb%8)U2k#R}f zcV-c|K0dfH>%I*OKQeOIUL1^sS$tS<-_hOhLBb9eH;&uaq+m1?qj9HD(nEv&<<92* z>ZgZ!)P_~h#x1DFV?k*+ zx){d|Z=abQVrt*R`U41NA8_SBlRT7}HmPHvNvIBOA$6 z5}#JOPN4VurU5A{wC{Be@r*f~jhj_ga`YZQf0O?MY(SI0)+NnmBRt*s*1j8m;=UVK zo(a%y7hyfVP6iC(9wA{h4=Ni65Y_`&QKirY$gW!Hp!uzqP6Xm*^jzsjT7DF8;HbmurZcEYrQQjHKqs~d&r5fcvqHQdu075#z#_W#!(< z#ui|OaOI4?!zOFzNcLVmpu(PIX096MZcS%KCuLq$RhD+=jzHp;RGg4Q`QD)CQh>+ zh1@=MH13z={*dWr3uzVo=eCpIh1_R$R%SMQ>2_-4J)-BHKIh(=<^=vWDp_2va#SdX z2nc{U-)uMd0t^6x!C?@8U<@`727kmr5YP-hEeVOnV$rzdc0C`EM`DnOlv*ngkjiCJ zxn#COCxy$VQ#qv8RWOB3r%}1Y?tMRvOl33L1l|uhqRnJ+8bo@RC703ZGWc~er$3@q z=9QY&cD-M)SYY+b6;7W`pxP|9x{RXPZ=%^Q6Uvp&wQ{FjZFh@*v`(u~kJ;$f>n;lW zfxThj_smu|9gn=>G7{`IFB6l!S4FvSfGxYyMPK$K_ z$4yBT#=FrJ8<9w*lx$r`wzRaX1yWS)Jn+er#UW8SvW+n~O_D6F8PzoUw@M-q5**&s zkiBHxzGzZ^MvlTt3NCRUMiKvA!Iioerq;+T2W3~)t!A&&RdRV#t9GRwRoYL*VNgg` zjbBndmbJ4Z(QBo_aog0*w{hBb#lv?;_Z8VWU34Yj*s^y`+fH6|4a-_iH|ocF%ojcc z%3fEE?v>y8JePt*mhK^M;4QurhdbE2ErVKD)A@;i-I#1~h2(eUFze%z%fFCN`AnCM zWbPJ3hGmgnJwD~xi=UEZ`QoXcXHwQHL1lT(p_;LIhJ#G$SH^>z%4@B9m{vAbpQ^#g z9a^tyaHg%QT+@!BrRm!y%d>3@jjgey+HTXd?Uu^%xaV7=^}B8NM%Sc18V2*f>^k27 z)-+px#xcV?+vf+6?|L+Qw{dxo5lL=*R+q`UJjWwDPn*VP&GKBf<-+t^uS>i1{RIck z^*sMohjUl=NylH1wVMwBI?N!W*wy0z06_p1Iox&fR@v7q-A{^!au%$uUH6<9mlJut zSCH!Zvqt^t${nOp?8)BuxiNcllV9j+zZ_YA>-ygV74~~sH`P-7SVzjz{=c3T`7eJy zuKyp>_J9xtoxn55{2pWXfQB!H7_9VFVn5W_k~%H;7f?yU{mJ>IRiKa=u}lFNjO-7Zf;xi(!m^ z4uw$+-9)&U{9-ImIT0QyLiNiMQRG-BQEC}NSVI>cEMSVTW-G-;Z5f_)PKZ&7#m2`$ z8>1WnjInvu#)rQhAJlMf@#;3mXyY81OniV*MMg*F;US~kh)^#QACc)A@0LLghlJ`! zlO!-kn*-cA(h2fLrw<~NA%c`pT2C#12|*#_#7HA2{UE^fF9c)y{zo!$G{{FUDc&@s zk&7BzKiN|OC8B$ka>5nLRtByi?6FBQZd^xsjKHP}NR)80YE1I)Gg~ClF|xj7uKBGu zCR1{jb7A{UW#=}c%(|D8u5Pxee;_A%Dx3%6K+HMYIA2`jo+cJ@Ey?!dCak-EYx9`b zPxr?<7Szat(z<&%0u+Rw{Ej6Ear7VOdPER;tCFR5jj0M12WUa=eDpSaBdKRe5p^h) z6cTSs+2)rhRKlh)PJ>cbPdF(A{glr>l~ZZ1yeWEDrgW}e&&GLBX!6H8wDuI#T72f| z#5Ja=PLb3)pFv;cNHn!svrdG6Q&yf$Tr~ADzS7FB<>+lWs%d(jRw~aDqAg%b6}hL+ z+P_&7rF2xa>L@$<-y7>qQmo9%s?@6gOI`hPJoWOM*JQ_Hj$Mp1HcF7#a^FX2?M)(7 zxvRs)lFbWYIYV?^i&P5mziffSr&AuoS!qLK+EuBw%7W9C8zl)W8h@mJ_THFU+D&P# z;-9ut7N=AzeQf5Hu#XoGw#m$BnDzjY6 z#SAX#BE2j!L|v=GTyI@)u{Xs}UTaHq?}hKY=koGgQuBVV-7>i9R{C8l{RHj?1En|u z>R&tFa7e{T6k!T4J6VN*kf>+JWpSSD5(>!bmp z$jy}4*oFe^JP@sac3E_}_(x0Rt*NhOA~Dw5>ubQBe{?oRx!5*OWMNHxv!({oe|w^B z=4?vC_Ez!Sx3Z(liAB1$KJT{M&j!&=xxOuC`eB>@TSkp>UAP7&+BcZN*5I4*ALLF!z;?}L-JF(x%5aVsb@?P)&s?qf@_9SW zFza38s=s-2z8Ro(EGm|p-#_6VZN_)@59s>WnDUN+#x=he<=r)p^88(y_;z^ejQ4M} zeHqsJXFloJf3R|{HN>JK0^%JTwb=eg*os#sz_`y*5B*6+TNd8#*tb}bykdj3Qp_dT z&snetp^G|y#B|uEz+}xa_ZL7t8Ohivpk;WVyVx@n2PD0_l6$ro@X3fe>ucE z4=LDLDtdU&P@_1$-10kb(r52Y)X<6`_37u&txr3D{TEsEJnzx=UKQ=$-@o5|!-)0U zv%!b}00IDi!2l3=L?#srhC^Y{_=H9o1A)O{u^6O(8ZQ!ufMd`{{BAiTl0u^~X)GpB z29(QX()oloS0aH&gQzR3Jol;R*q}Z#I3iRH|QKDEY6?4vxB=ycpIcO7!S;W3$fu1@`$%ic4YYn^W+jnP{&8QC6#DV)?`FY>Kqw^gmB z>~nfursl1mw(hX?N#q+}zKCUax*NW)aTDY5G@PBUFK^K2YImA`MUM@`zjI;{xEw0g zUWZsbI}Vf|!R2wjn0H?c`^Ui8L;Z8Dwqx^u)w22hOFR1NJCF;srMt_t3bnv&6F&XF zkbEHiL2!&F=fNx-9L~b9L;$`+DuZVNL$IVi#zSi)PU^7_`$nogF-v~*rBFN2>O~PM zkqkyqjCmfnQG%RkTki3r_KOoMqbp4zT~)Vj+{f8Zt>ItUmyF|NS=AGQ_*HkTEr3U{Tm@Iz z6r6Q?-xS6Vh2L@wYae0oln-fBm~Ix-LwKDPe!w{NO^DDotzC>xb>=gK z<@1+J`GkXt-8J3?mQ&VIn=l{LXlVA+(-m1jBA@snNJ9yOlCnsp@*;@QQG zrdhIfX@Xap_Lr<>_x_EOY86$1tm{^`rAX}a?vaUYxE^hBDm$LnmgRST)u!mU{nNhK zd)^1KSiAQLr`G$<1Hob2#Mdx?S_qA&bzJ(&D)!s#Ts#0m8pm#&er{L})Jn;Xo(o`yR9%cW`O^GzS9u z;CaP?YQhvjC1ms+To`-~gp)!C_UTHi*L<+8|G+o(2p_aBhpiuz;e}DuEC9g2QNu3?3Qqi7?_HL#WcK9~44% z5atR+C<_K+dSi)ix;#F}&llI^BaE)od$9&Wz}Q?+2)gWu<%pt{<*>Gq zo18w&b#Itu%_W}96=^Xt14T&2pX1xVQqK87L3rH+9xQ7?PgXB~GML*c$m^F~QVLPT zsGAk#i&K|z23bd$Ll))AsFbnot;ciA92sH*lhMvc%hmBSFY>WwGp1j7Ia9a zXDTG+7HYJzmqEHGOjX4vf^*`HKdEOYX3Zj#?ntnbmqy)={B&D~xPQp%N=qsl=%|Jq zn^0*N6{JIpp>hU@RVWic<#hmLwB|6=wC6i$eKb;xdAGEGrTteY@+OqB!THZ6i4mvm zUaD16b=L|_SfTxRuJu{k&l#Ox8`Ri))!DMv3OyaGjKQqTwzODC(OY9xb+S;#ghFU( zStykRWR@9%*2O7C*b^I|wEAdRyC-PY4VRamu+|dwQ+sEo-|MzCu}h5g9BkiMCdzKhf7`Xsw0>vUl#S*w-I< zYd!P9m#YX<%d2~A%{mGg^k2)t?Hen>1!}&bGfMOHApeQhrg=dFLuw zY|)d@G%c^%LqX-@)oUSsV#n7PPHIJaNi24f!g(Ju>WCepa^7x~`Zn%sqOGU3zEjcq zi!0%1&8eyGsi6862b`9I7_= z-+J$HXWGMx_B;X6`!{~Uy$xmdzNyiFE!Ms69#g8(PU)q32TA3AV>9#LEt~i=a+|!% zZ%n@j!@U2c@A!wW@II%{yDjGN9v{ed&q1~O#tGKm1C#R%vFCYLJHJMzIC?MP~dLgv(x7XY4Ki?`u&ed@ShKOou1u)#(nuw zQ~hnV@SYpSaZTXIde7JUk5csv0>7wW{!dOkFU0jPh{~_b{;x9pj-2=JVDj&bk1qt= zE0VBp=>JT>&rlM>&;0*yUeqrO0jFyKFgXM7V*1cVXb=whusrU_nE{YCwJ*~D@7VoM zeFab6@&{!As5t{rNbk;WwD4$u-U{f?5K3+kPSDU|{mbZ(Ps0W5=Lo9k;>!fxFC6%8 zz@Bf52ucA0%0UAU=3cIt)M~E*P>k8oklS$7O)s={Fp9l!p#a0x3TTG~Fc$s~7YVPn z?2bbR5N^28cHU5W{xInX5X$awvg^>C;_xQ&u$X}ghFVN&#UiF|&!#MYEMAi-;3?5G z#X@jNjATAB84vKRo9x`d2AJr`itI2@@vy%X&6KQg_BF~g1F!`5ab)+e&k#*G%28Jd zP~z3`4FoRZ7>n-`P;nIzT=P+8%`m*%FlPVolD6)f4^fR4@D86VuLKcj?{Sc5?>N_y zzZ&s-91&nH(WuFq(B*vB!r_z&|4QF!!m z!w-%_7%`y95V07pwAOLa9qA7qG7}l39}Ln4tgk&D@$~?1JlV1R8!{lL5px|<3mj4< zB@xplkXG@pbr2E=|BLb3=Xo| zuK=2ocP31Q4pNM`5Zdb!X%#VJD$xTj(AMv)ktkAl@=>1)u2S|=4$KSA=TLa-lIIIe z)eaJ|9@67068#TQza4a0~XUsG!XqY(%B0x=`{-j9J7xR(^)VNSoPC#_mfo-a}PDJb1$*s zEz^4@Z&b6hlQ|J<8?xUd(?qUQi#QJCzb;KS?`Z^4AvVr`i8E7z(^CU9^RXW@p)kww zF;i;^Q(*kF={wW4A8)%iQyCW1D?U;^DUb&}@`lY5i9gbL9Ml&V^GhiaeLFNN^fL=5 zv9UUICmA#eFGXZ>kn=la5X7cQMB~K{>Y%hzzKQ2DsSWu>jiD_ ziu5zX2+}TpVKd1f)Gx4b*i!^Yc!Ws;!j^Dvm`d?zcjJH4Q&i7UWBL8&v;Ijf+#$u^+Ib zPn5+{G}|_mwM+B^OI0ORH4!Q`4^9)yBo!LaH2X*k_f^z^2ThMrPSr_uQ4KYJ4fN#b zHA7PMK;A}IRCJ>owQk)NAwUraJB@`wbzMl4t3Pz}OciM#l?6z%yG*i|D^(_Fm3b=E zg;-R7Nm!KQ9B9azyM=ZEAZmC9f^ThDhf*NG%HQF=C2 zN0(V|ch!9NF>EuXWiTmkmfKO6M|qeRdp75Lcd1O5muOMRDEGx^);mL%{~gx@YBvXP z_yKU!-D556X5azd=i=BBBC-INwY+_y7U`gh62tctkV-3WGyp5a?uo9uEJE zL!&?#>bxPG?eiq{??Tp3CI2$%OtF zM4-o}^U3_y6HA%K=hSM9?oBPLROwVY6-seWq||BExpe+pSEx`dwMu2O(IBkX?UvYW zj?Y@V*Jd{>72=CVvEOe~tF@+ob6C38u5+wK9_5X%Q!us6l_J+_$>eV~YyL*7RJY~v zdAx=P7nR51teR{-t4T}E;I(+GR-T)dxY;jztW9?_q0sL3Ts^j9X|1H`cNz?~|Cc%E zYI2)whVQGr&Sg1WEf%A*-Pvq-*UV;jbJyr``}{8+&j*h0=55}seZ#ST#N=nVzn^7; zZr?A?(_q&?kP^c3zR!a`z&Wo{cKW|h+rH8_dRdhH1j^~tU~`f&??Ia zKd`gg4@F8eQ0zSr{0|B_@QhlzK}|ej;INOQJ03?dqqz#h4-8ET!EgL=B|4G(0`A4~ z3^^pXFnk9D$_-SLBu7$zbGY-w(JPZ9$@3Gz5j`^dnE*VIbgvspv7B!FJ~DfO2FP=? zeJnQ81Z?fMkAyJ|MzGxJILz{Kc1f>|v(&ZI@OjL@cJvmVI1UEuc zQk_8-Owz4)GS<{}b7EQ26`@#IZ+uT*MpDejELTw_B@5gY6s1es*DX11RJQ%CWLfiE zmuWP0OzB6+mAsi{QnZ!1CD>G*^;p<8rEziGSZ)(u+?Z6Af!;J+9b-(Gg=Kx(^JTMl z-thI+hg4{KGPfjuRPIjlPm96?f+C3OKJ>pyCR)1YSw3)!qbTKzcw-ecp^!+|riWzP zRt2j#X!WGKqhQ!&`=~{j9glF=kj^us+q&fgjnwzfg;hrP<}aXa8ulw}ZF&CHpw?8T z-E?Z3h6`qFlm;meF6w?4rfa*V!L@I9HP^mV`tJ*E@jBLjZ+UPWX2-!`yKX_n(Hfpf zzw0~3k;!m%o)0%}ujS8xW7;nxj&!>A&5d%qe=T_59S38;?$gHeRP!2oe28M(cNgC1 zeji8MZ2Wgas`0gUOQ&$2KPlw$+P53#Y8t*DNsz2`m*ys6Ojp@4qg)S^lLlnVb_Et< zTw9A$&SXtlmh`0j!I_gufkM1Q-iJc5!ylkAa&J{}; zIX0(M^q*3~Xin+&XC)N>o0IxmP)X}PW;`H&new)1%ZLLqs1*^2b4p21iWx+x#L<0p z8jQ|5Gcc&+r=&5GgiP8LujwqPq0df;QTae=8Crs8vLuB#@;6N+X%A)N{hu;wY_VhX zk*CxIg;UyHQR+oWsZ}nfRN9?W>V;9NRZgl^TCG*;#aXLWZmw0@y;ti6VXRe-vQ}Du z%~|V3X{}YRwpQBRTkC~!u2s&uS6b~|>&16wWm=(A0^mhU#EXxN@Qg@F4PM#ZL#MVN zh1c3_+LYwd-x zwpPyCTU%{y?ZbqyN%lwA%Lqtq`!N82BmSMD%NJwIHIb3`9z;^xb93&6(YjYo>Rnr{ zb?(L4yH{@RUE95P?*-w!SB~;tTg`dzMd`g%Ntx7QC2s6W54jh%-jJIOO^iMBxk|SE z5?d>J@CE_ESPueVTn&NnMhU@KF9u-T9fR9CaMLKc*kcxA++B+pCn`)t(Ee|S<6{(E@j-km-7Z;%vp~zW?apg^F|TKWQv-V z0vU=;1;NRV<{KOf5KQ!mu{h#y(fzVnHLTFtLq4Y+H(ONG?Xx$xC^SW@& z_ae!o{N{znAo`Hb{$dh4_Z>8zQz=;-wc@;iN3A84Tho_RX!^N0^=_`R+Q!i8tw})i z?y}YTcUo(z?XPV9wrNvWU~B59tToQYuNxN5>?OgjC=SZ28zX4#Eq}6qZ640Q6oYJS zZI7=u?WxVVM=D{x%Ws?Uu_E}28%Mp=LUg6?Vm1QB6D>_O_OAGzT7isjnN_%G?*NUA%y}C$D)h#C@<*A=_&+|#+}Cb&4pGrJ*9_*|8)Ea0 zm(sQ;Lg&33q4fw2*oBys#9+6YI$4=v8vpMmflTBUZM2|;{+d@}CbN67HrnZY~n zmhqDp%=Qkk=W2(bTs{lRb?+y~%9n@q{*%1;s*&TpT)gy0Qur>t+`3fBlFfyJLC90D z9`xah)H;`VU+j~Sc-ge}A1iL_9>c=C&&%-48lr9wtL%Aq%=F)D)Rb5s-1qmh;r?Hs zek8m3QR;5|KR4@tcps(veBY(}zAaWSf5GdQ&wBfR1O9$~Oz&^*@NcgF4i@UK{AG_m z{g2lHZ&LaY0Rbjx`j3MGZ~FBR5dUwo`ff!4FS7nE`0_{u{Lk|QFVg_8lLGK4q>uFb zutbz@!vw_}1ny}A&?LFgIRo##0Ptq+um1xOaFb1`1dthj2GA7ykWlc@9QAMB{SXHN zuyX>?I|px*15QZ>?n?%bmizB2NwApyaH9rL!3pQ-1#qbQ4hI4+=22cA68*zNHp4zQgAZ!rPzeFhL)6A@7eXAu?<_ULf{ z)o;e(kvR)cn*h-T3~?0>acK`QM-D@i3bs15$@v~(ODW%7Xqw?hQ#|ACdwx7da&<*YPb#3_XB+S3)(@^w+v4U8Bew= zF`F1s!xwRT8u13^k*NxBOA9f19?&xgvGo%%{}D0<;IbDFk{<{%Cmd1{9C1k-5P1<1 zXB(066RoJSP}d)i$06?k#SJS0FkXj`Jta@u5wKAskR>G24(akYCbAtP@arLOog|Sp zACX6Y7;*&_BpD&HF(c9QAyJnp5D6r*KO!&vBCrQ4Pzf8cDH^f?7t&=bGP5eu46=zsU)UZ1CdK)(z$%XX(xkCWpPQgVn--{i%h378U*@PM48U0GYUNlB^jhiWAsU- z7H=-0)n+u>WLBL$tWxT9sGW98JEY5I)=H&D8(XzlZPSXC!oy&NU9Oh8^}hFUt6c20 zdu2oeg_TL>cWX_`F@}g=?YRl%wjWfy%5ixMW={1#qgmydn$2S`ebVA|8eF!gJ*2IF z#V~j5?vG=d(`_!7iuQ7!T-((38Tb* z@V!mLy8Xcq1R(xCPBWOLtmE8kr+xN{C7}r!qi-p_M{P~34HO0|kv6r?jS>W;h9cwF;J);TNus$<$ z-12PMf7JJ_35rR^fcoWBQd$-&sW3p}B-iNzs z-4^E=;C9vty>cCHO^fbVUR&NO(=r*~y(kmU>`VvL#;Vj`4@)mi$V|wOehH?E%kTZO zCo1kbCr`(VBS|;1$S>*)-;$dhhY& zy2ujqU|bw?Pt{E~SB9e?6eeMiiH$;ap9LUms#Fhbg|bx`a+M?8RIlb1J*X1+lR6!H z@O~D-XMW~jQ_+O5X}ChCve}_?U4t<$D#dst#vzm*fAArZ!uFvQ98_6~u~AyU#wzKb z>>+URjq1SoR!k#*i&c6NnC3;cuND_7)P89O9Ga+i8RKkRfG+ATL?y8ZBs-Rh(hbhU zSeXPObB>Qu(n!Z>!676RaDT;;JVc2N6p~xBk#V|7L)bAU9Fg`N4=G`D(fm4oh zVf#sk^$_8lsE$zBgF}Rm4<;0jkP;d(MYxqLpZq?E&k>J*$=MYFJiGHx-X_FnCo z>W7L5)lHXGA@FCa%J>Hu`WR#V^CVIHKgab{IFG)f;oO&p^p04`*=F(Jsb>%I?2)j4 zw!)a;9PMH2jzPjXBHz!PCw?(yM9*#)Qe539oA0WZzWRq1>Y5Qx?8cX~_ybMk#E)rb z@$l9XBUacgk76=Ap3`viDbDKUtS?58%o(ELV~MAxt^TsrcRN#GI9aMIeyY1!k7Vm> z+p%!2RjUuPTkTE9u2MF=+jh%oT%A>aok{M)g1IYWZtdG&vldX^8|zAK`>Uoi4zt&r zHyLfrYb7_np2WN_bntCnPq!A}-#d4J>h2wqwsz~nTbqDxjnTDEKO5rPPmWTI0gaIc z!k8QSk8GIx&NvpWv$_71p#Ii|YQtoLMa3x}^P-%sHjx|i)9i;1{bWu-l* zIB{J#Y@4Y7ApD@&AW;d1pT1yqc%xOxxXlwOr?4SGjUr%kS|9U&~svxNILSdcRI{=euk0 z{jZCVzZaeJpU0Q?ZpE-Tr_0V=8z84@;>rOKtkSsw)6j`gZ02G zFgo-GwTj$8tIxoTfpCOez|0Un1Q9^A8$l}zLPMZF zY%3?@$h{MaJ3K78%f`NcLr^?BVnB2+Ks*M)#2%jH9lq=w!o$fzR5i4O3O>Xly)-#H z%q6g_CqHW&zkA_93;x2q5<;Xm!!#DO>?)iC3Bi+yL`iG6JLfXnMm$7BKJ+%fJVKRJ zOualg!t4%@tUJ8iB0m&0MHCe_vS7VhrNs#Zy;y@ilBqwNQH~0KtubsjKg>b8yj;P2 zEk1k?L;3)~!>YXmO+-8BL|jTl3t+|}%e~}7Mf@c{)F#55YQ~IgM!amWOg}Z;WIi)d zEbDe3tY=2_MyXU&#uJFb`^35Ybiz63$9!PNly<};az`qjzoZhLWH>|1`opAD#@oe5 zV~($5XR@>>$LlnIyNmiqd`Cj$UPe4m#*-JsnmI?rinjcEKOBrlQ*}rrVZq{u$NTul z!$RDFNnAZh6oWH#e>&V?IC{Ik1dzq-0XV5;NbGG#%s#_9fJocj$kd)koTA69 znMsVD$r3lm)DB5BA3;=MMQ+jJgqo?L?%VVi9FOO%3PdD?2*TW zph&!?OHp!45NnX^oYD$y?DwWb4E)-mBgt6#$2pIq`x`H@Vz>|OKiYK zv%5Tmugo-mOTlxiLS(v0q)5g*sjut+Obc8{@l?oi7%N1uuDEyoNuWDYMmTfL91)cyWUO8{ip15&Fr+!Z05%Uj1fHICE9g6#B)M4 zC{C28$P8@Gc+Sp@l}^;*MI`G@<9*6J|4kERPYaKKPg)MTYZU+U zGK*5CvUDAV2|+o@P^{`t1rW~c`Zk3lG|ZOLJc7|=lF=k+(KFGcO%YL(&Cmd@&=UC2 zB`Z>YyjsT%B}#OUw&Qk7EeBEU2+use(hP3UwIWcu5>I+G&NOIFMJ_v4CrlKX$zr0j z;QBq%T)M&2lA_+KiX6=7yS}+lMs-9zMA%Wwc|!}gMnw)g)CbD!L(v3*)7>u8jWyK8 zP0c)+51g~ggomhueoP$|QxxD*WdqZdQc-k&tJLZpQ@mGEo1M4%tJTvUs|^NIY;I7+ z7*5OkP*qx1El<#WP{8_URGf9zRbb8BGSEDs(gcE2ksQmhc~nhNJk?{v1W#0ig zwLRDn;aDWls%37~Z7$IT4^_o5)C@^Pt%%rdiP!at%9V@R-22$ojn$mm&t)Rj6??rR z`y51zIOSB$ouyZr(oosUS##Fd#iZJQD=APVLNt5Js?%rGGJH`%*2RFnMP)zREuvc7 z7*frlQI%sxO!7*7g-LS#T8)^zWHQ^--PPSZ*iAt+#Z6UoAlCIE*`2FZUAfCOtvCGA z%0y8?iPq2MXjo;uLb3B$HI3YCBU`Ov+q`YR)xRrCn%yjj+yyGA4a!_4(L=?5aoLqG zRPB~lt-ak%rp+N8-JR218E;&*dQ^QrJhjc)g|1xn*h)=4U40VU&3?!ro0dU6kO?)!NtnPDdqeUDF0qg}=E2mD@FvT*cAeB>>xv0^UXI-V6}lWluAu z)Lw*3U(54dRH|8B*51X}-p!wXSpBxfEydpr#9Mt*!zJC`?aAHn%-#i$S+x<|)%V@K zd0Hj;;HwwkwB}xO%(}?c`Ntmtak);64+OP6|?95@40| z(xuDJ+MrZw+1n+*agq-t2QnK2!pI^~+xJ@!w{^FO)=U?lq0Rs~vrJ{#ecH(on0 zP$mLV#6RM-$u(tzC6$^}t%+V0w=-4M;?;^>mL%g;ePV>bVU46t#uwjiEMV2d;O%$f z%;#eM7UK&m-~A0?ZI0k(GtV|fQKV~QweTF}#Nq}5;^WU%zDZ=Q`{Wh~OMVb!4DVsK z6x{wP&z*R|6piFFqs%6MGGWwSWNm(AeOuE0DCAx@WJQ#(bEQ>SpDDQWh$IS4859L+(`JA&2;Y zfPgt4Oh@=L!i+JL>4IqheV=42)r^E8;-^y@#!R%O(&E}Wl}&4 zwmAQg%H~n%&;lJWn9Sqy>BOQzDWA#Zvw4Izc}1H@=G1x2>YX%_(`eL6lrn=*g;c0= z`mH{xu zjn3M!+dYM|bzsSDC78|@n=hZ^apD|aPd81>lI`*Qi+1mSqmJowyI0X2g zryDSFbWZ+%#0s+Q9KR9cqXMDpi}eybkS5OtAB97YkKXt&I6qRP_-}Qj>J-=d`uL-51&GHLEMW^h|4F#82gh z;K+4-w{1I;oUv3~)#WUUKo$H6Slu<8DO%i2J?mgvbEO$g!xzkLM_4!2k9pWl{mFsM zPo1TI9@;m(VTD8(Tp4WHn4P&9M3@BkaNXDq)URDQy{#N%I6f^_-;qWeH{jT-xs}@W zz8#dcbXwC$Vpm+1jb^wO5h!4|rhkX58O~LBW4HE;h~BnVKWt=JB!6$?cWz^kYB`o! zOWhjYvvuVe9$%zX)IOfz<{5SRr)biCZ=~OUdRBL;XS+Uz?N67M?y_PVg%z@GRo<6h z?YlO=!DP^mGrC8(=C8SMFE&c8X`1&FWNbT)Z?w|e*A>7x+BNaI@ch;n!oNA?-@9*a zPV*%4IK=Z>8UQ(X43MB(j4#hABDz;_U{-VPe$Ivjzvl4$V0)W<&|MwD zXhN7^^FV5Ft)VcL*!$B1Zh8C(L-gSgTNAB_kwP)NIJE=f zQ}T(CA}}5(*#F{`0dUb(=S6qn7Gv{Ti;sdl#MtW&qq!n*Wh9SxCvW$stN?RDVSA%)fa73>PdBKQDSYowuy1VtkW@%aS3mCEuZ5WoU_JgmRaV>;4J5fb7DPBH3JN1Q&4v^f;>;= z8968n!H_aWf;?A$GA2~Xp;GQx%lZ>EC`9(6@Rn-H$s0Q-6UUB|dQQ7KDL^PRzKiqS zT+()bGZ!5rZInTaN6FtzCA}7Po|Zf5i%g95 zP*#*R{LuKlO{&Dps*-+;)S5j#PYq3Zv*vwNIcQr~Vc;%Oyqv=-7ZaDIZ;Q2hvdVH} zPABDAs@2kT)4G>UX8muUR7M_ED+bmi#Z6K#rNcHF%=Rk-rLPj1&{H}!U#b;$e3LeI z*@FE>>O}8yNODKmdo1{WtevK_^>)!Lczt7M&5xS$<_B7slIJZvthKhP+0xqYR_P^# zqPFg?SIM_E?X{VF*CyUrg*9TLG>osnaS7xqjd|0wdu-XW7b|*yKfOi^o+(z+0 zUZd^1HwKB=Yu7`neAcz*{>MqXtzvHl@1JV*GueB|a+-P4xtBVBiBgOH+APKCq}RFw zGt38mqZPElmqj9$yJLN+_19lmKDZIW1uPa>@_eqcRyS9BFDkwheRrDu;@lwZab>)J z5dxZGxs2-ZJ?+1@k~r2ZGam^x zQ#Bsk4cyN7y1d@&?uewi3YC{EEHoke!)~``Hdu56?3cGi+mqY07 zS0+`S;<&r7V(jfNQlF$&cbhM3ZJZ-G>DGJN7KbtJ*#D+?zSybP4|n2i`EPaA-I3cv zO7AT(w=%_le%GcKiWHb$#P!bo)lg%KSsn|(@~)uO(PM;X+()nXPaN4htB3B+SIhWo z58gbX&+(1x#<)*7(A;?sX*?K|Z?HZSd_P&L?wQq0f{tt-vKB3NbB4(|dv&EhcKj1SKpw|21g7!Zt+ia&x z;HP5@a^FqUJYRXydzZ=guOL)6*MZF)H^*Nd+1+&^Z-B33*!PZ84KhyT_TE3)WeNNF_GZk!Yuz2@O4is>i$oA^uAlu z{kiGnzCvpHpUcF|AKLfMK=H4~s(5@7mFM@gx)-wqD`HtlN3XcJh z9Qx1{^bIWk4VeDVs=BUz`tSbDP4?QYSo@8Ce3p<7%a3;FkQU)>YP?U*{Yd)r#;)ei z4D@er121a)@BocZqW>!H^{(v%Z(#(lNZC+P0k4eGhBF1JeE~3m1}{Si&U*pkB=~S7 z^Y7dtj7tgQINSnL3@~PUf@fvV znGkIY&fLe5HrH!i7X`}|ubT!?@f8p232{#1e+*_Z@V2orl(BJN5V2(akS3BbN`CRO z#Bm(G?+%ktcNh_%*zutg5g`3Bj@yNs9B3yQkO=89X%A3A50KQ+?^hi|s~*Hl8*y6@ z#XBC5vkI%_Ar5H7G4%=#$sG_$5D|jUv4;ZjoMunk4kv#hgHIq*GV8JP956WnjWrT- ze<-=~JX{h98}L&Zv1o{LNQp95eR0aYanB#pP%+{CC!hcTQh^O}E_N`;%+cCAP|XYI zb0(=Dl- zLhCUC_c2EsF_Od}00T#7YwjkV+x2D5REA6_rV*G0D6NSu>u?qf;5Q;)zA0QRx%;-6EMpn#XCh zsB9u&`IC`hr-3-QSKAgTx#&ga`5Z3u<8IY=Y?-b0$GPwRzDbgxf4#hKBk-5M z%>$g5zmQ9mva9YK80o(aWE9Fk4$GATz>g|V1VZbYlKsNZ1T@>k(JHGG#ZfDS?Yjyb z4C_Km6h8Yzj#}p9KahL^+{TamLe8wN6nyf$?7Qz9KQ9bN6hLwlyn?o_1UTtNQ1q<2 zND@T0@SzYCqP5BJ>$5FFf3qX3;J33%wzVPQ)$o!3xZ)vcXe+lv&rbhQlCVv9mG#E&J| z@(LMNT~z$xf=U+yQEOb&-N|I(wpBjkUQ@-OqujS0S&7{j?0anAvaRUV&$O;ZaNh1r z^NKXsU5ArRP{tE>e`ZYnF?r^cJ_l#uxV`e(-*ui)(ck!fEi+Bo){mS_xwV;|N>PrB zWMSH*pKn)`O`B<9nlkB%C3dBWmboi6ACO^M-g{k9+IvT#WI9|@tTLzh$bA@_ZecNL zxHg$CU-caWGwt}hg4x*Dw3}%{oCd|T>036fRAbfJv8hYie@t`A?W?YVyzLo$>zLjb z#J|O4PZvYJ!hAMMMPrvgRn^_^bH~PZ)~_E{=lxb$+;?4_FSGI(mjl=J93*PU@cN%K z-*>!kQQY=g%ztunchoajYP!Ue;qzW*0{C>D7pW;oy!ua{b~O)ExcD~@TW{-F|Jm|> z6QAhe`ajL*97y}U_#66EK_5?i`R;{BHX?so~ z@5NW|72vd4e(W9hNf%GZvX+ho6ZGd+?`*yjW0o2;AhCKyBst2xhE7F(f114O zDl@8wGg=KhrBv{u6Dlb_3SLvG6ri6i>Ko7*^*@}w0HZ8sp0x>b@g!u~n)BR@ugY;V zWrS3rG4eD*ih|}T{X?oNMZrqx$4DeKBA!%`($Z*T-wrHrrIb#t(5dNEY*9y6%U!z&0Q&2r(7wt@sag5xYL>uMcplStM)36K)O9f>eVH7 z)t&>^2n}B8RP(C!mXX$p8%gM-_@*`vh}VlvRHSshuJ#Is)k^hBY(t!|(2g@rD_v8o zeSMboK4!9c7bv0)GJn=a;n_LUWm%1=f0eP3_*tugO6ICgMb?7nN2{${R$aTQ)atp? z*x6$m<+-r-{^ZI#M^V}egeJ||yjPX5z^_c9GIjpcx|tJ7Z%pB~=VI&4NZ%)ItwLB6 znj+c>Ykpi@OtklbjHVc|8lr(Ath8dfG>Rl~@Y5@7E$$^v7%vH`js{~@`t(SPe*q6A z^5U`UZsXR+J&T-;YP|RIkKg2lG%$@<#8&3+TV#!9uiYKLu#TGI+rw1lrAETHn%Cqz zvyf=4iIovr4q}=0DPJid$*+0Xuq-l2y9Il%e5wykXoMYZra_v zaJIJC;E6#IpPg3Jb=G zEEPUOo4HG=L37$aTF*e_6TegV3)A@&qd^V>iBc%+jyK@2mQAWapvOBVxHly)GQ%N|>JVi3Uy<5zu3Qa-uDnol@ zLaaGDJUS@+Si`%e#XLPci^8x>UNfRkKcrVf>}9z8BfQixKqHmC6dt;SM8E7+rV_#$ z0}MH|YCpkqKhvT_)N(*vPQU~{Elg=clwU&2+Lwf7e?}Ak5O4uPQqc;dsywK>I@0~R z>?EyY3Bh}pK+J7K{1ZU*QlqR6#S|38V4_E>!$_of#l%2Cq+qp#Gm2y0seCF$Gd@Cj zI>;jM#Do&V+-s!r2s^A?#2l5!^n^wztw^*uiY$sfG*m-lIYvBn$GDO~V~If&5=67= zH~RU;e_N|Q`{TrOwZII9K6Bzm%k9S8sz{sJ$()-(3qHvcyhz-YNvsFSd}7M+E6B*b z$ueClgq+I+rn{smz}%QhRGCPuw+b|^#3Z`Mj5fqv14uJwz$@L$#9P8#qD#E0tQq%S4gMLNA`gZp!@N ze@sKkG;?*!B=k?DWY5r(P24)oyxu{)*UV!>P8?NF9Qi`jolZ>h%uMDvEbmG*q(#)H zLmaEXlxC+KVaA+RM}+cFB?ZJB$k5EfP;8>c^!3d&R?tMV(O~||R3Oa^zfR>2&6EMj zW4h5r8cmH8(JcZ{44g{L@6c5tOm!d5e}xxIr2$GkB2hH#(0qbMAga7Tp2z7nE`z|j z)6h@_Etx5+x5KIl?IO}t&?RLa%hcP>tsT+z2-77q%^e`jyv(jZ=f?ogP`x@wL*k=d zH=SKPQ!O7-l!;N*5)4%9%@izC?B~n0 zRmC_}g(KBOnpQBhN)=Dl$lz5F#Z@InRW&5kT0GTVQ^?gPReeGsqt2D>T2iEp)SY40 zVNq0dG^p)gQ}tWc4LDa757f*?f6(=0OBFdrwQ^K!3=BBxgv|7QF*h&p8KU(?`e@rEzLCaN0 zzt}~cJY|8{OiEgPrrEWq*6D>=X|GXrrdpk@A=RzT)sHORINwcnfTnTf3#e6%v_C~T=bIMO{-f~#!+p~Q`NX#9n4)_d|g$X-C?F# znIYYk*in7c){WXzt^>$b#lcojGT}|2ET5?|f6f7;dc-v#$s*pA;# zJ>L!Hs}1+08^$0U?~pZijd!J^YZH-~J3?Rs5rt6I=ck;Z6Nth4Ep1so@qC;Qjnz z0?gq?87kf!on9MZf9@dN{vX^P1L5`;lBJ%F_?0aE`;)Gp&OxKjJY&tSWKKm|Zav|yKIA?yVU8}@rb=8!e?r?HOW}4uWNpdhW=~u; z4P!B;2t~WMo{B6SY?g{Wgb3dE>`6}NnQ?BWy?llY~s)Dq{or; zS{*MZl0V{pWJR)`v=_LS%~ zy6GNyX#P2AUL$EfZRv)1+5T_ob*fs1nrF6|VP>68e@>t2?XKyzcj>`{YBq4^E_!M< zS6wEPY9^BEW`Jr&kZKNm}sij=$>;#9eN7Y<1mi zmEvqZf5+<{uidW3UiQUoz2j^i&fLzxY+lgp_0H`s&1Ig(?KSc0j>~0cf9-zOUhd4@ zhRy57)NP)PVTQ_O?%3_kW>TC8m)?ZMB+MZ&a);3NVqq4~48u`feCh7lZPp2*efe&t z>~7ALXtuF#p6wt$>uS#L?;h{#Hll9!@+{ise_y8F?@sq)F7s}d;BS5*Zw^53ru=LU z`0L*J@5#R|4wmot0dL_c(zT;61)hO~vc!Qv6f)FM?Zl*OYlz4Sg1{^LK~ zucj?Vd&7~&BoPh{^D%DS?C-lC)|*w`@OXS)KPCIWF zCd{3L)L7O8m0wxbi}7W!cB3O^w^Sl~WmykRp=l@9#ldmbR_(8IJJdRpe``yVz1Yv( zS0Y1i-dC)9ZCtm+<9a4{eA#zh4V@&8#41%nj37uNRZd0jof{Kj&aD@QPxsY%c;48~ zHI3tzJ}p+`816-pSc+YP zrLF6EOjAfK$tUTWULNU1e^a4H%WH$n*Oe*Bhf!YiE>?9e|gn1oyITJcGgRA zTgn~fx8L+^H$~pYz7u8LWFA+MUiGgRn7aBarYWiU_TQai`**jesX;JGq_FjTKY zHpuv1D;kFmQWv23cMKhQO@`3WtHd_F57|^mTrl+pM5nC`Qj|f6j-Dw%*qr7fW5ar= zDl4D}pkpHZQ!Q~Bt;N1Q>f2gwU1e}x8iWEUA6xaPb5m~!@RIrn6>X4@H?DY|jZ+06;&`;eF|wsNM4+d1Xz zzb#X}vd*?yJtu^wo<*j7JlX8Z=j{1`%uLlOty>P4%zKs*A5JYBt#&NSkh$;hXGb_swYABjP*cP=% zkVS5me~eniFWT5%Y-+Tz=C;V7YbNJxyh*N6Hq6u2iDgV>ojlc%f7(ThXp>c_t#+Dw z*|;HUrv;3*4zjdXTUd%MsqwTF_Sw939WH%QFpqRV4MxvnwNvddspWcQZHwHkaymR3lXI@tZnxX@ z{-JchTd#OLHVW5>!(VYzY;4ybi^)Rpe^&eCizAf6&x{2v5-6n-#!mCCg?&hTrjCW5aTfqz-=5E5k#>w`w}~_N_P;e@LXD< zGthg}6|d1t;|i_|)HIRC3p9M9L(${!AVl%Biz7)gQ?nGYQM(}WNet?czDe(Fq?EnQ z`#^?1(zJ;o$MXzn3pvvqmcz>ve?-?dNb@_D4omAJRW&Ygl&d4DgH?j2UwlJ*LxU+=C1f?_u1ZzSOFMk!?9c-Bb1 z8>gROSe{W);db_hi8?igF>TWDz2By2 zw(cn>PP(o=0MEKam7wQZ-ld*j+KoYA=-8FRrRx}Wooj5@2B(Ma7;dkx%eI!)6jM|7 z!;tKAcIy>w%_h4O?7NoR9c?@YiAqyEMP+gDl4kG4>=)++vTt0he?`e%+y^@=?-r*G zzHZO|70hvzR}Rf;9TpkYQk{1Epep zo#S-=XX(m)G3$7Kf3C&$zt?or-@5gG$Qf-w6mbFF3N>DBA_%~jqXAkx34xEQ3LVGZ zOk9inc5rRSvRFj`o6I4C&neQpNK&a^Tn<%SGIf5q6eSYt7PO}tn1`yy0D ziP44tMktvKVMI}jF^z!6de1^1wdC00WM=Debs;$NkRG57kl4~4=P?C$w_-8R`MOljOI%icm)g`AL z1gh08e|}PGpFkz#5vo;^h|a3@NoPsgh%gB+)%DpAD;&*|6CStHdbp7*RCsD{%1+11 z(PS&-cCR(=u*@kLODxTPu-0DCReGCftn7xQlwx^UMiFMJ)u&Gq#dZzmJiw)NTRF^cgx?F(nH@LtHz8pOQ~L8mYg2UZ1=6&Z*Ad&B+d!>F(091KuVSbta_)V* zzj1>TM>;Qnax{^~&eHj0e3fh?c09=!*5k_z!qM?mF2u4QF65knh^w7p#hI%FM@knK zqOMiN83xhY-0_X_er?P7b>Xx0kv+35e~P#d0*&P~+m)v_MZmeoKi~Z6l(Up8%XrU4 zV_6%HF%9HDmd>hXyv2L+^J2(&r$}hWcaL25OriG2J7o&nrL>+uu9_bLU1o(q^!BAy z`0fmAZ0{5BZa$~@A*A648f|1>Bq7?rR$Vy>~_Sa+#+^#+|)^>fI^VPpfg> z&EXjI2D^aSpFKDI&P1tq4MAKJZQG3rpRbP$-&g~FY50w%kOOq!S-*d29q~}NhE3$s zGllSuC1Q0(VCK7XjPBkoyZB5af7`roiSX@nczFiC-+UvaYdh1$XhxjnJf`9Ftev;q zw&7HK&dPI}>6^A5slQyuh$?z%JQcOt>rd^bSt8>@~*em&86nooi&HtG#UoGmIV@uX8Wvnfg z>%YGzFnY~t@jcyZab72Lcn=HT_&>|aHP_zWjnnWUf716{ugT}VPp15}l!Usnwe6oV zxU5}X``@3h^}pYzWDiUEf4{r-p}u)*|5NR=<19ClezY6UG()o~909-*-!J30KSTmP z8?LSUs6VUUzEk-%8)ZPN?7qXIzO)Ivd%G@+N;~uJvdgK!Q+2Z(13zR07K4((6B;JvGUb<{1f4&p@!So0_1MtEd z@xiZi(A4Itim)e9=ry?!&yQ64MPe^KMnE#zPvKEtL;Ksps;w7 zJweO8no*LV+KsykEwH7qbMh(}y*_j!!V1hl+y%mLwKU7kz6;wr!+)zh9lwLUL37A2 zgd{>^%|Vm4!K^I1f2+mAd`?6odqoT!#7qD~tF}P&5=Dc~#G^dBj9EF9Ma8?9#iT35 z{7)EEH$C)FMO;I?Qf;}4M<5HB7vxJpOe)4SH8@8M>5UMC?+=)9S_a zQ9>jhBis*$9s`JJWIJ$e`2cYs>GaDwF?|W6cV_k zH%3Bo#!-Dnyhg`lZbf`ZLxf31+T+Bmb+p`U#{3mU{EEfv9Xzab!Nc)9qSv{G7%F1ML;pQ%tjj$kHKQGX(X0S zCzMKMP^k=Ne^)P5czcqg+QZC<+K?^B1ul9$!ZlD zJx-}QkIyIcxrJJ(UaQh<5X&5XiAQKU}b z_nWR43w^^=Z}^+l-xU~&R_&OJZaW!##pUvme9m(Be}>KAGr4*l&n1?XXmvV0CO=V9 z)?O&OEd3iPs?BU^^_(8g!-34%cN**_+PQ+r+BLkouIrg(=WqHqYSb5jwWIXzygT<} zo8Hp#dws2@zn93n?mLoiUgyCw*!y$dZtv5f@8|e;*%awBkOp*+!YKBW=&81 zsean9g*|{_7j5x%+jo>NOw_ne9UEYF)oX~`mdq!H;rHFV#^9Kh*@t70P04uUxK-5` z)fpx)R#tItRMy^@we6c>c^-1~WI3E!e`sb$zSD2-Rk!tex zit1+*hS$DdG>*frYx;i)uI*T!1+C~gPO*(?9A0v{XS|;kpJ!P$RhRPHO;^q6f832% zt8l#*<+5n^zU@yWmHkP*>Nr*ZN9dKa-OBd7*KKrG_I3~6@?D=;yC28-{Dz~6#Ti<% zDwPlCrbyZUK_BSaAL@HuqANZkcmJr1(cE?X-tE2DFpGHcg{k)I+rH7M^jjVE;#fUI z-S&R|4nBro`r%kq;*Pa1j48S-(Y+QhVTvrtGF8Zp(H1VhfTw` z=q$$|#3pqsS;s-w*4y1|Fow|uJHrFeIRDy~zeAh`S z8!Dw6@|6yDPfM6BBp3vef0t5~vcpJs0->x(m6DPXNr-w%UOb$P@x5imi1jY!Q|Exr z`R|&SrpXi8FnZ(-Xv--Axt{5CByWY`%GftH);y7vQq8% zQkqf4-bB-95oXJjmCo)$O?NX9qpU1}gq5ez^bJGTJg1s5_Dd$Ze-STSM5|^pb`4JG z#XM#V^KO&sPtmr~Kj-X;f)q8xQw0j{8wAUK(x!t>xbqiWeHdnssgyVPb1>h<6PD1u z+|LS)GG?2_iImC`#;G$a=>+(84c3lM`4LXx1uB-b#*)=a%T1)EB93)ltw$+^MBS9n zhEm3Z)A%n?Tg^~Ke>BpFQ<|MSUM%^djjoGVikn(ng^6*sntiw_?^b2x{-~6O#W@sd zS}ApFrL}sAF8ak|YGVPSEM%9^7=K@A?SQV7`aRcnhgam)e4_Q{zuGzPV5(dDu=T~= zLYb>ctYrk4m14?L2^VDKy=AI)U2I#LS3PR&x4E`{;aEz^e^hHlZ;n>-0Mv?mRAlW( zx3%hiQp;Umq}{5K^(vKA`)hhG9a6Hg!co=>9ek{X#gb$pvN;7h9C z@FC1mR?aWr$0)%aaUmivcI`jR3qY%dNu+mz%H8JxN|Z(gmsS?n+G335umjA!_d54j zHnoItEC--VK2|~4Opak#8-?=D8M;_*{amZDVWxt5rniq3 zjH{5e^*GB8S1w(>MO||CO3IO5;Ave>p>tLH(77W!X8EjmbuH=C)XzZX9Lb$D-n6#a z<3Cv}p|5R(e6{%Uh2!h5qBLHR&u)g$>Gore$vaE16WW{Nrje9(A(qv2^F3F zzr{FHe{|t&#kg-aqnW^Rl@taJv!*cuR+=Lff(vu&rywjWgF9FLPJK3CG1cCGMD zXJB*h55{{s^Hx3$qb6M+%3E6aYW(lLF`o1^+j@%d{s*UcKMLabn-gj`6|Q(L0@r)P z9cl@+o_SVjz&gu)Z!D9DHM)!78taekUOmeN@JeJ6L{Y|pjxwu9xoN}XZK_rmsv z7us4&M|FNzPBY!5cl!@+SUyRoHfwk3j#c z^6+QiXupPF`)|#r{nNSg&j;y!@g4ab@4;iv#SDF4C;9$U-hNKc<-Z;9^nP#Sb+3D` z3Qy@zKbPzBPuJ=`&$abPUNnD z@o*kePwsXvcJ8kk0**}jO!DXO2se;Cmd{}AE4Ke@*dmZJD=)h0kP^3V!syUA{xB~1 zP*C77Ht0|q^pFbzPKwG;K?Z6m2C!8Dk0$>Rg#b`)y$|~6;)?`h^8j!L|3z-}fA9qH zu#F1M+WYS10xv%O?r8XrX9(~y0grD7D+KwF0OJjp2arV#a4IOTs|B!%{P4p15X_y8 zj@QpF|FB}?Z}SGv=(Mnw_0T5=P{{|7j|b4j0MP>uu@e#u2@)d;{zBghPQV}l2lxO3 z1OP$d5QrQO3k8M2ps<)sJ_QE=f5qZanAC1L9e>B*k%;^QymP_G~X>=Yb1CY()v*<*|cR`3zXwaFArfDapNTd|0ESeugluRJg*tJf16{S{d z6bRg=bzY{`E7R$vR>4AxSz|Q#b%tAVn%n5{N*%%_Y`M;CR*VJS*IvEQe=pREbk4a* zyJB(|N-jegfw<*wxNCk}Gj5~et~n{)+Q*pEXfk>&Mk^nxqgAvxJWY<%Th40qdThqy zFP^jSFjt+_+Uu^TZD;w}Rr8O7)bFc1x~C3RjoNdn`z}WQlZV&yB^4QEtF76t@^SSV z9Ya09jmqEdSIZaMw!6vge}1WV=ZTrTJuV~wDSZ0+^jak$g&y zMeZy&7|2l6YSu#xGXo$Zw{y&oHX%_Y?-x3Xl)W`L@+4U(Gg6cXL`c)c z1r|a`jGr)3%S96((Q;)iNHTIn`y|M!{GCkG^1`bb%JW3cP15j$w*6BQjbQdnQw?J) zQd3n~L&Pw&+g#T5f7IhsNmKn>T358>^oYaOZ2e%+6E%Gdz*H@M-c(Q}7WUOtHMteq z*4-&hQ?s>QEL9eCi%(F@U0qRLXw%1N!6Lzac<1{+ z5{&43{cEG?e^xG$tKGV8->2erp0$d->f_@1t?!Q^LG&B~3+c)9$Mr?^yQ< zCRDwY^!N7HuPM3lbte?E=uIA7!AL#&cVu{e1g+Rn{iPqqcN@2t-Dn&&(Q*1+S5Mt` zefEdj@P2op;`f}!`Rw^SmRBaxo*$X)cYj^i+I<^Wf5pb@b`R^Gbrz3Zvpk2o?^+AK zd+)LJzoaWq(VFI7q(|0dr@WJP{@Ly|naLnchz(@lI zVC%JmaH;6QW(MTnJS%-F)&s*xNd}NqI7W}o6~Kql1XKcSf)FtE7AP>p-GkJEkm>3` zQ|k|*e;hr95Dpi=6;AWrTjM3LQBOmtg%;xkMtD%(DZ3b%2_j5kg7Ib$!sEdS$n0K( zQF19ogbw+mL_CJCDhosya{J9pFo03oGQjBg^`FBbkWm@d!&prUU`&#Gk^TwB_lXmu zY7j$cB^=;m@_q3ZDn@xZC}QM@it*A=ex#?1PwZ{$j@oRTiYovUQOXMz}bSAsFnDiqXbR#aV?ECVUN+k&3+}aE?bX`WVEHD4eqX<=qmrTMn92M5)mDrM(3}(sGf+*-Ii|TyUUMR&r08 z5ep{`BAs;37Sn2RK_9&)rmxa#zzTsOrmW+K^!8>&+6_@>l_#6i&TUC)|0XHqSEaO) ze@uC8O=uzbsk0_R)mdv#sSI0cv|gdfe+hFXDO}5_HCC`y`cqXaRG+IdwszIoM?Gf+ zQ=XJ2pU*mXU+N`Lue9cxRT)n(rDUC*_8OK}n&nVy4N99cZoWnPDM~1O_OdftQBS#j zW?5Aqtk!y~I{P~Q!efWpl2!7KK<@%U-8-u9;Pe+1V;#FY3Kqe};A0 zo?J@VVy$JSvUaw@R{C#fZhgFsRjzc$do@xmm0qzoLBd<;*Bxst_ohrb>Q9S7Kx#d1 zyz)-OUHb)IY<hdXH~y6@HuZQovnkIcDm-%DVQ(_gtxiQ!d@=x^||RT`ONu zszc>dA zTkI7$E#3XK)jr!^yhDX>_6fFmNW|4PgW6mPVaVmp#kIo+TO~_JPqiC1 z2X#CSVe>_3bsBRlrj4zfxUI_kNXYqK+TP3QZRkE3%2#I}SUhQ*X2y6pe;8{2Wg2f^ zDShd_cH=hXI`J@b<>a)v4;^D|2aIwiJjNC`M&r{#pedF((;4dq+H5hRbA5lu+B+jy zoc)O|?o7a0LmODU=d1IsR>t`URBJ77p>yqP&-LF0>#X;!G}Z;i8Pi2&E0Lw|q-?{t z#=2}xL4NPG+SGct8ta;4f2*!85zg2fQ|()SqA%t@wi&w$VvV`7H16b`SHELhUAtj3 z#hBCEM--r)Q*btZqS;&9e`x33fVX@f+!~igZGEStcK-p~Io~I6bPcfao!;A7FE?Vl z-?#VeBjDTDO=->RllX=a-21cUK|X+e#kRd z?=Ks%8;;lS_hCi(yPJ{q|Dn)&Z;hrsBf5NEN#VawM&KUHf9v!=-MIfI%;#FO^Zov{ z@IBY_@n37)I+Kd8^NKqo-HnYS&tGqtT^*{TpGE?!u1HM2r z-at$JI&P8>{kt0b!K;G6L@GVgzqtG!G!jWbIhELEIodx7}v@E_0hr<*gs>3BVgaO2Z=ffm7e?LQ!IE)%XH z#e8Q*e?%XyWB$4HcR5sM6g+lEJb6L1Xhno+L@PtGq4GNh44 zuL zE8M=>8%bn>N+hMif~-IkbwXSWj8t|>46jPd~g$aLmTOxH^*EsjL+NHpL{f4q0ie6`Lgh)z7~PbA^H1lB=(A<8WIz*PFq z3>Qws{mG0^%haR9Wam$0q|W?NL#*kHr|^i)pm3&O<*M!eNNH4!x1m`@S(&6K`Tr5R7X0K>f!k38N_Eay*Me+$ht z_fa(s&V35e%?Q54YEiq?(nR*pH3QOo9Z&@RzvQRUZ0o~(xl)Z5P*hC5r6EzhB+b1g zQQ(nJL^8qT_s~TDQZ+TwEgjN5D$<2FPRxk8l_F7H572>^vsqXugUptx%!^O~8u*5X z{LTpj)|M<5j!DG|LghYF z)Z(z!H_%NnQw2^Jp%xvXU^oz75Io6$WN~K%YJs(pft=1he*6n@HHF>thcF9zn%vD&#y?R%KsnyM4 zR^x$Jd{)y;|Ir18*W>co%}G<$Y*=*tO`Q2s#catrq{dav8(BSx+cc0vbynEyKUZ~}*G!VzOmE1{ z!rDAO)4iHmU71sCaN7NjP+M$O<$>Gdiram9+sv%nMDW{Hbyv;Bf80$^ShY{QT)Nez zwpY`C)1)}t6@J_e$kgqkOf?SBHLqG7mE1MU+(eJdC1%?_o7MGONqpd0^?X$YS6KC& zI8Dh>O~>23`c|bbT;xjAtr6RevC;ek-QCq&1UB6TVB00O+}+0AopIcRm|KEAz7@q* z9m_O~z+0u@U8RlPf2~tk{hm&(>Ct`GR%PYe)2vrT_+3M&!ws0kRSQ{7>q=e6Up4w) z^rKzH{N2^OTm8DZ?HAn5&C0d!*iF68Wy@DJzh4FT&yatI@cx#54IC3L)P!4%1w=Cl zw@jr-#mPos{LGl5)7X{+)~!w5C6-Uka>z*X8vX5Hg%(7me|cT50#k)c;hbpSZMa<& zxVtruVa+k$RUBLGb=eL8&GskVHTGhS<4PVIRCX(174$L%+~1Y%VpX-`C4k}P zN=kI${xMc15?M9{*#(*6wZdX`{bBW^TjW6~jyBfyU}5F;0 zo=;)zmtFmf;@&u8c1vN6!^+uVG#KbrfrP*s~rf}VF3Fde`Ox!9wpw!24g+#W3Faleowd6 zmR*ZVWUY2!j!$IPDN5|PT=j+J!EI!2TW8iL=X`SKK7(MEb!DwE(B5}eh2+uzbU=&0 zwpiq&6;)R=O7J=#Jg4zlfX-u8yCYDoXfoKjx>VAJ-V?gSi zz+c`}>OCpw&S+M4I@^69WTvTUc7|xw@98yyYE3iHo|o!2rqzzI-AxndhB8L(UFDXW zXttqh-h}BsqiX%lNG568PJnCO*l8}ST$x>#tYBeCzJF&U(3n;c%;^b^!9@n^fr_K;zANa@C99MoH4(L{<_%;2U3Y~CDCrkmwmDQZ@l?Z(t? zJox4=!)zYU)tqc?7EotqKW!cI>IMPjb&=_&;ZKg%wkWj-j>Uq3Y)T@c$0)&kyh45bmDs z=w4)PprrAa3unI@*w+l|hVv)8%M_`=4?D^npt51RFGb*g=ElmiAza~WJjfp|3nvuF zKCE%xwDI<|bA0M>&ouIN(eLKe?UwZLr8Qn^JZ*((Z*6~bp5XJ{0&V3TRxV?sHz4XJ zV}EQGkn?uDVm|=9mQd_1$Xk_>Y!2vd#zf;+J|NdWZSM7Sj=E=ChH+;@zDcMm$u7h(6ePI7%o_C)O8{*~*n+M7xaiA_R) z5CCA%e`Dt}>~O#G^wFqZ&S-6{a?gBL-gD3x|96)qRBqMDUn6n0H}?OSVmC+LCV!&w zuZ21Hm-(+j;jfb4Pfd2TuxnlZ-hv<$AB2 z_CA?;@2zb29eg@2{O73sPOJI-cyFJw!^g%^m$kY7$bEj2FQ1d!blv-wwR^wYO0U#r zf6sgO;e8KW{QuH@9}N%R;P^+?W>4GuN8)uvTD>pE?RJ*^tsGj>P3*#qT7Rh5+PJXn zn4E|f4%B5d6PVVU@ZRSKw)dWvd*>$USNL_m1ay4Dc}M#BmgI;41OWqoz~In06cP^z z0KlNIPy|9335des5NMQIFA9xEV^K)VdPN6{LF6(yM5aX`l}DwKX?Ox-Etf_nlG&Wz zbtsa}r*pZ?{x=nsPh*rSM1R74Nv2Y$@!AZ^mrSVBYSNmmUbR20(B$>G$eIe3hy+sS*=n#-U97Qz|n2>Og;YH7Qa|9wTw+V%Zi)h z@{v4^;_GpwWwP`P&VMO)&tt4n`5relqOIfg*gWJm=Mc@_Z28Q_;(z&}wqrGx9oF}4 z2AJt)yd3WHMSa$BsvN7tn}34jboTr_me*Cg>h-5w?)7ss;Hz^oQQcRQ!>hx0c$*H! z$}6>)!*n@4M{J2RB!6ilztX)AJHXSdRW7y@ z1YH==SBA z)Fr(KSaq|@bAP>#jB#Y!_hrLkL=k;4Gh4UIzi8kS#s_XCca`qBTz1=mXID5)jY8Db zejcLX6#Z#}!&qhYW7l=oD{EU+Md=t)84e)}qBAY@VO%87>SadJgG|ZhNY^p5FUy62d+jenGp)G>w;yVjDjD!)co~m1^$P z9(kkhj}ke!Y8Yn`ymMEdHwo`n|0blkyZyb)-x)^>sPkPmgIv#_K*SG~_h`nes930`l>;j#A8#U4WzcCbygHdr9=R%^y{Z#9-d7F4O=D;v4g$FF%T7F zku0#%7N>W}vX`t|R}I~+MF^J-og7qUFdi?dIC@qd#AbI#5a9!mF&Y5)f&mS~xrBb8C=+auHR=r<|*JHBF4F(|>vQcajSq%!;Z;w_g)kSC@_2W?2g5YT=pJZS%7E_Tv|<-0W?)%Fe=nyWsM5+v!%9YsTJVFZNlVE3L8Q zYkAVX9;=_K?cMg!%WjH4sO;dh{0k3{6W;6e{g|5Nns=$&_kJ~B4UXvjJbzAu;^sZD z6Eyk1a7xz#E-y>^jz15RX5v9m8`9H4>qG$mK@R%!2(&1RM)X6h8`k4OFMKM(Loh4q z>bR+E^7}<=oJ$6;&|{+;xlm*t>_?HTgt)_QY#kQ9l0u0Iz7iZiB}wuMqaw%u2Oqys%tF@w)3Hw<+p4k{4?^+!##+xG3$6B?OFMAO9f zlsvH%Z93JJ6;~CvwL>jRK()lpFxC)-QBt{eRKH;}%-w-1u+}|8OMgg}l(SyavsG@) zKor%PI8ZPAX=NkzvvX2g)V$?qLlTu|WuZ4T^1s|SJnd~-^)+8zQcp@LyG&8bOR)|Lls$29&0d||hJ8;jx3CJw;gw{{tH;c~s% zkK|VU;fY*T)D(t6$nhMO$$w|<*2UJ(W_(X6wo-K- zb<1LQ?>)yRToOAz)wuNxfvDF@W~WJ<8Z+53>o^77^qih=%=g*vf!{!#R6o1;JY|*0 zcvm;0Tvb^h)LK)?p8#0YCM2^dED*fX8Y3oVWP`-gFRaa6RuUC-UbX^b$$W zmG(T>H3p2F4Svui^g;&;@Xov!gb)?_!lp$2;K}NOkeLC%7tE+3WHfIKsuDNobqU@J z-FF6jjl?pl>(ogcb&(Odkdn@3N^9VUFRfTZmZag^?0+amu}$SSXkttrLVhPvJwiq@ zT?`JaTwZYk3_zyq7u_nfNbq(P!)C0}n}jnmae6mL6!yPjw0wlBUFp84)Yam=6mU_9 zqQ*kVnxjE>knUnOH<;kV7l@=r3jlWRc(?b_pum#5M@HM zm6Bzy$bU8%OJs2tk`H~pM@FkAm^KV&ze(x^v5YXJCU1~Ym+sM zO{29q;Y6Ny?7~vd=g&C{?1g-E3IIBHyCUaI=zo#y`bd|`83EvA;h^);(@a?W_2xw! zlkJHwPswXSAaxE(kt~fT&;&zLY(^&0Ad}L>WI~vuksrhSe>NI6_2yeAqtO1J#tKx? z9UTU8aN(%8sqs;2BOPQFI*n9nhdF9hDRNM`?ntLQRpui|tBe|bEGowVsH&8yk>*R# z)_*d%Dn(US)wSYEiq|q{?I@^KW`DD~&hlt|U$2G|!B?ndNh76yg|o6#RY^x(6A!x z&2g_3p3BXFHl8GUW~N%yFw@BT)pwM zrI#7Q4%ke!VeD?7zLP&hX-wgwaO5=7R#!~uTTQ1l9cr+%t{CZJ)uuBx2h(yRQfr)l zVYO~z#yXY@W^GZdEC!Ik`ZrH%9PNs>b;#Jd)k0}}VY4z0V`VU3VC345vw!lI1lXD% zV>3+oA+}Tf+!@x_>+KD@ZZ6ZD+gDrdeF+xucF5k-e{RW*;kxlA;@rE-Y-6gGx$?%* z-uxSdqHWV+_Wsh`n^Kr>UD?BM#Jb}f3ubTKUvxLt*_M>B%;)>vE^4uEEzax#Xerd~ zieD&LEoAs!9o@(E)Mm*}&wr5KJ>$SLl(wQAMqJoFd9!#d$L5*4f^=RtvRg@y)iu|p z;0|%4^!`Zc^24g;i_zEm_YCH-he$}irN24480)+{rl>td&#y0M=Y7kocV4lZde=Sa zPCKyC-X+p>itXrpE4NN=>1X-^@9lbmUFDw>sQQnKzI`9NXC1@dH-FD_@?G}(W`8-W zy95*Ho)@EddJER?-=^`DQy%yYwy*tHllDFO;P#%ys5Ra#@}7S0`X719eXkVoo^ zyti6Co>a{Idy!TzlZ?K9!0c=mn-VgDIfufDOdHm7lz)xyJ>QnMAAicnUnAc$Pr~p1 zqfXsVqpH7u;+b8vsek@Y=i}2$wD3=TY`ZOt{x3X*@Bk+-?0b*^`GV&FuR#9qodHjL zxl79S%^b+^27*t*27R-&-(vP-ZPLK_pk*7@J#&(Ap-CoyHHgC zEujIhSpyC(D{ef#uw1GzcHhuE^iC%OaAGvhZs_ns2uXDWkAD*fuojQ-IR3C|`4ANW z2ek-rQ3h*I3GbxeaGLzj@M!L){BWY%(7y&xI|+}Q2jm+B5BCH~9|=!fJn&-*f|Ux; z!0$$(3UDM?uym(D(082N4{Ehvf|jj6X=vFmx zgr^c03{lStvNr1Fha|9h8In@_(q8{E0RWO?2hwL95o;pN^&e79uMxKJ5qku3ZZ}c| z7;;#DQGeSa4Y?u(IzMqdC87i%j!bsrYFSdBBN9$_!gnh$i6hccE0SdqYlA6_bUe*Z zEQ@0jGQ8;xD%8=hEDaYaPRSb4{NmA788T}b3~u)$8u{x;Xb)7@(2~^gF%D6mCymtT zQqY#|tYuP;`EKP7kIMoP84wcl9J4O=jd3oLg?}#a`765$fFO(v4FFj0Qi zE5R?*7cz4VG_7?pu4gn6_cAjJ2o8-c(@_jCQ#R2G{Vl;Rvw;;;5f*G=FN3E0^8TUI z0^XAbz*5%}GfOz~*ygidqq5BLQ;Q>$aXNBwGjM|jlVt+Z*AV)3Rd|w=vVYwvyF9G3N`Dg9UTT1+&iHb2%gv**SA< zJ?*tVGY>v<`aP5uBhsdA4qQFNPc}1s5%JS8?$mV%!!^^HFf#oAlpzd@UmH|aKy&#K zO3>`H{)I3PMKj$;Gp!#~fh0~|zLavk5r26|v%*D`O&%}&*0g6jv{^n--8wTU*c4?& zau-IkLp>|t^l@D%b3a3F%}2DC4)f_D6oV+V!AndFH1sV)ZSdSwktmcSJRmCNIB*nfPU zTQitV;`3>|ia!I8&gc{x6$Vj0l}sqGNOb0pL#I>d6v{;Ql~1WuAk})rzO^`-(_$6a z?2ZLSt=Z<2D+QvtRD#lH_Q=($<#m(Wt#xYs+WBdYTrV}t-TGZ&!QA7Qc}ylp2g1%T z6ihur#Vop9va`Ge_c>0@)N(nT?thONldN20Onu-eHrk_}bTYVkXGUWZY9 z*KVj;`)2=vxRUU=I||P#W4Yz*v76p}Cm&PA)OnqbhFhIy;%xd{Ul${-X7Bj39hi@Y zwcX=Rvz~tER|)XHM(o~-_iurl@;xu&u#Bv(qBQ+6$kG0DA0{%^`6X(qihs{NjvLgh z!j7BhtUT;$OzNsod=A}1uItGVw6F`y2*VA$IOa9W6aLn}2!u3+zYroR8$?fQj?+Yu z^iv)@4!nl>q4ERl*uBrxVHc;VoOct%?IdKhLy=^tmBr8`JorQqw2cyNES(g>QPlA% z&~NPN4a_ra(AUlGnqLpk^rZ;jP%!+@P|(9;zW0@qG$82-*we4$*^b_|zgJrioTELf5~ksj4Gyni7t)D-1~QP=Y| ziA`MX<*{YlHH6~mTDKZIX}Pt;%WYg%H9vAGbv%uDT9*ZnKU^}cnOE7@wV`?2HQoNb zU6RfFEz=YVD}7K{CB*<>N#)InO*gxDRozh@DM#SdE=c&|wXO#}N;c*-kXbhU_nKrj zEoDaFGrf^bV|R8AvVYm{u5q8Y(PURdq>FlcYrie2%zQzuDsHf8X|}MP=~-RhAM5!( zGm%etTEm@UdlqE(uR1+jlu}hSds0$Z{$+<>IgIn3<=ZVgmfG8{i>}%-y-#!QYChWW z?f2$wwOCd3OR!b@ts#WMc~!TZaqO1E!bDeY;g4=yb)~%Xn1Ak|+2A|v>A&q9`?h>J-g=hecur8cRuG^%}w6#dF|%@m*K%lJm2EGay~~B#wyrHyVRRyF7;otrfrX* z!L%2!=-$(_fPat;B08oNk{mPUdJeVvK6nJ+ldJ}UuZ7<{=qT_XsyuWqHTS^gcGsa( zl6vrX=)A}Lu;7Dhg$~i`Ll>q!&N+ip`=qHg(RIIrl!9il5j38y*tKZlYa|hV+D#3E!V;G_X*vD`HK+c zl*2XR`k@pUdr|5-sn&-XWBhxD#~tjf1MvVEn~I8S#d^eb-v3qvepL~zCBx?z3go0) zfvjmsyeTm!>7czxnj0SHKS{j>3@gUq%Adv7#vd9b;!n(p*qk89iURI6)IWJF^H)J%1yFe42CWe8*>#NT59MXOseIP&u(R zX;luOul_+tnR`Pe)e(NvhMUVOCST~SKBiO7X35zcj$*w8bu@y2$;uT;X+=e+kv0OI zcC|-YVQ4`}F`OVewNjJPKqM_K=ff$*O(rW1aWbGwP8Zp)}T*+ew8t?DaF5Y+l*EyJ1ypTvW1@E+koU ziDqN{u4okI#a4>(XlmO6xHbNWM4NdzZGSWkop&~>SDKka>}|NMHr|3;x{jf3&8mfV z{ps39eQNEsld`uO%UugGZDzg9p3+kER{LvX67_z(HAd81+tVK|%^0D#dilh=!FZ}& z_;ap)T0;s|fA3_;z0t<_VH!7f*ah{!*B=TR3-u|lwnst z7krpy-M_5Y3o>hEcE|V^zt;I$IV}olgd&c8_INyGd$&DP8XV1)y24RBSxsPV$Dy)OdQ9 zRh$;JwJjdkH@{Tl2JJbt14ek&8yZ=u_pLQnaMt=SXz6@yuO`;M&pIoJb+kHE5re5{NyU`l$Qz^9U9_QK3vgI%BwX1gIn%mhDSLkCkal4Nhn`Gs??`rs+EcJXbAax#rV+j}1dY^(Rg6JFz^RsIBU z4dqEUR~f!q|Bf@wE5o`8_kZLoXwUBovA@nC-QR~OzhLB9m7-{>^^z=l<6FB?a|{vD>o)P;&zc_Z{{`Q< z9nzTb3&nJ-d7C=-YumehXj?l(OcC)c&Hbai_fILqI9;Eq)E~fbUVp>Sa2ImsJL9}a zxA)%acYXBvtH;P)fW>-Fx^K)U!`eS{q5lr_Vm`x}bTdjFO6KK2`w?>2&YFP`=n zx6s%PpXob(A@=?U=Gy+~%X_xg^4L4QaGz)K^VII-J2Of9uL-|+z8~{Hzu_*=!!&+3 z_uMbL6 zQ|_WOlsO~BnyYL;qq?bU;5~-{BD)VW z<~wT*Ju{#?yZk*9+d$LaG~?I6OXM|U^uOcUyAxwIlgE>@-G8#W3%&bhxw8j6E5^Uv zE5dpis}uMV5-xxfT8Sa48l%CE*rvL90Wo@Exm+hNyBNHTJHR_Cumm6z+snK|i@t;D zzH>IaOB=lVWkWNjLaIx^EFrf{6vNv#y<9s&p?N+d@j*c3xKivmY%VAJEWc~#L?bUf z^a4b4>cFePxqswU#GF;9{8%uf**@Go!PE;qv;o1%t;Eu3xqLdr6i}^;`aWbm5i>kQ z1V+VRv%u3Oxf_nB6Ic}lWkh^cMbu$3G8;p*W5NmXM9ONrEKEi$UKpHDA*5-Uu*St4 zYrM-XMhsL(EEXn&dPGaq6`;b$OmHE5AVP#&Lga2m%zsQp+-Ak|>_EJDMr2t>oN`D! zN+!%{MoDfztc1lg@H}iXN3@5nYOO|FkT4s!M!5FK3?4gVZ^XQl#1wJG1XP_|bH4O_ zK|C`<#B2%_g~(i#IZQ3TgjYqm;mM4ZMWggb{7y+EhDbDm3fkW(bYMk1g-A-ksB5rB zIWx$STYpHTr%1Ea#5_e4)M-kTYDk+oMqF-2(3On8DxCR187Kqx5^w}%e0U`9JaoEImZm9INU+Yyn0HE zMaDEAFCukHv-U)sy&AMaznlg^ii`R-QN(9`*1e!<~p~<|8%klZVG@eWpJj@}i%rt$jL|V*T-Ax36NlRl*ba6)fu1V9K zJ_D?yAf<|;FOXTPBRj4<)H*jZK}(F^O?pd2Wa>+Zwn%J-#l&?3p9qdekE ztm%uazR*0;&cw@6EIv&n8${wmA%MM2T$v9Q3Q-jMQ0)29Jr_+J;Y}3`PQ0~Dtj5p$ z(@{keG+c{O=%>lW2GOk{OobmdEfPzFhkr2DBR}lqP~{oW@d;4`s8XdB3zaa@%>7Tr z6wWN%&BZFw-3w8a-IYx-(majH^&!yRDbpWGF0-eQPl%bbs|&2_s?w?(|>W? zRaFs-?I%!u0n*)1%AG9KZ7x+S2vhwi%EZ6ZH91QqKvGn#N#OfceOy)qkTk%)Qga>D zonKPLJ5g0kJFQJ9?GV$QE7l!R*3%AD1z1sBXV$~RAMF^^g+^6%PS*WdP5oKS)o8l) z1k>$7RnRo!a9?RU|z z(bk;>#I*lX)icbL)ERp)484wz%cwO3G@fFv&n&mMH42tVrqj)H%$<6+og2zEb=T#I zR7F0HEs9l*iq?IY%(Zk`y0sJSCO3_2+6{Wy<)c?AZC1Ua)!n35VDwg%Mt@pmr`kMu zQ&h6Kb*tK)FWLpCSj9tH@Y~tVhS{Zr)xA4e{j=Ig{aPG;+hs^w6}S|=R#~00TSc>4 zY=YO7u2ki@SB-?)#BAEtt4G~SN>zYa-EG({zuDEBTV1c)Ex6o;xsBDg5RIzbt(n}= z^xLhUT>Uv))w|bh!`tP*Tz{p!PR#*8<;+^$(@|yAO_i`$)yB}BmeZ}h-HqH_-O^m) zLR`GW++E*Y1>u}6&sv36+*Q2X&E;8@*<6+0-BrMnMVMH7_+Oy2^ro!_6qh0T20a0q$1#s3}DQ2V7?7FMhTY|3fSSbVF<}! z{tl786Dw8{4Ner{J{Mtl!C`I~9DN3!mJ(sD-`&0AVCA{t#m-?`5Mh=X;vN~{_985v zArlrG*YREA1_@#1xPM|b%Hnns;)WyQy`bWjEFShIVYVGhmMdHZ{8e5s+BPj)hALBg zmSWyEV$Lq(Y2V{HnPOff)FooiOpD2JDBx?WBIxQvXn)T=kGRaLxx{u|UN~e{Z zz7%C1US_UqV^(Y923Rg8U1rIW4-hK(Db!7&7C^mrS>TTz4f9NKJWu}4UPKC5CgX6UTm{w=Hil^A< ze~1WxIiDQy?WR;^d8 zR%=tLjc&a?uGj2S8w?`LW3)`{_M2t4-8{A1?pHgd*6nw=-Rt+8)n3nhzTW6K93~dE zfWu;OSiEJ+5sju}@)S${4<}y9W%F3v&Tl=R&}e0l%@sR4rI+Ppx{8*cS+B9{wi`XB zmN_PiNq?gny&N`0yM;++JPqsq1q9t$qtNI3`bi^`?72~Rq$WKd>1}sAeXn=D-_B*c z9zQ3RbJ?nTHhsRYo89mDRlQ#yr`H$rr~Wmb$`{-G|G)`L^#H(-D=_gca2rbgK+tR) z_d!s6B?+>SLmdl0&}1nM!;J(Z4#W`)qYNu;+kZ#^fKFmo?jfk#s@$b5yfon;$1(8a zMXsZt6F;e3Xx>LIN<$ySQY;T3NYYG|C8upuNhV4Xj6D)cQmm~bNzv@DEz66fKQ7D@ z3sEf0Q!LFR$@4tbHM|of**DG#EWbF;ldAHBG=%)j3bXv&BJC)Rih-OjOggQB>8HY@t=v(`8{; z)|IR$S=Q4Paa`Ax>*-zB$*msfMhXmHKiHI%haoj+vHo+PCOSBXGSFp<70{FtUqHIj zBU?{Au+s@(QuhUebKMZ#$#KQf((QG~w||AxdESiO4et{VYR*mh2og1r0yZk4U=zKn9^rqr4&L5Ygq{VWryUHav6_U&VDTW z;(2~$NYWXeZJVZ;j&V(3ncjV$w0Cxan`gQnjhE+IJl~?}T5bWP>C--$sLk{4Xn*KC z8AXw+S=NQ6t7|!{&#l`j_Dia17=F>UzB-=Sx2+p?;kmziR_VL8JGSw?%zM`PzpA^J z0l~367YW0z8}|{#u)H$~ut+&IkFi@>4=1chHk@sd@m!BBmu0re8ZylNAQ8i1{ z%{*saR&@E7S)pf*|6AAh6(3`yc7NTaGuih(4N2YN-UUtLcwSC<&gdRThu-;KimR?{ z_QU&Y<8&N%$=UXer_p+tN0I5kVd$g*5(l)%lv2oJM>u~%0&@7+%u9f9J~2T!kZdD7WkL~>H9)A+ ze&b|qKC!+zJgDe`liuIaG9QLSgvJ!fR!iyRm(YB4<(AV zmog$-A8Bg;9rU-C67_n^xW_OMq`!*O3MI^CT{Dc-&@)8g_LMoOOn;`kCUa^kHZkd{ zL*(l%~yINkZ8hGasEJpwzl#(+SZg z<_fB*$u6Zz>AO_VU09)2YI~__il_(x1OWnnfFPg z2?mTuW6}8phD9TiNo3M^>}oj&jKJk`iG;>QDuT-Fp88wJQ@WtkaUo$$w`<}&tKY#SYw z!BwHRiF98VOMlhiH98EX(@}}qZFbhpZ9{Xnly3Kv9rpey!Qn!u%jKnb9K@tkVA+FHUl@UmiT$ry(NyMKf z%1{KO21v+6s^d!XoVhN+(%R)L%hMa5F+FnJ%^xsxB-u8s^GnGvP1Bs0I58|-(>qR+ z-1j{sbAR;tK&$iu|2$4m%-2CtR9a6$K~y~$r9~)vQ6M3VoNG(8E3GW%9Oukq9LMO5 zCeyLf)jG>Gab+V^NfdkIR?!jT#YiZWb!9=;(q(X+S8)x0T-I@&eOTB~C5=YdD{Yr! zyS3`$XgGFi`Dsy4rL8W+RmHPsDVFNda3S{-;eS`#$~CcST-cCi^BhfeA%YWGy({+B}6Fu!I-wMVIx7#)(WYE(OEymNR=ruo3;>cV|zej5< zZ&b7hg*$-OlVa54;dnYzhUJswCzHCkCBK(I*;;>_J^4+8oMr7c6Q9`mjv-;IdG?Q; zFn_v5;d5nLR)nhQnvRzKU{~gYTk02{w`%LI)_JVo*_Nq0>-#L5qGr1ieW|axqtmx- zdUkuW?H5MvyU06JS-i#DWTmE9kfm8tq%^KZr(-(3Mu71cY6zkfw~^_Evrf$^OOXLn6~is9Z*yRU19b=(Ji zt#~~4I{x_bM-?L3K8%j%yr(Pk@ag#W2GY{1btwS_2XD!n$Ci?02nN1fB&U z3|>Szh7d*(H;8KeA8bK~@eN=+7-z$^S=)#-U-vW`R!5Ix%>xZywZc zkWuKxLg>DRBns1sQHDcDwV0)(TL_R6MeNChHxMGi=3;8GQ8+~{7LId*`-1DU}+a3#*K)UFglZe>9S`?T;RMLp^E}sN6oCeIZO|#JCTG;ehpr-5PFe{hx&1&R z{Tf0OHg!+>CVpt-RityKZhz7R|2Aj6Cz>=?kjzQZ1!)x?W)q3oQ)pz6>6)39hYc1F z;r>6A$uv~v3J}7`JmP6(Kt{zS#avN6mQ}jQZRk@ou5(S@yb8HHYuzTFb; zOJ^)qvxL^w-`eX}6z$zDuI*OHTH9qn?fhqVBpnmSWr&IDJ1tYnfPa;nkx*(J{70n% zrl|I@WNs-rl*z`gEq}^uXKg(*E!TaiSF5{it`&Kdu#RQN8`e>;M2>CRj5B$9`xTV8dPw_D1TSt*V%dv zc<@c0xGbLz;Y=o5@lFc3b&9j%OGdh|gt1B3R}$XJ-Hepwet(4cypv+t4&yNex@MDJ z_hbX-j};O)xYxH0lk9|qpY7tr_~xC+^W&80W(3O=Qz&2rQG{gP8q4JqE@J{6k#asE z$~mffUpI|2)J|!7c&xT(Ywetx21>t@uQ$>BwSaRb8OzwKL*oqfmtW3jD0(c_=FCN# zaUNH|c_%vPQh#AYG3%65FY4wwS?rb}QIj15gT7VL{BgX!kV&Nx|k;3*3^r8F~E z+YFp(B6)P^ZqSsPB#~>4twD39*xjg;bMr$FABlbjJQYrcxFS;O2vt#upnOuF|8mPO+f^VwmOtkvbpSVy8o{>o!I zMi3$jfT-ZA0b*F<>P=A*vL}C6wK!7>l40rqd0R{koAs`53 zHXRIuLI5zhG(IgCj6&e?Xq+Y^5R1m7FsM8hA03UwV-mRJk{ut1%A``sq}D+Ug~DUe zxwOi0E}BdvbGcO7jYo{hBlD?DS}Q1+Q{r^lgRd71{iHO;D=C zYkySA6_%+qwpwV_%O%dSN4ijBl{;-ZaeT2>ZueXzs|Qt?%I-CK^p>B7mf0^jT&21T z60hR!)BLPoC!L38u360l*FQ+p%WL{O9;+QEzH2O$$$pPhrP}8-T5KND^^Cjf_Zi&Q z?;&W4)U|ca1`A0T+tM^#t$wzx07hzpn${*g&i+e2_bE zj2iPNFq(Sa!0oHW0xj_SuL-;9AxFB_=t!!Z=TF$^;nz<(?3 zM;=0>b+jUvBrq^jOY5md1ssc{s!DKL;q>jknh^n(OTZE~Xq zO3MS|OldT_Gcs8;EH8v{3&rnLg&FD#cX$oYMp@r@HbRq+H3VOc7qbyrO9)WF+W)Wxwv zSr0{a&{OrywOZNh0|7}{lVweJ)pu?CS2?xSRYBH}9jR|Vcgk&E!n4j5JbzYqGK}X` zuUs!*SS&IAfgp;rXKhLG+*fW@IQ1DOI=F5Bd{K0*zkxc`&1oq__!~CxJ-M{wmO=Ps z_{vfjzIBJS8Abg*!nM7(nc|ujO^Zp&y?bz4*-aMn;5OtXA>DU$(?C>FG;Nb&w7lPw z=UWD!IpXy0vxr-Iu0^*=`+pW8SnPO?wHe=f{fDV*w6gK5$~bEoisekU0hZ30d?B9j zJXIyNDK$hToZea9`$E8@1!QVt7Sy!*kt=`+uc&dxo#JPc*8rI_q`vr#r%Di{POf3R&Vr4Iy3OsmGj(rDTVSf7wFU-`S(#w z*_pluu$&M1-*;zLYfy91fQ!GA`M6Cua765r%Gc&|~6K^JulVN#%m%PDj>*go*!TxEN)8ZJ86s>vA%-it3l zsl6wm%00P9vVUP;gH&Nr1{p!OjBMg` zZ%Fcd%0{(W1zkh`bx;;3!>BC+V}vquQOO&_S7#xmJcE-?`V+haVDO~$A%m^*Q68ur z2&2=_RFU=*$cIj&;!AvxiFRB%mERyy8-OQJ-W8R0HbCSIEK$*dpu~lahKI zL)PUKAZy=8vwunlL^ew@Ba2Bju#DxzSrarRLAqyA%y(Sn-0GRw<_w7o93m z>e@=u>1yTR6;5)r)`wGdjZ>o}_LA``ILs)oGYj=XZgE-vzZn~5-g z3PWvRvuCgOMg!DYiAe~}gt=87u(OL3Q)kkYrqReaxDxb)&`ilGF(%&_=CI=zmNcy^RXpz)TNsueF}PG(zRW>0fc5 zya1zi7Kq%-K*Vl-54tt-rckQahGjLkVWn)bg5?_S4s8 z-DZ-_t&K?5`Qqypg)J^6!B(QdRV({8ul$#__-12M8<7~S1sYlxu3w-lwTm!)KX*8H z0Ds*%6^*8R>BUk0Tj9(rmvH@aSrf*+*;@N3vP~(w7_SFs9B*V$W#7kF^6^_*y_~A< zT+cPd^;z1Lnw}(YwX=PW+xrKhaTXEJmE~n!T%~YgoU+9+F6vq+TamDyFSL`M(dlbb z7OG3QP|__hY3GZh==)yAxgQhZEftyZ#DB5Q602ZA>TQj0X=;iZImTkyBTDZZW}XNi zSl}l`uQG1ItNE0~=S;JeU%gY=cLM@Sd4q?sC1t;R8t6xy?1~9|jG~pQY3-c>tK+VQ zwmPD)Z+*3bVm`3aJ4J+RH^Z%EcEHygJ2-DG2MePnrJwKVP81B7|*%n}!?teUy ziLJI%bh;-jZQZHEvZZds`)2cPyHRKPMiAUH?~i0X%bf3oZo3xB&0eTy!~&27l#S*FEaak&^GF@y{x+q3W(9XA&-?*|k@mY%Hg) z@fy?SoF^1?XG_nw2Q1_rmX2GC&*iuA((0PSk|&O_+VjSm$5R&E-agAlW2a={{1>yx zeT&z2uNzod`=~w-S={nhR^N`tyiJV{#QIl?@EZT+<%|I!;l|?bS-%5wi+}g;x!hL0 zRtMK^?#-uqCx-1`d#QAOmFZf~o$uG+dYkWzyIb+{9?yzlub9`1 zhT_eL;}5vwO^)M_3cIab_w2OzF6zjw;4{wht*`pjYgGBK%IYryQqRAgr4 z=g;EuqZI8BjyRBpE({pgFJxhGp8(Hx3u!0@{05U&ieI@axJ3uDm>>|GO$ zHr?)CM-VFWaUl&+DGiLx6%6{wD|-$_XA|)5?X1H5aN7@0*zwUcd`~qH5eoV7BK~d| z(Q2@gQDqkcvkQ?JsDBJG5$e$ytMwQ$D16T1SCKOsjwgeHvRC=C8^sVu(1`9JfW(GB@sy=#D5p)5ZO=U z&|APdsRkA{E{wH?u8+byxkLs34% z@S_*9(HRgCDv?np?%{tUQHLjT`wP+Lj8c^R(8(dN0EAFkACaQ?(dtL6zYY<1AhFQn zlG7U#4Iyvw7IEJzP!9xhyCQ2Ax3BXLBl6ON#Vc|T6LU2&?iV3VRy45XHPQz%^Gzvk zfhE$pCC`g2!~rVuWjIFB8&c@aQ)MM(s}9g5CUH*_688Kt_Y8jn*9+5%L9>xCvwJd$ zfaLIH9gmz`vv)Yt2?Of@`a`bnv0pip-8RnY57S`|lKmelFE_J^D3a?I)2%f`&nVEx zHH)7wEaQ#fbOOBDoGwL3)fwNwufWNN5u=Jk;k+uJ2bAto;vHPIWa` zwCz4qUsm*WO*P3}1RFJ!A78MwR5W8&RsBd*uTM3{U^InKQ0G_mcSLpdRyGG=6`x)w z@mtlMU9?M0RUb+yCsHfwG6`D}BwY_@l1)e~#A8DQ5daWn5} zl>uKi<6icubH)j1*F|hphiUdhayL_L7XMgR17Md2Xjcn#_YZcLIb&3*a~B~&mhW=b z@pZ@Xa+Vufmq~H9RbUr$bQec2mt$@b#d3dFSu=MLX%_o<_hn<&V_CNR?iaOZw+(M} zTX*+6ahB&7_f=3A(_%LrY}d1T*QHi*8+{ZdHG^m(hsASO1e_#ULn<+3mltC;)qXZ! z$^w^n3V=~pLv7cQekXS~68?A66sQ`diU zgZBY-u()?r?|hW`dDs|%7)O2PswLO4fEDY2%E^O=O@Ma&huCgo?lph60(v-SML1($ zxGRJ)r*7CKh8QVz*e!YZF>7_{dxeQSH;YdAg0C08Y&egIRLh2!fnYb+gxJvtxV2We zU3#~xI2gl?7w>l1tyb7|DmddwSfzh!mg?B}iHl+>fAjB!*HMeOr-N34F(S2sbQg(e zd3qSDNf^0fc>{yEi;{T1jaDm?r%hP5H;z}YiWkju_;B(>hlRJpf;UBt@a>N|NG;fH zlx$4d82^hm_j375mlJJ|!!v`}cu+OJlQ|`U*qL1v*@^hqk+`Lpxml7n=aGMyv5q+1 zlNX1P_{Emj&wbfnmsyQ_xpRz-8;sa5d>L1qXML5`Qr;Kwm|16+g$tMWxqvsDcX(3w zIcsS6jgV1+keK_JnXQ|*sR0=mlvS^g*_oW#os3zbWBH|**sq$YbA(y-On6ImdAWrb zVTDmeg_ggLnN?wyub(<2iK>6Ko7I(`*?VbNfp%pmAyxU9VsDeVzRp9vgM>?WX$@%+ZS3InX-=wsrUJx z`46yq*{to&7unF3DyF~8=d0T^oO;8q`PYT?si=BUubUrF`#vjpVXHY|t{HChT6?aT zQ$jf_uL94nMa!)k`cHov{(0L2WtwBEh1IAxQ?TqSu`(w;X>JYg|T^<8Ql62fe$K zs;@o48(F5hg~0hKC;BVFo7K9SzIGFnz4o2QnxV8>rMh~zv>Z!WTYbS>SGPI0!~6Tm z8f~_9SC*TH83b9djhO6P<9?c_y_u)B|^ITL@eTy(*jkHx&Hx*X||9EY>n+f<6@vmDdQh`q;q9huTSanJeX z*t@yZO{th0kJD!AL#}(=Jv-abB69TezTBT&>>aC)Ovzfm4qeuIoH=${L4nu z7rUI3++B}8gd=t|fyWX9<viUQ^XNK}-6iMX&eCS{RmT2v;6xwbg%|3K2k3hn z+dNI=-YWS11K!?w=XH3@9?GKLGG1Jv>QrCs9;tufUR~wez3$$}#8i3VzE#NjMdh)Z zb2xM49joa*$%bBS;(pudeciSF&EuUD?ou1>pCK51-mkslLEjzP{p;<5004nMzz{$T z9tQ#d0AWx_EItnmhrpsyI8;6j2#dv{@mSn`ISPuyq!CDLCJhUWN}^IZ6n+0Og~p=t zNpyd%BPE2!;F6g1ns+Lo$>fnp)H-`CovWxm_o>+${#i>-dcTX=VR_WOgDnqRs6T@^nQd7$*G zx(*h19oO^e@P7=~y5G2={B*mWqhjPeD$`u^BF_V~{x+;@?z<&!+q}H5uiPAwGcJGZ zDB2_pJQV*W?|T6YKCR>-3b@ZR#S<-1n^x~bZ1eJ?Hc@;*89$J`0T?~7YR>{iNStuC zLTd~z+QKf(gxko?(|*Ld>=Wq|xiXZX<|htwxad7_M6B?`PyD?kL(%+1@4ShFj-{>A ze0<-(t_%+qvJngfxIRtVl^ed&EK7ed%}+$DB10~lCps}m9MK@a(%k~)#<0Zk9K$TS z@gUN0$^LVn20HOUQWWJn(?w2H-mO#c9I-;wtb=Ei)3;kzxOHi8VAxERg~-+Y*J)u_&LeVKkOh;1;+LGuY|>c`|B2*jO|NccvDLR+ zU{{timt0rI2QXzfMPVXbckX`?58d~*y)hxSb{l=^cE&G&PSRzUquAH}uMOOK1J$7| z*gktP)^og-uQru_V`A-=Wxp|Jn8wu?(fZwuk4rO6gQxAeCjDvI5}rk|W%vz&p6okz z7N6?bq~j6hxzx>E?wR*1uc%y8?L6L`zW1_CaDJsfaJE*vdhvUXOTT~XmyYY#anMfF z%-r33&v5L#Ca<(-_rD?C);(8CCH9%l<L{eO_93JznU3KUwcj`rF%0&*l6)g^KN! zBe-b}>ASqgdgdS80eyc@P4hfQLgkg(t9DJ;<()=A>Co%~cq(<1J!M4nSyT6Aj)Cq% zXY`lZn@w_0#a=-8Xz`$`M{13J13?3)2G|OmfDZlTCFS7(*t95p@8!}%M}+HI)3=3C z@#aLB2(Z!XqkAODyrV{w7Rh3mQ%ZeH9^%r=kt6bo5mk1(=2U;BB6I3sZ`sNnggFLa zOYx2mrQ*iO^6X*6YLD^uK|~mD0b}G8g3ZCnJciv3q)StVOY#n}X#pUkf^cjvf=WiW zPHI?t6_HXJP)PX$6r{80kFq*a$5~GLqmvbEaJD$R30ofJ9JPcG{zJscaJeKTq?AzR zRW|omBH5GVmvVo7$-MXLB;^dfm$G6OLxq7b+X9G~5Kdss_NNdSgZ*~1CQd_`^$%mT z(1xzT$42SF9p@Cjj&pK!N@-B;WUTXs5IQ!?I2|>nEcKX@<`2*atnZ)%&2#h0d`g!o zCR}`*Tq`DFPZ|F~WxPV0&jJO{qEmW3Rb^#E zjS|8+(r2ekspU14H8woZiOERn#6qTX0Qs!&St0%%ott#iTW z(TaytCEb0nR(12*3WZi{Ei9%JDtp?h*(vR1u&*}K(An!-Z!N`uu2v4k+M79Rt4zkX zR_>@-x>0c{O+B7=n!;JTZ*pq2&AOEGoZUNLb*+CS(YTYA;9crzTW)2-lD91U%K9NT z=j2hl_mcBoyTeECeaE^JqUlp>yLE4s@w*rH?N;l#Z14TSw%69*;EWG$WG#1_SC-gb z%8_knb=kc3>j>M~YlJ1H7Qa-k;@ccSgKC}K!Pu7MUz*K{uXW(VIFl9P>{Dy7t~SG$ zj`4pyO8IlK^`o{}(-q#V4U6Ut_{O(K5mfvKjB*w;z4xak%)9xKadrO3Sw|IPOHoDc zR#T%l(ye9d?P!@@mQRrmG)8i2rc~M3GOS&g7fJI}vE9DLSsw*SY&`-p4jNTiLj~uF zU5K;3f5n)?6lDDmmb4y>nOP!+VTg}maUOq=%(`OL<>Gn>Qx%N6}i&Z-E&X3D^ zeuU|qlZbT&LdzKQJ>`5qqOz7_)41PFU43_>vNnx(I!c)8DovtkZl=&$UsUOwiK{Vf z3)VOvXyLtkS~Z@_m2n4ZZC#zWHtvMSI|pmhtYfk;1)bd4ChF}y4Y+nT(ZXAMV`_h` zJ)|ytgUl1>T<-1VsrT;Kx*M-r+f9kU^=+llJO@4Q%*m=SX26#l!ufE^+rQrqjqqd1k`G;1m>C_56f<0?BQ1E72if@I)hRAV8)1OXciThC~4y)C?SF0X&$I1_sG?t{=P$3NU0-;VJ;InMX*J>y(vfL*)= zr8XBA;k_$yX-b#Y^EY1Xd*=l83_H}jhNS5|<3MV?dDgq8Cg)sQ`|xf3y1H+9@Ulaq zZrziBdp{8J9WSVLj;T0%|8vTn-^B3j^QnA4VZZ$1>v$gpp0Wp<&puPcc;9~&xo1~K z^5^?oPE@!Tn@0@qMS~e4l6A zeU1U{9;?u4KY^{iM^E!Rf6si`NN3eKKa939N4wi1KkQfM{e2bon67QH{AMS=9`>tk zza8+rtK7H)(7k)pyyO8syQF`(quo5~s?zgkzuzTXJd;LIb`@ie* zJL^6@gbFId9>61IFl-Gz(j7pK&foP%quAr zC&BydLR;{+BZ|S47(i3$!Fsd6)8ZiW^b;eIlln3y!#1M}IiGOW74%D+qB}3!$HYuW z!*k`nL)5#IU$opq#B6_6z8i17{8K&LSG4R~KwMcxbU-t_Nx^I*C&W_2tF=K>*F!tw zMT6(Xo43VGXT{`IMWZRj)F8m3^+v2=MvP`YoD{XBHoChdyd+`5gfGV2U&U(OzXLG7 ztaHY6M@E99w?n2Mgg-}fUqAeBJOnO8lyN~TS41Ru#DnBV)NX%8i+H{KR66{6H`}$y z3=hXseKIUyLlkttL}JHWZAb#J#~UR^Omj!Pg2ps~!2E>AyokqqUAshw!USo@{Da9n zfj#s8N8E5Fsu`>CM;y_i=RtH_+J z%8aGPR4YiNX2v``LXBR9#q58+NX)Ijv)IfO%`m)0I3&Q# z?7Pb{;mVVQO)SOCyr@RB#>HI6%k;U-lzc!7UrnrtPGoSw%#O}`=16MmA)2|zs>(xX zf4NhZ46~WUsRkLrum|xI#SrYLJki6Hw#}4~O_Z3&oXAYGYeQVxPF#aU^!Z2Z_d|Tv zL_|f-BnE#yw8c-<*3cXXNhIq~O#Vb$v$&)(PsA|E9NSQI!O(2t$duAd#E;L+zR=8> zO+^1nbl1+U3#T0jO6>_y?AK6SX;DN%&D{XKl@3GXa?t%6QI!8k+sRB!$CP^}?K)dW!7%FT>8KD2Ai-4n$1r|}gFH%(((&W3* z4FOVpDMk$_P_;3i@*f~NheXU#h{N{8@#e|vd{11{&#Z6GO(LnRY|vFQ(h#SJ#iMbaGL$AvCQ6tvSlP1HR; z$*q4&)nqQnWmLGe62Bb0RYQ(ctx?o#?9$ynM2#KLHC0TlD?G&&Q6(Zz-Cp%q7<^p&4p{zZBBnp)kHn=P*ac15U5UyhDA{#|*|nX(og>x^z0d`d*KLte)m78=h1h?D zyx4V=+7d(AWoK4psnvy_SPg{IU1~&4q|fD?MqD@AQ!ZIGc2^yk+S$xn?8e$WTw6q~ z4Eva@(pFQ=z6kW6)1p5OQY{PXHYL;tqa3f%g=W&6c3GvL+RT);^lVwpY|phI!X1KD z1istNcUrX=xc$Y>6|&sx<<_0aT`Pb8xbjKO?W@Y0fz~CqIpl_1{ae^YYP)sPSQSFk zGYefzwps<;+}!Ql)yz>%%h*(cUA4|!jn&On#Vp0fTOFM@_2AX?=GmQy$}PxVMX=sI zkY06dT=ntWHQwHghu!6k-8D7473^0f;n^K=Ma|`2{Ow;1yyt zVHNq=y^CM=5HA}FG}UxL9ui;v7FE_0xSknaEZkq#72eJd;iaIZwU}NM<3=7FV4fes zt{XYNBVQEz;pN&-mAm3Z=`Vk6r_<%xkk%$%wkg`hY~NkV%MK>ueOl0NEMfd&VAL(+ zNX_Eab>iijRu(Ve&8Aa4p1*l+6SMS|-FXR!bO+0pH1&Oy^ZCjJ=veL5V}2LkHLzc0 z)?x);-Ua(!rbON*BidY-V;${Pe3fKIN?mR>;YLa1Zb#GvvER-&WcGhcVumisE=AH+ z0^(Lf63GT4at;UG`X6=1k@8QQlq3Um!gnDsHs8nImn8YF#6oXUhRk}51 zJxQ)uX^`2yip^)VSS=R2Wty81tybo>YmI)7U5MLk_Nk@b^-F)C-!JzmE!PKsszofd zi_RX|YQbZ%7D~QLEtkmU^I5BWR@0QjL-R5H9&Yeq9{@3>VB&kv16*>7w-46NssDB|(p-5qA5rFQFeI{iE^Zl$)mcRJnc#mBYe zm*_nnzITt=@6&(CHT14$&&7@S{k)!dKl3=&zs@_7_dW{hs>Q%A+j#;&t&7zLvPjyd z#Ue~n6aautx-SbN=xSpMz-r0_f+g;HHmajgygZb`PwN=(x$UfCu|BMH5}!uxOlKQG z?_6@xM{W}{AG2|*F(9Uq>xUvY@uS z1y9f2BIHtO-4`a$OLZ$8Q*yL4_s~@A^87m$BpXCA6zx$i)J%O^_)+pLUn)~7jaNO^ zu3cw1RLp<1ZsSxn%U3d3PQ8CjSG2W10atX@i#a;BO`z8#F+3>^rU(2+{VOq3lHj^= zUAqp`?gT+rS@MO__Et4LFKFA9^_rB}jBVR$T5BDbe785e?>k@D&INQ@QPtNk;P@L! zcgPq_v3cECE%Axj*p?KaVUR8Rh&Y%AIg8{KmK=YOU@^8o2w3mE<1}RH4eF$2xTZrS z-*%oik=55;OPS}Bu5nW3bp~TtV>6yDk!bbQ^uXph-HoE?Ru-J{>Y47XDOY-)AD`qI zPGvRgiWLh0X}TNts9lo=wO(jC{;#*CcCP4!;A zdToCkBAK$gJ2pwXTo{Y>!|_e0DXr-{7Q>ryGG0B+^0MvU%i|fZLq~C!w;hG4yo^g+ z^qo5~&hr~=x!ELLqkQdceZ#-e_esx#-*Q$)VbCaCgPY>LJvWIVczO3S*!6mUorHRN zH*M$g-UqJK`ab4K)cF3*9pz{(S7Y%uzYc#Z?fIVP*Me=m@7X*1&kF(fe(;}=_}pv% zSwvOky7rR%-|23DjVbds$JqiC8=rxt9d9=k&R3a&^q-0caLKeq}Wg262eTXZDImi_$91<*YkI(ixw)pQL*fE}rsmeyQNedyH zJZwor{B)0zl1<~}n0Yc?8b}F6DcyhDEt0P0Qbm;T{iQ-^l+wlK$g?LaWu&=Tk|IJ& zWdANA6t8-+>0e7Zi0dWu?U)lDW;BUH>1IiCn9(UfO9`51=3Lit6J<|L8MQa&bl989 z+G)BOg)b)rvYV5o$Ii3qIcH??dXu76nknHF=B&e>QkHii0?QrAGm(jFXn}v9IH^D2 z*>`X6T5-?0$hT4Bt&0-&NKomKMQ7y=qV!Ht(WuoOXRP&%Gxj@AdM!Puf`^P$ZfDHe zL|5L7RHg93a?%=2IjOq*r!<|q(v=fR>4hYlRL*-%%4JUJr9!AQdGJ)IA5tWxP@%|r zju*OtD=K9csD)0L%4)X~DXo7utMw+VyNbs=YNahVbzY*@S@&7rokywB{<+o4>r&{% zsI2ggfE4GKUrFrIEzdAA8bn?!#}6Kv2GvA1?6 zs@yw`Bd!&$TK10HI8?7<$&H98v;3?r`v`olkwm(NCIS#p1$XXhIJq|-c~={kYA*?_ zx3*S1-ni{~?$oy7XUV>3kJs{A%{T{9szwa4BX6!Bay2UZ(Fi zaB3yMSO*8l>;Z!C#tDD2*V7Ezd(DK-MP9=2M-WB)M~87maJ+b;T4DSvJL{eV!icsQ zVtiSK9KFznkf4=gq7ik^lSapAPVQKB$dU0W&dDjSCCsci^V3o&BREB{GCcFj1>-(Nh>3bKvWiHe;l<#&H+mSVl^uUU2R=Coy> zrYwcbnAvt#EeQ>@0E__e!%)kCYiT2 z|5xcqBdn^1Sh;_BYh^`UbE)<+^4S?W-Do|xs5OJCBbuO;N(_L9)`?27bP9x{t-m(+ z4Pe@;yLX+<>X>)F^xTuTc;kIc;_(J5)H(}dU|Qk9U&ix|94R{Qeg5h<%1zZ4{@Fmc(`U}zfGryasC0m)MqAz8#^23u26rcIL9I5ToaC39zD%))|%ux z^OxJsddGRfgyMQ`)p2fNr+5~>=A8cgaDIbUIc3P_eG8^-jN86Cu3+mtuaWZJv9@n- zNaH$-jrF_9Q41;=09ZdC66Lf#(IIa}x<9jSgkHn?eh}-uSi^Bj>3upikmh-hm3S7# z-c#$X>h*ux?(J^wRyOX<*`6a99vR z{Q`p{hr;2~S)5jHMuO5PlA3J_qYr`8DsmcaDi2kuS0)rm&32_Etw^1h+dJK|#bHU>*xL9p!`5(g4?ieg=YTpsf zLaTo_oW(~kppoA3cl!PxJEhUh3I)X8Fu{SQk-^fLLd-zkw83DRF;l(zr4FgjLsJ+Ed{sIu=mjmg)3>p?E$U zXy2ChAy}C8X{m?W7(y6_p;)*~pCsA7vMCZOyo?f$B1+V+1)t7{4#u$E@KLLap48vcU0CMr7EcHQLB-A@tI z{OCRmhqCR5QOCvbL9Vs7;(+L=U4FlAm~!T}ZMPc-gs>@M#K7yiilD++I~s(+QhYYH zvo3V!3u=~}cLu-Zj7hb?a!fkbxG)Tz_sbl-xtPT=i8{Qqa|hQLc5$cm$;E$jy8|4} z?Mv%ItMV*QMa*oBTQjlre1}X>G6u6#U2`oqk+N|eJ_XKa+^a)^HC*`;*xAjF;nHx8 zamvZI&0_@5>|Lu))L+e`II1@c(-6NmMT=gnZFlN?MK_Iqg4j4`Uw_(m{rit?HtpMc zmA5FXhvB%`7n`$q9#*s8Xr6ylR^V~nUx>e^t^a4|SjW!glO6{}?ReZnr1^T1u3s5Q zuVtO@z9yM6x$C+P$AfHX&MJ5BG!G}E#<`l~MfCY;`;*OAj_s-Sx}QJH)il0e#pF|D z@lg3ZYY%#6euOXT`LVxu68F4+e>T>-uR6>0Kb6e=oP(rnkA?o4XR?3jjkE7qFRlC< zcjRkaoAGKd*u=h<{NhyupDhTXbUZjP@|2@*fG-{jytESp6x1nEuSy2BxCX{y0-k#f zE$u;Np8+9z{DUiT8^2h83fv+of^h9RzSM^T-qX#7Otuk0NL>BlGx}7K1|`5&q_<*& z(TR}d>Mba10$$lJc#wa&aY96N_~66kh7Y)k(UA0FO|jKf}b|7vc0%fl<0VNNBA5;=4+bQ9eA!WlslUQ*n!t8N$if=(Hm`v3JP4 ze-H8e_T<^TG|Rzx6ou;!U76aA=q^l|16>54q?UDZEtkX@a{+(fta6ew5f#aFBI)AH z$7hmB$vDY-4`g(jiLyQ|wkZg|CLA%DtJvQAkE#6pLV1lfU;HhZZBleV=Jzln?01|q(qB&L%I2p$M_h32vrUINFXmMFaEnz{ zy4l2)XSEoeOe%kgPQ#Z!q&$0}6K)5~$_mM6B;JOUDo;o18tW)~Bcd%HQBM`KJm#B5 zno))j(Kq(sC%jjFDGFA#ex?t9)eP$)~hKp5sbe<`_h@{kh0aWLcf2{im`1iTeP~Cy5+aXab+xBsycBx+t}C1ZmOIox3!Mw+ZuOpuN6bMz4*Pha{eg0{eGa;d#?2sb<`RWU|H?mx~W3oTPQSMKD#}tuPsiryDDdl!8ID)UPPxYy*a{Be{Uukp84i_s$BVa^ZjJIGhaqi>@jvQJ<^(*Cqqk&5lnSm z-?_O`+UZF#V6*;o(>d9QXN*yvwEi!s`ig64*mE*pmWr_TT8-?qMXV-1yH?uXgKKJf zS!#^)7nu14y%8<&7ytqa68rikBrw84_%$GSMn%wGrFFCM93Qu4ii z&0H>mX9y8^Ge-l{yT@K)mIk)ARrSccqbBHhS(@>dM&7v}iSnLxfp?b#q8v*)=u3Yy z%HVb_vOOo3^n0H`b^U+pT-S8&?j6^n18 z^D7yva#V%e{P&<}JPpZkk0*>A{=jtJhtRvffu5BvlaedrO}eNaq>OSfqZVt&hsmX>*UFOF5(D!IQe{c?uWkk24jC4)f{|x z>N{6AdPmQHHm-W5vUiqIdAJ6E_d9Bb8g6$BZC2b~=M8LT9)5LFfH)a^M$&ZG*)?Xw zYc=L=SIB9k6IQ3aBK8hum)dt`Gg>#pfe1E%CPHwB3V?V1ZP*5DSQJ_4E`yjrgZLkE zHYIxY&UxrOMz`yNmx6?b_eOvCH-7jgfVWO)2sL*oLxQ#Zd#FNeXe4=JwSyQufyhLG z(2xoPd(Q^0PVK^0qs0>U*d~#@Tg_i|t7+yJs z_Jy|phj+7kXmx|vMQ-R%d?&_ZUxjDvdX#_Zh-Z3;h@x&N zi-EH%i3o&-$BcdGbctwxT6nmD*B2E-+*d|oS25v##c>bvBXTHIY7`QGXd;U@=!90s zb+{~mR6<5Ltcge}G=^_uSG9@7DRjq&jp&1dcgb-W6JbTwbx1ggx1oi2U1NyVh?rxG zw!Tj`<$nljg(p~NvZ#NFh@KmR+<|DVh)6+cp@nOh(SP`j;6-nE38} z`F3#`hKK1$m$H9qm)Cw91HlFR%V8PJ|M(ws-)eQ5ZZ$*5!* zB9FzQmU#u8=K-DxL7pkxmdS;WDf)(Kr<~}xnn~f4c^rs&ot9^spID}KIrxs*%q-{Z ze@VifDdwOjz?}APl0+StDbopC%ufI0B#bg`ruHZCxMZT2%a|6bRH;Rx z7we!&o``??J(>C_CkjlY`JtcsEu84+qiNcmdIyC$_oI3Xp~>l^wn?Qb6Q))Tq#9wQ zIa;RLMx|Nmp$Z+MS<`%4BAj`0l9~vknMZvZDP!4FpXz_5s#b%lFr|6lplURf_}YtF zX{Xmap_v4c3U!SNKb~4!kD)p|Cn6IV!m0F(5kh}CF97#1HyBE_Z>UqTf4B*mDx8op z->CQfs#>+9s*Iufx1Pw=pW3mhYKNaXM1JaeqZ%%J>cgB0gq&(2s5ySB*y?^dMW;C( zpSs7Zs?2&h7oDm=r%8;gn%JP{nH3s~l=>@@;qspm8>q?~rpn5tc@3*6&8zCuuBv`0 z+LeE-sqI?%TdVp_tQz*O1xc?dzpomIt2#@oO3J~th))W%Hyu+1h6{{unHiny8f^$6n0wru&WfV`p&JZ z1)%yCv53^Ky9Tq{2(k)CtXjK*i%GEiCzO8+K&@Ksv}nq%X$Q2*_@{dttBU=m*_*B# zRkDjq6dPcH8r-tGDiNzvt@7ot>oBwGDy>^>waW{p=}@*B>$Tdpw<;@_T2!%X1gu(4 zv|BQt8#Aw4U$Q$_Y>Q5@8#S|v^R-JQt~+3cx&ec@c(h6+v>NB1NBl0!lC_Hvt$Kg_ zw2O4En`^gwFtn?MwA-GyD?77E@3V`Bx{89j3rDRR60ynUy141OD(!QnpLhy(?$0K^eBGp}lql6$+|LX1Hc5 zp_LE;n=9oVat=HCqgSDyn#FXw%V>Y13y-A2NVF>{z5D*YIdHQZ6R*g+x$Cr6>bwR*ot`Y0K zxVx9RS~9@Q2(6n|pUe&%tNpr6IJ0VbvTO&z+B>q^u*2Le70blJ>!rXu!oz>OBEZYS z!t1cX3uSsNCBOS`!(0Ep98kL2$Hdz?z3V&0o3^l-!NjZHt;^7B%mT!l%foC!!g4di ztW(3xOt`E9yc}_}s!_*bb;OJJ#e5sbOh(1*g11Y*x_m{!d|<&0iN%YP#2j0~JYBGS zhs9hU!CYg;>mb7$+Ql{MwwF1(@a2#5u0b5(f|Q0f0Im42Spv z3IK#c05G^S{tpR-fMC%$NCG7ig2CbO2xK}p6_7+^5;#O=EhmynWm35unlCJs%cT&R zluBVYoJ8QV`K;zk6PJI_r?Uzi(t$IdQfBm7j8Y*;l~XBFN|jD^CyY_%)M)(%qfW0< zsGSmS=x+5^Y{uTVU2|G*^91 zeif&jTJ$r_Chn(>*lqW?K3++u4H&4Ue0yFLV$kxG4ix!Z;FmvMNKTZq2 zrowMZvj4*@L#BViv9P-?zod{f7VEf=^U(f8@7zS~#qX3?7_tv!O$tWu%yQ^Mv1|n$ zzz{@r2)yyk4-G#ptR(@hF=T%TxKZ+r2Ez>$KMJ&R!^JI0ax`T8M$rs<*SM?%#V$PQ zguN+CsXW5VNz-IUG&J*!c{)ZBRM#XjGaUHx&u;_luE>9r{A(IM60^NRNKg}hM$NQ@ zs}o8yWh+F*Foh*dP_wlWEzA&mt3NvPRL?-kbu~9RQZhvER8^9^%D1Daq@xlx)XPe< z9Vfw(jibo5YKwjQaUL`r%&+a10;jTUjiOV>yHGkaL&N-3fnJ!HWWEjRbiedRaU6D}JmSJt<*>rK1 z+?c(wndaHt3 z8uAZKpY`p3WY~5#!5KFVycC{m=w`9G?whXdyYCy$^}X+#?)|^;91jJ-@SHCV!|@zX z6~*zKZym?-9FHZ*@|>?N%kv!1HO=#!?>*1-9S#uA>r*m+uC9BI#ne_v$^?QUmQuQo z^<95Tceyq_EK5i9UGIJ0_#O|1;rN~}jpO+qPnG5Qo^PG!`W}y^>H40pt?T<9&$aFQ zp6`XUb$Z`V*=_T6XWm#TnlrN_-M_;EYk?g8E_Cf4MEbD?wvtqgbl2g!UF*P8tDHW z1Kff_LK^#$Yg$>Ux*0x)YX_mkIfqd09z)oD58?zth*1tAL|Bax;zUV_Q7$G#*qsyH zJLyEO;NZe%^6i~r9({$XD-)PG7owz4j8Tp*( zt~{INWfkI)3WX555fJEC3t>S%C??uY$Y^0GNep3=a?V;yS#2%l#JQJJ?p{mTeJ|z& z!I)DHVl)X_NnNVRB@(g`%PCzfTNHmPg@gVVOnCJDskt}j1Q(mrhH_3= z6FBEI>77q{bWXY5JKm)2o)gWxO9)Qt3a&MZ$UL`|pa$C=a8 zhEUoaL+FJOqEt?bQCclU=*1bMRBdZaq~K5}TcI2cI@sgp`8jSMlEMloO9;Q56mlNuJO?dO!CKBpu zaH^xcHzlrVN`)Gw=n^1ERa#aoTBTXsT~S;$s`2vcj#GR}bGH0ZwqV=pJiL)kd&BPl!U<^f{ zr;nNdIom~OZ6t8C_JFHex#MbG^{y>8${kxvSr@H@8n(^)-ISYq2W|bgtW>%T+!pn5 zj>Wl+w<6&z%anzT%W|PJ?&qYUhZo2-j!=}+gjQ>(bLCo7lk^_uxNCozd2dASc20O@ z$dQ&__Ey1FBPuscx0UE-UWoaD?&d2l=ksFt%yv^W=7^V^UfyoXPWL=0EbW~qrfP`U z_3Y=Tp`jO+f6!B^uunDFm9cTY*(9Hag`#!&Rh9?6HY$f@?;$Pp0xJZLZH~pj)EX zn@ryOTD{+Cmpld=4>Ypeu^1bq_Wwu7VexpI#ip-VzGHtgc^HLytD3FlDtT&Fj)$j( zYGs#8epc04pUt9}{gjtioQ-C2n~l!SaiOSTAiHh%8x4xm@#vgAw)Tb3+w!D!eg`je zUCDK?y&0c&Q{TJnC0zU!2II&LzJ9v-vh__T6-f0|x7c2Uyhc^zh+Qb93}qqr zflqm#w`w9`VyMmiewtXuCQu%TrY>rtSp8OR+12^QY9q#N)R9&<(M4I^7`9QBBDF$5 zljDg7R!ZfSMoM+#DIz40<;b11fF4PL2b3mek!pXA=2+dqnB-|`fRmckIzU8Y>CS7K zrzbi@cGdR6e2XXtB7}!$nHFE6=%~^+p`jU8l$GfuGG3kOIn^nrCfZ^Bn4_8rfO_64 zj&`4Er+P4+spUFyfvF0votYy!#JDyuBq{wLbz$081goO^0lcFVnmUC>E9(Yk zw5xw9;>mF<8sb~85?Y3>vfR3rWU#6A3aN*txlZAaYY8^xtnHMdh_>$*%DcIj3SPgr zZknE3hZ^fuqP8$g67Ps@`yNNM>bws0sIQmK+P!ec`j4`&*B;}+tUPHox+oj=>&Ne@ z!rI3vTGr9Qaa=`n!ZKU>mcQ0))`q}Z91VY3#2$M#dycV8vXQ$nYlAe>F)O!4Z|rQk zSc)y>iH|}ZR=SCWtHf3egOk`=#ZO^*`UagfJ$`7mS?k)FwJD~rWy!JYBTUucof=ZS zBS>+z(X_PM*3suyFCn~gz5_dsHwm+gqIXUzf!^UR8-cHK+-FL|cRR}pf-k;j4cUJ# zu6py{c}AP1tK(S9lh1NaV}Px>Jc|70y8YIm-S+O5Ooenz%Z=ao&95)hcgl;b==>^? z>h5;lJIr{w6z`YicTT6j^(i;soc6q)&!G3TJtx_#JPz8n?LAw|%GLg!pU%v-|3RHM)Suhfb_$vbqh~bs z+7rioFj5D=Cq)8a`@?k1+4{E^LaiM_Nq!K4aKQys`=4X6CJ*KdLP#8C+X4Vjj!GNC zXUs+5G8lJodIiE5M-X4Tx?t}13Bi}u03nnYgG;$+LD-b{)l@2EaULNnCFoW<1Fk!4{w-h>S0m5~lc2{UbC1BQ0c~pe4H*%t{SG z?9x=BwTiQ&nlwO7S}ns!=_Hk$5@Jq&Nvg9L$thYyq>NFil}IT;B@*lken%2nAtvBw z(Gx&ujOA;)=+Pr%k~NtUZSKc;XACAx2bPbrLPn;?5#fumRE{zY%P!XvIj!fw>0~Qo$Uhr~%Q%Mg*=Os9#@_KJl3G)#Y zg(0MrVm3e41x~1xCZLru)XUhIM5$Xhq4R=FPbz;hDi2(d+r{L?#V`l7Me ztqS5DQl+$^)wB8yRjKUEr*nRPrm1=VP$m@mY0_R**LeG0>up@F6X4y{Hiud)9de9S zC7o2Z@nB`sY^pF&w$({ROQy|{MAjO&RjRJttF&I3@>;RBsy#Jo4X0<7=A72cFHb3* zYoD~n!jr0w;^~Y)wv2k3vkJv#?fsptc9xRNs;=DTy@`=@3gO$SCqQj~Jm{k^mZv(^ z81bg!o3l0Y)K>dFc&;_LypH11PfJa3sU2lesaD0d$%$+(M3!gu(yeZ?nCBYPj#?K4z>ZcA@Po z$`|H);)=bZbDkT`c)C62ELm^023yp5Dq-e}fqFD8_@8*kAlo`$adRe&)$u~RH6}vaJ0ovq zJsX*_^YGPK?Zd=>Jqdkpp2W*p(_!c>QI@sDxYt+ur_mi2`m?kV-C5TZ<1LM^_Wn|! zdY>b39h-ge*3hANR`*+6qn0$@?$fy^a^bzPqPVW$vzRX>>Gdv&XY7VBq}MNKk&a$x}}~-MQtgMCoSP^$8mtBjQ(W)YU2ZS4Cm` zTefa46{fW=?Be+5+hzQI^CShB@S05e?oN5a7y0(&T9dH3wxg#zq!TcmLyl{GCFLgD zII}!YIcnU0udI6)hvRpvhzt;WAby!mkD5XZ+G-9pA>7%XQtl-M#Z0bJq7> z?|3?^&hQ+2**2eHx_mTuYKIHN=-$l=J;x)?8nf5>FIm;~2bSVqyUf&Yq3qHZ#y{RK zHvG)fn>!yJ{JFp6JAbv;J`dRW)*n}Xe`xd-+{OBThq-@QIpUlB_TA~Vmxa+o>Yn;Nz~ZM&7Z~m->H=y#pfT5^H`nV zhpG3U9e0)~>Y%O%oDs&BrUTqP$)MHmMiu`Kjt2^&03iARpRMp91=L;g0vJQ%8wqWJk%ku_x(1>$CL4Bt8sV)+PWA|4?iv}zMIpKQ9(jKs<>_8N zBVon2;oWng<{+X*4Pv$-;$@|u;pie6dmEdKl zoy^Oj@+O#Am=`uNhUO|_*xpK}5TafZ-C`D8vNItVP}=eG;}zr~DcoX~I-}L|;_)ee zohCM24jP~W7|C`Sqsk&7?kgXC!6TA38c}+Lnm?3YFdl|PBd$Utavw^8gq+?Kqs`C; z`a;`O`=g#8;nE;l1}sr2sh*Miq%JF4R7WLi4nI!fgUcE*K>4VoulmEmB2>9!>nMdZyx7g9JFL6&5GOW|5brDiFhLPuYQ zN+Bjq8p=0ezAvD#|02>;<(gV0Rr#3tEz?pzV75D6VS%K!R3#2rW%cQ$PF!U1McevP z<G;$>+{CQyWfN;EFe5N>S!2P+?MH95Q0s9%tsFX;M;arTxZ`+HRgsV`e&Pq?J;O zE;wPpX69M?WqL1WPHp6wfnfF{W)4JU;!~WSNnr9crY0HYhG-#*TV}Rl+(J&^9&A?< zQY8XzB_=7`N^oQ*TgWD1TDC)f z77Yx8L!kf|q$U3shr)mnDAaZn5Q|5n(MV)oIUnLRiAlbMaCj&3G&##wT-RhTdo7v|Ryi9&G zlfTUJ6dc|+51W=wYw@ebD({@Z<1Us=7FSiA$!R4S+pf~psoCx+8*MfEp)kPEBC_3W ziw&jM<29Jw^^bKT-es|W+q_*1Q_<{oc|A=&n?H-NUh_P9rn@8E&rZ2=zSXPSXz*-g zek!&1JrVl+I(rY_r_RHs?iw1=y$pL~p1p3|5a+=#Q`o&SZ|eZ$K+uzJ^CWJ35ck3E zYMS67&JwEUGOmmq$GYshjKoE7d;r-k<^&Av=ZF|1FN-zHBM3UadMl}2tLRQsP zQ6$6D#C;LiG7WPSM6fCA>H0>P3|S29J}V70Z6du9^)W?uLbhM+5|RkE#d#dSa}GcWIJ}psLDks zp4->dGhVRuPPrA;2aLoue-Ajpwig)Jk9=T?N(t`0sI3$j+lh-u@(w>p#^_>X0(NLM zK9-rh1d+{jMLKveHqPzWui^I;H2~kPR*fiM>sPZ2Jl(4%Z0m_hZ-L6NOODm*FypYl~$jJF2 z8;UNFg-|2hmz)PR#&Y_iE8Hld8h zlkI9$!lg|@;e7{a6V5I)xn`P~BHFmWYKA}nu+xOlQG!t+aSFvrgT9eP6_1i>y)yE zbxLVDhUER~M7kAriT=c*{17Sl?`m4)xMH{K^scd z>!T;F6pkHPc`;1Q?4+3$`dwHEC19z&x36{@6VqBsai=u{vC>H^nn5$4P~ zdV^(_eCjDqm5ex=*FIETq?R_C?b?YsU09XHxt6}zA=_lhtn^E^)}5+YJ65vXm4}%$ zYG2mZA7gCok9D=^yxfPka;}7b+k^KyGu*Vxbf&Eyv=Co7raC zY_+_rQvYC?H+t^v?YB5y!oDT%45Sqav6l+`+)D3MY?bG}*kWN`JV98TD<;1;QHEV> zLxW$w554aXA>sRhR_EocOgP$}VYSzXZ%u!HRt>t|>@6y>GDe@)CMKJI`q^PA1j>JR z82ey*|6(u=M}rj-+C@7!X)om)z_{#n4}p|Pi@_`(!~A~$00aO)78R1tS28?EEm(10 zC%~(lG(t?{URU$Uh}4ADNg3l@=iJ?+NWs6t+SH$Zi&m1%S&clJ)5cS+};)QOyiy9y>i~b znmeMTXi13YFu`dSr#vG!h zbKO~ec&8QByLsSl42{PgMy%uYA5L%`wH?}xjYwZ@Azks^=)I; zE;-`u*biIid>yX42VdX%M_=46m*9JXr(om`MsptsiwF0qi5({h@G0xm`UiFCd=Ge6 z4wZyCpK6!5tDn-o-{C!jRPP=yP-rdJQhT;;;kkR+dt6C>;k)m_Y?T|vX-m=GeP#0X z{SP^O-B-K(OS0C~Be8k~gQP8ozrMdq?(6TpJ-mikz~7f6`_Jq9_m)NXUw^*2J?P_U z*50pJeD9pwPyG9h49&0v_Rq%1&n*8Bl)R68#Sei24oLpb`vPu)#82k|@7)9MnFBAb z+RupmErkSstgyl8+`~{u0B{KbZAAo79QzOX0Z>}CkQ(=I=>||D_pjjskJkdsdiZc% z1}&uIZp{F&dj9Z0o-g8+@1qDxJo<)*1&?nCsX+-)Ou;UF(*l&=D3lGBoP_qkcX8CV8{}7(|PNfPmcI@=^ zNczgH6!uQK`|zy`OUn+3qT`VH0L@^~(8~610PHZy1deoU%w-A>w+pYK6L8rS&McU4 z&c)874$z4aaQzZYLdK1!5|JKw&$QgmYVR-5|8B1hr}GlgM+D{R6Od5p5sem*?*`|= z7^&WWhj7;!Z;JD z5!D<~*&d9V`7AdQhKQ8vP~yxa{W;Y36T#cQntm+`3|h~&xQmfQ3fNC0~pSK zrzuj&BN4MIEx3b{zamd#E%4nOjg0w{k1TQ{Am@oOZD}S_!5|XDGI2X4ugNd&nFF%U z(~l(MGQ9|Hc>dEMv+>IYam@+S2`X<+cVx>m?YjxGWh8SC8Z%`!k=-8>*EFZ?G6%x1 z4h=GptuoR{GZRqpPA?*FR%DX}Dv#5DI5QOhDo-@>vo$kgEwTkRky7Q;VKB3?FLDVs zu$MDZ^CpuRwKI6+6U{O+fi!WY6|;#s2zfp+GY(JDIkRUgvx_{ljXTq=IF|{cWT|)35NRKwN zZk-uOs(_Rh>a$Nm6a5nly)=}6eM+>UOmS#Ml+{WqyGfI>o|L^h@KHS!yHGU2PqST6 z3aK~Lw>op)%=G^uvk*{pgAo-3_B7=!l(SDY2P06p{`1o{u=`LI8C3N#FbN?)5!Y1G zUs2M_Q8imrbj>!?Lf+KrR`0V@GIdh2&m0unP>dN)45d@EWmj^!R8IkaRFy$U)m2wA zizBor8fdRo)CoXJyHYb6Ds_ymb!|ztaZv9wQWaTF^(Ow6fi2a8I~7vtRj_F_kvsIu zS#-k!^_@L+p-*+C9q?CLwYgTc7h5#!suKZG3#U<35m}VQU$dQAR0BEaTAG#DV70qd zb<%>>!$OZ6PIc>4HSaWkRr3}O!&J2D`mZ%r34>#G_gZw1VYCJ@l&<2Fe%a=wTaj~O zG?hbAe^Q2jTjwueb;o2DgJE@aly*l4btX%tUk*0EUj znmBf!P*$S4_Q^4pt6TQ3Ay)NWv|())yJ~e`Y!-cAR>t`BZD2PlaicM2_7829-(}UK zG!w02mgL;ly)!bL>Q*;zBg1Jn`EYUNZ{n+AHlcGC?`YOraD-oRGH-Ep*>SXiVE0cs z7bRU*#GF>j%2fG(b2968me);pKR(mpXSa+emkK@?AyKfSbyn~}mwjKA$z1ROJ$9XT zmj!m$v3C?W$M+F+cYAfVw>S46eN?3zw!?Clk$E;wy|&S2)?+yGisRSsWi^w4W>s|8 zvvm=9dza}jH@{*R_hePgTgVSg7hzbo=Bzgbeb3Ba_sM&I*By9R6L^wufNL*!clB6| zt8;1ij>SeID!)IL-Y|G&WrDKL^*rj90Y^2pRpm*6w(EK~^B8q$PBxo$cu{LNuXi<% zh^VSDr3XUPi(e7DLfDdd*sh5QZ%Y+*SJNX;*qtcM;X^Bdcy|?k7hhxX(}Z+;56bb3 z)Tt(RQ)JbDc}>GDjg)C0O&^1pDDU_FH1j~KC% z7_?FX?~hkR_!rN1c!3JI$CDUGjP|vSIC#7{B;S@RaOE9F@|TU+)nC|qiB~OF35SoF z4F^;!Zy5u5m?G);!$A2Fcox->bqjO^H<9@^m02Z!e4`1IunSEBp_4fKW0@3dl%n7A96Sq4xUZ=HF$i}GDNd24Gqe~$Nq zN%Kt(*sYYd&zS2ui+Pz08J%-ht#B3-jk&bpdCy!~)t>q3oQO@I*}G#HPny}oX_;A+ zd5MvKxpZugGUnN7m#+<-_phP2F*R2^lUh%E`2};C^@@=)s+xs-84fmCfvj>pp7Ovjx=*L^ErVKrm5#K3t%%F3R*Rjhf1bF9q1e}`8QkPL ziAH++0<`CzIDfBLeOH=MmwOwbm_e`@9fg)JojP%!HZ`)EQ9=5>vsT%xI=^WcxmuUV zV79HFuMS{DM}PKFubKih`&oBdRT@|yuKOvkIb$*S!*;jDt8SmLTAi?)eMcB`vG;F( zu~rGH`sY0P<3)RovU^jx)yrw=JG484GU5RLfdBw7Km-^80)N4vuy{N!76FMuU~uTP z5+@3bL1R&PbV@lDkHR6*2xJ}`9*4-I(g?hERRxsCq|s@VCUG~5NvBZQ)Vg6Rn8fAq z`NaxdN1e`NGRhQQZ#AdX=G53-QcnnfpVg}}T2*3E2B}yqmRhX(jaQM!EAhCcs?Bke z*zQ$IOqK^xw$dw<3U%t?V829fx7!u&>o&vSaW}~h2M15UV{F+RRkImo$mOkbjLlCu zVVUQx)x3Q|xo4wkG&Kn&rss&3>T_6VZ2I%B)@?R=eQv6QUD!`*d>vHootLM7-7<5k z^hOc5!fEofN?wy4tEKeq_Z*&5QM}1?Wqry1>gibT_&t=GzcUx8>U%EVAEggR=geTR zzdaMM+PBZ^y7j*BEAEfHP5V0Z!43lc`!1~96$q@2YnJ?dlp*FP!DD#<)g zQ*5xnk3>H1yDc2}V5KK}m`xVVJ6Xh8#$~t*4ND1WSB)HT3RNKt7R9K*&=Sl8l%l041aciOAu^%4A|Hlu^L=<`bOXWQ-@pN zHO(6);a5dQ#8@^(?S#fOI@@7oHhu3KN4aJDc-8FgT_!RZekF}t6D_q=<#z5jf@aVD zGmhU=R$-$%*G_*q;5gK&eCfFsk2~Dgm6ok)y6rWPY!|FYfL;)PwP#{jSUvrmYf_dQ zNoRWXvzpe?6ZqJ zZI|^GW>3Rzw+C;3h4hss;~iDqR}YSE`{z%r;~dU{#&~`glfi5@SBYr!eq?jUaelM2 zp<15r!RzR%7axgV-Zz%;&%2ea?qQ#+H*d$mqA@p70b3&xX>RTKGzUoR5HnJD&Fy)$ zcjE3Kqi%IgO~O7SD&QXTk$A6dioAz@&ELcoNDhhw9mT4Dxf$b(Z7rd+J*W8doU3?% z@PY8W2tx@`;=ogok@CWGj<+9lH+*d{tUpLM1)fvpPKe$b!l$NvU+VXOYZ3cGC%qL~ z!@7ZSS_ZWU9{C{BgMx3KFT0qX8r_6|&`%OZ;^h*-s%F39)P+u8FFJa7#q$P}dYp^IgZ%pmYdHW=Zgfy+sz zjy^``ZztVr6Mk}5CBv8xBGHN)S#pvwzL@N|;KZA75@jmE^jKchTv=<85;aD~TM67u zjdF`hR;{^8``u)*if)2fKpBe>j%-18u#Lq{c}FdOBea~B6Dm%^xQ1M%JYkpB6-{l0vigT6ePl-)2QS7NT(j~382|WZSl+KaSB5%sK8!j5$prIue zP|N9)KBd(Om~)v!O=zhF-8AcmM>S+nrH-H|d{CGz`jXQLqVcBGDVvb8Z$alRCM4W? zgvL^TfGN8pqQ*9N$ahmECnVK>&3Ru>nwwGT4ONshO{Uah<45U}GKG{KYfwsP z(wv~|fk;I5Do>pX)i?qtG z)>-FL8eJKi(gL0{slxH+9P}|Y?qJahYWyRAJ(#K14z9~vLi1vyy`EMsyVwW`X(;t= zhE%@8&^n=8D+x5A6_T@9i(OqE2~=el0@zVX-y3Qqie@zKyH<*?X01$cpYwK!E$NqA zrd)KiRI(;kyBl3+tuu*LfrL|gkdf+Lua~c(D%ojyXeV{Us}}~sMku{HOC{&DcGB5@ z*^3o)6x~#;@QQxV+nrYEjrp!v0)tzM$0=(p0%z1VGhljITCP3BmhdLtU#T5(t=;Kp zxE5hos@qED1%A9WTENWAO@ry}Ny5}}E8Yv`KHn|mR9BkmRhhw0m@VzJ#n%Gh8slN* z*_JBh0xl6r20!CV@|{+RjMBSQeJxdgAD~ydUA&yDmTKkTY00*#W(&oTXVwGCILg)9 zit#*iX~nm;LfXliHJD&tL|l|>EJM64R+#kc&ziGdUWr9}^j3nt7!NDzT_u_{&MvZ;DY0e! z5w6qT$Im)jSnaJNJoMX-&iF4&>-|Ny^(M&F*K*x%z1t+Vres|j!%%Du9h5e`@w3)% zGv>_qk1dwglG}@Yxn145c6-u)xjX-4?5+WbGS?L07>h#fJ>j)>?-jV(Zq{aNS9rGK zyuG?tIqIz8$9Fa@)0{a|Z_MYbqrS4>wr5lFjZ@DxUXaWD*==v5>$`EU8QL3ffjXWE zwK&!H-uj1e^W9&}rFI?STu+wadhY%9b*F&4KyPEa{uG zsiuD4M7f6O*qf`l^0uS9_b!3nSUZ|s^byW_7ez(=?)~yji;b?wSLwWWr{BJV+_+y< zSzQ~X=aaA9de(07y?X?8e?jK?S0&rrW3%D7kF4uYK<(X|pKw1d(P%F&7=`>KExWqf|5Rqf5Ai#LJS^2!gxKLBENJZKP&*h zW9t`NdchPUDpMW6xd%cUguzq~y&N67)C)Qs7{ZhB!K5WT%gjJDCq0Aj!z27a8*)7( ze6*x4LtHb#lq$o2q%gyrBtv7WxKr&xd@VopD?+3XL=*+VOIf`;okTNa!t_1Ev^m2Z zJhem{!dwKw)HOtuEGHB4 z!CY5Gll4RVPaAs}J^--`$c)3le}_EP5ZpUO)N345A zRCPyeG)DXy#}q$DoN>i$K0}0oN2F)Qqz%W^Mo3(H$aDll40p!VNl283$1H)y+=jn= zYRBY2NAz5O$Ba!xe2vIdC`km3NJKoz3@6Aui%E1{NrXa4l$ACFmC1CQK-8Q_^Z`c9 zkwYYZ$IO7pEL=%6qRDI?$)t=&Or=SDrpLsX#{8Wh42sHhQA(_-N|YQ$Op(UKlgebD zN9>8n1Q5#fqs25i%FIg3G^)#FnaCutN!!J)x>=EbN+`ZrOFlWb8(SoR6aq_$VZfDM?B<9jM7cix6Xv&NQC1~TQ8j+PlVo01m8t0??UY8&kX*_B;rkk+)q5Q$Atb)EU?fV{m+!x z&{X%z)ZEUYDioq7quN)AkxK`$Oo~faE@S3@4#P`1JgZOi|ImEHwOsvC9Rg2n6w2iG zOFZe&brw*B^vv|r$8`fyocc+;{!Hx~&&3%`-0ac=Fi~`T`_YX1L#-mo z{TxbcmC@{dQAH%tEc(*K$xQtP#`P>u^)5`r#ZmPp%xx)2eJWE#w?i!MQv~GET_Mwd z6b4d-1XFz*Qe45(MJGs&HP6jINPRU@^#ryJKv9&SQr$e$G&|GX8q>W1R8z~;Z0%7! zLry!Ck3?UdjA2F~&$uir(Cc#3oiG)gEnU@BB}z3(%uQF-*Nsv})o@pw*;eg#)+K#W z6zA7vebw}`SIuD8y=PW6epT$ESQH%A1$RibaM$ICO*_0X{CWuM;jFz2vS_G(z7mvJ z>Ak#Toy%=jSS@H)JowiwgV$WL)UAQf{VUB)iCA@h){THr6t2j{nOQZP(cPTcEQ{Gq zn$=yO*i7!(WZl{vOH*{=+0}bf)i_pFK39d8R}`Sy4R_jvhgE%@SEK&g<)lh|1KRVs zT3wjhC2Cr=Nm|VGSZyO)b)L_EeXrT2v0BxV+m&?BRjONMIN7x$ycNAyCAQlQgy+Tdl@h#j4rGtI<`y*7WbP-N9T$3R-i;Ty?$LmBiAWG$zx>x?1k3 zK(Ex~Vz;t$!r}#8bA#C>$Wo1KTs4GKr1jf8(Oujx+bx4!ox?+FRwJTwS=`?T5_`=U%|G`I-nG%u{qkM~@!nmd-X;23J%3)^qhF0S(zX8H96;X8`d>}|Of~&l?Nd{g{9Y~v zOtsx#{eaxm8(} z(QPJQJ>y}GgiLlD;EoJmy{FR!rr^EyV1<#)rP4wj|bGC*lqY<0Zvn&7Vzwb^+pq!q!e4LUtWa zZZ6Sg4P#Z>;uUOT_BG*#Hpo6L;3g|k&2(fACa(S}W9}*9mLKCrD&xd&X3LO58N1V}V z6uM+Sl}(jLBU5^XJ`q-^L~9hf#cs1-uvaG583lUDXS7;vR$Cl;%_q0o?ozvr&h2%x z)S*>MZFbv#YrIe|^z0qm4Tr))FPAJ1BOQ;(Td=jPozp9k%uI24n^rc#dWF~T*sGp; zlajErL%r7k>i4My#Zde}$pbH^Q~v73n($dXJEBuPpnOCiazWSIv@ zEmW$1qssCGrv=E;jJ)1T68d``Oip~V%uMmLw$9CPeA1Ln^D7?$zmB8Yi$D-MY>mL^ z>HdE_=*#@}A?NwhdzmNv29Q3eWa!&ZPgMCkps*bWMWSvj;|a^Nl5sewZv8z`x|Hj2 zO~|thH&r=w6GD*H&O*6Z$J1*iPQf)oSzOD1bKB zzAivU5qm2q(xGG79D>s$>fm;=2?}{)M?f++Dc76>`V;D_; zi-=?LCJ>!n3=SJ;qL`Esl4Gt5tBd2!EIk6MNNoQAPROgy`LR?44VGss9V&xR)I}9K z(Uh7X_fIcQh5%3p_D88KnRU~Ud`Twf)|^p%Go+4TD#Frsz6e5t_LP4784Z2gNN+tc}r0#tcm&52Y`Xr8r1e->qlv*_QMM<1QYBZQ7PBRFGN@X=l zMLKB=g-al>_eNX|E2W`n;(AIhM^O~kYxY|_w!3M! z+-`SSjgE(Vy5Gh1Q|-3y3&G=Zc~tz}^Ow))bb3?CM&FUv;qbb*jxR5-)9`frz8@!- z&7$w`J3Eg|BikhL@_qB`mBOtzj#Mo-mAP% zTY&|<@T@GqD(-YI4W@3~IS<4U3KI!8FN{SK#R?Kn4Mi~f=N85hi%S=^@ib){#1P9R z96Zsac^}9N{Cye1k_?Rs$5K3d5=l~QoZ?B2w2LRoj#QH>x-uNGEwd0R=&V01+#Iz+ z2}D?zBMTwyv%jkx!n#3!5Io+G#>xbz6wb4(F+0nU-1PKHD3t!m&}eN7Lr^V5w2sj0 z?HbKL3q>BLQg3wm6VEie14_@X^)(vNDK$RcRCN_1kWp;yQ#(}by*Ry3m3xU&wRNQZ zQPxS_b56o_YuQl6Rr5hvS2bJJ4=&Z+VPPv3P# zW)S%!C-xlU%&3F`0Y`77=X>K5Mi*0E_|83#5|Tqm+7;WC5Yl!x)+^Hl{#%T zO`4)ZIO_0a-!{*BRll=68Y^j%YB%Q4-!03N5j^fIOMSIRJHutRTRYX)x7d626~S$^ zHwkNQx&F(jIQ&eBz(zacJ;_GAI*H0JJjF@KaI|MFe{tJ?opI3a{N!8BRowpj(_uYl zH^w}@Mx)s>9U}?4LA+CI)pkAQRo`%ZcNdyOz6W38bE%hUN_N(7N8IuKgq7v=UJ~w@ zMS8LguuR($M4V!9938QHi;MFEOaCgA!%OK6(Vt~rq^0X<&$jL4(|&qeNqM}VjI*EV zE`TW=_q6wakjI~sYk16=`#Y!byP!FydIfd~x>uy~m5c)}Z|y8VS6u>7ydZCIEt0_4 z9<`s-a)3}(z`sH-24Q23hAB1=L3m#ak_;kpu;HLWXin>4v_6G!akRt5y0an?Z-Y?f z!aoQZ2^H)%hwyF{MK?1FU%XU{FZv@mXpsh@N=`L@&!~gEL;8RRvI9RwWjV$Ys)mZ8 zS|z5rvPEbm2P1OZhH8R6yr`CABkWd(N)kV|w;di`yiidR1zko73mu%ihmqypL?byS z`=q+4lP(rPnmIP09)y*J5p{FP`7t166p%HtN=7K@O(7*|@RjB!Sit!@+~u2-m5~wM z%0(G}E@k|vm=dZ@N*RW{ zA8C%9&ZZYmj^)S!R>AW%3Y77Yi4LSVocU;+ORg+yS05Cl>G4v9u%QMlxG1r&-!U{LtH zdQB&kLZlIiET&yAm_}idXoSXX5tYT|vw7qeR|K6;XjD2S7L7-wPbP5bttOpMl}M<6 zH43Cw8zh)cV-;HDibEl+S8BDX-HOc;rPyroyCtqUKY`Tk*E^-w?MAfIZuh%oO1FHN z-fXgaZUTEhuwJm(8}<(uW2@q^cwBsbML@shb6LFQKLwr5Wg}ILL>oz$#Kq}wnQU3q z_hn~MMUTDx6_UXFRcsa_30fv z2XnRG@OXSu?nJZ4(SiCf{hlv+5$^HleP16ZSAR6%{Xbv7=M&1gJP(`Zo4{=g?CL)c z>*V%9NlW0nLGWxP3Brx*YTUU_)CC4Q5aXo-rBMVby}+>>-w{NSL`@aNk!rJl3%M`L z+Ydc4qt=Tie-AMo9EZuOP4lD;=(KdS55I%WI!2 zAoAkfFER53H6TaK^no!xlA@^6sgi2fG%7M;WXrBG%q2BV^CE35L{ii_I7$+eCp|%P zlqo<=v)s=@L9@ioB~7yp7`M@Xi-cnqQPKSjyi&94n=R6E?L#lrbd@(d)U-@)^F8%U z(;lm{V$|-{(~Vf1PL#Y?OsrHw`%}5p^yN@k6s3y8SI~uO<=Hh<9Yxsj9eC}v2^%E4 z*{Gc>T-NrLV7gm1#iE~BPc?~dqqho^bx$`N^>$g-HPLNd(4D5E)>gfLm3qn3B1+yM zNAdoDD#;4s+qS7Zgygcy0n~doHsvFLVUA`igJA7cVTRlm{o`lfmJEoYUKHj+!dZDn zPSDKvEd_MWv-N?Jqgkafl+jsSmwBYp#kZYf@I@n_sa9R@U+0ykfq7->E^MwqS#<2a zxjB{5r`%dji8<+cCXJkbGT9D&uIqOuN3zQLHa(DQQ6|9WZTe=W>1De;1G&z6tv^5R zIqv&J>lv=UvN@X;f41!lM%TpaoJ5DA>)a-xzwo?vi^*FXuOYDV%8vD6@0`Zjrty_; zM6B?9?z7TfoVNo^aeXaq)@W6qFV$3?KTX_q9n`DMcGdO_)o*!!pDDRb-Inj<y*%4;QTU5}Nxd-i83bM&8v@c(W9m&E9O zT34xc@cxEi@0xSPe`ay}GAB0w;2X$ytzAyHXV%OgX~tcU72TyLUgX_86hhEB9Xxh& z>z{lcRc7iD7g#iZLf^}kf6b;0x))svV0zkuD9!*pxKz~PIp0~Z<)}jkGYwE9{eEyF z6{)4o30DLzelYpYL{~=;%S1|uZ5|k>NA95EtUN}q8X!CPTNfZodx?;mBpyhG72RA= zh|rQQuecbv;-ofT57sFr=;H(4>SK#B)wo6oodF}HevM6kIx)nU+XCP86O5)Y&`5a= z8`{)yb@1*bK66Y&02G;k=CSW1nDSJg2_1UIIgBS$u?mcorGW-wG(8x??BpC(DhdIs z8Fy_9C3@76#o>*|_%9aPy0C0=<}F0oVzZRAxl!pvj3)8I+3FJ#P24uTbNOvL{QbCI2M%|r6rDkoiJH8sHXI~rwKxkPkKd1DJ1=( z^a*QIws%A-6;Ow?zJ*j87+Gj_G?x%so7E&uMXGeOr?WbwQWrZZ33Jvn405#);nF>Z zNdHZedZ(4T&`N7 z!xk%l>ye1|9eAM0Wp8ACz=QV+zE0WmZtZntv+(K?+N2#~U>&csGQx?#D<5qutypz3ChR!di5V>P z*|T>|cHT<2Y3MXKyRc&OT1(Ln?Qu^_n??70$giFd2apZ!4Z1++^EHCD!s|S_QI~r+1GAuZW(1zg5$(Y8GJB}!^75k zyVpV$i(b5)dN5I#r$5U&

QjI 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 GUi&{nDYz{F diff --git a/build_scripts/build_dmg.js b/build_scripts/build_dmg.js index baaccc91ab88..e7bbd96b48ef 100644 --- a/build_scripts/build_dmg.js +++ b/build_scripts/build_dmg.js @@ -6,13 +6,13 @@ function getContents(opts) { return [ { x: 466, - y: 344, + y: 280, type: 'link', path: '/Applications', }, { x: 192, - y: 344, + y: 280, type: 'file', path: opts.appPath, } From 018b67f0add720be50ca7ccb102142f263945e81 Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Thu, 3 Mar 2022 05:47:50 +0100 Subject: [PATCH 156/378] improve the picking of free ports for tests (#10491) --- tests/util/socket.py | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/tests/util/socket.py b/tests/util/socket.py index b4ff07d2757b..f5f5bd7fb8cb 100644 --- a/tests/util/socket.py +++ b/tests/util/socket.py @@ -1,11 +1,25 @@ +import random import socket +from typing import Set + +recent_ports: Set[int] = set() def find_available_listen_port(name: str = "free") -> int: - s = socket.socket() - s.bind(("127.0.0.1", 0)) - addr = s.getsockname() - assert addr[1] > 0 - s.close() - print(f"{name} port: {addr[1]}") - return int(addr[1]) + global recent_ports + + while True: + port = random.randint(2000, 65535) + if port in recent_ports: + continue + + s = socket.socket() + try: + s.bind(("127.0.0.1", port)) + except BaseException: + s.close() + continue + s.close() + recent_ports.add(port) + print(f"{name} port: {port}") + return port From 36e95afc24a56839ad46e47b7186a94f7bbaba19 Mon Sep 17 00:00:00 2001 From: Mariano Sorgente <3069354+mariano54@users.noreply.github.com> Date: Thu, 3 Mar 2022 11:36:03 -0500 Subject: [PATCH 157/378] Hide balances until we are synced (#10501) --- chia/cmds/wallet_funcs.py | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index c4759e232a92..e0db35c53112 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -461,20 +461,31 @@ async def print_balances(args: dict, wallet_client: WalletRpcClient, fingerprint config = load_config(DEFAULT_ROOT_PATH, "config.yaml") address_prefix = config["network_overrides"]["config"][config["selected_network"]]["address_prefix"] + is_synced: bool = await wallet_client.get_synced() + is_syncing: bool = await wallet_client.get_sync_status() + print(f"Wallet height: {await wallet_client.get_height_info()}") - print(f"Sync status: {'Synced' if (await wallet_client.get_synced()) else 'Not synced'}") - print(f"Balances, fingerprint: {fingerprint}") - for summary in summaries_response: - wallet_id = summary["id"] - balances = await wallet_client.get_wallet_balance(wallet_id) - typ = WalletType(int(summary["type"])) - address_prefix, scale = wallet_coin_unit(typ, address_prefix) - print(f"Wallet ID {wallet_id} type {typ.name} {summary['name']}") - print(f" -Total Balance: {print_balance(balances['confirmed_wallet_balance'], scale, address_prefix)}") - print( - f" -Pending Total Balance: {print_balance(balances['unconfirmed_wallet_balance'], scale, address_prefix)}" - ) - print(f" -Spendable: {print_balance(balances['spendable_balance'], scale, address_prefix)}") + if is_syncing: + print("Sync status: Syncing...") + elif is_synced: + print("Sync status: Synced") + else: + print("Sync status: Not synced") + + if not is_syncing and is_synced: + print(f"Balances, fingerprint: {fingerprint}") + for summary in summaries_response: + wallet_id = summary["id"] + balances = await wallet_client.get_wallet_balance(wallet_id) + typ = WalletType(int(summary["type"])) + address_prefix, scale = wallet_coin_unit(typ, address_prefix) + print(f"Wallet ID {wallet_id} type {typ.name} {summary['name']}") + print(f" -Total Balance: {print_balance(balances['confirmed_wallet_balance'], scale, address_prefix)}") + print( + f" -Pending Total Balance: " + f"{print_balance(balances['unconfirmed_wallet_balance'], scale, address_prefix)}" + ) + print(f" -Spendable: {print_balance(balances['spendable_balance'], scale, address_prefix)}") print(" ") trusted_peers: Dict = config.get("trusted_peers", {}) From 50857caf8c4d14741de53cdb18f4bccf11921c8f Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 3 Mar 2022 13:27:36 -0500 Subject: [PATCH 158/378] make multiprocessing start method configurable (#10528) * make multiprocessing start method configurable * forkserver * corrections * fixup * optional * more optional * stop attempting anchors in the yaml * rework config handling * comment --- chia/__init__.py | 19 ------------ chia/consensus/blockchain.py | 3 ++ chia/full_node/full_node.py | 31 ++++++++++++++++--- chia/full_node/mempool_manager.py | 15 ++++++++-- chia/full_node/weight_proof.py | 4 +++ chia/timelord/timelord.py | 6 ++++ chia/util/config.py | 35 ++++++++++++++++++++++ chia/util/initial-config.yaml | 6 ++++ chia/wallet/wallet_state_manager.py | 11 ++++++- chia/wallet/wallet_weight_proof_handler.py | 3 ++ 10 files changed, 107 insertions(+), 26 deletions(-) diff --git a/chia/__init__.py b/chia/__init__.py index 9c099f5d0b48..c136c9c3d63f 100644 --- a/chia/__init__.py +++ b/chia/__init__.py @@ -1,24 +1,5 @@ -import multiprocessing - from pkg_resources import DistributionNotFound, get_distribution, resource_filename -# The default multiprocessing start method on Linux has resulted in various issues. -# Several have been around resources being inherited by the worker processes resulting -# in ports, files, or streams, being held open unexpectedly. This can also affect -# memory used by the subprocesses and such. - -start_method = "spawn" -try: - # Set the start method. This may already have been done by the test suite. - multiprocessing.set_start_method(start_method) -except RuntimeError: - # Setting can fail if it has already been done. We do not care about the failure - # if the start method is what we want it to be anyways. - if multiprocessing.get_start_method(allow_none=True) != start_method: - # The start method is not what we wanted. We do not want to continue with - # this without further consideration. - raise - try: __version__ = get_distribution("chia-blockchain").version except DistributionNotFound: diff --git a/chia/consensus/blockchain.py b/chia/consensus/blockchain.py index 24780850c5fd..7af1cbb8ca34 100644 --- a/chia/consensus/blockchain.py +++ b/chia/consensus/blockchain.py @@ -5,6 +5,7 @@ import traceback from concurrent.futures.process import ProcessPoolExecutor from enum import Enum +from multiprocessing.context import BaseContext from pathlib import Path from typing import Dict, List, Optional, Set, Tuple @@ -105,6 +106,7 @@ async def create( hint_store: HintStore, blockchain_dir: Path, reserved_cores: int, + multiprocessing_context: Optional[BaseContext] = None, ): """ Initializes a blockchain with the BlockRecords from disk, assuming they have all been @@ -120,6 +122,7 @@ async def create( num_workers = max(cpu_count - reserved_cores, 1) self.pool = ProcessPoolExecutor( max_workers=num_workers, + mp_context=multiprocessing_context, initializer=setproctitle, initargs=(f"{getproctitle()}_worker",), ) diff --git a/chia/full_node/full_node.py b/chia/full_node/full_node.py index 5aeb608634e9..0c4e2c534a60 100644 --- a/chia/full_node/full_node.py +++ b/chia/full_node/full_node.py @@ -2,6 +2,8 @@ import contextlib import dataclasses import logging +import multiprocessing +from multiprocessing.context import BaseContext import random import time import traceback @@ -63,7 +65,7 @@ from chia.util.bech32m import encode_puzzle_hash from chia.util.check_fork_next_block import check_fork_next_block from chia.util.condition_tools import pkm_pairs -from chia.util.config import PEER_DB_PATH_KEY_DEPRECATED +from chia.util.config import PEER_DB_PATH_KEY_DEPRECATED, process_config_start_method from chia.util.db_wrapper import DBWrapper from chia.util.errors import ConsensusError, Err, ValidationError from chia.util.ints import uint8, uint32, uint64, uint128 @@ -95,6 +97,7 @@ class FullNode: state_changed_callback: Optional[Callable] timelord_lock: asyncio.Lock initialized: bool + multiprocessing_start_context: Optional[BaseContext] weight_proof_handler: Optional[WeightProofHandler] _ui_tasks: Set[asyncio.Task] _blockchain_lock_queue: LockQueue @@ -126,6 +129,10 @@ def __init__( self.compact_vdf_requests: Set[bytes32] = set() self.log = logging.getLogger(name if name else __name__) + # TODO: Logging isn't setup yet so the log entries related to parsing the + # config would end up on stdout if handled here. + self.multiprocessing_context = None + # Used for metrics self.dropped_tx: Set[bytes32] = set() self.not_dropped_tx = 0 @@ -185,10 +192,22 @@ def sql_trace_callback(req: str): self.log.info("Initializing blockchain from disk") start_time = time.time() reserved_cores = self.config.get("reserved_cores", 0) + multiprocessing_start_method = process_config_start_method(config=self.config, log=self.log) + self.multiprocessing_context = multiprocessing.get_context(method=multiprocessing_start_method) self.blockchain = await Blockchain.create( - self.coin_store, self.block_store, self.constants, self.hint_store, self.db_path.parent, reserved_cores + coin_store=self.coin_store, + block_store=self.block_store, + consensus_constants=self.constants, + hint_store=self.hint_store, + blockchain_dir=self.db_path.parent, + reserved_cores=reserved_cores, + multiprocessing_context=self.multiprocessing_context, + ) + self.mempool_manager = MempoolManager( + coin_store=self.coin_store, + consensus_constants=self.constants, + multiprocessing_context=self.multiprocessing_context, ) - self.mempool_manager = MempoolManager(self.coin_store, self.constants) # Blocks are validated under high priority, and transactions under low priority. This guarantees blocks will # be validated first. @@ -285,7 +304,11 @@ async def _handle_transactions(self): raise async def initialize_weight_proof(self): - self.weight_proof_handler = WeightProofHandler(self.constants, self.blockchain) + self.weight_proof_handler = WeightProofHandler( + constants=self.constants, + blockchain=self.blockchain, + multiprocessing_context=self.multiprocessing_context, + ) peak = self.blockchain.get_peak() if peak is not None: await self.weight_proof_handler.create_sub_epoch_segments() diff --git a/chia/full_node/mempool_manager.py b/chia/full_node/mempool_manager.py index 942ef1991188..fa52d06fe034 100644 --- a/chia/full_node/mempool_manager.py +++ b/chia/full_node/mempool_manager.py @@ -2,6 +2,7 @@ import collections import dataclasses import logging +from multiprocessing.context import BaseContext import time from concurrent.futures.process import ProcessPoolExecutor from typing import Dict, List, Optional, Set, Tuple @@ -78,7 +79,12 @@ def validate_clvm_and_signature( class MempoolManager: - def __init__(self, coin_store: CoinStore, consensus_constants: ConsensusConstants): + def __init__( + self, + coin_store: CoinStore, + consensus_constants: ConsensusConstants, + multiprocessing_context: Optional[BaseContext] = None, + ): self.constants: ConsensusConstants = consensus_constants self.constants_json = recurse_jsonify(dataclasses.asdict(self.constants)) @@ -99,7 +105,12 @@ def __init__(self, coin_store: CoinStore, consensus_constants: ConsensusConstant # Transactions that were unable to enter mempool, used for retry. (they were invalid) self.potential_cache = PendingTxCache(self.constants.MAX_BLOCK_COST_CLVM * 1) self.seen_cache_size = 10000 - self.pool = ProcessPoolExecutor(max_workers=2, initializer=setproctitle, initargs=(f"{getproctitle()}_worker",)) + self.pool = ProcessPoolExecutor( + max_workers=2, + mp_context=multiprocessing_context, + initializer=setproctitle, + initargs=(f"{getproctitle()}_worker",), + ) # The mempool will correspond to a certain peak self.peak: Optional[BlockRecord] = None diff --git a/chia/full_node/weight_proof.py b/chia/full_node/weight_proof.py index c6d1ad7ab6e7..1fac85317571 100644 --- a/chia/full_node/weight_proof.py +++ b/chia/full_node/weight_proof.py @@ -2,6 +2,7 @@ import dataclasses import logging import math +from multiprocessing.context import BaseContext import pathlib import random from concurrent.futures.process import ProcessPoolExecutor @@ -60,6 +61,7 @@ def __init__( self, constants: ConsensusConstants, blockchain: BlockchainInterface, + multiprocessing_context: Optional[BaseContext] = None, ): self.tip: Optional[bytes32] = None self.proof: Optional[WeightProof] = None @@ -67,6 +69,7 @@ def __init__( self.blockchain = blockchain self.lock = asyncio.Lock() self._num_processes = 4 + self.multiprocessing_context = multiprocessing_context async def get_proof_of_weight(self, tip: bytes32) -> Optional[WeightProof]: @@ -624,6 +627,7 @@ async def validate_weight_proof(self, weight_proof: WeightProof) -> Tuple[bool, # TODO: Consider implementing an async polling closer for the executor. with ProcessPoolExecutor( max_workers=self._num_processes, + mp_context=self.multiprocessing_context, initializer=setproctitle, initargs=(f"{getproctitle()}_worker",), ) as executor: diff --git a/chia/timelord/timelord.py b/chia/timelord/timelord.py index e74bd998b83c..e3fd5ae58a1b 100644 --- a/chia/timelord/timelord.py +++ b/chia/timelord/timelord.py @@ -2,6 +2,7 @@ import dataclasses import io import logging +import multiprocessing import os import random import time @@ -32,6 +33,7 @@ from chia.types.blockchain_format.sub_epoch_summary import SubEpochSummary from chia.types.blockchain_format.vdf import VDFInfo, VDFProof from chia.types.end_of_slot_bundle import EndOfSubSlotBundle +from chia.util.config import process_config_start_method from chia.util.ints import uint8, uint16, uint32, uint64, uint128 from chia.util.setproctitle import getproctitle, setproctitle from chia.util.streamable import Streamable, streamable @@ -100,6 +102,9 @@ def __init__(self, root_path, config: Dict, constants: ConsensusConstants): # Used to label proofs in `finished_proofs` and to only filter proofs corresponding to the most recent state. self.num_resets: int = 0 + multiprocessing_start_method = process_config_start_method(config=self.config, log=log) + self.multiprocessing_context = multiprocessing.get_context(method=multiprocessing_start_method) + self.process_communication_tasks: List[asyncio.Task] = [] self.main_loop = None self.vdf_server = None @@ -135,6 +140,7 @@ async def _start(self): workers = self.config.get("slow_bluebox_process_count", 1) self.bluebox_pool = ProcessPoolExecutor( max_workers=workers, + mp_context=self.multiprocessing_context, initializer=setproctitle, initargs=(f"{getproctitle()}_worker",), ) diff --git a/chia/util/config.py b/chia/util/config.py index 6e702adae265..3cbb2e064b03 100644 --- a/chia/util/config.py +++ b/chia/util/config.py @@ -1,4 +1,5 @@ import argparse +import logging import os import shutil import sys @@ -7,6 +8,7 @@ import pkg_resources import yaml +from typing_extensions import Literal from chia.util.path import mkdir @@ -162,3 +164,36 @@ def traverse_dict(d: Dict, key_path: str) -> Any: return val else: raise KeyError(f"value not found for key: {key}") + + +start_methods: Dict[str, Optional[Literal["fork", "forkserver", "spawn"]]] = { + "default": None, + "fork": "fork", + "forkserver": "forkserver", + "spawn": "spawn", +} + + +def process_config_start_method( + config: Dict[str, Any], + log=logging.Logger, +) -> Optional[Literal["fork", "forkserver", "spawn"]]: + from_config = config.get("multiprocessing_start_method") + + # handle not only the key being missing, but also set to None + if from_config is None: + from_config = "default" + + processed_method = start_methods[from_config] + + if processed_method is None: + start_methods_string = ", ".join(option for option in start_methods.keys()) + log.warning( + f"Using default multiprocessing start method, configured start method {from_config!r} not available in:" + f" {start_methods_string}" + ) + return None + + log.info(f"Chosen multiprocessing start method: {processed_method}") + + return processed_method diff --git a/chia/util/initial-config.yaml b/chia/util/initial-config.yaml index 43bd7f154ca6..df2ceb32e9dc 100644 --- a/chia/util/initial-config.yaml +++ b/chia/util/initial-config.yaml @@ -305,6 +305,8 @@ timelord: # If `slow_bluebox` is True, launches `slow_bluebox_process_count` processes. slow_bluebox_process_count: 1 + multiprocessing_start_method: default + start_rpc_server: True rpc_port: 8557 @@ -340,6 +342,8 @@ full_node: simulator_peer_db_path: sim_db/peer_table_node.sqlite simulator_peers_file_path: sim_db/peer_table_node.dat + multiprocessing_start_method: default + # If True, starts an RPC server at the following port start_rpc_server: True rpc_port: 8555 @@ -484,6 +488,8 @@ wallet: host: *self_hostname port: 8444 + multiprocessing_start_method: default + testing: False # v2 used by the light wallet sync protocol database_path: wallet/db/blockchain_wallet_v2_CHALLENGE_KEY.sqlite diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 052be2af4761..3230a0d4797f 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -1,6 +1,8 @@ import asyncio import json import logging +import multiprocessing +import multiprocessing.context import time from collections import defaultdict from pathlib import Path @@ -24,6 +26,7 @@ from chia.types.full_block import FullBlock from chia.types.mempool_inclusion_status import MempoolInclusionStatus from chia.util.byte_types import hexstr_to_bytes +from chia.util.config import process_config_start_method from chia.util.db_wrapper import DBWrapper from chia.util.errors import Err from chia.util.ints import uint32, uint64, uint128, uint8 @@ -101,6 +104,7 @@ class WalletStateManager: sync_store: WalletSyncStore finished_sync_up_to: uint32 interested_store: WalletInterestedStore + multiprocessing_context: multiprocessing.context.BaseContext weight_proof_handler: WalletWeightProofHandler server: ChiaServer root_path: Path @@ -152,7 +156,12 @@ async def create( self.sync_mode = False self.sync_target = uint32(0) self.finished_sync_up_to = uint32(0) - self.weight_proof_handler = WalletWeightProofHandler(self.constants) + multiprocessing_start_method = process_config_start_method(config=self.config, log=self.log) + self.multiprocessing_context = multiprocessing.get_context(method=multiprocessing_start_method) + self.weight_proof_handler = WalletWeightProofHandler( + constants=self.constants, + multiprocessing_context=self.multiprocessing_context, + ) self.blockchain = await WalletBlockchain.create(self.basic_store, self.constants, self.weight_proof_handler) self.state_changed_callback = None diff --git a/chia/wallet/wallet_weight_proof_handler.py b/chia/wallet/wallet_weight_proof_handler.py index 6d62bad39e1a..b3e614654780 100644 --- a/chia/wallet/wallet_weight_proof_handler.py +++ b/chia/wallet/wallet_weight_proof_handler.py @@ -4,6 +4,7 @@ import random import tempfile from concurrent.futures.process import ProcessPoolExecutor +from multiprocessing.context import BaseContext from typing import IO, List, Tuple, Optional from chia.consensus.block_record import BlockRecord @@ -42,12 +43,14 @@ class WalletWeightProofHandler: def __init__( self, constants: ConsensusConstants, + multiprocessing_context: BaseContext, ): self._constants = constants self._num_processes = 4 self._executor_shutdown_tempfile: IO = _create_shutdown_file() self._executor: ProcessPoolExecutor = ProcessPoolExecutor( self._num_processes, + mp_context=multiprocessing_context, initializer=setproctitle, initargs=(f"{getproctitle()}_worker",), ) From 841754b44494451a9e3e537575eeec431fe533d1 Mon Sep 17 00:00:00 2001 From: Chris Marslender Date: Thu, 3 Mar 2022 12:28:02 -0600 Subject: [PATCH 159/378] Update URL for direct download of windows whl for upnp (#10540) --- build_scripts/build_windows.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build_scripts/build_windows.ps1 b/build_scripts/build_windows.ps1 index 58af0d585d2c..52a2a14b63be 100644 --- a/build_scripts/build_windows.ps1 +++ b/build_scripts/build_windows.ps1 @@ -10,7 +10,8 @@ git status Write-Output " ---" Write-Output "curl miniupnpc" Write-Output " ---" -Invoke-WebRequest -Uri "https://pypi.chia.net/simple/miniupnpc/miniupnpc-2.2.2-cp39-cp39-win_amd64.whl" -OutFile "miniupnpc-2.2.2-cp39-cp39-win_amd64.whl" +# download.chia.net is the CDN url behind all the files that are actually on pypi.chia.net/simple now +Invoke-WebRequest -Uri "https://download.chia.net/simple/miniupnpc/miniupnpc-2.2.2-cp39-cp39-win_amd64.whl" -OutFile "miniupnpc-2.2.2-cp39-cp39-win_amd64.whl" Write-Output "Using win_amd64 python 3.9 wheel from https://github.com/miniupnp/miniupnp/pull/475 (2.2.0-RC1)" Write-Output "Actual build from https://github.com/miniupnp/miniupnp/commit/7783ac1545f70e3341da5866069bde88244dd848" If ($LastExitCode -gt 0){ From 8cf35716f5344a4e8b869d6c153f5574dd86b76b Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Thu, 3 Mar 2022 14:06:00 -0700 Subject: [PATCH 160/378] Add incoming tx records when cancelling an offer (#10538) * Add incoming tx records when cancelling an offer * show fee on all txs --- chia/wallet/trade_manager.py | 24 +++++++++++++++++++++++- tests/wallet/cat_wallet/test_trades.py | 10 ++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/chia/wallet/trade_manager.py b/chia/wallet/trade_manager.py index b66acc9898bc..50d6913e2b4c 100644 --- a/chia/wallet/trade_manager.py +++ b/chia/wallet/trade_manager.py @@ -212,8 +212,30 @@ async def cancel_pending_offer_safely( all_txs.append(tx) fee_to_pay = uint64(0) + cancellation_addition = Coin(coin.name(), new_ph, coin.amount) + all_txs.append( + TransactionRecord( + confirmed_at_height=uint32(0), + created_at_time=uint64(int(time.time())), + to_puzzle_hash=new_ph, + amount=coin.amount, + fee_amount=fee, + confirmed=False, + sent=uint32(10), + spend_bundle=None, + additions=[cancellation_addition], + removals=[coin], + wallet_id=wallet.id(), + sent_to=[], + trade_id=None, + type=uint32(TransactionType.INCOMING_TX.value), + name=cancellation_addition.name(), + memos=[], + ) + ) + for tx in all_txs: - await self.wallet_state_manager.add_pending_transaction(tx_record=tx) + await self.wallet_state_manager.add_pending_transaction(tx_record=dataclasses.replace(tx, fee_amount=fee)) await self.trade_store.set_status(trade_id, TradeStatus.PENDING_CANCEL, False) diff --git a/tests/wallet/cat_wallet/test_trades.py b/tests/wallet/cat_wallet/test_trades.py index cc191fc2db6d..817a311f3e2a 100644 --- a/tests/wallet/cat_wallet/test_trades.py +++ b/tests/wallet/cat_wallet/test_trades.py @@ -13,6 +13,7 @@ from chia.wallet.trading.offer import Offer from chia.wallet.trading.trade_status import TradeStatus from chia.wallet.transaction_record import TransactionRecord +from chia.wallet.util.transaction_type import TransactionType from tests.pools.test_pool_rpc import wallet_is_synced from tests.setup_nodes import self_hostname, setup_simulators_and_wallets from tests.time_out_assert import time_out_assert @@ -481,6 +482,15 @@ async def get_trade_and_status(trade_manager, trade) -> TradeStatus: if tx.spend_bundle is not None: await time_out_assert(15, tx_in_pool, True, full_node.full_node.mempool_manager, tx.spend_bundle.name()) + sum_of_outgoing = uint64(0) + sum_of_incoming = uint64(0) + for tx in txs: + if tx.type == TransactionType.OUTGOING_TX.value: + sum_of_outgoing = uint64(sum_of_outgoing + tx.amount) + elif tx.type == TransactionType.INCOMING_TX.value: + sum_of_incoming = uint64(sum_of_incoming + tx.amount) + assert (sum_of_outgoing - sum_of_incoming) == 0 + for i in range(1, buffer_blocks): await full_node.farm_new_transaction_block(FarmNewBlockProtocol(token_bytes())) From aa6d4a7fb4db08df13c8e7664aa546cb691a5fbd Mon Sep 17 00:00:00 2001 From: dustinface <35775977+xdustinface@users.noreply.github.com> Date: Fri, 4 Mar 2022 01:41:50 +0100 Subject: [PATCH 161/378] wallet: Reduce log level for `Pulled from queue` message (#10529) --- chia/wallet/wallet_node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index dd636ac56118..9ddddf96900f 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -355,7 +355,7 @@ async def _process_new_subscriptions(self): try: peer, item = None, None item = await self.new_peak_queue.get() - self.log.info(f"Pulled from queue: {item}") + self.log.debug(f"Pulled from queue: {item}") assert item is not None if item.item_type == NewPeakQueueTypes.COIN_ID_SUBSCRIPTION: # Subscriptions are the highest priority, because we don't want to process any more peaks or From 5ac986c004125c0eaf5015c30135b25ad47e8ecb Mon Sep 17 00:00:00 2001 From: dustinface <35775977+xdustinface@users.noreply.github.com> Date: Fri, 4 Mar 2022 01:42:48 +0100 Subject: [PATCH 162/378] pools: Drop redundant `PoolWalletInfo.from_json_dict` (#10524) It exists the same way in its base class `Streamable`. --- chia/pools/pool_wallet_info.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/chia/pools/pool_wallet_info.py b/chia/pools/pool_wallet_info.py index 6437baf30e1a..42c5e4aebc9c 100644 --- a/chia/pools/pool_wallet_info.py +++ b/chia/pools/pool_wallet_info.py @@ -1,6 +1,6 @@ from dataclasses import dataclass from enum import IntEnum -from typing import Optional, Dict, Any +from typing import Optional, Dict from blspy import G1Element @@ -10,7 +10,7 @@ from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.byte_types import hexstr_to_bytes from chia.util.ints import uint32, uint8 -from chia.util.streamable import streamable, Streamable, dataclass_from_dict +from chia.util.streamable import streamable, Streamable class PoolSingletonState(IntEnum): @@ -113,7 +113,3 @@ class PoolWalletInfo(Streamable): current_inner: Program # Inner puzzle in current singleton, not revealed yet tip_singleton_coin_id: bytes32 singleton_block_height: uint32 # Block height that current PoolState is from - - @classmethod - def from_json_dict(cls: Any, json_dict: Dict) -> Any: - return dataclass_from_dict(cls, json_dict) From c6978f07b1e056e5486faec913707ea1a5d3078e Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 4 Mar 2022 10:46:54 -0500 Subject: [PATCH 163/378] correct multiprocessing start method logging, add python_default (#10547) * correct multiprocessing start method logging, add python_default * todo -> regular comment --- chia/util/config.py | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/chia/util/config.py b/chia/util/config.py index 3cbb2e064b03..589d7383a49a 100644 --- a/chia/util/config.py +++ b/chia/util/config.py @@ -166,8 +166,11 @@ def traverse_dict(d: Dict, key_path: str) -> Any: raise KeyError(f"value not found for key: {key}") -start_methods: Dict[str, Optional[Literal["fork", "forkserver", "spawn"]]] = { +method_strings = Literal["default", "python_default", "fork", "forkserver", "spawn"] +method_values = Optional[Literal["fork", "forkserver", "spawn"]] +start_methods: Dict[method_strings, method_values] = { "default": None, + "python_default": None, "fork": "fork", "forkserver": "forkserver", "spawn": "spawn", @@ -177,23 +180,23 @@ def traverse_dict(d: Dict, key_path: str) -> Any: def process_config_start_method( config: Dict[str, Any], log=logging.Logger, -) -> Optional[Literal["fork", "forkserver", "spawn"]]: - from_config = config.get("multiprocessing_start_method") +) -> method_values: + from_config: object = config.get("multiprocessing_start_method") - # handle not only the key being missing, but also set to None + choice: method_strings if from_config is None: - from_config = "default" - - processed_method = start_methods[from_config] - - if processed_method is None: + # handle not only the key being missing, but also set to None + choice = "default" + elif from_config not in start_methods.keys(): start_methods_string = ", ".join(option for option in start_methods.keys()) - log.warning( - f"Using default multiprocessing start method, configured start method {from_config!r} not available in:" - f" {start_methods_string}" - ) - return None + log.warning(f"Configured start method {from_config!r} not available in: {start_methods_string}") + choice = "default" + else: + # mypy doesn't realize that by the time we get here from_config must be one of + # the keys in `start_methods` due to the above `not in` condition. + choice = from_config # type: ignore[assignment] - log.info(f"Chosen multiprocessing start method: {processed_method}") + processed_method = start_methods[choice] + log.info(f"Selected multiprocessing start method: {choice}") return processed_method From cce361c0c6b1961e3adc29ed07bb9a261b757743 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 4 Mar 2022 10:53:50 -0500 Subject: [PATCH 164/378] correct some comments to refer to CATs (#10548) * correct some comments to refer to CATs * one more * and in a test --- chia/rpc/wallet_rpc_api.py | 4 ++-- chia/wallet/wallet.py | 2 +- chia/wallet/wallet_action_store.py | 2 +- tests/wallet/rpc/test_wallet_rpc.py | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 5e32623680d0..cc0d58b593ca 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -86,7 +86,7 @@ def get_routes(self) -> Dict[str, Callable]: "/get_farmed_amount": self.get_farmed_amount, "/create_signed_transaction": self.create_signed_transaction, "/delete_unconfirmed_transactions": self.delete_unconfirmed_transactions, - # Coloured coins and trading + # CATs and trading "/cat_set_name": self.cat_set_name, "/cat_asset_id_to_name": self.cat_asset_id_to_name, "/cat_get_name": self.cat_get_name, @@ -811,7 +811,7 @@ async def delete_unconfirmed_transactions(self, request): return {} ########################################################################################## - # Coloured Coins and Trading + # CATs and Trading ########################################################################################## async def get_cat_list(self, request): diff --git a/chia/wallet/wallet.py b/chia/wallet/wallet.py index 583707f35da4..0df7c795ec92 100644 --- a/chia/wallet/wallet.py +++ b/chia/wallet/wallet.py @@ -504,7 +504,7 @@ async def push_transaction(self, tx: TransactionRecord) -> None: await self.wallet_state_manager.add_pending_transaction(tx) await self.wallet_state_manager.wallet_node.update_ui() - # This is to be aggregated together with a coloured coin offer to ensure that the trade happens + # This is to be aggregated together with a CAT offer to ensure that the trade happens async def create_spend_bundle_relative_chia(self, chia_amount: int, exclude: List[Coin]) -> SpendBundle: list_of_solutions = [] utxos = None diff --git a/chia/wallet/wallet_action_store.py b/chia/wallet/wallet_action_store.py index 59f2c3f05c0c..f50af3229b8d 100644 --- a/chia/wallet/wallet_action_store.py +++ b/chia/wallet/wallet_action_store.py @@ -11,7 +11,7 @@ class WalletActionStore: """ WalletActionStore keeps track of all wallet actions that require persistence. - Used by Colored coins, Atomic swaps, Rate Limited, and Authorized payee wallets + Used by CATs, Atomic swaps, Rate Limited, and Authorized payee wallets """ db_connection: aiosqlite.Connection diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index 630688e74007..0666a9768995 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -437,8 +437,8 @@ async def eventual_balance_det(c, wallet_id: str): res = await client_2.create_wallet_for_existing_cat(asset_id) assert res["success"] cat_1_id = res["wallet_id"] - colour_1 = bytes.fromhex(res["asset_id"]) - assert colour_1 == asset_id + cat_1_asset_id = bytes.fromhex(res["asset_id"]) + assert cat_1_asset_id == asset_id await asyncio.sleep(1) for i in range(0, 5): From 7211d9d4f049be57277b3a01d30b544ae2124d20 Mon Sep 17 00:00:00 2001 From: Mariano Sorgente <3069354+mariano54@users.noreply.github.com> Date: Fri, 4 Mar 2022 11:25:22 -0500 Subject: [PATCH 165/378] Detect hints correctly in the TX (#10543) * Detect hints correctly in the TX * Only support list hints --- chia/wallet/util/compute_hints.py | 36 ++++++++++--------------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/chia/wallet/util/compute_hints.py b/chia/wallet/util/compute_hints.py index b6d924bbdcbf..36d35ebd1865 100644 --- a/chia/wallet/util/compute_hints.py +++ b/chia/wallet/util/compute_hints.py @@ -1,34 +1,20 @@ from typing import List -from blspy import G2Element - +from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.condition_opcodes import ConditionOpcode from chia.types.blockchain_format.program import INFINITE_COST from chia.types.coin_spend import CoinSpend -from chia.full_node.mempool_check_conditions import get_name_puzzle_conditions -from chia.consensus.default_constants import DEFAULT_CONSTANTS -from chia.types.spend_bundle import SpendBundle -from chia.full_node.bundle_tools import simple_solution_generator - - -def compute_coin_hints(cs: CoinSpend) -> List[bytes]: - bundle = SpendBundle([cs], G2Element()) - generator = simple_solution_generator(bundle) - npc_result = get_name_puzzle_conditions( - generator, - INFINITE_COST, - cost_per_byte=DEFAULT_CONSTANTS.COST_PER_BYTE, - mempool_mode=False, - height=DEFAULT_CONSTANTS.SOFT_FORK_HEIGHT, - ) - h_list = [] - for npc in npc_result.npc_list: - for opcode, conditions in npc.conditions: - if opcode == ConditionOpcode.CREATE_COIN: - for condition in conditions: - if len(condition.vars) > 2 and condition.vars[2] != b"": - h_list.append(condition.vars[2]) +def compute_coin_hints(cs: CoinSpend) -> List[bytes32]: + _, result_program = cs.puzzle_reveal.run_with_cost(INFINITE_COST, cs.solution) + h_list: List[bytes32] = [] + for condition_data in result_program.as_python(): + condition = condition_data[0] + args = condition_data[1:] + if condition == ConditionOpcode.CREATE_COIN and len(args) > 2: + if isinstance(args[2], list): + if isinstance(args[2][0], bytes): + h_list.append(bytes32(args[2][0])) return h_list From 05f96670181654415b1ad219d38463228f30b0ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Or=C5=A1uli=C4=87?= Date: Fri, 4 Mar 2022 17:27:15 +0100 Subject: [PATCH 166/378] Allow CAT autodiscovery, refactor CAT default naming (#10308) * Allow CAT autodiscovery * Add wallet autodiscovery test * Use dict get for automatically_add_unknown_cats with default false * Refactor name generation for new CATs into one place * Remove hardcoded default cat wallet name from wallet rpc test * initial-config.yaml comment nit --- chia/rpc/wallet_rpc_api.py | 3 ++- chia/util/initial-config.yaml | 4 ++++ chia/wallet/cat_wallet/cat_wallet.py | 23 ++++++++++++++++++++-- chia/wallet/wallet_state_manager.py | 4 +++- tests/wallet/cat_wallet/test_cat_wallet.py | 20 ++++++++++++++----- tests/wallet/rpc/test_wallet_rpc.py | 5 ++++- 6 files changed, 49 insertions(+), 10 deletions(-) diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index cc0d58b593ca..dc88e0cf7255 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -429,7 +429,8 @@ async def create_new_wallet(self, request: Dict): fee = uint64(request.get("fee", 0)) if request["wallet_type"] == "cat_wallet": - name = request.get("name", "CAT Wallet") + # If not provided, the name will be autogenerated based on the tail hash. + name = request.get("name", None) if request["mode"] == "new": async with self.service.wallet_state_manager.lock: cat_wallet: CATWallet = await CATWallet.create_new_cat_wallet( diff --git a/chia/util/initial-config.yaml b/chia/util/initial-config.yaml index df2ceb32e9dc..edaf821f481c 100644 --- a/chia/util/initial-config.yaml +++ b/chia/util/initial-config.yaml @@ -528,3 +528,7 @@ wallet: # timeout for weight proof request weight_proof_timeout: *weight_proof_timeout + + # 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 diff --git a/chia/wallet/cat_wallet/cat_wallet.py b/chia/wallet/cat_wallet/cat_wallet.py index eec51c2c8377..38a8e373fc71 100644 --- a/chia/wallet/cat_wallet/cat_wallet.py +++ b/chia/wallet/cat_wallet/cat_wallet.py @@ -64,9 +64,13 @@ class CATWallet: cost_of_single_tx: Optional[int] lineage_store: CATLineageStore + @staticmethod + def default_wallet_name_for_unknown_cat(limitations_program_hash_hex: str) -> str: + return f"CAT {limitations_program_hash_hex[:16]}..." + @staticmethod async def create_new_cat_wallet( - wallet_state_manager: Any, wallet: Wallet, cat_tail_info: Dict[str, Any], amount: uint64, name="CAT WALLET" + wallet_state_manager: Any, wallet: Wallet, cat_tail_info: Dict[str, Any], amount: uint64, name=None ): self = CATWallet() self.cost_of_single_tx = None @@ -82,6 +86,12 @@ async def create_new_cat_wallet( empty_bytes = bytes32(32 * b"\0") self.cat_info = CATInfo(empty_bytes, None) info_as_string = bytes(self.cat_info).hex() + # If the name is not provided, it will be autogenerated based on the resulting tail hash. + # For now, give the wallet a temporary name "CAT WALLET" until we get the tail hash + original_name = name + if name is None: + name = "CAT WALLET" + self.wallet_info = await wallet_state_manager.user_store.create_wallet(name, WalletType.CAT, info_as_string) if self.wallet_info is None: raise ValueError("Internal Error") @@ -106,6 +116,13 @@ async def create_new_cat_wallet( await self.wallet_state_manager.add_new_wallet(self, self.id()) + # If the new CAT name wasn't originally provided, we used a temporary name before issuance + # since we didn't yet know the TAIL. Now we know the TAIL, we can update the name + # according to the template name for unknown/new CATs. + if original_name is None: + name = self.default_wallet_name_for_unknown_cat(self.cat_info.limitations_program_hash.hex()) + await self.set_name(name) + # Change and actual CAT coin non_ephemeral_coins: List[Coin] = spend_bundle.not_ephemeral_additions() cat_coin = None @@ -147,7 +164,7 @@ async def create_new_cat_wallet( @staticmethod async def create_wallet_for_cat( - wallet_state_manager: Any, wallet: Wallet, limitations_program_hash_hex: str, name="CAT WALLET" + wallet_state_manager: Any, wallet: Wallet, limitations_program_hash_hex: str, name=None ) -> CATWallet: self = CATWallet() self.cost_of_single_tx = None @@ -166,6 +183,8 @@ async def create_wallet_for_cat( if limitations_program_hash_hex in DEFAULT_CATS: cat_info = DEFAULT_CATS[limitations_program_hash_hex] name = cat_info["name"] + elif name is None: + name = self.default_wallet_name_for_unknown_cat(limitations_program_hash_hex) limitations_program_hash = bytes32(hexstr_to_bytes(limitations_program_hash_hex)) self.cat_info = CATInfo(limitations_program_hash, None) diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 3230a0d4797f..8135f46d4f42 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -594,7 +594,9 @@ async def fetch_parent_and_check_for_cat( cat_puzzle = construct_cat_puzzle(CAT_MOD, bytes32(bytes(tail_hash)[1:]), our_inner_puzzle) if cat_puzzle.get_tree_hash() != coin_state.coin.puzzle_hash: return None, None - if bytes(tail_hash).hex()[2:] in self.default_cats: + if bytes(tail_hash).hex()[2:] in self.default_cats or self.config.get( + "automatically_add_unknown_cats", False + ): cat_wallet = await CATWallet.create_wallet_for_cat( self, self.main_wallet, bytes(tail_hash).hex()[2:] ) diff --git a/tests/wallet/cat_wallet/test_cat_wallet.py b/tests/wallet/cat_wallet/test_cat_wallet.py index 4bb8c894bc0e..26c96ebdf51d 100644 --- a/tests/wallet/cat_wallet/test_cat_wallet.py +++ b/tests/wallet/cat_wallet/test_cat_wallet.py @@ -645,8 +645,12 @@ async def check_all_there(): "trusted", [True, False], ) + @pytest.mark.parametrize( + "autodiscovery", + [True, False], + ) @pytest.mark.asyncio - async def test_cat_hint(self, two_wallet_nodes, trusted): + async def test_cat_hint(self, two_wallet_nodes, trusted, autodiscovery): num_blocks = 3 full_nodes, wallets = two_wallet_nodes full_node_api = full_nodes[0] @@ -663,6 +667,8 @@ async def test_cat_hint(self, two_wallet_nodes, trusted): else: wallet_node.config["trusted_peers"] = {} wallet_node_2.config["trusted_peers"] = {} + wallet_node.config["automatically_add_unknown_cats"] = autodiscovery + wallet_node_2.config["automatically_add_unknown_cats"] = autodiscovery await server_2.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) await server_3.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) @@ -711,11 +717,15 @@ async def test_cat_hint(self, two_wallet_nodes, trusted): await time_out_assert(15, cat_wallet.get_confirmed_balance, 40) await time_out_assert(15, cat_wallet.get_unconfirmed_balance, 40) - # First we test that no wallet was created async def check_wallets(node): return len(node.wallet_state_manager.wallets.keys()) - await time_out_assert(10, check_wallets, 1, wallet_node_2) + if autodiscovery: + # Autodiscovery enabled: test that wallet was created at this point + await time_out_assert(10, check_wallets, 2, wallet_node_2) + else: + # Autodiscovery disabled: test that no wallet was created + await time_out_assert(10, check_wallets, 1, wallet_node_2) # Then we update the wallet's default CATs wallet_node_2.wallet_state_manager.default_cats = { @@ -742,11 +752,11 @@ async def check_wallets(node): await time_out_assert(15, cat_wallet.get_confirmed_balance, 30) await time_out_assert(15, cat_wallet.get_unconfirmed_balance, 30) - # Now we check that another wallet WAS created + # Now we check that another wallet WAS created, even if autodiscovery was disabled await time_out_assert(10, check_wallets, 2, wallet_node_2) cat_wallet_2 = wallet_node_2.wallet_state_manager.wallets[2] - # Previous balance + balance that triggered creation + # Previous balance + balance that triggered creation in case of disabled autodiscovery await time_out_assert(30, cat_wallet_2.get_confirmed_balance, 70) await time_out_assert(30, cat_wallet_2.get_unconfirmed_balance, 70) diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index 0666a9768995..90b0866c0f53 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -29,6 +29,7 @@ from chia.wallet.derive_keys import master_sk_to_wallet_sk from chia.util.ints import uint16, uint32, uint64 from chia.wallet.cat_wallet.cat_constants import DEFAULT_CATS +from chia.wallet.cat_wallet.cat_wallet import CATWallet from chia.wallet.trading.trade_status import TradeStatus from chia.wallet.transaction_record import TransactionRecord from chia.wallet.transaction_sorting import SortKey @@ -410,7 +411,9 @@ async def eventual_balance_det(c, wallet_id: str): assert bal_0["pending_coin_removal_count"] == 1 col = await client.get_cat_asset_id(cat_0_id) assert col == asset_id - assert (await client.get_cat_name(cat_0_id)) == "CAT Wallet" + assert (await client.get_cat_name(cat_0_id)) == CATWallet.default_wallet_name_for_unknown_cat( + asset_id.hex() + ) await client.set_cat_name(cat_0_id, "My cat") assert (await client.get_cat_name(cat_0_id)) == "My cat" wid, name = await client.cat_asset_id_to_name(col) From cf54aae2b79abbdb0682f0640335d1bb24bd6257 Mon Sep 17 00:00:00 2001 From: Yostra Date: Fri, 4 Mar 2022 12:48:36 -0500 Subject: [PATCH 167/378] Wallet consistancy (#10532) * use db transaction, -1 in synced up to height, delete unused funcitons * use transaction info in key-val-store/pool-store * cat stores * db lock * remove unused lock, set synced not always in transaction * fix store tests Co-authored-by: wjblanke --- chia/pools/pool_wallet.py | 12 +++-- chia/wallet/cat_wallet/cat_wallet.py | 28 ++++++---- chia/wallet/cat_wallet/lineage_store.py | 63 ++++++++++++++-------- chia/wallet/key_val_store.py | 26 ++++++--- chia/wallet/puzzles/genesis_checkers.py | 2 +- chia/wallet/puzzles/tails.py | 2 +- chia/wallet/trade_manager.py | 2 +- chia/wallet/trading/trade_store.py | 39 +++----------- chia/wallet/wallet_blockchain.py | 4 +- chia/wallet/wallet_node.py | 72 +++++++++++++++---------- chia/wallet/wallet_pool_store.py | 58 +++++++++++--------- chia/wallet/wallet_state_manager.py | 23 ++++---- tests/pools/test_wallet_pool_store.py | 12 ++--- 13 files changed, 192 insertions(+), 151 deletions(-) diff --git a/chia/pools/pool_wallet.py b/chia/pools/pool_wallet.py index 881f049e1826..1c22034f6a7f 100644 --- a/chia/pools/pool_wallet.py +++ b/chia/pools/pool_wallet.py @@ -280,7 +280,7 @@ async def apply_state_transition(self, new_state: CoinSpend, block_height: uint3 ) return False - await self.wallet_state_manager.pool_store.add_spend(self.wallet_id, new_state, block_height) + await self.wallet_state_manager.pool_store.add_spend(self.wallet_id, new_state, block_height, True) tip_spend = (await self.get_tip())[1] self.log.info(f"New PoolWallet singleton tip_coin: {tip_spend} farmed at height {block_height}") @@ -350,12 +350,16 @@ async def create( if spend.coin.name() == launcher_coin_id: launcher_spend = spend assert launcher_spend is not None - await self.wallet_state_manager.pool_store.add_spend(self.wallet_id, launcher_spend, block_height) + await self.wallet_state_manager.pool_store.add_spend( + self.wallet_id, launcher_spend, block_height, in_transaction + ) await self.update_pool_config() p2_puzzle_hash: bytes32 = (await self.get_current_state()).p2_singleton_puzzle_hash - await self.wallet_state_manager.add_new_wallet(self, self.wallet_info.id, create_puzzle_hashes=False) - await self.wallet_state_manager.add_interested_puzzle_hashes([p2_puzzle_hash], [self.wallet_id], False) + await self.wallet_state_manager.add_new_wallet( + self, self.wallet_info.id, create_puzzle_hashes=False, in_transaction=in_transaction + ) + await self.wallet_state_manager.add_interested_puzzle_hashes([p2_puzzle_hash], [self.wallet_id], in_transaction) return self @staticmethod diff --git a/chia/wallet/cat_wallet/cat_wallet.py b/chia/wallet/cat_wallet/cat_wallet.py index 38a8e373fc71..6d9f78b6bf7e 100644 --- a/chia/wallet/cat_wallet/cat_wallet.py +++ b/chia/wallet/cat_wallet/cat_wallet.py @@ -164,7 +164,11 @@ async def create_new_cat_wallet( @staticmethod async def create_wallet_for_cat( - wallet_state_manager: Any, wallet: Wallet, limitations_program_hash_hex: str, name=None + wallet_state_manager: Any, + wallet: Wallet, + limitations_program_hash_hex: str, + name=None, + in_transaction=False, ) -> CATWallet: self = CATWallet() self.cost_of_single_tx = None @@ -189,12 +193,16 @@ async def create_wallet_for_cat( limitations_program_hash = bytes32(hexstr_to_bytes(limitations_program_hash_hex)) self.cat_info = CATInfo(limitations_program_hash, None) info_as_string = bytes(self.cat_info).hex() - self.wallet_info = await wallet_state_manager.user_store.create_wallet(name, WalletType.CAT, info_as_string) + self.wallet_info = await wallet_state_manager.user_store.create_wallet( + name, WalletType.CAT, info_as_string, in_transaction=in_transaction + ) if self.wallet_info is None: raise Exception("wallet_info is None") - self.lineage_store = await CATLineageStore.create(self.wallet_state_manager.db_wrapper, self.get_asset_id()) - await self.wallet_state_manager.add_new_wallet(self, self.id()) + self.lineage_store = await CATLineageStore.create( + self.wallet_state_manager.db_wrapper, self.get_asset_id(), in_transaction=in_transaction + ) + await self.wallet_state_manager.add_new_wallet(self, self.id(), in_transaction=in_transaction) return self @staticmethod @@ -220,7 +228,7 @@ async def create( self.cat_info = CATInfo(cat_info.limitations_program_hash, cat_info.my_tail) self.lineage_store = await CATLineageStore.create(self.wallet_state_manager.db_wrapper, self.get_asset_id()) for coin_id, lineage in cat_info.lineage_proofs: - await self.add_lineage(coin_id, lineage) + await self.add_lineage(coin_id, lineage, False) await self.save_info(self.cat_info, False) return self @@ -310,7 +318,7 @@ async def coin_added(self, coin: Coin, height: uint32): inner_puzzle = await self.inner_puzzle_for_cat_puzhash(coin.puzzle_hash) lineage_proof = LineageProof(coin.parent_coin_info, inner_puzzle.get_tree_hash(), coin.amount) - await self.add_lineage(coin.name(), lineage_proof) + await self.add_lineage(coin.name(), lineage_proof, True) lineage = await self.get_lineage_proof_for_coin(coin) @@ -349,7 +357,9 @@ async def puzzle_solution_received(self, coin_spend: CoinSpend): if parent_coin is None: raise ValueError("Error in finding parent") await self.add_lineage( - coin_name, LineageProof(parent_coin.parent_coin_info, inner_puzzle.get_tree_hash(), parent_coin.amount) + coin_name, + LineageProof(parent_coin.parent_coin_info, inner_puzzle.get_tree_hash(), parent_coin.amount), + True, ) else: # The parent is not a CAT which means we need to scrub all of its children from our DB @@ -778,14 +788,14 @@ async def generate_signed_transaction( return tx_list - async def add_lineage(self, name: bytes32, lineage: Optional[LineageProof]): + async def add_lineage(self, name: bytes32, lineage: Optional[LineageProof], in_transaction): """ Lineage proofs are stored as a list of parent coins and the lineage proof you will need if they are the parent of the coin you are trying to spend. 'If I'm your parent, here's the info you need to spend yourself' """ self.log.info(f"Adding parent {name}: {lineage}") if lineage is not None: - await self.lineage_store.add_lineage_proof(name, lineage) + await self.lineage_store.add_lineage_proof(name, lineage, in_transaction) async def remove_lineage(self, name: bytes32): self.log.info(f"Removing parent {name} (probably had a non-CAT parent)") diff --git a/chia/wallet/cat_wallet/lineage_store.py b/chia/wallet/cat_wallet/lineage_store.py index 7d50e009be2a..48a4a4220b94 100644 --- a/chia/wallet/cat_wallet/lineage_store.py +++ b/chia/wallet/cat_wallet/lineage_store.py @@ -22,16 +22,21 @@ class CATLineageStore: table_name: str @classmethod - async def create(cls, db_wrapper: DBWrapper, asset_id: str): + async def create(cls, db_wrapper: DBWrapper, asset_id: str, in_transaction=False): self = cls() self.table_name = f"lineage_proofs_{asset_id}" self.db_wrapper = db_wrapper self.db_connection = self.db_wrapper.db - await self.db_connection.execute( - (f"CREATE TABLE IF NOT EXISTS {self.table_name}(" " coin_id text PRIMARY KEY," " lineage blob)") - ) - - await self.db_connection.commit() + if not in_transaction: + await self.db_wrapper.lock.acquire() + try: + await self.db_connection.execute( + (f"CREATE TABLE IF NOT EXISTS {self.table_name}(" " coin_id text PRIMARY KEY," " lineage blob)") + ) + finally: + if not in_transaction: + await self.db_connection.commit() + self.db_wrapper.lock.release() return self async def close(self): @@ -42,23 +47,35 @@ async def _clear_database(self): await cursor.close() await self.db_connection.commit() - async def add_lineage_proof(self, coin_id: bytes32, lineage: LineageProof) -> None: - cursor = await self.db_connection.execute( - f"INSERT OR REPLACE INTO {self.table_name} VALUES(?, ?)", - (coin_id.hex(), bytes(lineage)), - ) - - await cursor.close() - await self.db_connection.commit() - - async def remove_lineage_proof(self, coin_id: bytes32) -> None: - cursor = await self.db_connection.execute( - f"DELETE FROM {self.table_name} WHERE coin_id=?;", - (coin_id.hex(),), - ) - - await cursor.close() - await self.db_connection.commit() + async def add_lineage_proof(self, coin_id: bytes32, lineage: LineageProof, in_transaction) -> None: + if not in_transaction: + await self.db_wrapper.lock.acquire() + try: + cursor = await self.db_connection.execute( + f"INSERT OR REPLACE INTO {self.table_name} VALUES(?, ?)", + (coin_id.hex(), bytes(lineage)), + ) + + await cursor.close() + finally: + if not in_transaction: + await self.db_connection.commit() + self.db_wrapper.lock.release() + + async def remove_lineage_proof(self, coin_id: bytes32, in_transaction=True) -> None: + if not in_transaction: + await self.db_wrapper.lock.acquire() + try: + cursor = await self.db_connection.execute( + f"DELETE FROM {self.table_name} WHERE coin_id=?;", + (coin_id.hex(),), + ) + + await cursor.close() + finally: + if not in_transaction: + await self.db_connection.commit() + self.db_wrapper.lock.release() async def get_lineage_proof(self, coin_id: bytes32) -> Optional[LineageProof]: diff --git a/chia/wallet/key_val_store.py b/chia/wallet/key_val_store.py index 1fbdd05a37eb..631641a16482 100644 --- a/chia/wallet/key_val_store.py +++ b/chia/wallet/key_val_store.py @@ -46,19 +46,31 @@ async def get_object(self, key: str, object_type: Any) -> Any: return object_type.from_bytes(row[1]) - async def set_object(self, key: str, obj: Any): + async def set_object(self, key: str, obj: Any, in_transaction=False): """ Adds object to key val store. Obj MUST support __bytes__ and bytes() methods. """ - async with self.db_wrapper.lock: + if not in_transaction: + await self.db_wrapper.lock.acquire() + + try: cursor = await self.db_connection.execute( "INSERT OR REPLACE INTO key_val_store VALUES(?, ?)", (key, bytes(obj)), ) await cursor.close() - await self.db_connection.commit() + finally: + if not in_transaction: + await self.db_connection.commit() + self.db_wrapper.lock.release() - async def remove_object(self, key: str): - cursor = await self.db_connection.execute("DELETE FROM key_val_store where key=?", (key,)) - await cursor.close() - await self.db_connection.commit() + async def remove_object(self, key: str, in_transaction=False): + if not in_transaction: + await self.db_wrapper.lock.acquire() + try: + cursor = await self.db_connection.execute("DELETE FROM key_val_store where key=?", (key,)) + await cursor.close() + finally: + if not in_transaction: + await self.db_connection.commit() + self.db_wrapper.lock.release() diff --git a/chia/wallet/puzzles/genesis_checkers.py b/chia/wallet/puzzles/genesis_checkers.py index 5f2bb8f871fb..b21ac5a7f9ac 100644 --- a/chia/wallet/puzzles/genesis_checkers.py +++ b/chia/wallet/puzzles/genesis_checkers.py @@ -72,7 +72,7 @@ async def generate_issuance_bundle(cls, wallet, _: Dict, amount: uint64) -> Tupl origin_id = origin.name() cat_inner: Program = await wallet.get_new_inner_puzzle() - await wallet.add_lineage(origin_id, LineageProof()) + await wallet.add_lineage(origin_id, LineageProof(), False) genesis_coin_checker: Program = cls.construct([Program.to(origin_id)]) minted_cat_puzzle_hash: bytes32 = construct_cat_puzzle( diff --git a/chia/wallet/puzzles/tails.py b/chia/wallet/puzzles/tails.py index b2c9fa960197..4af3590cb395 100644 --- a/chia/wallet/puzzles/tails.py +++ b/chia/wallet/puzzles/tails.py @@ -72,7 +72,7 @@ async def generate_issuance_bundle(cls, wallet, _: Dict, amount: uint64) -> Tupl origin_id = origin.name() cat_inner: Program = await wallet.get_new_inner_puzzle() - await wallet.add_lineage(origin_id, LineageProof()) + await wallet.add_lineage(origin_id, LineageProof(), False) tail: Program = cls.construct([Program.to(origin_id)]) minted_cat_puzzle_hash: bytes32 = construct_cat_puzzle(CAT_MOD, tail.get_tree_hash(), cat_inner).get_tree_hash() diff --git a/chia/wallet/trade_manager.py b/chia/wallet/trade_manager.py index 50d6913e2b4c..dc9d9125d1e3 100644 --- a/chia/wallet/trade_manager.py +++ b/chia/wallet/trade_manager.py @@ -122,7 +122,7 @@ async def coins_of_interest_farmed(self, coin_state: CoinState, fork_height: Opt for tx in tx_records: if TradeStatus(trade.status) == TradeStatus.PENDING_ACCEPT: await self.wallet_state_manager.add_transaction( - dataclasses.replace(tx, confirmed_at_height=height, confirmed=True) + dataclasses.replace(tx, confirmed_at_height=height, confirmed=True), in_transaction=True ) self.log.info(f"Trade with id: {trade.trade_id} confirmed at height: {height}") diff --git a/chia/wallet/trading/trade_store.py b/chia/wallet/trading/trade_store.py index 83bacf39b125..8c0b8cf9533f 100644 --- a/chia/wallet/trading/trade_store.py +++ b/chia/wallet/trading/trade_store.py @@ -205,31 +205,6 @@ async def increment_sent( await self.add_trade_record(tx, False) return True - async def set_not_sent(self, id: bytes32): - """ - Updates trade sent count to 0. - """ - - current: Optional[TradeRecord] = await self.get_trade_record(id) - if current is None: - return None - - tx: TradeRecord = TradeRecord( - confirmed_at_index=current.confirmed_at_index, - accepted_at_time=current.accepted_at_time, - created_at_time=current.created_at_time, - is_my_offer=current.is_my_offer, - sent=uint32(0), - offer=current.offer, - taken_offer=current.taken_offer, - coins_of_interest=current.coins_of_interest, - trade_id=current.trade_id, - status=uint32(TradeStatus.PENDING_CONFIRM.value), - sent_to=[], - ) - - await self.add_trade_record(tx, False) - async def get_trades_count(self) -> Tuple[int, int, int]: """ Returns the number of trades in the database broken down by is_my_offer status @@ -447,10 +422,10 @@ async def get_trades_above(self, height: uint32) -> List[TradeRecord]: return records async def rollback_to_block(self, block_index): - - # Delete from storage - cursor = await self.db_connection.execute( - "DELETE FROM trade_records WHERE confirmed_at_index>?", (block_index,) - ) - await cursor.close() - await self.db_connection.commit() + async with self.db_wrapper.lock: + # Delete from storage + cursor = await self.db_connection.execute( + "DELETE FROM trade_records WHERE confirmed_at_index>?", (block_index,) + ) + await cursor.close() + await self.db_connection.commit() diff --git a/chia/wallet/wallet_blockchain.py b/chia/wallet/wallet_blockchain.py index dcc24575881e..f4ea59d1b9b6 100644 --- a/chia/wallet/wallet_blockchain.py +++ b/chia/wallet/wallet_blockchain.py @@ -184,9 +184,9 @@ async def get_peak_block(self) -> Optional[HeaderBlock]: return self._peak return await self._basic_store.get_object("PEAK_BLOCK", HeaderBlock) - async def set_finished_sync_up_to(self, height: int): + async def set_finished_sync_up_to(self, height: int, in_transaction=False): if height > await self.get_finished_sync_up_to(): - await self._basic_store.set_object("FINISHED_SYNC_UP_TO", uint32(height)) + await self._basic_store.set_object("FINISHED_SYNC_UP_TO", uint32(height), in_transaction) await self.clean_block_records() async def get_finished_sync_up_to(self): diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index 9ddddf96900f..6d3f03851802 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -97,7 +97,6 @@ class WalletNode: node_peaks: Dict[bytes32, Tuple[uint32, bytes32]] validation_semaphore: Optional[asyncio.Semaphore] local_node_synced: bool - new_state_lock: Optional[asyncio.Lock] def __init__( self, @@ -120,7 +119,6 @@ def __init__( self.proof_hashes: List = [] self.state_changed_callback = None self.wallet_state_manager = None - self.new_state_lock = None self.server = None self.wsm_close_task = None self.sync_task: Optional[asyncio.Task] = None @@ -591,12 +589,11 @@ async def receive_state_from_peer( # TODO: optimize fetching if self.validation_semaphore is None: self.validation_semaphore = asyncio.Semaphore(6) - if self.new_state_lock is None: - self.new_state_lock = asyncio.Lock() # If there is a fork, we need to ensure that we roll back in trusted mode to properly handle reorgs if trusted and fork_height is not None and height is not None and fork_height != height - 1: await self.wallet_state_manager.reorg_rollback(fork_height) + await self.wallet_state_manager.blockchain.set_finished_sync_up_to(fork_height) cache: PeerRequestCache = self.get_cache_for_peer(peer) if fork_height is not None: cache.clear_after_height(fork_height) @@ -610,6 +607,7 @@ async def receive_state_from_peer( items = sorted(items_input, key=last_change_height_cs) async def receive_and_validate(inner_states: List[CoinState], inner_idx_start: int, cs_heights: List[uint32]): + assert self.wallet_state_manager is not None try: assert self.validation_semaphore is not None async with self.validation_semaphore: @@ -624,25 +622,37 @@ async def receive_and_validate(inner_states: List[CoinState], inner_idx_start: i if await self.validate_received_state_from_peer(inner_state, peer, cache, fork_height) ] if len(valid_states) > 0: - assert self.new_state_lock is not None - async with self.new_state_lock: + async with self.wallet_state_manager.db_wrapper.lock: self.log.info( f"new coin state received ({inner_idx_start}-" f"{inner_idx_start + len(inner_states) - 1}/ {len(items)})" ) if self.wallet_state_manager is None: return - await self.wallet_state_manager.new_coin_state(valid_states, peer, fork_height) - - if update_finished_height: - if len(cs_heights) == 1: - # We have processed all past tasks, so we can increase the height safely - synced_up_to = last_change_height_cs(valid_states[-1]) - 1 - else: - # We know we have processed everything before this min height - synced_up_to = min(cs_heights) - await self.wallet_state_manager.blockchain.set_finished_sync_up_to(synced_up_to) - + try: + await self.wallet_state_manager.db_wrapper.commit_transaction() + await self.wallet_state_manager.db_wrapper.begin_transaction() + await self.wallet_state_manager.new_coin_state(valid_states, peer, fork_height) + + if update_finished_height: + if len(cs_heights) == 1: + # We have processed all past tasks, so we can increase the height safely + synced_up_to = last_change_height_cs(valid_states[-1]) - 1 + else: + # We know we have processed everything before this min height + synced_up_to = min(cs_heights) - 1 + await self.wallet_state_manager.blockchain.set_finished_sync_up_to( + synced_up_to, in_transaction=True + ) + await self.wallet_state_manager.db_wrapper.commit_transaction() + + except Exception as e: + tb = traceback.format_exc() + self.log.error(f"Exception while adding state: {e} {tb}") + await self.wallet_state_manager.db_wrapper.rollback_transaction() + await self.wallet_state_manager.coin_store.rebuild_wallet_cache() + await self.wallet_state_manager.tx_store.rebuild_tx_cache() + await self.wallet_state_manager.pool_store.rebuild_cache() except Exception as e: tb = traceback.format_exc() self.log.error(f"Exception while adding state: {e} {tb}") @@ -661,16 +671,24 @@ async def receive_and_validate(inner_states: List[CoinState], inner_idx_start: i self.log.error(f"Disconnected from peer {peer.peer_node_id} host {peer.peer_host}") return False if trusted: - try: - self.log.info(f"new coin state received ({idx}-" f"{idx + len(states) - 1}/ {len(items)})") - await self.wallet_state_manager.new_coin_state(states, peer, fork_height) - await self.wallet_state_manager.blockchain.set_finished_sync_up_to( - last_change_height_cs(states[-1]) - 1 - ) - except Exception as e: - tb = traceback.format_exc() - self.log.error(f"Error adding states.. {e} {tb}") - return False + async with self.wallet_state_manager.db_wrapper.lock: + try: + self.log.info(f"new coin state received ({idx}-" f"{idx + len(states) - 1}/ {len(items)})") + await self.wallet_state_manager.db_wrapper.commit_transaction() + await self.wallet_state_manager.db_wrapper.begin_transaction() + await self.wallet_state_manager.new_coin_state(states, peer, fork_height) + await self.wallet_state_manager.db_wrapper.commit_transaction() + await self.wallet_state_manager.blockchain.set_finished_sync_up_to( + last_change_height_cs(states[-1]) - 1, in_transaction=True + ) + except Exception as e: + await self.wallet_state_manager.db_wrapper.rollback_transaction() + await self.wallet_state_manager.coin_store.rebuild_wallet_cache() + await self.wallet_state_manager.tx_store.rebuild_tx_cache() + await self.wallet_state_manager.pool_store.rebuild_cache() + tb = traceback.format_exc() + self.log.error(f"Error adding states.. {e} {tb}") + return False else: while len(concurrent_tasks_cs_heights) >= target_concurrent_tasks: await asyncio.sleep(0.1) diff --git a/chia/wallet/wallet_pool_store.py b/chia/wallet/wallet_pool_store.py index 621dcfd81819..26285ba73178 100644 --- a/chia/wallet/wallet_pool_store.py +++ b/chia/wallet/wallet_pool_store.py @@ -40,6 +40,7 @@ async def add_spend( wallet_id: int, spend: CoinSpend, height: uint32, + in_transaction=False, ) -> None: """ Appends (or replaces) entries in the DB. The new list must be at least as long as the existing list, and the @@ -47,31 +48,38 @@ async def add_spend( until db_wrapper.commit() is called. However it is written to the cache, so it can be fetched with get_all_state_transitions. """ - if wallet_id not in self._state_transitions_cache: - self._state_transitions_cache[wallet_id] = [] - all_state_transitions: List[Tuple[uint32, CoinSpend]] = self.get_spends_for_wallet(wallet_id) - - if (height, spend) in all_state_transitions: - return - - if len(all_state_transitions) > 0: - if height < all_state_transitions[-1][0]: - raise ValueError("Height cannot go down") - if spend.coin.parent_coin_info != all_state_transitions[-1][1].coin.name(): - raise ValueError("New spend does not extend") - - all_state_transitions.append((height, spend)) - - cursor = await self.db_connection.execute( - "INSERT OR REPLACE INTO pool_state_transitions VALUES (?, ?, ?, ?)", - ( - len(all_state_transitions) - 1, - wallet_id, - height, - bytes(spend), - ), - ) - await cursor.close() + if not in_transaction: + await self.db_wrapper.lock.acquire() + try: + if wallet_id not in self._state_transitions_cache: + self._state_transitions_cache[wallet_id] = [] + all_state_transitions: List[Tuple[uint32, CoinSpend]] = self.get_spends_for_wallet(wallet_id) + + if (height, spend) in all_state_transitions: + return + + if len(all_state_transitions) > 0: + if height < all_state_transitions[-1][0]: + raise ValueError("Height cannot go down") + if spend.coin.parent_coin_info != all_state_transitions[-1][1].coin.name(): + raise ValueError("New spend does not extend") + + all_state_transitions.append((height, spend)) + + cursor = await self.db_connection.execute( + "INSERT OR REPLACE INTO pool_state_transitions VALUES (?, ?, ?, ?)", + ( + len(all_state_transitions) - 1, + wallet_id, + height, + bytes(spend), + ), + ) + await cursor.close() + finally: + if not in_transaction: + await self.db_connection.commit() + self.db_wrapper.lock.release() def get_spends_for_wallet(self, wallet_id: int) -> List[Tuple[uint32, CoinSpend]]: """ diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 8135f46d4f42..2968b9b79e45 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -598,7 +598,7 @@ async def fetch_parent_and_check_for_cat( "automatically_add_unknown_cats", False ): cat_wallet = await CATWallet.create_wallet_for_cat( - self, self.main_wallet, bytes(tail_hash).hex()[2:] + self, self.main_wallet, bytes(tail_hash).hex()[2:], in_transaction=True ) wallet_id = cat_wallet.id() wallet_type = WalletType(cat_wallet.type()) @@ -731,7 +731,7 @@ async def new_coin_state( name=bytes32(token_bytes()), memos=[], ) - await self.tx_store.add_transaction_record(tx_record, False) + await self.tx_store.add_transaction_record(tx_record, True) children = await self.wallet_node.fetch_children(peer, coin_state.coin.name(), fork_height) assert children is not None @@ -786,7 +786,7 @@ async def new_coin_state( memos=[], ) - await self.tx_store.add_transaction_record(tx_record, False) + await self.tx_store.add_transaction_record(tx_record, True) else: await self.coin_store.set_spent(coin_state.coin.name(), coin_state.spent_height) rem_tx_records: List[TransactionRecord] = [] @@ -870,7 +870,7 @@ async def new_coin_state( child.coin.name(), [launcher_spend], child.spent_height, - False, + True, "pool_wallet", ) coin_added = launcher_spend.additions()[0] @@ -1030,7 +1030,7 @@ async def coin_added( wallet = self.wallets[wallet_id] await wallet.coin_added(coin, height) - await self.create_more_puzzle_hashes() + await self.create_more_puzzle_hashes(in_transaction=True) return coin_record_1 async def add_pending_transaction(self, tx_record: TransactionRecord): @@ -1047,11 +1047,11 @@ async def add_pending_transaction(self, tx_record: TransactionRecord): self.tx_pending_changed() self.state_changed("pending_transaction", tx_record.wallet_id) - async def add_transaction(self, tx_record: TransactionRecord): + async def add_transaction(self, tx_record: TransactionRecord, in_transaction=False): """ Called from wallet to add transaction that is not being set to full_node """ - await self.tx_store.add_transaction_record(tx_record, False) + await self.tx_store.add_transaction_record(tx_record, in_transaction) self.state_changed("pending_transaction", tx_record.wallet_id) async def remove_from_queue( @@ -1123,7 +1123,7 @@ async def reorg_rollback(self, height: int): if remove: remove_ids.append(wallet_id) for wallet_id in remove_ids: - await self.user_store.delete_wallet(wallet_id, in_transaction=True) + await self.user_store.delete_wallet(wallet_id, in_transaction=False) self.wallets.pop(wallet_id) async def _await_closed(self) -> None: @@ -1153,10 +1153,10 @@ async def get_wallet_for_asset_id(self, asset_id: str): return wallet return None - async def add_new_wallet(self, wallet: Any, wallet_id: int, create_puzzle_hashes=True): + async def add_new_wallet(self, wallet: Any, wallet_id: int, create_puzzle_hashes=True, in_transaction=False): self.wallets[uint32(wallet_id)] = wallet if create_puzzle_hashes: - await self.create_more_puzzle_hashes() + await self.create_more_puzzle_hashes(in_transaction=in_transaction) self.state_changed("wallet_created") async def get_spendable_coins_for_wallet(self, wallet_id: int, records=None) -> Set[WalletCoinRecord]: @@ -1191,9 +1191,6 @@ async def create_action( await self.action_store.create_action(name, wallet_id, wallet_type, callback, done, data, in_transaction) self.tx_pending_changed() - async def set_action_done(self, action_id: int): - await self.action_store.action_done(action_id) - async def generator_received(self, height: uint32, header_hash: uint32, program: Program): actions: List[WalletAction] = await self.action_store.get_all_pending_actions() diff --git a/tests/pools/test_wallet_pool_store.py b/tests/pools/test_wallet_pool_store.py index 581d62c010d1..bed22440c9aa 100644 --- a/tests/pools/test_wallet_pool_store.py +++ b/tests/pools/test_wallet_pool_store.py @@ -64,15 +64,15 @@ async def test_store(self): assert store.get_spends_for_wallet(0) == [] assert store.get_spends_for_wallet(1) == [] - await store.add_spend(1, solution_1, 100) + await store.add_spend(1, solution_1, 100, True) assert store.get_spends_for_wallet(1) == [(100, solution_1)] # Idempotent - await store.add_spend(1, solution_1, 100) + await store.add_spend(1, solution_1, 100, True) assert store.get_spends_for_wallet(1) == [(100, solution_1)] with pytest.raises(ValueError): - await store.add_spend(1, solution_1, 101) + await store.add_spend(1, solution_1, 101, True) # Rebuild cache, no longer present await db_wrapper.rollback_transaction() @@ -80,18 +80,18 @@ async def test_store(self): assert store.get_spends_for_wallet(1) == [] await store.rebuild_cache() - await store.add_spend(1, solution_1, 100) + await store.add_spend(1, solution_1, 100, False) assert store.get_spends_for_wallet(1) == [(100, solution_1)] solution_1_alt: CoinSpend = make_child_solution(solution_0_alt) with pytest.raises(ValueError): - await store.add_spend(1, solution_1_alt, 100) + await store.add_spend(1, solution_1_alt, 100, False) assert store.get_spends_for_wallet(1) == [(100, solution_1)] solution_2: CoinSpend = make_child_solution(solution_1) - await store.add_spend(1, solution_2, 100) + await store.add_spend(1, solution_2, 100, False) await store.rebuild_cache() solution_3: CoinSpend = make_child_solution(solution_2) await store.add_spend(1, solution_3, 100) From 0909fe767299ed194ec4742fe34d0cba242795fe Mon Sep 17 00:00:00 2001 From: dustinface <35775977+xdustinface@users.noreply.github.com> Date: Fri, 4 Mar 2022 18:51:39 +0100 Subject: [PATCH 168/378] benchmarks: Implement streamable data comparison (#10433) --- benchmarks/streamable.py | 103 +++++++++++++++++++++++++++++++++------ benchmarks/utils.py | 19 ++++++++ 2 files changed, 106 insertions(+), 16 deletions(-) diff --git a/benchmarks/streamable.py b/benchmarks/streamable.py index 135420202972..47e77d224a06 100644 --- a/benchmarks/streamable.py +++ b/benchmarks/streamable.py @@ -1,17 +1,21 @@ +import json +import sys from dataclasses import dataclass from enum import Enum from statistics import stdev from time import process_time as clock -from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union +from typing import Any, Callable, Dict, List, Optional, TextIO, Tuple, Type, Union import click -from utils import EnumType, rand_bytes, rand_full_block, rand_hash +from utils import EnumType, get_commit_hash, rand_bytes, rand_full_block, rand_hash from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.full_block import FullBlock from chia.util.ints import uint8, uint64 from chia.util.streamable import Streamable, streamable +_version = 1 + @dataclass(frozen=True) @streamable @@ -82,6 +86,25 @@ def print_row( print(f"{mode} | {us_per_iteration} | {stdev_us_per_iteration} | {avg_iterations} | {stdev_iterations}", end=end) +@dataclass +class BenchmarkResults: + us_per_iteration: float + stdev_us_per_iteration: float + avg_iterations: int + stdev_iterations: float + + +def print_results(mode: str, bench_result: BenchmarkResults, final: bool) -> None: + print_row( + mode=mode, + us_per_iteration=bench_result.us_per_iteration, + stdev_us_per_iteration=bench_result.stdev_us_per_iteration, + avg_iterations=bench_result.avg_iterations, + stdev_iterations=bench_result.stdev_iterations, + end="\n" if final else "\r", + ) + + # The strings in this Enum are by purpose. See benchmark.utils.EnumType. class Data(str, Enum): all = "all" @@ -158,18 +181,60 @@ def calc_stdev_percent(iterations: List[int], avg: float) -> float: return int((deviation / avg * 100) * 100) / 100 +def pop_data(key: str, *, old: Dict[str, Any], new: Dict[str, Any]) -> Tuple[Any, Any]: + if key not in old: + sys.exit(f"{key} missing in old") + if key not in new: + sys.exit(f"{key} missing in new") + return old.pop(key), new.pop(key) + + +def print_compare_row(c0: str, c1: Union[str, float], c2: Union[str, float], c3: Union[str, float]) -> None: + c0 = "{0:<12}".format(f"{c0}") + c1 = "{0:<16}".format(f"{c1}") + c2 = "{0:<16}".format(f"{c2}") + c3 = "{0:<12}".format(f"{c3}") + print(f"{c0} | {c1} | {c2} | {c3}") + + +def compare_results( + old: Dict[str, Dict[str, Dict[str, Union[float, int]]]], new: Dict[str, Dict[str, Dict[str, Union[float, int]]]] +) -> None: + old_version, new_version = pop_data("version", old=old, new=new) + if old_version != new_version: + sys.exit(f"version missmatch: old: {old_version} vs new: {new_version}") + old_commit_hash, new_commit_hash = pop_data("commit_hash", old=old, new=new) + for data, modes in new.items(): + if data not in old: + continue + print(f"\ncompare: {data}, old: {old_commit_hash}, new: {new_commit_hash}") + print_compare_row("mode", "µs/iteration old", "µs/iteration new", "diff %") + for mode, results in modes.items(): + if mode not in old[data]: + continue + old_us, new_us = pop_data("us_per_iteration", old=old[data][mode], new=results) + print_compare_row(mode, old_us, new_us, int((new_us - old_us) / old_us * 10000) / 100) + + @click.command() @click.option("-d", "--data", default=Data.all, type=EnumType(Data)) @click.option("-m", "--mode", default=Mode.all, type=EnumType(Mode)) @click.option("-r", "--runs", default=100, help="Number of benchmark runs to average results") @click.option("-t", "--ms", default=50, help="Milliseconds per run") @click.option("--live/--no-live", default=False, help="Print live results (slower)") -def run(data: Data, mode: Mode, runs: int, ms: int, live: bool) -> None: +@click.option("-o", "--output", type=click.File("w"), help="Write the results to a file") +@click.option("-c", "--compare", type=click.File("r"), help="Compare to the results from a file") +def run(data: Data, mode: Mode, runs: int, ms: int, live: bool, output: TextIO, compare: TextIO) -> None: results: Dict[Data, Dict[Mode, List[List[int]]]] = {} + bench_results: Dict[str, Any] = {"version": _version, "commit_hash": get_commit_hash()} for current_data, parameter in benchmark_parameter.items(): - results[current_data] = {} if data == Data.all or current_data == data: - print(f"\nruns: {runs}, ms/run: {ms}, benchmarks: {mode.name}, data: {parameter.data_class.__name__}") + results[current_data] = {} + bench_results[current_data] = {} + print( + f"\nbenchmarks: {mode.name}, data: {parameter.data_class.__name__} runs: {runs}, ms/run: {ms}, " + f"commit_hash: {bench_results['commit_hash']}" + ) print_row( mode="mode", us_per_iteration="µs/iteration", @@ -184,22 +249,21 @@ def run(data: Data, mode: Mode, runs: int, ms: int, live: bool) -> None: all_results: List[List[int]] = results[current_data][current_mode] obj = parameter.object_creation_cb() - def print_results(print_run: int, final: bool) -> None: + def get_bench_results() -> BenchmarkResults: all_runtimes: List[int] = [x for inner in all_results for x in inner] total_iterations: int = len(all_runtimes) total_elapsed_us: int = sum(all_runtimes) - avg_iterations: float = total_iterations / print_run + avg_iterations: float = total_iterations / len(all_results) stdev_iterations: float = calc_stdev_percent([len(x) for x in all_results], avg_iterations) + us_per_iteration: float = total_elapsed_us / total_iterations stdev_us_per_iteration: float = calc_stdev_percent( all_runtimes, total_elapsed_us / total_iterations ) - print_row( - mode=current_mode.name, - us_per_iteration=int(total_elapsed_us / total_iterations * 100) / 100, - stdev_us_per_iteration=stdev_us_per_iteration, - avg_iterations=int(avg_iterations), - stdev_iterations=stdev_iterations, - end="\n" if final else "\r", + return BenchmarkResults( + int(us_per_iteration * 100) / 100, + stdev_us_per_iteration, + int(avg_iterations), + stdev_iterations, ) current_run: int = 0 @@ -219,9 +283,16 @@ def print_results(print_run: int, final: bool) -> None: us_iteration_results = run_for_ms(lambda: conversion_cb(prepared_obj), ms) all_results.append(us_iteration_results) if live: - print_results(current_run, False) + print_results(current_mode.name, get_bench_results(), False) assert current_run == runs - print_results(runs, True) + bench_result = get_bench_results() + bench_results[current_data][current_mode] = bench_result.__dict__ + print_results(current_mode.name, bench_result, True) + json_output = json.dumps(bench_results) + if output: + output.write(json_output) + if compare: + compare_results(json.load(compare), json.loads(json_output)) if __name__ == "__main__": diff --git a/benchmarks/utils.py b/benchmarks/utils.py index 3f3604bd750a..bda6216385c4 100644 --- a/benchmarks/utils.py +++ b/benchmarks/utils.py @@ -19,6 +19,7 @@ import aiosqlite import click import os +import subprocess import sys import random from blspy import G2Element, G1Element, AugSchemeMPL @@ -197,3 +198,21 @@ def sql_trace_callback(req: str): await connection.execute("pragma synchronous=full") return DBWrapper(connection, db_version) + + +def get_commit_hash() -> str: + try: + os.chdir(Path(os.path.realpath(__file__)).parent) + commit_hash = ( + subprocess.run(["git", "rev-parse", "--short", "HEAD"], check=True, stdout=subprocess.PIPE) + .stdout.decode("utf-8") + .strip() + ) + except Exception: + sys.exit("Failed to get the commit hash") + try: + if len(subprocess.run(["git", "status", "-s"], check=True, stdout=subprocess.PIPE).stdout) > 0: + raise Exception() + except Exception: + commit_hash += "-dirty" + return commit_hash From 058eb33abb4bd9c87db402214fc0d27da8426704 Mon Sep 17 00:00:00 2001 From: Florin Chirica Date: Sat, 5 Mar 2022 00:04:52 +0200 Subject: [PATCH 169/378] Resubmit peak to timelord for failure. (#10551) * Initial commit resubmit peak to timelord. * Change how toandle exception. --- chia/full_node/full_node.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/chia/full_node/full_node.py b/chia/full_node/full_node.py index 0c4e2c534a60..bce9aa04b36d 100644 --- a/chia/full_node/full_node.py +++ b/chia/full_node/full_node.py @@ -1414,6 +1414,7 @@ async def respond_block( self, respond_block: full_node_protocol.RespondBlock, peer: Optional[ws.WSChiaConnection] = None, + raise_on_disconnected: bool = False, ) -> Optional[Message]: """ Receive a full block from a peer full node (or ourselves). @@ -1535,6 +1536,8 @@ async def respond_block( elif added == ReceiveBlockResult.DISCONNECTED_BLOCK: self.log.info(f"Disconnected block {header_hash} at height {block.height}") + if raise_on_disconnected: + raise RuntimeError("Expected block to be added, received disconnected block.") return None elif added == ReceiveBlockResult.NEW_PEAK: # Only propagate blocks which extend the blockchain (becomes one of the heads) @@ -1918,7 +1921,7 @@ async def new_infusion_point_vdf( self.log.warning("Trying to make a pre-farm block but height is not 0") return None try: - await self.respond_block(full_node_protocol.RespondBlock(block)) + await self.respond_block(full_node_protocol.RespondBlock(block), raise_on_disconnected=True) except Exception as e: self.log.warning(f"Consensus error validating block: {e}") if timelord_peer is not None: From 4c345e2fa5a79eba30746163296367ee857b0ad9 Mon Sep 17 00:00:00 2001 From: Richard Kiss Date: Fri, 4 Mar 2022 14:05:12 -0800 Subject: [PATCH 170/378] Add some logging. (#10556) --- chia/cmds/show.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/chia/cmds/show.py b/chia/cmds/show.py index ed03ef72b7e7..e3224edb0eb1 100644 --- a/chia/cmds/show.py +++ b/chia/cmds/show.py @@ -118,7 +118,10 @@ async def show_async( elif peak is not None and sync_mode: sync_max_block = blockchain_state["sync"]["sync_tip_height"] sync_current_block = blockchain_state["sync"]["sync_progress_height"] - print(f"Current Blockchain Status: Syncing {sync_current_block}/{sync_max_block}.") + print( + f"Current Blockchain Status: Syncing {sync_current_block}/{sync_max_block} " + f"({sync_max_block - sync_current_block} behind)." + ) print("Peak: Hash:", peak.header_hash if peak is not None else "") elif peak is not None: print(f"Current Blockchain Status: Not Synced. Peak height: {peak.height}") From 816251ec0dafb2e522d7c18f9bd5bcf01f07e086 Mon Sep 17 00:00:00 2001 From: ChiaMineJP Date: Sat, 5 Mar 2022 07:06:13 +0900 Subject: [PATCH 171/378] Fix `install-gui.sh` (#10460) * Fixed an issue where running with gui git branch specified failed * Fixed an issue where install-gui.sh failed if npm>=7 and NodeJS < 16 were installed * Fixed inconsistent npm path issue * Fixed typo --- install-gui.sh | 46 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/install-gui.sh b/install-gui.sh index 89cb6bbba7c7..bda599762dfb 100755 --- a/install-gui.sh +++ b/install-gui.sh @@ -32,13 +32,20 @@ nodejs_is_installed(){ } do_install_npm_locally(){ + NODEJS_VERSION="$(node -v | cut -d'.' -f 1 | sed -e 's/^v//')" NPM_VERSION="$(npm -v | cut -d'.' -f 1)" - if [ "$NPM_VERSION" -lt "7" ]; then - echo "Current npm version($(npm -v)) is less than 7. GUI app requires npm>=7." + + if [ "$NODEJS_VERSION" -lt "16" ] || [ "$NPM_VERSION" -lt "7" ]; then + if [ "$NODEJS_VERSION" -lt "16" ]; then + echo "Current NodeJS version($(node -v)) is less than 16. GUI app requires NodeJS>=16." + fi + if [ "$NPM_VERSION" -lt "7" ]; then + echo "Current npm version($(npm -v)) is less than 7. GUI app requires npm>=7." + fi if [ "$(uname)" = "OpenBSD" ] || [ "$(uname)" = "FreeBSD" ]; then # `n` package does not support OpenBSD/FreeBSD - echo "Please install npm>=7 manually" + echo "Please install NodeJS>=16 and/or npm>=7 manually" exit 1 fi @@ -61,17 +68,39 @@ do_install_npm_locally(){ # `n 16` here installs nodejs@16 under $N_PREFIX directory echo "n 16" n 16 + echo "Current NodeJS version: $(node -v)" echo "Current npm version: $(npm -v)" + if [ "$(node -v | cut -d'.' -f 1 | sed -e 's/^v//')" -lt "16" ]; then + echo "Error: Failed to install NodeJS>=16" + exit 1 + fi if [ "$(npm -v | cut -d'.' -f 1)" -lt "7" ]; then echo "Error: Failed to install npm>=7" exit 1 fi cd "${SCRIPT_DIR}" else + echo "Found NodeJS $(node -v)" echo "Found npm $(npm -v)" fi } +# Work around for inconsistent `npm` exec path issue +# https://github.com/Chia-Network/chia-blockchain/pull/10460#issuecomment-1054492495 +patch_inconsistent_npm_issue(){ + node_module_dir=$1 + if [ ! -d "$node_module_dir" ]; then + mkdir "$node_module_dir" + fi + if [ ! -d "${node_module_dir}/.bin" ]; then + mkdir "${node_module_dir}/.bin" + fi + if [ -e "${node_module_dir}/.bin/npm" ]; then + rm -f "${node_module_dir}/.bin/npm" + fi + ln -s "$(command -v npm)" "${node_module_dir}/.bin/npm" +} + # Manage npm and other install requirements on an OS specific basis if [ "$(uname)" = "Linux" ]; then #LINUX=1 @@ -155,17 +184,22 @@ if [ ! "$CI" ]; then if [ "$SUBMODULE_BRANCH" ]; then - git fetch - git checkout "$SUBMODULE_BRANCH" - git pull + git fetch --all + git reset --hard "$SUBMODULE_BRANCH" echo "" echo "Building the GUI with branch $SUBMODULE_BRANCH" echo "" fi + # Work around for inconsistent `npm` exec path issue + # https://github.com/Chia-Network/chia-blockchain/pull/10460#issuecomment-1054492495 + patch_inconsistent_npm_issue "../node_modules" + npm ci npm audit fix || true npm run build + + # Set modified output of `chia version` to version property of GUI's package.json python ../installhelper.py else echo "Skipping node.js in install.sh on MacOS ci." From 4bd5c53f48cb049eff36c87c00d21b1f2dd26b27 Mon Sep 17 00:00:00 2001 From: Mariano Sorgente <3069354+mariano54@users.noreply.github.com> Date: Fri, 4 Mar 2022 20:10:04 -0500 Subject: [PATCH 172/378] Add fee to changing pool, and more PlotNFT syncing fixes (#10545) * Add fee to changing pool, and more aggressive disconnect of untrusted * Don't publish fee TX * Lint * Small plotnft related fixes * More plotnft fixes * Apply quexington suggestion * correct param for in_transaction * Support get_transaction and get_transactions in plotnft * Re-add publish_transaction and add comment * Don't rerun additions --- chia/cmds/wallet_funcs.py | 8 ++++---- chia/pools/pool_wallet.py | 24 ++++++++++++++++++------ chia/rpc/wallet_rpc_api.py | 13 +++++++++++-- chia/wallet/wallet.py | 7 +++++-- chia/wallet/wallet_node.py | 9 +++++++-- chia/wallet/wallet_state_manager.py | 6 ++++-- 6 files changed, 49 insertions(+), 18 deletions(-) diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index e0db35c53112..25877fffda92 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -41,12 +41,12 @@ def print_transaction(tx: TransactionRecord, verbose: bool, name, address_prefix def get_mojo_per_unit(wallet_type: WalletType) -> int: mojo_per_unit: int - if wallet_type == WalletType.STANDARD_WALLET: + if wallet_type == WalletType.STANDARD_WALLET or wallet_type == WalletType.POOLING_WALLET: mojo_per_unit = units["chia"] elif wallet_type == WalletType.CAT: mojo_per_unit = units["cat"] else: - raise LookupError("Only standard wallet and CAT wallets are supported") + raise LookupError("Only standard wallet, CAT wallets, and Plot NFTs are supported") return mojo_per_unit @@ -68,12 +68,12 @@ async def get_name_for_wallet_id( wallet_id: int, wallet_client: WalletRpcClient, ): - if wallet_type == WalletType.STANDARD_WALLET: + if wallet_type == WalletType.STANDARD_WALLET or wallet_type == WalletType.POOLING_WALLET: name = config["network_overrides"]["config"][config["selected_network"]]["address_prefix"].upper() elif wallet_type == WalletType.CAT: name = await wallet_client.get_cat_name(wallet_id=str(wallet_id)) else: - raise LookupError("Only standard wallet and CAT wallets are supported") + raise LookupError("Only standard wallet, CAT wallets, and Plot NFTs are supported") return name diff --git a/chia/pools/pool_wallet.py b/chia/pools/pool_wallet.py index 1c22034f6a7f..a748f1320fa7 100644 --- a/chia/pools/pool_wallet.py +++ b/chia/pools/pool_wallet.py @@ -500,6 +500,12 @@ async def generate_fee_transaction(self, fee: uint64, coin_announcements=None) - return fee_tx async def publish_transactions(self, travel_tx: TransactionRecord, fee_tx: Optional[TransactionRecord]): + # We create two transaction records, one for the pool wallet to keep track of the travel TX, and another + # for the standard wallet to keep track of the fee. However, we will only submit the first one to the + # blockchain, and this one has the fee inside it as well. + # The fee tx, if present, will be added to the DB with no spend_bundle set, which has the effect that it + # will not be sent to full nodes. + await self.wallet_state_manager.add_pending_transaction(travel_tx) if fee_tx is not None: await self.wallet_state_manager.add_pending_transaction(dataclasses.replace(fee_tx, spend_bundle=None)) @@ -576,14 +582,14 @@ async def generate_travel_transactions(self, fee: uint64) -> Tuple[TransactionRe else: raise RuntimeError("Invalid state") - fee_tx = None - if fee > 0: - fee_tx = await self.generate_fee_transaction(fee) - signed_spend_bundle = await self.sign(outgoing_coin_spend) assert signed_spend_bundle.removals()[0].puzzle_hash == singleton.puzzle_hash assert signed_spend_bundle.removals()[0].name() == singleton.name() assert signed_spend_bundle is not None + fee_tx: Optional[TransactionRecord] = None + if fee > 0: + fee_tx = await self.generate_fee_transaction(fee) + signed_spend_bundle = SpendBundle.aggregate([signed_spend_bundle, fee_tx.spend_bundle]) tx_record = TransactionRecord( confirmed_at_height=uint32(0), @@ -722,7 +728,10 @@ async def join_pool( if current_state.current.state == LEAVING_POOL: history: List[Tuple[uint32, CoinSpend]] = await self.get_spend_history() last_height: uint32 = history[-1][0] - if self.wallet_state_manager.get_peak().height <= last_height + current_state.current.relative_lock_height: + if ( + self.wallet_state_manager.blockchain.get_peak_height() + <= last_height + current_state.current.relative_lock_height + ): raise ValueError( f"Cannot join a pool until height {last_height + current_state.current.relative_lock_height}" ) @@ -755,7 +764,10 @@ async def self_pool(self, fee: uint64) -> Tuple[uint64, TransactionRecord, Optio total_fee = fee history: List[Tuple[uint32, CoinSpend]] = await self.get_spend_history() last_height: uint32 = history[-1][0] - if self.wallet_state_manager.get_peak().height <= last_height + current_state.current.relative_lock_height: + if ( + self.wallet_state_manager.blockchain.get_peak_height() + <= last_height + current_state.current.relative_lock_height + ): raise ValueError( f"Cannot self pool until height {last_height + current_state.current.relative_lock_height}" ) diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index dc88e0cf7255..af5efbc4c72e 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -18,7 +18,7 @@ from chia.types.spend_bundle import SpendBundle from chia.util.bech32m import decode_puzzle_hash, encode_puzzle_hash from chia.util.byte_types import hexstr_to_bytes -from chia.util.ints import uint32, uint64 +from chia.util.ints import uint32, uint64, uint8 from chia.util.keychain import KeyringIsLocked, bytes_to_mnemonic, generate_mnemonic from chia.util.path import path_from_root from chia.util.ws_message import WsRpcMessage, create_payload_dict @@ -1322,6 +1322,9 @@ async def pw_join_pool(self, request) -> Dict: fee = uint64(request.get("fee", 0)) wallet_id = uint32(request["wallet_id"]) wallet: PoolWallet = self.service.wallet_state_manager.wallets[wallet_id] + if wallet.type() != uint8(WalletType.POOLING_WALLET): + raise ValueError(f"Wallet with wallet id: {wallet_id} is not a plotNFT wallet.") + pool_wallet_info: PoolWalletInfo = await wallet.get_current_state() owner_pubkey = pool_wallet_info.current.owner_pubkey target_puzzlehash = None @@ -1352,6 +1355,8 @@ async def pw_self_pool(self, request) -> Dict: fee = uint64(request.get("fee", 0)) wallet_id = uint32(request["wallet_id"]) wallet: PoolWallet = self.service.wallet_state_manager.wallets[wallet_id] + if wallet.type() != uint8(WalletType.POOLING_WALLET): + raise ValueError(f"Wallet with wallet id: {wallet_id} is not a plotNFT wallet.") if await self.service.wallet_state_manager.synced() is False: raise ValueError("Wallet needs to be fully synced.") @@ -1369,6 +1374,8 @@ async def pw_absorb_rewards(self, request) -> Dict: fee = uint64(request.get("fee", 0)) wallet_id = uint32(request["wallet_id"]) wallet: PoolWallet = self.service.wallet_state_manager.wallets[wallet_id] + if wallet.type() != uint8(WalletType.POOLING_WALLET): + raise ValueError(f"Wallet with wallet id: {wallet_id} is not a plotNFT wallet.") async with self.service.wallet_state_manager.lock: transaction, fee_tx = await wallet.claim_pool_rewards(fee) @@ -1381,8 +1388,10 @@ async def pw_status(self, request) -> Dict: return {"success": False, "error": "not_initialized"} wallet_id = uint32(request["wallet_id"]) wallet: PoolWallet = self.service.wallet_state_manager.wallets[wallet_id] + if wallet.type() != WalletType.POOLING_WALLET.value: - raise ValueError(f"wallet_id {wallet_id} is not a pooling wallet") + raise ValueError(f"Wallet with wallet id: {wallet_id} is not a plotNFT wallet.") + state: PoolWalletInfo = await wallet.get_current_state() unconfirmed_transactions: List[TransactionRecord] = await wallet.get_unconfirmed_transactions() return { diff --git a/chia/wallet/wallet.py b/chia/wallet/wallet.py index 0df7c795ec92..6f0fa79126e7 100644 --- a/chia/wallet/wallet.py +++ b/chia/wallet/wallet.py @@ -120,12 +120,15 @@ async def get_spendable_balance(self, unspent_records=None) -> uint128: return spendable async def get_pending_change_balance(self) -> uint64: - unconfirmed_tx = await self.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(self.id()) + unconfirmed_tx: List[TransactionRecord] = await self.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( + self.id() + ) addition_amount = 0 for record in unconfirmed_tx: if not record.is_in_mempool(): - self.log.warning(f"Record: {record} not in mempool, {record.sent_to}") + if record.spend_bundle is not None: + self.log.warning(f"Record: {record} not in mempool, {record.sent_to}") continue our_spend = False for coin in record.removals: diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index 6d3f03851802..720ba71b0740 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -534,14 +534,14 @@ def is_new_state_update(cs: CoinState) -> bool: # The number of coin id updates are usually going to be significantly less than ph updates, so we can # sync from 0 every time. - continue_while = False + continue_while = True all_coin_ids: List[bytes32] = await self.get_coin_ids_to_subscribe(0) already_checked_coin_ids: Set[bytes32] = set() while continue_while: one_k_chunks = chunks(all_coin_ids, 1000) for chunk in one_k_chunks: c_update_res: List[CoinState] = await subscribe_to_coin_updates(chunk, full_node, 0) - c_update_res = list(filter(is_new_state_update, c_update_res)) + if not await self.receive_state_from_peer(c_update_res, full_node): # If something goes wrong, abort sync return @@ -878,6 +878,11 @@ async def new_peak_wallet(self, new_peak: wallet_protocol.NewPeakWallet, peer: W if ( peer.peer_node_id not in self.synced_peers or far_behind ) and new_peak.height >= self.constants.WEIGHT_PROOF_RECENT_BLOCKS: + if await self.check_for_synced_trusted_peer(header_block, request_time): + self.wallet_state_manager.set_sync_mode(False) + self.log.info("Cancelling untrusted sync, we are connected to a trusted peer") + return + syncing = False if far_behind or len(self.synced_peers) == 0: syncing = True diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 2968b9b79e45..53de1422764a 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -873,7 +873,9 @@ async def new_coin_state( True, "pool_wallet", ) - coin_added = launcher_spend.additions()[0] + launcher_spend_additions = launcher_spend.additions() + assert len(launcher_spend_additions) == 1 + coin_added = launcher_spend_additions[0] await self.coin_added( coin_added, coin_state.spent_height, [], pool_wallet.id(), WalletType(pool_wallet.type()) ) @@ -1043,7 +1045,7 @@ async def add_pending_transaction(self, tx_record: TransactionRecord): all_coins_names.extend([coin.name() for coin in tx_record.additions]) all_coins_names.extend([coin.name() for coin in tx_record.removals]) - await self.add_interested_coin_ids(all_coins_names) + await self.add_interested_coin_ids(all_coins_names, False) self.tx_pending_changed() self.state_changed("pending_transaction", tx_record.wallet_id) From 0e7cc5a88393ef02b4057dd4bf894be2e73bc00b Mon Sep 17 00:00:00 2001 From: William Allen Date: Mon, 7 Mar 2022 19:12:31 -0600 Subject: [PATCH 173/378] adding 1.3.0 release notes to changelog (#10578) * adding 1.3.0 release notes to changelog * Typos. thx @paninaro * Adding requested adjustments to changelog --- CHANGELOG.md | 102 ++++++++++++++++++++++++++++----------------------- 1 file changed, 56 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f140f5acdf3..b49c2d05208b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,78 +6,88 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project does not yet adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) for setuptools_scm/PEP 440 reasons. -## 1.3.0 Beta Chia blockchain 2022-2-10 - -We at Chia have been working hard to bring all of our new features together into one easy-to-use release. Today, we’re proud to announce the beta release of our 1.3 client. Because this release is still in beta, we recommend that you only install it on non mission-critical systems. If you are running a large farm, you should wait for the full 1.3 release before upgrading. When will the full version of 1.3 be released? Soon. +## 1.3.0 Chia blockchain 2022-3-07 ### Added: -- CAT wallet support - add wallets for your favorite CATs -- Offers - make, take, and share your offers -- Integrated light wallet sync - to get you synced up faster while your full node syncs -- Wallet mode - Access just the wallet features to make and receive transactions -- Farmer mode - All your farming tools, and full node, while getting all the benefits of the upgraded wallet features -- New v2 DB - improved compression for smaller footprint -- Key derivation tool via CLI - lets you derive wallet addresses, child keys, and also search your keys for arbitrary wallet addresses/keys -- Light wallet data migration - CAT wallets you set up and your offer history will be carried over -- The farmer will report version info in User-Agent field for pool protocol (Thanks @FazendaPool) -- Added new RPC, get_version, to the daemon to return the version of Chia (Thanks @dkackman) +- CAT wallet support - add wallets for your favorite CATs. +- Offers - make, take, and share your offers. +- Integrated lite wallet sync - to get you synced up faster while your full node syncs. +- Wallet mode - Access just the wallet features to make and receive transactions. +- Farmer mode - All your farming tools, and full node, while getting all the benefits of the upgraded wallet features. +- New v2 DB - improved compression for smaller footprint (the v2 DB is created alongside the v1 DB. Please be sure to have enough disk space before executing the DB upgrade command). +- Key derivation tool via CLI - lets you derive wallet addresses, child keys, and also search your keys for arbitrary wallet addresses/keys. +- Lite wallet data migration - CAT wallets you set up and your offer history will be carried over. +- The farmer will report version info in User-Agent field for pool protocol (Thanks @FazendaPool). +- Added new RPC, get_version, to the daemon to return the version of Chia (Thanks @dkackman). - Added new config.yaml setting, reserved_cores, to specify how many cores Chia will not use when launching process pools. Using 0 will allow Chia to use all cores for process pools. Set the default to 0 to allow Chia to use all cores. This can result in faster syncing and better performance overall especially on lower-end CPUs like the Raspberry Pi4. - Added new RPC, get_logged_in_fingerprint, to the wallet to return the currently logged in fingerprint. - Added new CLI option, chia keys derive, to allow deriving any number of keys in various ways. This is particularly useful to do an exhaustive search for a given address using chia keys derive search. -- Div soft fork block height set to 2,300,000 +- Div soft fork block height set to 2,300,000. +- Added the ability to add an optional fee for creating and changing plot NFTs. +- Added *multiprocessing_start_method:* entry in config.yaml that allows setting the python *start method* for multiprocessing (default is *spawn* on Windows & MacOS, *fork* on Unix). +- Added option to "Cancel transaction" accepted offers that are stuck in "pending". ### Changed: -- Light wallet client sync updated to only require 3 peers instead of 5 -- Only CATs from the default CAT list will be automatically added, all other unknown CATs will need to be manually added -- New sorting pattern for offer history - Open/pending offers sorted on top ordered by creation date > confirmation block height > trade id, and then Confirmed and Cancelled offers sorted by the same order -- When plotting multiple plots with the GUI, new items are taken from the top of the list instead of the bottom -- CA certificate store update -- VDF, chiapos, and blspy workflows updated to support python 3.10 wheels +- Lite wallet client sync updated to only require 3 peers instead of 5. +- Only CATs from the default CAT list will be automatically added, all other unknown CATs will need to be manually added (thanks to @ojura, this behavior can be toggled in config.yaml). +- New sorting pattern for offer history - Open/pending offers sorted on top ordered by creation date > confirmation block height > trade id, and then Confirmed and Cancelled offers sorted by the same order. +- When plotting multiple plots with the GUI, new items are taken from the top of the list instead of the bottom. +- CA certificate store update. +- VDF, chiapos, and blspy workflows updated to support python 3.10 wheels. - We now store peers and peer information in a serialized format instead of sqlite. The new files are called peers.dat and wallet_peers.dat. New settings peers_file_path and wallet_peers_file_path added to config.yaml. -- CLI option chia show will display the currently selected network (mainnet or testnet) -- CLI option chia plots check will display the Pool Contract Address for Portable (PlotNFT) plots -- Thanks to @cross for adding the ability to resolve IPv6 from hostnames in config.yaml. Added new config option prefer_ipv6 to toggle whether to resolve to IPv6 or IPv4. Default is false (IPv4) +- CLI option chia show will display the currently selected network (mainnet or testnet). +- CLI option chia plots check will display the Pool Contract Address for Portable (PlotNFT) plots. +- Thanks to @cross for adding the ability to resolve IPv6 from hostnames in config.yaml. Added new config option prefer_ipv6 to toggle whether to resolve to IPv6 or IPv4. Default is false (IPv4). - The default timeout when syncing the node was increased from 10 seconds to 30 seconds to avoid timing out when syncing from slower peers. -- TLS 1.2 is now the minimum required for all communication including peer-to-peer. The TLS 1.2 allowed cipher list is set to: "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256" +- TLS 1.2 is now the minimum required for all communication including peer-to-peer. The TLS 1.2 allowed cipher list is set to: "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256". - In a future release the minimum TLS version will be set to TLS 1.3. A warning in the log will be emitted if the version of openssl in use does not support TLS 1.3. If supported, all local connections will be restricted to TLS 1.3. -- The new testnet is testnet10 -- Switch to using npm ci from npm install in the GUI install scripts +- The new testnet is testnet10. +- Switch to using npm ci from npm install in the GUI install scripts. - Improved sync performance of the full node by doing BLS validation in separate processes. - Default log rotation was changed to 50MiB from 20MiB - added config.yaml setting log_maxbytesrotation to configure this. - Thanks to @cross for an optimization to chiapos to use rename instead of copy if the tmp2 and final files are on the same filesystem. -- Updated to use chiapos 1.0.9 -- Updated to use blspy 1.0.8 -- Implemented a limit to the number of PlotNFTs a user can create - with the limit set to 20. This is to prevent users from incorrectly creating multiple PlotNFTs. This limit can be overridden for those users who have specific use cases that require more than 20 PlotNFTs +- Updated to use chiapos 1.0.9. +- Updated to use blspy 1.0.8. +- Implemented a limit to the number of PlotNFTs a user can create - with the limit set to 20. This is to prevent users from incorrectly creating multiple PlotNFTs. This limit can be overridden for those users who have specific use cases that require more than 20 PlotNFTs. +- Removed the option to display "All" rows per page on the transactions page of the GUI. +- Updated the background image for the MacOS installer. +- Changed the behavior of what info is displayed if the database is still syncing. ### Fixed: -- Offer history limit has been fixed to show all offers now instead of limiting to just 49 offers -- Fixed issues with using madmax CLI options -w, -G, -2, -t and -d (Issue 9163) (thanks @randomisresistance and @lasers8oclockday1) -- Fixed issues with CLI option –passhrase-file (Issue 9032) (thanks @moonlitbugs) -- Fixed issues with displaying IPv6 address in CLI with chia show -c -- Thanks to @chuwt for fix to looping logic during node synching -- Fixed the chia-blockchain RPM to set the permission of chrome-sandbox properly +- Offer history limit has been fixed to show all offers now instead of limiting to just 49 offers. +- Fixed issues with using madmax CLI options -w, -G, -2, -t and -d (Issue 9163) (thanks @randomisresistance and @lasers8oclockday1). +- Fixed issues with CLI option –passhrase-file (Issue 9032) (thanks @moonlitbugs). +- Fixed issues with displaying IPv6 address in CLI with chia show -c. +- Thanks to @chuwt for fix to looping logic during node synching. +- Fixed the chia-blockchain RPM to set the permission of chrome-sandbox properly. - Fixed issues where the wallet code would not generate enough addresses when looking for coins, which can result in missed coins due to the address not being checked. Deprecated the config setting initial_num_public_keys_new_wallet. The config setting initial_num_public_keys is now used in all cases. -- Thanks to @risner for fixes related to using colorlog -- Fixed issues in reading the pool_list from config if set to null -- Fixed display info in CLI chia show -c when No Info should be displayed -- Thanks to @madMAx42v3r for fixes in chiapos related to a possible race condition when multiple threads call Verifier::ValidateProof -- Thanks to @PastaPastaPasta for some compiler warning fixes in bls-signatures -- Thanks to @random-zebra for fixing a bug in the bls-signature copy assignment operator -- Thanks to @lourkeur for fixes in blspy related to pybind11 2.8+ -- Thanks to @nioos-ledger with a fix to the python implementation of bls-signatures +- Thanks to @risner for fixes related to using colorlog. +- Fixed issues in reading the pool_list from config if set to null. +- Fixed display info in CLI chia show -c when No Info should be displayed. +- Thanks to @madMAx42v3r for fixes in chiapos related to a possible race condition when multiple threads call Verifier::ValidateProof. +- Thanks to @PastaPastaPasta for some compiler warning fixes in bls-signatures. +- Thanks to @random-zebra for fixing a bug in the bls-signature copy assignment operator. +- Thanks to @lourkeur for fixes in blspy related to pybind11 2.8+. +- Thanks to @nioos-ledger with a fix to the python implementation of bls-signatures. +- Thanks to @yan74 for help debugging a race condition writing to config.yaml during beta. +- Fixed issue where the DB could lose the peak of the chain when receiving a compressed block. +- Fixed showing inbound transaction after an offer is cancelled. +- Fixed blockchain fee "Value seems high" message showing up when it shouldn't. ### Known Issues: -- When you are adding plots and you choose the option to “create a Plot NFT”, you will get an error message “Initial_target_state” and the plots will not get created +- When you are adding plots and you choose the option to “create a Plot NFT”, you will get an error message “Initial_target_state” and the plots will not get created. - Workaround: Create the Plot NFT first in the “Pool” tab, and then add your plots and choose the created plot NFT in the drop down. - If you are installing on a machine for the first time, when the GUI loads and you don’t have any pre-existing wallet keys, the GUI will flicker and not load anything. - - Workaround: close and relaunch the GUI -- When you close the Chia app, regardless if you are in farmer mode or wallet, the content on the exit dialog isn’t correct + - Workaround: close and relaunch the GUI. +- When you close the Chia app, regardless if you are in farmer mode or wallet, the content on the exit dialog isn’t correct. - If you start with wallet mode and then switch to farmer mode and back to wallet mode, the full node will continue to sync in the background. To get the full node to stop syncing after switching to wallet mode, you will need to close the Chia and relaunch the Chia app. - Wallets with large number of transactions or large number of coins will take longer to sync (more than a few minutes), but should take less time than a full node sync. It could fail in some cases. +- Huge numbers cannot be put into amount/fee input for transactions in the GUI. +- Some Linux systems experience excessive memory usage with the value *default*/*python_default*/*fork* configured for *multiprocessing_start_method:*. Setting this value to *spawn* may produce better results, but in some uncommon cases, is know to cause crashes. ## 1.2.11 Chia blockchain 2021-11-4 From 09927104925cfa895c475c6708a5a8cd06970765 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Tue, 8 Mar 2022 11:48:19 -0500 Subject: [PATCH 174/378] Require pytest-asyncio>=0.17.0 for @fixture() (#10560) --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index cfca00d8326a..e795477d13bd 100644 --- a/setup.py +++ b/setup.py @@ -44,7 +44,8 @@ "build", "pre-commit", "pytest", - "pytest-asyncio", + # >=0.17.0 for the fixture decorator + "pytest-asyncio >=0.17.0", "pytest-monitor; sys_platform == 'linux'", "pytest-xdist", "twine", From 5eca46491938b915686f68e6e974c0a36d1cea0d Mon Sep 17 00:00:00 2001 From: Adam Kelly <338792+aqk@users.noreply.github.com> Date: Tue, 8 Mar 2022 08:48:43 -0800 Subject: [PATCH 175/378] Convert tests/core/util/test_streamable.py to use pytest. Remove unneeded class. (#10577) --- tests/core/util/test_streamable.py | 533 +++++++++++++++-------------- 1 file changed, 274 insertions(+), 259 deletions(-) diff --git a/tests/core/util/test_streamable.py b/tests/core/util/test_streamable.py index dece7be79934..0d029eace28b 100644 --- a/tests/core/util/test_streamable.py +++ b/tests/core/util/test_streamable.py @@ -1,4 +1,3 @@ -import unittest from dataclasses import dataclass from typing import List, Optional, Tuple import io @@ -29,347 +28,363 @@ from tests.setup_nodes import bt, test_constants -class TestStreamable(unittest.TestCase): - def test_basic(self): - @dataclass(frozen=True) - @streamable - class TestClass(Streamable): - a: uint32 - b: uint32 - c: List[uint32] - d: List[List[uint32]] - e: Optional[uint32] - f: Optional[uint32] - g: Tuple[uint32, str, bytes] +def test_basic(): + @dataclass(frozen=True) + @streamable + class TestClass(Streamable): + a: uint32 + b: uint32 + c: List[uint32] + d: List[List[uint32]] + e: Optional[uint32] + f: Optional[uint32] + g: Tuple[uint32, str, bytes] + + a = TestClass(24, 352, [1, 2, 4], [[1, 2, 3], [3, 4]], 728, None, (383, "hello", b"goodbye")) + + b: bytes = bytes(a) + assert a == TestClass.from_bytes(b) + + +def test_variable_size(): + @dataclass(frozen=True) + @streamable + class TestClass2(Streamable): + a: uint32 + b: uint32 + c: bytes - a = TestClass(24, 352, [1, 2, 4], [[1, 2, 3], [3, 4]], 728, None, (383, "hello", b"goodbye")) + a = TestClass2(uint32(1), uint32(2), b"3") + bytes(a) - b: bytes = bytes(a) - assert a == TestClass.from_bytes(b) + with raises(NotImplementedError): - def test_variablesize(self): @dataclass(frozen=True) @streamable - class TestClass2(Streamable): - a: uint32 - b: uint32 - c: bytes + class TestClass3(Streamable): + a: int - a = TestClass2(uint32(1), uint32(2), b"3") - bytes(a) - with raises(NotImplementedError): +def test_json(): + block = bt.create_genesis_block(test_constants, bytes([0] * 32), b"0") - @dataclass(frozen=True) - @streamable - class TestClass3(Streamable): - a: int + dict_block = block.to_json_dict() + assert FullBlock.from_json_dict(dict_block) == block - def test_json(self): - block = bt.create_genesis_block(test_constants, bytes([0] * 32), b"0") - dict_block = block.to_json_dict() - assert FullBlock.from_json_dict(dict_block) == block +def test_recursive_json(): + @dataclass(frozen=True) + @streamable + class TestClass1(Streamable): + a: List[uint32] - def test_recursive_json(self): - @dataclass(frozen=True) - @streamable - class TestClass1(Streamable): - a: List[uint32] + @dataclass(frozen=True) + @streamable + class TestClass2(Streamable): + a: uint32 + b: List[Optional[List[TestClass1]]] + c: bytes32 - @dataclass(frozen=True) - @streamable - class TestClass2(Streamable): - a: uint32 - b: List[Optional[List[TestClass1]]] - c: bytes32 + tc1_a = TestClass1([uint32(1), uint32(2)]) + tc1_b = TestClass1([uint32(4), uint32(5)]) + tc1_c = TestClass1([uint32(7), uint32(8)]) - tc1_a = TestClass1([uint32(1), uint32(2)]) - tc1_b = TestClass1([uint32(4), uint32(5)]) - tc1_c = TestClass1([uint32(7), uint32(8)]) + tc2 = TestClass2(uint32(5), [[tc1_a], [tc1_b, tc1_c], None], bytes32(bytes([1] * 32))) + assert TestClass2.from_json_dict(tc2.to_json_dict()) == tc2 - tc2 = TestClass2(uint32(5), [[tc1_a], [tc1_b, tc1_c], None], bytes32(bytes([1] * 32))) - assert TestClass2.from_json_dict(tc2.to_json_dict()) == tc2 - def test_recursive_types(self): - coin: Optional[Coin] = None - l1 = [(bytes32([2] * 32), coin)] - rr = RespondRemovals(uint32(1), bytes32([1] * 32), l1, None) - RespondRemovals(rr.height, rr.header_hash, rr.coins, rr.proofs) +def test_recursive_types(): + coin: Optional[Coin] = None + l1 = [(bytes32([2] * 32), coin)] + rr = RespondRemovals(uint32(1), bytes32([1] * 32), l1, None) + RespondRemovals(rr.height, rr.header_hash, rr.coins, rr.proofs) - def test_ambiguous_deserialization_optionals(self): - with raises(AssertionError): - SubEpochChallengeSegment.from_bytes(b"\x00\x00\x00\x03\xff\xff\xff\xff") - @dataclass(frozen=True) - @streamable - class TestClassOptional(Streamable): - a: Optional[uint8] +def test_ambiguous_deserialization_optionals(): + with raises(AssertionError): + SubEpochChallengeSegment.from_bytes(b"\x00\x00\x00\x03\xff\xff\xff\xff") - # Does not have the required elements - with raises(AssertionError): - TestClassOptional.from_bytes(bytes([])) + @dataclass(frozen=True) + @streamable + class TestClassOptional(Streamable): + a: Optional[uint8] - TestClassOptional.from_bytes(bytes([0])) - TestClassOptional.from_bytes(bytes([1, 2])) + # Does not have the required elements + with raises(AssertionError): + TestClassOptional.from_bytes(bytes([])) - def test_ambiguous_deserialization_int(self): - @dataclass(frozen=True) - @streamable - class TestClassUint(Streamable): - a: uint32 + TestClassOptional.from_bytes(bytes([0])) + TestClassOptional.from_bytes(bytes([1, 2])) - # Does not have the required uint size - with raises(AssertionError): - TestClassUint.from_bytes(b"\x00\x00") - def test_ambiguous_deserialization_list(self): - @dataclass(frozen=True) - @streamable - class TestClassList(Streamable): - a: List[uint8] +def test_ambiguous_deserialization_int(): + @dataclass(frozen=True) + @streamable + class TestClassUint(Streamable): + a: uint32 - # Does not have the required elements - with raises(AssertionError): - TestClassList.from_bytes(bytes([0, 0, 100, 24])) + # Does not have the required uint size + with raises(AssertionError): + TestClassUint.from_bytes(b"\x00\x00") - def test_ambiguous_deserialization_tuple(self): - @dataclass(frozen=True) - @streamable - class TestClassTuple(Streamable): - a: Tuple[uint8, str] - # Does not have the required elements - with raises(AssertionError): - TestClassTuple.from_bytes(bytes([0, 0, 100, 24])) +def test_ambiguous_deserialization_list(): + @dataclass(frozen=True) + @streamable + class TestClassList(Streamable): + a: List[uint8] - def test_ambiguous_deserialization_str(self): - @dataclass(frozen=True) - @streamable - class TestClassStr(Streamable): - a: str + # Does not have the required elements + with raises(AssertionError): + TestClassList.from_bytes(bytes([0, 0, 100, 24])) - # Does not have the required str size - with raises(AssertionError): - TestClassStr.from_bytes(bytes([0, 0, 100, 24, 52])) - def test_ambiguous_deserialization_bytes(self): - @dataclass(frozen=True) - @streamable - class TestClassBytes(Streamable): - a: bytes +def test_ambiguous_deserialization_tuple(): + @dataclass(frozen=True) + @streamable + class TestClassTuple(Streamable): + a: Tuple[uint8, str] - # Does not have the required str size - with raises(AssertionError): - TestClassBytes.from_bytes(bytes([0, 0, 100, 24, 52])) + # Does not have the required elements + with raises(AssertionError): + TestClassTuple.from_bytes(bytes([0, 0, 100, 24])) - with raises(AssertionError): - TestClassBytes.from_bytes(bytes([0, 0, 0, 1])) - TestClassBytes.from_bytes(bytes([0, 0, 0, 1, 52])) - TestClassBytes.from_bytes(bytes([0, 0, 0, 2, 52, 21])) +def test_ambiguous_deserialization_str(): + @dataclass(frozen=True) + @streamable + class TestClassStr(Streamable): + a: str - def test_ambiguous_deserialization_bool(self): - @dataclass(frozen=True) - @streamable - class TestClassBool(Streamable): - a: bool + # Does not have the required str size + with raises(AssertionError): + TestClassStr.from_bytes(bytes([0, 0, 100, 24, 52])) - # Does not have the required str size - with raises(AssertionError): - TestClassBool.from_bytes(bytes([])) - TestClassBool.from_bytes(bytes([0])) - TestClassBool.from_bytes(bytes([1])) +def test_ambiguous_deserialization_bytes(): + @dataclass(frozen=True) + @streamable + class TestClassBytes(Streamable): + a: bytes - def test_ambiguous_deserialization_program(self): - @dataclass(frozen=True) - @streamable - class TestClassProgram(Streamable): - a: Program + # Does not have the required str size + with raises(AssertionError): + TestClassBytes.from_bytes(bytes([0, 0, 100, 24, 52])) - program = Program.to(binutils.assemble("()")) + with raises(AssertionError): + TestClassBytes.from_bytes(bytes([0, 0, 0, 1])) - TestClassProgram.from_bytes(bytes(program)) + TestClassBytes.from_bytes(bytes([0, 0, 0, 1, 52])) + TestClassBytes.from_bytes(bytes([0, 0, 0, 2, 52, 21])) - with raises(AssertionError): - TestClassProgram.from_bytes(bytes(program) + b"9") - def test_streamable_empty(self): - @dataclass(frozen=True) - @streamable - class A(Streamable): - pass +def test_ambiguous_deserialization_bool(): + @dataclass(frozen=True) + @streamable + class TestClassBool(Streamable): + a: bool + + # Does not have the required str size + with raises(AssertionError): + TestClassBool.from_bytes(bytes([])) + + TestClassBool.from_bytes(bytes([0])) + TestClassBool.from_bytes(bytes([1])) + + +def test_ambiguous_deserialization_program(): + @dataclass(frozen=True) + @streamable + class TestClassProgram(Streamable): + a: Program + + program = Program.to(binutils.assemble("()")) + + TestClassProgram.from_bytes(bytes(program)) + + with raises(AssertionError): + TestClassProgram.from_bytes(bytes(program) + b"9") + + +def test_streamable_empty(): + @dataclass(frozen=True) + @streamable + class A(Streamable): + pass + + assert A.from_bytes(bytes(A())) == A() + + +def test_parse_bool(): + assert not parse_bool(io.BytesIO(b"\x00")) + assert parse_bool(io.BytesIO(b"\x01")) + + # EOF + with raises(AssertionError): + parse_bool(io.BytesIO(b"")) + + with raises(ValueError): + parse_bool(io.BytesIO(b"\xff")) + + with raises(ValueError): + parse_bool(io.BytesIO(b"\x02")) - assert A.from_bytes(bytes(A())) == A() - def test_parse_bool(self): - assert not parse_bool(io.BytesIO(b"\x00")) - assert parse_bool(io.BytesIO(b"\x01")) +def test_uint32(): + assert parse_uint32(io.BytesIO(b"\x00\x00\x00\x00")) == 0 + assert parse_uint32(io.BytesIO(b"\x00\x00\x00\x01")) == 1 + assert parse_uint32(io.BytesIO(b"\x00\x00\x00\x01"), "little") == 16777216 + assert parse_uint32(io.BytesIO(b"\x01\x00\x00\x00")) == 16777216 + assert parse_uint32(io.BytesIO(b"\x01\x00\x00\x00"), "little") == 1 + assert parse_uint32(io.BytesIO(b"\xff\xff\xff\xff"), "little") == 4294967295 - # EOF - with raises(AssertionError): - parse_bool(io.BytesIO(b"")) + def test_write(value, byteorder): + f = io.BytesIO() + write_uint32(f, uint32(value), byteorder) + f.seek(0) + assert parse_uint32(f, byteorder) == value - with raises(ValueError): - parse_bool(io.BytesIO(b"\xff")) + test_write(1, "big") + test_write(1, "little") + test_write(4294967295, "big") + test_write(4294967295, "little") - with raises(ValueError): - parse_bool(io.BytesIO(b"\x02")) + with raises(AssertionError): + parse_uint32(io.BytesIO(b"")) + with raises(AssertionError): + parse_uint32(io.BytesIO(b"\x00")) + with raises(AssertionError): + parse_uint32(io.BytesIO(b"\x00\x00")) + with raises(AssertionError): + parse_uint32(io.BytesIO(b"\x00\x00\x00")) - def test_uint32(self): - assert parse_uint32(io.BytesIO(b"\x00\x00\x00\x00")) == 0 - assert parse_uint32(io.BytesIO(b"\x00\x00\x00\x01")) == 1 - assert parse_uint32(io.BytesIO(b"\x00\x00\x00\x01"), "little") == 16777216 - assert parse_uint32(io.BytesIO(b"\x01\x00\x00\x00")) == 16777216 - assert parse_uint32(io.BytesIO(b"\x01\x00\x00\x00"), "little") == 1 - assert parse_uint32(io.BytesIO(b"\xff\xff\xff\xff"), "little") == 4294967295 - def test_write(value, byteorder): - f = io.BytesIO() - write_uint32(f, uint32(value), byteorder) - f.seek(0) - assert parse_uint32(f, byteorder) == value +def test_parse_optional(): + assert parse_optional(io.BytesIO(b"\x00"), parse_bool) is None + assert parse_optional(io.BytesIO(b"\x01\x01"), parse_bool) + assert not parse_optional(io.BytesIO(b"\x01\x00"), parse_bool) - test_write(1, "big") - test_write(1, "little") - test_write(4294967295, "big") - test_write(4294967295, "little") + # EOF + with raises(AssertionError): + parse_optional(io.BytesIO(b"\x01"), parse_bool) - with raises(AssertionError): - parse_uint32(io.BytesIO(b"")) - with raises(AssertionError): - parse_uint32(io.BytesIO(b"\x00")) - with raises(AssertionError): - parse_uint32(io.BytesIO(b"\x00\x00")) - with raises(AssertionError): - parse_uint32(io.BytesIO(b"\x00\x00\x00")) + # optional must be 0 or 1 + with raises(ValueError): + parse_optional(io.BytesIO(b"\x02\x00"), parse_bool) - def test_parse_optional(self): - assert parse_optional(io.BytesIO(b"\x00"), parse_bool) is None - assert parse_optional(io.BytesIO(b"\x01\x01"), parse_bool) - assert not parse_optional(io.BytesIO(b"\x01\x00"), parse_bool) + with raises(ValueError): + parse_optional(io.BytesIO(b"\xff\x00"), parse_bool) - # EOF - with raises(AssertionError): - parse_optional(io.BytesIO(b"\x01"), parse_bool) - # optional must be 0 or 1 - with raises(ValueError): - parse_optional(io.BytesIO(b"\x02\x00"), parse_bool) +def test_parse_bytes(): - with raises(ValueError): - parse_optional(io.BytesIO(b"\xff\x00"), parse_bool) + assert parse_bytes(io.BytesIO(b"\x00\x00\x00\x00")) == b"" + assert parse_bytes(io.BytesIO(b"\x00\x00\x00\x01\xff")) == b"\xff" - def test_parse_bytes(self): + # 512 bytes + assert parse_bytes(io.BytesIO(b"\x00\x00\x02\x00" + b"a" * 512)) == b"a" * 512 - assert parse_bytes(io.BytesIO(b"\x00\x00\x00\x00")) == b"" - assert parse_bytes(io.BytesIO(b"\x00\x00\x00\x01\xff")) == b"\xff" + # 255 bytes + assert parse_bytes(io.BytesIO(b"\x00\x00\x00\xff" + b"b" * 255)) == b"b" * 255 - # 512 bytes - assert parse_bytes(io.BytesIO(b"\x00\x00\x02\x00" + b"a" * 512)) == b"a" * 512 + # EOF + with raises(AssertionError): + parse_bytes(io.BytesIO(b"\x00\x00\x00\xff\x01\x02\x03")) - # 255 bytes - assert parse_bytes(io.BytesIO(b"\x00\x00\x00\xff" + b"b" * 255)) == b"b" * 255 + with raises(AssertionError): + parse_bytes(io.BytesIO(b"\xff\xff\xff\xff")) - # EOF - with raises(AssertionError): - parse_bytes(io.BytesIO(b"\x00\x00\x00\xff\x01\x02\x03")) + with raises(AssertionError): + parse_bytes(io.BytesIO(b"\xff\xff\xff\xff" + b"a" * 512)) - with raises(AssertionError): - parse_bytes(io.BytesIO(b"\xff\xff\xff\xff")) + # EOF off by one + with raises(AssertionError): + parse_bytes(io.BytesIO(b"\x00\x00\x02\x01" + b"a" * 512)) - with raises(AssertionError): - parse_bytes(io.BytesIO(b"\xff\xff\xff\xff" + b"a" * 512)) - # EOF off by one - with raises(AssertionError): - parse_bytes(io.BytesIO(b"\x00\x00\x02\x01" + b"a" * 512)) +def test_parse_list(): - def test_parse_list(self): + assert parse_list(io.BytesIO(b"\x00\x00\x00\x00"), parse_bool) == [] + assert parse_list(io.BytesIO(b"\x00\x00\x00\x01\x01"), parse_bool) == [True] + assert parse_list(io.BytesIO(b"\x00\x00\x00\x03\x01\x00\x01"), parse_bool) == [True, False, True] - assert parse_list(io.BytesIO(b"\x00\x00\x00\x00"), parse_bool) == [] - assert parse_list(io.BytesIO(b"\x00\x00\x00\x01\x01"), parse_bool) == [True] - assert parse_list(io.BytesIO(b"\x00\x00\x00\x03\x01\x00\x01"), parse_bool) == [True, False, True] + # EOF + with raises(AssertionError): + parse_list(io.BytesIO(b"\x00\x00\x00\x01"), parse_bool) - # EOF - with raises(AssertionError): - parse_list(io.BytesIO(b"\x00\x00\x00\x01"), parse_bool) + with raises(AssertionError): + parse_list(io.BytesIO(b"\x00\x00\x00\xff\x00\x00"), parse_bool) - with raises(AssertionError): - parse_list(io.BytesIO(b"\x00\x00\x00\xff\x00\x00"), parse_bool) + with raises(AssertionError): + parse_list(io.BytesIO(b"\xff\xff\xff\xff\x00\x00"), parse_bool) - with raises(AssertionError): - parse_list(io.BytesIO(b"\xff\xff\xff\xff\x00\x00"), parse_bool) + # failure to parser internal type + with raises(ValueError): + parse_list(io.BytesIO(b"\x00\x00\x00\x01\x02"), parse_bool) - # failure to parser internal type - with raises(ValueError): - parse_list(io.BytesIO(b"\x00\x00\x00\x01\x02"), parse_bool) - def test_parse_tuple(self): +def test_parse_tuple(): - assert parse_tuple(io.BytesIO(b""), []) == () - assert parse_tuple(io.BytesIO(b"\x00\x00"), [parse_bool, parse_bool]) == (False, False) - assert parse_tuple(io.BytesIO(b"\x00\x01"), [parse_bool, parse_bool]) == (False, True) + assert parse_tuple(io.BytesIO(b""), []) == () + assert parse_tuple(io.BytesIO(b"\x00\x00"), [parse_bool, parse_bool]) == (False, False) + assert parse_tuple(io.BytesIO(b"\x00\x01"), [parse_bool, parse_bool]) == (False, True) - # error in parsing internal type - with raises(ValueError): - parse_tuple(io.BytesIO(b"\x00\x02"), [parse_bool, parse_bool]) + # error in parsing internal type + with raises(ValueError): + parse_tuple(io.BytesIO(b"\x00\x02"), [parse_bool, parse_bool]) - # EOF - with raises(AssertionError): - parse_tuple(io.BytesIO(b"\x00"), [parse_bool, parse_bool]) + # EOF + with raises(AssertionError): + parse_tuple(io.BytesIO(b"\x00"), [parse_bool, parse_bool]) - def test_parse_size_hints(self): - class TestFromBytes: - b: bytes - @classmethod - def from_bytes(self, b): - ret = TestFromBytes() - ret.b = b - return ret +def test_parse_size_hints(): + class TestFromBytes: + b: bytes - assert parse_size_hints(io.BytesIO(b"1337"), TestFromBytes, 4).b == b"1337" + @classmethod + def from_bytes(cls, b): + ret = TestFromBytes() + ret.b = b + return ret - # EOF - with raises(AssertionError): - parse_size_hints(io.BytesIO(b"133"), TestFromBytes, 4) + assert parse_size_hints(io.BytesIO(b"1337"), TestFromBytes, 4).b == b"1337" - class FailFromBytes: - @classmethod - def from_bytes(self, b): - raise ValueError() + # EOF + with raises(AssertionError): + parse_size_hints(io.BytesIO(b"133"), TestFromBytes, 4) - # error in underlying type - with raises(ValueError): - parse_size_hints(io.BytesIO(b"1337"), FailFromBytes, 4) + class FailFromBytes: + @classmethod + def from_bytes(cls, b): + raise ValueError() - def test_parse_str(self): + # error in underlying type + with raises(ValueError): + parse_size_hints(io.BytesIO(b"1337"), FailFromBytes, 4) - assert parse_str(io.BytesIO(b"\x00\x00\x00\x00")) == "" - assert parse_str(io.BytesIO(b"\x00\x00\x00\x01a")) == "a" - # 512 bytes - assert parse_str(io.BytesIO(b"\x00\x00\x02\x00" + b"a" * 512)) == "a" * 512 +def test_parse_str(): - # 255 bytes - assert parse_str(io.BytesIO(b"\x00\x00\x00\xff" + b"b" * 255)) == "b" * 255 + assert parse_str(io.BytesIO(b"\x00\x00\x00\x00")) == "" + assert parse_str(io.BytesIO(b"\x00\x00\x00\x01a")) == "a" - # EOF - with raises(AssertionError): - parse_str(io.BytesIO(b"\x00\x00\x00\xff\x01\x02\x03")) + # 512 bytes + assert parse_str(io.BytesIO(b"\x00\x00\x02\x00" + b"a" * 512)) == "a" * 512 - with raises(AssertionError): - parse_str(io.BytesIO(b"\xff\xff\xff\xff")) + # 255 bytes + assert parse_str(io.BytesIO(b"\x00\x00\x00\xff" + b"b" * 255)) == "b" * 255 - with raises(AssertionError): - parse_str(io.BytesIO(b"\xff\xff\xff\xff" + b"a" * 512)) + # EOF + with raises(AssertionError): + parse_str(io.BytesIO(b"\x00\x00\x00\xff\x01\x02\x03")) - # EOF off by one - with raises(AssertionError): - parse_str(io.BytesIO(b"\x00\x00\x02\x01" + b"a" * 512)) + with raises(AssertionError): + parse_str(io.BytesIO(b"\xff\xff\xff\xff")) + with raises(AssertionError): + parse_str(io.BytesIO(b"\xff\xff\xff\xff" + b"a" * 512)) -if __name__ == "__main__": - unittest.main() + # EOF off by one + with raises(AssertionError): + parse_str(io.BytesIO(b"\x00\x00\x02\x01" + b"a" * 512)) From 65deb79c41c23b34c47bea44536ae2df9b109e08 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Tue, 8 Mar 2022 11:49:02 -0500 Subject: [PATCH 176/378] make sync fixtures not use async def (#10504) This is particularly relevant in cases where the scope is not function as that forces use of a wider scoped event loop fixture as well. --- tests/conftest.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index e7bb78dad5e0..d0cad74baff7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -48,22 +48,22 @@ def softfork_height(request): block_format_version = "rc4" -@pytest_asyncio.fixture(scope="session") -async def default_400_blocks(): +@pytest.fixture(scope="session") +def default_400_blocks(): from tests.util.blockchain import persistent_blocks return persistent_blocks(400, f"test_blocks_400_{block_format_version}.db", seed=b"alternate2") -@pytest_asyncio.fixture(scope="session") -async def default_1000_blocks(): +@pytest.fixture(scope="session") +def default_1000_blocks(): from tests.util.blockchain import persistent_blocks return persistent_blocks(1000, f"test_blocks_1000_{block_format_version}.db") -@pytest_asyncio.fixture(scope="session") -async def pre_genesis_empty_slots_1000_blocks(): +@pytest.fixture(scope="session") +def pre_genesis_empty_slots_1000_blocks(): from tests.util.blockchain import persistent_blocks return persistent_blocks( @@ -71,22 +71,22 @@ async def pre_genesis_empty_slots_1000_blocks(): ) -@pytest_asyncio.fixture(scope="session") -async def default_10000_blocks(): +@pytest.fixture(scope="session") +def default_10000_blocks(): from tests.util.blockchain import persistent_blocks return persistent_blocks(10000, f"test_blocks_10000_{block_format_version}.db") -@pytest_asyncio.fixture(scope="session") -async def default_20000_blocks(): +@pytest.fixture(scope="session") +def default_20000_blocks(): from tests.util.blockchain import persistent_blocks return persistent_blocks(20000, f"test_blocks_20000_{block_format_version}.db") -@pytest_asyncio.fixture(scope="session") -async def default_10000_blocks_compact(): +@pytest.fixture(scope="session") +def default_10000_blocks_compact(): from tests.util.blockchain import persistent_blocks return persistent_blocks( @@ -99,7 +99,7 @@ async def default_10000_blocks_compact(): ) -@pytest_asyncio.fixture(scope="function") -async def tmp_dir(): +@pytest.fixture(scope="function") +def tmp_dir(): with tempfile.TemporaryDirectory() as folder: yield Path(folder) From 6ff6fbe7de58748cbfa91eb7578dc027684ee6bf Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Tue, 8 Mar 2022 11:49:20 -0500 Subject: [PATCH 177/378] Fixup some hidden test errors (#10442) --- tests/core/full_node/test_full_node.py | 7 ++++--- tests/core/full_node/test_mempool.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/core/full_node/test_full_node.py b/tests/core/full_node/test_full_node.py index 3e54c2e1015d..07d3c0054cf5 100644 --- a/tests/core/full_node/test_full_node.py +++ b/tests/core/full_node/test_full_node.py @@ -1085,7 +1085,7 @@ async def test_respond_transaction_fail(self, wallet_nodes): receiver_puzzlehash = wallet_receiver.get_new_puzzlehash() blocks_new = bt.get_consecutive_blocks( - 2, + 3, block_list_input=blocks, guarantee_transaction_block=True, farmer_reward_puzzle_hash=cb_ph, @@ -1095,10 +1095,11 @@ async def test_respond_transaction_fail(self, wallet_nodes): while incoming_queue.qsize() > 0: await incoming_queue.get() + await full_node_1.full_node.respond_block(fnp.RespondBlock(blocks_new[-3]), peer) await full_node_1.full_node.respond_block(fnp.RespondBlock(blocks_new[-2]), peer) await full_node_1.full_node.respond_block(fnp.RespondBlock(blocks_new[-1]), peer) - await time_out_assert(10, time_out_messages(incoming_queue, "new_peak", 2)) + await time_out_assert(10, time_out_messages(incoming_queue, "new_peak", 3)) # Invalid transaction does not propagate spend_bundle = wallet_a.generate_signed_transaction( 100000000000000, @@ -1120,7 +1121,7 @@ async def test_request_block(self, wallet_nodes): blocks = await full_node_1.get_all_full_blocks() blocks = bt.get_consecutive_blocks( - 2, + 3, block_list_input=blocks, guarantee_transaction_block=True, farmer_reward_puzzle_hash=wallet_a.get_new_puzzlehash(), diff --git a/tests/core/full_node/test_mempool.py b/tests/core/full_node/test_mempool.py index 2b60c35e178c..6b0437c8061d 100644 --- a/tests/core/full_node/test_mempool.py +++ b/tests/core/full_node/test_mempool.py @@ -2365,7 +2365,7 @@ async def test_invalid_coin_spend_coin(self, two_nodes): 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_2, blocks[-1].height) + await time_out_assert(60, node_height_at_least, True, full_node_1, blocks[-1].height) spend_bundle = generate_test_spend_bundle(list(blocks[-1].get_included_reward_coins())[0]) coin_spend_0 = recursive_replace(spend_bundle.coin_spends[0], "coin.puzzle_hash", bytes32([1] * 32)) From 5b2f5772780d0370f76b0134db6a7fdc7af42862 Mon Sep 17 00:00:00 2001 From: William Allen Date: Tue, 8 Mar 2022 19:28:22 -0600 Subject: [PATCH 178/378] Touching up changelog (#10584) * removing known issue that was only applicable to a beta release * Adding additional fixes to the changelog * Adding Unreleased section to track upcoming changes --- CHANGELOG.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b49c2d05208b..1e3d3c07a15f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project does not yet adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) for setuptools_scm/PEP 440 reasons. +## [Unreleased] + + ## 1.3.0 Chia blockchain 2022-3-07 ### Added: @@ -54,6 +57,8 @@ for setuptools_scm/PEP 440 reasons. - Removed the option to display "All" rows per page on the transactions page of the GUI. - Updated the background image for the MacOS installer. - Changed the behavior of what info is displayed if the database is still syncing. + - It should not be expected that wallet info, such as payout address, should not reflect what their desired values until everything has completed syncing. + - The payout instructions may not be editable via the GUI until syncing has completed. ### Fixed: @@ -67,7 +72,7 @@ for setuptools_scm/PEP 440 reasons. - Thanks to @risner for fixes related to using colorlog. - Fixed issues in reading the pool_list from config if set to null. - Fixed display info in CLI chia show -c when No Info should be displayed. -- Thanks to @madMAx42v3r for fixes in chiapos related to a possible race condition when multiple threads call Verifier::ValidateProof. +- Thanks to @madMAx43v3r for fixes in chiapos related to a possible race condition when multiple threads call Verifier::ValidateProof. - Thanks to @PastaPastaPasta for some compiler warning fixes in bls-signatures. - Thanks to @random-zebra for fixing a bug in the bls-signature copy assignment operator. - Thanks to @lourkeur for fixes in blspy related to pybind11 2.8+. @@ -76,18 +81,23 @@ for setuptools_scm/PEP 440 reasons. - Fixed issue where the DB could lose the peak of the chain when receiving a compressed block. - Fixed showing inbound transaction after an offer is cancelled. - Fixed blockchain fee "Value seems high" message showing up when it shouldn't. +- Bugs in pool farming where auth key was being set incorrectly, leading to invalid signature bugs. +- Memory leak in the full node sync store where peak hashes were stored without being pruned. +- Fixed a timelord issue which could cause a few blocks to not be infused on chain if a certain proof of space signs conflicting blocks. ### Known Issues: - When you are adding plots and you choose the option to “create a Plot NFT”, you will get an error message “Initial_target_state” and the plots will not get created. - Workaround: Create the Plot NFT first in the “Pool” tab, and then add your plots and choose the created plot NFT in the drop down. -- If you are installing on a machine for the first time, when the GUI loads and you don’t have any pre-existing wallet keys, the GUI will flicker and not load anything. - - Workaround: close and relaunch the GUI. - When you close the Chia app, regardless if you are in farmer mode or wallet, the content on the exit dialog isn’t correct. - If you start with wallet mode and then switch to farmer mode and back to wallet mode, the full node will continue to sync in the background. To get the full node to stop syncing after switching to wallet mode, you will need to close the Chia and relaunch the Chia app. - Wallets with large number of transactions or large number of coins will take longer to sync (more than a few minutes), but should take less time than a full node sync. It could fail in some cases. - Huge numbers cannot be put into amount/fee input for transactions in the GUI. - Some Linux systems experience excessive memory usage with the value *default*/*python_default*/*fork* configured for *multiprocessing_start_method:*. Setting this value to *spawn* may produce better results, but in some uncommon cases, is know to cause crashes. +- Sending a TX with too low of a fee can cause an infinite spinner in the GUI when the mempool is full. + - Workaround: Restart the GUI, or clear unconfirmed TX. +- Claiming rewards when self-pooling using CLI will show an error message, but it will actually create the transaction. + ## 1.2.11 Chia blockchain 2021-11-4 @@ -123,6 +133,7 @@ This release also includes several important performance improvements as a resul - PlotNFT transactions via CLI (e.g. `chia plotnft join`) now accept a fee parameter, but it is not yet operable. + ## 1.2.10 Chia blockchain 2021-10-25 We have some great improvements in this release: We launched our migration of keys to a common encrypted keyring.yaml file, and we secure this with an optional passphrase in both GUI and CLI. We've added a passphrase hint in case you forget your passphrase. More info on our [wiki](https://github.com/Chia-Network/chia-blockchain/wiki/Passphrase-Protected-Chia-Keys-and-Key-Storage-Migration). We also launched a new Chialisp compiler in clvm_tools_rs which substantially improves compile time for Chialisp developers. We also addressed a widely reported issue in which a system failure, such as a power outage, would require some farmers to sync their full node from zero. This release also includes several other improvements and fixes. From a0897d8d31b6a22e33462ebb9f22e1e6d6f5806e Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Wed, 9 Mar 2022 06:40:52 -0500 Subject: [PATCH 179/378] context manager for socket in find_available_listen_port(), catch OSError (#10567) --- tests/util/socket.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/util/socket.py b/tests/util/socket.py index f5f5bd7fb8cb..39077e0c650b 100644 --- a/tests/util/socket.py +++ b/tests/util/socket.py @@ -13,13 +13,12 @@ def find_available_listen_port(name: str = "free") -> int: if port in recent_ports: continue - s = socket.socket() - try: - s.bind(("127.0.0.1", port)) - except BaseException: - s.close() - continue - s.close() + with socket.socket() as s: + try: + s.bind(("127.0.0.1", port)) + except OSError: + continue + recent_ports.add(port) print(f"{name} port: {port}") return port From 0e29dbc6d41f16f9b301aa580342a7a9eb51dd07 Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Wed, 9 Mar 2022 12:41:18 +0100 Subject: [PATCH 180/378] Bump CAT wallet test timeout to 40 minutes (#10605) --- .github/workflows/build-test-macos-core-full_node.yml | 2 +- .github/workflows/build-test-macos-wallet-cat_wallet.yml | 2 +- .github/workflows/build-test-ubuntu-core-full_node.yml | 2 +- .github/workflows/build-test-ubuntu-wallet-cat_wallet.yml | 2 +- tests/core/full_node/config.py | 2 +- tests/wallet/cat_wallet/config.py | 1 + 6 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 tests/wallet/cat_wallet/config.py diff --git a/.github/workflows/build-test-macos-core-full_node.yml b/.github/workflows/build-test-macos-core-full_node.yml index e601da8605d5..486377fbf5fa 100644 --- a/.github/workflows/build-test-macos-core-full_node.yml +++ b/.github/workflows/build-test-macos-core-full_node.yml @@ -22,7 +22,7 @@ jobs: build: name: MacOS core-full_node Tests runs-on: ${{ matrix.os }} - timeout-minutes: 40 + timeout-minutes: 50 strategy: fail-fast: false max-parallel: 4 diff --git a/.github/workflows/build-test-macos-wallet-cat_wallet.yml b/.github/workflows/build-test-macos-wallet-cat_wallet.yml index d02f866147c7..7c31d0c14f22 100644 --- a/.github/workflows/build-test-macos-wallet-cat_wallet.yml +++ b/.github/workflows/build-test-macos-wallet-cat_wallet.yml @@ -22,7 +22,7 @@ jobs: build: name: MacOS wallet-cat_wallet Tests runs-on: ${{ matrix.os }} - timeout-minutes: 30 + timeout-minutes: 50 strategy: fail-fast: false max-parallel: 4 diff --git a/.github/workflows/build-test-ubuntu-core-full_node.yml b/.github/workflows/build-test-ubuntu-core-full_node.yml index 217d17bb8829..558c1df426ad 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node.yml @@ -22,7 +22,7 @@ jobs: build: name: Ubuntu core-full_node Test runs-on: ${{ matrix.os }} - timeout-minutes: 40 + timeout-minutes: 50 strategy: fail-fast: false max-parallel: 4 diff --git a/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml index d14e2a3119ae..a1392ae82787 100644 --- a/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml @@ -22,7 +22,7 @@ jobs: build: name: Ubuntu wallet-cat_wallet Test runs-on: ${{ matrix.os }} - timeout-minutes: 30 + timeout-minutes: 50 strategy: fail-fast: false max-parallel: 4 diff --git a/tests/core/full_node/config.py b/tests/core/full_node/config.py index 24f501b9f693..510676ac744e 100644 --- a/tests/core/full_node/config.py +++ b/tests/core/full_node/config.py @@ -1,5 +1,5 @@ # flake8: noqa: E501 -job_timeout = 40 +job_timeout = 50 CHECK_RESOURCE_USAGE = """ - name: Check resource usage run: | diff --git a/tests/wallet/cat_wallet/config.py b/tests/wallet/cat_wallet/config.py new file mode 100644 index 000000000000..a805fb7b880c --- /dev/null +++ b/tests/wallet/cat_wallet/config.py @@ -0,0 +1 @@ +job_timeout = 50 From ff324095ccc04422317a44705dcf1175cf3728e6 Mon Sep 17 00:00:00 2001 From: Adam Kelly <338792+aqk@users.noreply.github.com> Date: Thu, 10 Mar 2022 11:06:49 -0800 Subject: [PATCH 181/378] Remove block tools and keychain globals (#10358) * Use bt fixture * rebase * Use local_hostname * Fix test_json (inheritance from unittest.TestCase) * Use correct BlockTools fixture for test_simulation * Pass bt fixture into cost calculation tests * flake8 * Add missing parameters to test functions * Fix from rebase issues * Remove set_shared_instance * Update comment * Remove unneeded comments * Remove unused code * Remove unused code, run `multiprocessing.set_start_method("spawn")` at correct time. * Revert unrelated change * Set daemon_port. Teardown services in correct order. BIG thanks to Mariano Sorgente for debugging help. * Add back type signature - rebase issue * Apply review fixes from Jeff * Document why we need a later pytest-asyncio version * Correct type for _configure_legacy_backend * See what's going on during CI mypy run * github workflows * mypy typing * Remove legacy Keyring create method * Start daemon first * Shutdown daemon coroutine properly * Remove un-needed daemon_port argument * Set chia-blockchain-gui to hash in main * Remove connect_to_daemon_port * Remove code that set "daemon_port" before calling `setup_daemon` * Remove self_hostname fixture and extra self_hostname global * Fix two test files that were not importing self_hostname * self_hostname fixture * Remove more unused test code * Simplify fixture --- setup.py | 3 +- tests/block_tools.py | 28 +- tests/blockchain/test_blockchain.py | 182 +++---- .../test_blockchain_transactions.py | 36 +- tests/conftest.py | 55 +- tests/connection_utils.py | 7 +- tests/core/daemon/test_daemon.py | 54 +- .../full_node/full_sync/test_full_sync.py | 38 +- .../core/full_node/stores/test_block_store.py | 17 +- .../core/full_node/stores/test_coin_store.py | 27 +- .../core/full_node/stores/test_hint_store.py | 3 +- tests/core/full_node/test_full_node.py | 123 ++--- tests/core/full_node/test_mempool.py | 505 ++++++++++-------- .../full_node/test_mempool_performance.py | 8 +- tests/core/full_node/test_node_load.py | 10 +- tests/core/full_node/test_performance.py | 12 +- tests/core/full_node/test_transactions.py | 8 +- tests/core/server/test_dos.py | 14 +- tests/core/ssl/test_ssl.py | 50 +- tests/core/test_cost_calculation.py | 6 +- tests/core/test_daemon_rpc.py | 13 +- tests/core/test_farmer_harvester_rpc.py | 12 +- tests/core/test_filter.py | 4 +- tests/core/test_full_node_rpc.py | 18 +- tests/core/test_merkle_set.py | 3 +- tests/core/util/test_streamable.py | 9 +- .../farmer_harvester/test_farmer_harvester.py | 10 +- tests/plotting/test_plot_manager.py | 5 +- tests/pools/test_pool_rpc.py | 39 +- tests/setup_nodes.py | 159 ++++-- tests/simulation/test_simulation.py | 11 +- tests/util/blockchain.py | 6 +- tests/wallet/cat_wallet/test_cat_wallet.py | 16 +- .../wallet/cat_wallet/test_offer_lifecycle.py | 2 +- tests/wallet/cat_wallet/test_trades.py | 4 +- tests/wallet/did_wallet/test_did.py | 10 +- tests/wallet/did_wallet/test_did_rpc.py | 4 +- tests/wallet/rl_wallet/test_rl_rpc.py | 4 +- tests/wallet/rl_wallet/test_rl_wallet.py | 4 +- tests/wallet/rpc/test_wallet_rpc.py | 10 +- .../simple_sync/test_simple_sync_protocol.py | 28 +- tests/wallet/sync/test_wallet_sync.py | 38 +- tests/wallet/test_wallet.py | 24 +- tests/wallet/test_wallet_blockchain.py | 4 +- tests/wallet/test_wallet_key_val_store.py | 3 +- tests/weight_proof/test_weight_proof.py | 6 +- 46 files changed, 889 insertions(+), 743 deletions(-) diff --git a/setup.py b/setup.py index e795477d13bd..65b344b1d620 100644 --- a/setup.py +++ b/setup.py @@ -44,8 +44,7 @@ "build", "pre-commit", "pytest", - # >=0.17.0 for the fixture decorator - "pytest-asyncio >=0.17.0", + "pytest-asyncio>=0.18.1", # require attribute 'fixture' "pytest-monitor; sys_platform == 'linux'", "pytest-xdist", "twine", diff --git a/tests/block_tools.py b/tests/block_tools.py index e667c50dc959..d364cbcaf14e 100644 --- a/tests/block_tools.py +++ b/tests/block_tools.py @@ -153,7 +153,7 @@ def __init__( self._config[service]["selected_network"] = "testnet0" # some tests start the daemon, make sure it's on a free port - self._config["daemon_port"] = find_available_listen_port("daemon port") + self._config["daemon_port"] = find_available_listen_port("BlockTools daemon") save_config(self.root_path, "config.yaml", self._config) overrides = self._config["network_overrides"]["constants"][self._config["selected_network"]] @@ -2012,13 +2012,34 @@ def create_test_unfinished_block( ) +# Remove these counters when `create_block_tools` and `create_block_tools_async` are removed +create_block_tools_async_count = 0 +create_block_tools_count = 0 + +# Note: tests that still use `create_block_tools` and `create_block_tools_async` should probably be +# moved to the bt fixture in conftest.py. Take special care to find out if the users of these functions +# need different BlockTools instances + +# All tests need different root directories containing different config.yaml files. +# The daemon's listen port is configured in the config.yaml, and the only way a test can control which +# listen port it uses is to write it to the config file. + + async def create_block_tools_async( constants: ConsensusConstants = test_constants, root_path: Optional[Path] = None, const_dict=None, keychain: Optional[Keychain] = None, ) -> BlockTools: - bt = BlockTools(constants, root_path, const_dict, keychain) + 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, + ) await bt.setup_keys() await bt.setup_plots() @@ -2031,6 +2052,9 @@ def create_block_tools( const_dict=None, keychain: Optional[Keychain] = 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) asyncio.get_event_loop().run_until_complete(bt.setup_keys()) diff --git a/tests/blockchain/test_blockchain.py b/tests/blockchain/test_blockchain.py index 4d35861a6095..c9490261231e 100644 --- a/tests/blockchain/test_blockchain.py +++ b/tests/blockchain/test_blockchain.py @@ -50,7 +50,7 @@ _validate_and_add_block_no_error, ) from tests.wallet_tools import WalletTool -from tests.setup_nodes import bt, test_constants +from tests.setup_nodes import test_constants from tests.util.blockchain import create_blockchain from tests.util.keyring import TempKeyring from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import ( @@ -94,29 +94,29 @@ async def test_block_tools_proofs(self): raise Exception("invalid proof") @pytest.mark.asyncio - async def test_non_overflow_genesis(self, empty_blockchain): + async def test_non_overflow_genesis(self, empty_blockchain, bt): assert empty_blockchain.get_peak() is None genesis = bt.get_consecutive_blocks(1, force_overflow=False)[0] await _validate_and_add_block(empty_blockchain, genesis) assert empty_blockchain.get_peak().height == 0 @pytest.mark.asyncio - async def test_overflow_genesis(self, empty_blockchain): + async def test_overflow_genesis(self, empty_blockchain, bt): genesis = bt.get_consecutive_blocks(1, force_overflow=True)[0] await _validate_and_add_block(empty_blockchain, genesis) @pytest.mark.asyncio - async def test_genesis_empty_slots(self, empty_blockchain): + async def test_genesis_empty_slots(self, empty_blockchain, bt): genesis = bt.get_consecutive_blocks(1, force_overflow=False, skip_slots=30)[0] await _validate_and_add_block(empty_blockchain, genesis) @pytest.mark.asyncio - async def test_overflow_genesis_empty_slots(self, empty_blockchain): + async def test_overflow_genesis_empty_slots(self, empty_blockchain, bt): genesis = bt.get_consecutive_blocks(1, force_overflow=True, skip_slots=3)[0] await _validate_and_add_block(empty_blockchain, genesis) @pytest.mark.asyncio - async def test_genesis_validate_1(self, empty_blockchain): + async def test_genesis_validate_1(self, empty_blockchain, bt): genesis = bt.get_consecutive_blocks(1, force_overflow=False)[0] bad_prev = bytes([1] * 32) genesis = recursive_replace(genesis, "foliage.prev_block_hash", bad_prev) @@ -251,7 +251,7 @@ async def test_long_chain(self, empty_blockchain, default_1000_blocks): assert empty_blockchain.get_peak().height == len(blocks) - 1 @pytest.mark.asyncio - async def test_unfinished_blocks(self, empty_blockchain, softfork_height): + async def test_unfinished_blocks(self, empty_blockchain, softfork_height, bt): blockchain = empty_blockchain blocks = bt.get_consecutive_blocks(3) for block in blocks[:-1]: @@ -301,13 +301,13 @@ async def test_unfinished_blocks(self, empty_blockchain, softfork_height): assert validate_res.error is None @pytest.mark.asyncio - async def test_empty_genesis(self, empty_blockchain): + async def test_empty_genesis(self, empty_blockchain, bt): blockchain = empty_blockchain for block in bt.get_consecutive_blocks(2, skip_slots=3): await _validate_and_add_block(empty_blockchain, block) @pytest.mark.asyncio - async def test_empty_slots_non_genesis(self, empty_blockchain): + async def test_empty_slots_non_genesis(self, empty_blockchain, bt): blockchain = empty_blockchain blocks = bt.get_consecutive_blocks(10) for block in blocks: @@ -319,7 +319,7 @@ async def test_empty_slots_non_genesis(self, empty_blockchain): assert blockchain.get_peak().height == 19 @pytest.mark.asyncio - async def test_one_sb_per_slot(self, empty_blockchain): + async def test_one_sb_per_slot(self, empty_blockchain, bt): blockchain = empty_blockchain num_blocks = 20 blocks = [] @@ -329,7 +329,7 @@ async def test_one_sb_per_slot(self, empty_blockchain): assert blockchain.get_peak().height == num_blocks - 1 @pytest.mark.asyncio - async def test_all_overflow(self, empty_blockchain): + async def test_all_overflow(self, empty_blockchain, bt): blockchain = empty_blockchain num_rounds = 5 blocks = [] @@ -342,7 +342,7 @@ async def test_all_overflow(self, empty_blockchain): assert blockchain.get_peak().height == num_blocks - 1 @pytest.mark.asyncio - async def test_unf_block_overflow(self, empty_blockchain, softfork_height): + async def test_unf_block_overflow(self, empty_blockchain, softfork_height, bt): blockchain = empty_blockchain blocks = [] @@ -386,7 +386,7 @@ async def test_unf_block_overflow(self, empty_blockchain, softfork_height): await _validate_and_add_block(blockchain, blocks[-1]) @pytest.mark.asyncio - async def test_one_sb_per_two_slots(self, empty_blockchain): + async def test_one_sb_per_two_slots(self, empty_blockchain, bt): blockchain = empty_blockchain num_blocks = 20 blocks = [] @@ -396,7 +396,7 @@ async def test_one_sb_per_two_slots(self, empty_blockchain): assert blockchain.get_peak().height == num_blocks - 1 @pytest.mark.asyncio - async def test_one_sb_per_five_slots(self, empty_blockchain): + async def test_one_sb_per_five_slots(self, empty_blockchain, bt): blockchain = empty_blockchain num_blocks = 10 blocks = [] @@ -406,14 +406,14 @@ async def test_one_sb_per_five_slots(self, empty_blockchain): assert blockchain.get_peak().height == num_blocks - 1 @pytest.mark.asyncio - async def test_basic_chain_overflow(self, empty_blockchain): + async def test_basic_chain_overflow(self, empty_blockchain, bt): blocks = bt.get_consecutive_blocks(5, force_overflow=True) for block in blocks: await _validate_and_add_block(empty_blockchain, block) assert empty_blockchain.get_peak().height == len(blocks) - 1 @pytest.mark.asyncio - async def test_one_sb_per_two_slots_force_overflow(self, empty_blockchain): + async def test_one_sb_per_two_slots_force_overflow(self, empty_blockchain, bt): blockchain = empty_blockchain num_blocks = 10 blocks = [] @@ -423,7 +423,7 @@ async def test_one_sb_per_two_slots_force_overflow(self, empty_blockchain): assert blockchain.get_peak().height == num_blocks - 1 @pytest.mark.asyncio - async def test_invalid_prev(self, empty_blockchain): + async def test_invalid_prev(self, empty_blockchain, bt): # 1 blocks = bt.get_consecutive_blocks(2, force_overflow=False) await _validate_and_add_block(empty_blockchain, blocks[0]) @@ -432,7 +432,7 @@ async def test_invalid_prev(self, empty_blockchain): await _validate_and_add_block(empty_blockchain, block_1_bad, expected_error=Err.INVALID_PREV_BLOCK_HASH) @pytest.mark.asyncio - async def test_invalid_pospace(self, empty_blockchain): + async def test_invalid_pospace(self, empty_blockchain, bt): # 2 blocks = bt.get_consecutive_blocks(2, force_overflow=False) await _validate_and_add_block(empty_blockchain, blocks[0]) @@ -441,7 +441,7 @@ async def test_invalid_pospace(self, empty_blockchain): await _validate_and_add_block(empty_blockchain, block_1_bad, expected_error=Err.INVALID_POSPACE) @pytest.mark.asyncio - async def test_invalid_sub_slot_challenge_hash_genesis(self, empty_blockchain): + async def test_invalid_sub_slot_challenge_hash_genesis(self, empty_blockchain, bt): # 2a blocks = bt.get_consecutive_blocks(1, force_overflow=False, skip_slots=1) new_finished_ss = recursive_replace( @@ -467,7 +467,7 @@ async def test_invalid_sub_slot_challenge_hash_genesis(self, empty_blockchain): await _validate_and_add_block(empty_blockchain, block_0_bad, expected_result=ReceiveBlockResult.INVALID_BLOCK) @pytest.mark.asyncio - async def test_invalid_sub_slot_challenge_hash_non_genesis(self, empty_blockchain): + async def test_invalid_sub_slot_challenge_hash_non_genesis(self, empty_blockchain, bt): # 2b blocks = bt.get_consecutive_blocks(1, force_overflow=False, skip_slots=0) blocks = bt.get_consecutive_blocks(1, force_overflow=False, skip_slots=1, block_list_input=blocks) @@ -494,7 +494,7 @@ async def test_invalid_sub_slot_challenge_hash_non_genesis(self, empty_blockchai await _validate_and_add_block(empty_blockchain, block_1_bad, expected_result=ReceiveBlockResult.INVALID_BLOCK) @pytest.mark.asyncio - async def test_invalid_sub_slot_challenge_hash_empty_ss(self, empty_blockchain): + async def test_invalid_sub_slot_challenge_hash_empty_ss(self, empty_blockchain, bt): # 2c blocks = bt.get_consecutive_blocks(1, force_overflow=False, skip_slots=0) blocks = bt.get_consecutive_blocks(1, force_overflow=False, skip_slots=2, block_list_input=blocks) @@ -521,7 +521,7 @@ async def test_invalid_sub_slot_challenge_hash_empty_ss(self, empty_blockchain): await _validate_and_add_block(empty_blockchain, block_1_bad, expected_result=ReceiveBlockResult.INVALID_BLOCK) @pytest.mark.asyncio - async def test_genesis_no_icc(self, empty_blockchain): + async def test_genesis_no_icc(self, empty_blockchain, bt): # 2d blocks = bt.get_consecutive_blocks(1, force_overflow=False, skip_slots=1) new_finished_ss = recursive_replace( @@ -627,7 +627,7 @@ async def test_invalid_icc_sub_slot_vdf(self, db_version): await self.do_test_invalid_icc_sub_slot_vdf(keychain, db_version) @pytest.mark.asyncio - async def test_invalid_icc_into_cc(self, empty_blockchain): + async def test_invalid_icc_into_cc(self, empty_blockchain, bt): blockchain = empty_blockchain blocks = bt.get_consecutive_blocks(1) await _validate_and_add_block(blockchain, blocks[0]) @@ -718,7 +718,7 @@ async def test_invalid_icc_into_cc(self, empty_blockchain): await _validate_and_add_block(blockchain, block) @pytest.mark.asyncio - async def test_empty_slot_no_ses(self, empty_blockchain): + async def test_empty_slot_no_ses(self, empty_blockchain, bt): # 2l blockchain = empty_blockchain blocks = bt.get_consecutive_blocks(1) @@ -747,7 +747,7 @@ async def test_empty_slot_no_ses(self, empty_blockchain): await _validate_and_add_block(blockchain, block_bad, expected_result=ReceiveBlockResult.INVALID_BLOCK) @pytest.mark.asyncio - async def test_empty_sub_slots_epoch(self, empty_blockchain): + async def test_empty_sub_slots_epoch(self, empty_blockchain, bt): # 2m # Tests adding an empty sub slot after the sub-epoch / epoch. # Also tests overflow block in epoch @@ -764,7 +764,7 @@ async def test_empty_sub_slots_epoch(self, empty_blockchain): ) @pytest.mark.asyncio - async def test_wrong_cc_hash_rc(self, empty_blockchain): + async def test_wrong_cc_hash_rc(self, empty_blockchain, bt): # 2o blockchain = empty_blockchain blocks = bt.get_consecutive_blocks(1, skip_slots=1) @@ -783,7 +783,7 @@ async def test_wrong_cc_hash_rc(self, empty_blockchain): await _validate_and_add_block(blockchain, block_1_bad, expected_error=Err.INVALID_CHALLENGE_SLOT_HASH_RC) @pytest.mark.asyncio - async def test_invalid_cc_sub_slot_vdf(self, empty_blockchain): + async def test_invalid_cc_sub_slot_vdf(self, empty_blockchain, bt): # 2q blocks = bt.get_consecutive_blocks(10) @@ -868,7 +868,7 @@ async def test_invalid_cc_sub_slot_vdf(self, empty_blockchain): await _validate_and_add_block(empty_blockchain, block) @pytest.mark.asyncio - async def test_invalid_rc_sub_slot_vdf(self, empty_blockchain): + async def test_invalid_rc_sub_slot_vdf(self, empty_blockchain, bt): # 2p blocks = bt.get_consecutive_blocks(10) for block in blocks: @@ -932,7 +932,7 @@ async def test_invalid_rc_sub_slot_vdf(self, empty_blockchain): await _validate_and_add_block(empty_blockchain, block) @pytest.mark.asyncio - async def test_genesis_bad_deficit(self, empty_blockchain): + async def test_genesis_bad_deficit(self, empty_blockchain, bt): # 2r block = bt.get_consecutive_blocks(1, skip_slots=2)[0] new_finished_ss = recursive_replace( @@ -948,7 +948,7 @@ async def test_genesis_bad_deficit(self, empty_blockchain): await _validate_and_add_block(empty_blockchain, block_bad, expected_error=Err.INVALID_DEFICIT) @pytest.mark.asyncio - async def test_reset_deficit(self, empty_blockchain): + async def test_reset_deficit(self, empty_blockchain, bt): # 2s, 2t blockchain = empty_blockchain blocks = bt.get_consecutive_blocks(2) @@ -982,7 +982,7 @@ async def test_reset_deficit(self, empty_blockchain): await _validate_and_add_block(empty_blockchain, blocks[-1]) @pytest.mark.asyncio - async def test_genesis_has_ses(self, empty_blockchain): + async def test_genesis_has_ses(self, empty_blockchain, bt): # 3a block = bt.get_consecutive_blocks(1, skip_slots=1)[0] new_finished_ss = recursive_replace( @@ -1010,7 +1010,7 @@ async def test_genesis_has_ses(self, empty_blockchain): ) @pytest.mark.asyncio - async def test_no_ses_if_no_se(self, empty_blockchain): + async def test_no_ses_if_no_se(self, empty_blockchain, bt): # 3b blocks = bt.get_consecutive_blocks(1) await _validate_and_add_block(empty_blockchain, blocks[0]) @@ -1056,7 +1056,7 @@ async def test_too_many_blocks(self, empty_blockchain): pass @pytest.mark.asyncio - async def test_bad_pos(self, empty_blockchain): + async def test_bad_pos(self, empty_blockchain, bt): # 5 blocks = bt.get_consecutive_blocks(2) await _validate_and_add_block(empty_blockchain, blocks[0]) @@ -1094,7 +1094,7 @@ async def test_bad_pos(self, empty_blockchain): # TODO: test not passing the plot filter @pytest.mark.asyncio - async def test_bad_signage_point_index(self, empty_blockchain): + async def test_bad_signage_point_index(self, empty_blockchain, bt): # 6 blocks = bt.get_consecutive_blocks(2) await _validate_and_add_block(empty_blockchain, blocks[0]) @@ -1111,7 +1111,7 @@ async def test_bad_signage_point_index(self, empty_blockchain): await _validate_and_add_block(empty_blockchain, block_bad, expected_error=Err.INVALID_SP_INDEX) @pytest.mark.asyncio - async def test_sp_0_no_sp(self, empty_blockchain): + async def test_sp_0_no_sp(self, empty_blockchain, bt): # 7 blocks = [] case_1, case_2 = False, False @@ -1136,7 +1136,7 @@ async def test_epoch_overflows(self, empty_blockchain): pass @pytest.mark.asyncio - async def test_bad_total_iters(self, empty_blockchain): + async def test_bad_total_iters(self, empty_blockchain, bt): # 10 blocks = bt.get_consecutive_blocks(2) await _validate_and_add_block(empty_blockchain, blocks[0]) @@ -1147,7 +1147,7 @@ async def test_bad_total_iters(self, empty_blockchain): await _validate_and_add_block(empty_blockchain, block_bad, expected_error=Err.INVALID_TOTAL_ITERS) @pytest.mark.asyncio - async def test_bad_rc_sp_vdf(self, empty_blockchain): + async def test_bad_rc_sp_vdf(self, empty_blockchain, bt): # 11 blocks = bt.get_consecutive_blocks(1) await _validate_and_add_block(empty_blockchain, blocks[0]) @@ -1181,7 +1181,7 @@ async def test_bad_rc_sp_vdf(self, empty_blockchain): await _validate_and_add_block(empty_blockchain, blocks[-1]) @pytest.mark.asyncio - async def test_bad_rc_sp_sig(self, empty_blockchain): + async def test_bad_rc_sp_sig(self, empty_blockchain, bt): # 12 blocks = bt.get_consecutive_blocks(2) await _validate_and_add_block(empty_blockchain, blocks[0]) @@ -1189,7 +1189,7 @@ async def test_bad_rc_sp_sig(self, empty_blockchain): await _validate_and_add_block(empty_blockchain, block_bad, expected_error=Err.INVALID_RC_SIGNATURE) @pytest.mark.asyncio - async def test_bad_cc_sp_vdf(self, empty_blockchain): + async def test_bad_cc_sp_vdf(self, empty_blockchain, bt): # 13. Note: does not validate fully due to proof of space being validated first blocks = bt.get_consecutive_blocks(1) @@ -1230,7 +1230,7 @@ async def test_bad_cc_sp_vdf(self, empty_blockchain): await _validate_and_add_block(empty_blockchain, blocks[-1]) @pytest.mark.asyncio - async def test_bad_cc_sp_sig(self, empty_blockchain): + async def test_bad_cc_sp_sig(self, empty_blockchain, bt): # 14 blocks = bt.get_consecutive_blocks(2) await _validate_and_add_block(empty_blockchain, blocks[0]) @@ -1245,7 +1245,7 @@ async def test_is_transaction_block(self, empty_blockchain): pass @pytest.mark.asyncio - async def test_bad_foliage_sb_sig(self, empty_blockchain): + async def test_bad_foliage_sb_sig(self, empty_blockchain, bt): # 16 blocks = bt.get_consecutive_blocks(2) await _validate_and_add_block(empty_blockchain, blocks[0]) @@ -1253,7 +1253,7 @@ async def test_bad_foliage_sb_sig(self, empty_blockchain): await _validate_and_add_block(empty_blockchain, block_bad, expected_error=Err.INVALID_PLOT_SIGNATURE) @pytest.mark.asyncio - async def test_bad_foliage_transaction_block_sig(self, empty_blockchain): + async def test_bad_foliage_transaction_block_sig(self, empty_blockchain, bt): # 17 blocks = bt.get_consecutive_blocks(1) await _validate_and_add_block(empty_blockchain, blocks[0]) @@ -1269,7 +1269,7 @@ async def test_bad_foliage_transaction_block_sig(self, empty_blockchain): await _validate_and_add_block(empty_blockchain, blocks[-1]) @pytest.mark.asyncio - async def test_unfinished_reward_chain_sb_hash(self, empty_blockchain): + async def test_unfinished_reward_chain_sb_hash(self, empty_blockchain, bt): # 18 blocks = bt.get_consecutive_blocks(2) await _validate_and_add_block(empty_blockchain, blocks[0]) @@ -1282,7 +1282,7 @@ async def test_unfinished_reward_chain_sb_hash(self, empty_blockchain): await _validate_and_add_block(empty_blockchain, block_bad, expected_error=Err.INVALID_URSB_HASH) @pytest.mark.asyncio - async def test_pool_target_height(self, empty_blockchain): + async def test_pool_target_height(self, empty_blockchain, bt): # 19 blocks = bt.get_consecutive_blocks(3) await _validate_and_add_block(empty_blockchain, blocks[0]) @@ -1294,7 +1294,7 @@ async def test_pool_target_height(self, empty_blockchain): await _validate_and_add_block(empty_blockchain, block_bad, expected_error=Err.OLD_POOL_TARGET) @pytest.mark.asyncio - async def test_pool_target_pre_farm(self, empty_blockchain): + async def test_pool_target_pre_farm(self, empty_blockchain, bt): # 20a blocks = bt.get_consecutive_blocks(1) block_bad: FullBlock = recursive_replace( @@ -1306,7 +1306,7 @@ async def test_pool_target_pre_farm(self, empty_blockchain): await _validate_and_add_block(empty_blockchain, block_bad, expected_error=Err.INVALID_PREFARM) @pytest.mark.asyncio - async def test_pool_target_signature(self, empty_blockchain): + async def test_pool_target_signature(self, empty_blockchain, bt): # 20b blocks_initial = bt.get_consecutive_blocks(2) await _validate_and_add_block(empty_blockchain, blocks_initial[0]) @@ -1330,7 +1330,7 @@ async def test_pool_target_signature(self, empty_blockchain): attempts += 1 @pytest.mark.asyncio - async def test_pool_target_contract(self, empty_blockchain): + async def test_pool_target_contract(self, empty_blockchain, bt): # 20c invalid pool target with contract blocks_initial = bt.get_consecutive_blocks(2) await _validate_and_add_block(empty_blockchain, blocks_initial[0]) @@ -1354,7 +1354,7 @@ async def test_pool_target_contract(self, empty_blockchain): attempts += 1 @pytest.mark.asyncio - async def test_foliage_data_presence(self, empty_blockchain): + async def test_foliage_data_presence(self, empty_blockchain, bt): # 22 blocks = bt.get_consecutive_blocks(1) await _validate_and_add_block(empty_blockchain, blocks[0]) @@ -1381,7 +1381,7 @@ async def test_foliage_data_presence(self, empty_blockchain): ) @pytest.mark.asyncio - async def test_foliage_transaction_block_hash(self, empty_blockchain): + async def test_foliage_transaction_block_hash(self, empty_blockchain, bt): # 23 blocks = bt.get_consecutive_blocks(1) await _validate_and_add_block(empty_blockchain, blocks[0]) @@ -1403,7 +1403,7 @@ async def test_foliage_transaction_block_hash(self, empty_blockchain): await _validate_and_add_block(empty_blockchain, blocks[-1]) @pytest.mark.asyncio - async def test_genesis_bad_prev_block(self, empty_blockchain): + async def test_genesis_bad_prev_block(self, empty_blockchain, bt): # 24a blocks = bt.get_consecutive_blocks(1) block_bad: FullBlock = recursive_replace( @@ -1418,7 +1418,7 @@ async def test_genesis_bad_prev_block(self, empty_blockchain): await _validate_and_add_block(empty_blockchain, block_bad, expected_error=Err.INVALID_PREV_BLOCK_HASH) @pytest.mark.asyncio - async def test_bad_prev_block_non_genesis(self, empty_blockchain): + async def test_bad_prev_block_non_genesis(self, empty_blockchain, bt): # 24b blocks = bt.get_consecutive_blocks(1) await _validate_and_add_block(empty_blockchain, blocks[0]) @@ -1439,7 +1439,7 @@ async def test_bad_prev_block_non_genesis(self, empty_blockchain): await _validate_and_add_block(empty_blockchain, blocks[-1]) @pytest.mark.asyncio - async def test_bad_filter_hash(self, empty_blockchain): + async def test_bad_filter_hash(self, empty_blockchain, bt): # 25 blocks = bt.get_consecutive_blocks(1) await _validate_and_add_block(empty_blockchain, blocks[0]) @@ -1462,7 +1462,7 @@ async def test_bad_filter_hash(self, empty_blockchain): await _validate_and_add_block(empty_blockchain, blocks[-1]) @pytest.mark.asyncio - async def test_bad_timestamp(self, empty_blockchain): + async def test_bad_timestamp(self, empty_blockchain, bt): # 26 blocks = bt.get_consecutive_blocks(1) await _validate_and_add_block(empty_blockchain, blocks[0]) @@ -1513,7 +1513,7 @@ async def test_bad_timestamp(self, empty_blockchain): await _validate_and_add_block(empty_blockchain, blocks[-1]) @pytest.mark.asyncio - async def test_height(self, empty_blockchain): + async def test_height(self, empty_blockchain, bt): # 27 blocks = bt.get_consecutive_blocks(2) await _validate_and_add_block(empty_blockchain, blocks[0]) @@ -1521,14 +1521,14 @@ async def test_height(self, empty_blockchain): await _validate_and_add_block(empty_blockchain, block_bad, expected_error=Err.INVALID_HEIGHT) @pytest.mark.asyncio - async def test_height_genesis(self, empty_blockchain): + async def test_height_genesis(self, empty_blockchain, bt): # 27 blocks = bt.get_consecutive_blocks(1) block_bad: FullBlock = recursive_replace(blocks[-1], "reward_chain_block.height", 1) await _validate_and_add_block(empty_blockchain, block_bad, expected_error=Err.INVALID_PREV_BLOCK_HASH) @pytest.mark.asyncio - async def test_weight(self, empty_blockchain): + async def test_weight(self, empty_blockchain, bt): # 28 blocks = bt.get_consecutive_blocks(2) await _validate_and_add_block(empty_blockchain, blocks[0]) @@ -1536,14 +1536,14 @@ async def test_weight(self, empty_blockchain): await _validate_and_add_block(empty_blockchain, block_bad, expected_error=Err.INVALID_WEIGHT) @pytest.mark.asyncio - async def test_weight_genesis(self, empty_blockchain): + async def test_weight_genesis(self, empty_blockchain, bt): # 28 blocks = bt.get_consecutive_blocks(1) block_bad: FullBlock = recursive_replace(blocks[-1], "reward_chain_block.weight", 0) await _validate_and_add_block(empty_blockchain, block_bad, expected_error=Err.INVALID_WEIGHT) @pytest.mark.asyncio - async def test_bad_cc_ip_vdf(self, empty_blockchain): + async def test_bad_cc_ip_vdf(self, empty_blockchain, bt): # 29 blocks = bt.get_consecutive_blocks(1) await _validate_and_add_block(empty_blockchain, blocks[0]) @@ -1571,7 +1571,7 @@ async def test_bad_cc_ip_vdf(self, empty_blockchain): await _validate_and_add_block(empty_blockchain, block_bad, expected_error=Err.INVALID_CC_IP_VDF) @pytest.mark.asyncio - async def test_bad_rc_ip_vdf(self, empty_blockchain): + async def test_bad_rc_ip_vdf(self, empty_blockchain, bt): # 30 blocks = bt.get_consecutive_blocks(1) await _validate_and_add_block(empty_blockchain, blocks[0]) @@ -1599,7 +1599,7 @@ async def test_bad_rc_ip_vdf(self, empty_blockchain): await _validate_and_add_block(empty_blockchain, block_bad, expected_error=Err.INVALID_RC_IP_VDF) @pytest.mark.asyncio - async def test_bad_icc_ip_vdf(self, empty_blockchain): + async def test_bad_icc_ip_vdf(self, empty_blockchain, bt): # 31 blocks = bt.get_consecutive_blocks(1) await _validate_and_add_block(empty_blockchain, blocks[0]) @@ -1631,7 +1631,7 @@ async def test_bad_icc_ip_vdf(self, empty_blockchain): await _validate_and_add_block(empty_blockchain, block_bad, expected_error=Err.INVALID_ICC_VDF) @pytest.mark.asyncio - async def test_reward_block_hash(self, empty_blockchain): + async def test_reward_block_hash(self, empty_blockchain, bt): # 32 blocks = bt.get_consecutive_blocks(2) await _validate_and_add_block(empty_blockchain, blocks[0]) @@ -1639,7 +1639,7 @@ async def test_reward_block_hash(self, empty_blockchain): await _validate_and_add_block(empty_blockchain, block_bad, expected_error=Err.INVALID_REWARD_BLOCK_HASH) @pytest.mark.asyncio - async def test_reward_block_hash_2(self, empty_blockchain): + async def test_reward_block_hash_2(self, empty_blockchain, bt): # 33 blocks = bt.get_consecutive_blocks(1) block_bad: FullBlock = recursive_replace(blocks[0], "reward_chain_block.is_transaction_block", False) @@ -1666,7 +1666,7 @@ async def test_reward_block_hash_2(self, empty_blockchain): class TestPreValidation: @pytest.mark.asyncio - async def test_pre_validation_fails_bad_blocks(self, empty_blockchain): + async def test_pre_validation_fails_bad_blocks(self, empty_blockchain, bt): blocks = bt.get_consecutive_blocks(2) await _validate_and_add_block(empty_blockchain, blocks[0]) @@ -1680,7 +1680,7 @@ async def test_pre_validation_fails_bad_blocks(self, empty_blockchain): assert res[1].error is not None @pytest.mark.asyncio - async def test_pre_validation(self, empty_blockchain, default_1000_blocks): + async def test_pre_validation(self, empty_blockchain, default_1000_blocks, bt): blocks = default_1000_blocks[:100] start = time.time() n_at_a_time = min(multiprocessing.cpu_count(), 32) @@ -1726,7 +1726,7 @@ class TestBodyValidation: (False, (ReceiveBlockResult.NEW_PEAK, None, 2)), ], ) - async def test_aggsig_garbage(self, empty_blockchain, opcode, with_garbage, expected): + async def test_aggsig_garbage(self, empty_blockchain, opcode, with_garbage, expected, bt): b = empty_blockchain blocks = bt.get_consecutive_blocks( 3, @@ -1797,7 +1797,7 @@ async def test_aggsig_garbage(self, empty_blockchain, opcode, with_garbage, expe (ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, 10032, ReceiveBlockResult.INVALID_BLOCK), ], ) - async def test_ephemeral_timelock(self, empty_blockchain, opcode, lock_value, expected): + async def test_ephemeral_timelock(self, empty_blockchain, opcode, lock_value, expected, bt): b = empty_blockchain blocks = bt.get_consecutive_blocks( 3, @@ -1847,7 +1847,7 @@ async def test_ephemeral_timelock(self, empty_blockchain, opcode, lock_value, ex assert c is not None and not c.spent @pytest.mark.asyncio - async def test_not_tx_block_but_has_data(self, empty_blockchain): + async def test_not_tx_block_but_has_data(self, empty_blockchain, bt): # 1 b = empty_blockchain blocks = bt.get_consecutive_blocks(1) @@ -1877,7 +1877,7 @@ async def test_not_tx_block_but_has_data(self, empty_blockchain): ) @pytest.mark.asyncio - async def test_tx_block_missing_data(self, empty_blockchain): + async def test_tx_block_missing_data(self, empty_blockchain, bt): # 2 b = empty_blockchain blocks = bt.get_consecutive_blocks(2, guarantee_transaction_block=True) @@ -1904,7 +1904,7 @@ async def test_tx_block_missing_data(self, empty_blockchain): return None @pytest.mark.asyncio - async def test_invalid_transactions_info_hash(self, empty_blockchain): + async def test_invalid_transactions_info_hash(self, empty_blockchain, bt): # 3 b = empty_blockchain blocks = bt.get_consecutive_blocks(2, guarantee_transaction_block=True) @@ -1925,7 +1925,7 @@ async def test_invalid_transactions_info_hash(self, empty_blockchain): await _validate_and_add_block(b, block, expected_error=Err.INVALID_TRANSACTIONS_INFO_HASH) @pytest.mark.asyncio - async def test_invalid_transactions_block_hash(self, empty_blockchain): + async def test_invalid_transactions_block_hash(self, empty_blockchain, bt): # 4 b = empty_blockchain blocks = bt.get_consecutive_blocks(2, guarantee_transaction_block=True) @@ -1939,7 +1939,7 @@ async def test_invalid_transactions_block_hash(self, empty_blockchain): await _validate_and_add_block(b, block, expected_error=Err.INVALID_FOLIAGE_BLOCK_HASH) @pytest.mark.asyncio - async def test_invalid_reward_claims(self, empty_blockchain): + async def test_invalid_reward_claims(self, empty_blockchain, bt): # 5 b = empty_blockchain blocks = bt.get_consecutive_blocks(2, guarantee_transaction_block=True) @@ -2007,7 +2007,7 @@ async def test_invalid_reward_claims(self, empty_blockchain): await _validate_and_add_block(b, block_2, expected_error=Err.INVALID_REWARD_COINS, skip_prevalidation=True) @pytest.mark.asyncio - async def test_invalid_transactions_generator_hash(self, empty_blockchain): + async def test_invalid_transactions_generator_hash(self, empty_blockchain, bt): # 7 b = empty_blockchain blocks = bt.get_consecutive_blocks(2, guarantee_transaction_block=True) @@ -2064,7 +2064,7 @@ async def test_invalid_transactions_generator_hash(self, empty_blockchain): await _validate_and_add_block(b, block_2, expected_error=Err.INVALID_TRANSACTIONS_GENERATOR_HASH) @pytest.mark.asyncio - async def test_invalid_transactions_ref_list(self, empty_blockchain): + async def test_invalid_transactions_ref_list(self, empty_blockchain, bt): # No generator should have [1]s for the root b = empty_blockchain blocks = bt.get_consecutive_blocks( @@ -2165,7 +2165,7 @@ async def test_invalid_transactions_ref_list(self, empty_blockchain): ) @pytest.mark.asyncio - async def test_cost_exceeds_max(self, empty_blockchain, softfork_height): + async def test_cost_exceeds_max(self, empty_blockchain, softfork_height, bt): # 7 b = empty_blockchain blocks = bt.get_consecutive_blocks( @@ -2211,12 +2211,12 @@ async def test_cost_exceeds_max(self, empty_blockchain, softfork_height): assert Err(results[0].error) == Err.BLOCK_COST_EXCEEDS_MAX @pytest.mark.asyncio - async def test_clvm_must_not_fail(self, empty_blockchain): + async def test_clvm_must_not_fail(self, empty_blockchain, bt): # 8 pass @pytest.mark.asyncio - async def test_invalid_cost_in_block(self, empty_blockchain, softfork_height): + async def test_invalid_cost_in_block(self, empty_blockchain, softfork_height, bt): # 9 b = empty_blockchain blocks = bt.get_consecutive_blocks( @@ -2320,7 +2320,7 @@ async def test_invalid_cost_in_block(self, empty_blockchain, softfork_height): # a general runtime error. The previous test tests this. @pytest.mark.asyncio - async def test_max_coin_amount(self, db_version): + async def test_max_coin_amount(self, db_version, bt): # 10 # TODO: fix, this is not reaching validation. Because we can't create a block with such amounts due to uint64 # limit in Coin @@ -2369,7 +2369,7 @@ async def test_max_coin_amount(self, db_version): # db_path.unlink() @pytest.mark.asyncio - async def test_invalid_merkle_roots(self, empty_blockchain): + async def test_invalid_merkle_roots(self, empty_blockchain, bt): # 11 b = empty_blockchain blocks = bt.get_consecutive_blocks( @@ -2418,7 +2418,7 @@ async def test_invalid_merkle_roots(self, empty_blockchain): await _validate_and_add_block(empty_blockchain, block_2, expected_error=Err.BAD_REMOVAL_ROOT) @pytest.mark.asyncio - async def test_invalid_filter(self, empty_blockchain): + async def test_invalid_filter(self, empty_blockchain, bt): # 12 b = empty_blockchain blocks = bt.get_consecutive_blocks( @@ -2452,7 +2452,7 @@ async def test_invalid_filter(self, empty_blockchain): await _validate_and_add_block(b, block_2, expected_error=Err.INVALID_TRANSACTIONS_FILTER_HASH) @pytest.mark.asyncio - async def test_duplicate_outputs(self, empty_blockchain): + async def test_duplicate_outputs(self, empty_blockchain, bt): # 13 b = empty_blockchain blocks = bt.get_consecutive_blocks( @@ -2482,7 +2482,7 @@ async def test_duplicate_outputs(self, empty_blockchain): await _validate_and_add_block(b, blocks[-1], expected_error=Err.DUPLICATE_OUTPUT) @pytest.mark.asyncio - async def test_duplicate_removals(self, empty_blockchain): + async def test_duplicate_removals(self, empty_blockchain, bt): # 14 b = empty_blockchain blocks = bt.get_consecutive_blocks( @@ -2511,7 +2511,7 @@ async def test_duplicate_removals(self, empty_blockchain): await _validate_and_add_block(b, blocks[-1], expected_error=Err.DOUBLE_SPEND) @pytest.mark.asyncio - async def test_double_spent_in_coin_store(self, empty_blockchain): + async def test_double_spent_in_coin_store(self, empty_blockchain, bt): # 15 b = empty_blockchain blocks = bt.get_consecutive_blocks( @@ -2545,7 +2545,7 @@ async def test_double_spent_in_coin_store(self, empty_blockchain): await _validate_and_add_block(b, blocks[-1], expected_error=Err.DOUBLE_SPEND) @pytest.mark.asyncio - async def test_double_spent_in_reorg(self, empty_blockchain): + async def test_double_spent_in_reorg(self, empty_blockchain, bt): # 15 b = empty_blockchain blocks = bt.get_consecutive_blocks( @@ -2637,7 +2637,7 @@ async def test_double_spent_in_reorg(self, empty_blockchain): assert first_coin is not None and farmer_coin.spent @pytest.mark.asyncio - async def test_minting_coin(self, empty_blockchain): + async def test_minting_coin(self, empty_blockchain, bt): # 16 Minting coin check b = empty_blockchain blocks = bt.get_consecutive_blocks( @@ -2675,7 +2675,7 @@ async def test_max_coin_amount_fee(self): pass @pytest.mark.asyncio - async def test_invalid_fees_in_block(self, empty_blockchain): + async def test_invalid_fees_in_block(self, empty_blockchain, bt): # 19 b = empty_blockchain blocks = bt.get_consecutive_blocks( @@ -2716,7 +2716,7 @@ async def test_invalid_fees_in_block(self, empty_blockchain): await _validate_and_add_block(b, block_2, expected_error=Err.INVALID_BLOCK_FEE_AMOUNT) @pytest.mark.asyncio - async def test_invalid_agg_sig(self, empty_blockchain): + async def test_invalid_agg_sig(self, empty_blockchain, bt): # 22 b = empty_blockchain blocks = bt.get_consecutive_blocks( @@ -2762,7 +2762,7 @@ async def test_invalid_agg_sig(self, empty_blockchain): class TestReorgs: @pytest.mark.asyncio - async def test_basic_reorg(self, empty_blockchain): + async def test_basic_reorg(self, empty_blockchain, bt): b = empty_blockchain blocks = bt.get_consecutive_blocks(15) @@ -2781,7 +2781,7 @@ async def test_basic_reorg(self, empty_blockchain): assert b.get_peak().height == 16 @pytest.mark.asyncio - async def test_long_reorg(self, empty_blockchain, default_10000_blocks): + async def test_long_reorg(self, empty_blockchain, default_10000_blocks, bt): # Reorg longer than a difficulty adjustment # Also tests higher weight chain but lower height b = empty_blockchain @@ -2833,7 +2833,7 @@ async def test_long_compact_blockchain(self, empty_blockchain, default_10000_blo assert b.get_peak().height == len(default_10000_blocks_compact) - 1 @pytest.mark.asyncio - async def test_reorg_from_genesis(self, empty_blockchain): + async def test_reorg_from_genesis(self, empty_blockchain, bt): b = empty_blockchain WALLET_A = WalletTool(b.constants) WALLET_A_PUZZLE_HASHES = [WALLET_A.get_new_puzzlehash() for _ in range(5)] @@ -2866,7 +2866,7 @@ async def test_reorg_from_genesis(self, empty_blockchain): assert b.get_peak().height == 17 @pytest.mark.asyncio - async def test_reorg_transaction(self, empty_blockchain): + async def test_reorg_transaction(self, empty_blockchain, bt): b = empty_blockchain wallet_a = WalletTool(b.constants) WALLET_A_PUZZLE_HASHES = [wallet_a.get_new_puzzlehash() for _ in range(5)] @@ -2911,7 +2911,7 @@ async def test_reorg_transaction(self, empty_blockchain): await _validate_and_add_block_no_error(b, block) @pytest.mark.asyncio - async def test_get_header_blocks_in_range_tx_filter(self, empty_blockchain): + async def test_get_header_blocks_in_range_tx_filter(self, empty_blockchain, bt): b = empty_blockchain blocks = bt.get_consecutive_blocks( 3, diff --git a/tests/blockchain/test_blockchain_transactions.py b/tests/blockchain/test_blockchain_transactions.py index 449773a4e28d..9f9c4f361406 100644 --- a/tests/blockchain/test_blockchain_transactions.py +++ b/tests/blockchain/test_blockchain_transactions.py @@ -14,7 +14,7 @@ from chia.util.ints import uint64 from tests.blockchain.blockchain_test_utils import _validate_and_add_block from tests.wallet_tools import WalletTool -from tests.setup_nodes import bt, setup_two_nodes, test_constants +from tests.setup_nodes import setup_two_nodes, test_constants from tests.util.generator_tools_testing import run_and_get_removals_and_additions BURN_PUZZLE_HASH = b"0" * 32 @@ -33,12 +33,12 @@ def event_loop(): class TestBlockchainTransactions: @pytest_asyncio.fixture(scope="function") - async def two_nodes(self, db_version): - async for _ in setup_two_nodes(test_constants, db_version=db_version): + async def two_nodes(self, db_version, self_hostname): + async for _ in setup_two_nodes(test_constants, db_version=db_version, self_hostname=self_hostname): yield _ @pytest.mark.asyncio - async def test_basic_blockchain_tx(self, two_nodes): + async def test_basic_blockchain_tx(self, two_nodes, bt): num_blocks = 10 wallet_a = WALLET_A coinbase_puzzlehash = WALLET_A_PUZZLE_HASHES[0] @@ -99,7 +99,7 @@ async def test_basic_blockchain_tx(self, two_nodes): assert not unspent.coinbase @pytest.mark.asyncio - async def test_validate_blockchain_with_double_spend(self, two_nodes): + async def test_validate_blockchain_with_double_spend(self, two_nodes, bt): num_blocks = 5 wallet_a = WALLET_A coinbase_puzzlehash = WALLET_A_PUZZLE_HASHES[0] @@ -137,7 +137,7 @@ async def test_validate_blockchain_with_double_spend(self, two_nodes): await _validate_and_add_block(full_node_1.blockchain, next_block, expected_error=Err.DOUBLE_SPEND) @pytest.mark.asyncio - async def test_validate_blockchain_duplicate_output(self, two_nodes): + async def test_validate_blockchain_duplicate_output(self, two_nodes, bt): num_blocks = 3 wallet_a = WALLET_A coinbase_puzzlehash = WALLET_A_PUZZLE_HASHES[0] @@ -175,7 +175,7 @@ async def test_validate_blockchain_duplicate_output(self, two_nodes): await _validate_and_add_block(full_node_1.blockchain, next_block, expected_error=Err.DUPLICATE_OUTPUT) @pytest.mark.asyncio - async def test_validate_blockchain_with_reorg_double_spend(self, two_nodes): + async def test_validate_blockchain_with_reorg_double_spend(self, two_nodes, bt): num_blocks = 10 wallet_a = WALLET_A coinbase_puzzlehash = WALLET_A_PUZZLE_HASHES[0] @@ -282,7 +282,7 @@ async def test_validate_blockchain_with_reorg_double_spend(self, two_nodes): await full_node_api_1.full_node.respond_block(full_node_protocol.RespondBlock(block)) @pytest.mark.asyncio - async def test_validate_blockchain_spend_reorg_coin(self, two_nodes, softfork_height): + async def test_validate_blockchain_spend_reorg_coin(self, two_nodes, softfork_height, bt): num_blocks = 10 wallet_a = WALLET_A coinbase_puzzlehash = WALLET_A_PUZZLE_HASHES[0] @@ -369,7 +369,7 @@ async def test_validate_blockchain_spend_reorg_coin(self, two_nodes, softfork_he await full_node_api_1.full_node.respond_block(full_node_protocol.RespondBlock(new_blocks[-1])) @pytest.mark.asyncio - async def test_validate_blockchain_spend_reorg_cb_coin(self, two_nodes): + async def test_validate_blockchain_spend_reorg_cb_coin(self, two_nodes, bt): num_blocks = 15 wallet_a = WALLET_A coinbase_puzzlehash = WALLET_A_PUZZLE_HASHES[0] @@ -412,7 +412,7 @@ async def test_validate_blockchain_spend_reorg_cb_coin(self, two_nodes): await full_node_api_1.full_node.respond_block(full_node_protocol.RespondBlock(new_blocks[-1])) @pytest.mark.asyncio - async def test_validate_blockchain_spend_reorg_since_genesis(self, two_nodes): + async def test_validate_blockchain_spend_reorg_since_genesis(self, two_nodes, bt): num_blocks = 10 wallet_a = WALLET_A coinbase_puzzlehash = WALLET_A_PUZZLE_HASHES[0] @@ -461,7 +461,7 @@ async def test_validate_blockchain_spend_reorg_since_genesis(self, two_nodes): await full_node_api_1.full_node.respond_block(full_node_protocol.RespondBlock(new_blocks[-1])) @pytest.mark.asyncio - async def test_assert_my_coin_id(self, two_nodes): + async def test_assert_my_coin_id(self, two_nodes, bt): num_blocks = 10 wallet_a = WALLET_A coinbase_puzzlehash = WALLET_A_PUZZLE_HASHES[0] @@ -528,7 +528,7 @@ async def test_assert_my_coin_id(self, two_nodes): await _validate_and_add_block(full_node_1.blockchain, new_blocks[-1]) @pytest.mark.asyncio - async def test_assert_coin_announcement_consumed(self, two_nodes): + async def test_assert_coin_announcement_consumed(self, two_nodes, bt): num_blocks = 10 wallet_a = WALLET_A @@ -610,7 +610,7 @@ async def test_assert_coin_announcement_consumed(self, two_nodes): await _validate_and_add_block(full_node_1.blockchain, new_blocks[-1]) @pytest.mark.asyncio - async def test_assert_puzzle_announcement_consumed(self, two_nodes): + async def test_assert_puzzle_announcement_consumed(self, two_nodes, bt): num_blocks = 10 wallet_a = WALLET_A @@ -692,7 +692,7 @@ async def test_assert_puzzle_announcement_consumed(self, two_nodes): await _validate_and_add_block(full_node_1.blockchain, new_blocks[-1]) @pytest.mark.asyncio - async def test_assert_height_absolute(self, two_nodes): + async def test_assert_height_absolute(self, two_nodes, bt): num_blocks = 10 wallet_a = WALLET_A coinbase_puzzlehash = WALLET_A_PUZZLE_HASHES[0] @@ -756,7 +756,7 @@ async def test_assert_height_absolute(self, two_nodes): await _validate_and_add_block(full_node_1.blockchain, new_blocks[-1]) @pytest.mark.asyncio - async def test_assert_height_relative(self, two_nodes): + async def test_assert_height_relative(self, two_nodes, bt): num_blocks = 11 wallet_a = WALLET_A coinbase_puzzlehash = WALLET_A_PUZZLE_HASHES[0] @@ -822,7 +822,7 @@ async def test_assert_height_relative(self, two_nodes): await _validate_and_add_block(full_node_1.blockchain, new_blocks[-1]) @pytest.mark.asyncio - async def test_assert_seconds_relative(self, two_nodes): + async def test_assert_seconds_relative(self, two_nodes, bt): num_blocks = 10 wallet_a = WALLET_A @@ -880,7 +880,7 @@ async def test_assert_seconds_relative(self, two_nodes): await _validate_and_add_block(full_node_1.blockchain, valid_new_blocks[-1]) @pytest.mark.asyncio - async def test_assert_seconds_absolute(self, two_nodes): + async def test_assert_seconds_absolute(self, two_nodes, bt): num_blocks = 10 wallet_a = WALLET_A @@ -939,7 +939,7 @@ async def test_assert_seconds_absolute(self, two_nodes): await _validate_and_add_block(full_node_1.blockchain, valid_new_blocks[-1]) @pytest.mark.asyncio - async def test_assert_fee_condition(self, two_nodes): + async def test_assert_fee_condition(self, two_nodes, bt): num_blocks = 10 wallet_a = WALLET_A diff --git a/tests/conftest.py b/tests/conftest.py index d0cad74baff7..13bb572b302b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,22 +1,46 @@ +# flake8: noqa E402 # See imports after multiprocessing.set_start_method import multiprocessing import pytest import pytest_asyncio import tempfile + +# Set spawn after stdlib imports, but before other imports +multiprocessing.set_start_method("spawn") + from pathlib import Path +from chia.util.keyring_wrapper import KeyringWrapper +from tests.block_tools import BlockTools, test_constants, create_block_tools +from tests.util.keyring import TempKeyring -multiprocessing.set_start_method("spawn") +@pytest.fixture(scope="session") +def get_keychain(): + with TempKeyring() as keychain: + yield keychain + KeyringWrapper.cleanup_shared_instance() + + +@pytest.fixture(scope="session", name="bt") +def bt(get_keychain) -> BlockTools: + # Note that this causes a lot of CPU and disk traffic - disk, DB, ports, process creation ... + _shared_block_tools = create_block_tools(constants=test_constants, keychain=get_keychain) + return _shared_block_tools + + +# if you have a system that has an unusual hostname for localhost and you want +# to run the tests, change the `self_hostname` fixture +@pytest_asyncio.fixture(scope="session") +def self_hostname(): + return "localhost" -# TODO: tests.setup_nodes (which is also imported by tests.util.blockchain) creates a -# global BlockTools at tests.setup_nodes.bt. This results in an attempt to create -# the chia root directory which the build scripts symlink to a sometimes-not-there -# directory. When not there Python complains since, well, the symlink is a file -# not a directory and also not pointing to a directory. In those same cases, -# these fixtures are not used. It would be good to refactor that global state -# creation, including the filesystem modification, away from the import but -# that seems like a separate step and until then locating the imports in the -# fixtures avoids the issue. +# NOTE: +# Instantiating the bt fixture results in an attempt to create the chia root directory +# which the build scripts symlink to a sometimes-not-there directory. +# When not there, Python complains since, well, the symlink is not a directory nor points to a directory. +# +# Now that we have removed the global at tests.setup_nodes.bt, we can move the imports out of +# the fixtures below. Just be aware of the filesystem modification during bt fixture creation @pytest_asyncio.fixture(scope="function", params=[1, 2]) @@ -52,14 +76,14 @@ def softfork_height(request): def default_400_blocks(): from tests.util.blockchain import persistent_blocks - return persistent_blocks(400, f"test_blocks_400_{block_format_version}.db", seed=b"alternate2") + return persistent_blocks(400, f"test_blocks_400_{block_format_version}.db", bt, seed=b"alternate2") @pytest.fixture(scope="session") def default_1000_blocks(): from tests.util.blockchain import persistent_blocks - return persistent_blocks(1000, f"test_blocks_1000_{block_format_version}.db") + return persistent_blocks(1000, f"test_blocks_1000_{block_format_version}.db", bt) @pytest.fixture(scope="session") @@ -67,7 +91,7 @@ def pre_genesis_empty_slots_1000_blocks(): from tests.util.blockchain import persistent_blocks return persistent_blocks( - 1000, f"pre_genesis_empty_slots_1000_blocks{block_format_version}.db", seed=b"alternate2", empty_sub_slots=1 + 1000, f"pre_genesis_empty_slots_1000_blocks{block_format_version}.db", bt, seed=b"alternate2", empty_sub_slots=1 ) @@ -75,14 +99,14 @@ def pre_genesis_empty_slots_1000_blocks(): def default_10000_blocks(): from tests.util.blockchain import persistent_blocks - return persistent_blocks(10000, f"test_blocks_10000_{block_format_version}.db") + return persistent_blocks(10000, f"test_blocks_10000_{block_format_version}.db", bt) @pytest.fixture(scope="session") def default_20000_blocks(): from tests.util.blockchain import persistent_blocks - return persistent_blocks(20000, f"test_blocks_20000_{block_format_version}.db") + return persistent_blocks(20000, f"test_blocks_20000_{block_format_version}.db", bt) @pytest.fixture(scope="session") @@ -92,6 +116,7 @@ def default_10000_blocks_compact(): return persistent_blocks( 10000, f"test_blocks_10000_compact_{block_format_version}.db", + bt, normalized_to_identity_cc_eos=True, normalized_to_identity_icc_eos=True, normalized_to_identity_cc_ip=True, diff --git a/tests/connection_utils.py b/tests/connection_utils.py index 9ef156b2053c..b178e730b8c8 100644 --- a/tests/connection_utils.py +++ b/tests/connection_utils.py @@ -15,13 +15,12 @@ from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.peer_info import PeerInfo from chia.util.ints import uint16 -from tests.setup_nodes import self_hostname from tests.time_out_assert import time_out_assert log = logging.getLogger(__name__) -async def disconnect_all_and_reconnect(server: ChiaServer, reconnect_to: ChiaServer) -> bool: +async def disconnect_all_and_reconnect(server: ChiaServer, reconnect_to: ChiaServer, self_hostname: str) -> bool: cons = list(server.all_connections.values())[:] for con in cons: await con.close() @@ -29,7 +28,7 @@ async def disconnect_all_and_reconnect(server: ChiaServer, reconnect_to: ChiaSer async def add_dummy_connection( - server: ChiaServer, dummy_port: int, type: NodeType = NodeType.FULL_NODE + server: ChiaServer, self_hostname: str, dummy_port: int, type: NodeType = NodeType.FULL_NODE ) -> Tuple[asyncio.Queue, bytes32]: timeout = aiohttp.ClientTimeout(total=10) session = aiohttp.ClientSession(timeout=timeout) @@ -66,7 +65,7 @@ async def add_dummy_connection( return incoming_queue, peer_id -async def connect_and_get_peer(server_1: ChiaServer, server_2: ChiaServer) -> WSChiaConnection: +async def connect_and_get_peer(server_1: ChiaServer, server_2: ChiaServer, self_hostname: str) -> WSChiaConnection: """ Connect server_2 to server_1, and get return the connection in server_1. """ diff --git a/tests/core/daemon/test_daemon.py b/tests/core/daemon/test_daemon.py index d3486f895f18..3225c474b524 100644 --- a/tests/core/daemon/test_daemon.py +++ b/tests/core/daemon/test_daemon.py @@ -1,3 +1,10 @@ +import aiohttp +import asyncio +import json +import logging +import pytest +import pytest_asyncio + from chia.daemon.server import WebSocketServer from chia.server.outbound_message import NodeType from chia.types.peer_info import PeerInfo @@ -6,32 +13,13 @@ from chia.util.keyring_wrapper import DEFAULT_PASSPHRASE_IF_NO_MASTER_PASSPHRASE from chia.util.ws_message import create_payload from tests.core.node_height import node_height_at_least -from tests.setup_nodes import setup_daemon, self_hostname, setup_full_system +from tests.setup_nodes import setup_daemon, setup_full_system from tests.simulation.test_simulation import test_constants_modified -from tests.time_out_assert import time_out_assert, time_out_assert_custom_interval +from tests.time_out_assert import time_out_assert_custom_interval, time_out_assert from tests.util.keyring import TempKeyring -import asyncio -import json - -import aiohttp -import pytest -import pytest_asyncio - class TestDaemon: - - # TODO: Ideally, the db_version should be the (parameterized) db_version - # fixture, to test all versions of the database schema. This doesn't work - # because of a hack in shutting down the full node, which means you cannot run - # more than one simulations per process. - @pytest_asyncio.fixture(scope="function") - async def simulation(self, get_b_tools, get_b_tools_1): - async for _ in setup_full_system( - test_constants_modified, b_tools=get_b_tools, b_tools_1=get_b_tools_1, connect_to_daemon=True, db_version=1 - ): - yield _ - @pytest_asyncio.fixture(scope="function") async def get_temp_keyring(self): with TempKeyring() as keychain: @@ -53,8 +41,24 @@ async def get_daemon_with_temp_keyring(self, get_b_tools): async for daemon in setup_daemon(btools=get_b_tools): yield get_b_tools, daemon + # TODO: Ideally, the db_version should be the (parameterized) db_version + # fixture, to test all versions of the database schema. This doesn't work + # because of a hack in shutting down the full node, which means you cannot run + # more than one simulations per process. + @pytest_asyncio.fixture(scope="function") + async def simulation(self, bt, get_b_tools, get_b_tools_1): + async for _ in setup_full_system( + test_constants_modified, + bt, + b_tools=get_b_tools, + b_tools_1=get_b_tools_1, + connect_to_daemon=True, + db_version=1, + ): + yield _ + @pytest.mark.asyncio - async def test_daemon_simulation(self, simulation, get_b_tools): + async def test_daemon_simulation(self, self_hostname, simulation, bt, get_b_tools, get_b_tools_1): node1, node2, _, _, _, _, _, _, _, _, server1, daemon1 = simulation node2_port = node2.full_node.config["port"] await server1.start_client(PeerInfo(self_hostname, uint16(node2_port))) @@ -66,15 +70,17 @@ async def num_connections(): await time_out_assert_custom_interval(60, 1, num_connections, 1) await time_out_assert(1500, node_height_at_least, True, node2, 1) + session = aiohttp.ClientSession() - ssl_context = get_b_tools.get_daemon_ssl_context() + log = logging.getLogger() + log.warning(f"Connecting to daemon on port {daemon1.daemon_port}") ws = await session.ws_connect( f"wss://127.0.0.1:{daemon1.daemon_port}", autoclose=True, autoping=True, heartbeat=60, - ssl_context=ssl_context, + ssl_context=get_b_tools.get_daemon_ssl_context(), max_msg_size=100 * 1024 * 1024, ) service_name = "test_service_name" diff --git a/tests/core/full_node/full_sync/test_full_sync.py b/tests/core/full_node/full_sync/test_full_sync.py index 30f05f289182..a0715978a99e 100644 --- a/tests/core/full_node/full_sync/test_full_sync.py +++ b/tests/core/full_node/full_sync/test_full_sync.py @@ -15,7 +15,7 @@ from chia.util.hash import std_hash from chia.util.ints import uint16 from tests.core.node_height import node_height_exactly, node_height_between -from tests.setup_nodes import bt, self_hostname, setup_n_nodes, setup_two_nodes, test_constants +from tests.setup_nodes import setup_n_nodes, setup_two_nodes, test_constants from tests.time_out_assert import time_out_assert @@ -30,27 +30,27 @@ def event_loop(): class TestFullSync: @pytest_asyncio.fixture(scope="function") - async def two_nodes(self, db_version): - async for _ in setup_two_nodes(test_constants, db_version=db_version): + async def two_nodes(self, db_version, self_hostname): + async for _ in setup_two_nodes(test_constants, db_version=db_version, self_hostname=self_hostname): yield _ @pytest_asyncio.fixture(scope="function") - async def three_nodes(self, db_version): - async for _ in setup_n_nodes(test_constants, 3, db_version=db_version): + async def three_nodes(self, db_version, self_hostname): + async for _ in setup_n_nodes(test_constants, 3, db_version=db_version, self_hostname=self_hostname): yield _ @pytest_asyncio.fixture(scope="function") - async def four_nodes(self, db_version): - async for _ in setup_n_nodes(test_constants, 4, db_version=db_version): + async def four_nodes(self, db_version, self_hostname): + async for _ in setup_n_nodes(test_constants, 4, db_version=db_version, self_hostname=self_hostname): yield _ @pytest_asyncio.fixture(scope="function") - async def five_nodes(self, db_version): - async for _ in setup_n_nodes(test_constants, 5, db_version=db_version): + async def five_nodes(self, db_version, self_hostname): + async for _ in setup_n_nodes(test_constants, 5, db_version=db_version, self_hostname=self_hostname): yield _ @pytest.mark.asyncio - async def test_long_sync_from_zero(self, five_nodes, default_400_blocks): + async def test_long_sync_from_zero(self, five_nodes, default_400_blocks, bt, self_hostname): # Must be larger than "sync_block_behind_threshold" in the config num_blocks = len(default_400_blocks) blocks: List[FullBlock] = default_400_blocks @@ -138,7 +138,9 @@ async def test_long_sync_from_zero(self, five_nodes, default_400_blocks): await time_out_assert(timeout_seconds, node_height_exactly, True, full_node_1, 409) @pytest.mark.asyncio - async def test_sync_from_fork_point_and_weight_proof(self, three_nodes, default_1000_blocks, default_400_blocks): + async def test_sync_from_fork_point_and_weight_proof( + self, three_nodes, default_1000_blocks, default_400_blocks, self_hostname + ): start = time.time() # Must be larger than "sync_block_behind_threshold" in the config num_blocks_initial = len(default_1000_blocks) - 50 @@ -209,7 +211,7 @@ def fn3_is_not_syncing(): await time_out_assert(180, node_height_exactly, True, full_node_2, 999) @pytest.mark.asyncio - async def test_batch_sync(self, two_nodes): + async def test_batch_sync(self, two_nodes, bt, self_hostname): # Must be below "sync_block_behind_threshold" in the config num_blocks = 20 num_blocks_2 = 9 @@ -232,7 +234,7 @@ async def test_batch_sync(self, two_nodes): await time_out_assert(60, node_height_exactly, True, full_node_2, num_blocks - 1) @pytest.mark.asyncio - async def test_backtrack_sync_1(self, two_nodes): + async def test_backtrack_sync_1(self, two_nodes, bt, self_hostname): blocks = bt.get_consecutive_blocks(1, skip_slots=1) blocks = bt.get_consecutive_blocks(1, blocks, skip_slots=0) blocks = bt.get_consecutive_blocks(1, blocks, skip_slots=0) @@ -249,7 +251,7 @@ async def test_backtrack_sync_1(self, two_nodes): await time_out_assert(60, node_height_exactly, True, full_node_2, 2) @pytest.mark.asyncio - async def test_backtrack_sync_2(self, two_nodes): + async def test_backtrack_sync_2(self, two_nodes, bt, self_hostname): blocks = bt.get_consecutive_blocks(1, skip_slots=3) blocks = bt.get_consecutive_blocks(8, blocks, skip_slots=0) full_node_1, full_node_2, server_1, server_2 = two_nodes @@ -265,7 +267,7 @@ async def test_backtrack_sync_2(self, two_nodes): await time_out_assert(60, node_height_exactly, True, full_node_2, 8) @pytest.mark.asyncio - async def test_close_height_but_big_reorg(self, three_nodes): + async def test_close_height_but_big_reorg(self, three_nodes, bt, self_hostname): blocks_a = bt.get_consecutive_blocks(50) blocks_b = bt.get_consecutive_blocks(51, seed=b"B") blocks_c = bt.get_consecutive_blocks(90, seed=b"C") @@ -303,7 +305,9 @@ async def test_close_height_but_big_reorg(self, three_nodes): await time_out_assert(60, node_height_exactly, True, full_node_3, 89) @pytest.mark.asyncio - async def test_sync_bad_peak_while_synced(self, three_nodes, default_1000_blocks, default_10000_blocks): + async def test_sync_bad_peak_while_synced( + self, three_nodes, default_1000_blocks, default_10000_blocks, self_hostname + ): # Must be larger than "sync_block_behind_threshold" in the config num_blocks_initial = len(default_1000_blocks) - 250 blocks_750 = default_1000_blocks[:num_blocks_initial] @@ -347,7 +351,7 @@ async def test_sync_bad_peak_while_synced(self, three_nodes, default_1000_blocks assert node_height_exactly(full_node_2, 999) @pytest.mark.asyncio - async def test_block_ses_mismatch(self, two_nodes, default_1000_blocks): + async def test_block_ses_mismatch(self, two_nodes, default_1000_blocks, self_hostname): full_node_1, full_node_2, server_1, server_2 = two_nodes blocks = default_1000_blocks diff --git a/tests/core/full_node/stores/test_block_store.py b/tests/core/full_node/stores/test_block_store.py index b33f759d9211..15ac9ad2f947 100644 --- a/tests/core/full_node/stores/test_block_store.py +++ b/tests/core/full_node/stores/test_block_store.py @@ -18,7 +18,8 @@ from chia.types.blockchain_format.program import SerializedProgram from tests.blockchain.blockchain_test_utils import _validate_and_add_block from tests.util.db_connection import DBConnection -from tests.setup_nodes import bt, test_constants +from tests.setup_nodes import test_constants + log = logging.getLogger(__name__) @@ -31,7 +32,7 @@ def event_loop(): class TestBlockStore: @pytest.mark.asyncio - async def test_block_store(self, tmp_dir, db_version): + async def test_block_store(self, tmp_dir, db_version, bt): assert sqlite3.threadsafety == 1 blocks = bt.get_consecutive_blocks(10) @@ -69,7 +70,7 @@ async def test_block_store(self, tmp_dir, db_version): assert len(block_record_records) == len(blocks) @pytest.mark.asyncio - async def test_deadlock(self, tmp_dir, db_version): + async def test_deadlock(self, tmp_dir, db_version, bt): """ This test was added because the store was deadlocking in certain situations, when fetching and adding blocks repeatedly. The issue was patched. @@ -102,7 +103,7 @@ async def test_deadlock(self, tmp_dir, db_version): await asyncio.gather(*tasks) @pytest.mark.asyncio - async def test_rollback(self, tmp_dir): + async def test_rollback(self, bt, tmp_dir): blocks = bt.get_consecutive_blocks(10) async with DBConnection(2) as db_wrapper: @@ -145,7 +146,7 @@ async def test_rollback(self, tmp_dir): count += 1 @pytest.mark.asyncio - async def test_count_compactified_blocks(self, tmp_dir, db_version): + async def test_count_compactified_blocks(self, bt, tmp_dir, db_version): blocks = bt.get_consecutive_blocks(10) async with DBConnection(db_version) as db_wrapper: @@ -164,7 +165,7 @@ async def test_count_compactified_blocks(self, tmp_dir, db_version): assert count == 0 @pytest.mark.asyncio - async def test_count_uncompactified_blocks(self, tmp_dir, db_version): + async def test_count_uncompactified_blocks(self, bt, tmp_dir, db_version): blocks = bt.get_consecutive_blocks(10) async with DBConnection(db_version) as db_wrapper: @@ -183,7 +184,7 @@ async def test_count_uncompactified_blocks(self, tmp_dir, db_version): assert count == 10 @pytest.mark.asyncio - async def test_replace_proof(self, tmp_dir, db_version): + async def test_replace_proof(self, bt, tmp_dir, db_version): blocks = bt.get_consecutive_blocks(10) def rand_bytes(num) -> bytes: @@ -227,7 +228,7 @@ def rand_vdf_proof() -> VDFProof: assert b.challenge_chain_ip_proof == proof @pytest.mark.asyncio - async def test_get_generator(self, db_version): + async def test_get_generator(self, bt, db_version): blocks = bt.get_consecutive_blocks(10) def generator(i: int) -> SerializedProgram: diff --git a/tests/core/full_node/stores/test_coin_store.py b/tests/core/full_node/stores/test_coin_store.py index ce8c8ef79bb9..8f9a0b2b7f6c 100644 --- a/tests/core/full_node/stores/test_coin_store.py +++ b/tests/core/full_node/stores/test_coin_store.py @@ -20,7 +20,7 @@ from chia.util.ints import uint64, uint32 from tests.blockchain.blockchain_test_utils import _validate_and_add_block from tests.wallet_tools import WalletTool -from tests.setup_nodes import bt, test_constants +from tests.setup_nodes import test_constants from chia.types.blockchain_format.sized_bytes import bytes32 from tests.util.db_connection import DBConnection @@ -59,7 +59,7 @@ def get_future_reward_coins(block: FullBlock) -> Tuple[Coin, Coin]: class TestCoinStoreWithBlocks: @pytest.mark.asyncio @pytest.mark.parametrize("cache_size", [0]) - async def test_basic_coin_store(self, cache_size: uint32, db_version, softfork_height): + async def test_basic_coin_store(self, cache_size: uint32, db_version, softfork_height, bt): wallet_a = WALLET_A reward_ph = wallet_a.get_new_puzzlehash() @@ -157,7 +157,7 @@ async def test_basic_coin_store(self, cache_size: uint32, db_version, softfork_h @pytest.mark.asyncio @pytest.mark.parametrize("cache_size", [0, 10, 100000]) - async def test_set_spent(self, cache_size: uint32, db_version): + async def test_set_spent(self, cache_size: uint32, db_version, bt): blocks = bt.get_consecutive_blocks(9, []) async with DBConnection(db_version) as db_wrapper: @@ -190,7 +190,7 @@ async def test_set_spent(self, cache_size: uint32, db_version): assert record.spent_block_index == block.height @pytest.mark.asyncio - async def test_num_unspent(self, db_version): + async def test_num_unspent(self, bt, db_version): blocks = bt.get_consecutive_blocks(37, []) expect_unspent = 0 @@ -223,7 +223,7 @@ async def test_num_unspent(self, db_version): @pytest.mark.asyncio @pytest.mark.parametrize("cache_size", [0, 10, 100000]) - async def test_rollback(self, cache_size: uint32, db_version): + async def test_rollback(self, cache_size: uint32, db_version, bt): blocks = bt.get_consecutive_blocks(20) async with DBConnection(db_version) as db_wrapper: @@ -275,7 +275,7 @@ async def test_rollback(self, cache_size: uint32, db_version): @pytest.mark.asyncio @pytest.mark.parametrize("cache_size", [0, 10, 100000]) - async def test_basic_reorg(self, cache_size: uint32, tmp_dir, db_version): + async def test_basic_reorg(self, cache_size: uint32, tmp_dir, db_version, bt): async with DBConnection(db_version) as db_wrapper: initial_block_count = 30 @@ -336,20 +336,15 @@ async def test_basic_reorg(self, cache_size: uint32, tmp_dir, db_version): @pytest.mark.asyncio @pytest.mark.parametrize("cache_size", [0, 10, 100000]) - async def test_get_puzzle_hash(self, cache_size: uint32, tmp_dir, db_version): + async def test_get_puzzle_hash(self, cache_size: uint32, tmp_dir, db_version, bt): async with DBConnection(db_version) as db_wrapper: num_blocks = 20 - farmer_ph = 32 * b"0" - pool_ph = 32 * b"1" - # TODO: address hint error and remove ignore - # error: Argument "farmer_reward_puzzle_hash" to "get_consecutive_blocks" of "BlockTools" has - # incompatible type "bytes"; expected "Optional[bytes32]" [arg-type] - # error: Argument "pool_reward_puzzle_hash" to "get_consecutive_blocks" of "BlockTools" has - # incompatible type "bytes"; expected "Optional[bytes32]" [arg-type] + farmer_ph = bytes32(32 * b"0") + pool_ph = bytes32(32 * b"1") blocks = bt.get_consecutive_blocks( num_blocks, - farmer_reward_puzzle_hash=farmer_ph, # type: ignore[arg-type] - pool_reward_puzzle_hash=pool_ph, # type: ignore[arg-type] + farmer_reward_puzzle_hash=farmer_ph, + pool_reward_puzzle_hash=pool_ph, guarantee_transaction_block=True, ) coin_store = await CoinStore.create(db_wrapper, cache_size=uint32(cache_size)) diff --git a/tests/core/full_node/stores/test_hint_store.py b/tests/core/full_node/stores/test_hint_store.py index 8b21ea56da52..69d4431113f1 100644 --- a/tests/core/full_node/stores/test_hint_store.py +++ b/tests/core/full_node/stores/test_hint_store.py @@ -12,7 +12,6 @@ from tests.blockchain.blockchain_test_utils import _validate_and_add_block, _validate_and_add_block_no_error from tests.util.db_connection import DBConnection from tests.wallet_tools import WalletTool -from tests.setup_nodes import bt @pytest.fixture(scope="module") @@ -115,7 +114,7 @@ async def test_duplicates(self, db_version): assert rows[0][0] == 4 @pytest.mark.asyncio - async def test_hints_in_blockchain(self, empty_blockchain): # noqa: F811 + async def test_hints_in_blockchain(self, empty_blockchain, bt): # noqa: F811 blockchain: Blockchain = empty_blockchain blocks = bt.get_consecutive_blocks( diff --git a/tests/core/full_node/test_full_node.py b/tests/core/full_node/test_full_node.py index 07d3c0054cf5..2c72be5c3ef8 100644 --- a/tests/core/full_node/test_full_node.py +++ b/tests/core/full_node/test_full_node.py @@ -1,4 +1,3 @@ -# flake8: noqa: F811, F401 import asyncio import dataclasses import logging @@ -12,7 +11,6 @@ import pytest import pytest_asyncio -from chia.consensus.blockchain import ReceiveBlockResult from chia.consensus.pot_iterations import is_overflow_block from chia.full_node.bundle_tools import detect_potential_template_generator from chia.full_node.full_node_api import FullNodeAPI @@ -47,7 +45,6 @@ ) from tests.pools.test_pool_rpc import wallet_is_synced from tests.wallet_tools import WalletTool -from chia.wallet.cat_wallet.cat_wallet import CATWallet from chia.wallet.transaction_record import TransactionRecord from tests.connection_utils import add_dummy_connection, connect_and_get_peer @@ -55,7 +52,7 @@ from tests.core.full_node.test_mempool_performance import wallet_height_at_least from tests.core.make_block_generator import make_spend_bundle from tests.core.node_height import node_height_at_least -from tests.setup_nodes import bt, self_hostname, setup_simulators_and_wallets, test_constants +from tests.setup_nodes import setup_simulators_and_wallets, test_constants from tests.time_out_assert import time_out_assert, time_out_assert_custom_interval, time_out_messages log = logging.getLogger(__name__) @@ -108,7 +105,7 @@ def event_loop(): @pytest_asyncio.fixture(scope="module") -async def wallet_nodes(): +async def wallet_nodes(bt): async_gen = setup_simulators_and_wallets(2, 1, {"MEMPOOL_BLOCK_BUFFER": 2, "MAX_BLOCK_COST_CLVM": 400000000}) nodes, wallets = await async_gen.__anext__() full_node_1 = nodes[0] @@ -142,7 +139,7 @@ async def setup_two_nodes_and_wallet(): @pytest_asyncio.fixture(scope="function") -async def wallet_nodes_mainnet(db_version): +async def wallet_nodes_mainnet(bt, db_version): async_gen = setup_simulators_and_wallets(2, 1, {"NETWORK_TYPE": 0}, db_version=db_version) nodes, wallets = await async_gen.__anext__() full_node_1 = nodes[0] @@ -160,7 +157,7 @@ async def wallet_nodes_mainnet(db_version): class TestFullNodeBlockCompression: @pytest.mark.asyncio @pytest.mark.parametrize("tx_size", [10000, 3000000000000]) - async def test_block_compression(self, setup_two_nodes_and_wallet, empty_blockchain, tx_size): + async def test_block_compression(self, setup_two_nodes_and_wallet, empty_blockchain, tx_size, bt, self_hostname): nodes, wallets = setup_two_nodes_and_wallet server_1 = nodes[0].full_node.server server_2 = nodes[1].full_node.server @@ -177,8 +174,8 @@ async def test_block_compression(self, setup_two_nodes_and_wallet, empty_blockch and full_node_1.full_node.block_store.db_wrapper.db_version >= 2 and full_node_2.full_node.block_store.db_wrapper.db_version >= 2 ) - _ = await connect_and_get_peer(server_1, server_2) - _ = await connect_and_get_peer(server_1, server_3) + _ = await connect_and_get_peer(server_1, server_2, self_hostname) + _ = await connect_and_get_peer(server_1, server_3, self_hostname) ph = await wallet.get_new_puzzlehash() @@ -460,7 +457,7 @@ async def test_spendbundle_serialization(self): assert bytes(sb) == bytes(protocol_message) @pytest.mark.asyncio - async def test_inbound_connection_limit(self, setup_four_nodes): + async def test_inbound_connection_limit(self, setup_four_nodes, self_hostname): nodes, _ = setup_four_nodes server_1 = nodes[0].full_node.server server_1.config["target_peer_count"] = 2 @@ -472,7 +469,7 @@ async def test_inbound_connection_limit(self, setup_four_nodes): assert len(server_1.get_full_node_connections()) == 2 @pytest.mark.asyncio - async def test_request_peers(self, wallet_nodes): + async def test_request_peers(self, wallet_nodes, self_hostname): full_node_1, full_node_2, server_1, server_2, wallet_a, wallet_receiver = wallet_nodes full_node_2.full_node.full_node_peers.address_manager.make_private_subnets_valid() await server_2.start_client(PeerInfo(self_hostname, uint16(server_1._port))) @@ -493,15 +490,15 @@ async def have_msgs(): full_node_1.full_node.full_node_peers.address_manager = AddressManager() @pytest.mark.asyncio - async def test_basic_chain(self, wallet_nodes): + async def test_basic_chain(self, wallet_nodes, bt, self_hostname): full_node_1, full_node_2, server_1, server_2, wallet_a, wallet_receiver = wallet_nodes - incoming_queue, _ = await add_dummy_connection(server_1, 12312) + incoming_queue, _ = await add_dummy_connection(server_1, self_hostname, 12312) expected_requests = 0 if await full_node_1.full_node.synced(): expected_requests = 1 await time_out_assert(10, time_out_messages(incoming_queue, "request_mempool_transactions", expected_requests)) - peer = await connect_and_get_peer(server_1, server_2) + peer = await connect_and_get_peer(server_1, server_2, self_hostname) blocks = bt.get_consecutive_blocks(1) for block in blocks[:1]: await full_node_1.full_node.respond_block(fnp.RespondBlock(block), peer) @@ -516,16 +513,16 @@ async def test_basic_chain(self, wallet_nodes): assert full_node_1.full_node.blockchain.get_peak().height == 29 @pytest.mark.asyncio - async def test_respond_end_of_sub_slot(self, wallet_nodes): + async def test_respond_end_of_sub_slot(self, wallet_nodes, bt, self_hostname): full_node_1, full_node_2, server_1, server_2, wallet_a, wallet_receiver = wallet_nodes - incoming_queue, dummy_node_id = await add_dummy_connection(server_1, 12312) + incoming_queue, dummy_node_id = await add_dummy_connection(server_1, self_hostname, 12312) expected_requests = 0 if await full_node_1.full_node.synced(): expected_requests = 1 await time_out_assert(10, time_out_messages(incoming_queue, "request_mempool_transactions", expected_requests)) - peer = await connect_and_get_peer(server_1, server_2) + peer = await connect_and_get_peer(server_1, server_2, self_hostname) # Create empty slots blocks = await full_node_1.get_all_full_blocks() @@ -574,24 +571,23 @@ async def test_respond_end_of_sub_slot(self, wallet_nodes): ) @pytest.mark.asyncio - async def test_respond_end_of_sub_slot_no_reorg(self, wallet_nodes): + async def test_respond_end_of_sub_slot_no_reorg(self, wallet_nodes, bt, self_hostname): full_node_1, full_node_2, server_1, server_2, wallet_a, wallet_receiver = wallet_nodes - incoming_queue, dummy_node_id = await add_dummy_connection(server_1, 12312) + incoming_queue, dummy_node_id = await add_dummy_connection(server_1, self_hostname, 12312) expected_requests = 0 if await full_node_1.full_node.synced(): expected_requests = 1 await time_out_assert(10, time_out_messages(incoming_queue, "request_mempool_transactions", expected_requests)) - peer = await connect_and_get_peer(server_1, server_2) + peer = await connect_and_get_peer(server_1, server_2, self_hostname) # First get two blocks in the same sub slot blocks = await full_node_1.get_all_full_blocks() - saved_seed = b"" + for i in range(0, 9999999): blocks = bt.get_consecutive_blocks(5, block_list_input=blocks, skip_slots=1, seed=i.to_bytes(4, "big")) if len(blocks[-1].finished_sub_slots) == 0: - saved_seed = i.to_bytes(4, "big") break # Then create a fork after the first block. @@ -612,20 +608,19 @@ async def test_respond_end_of_sub_slot_no_reorg(self, wallet_nodes): assert full_node_1.full_node.full_node_store.finished_sub_slots == original_ss @pytest.mark.asyncio - async def test_respond_end_of_sub_slot_race(self, wallet_nodes): + async def test_respond_end_of_sub_slot_race(self, wallet_nodes, bt, self_hostname): full_node_1, full_node_2, server_1, server_2, wallet_a, wallet_receiver = wallet_nodes - incoming_queue, dummy_node_id = await add_dummy_connection(server_1, 12312) + incoming_queue, dummy_node_id = await add_dummy_connection(server_1, self_hostname, 12312) expected_requests = 0 if await full_node_1.full_node.synced(): expected_requests = 1 await time_out_assert(10, time_out_messages(incoming_queue, "request_mempool_transactions", expected_requests)) - peer = await connect_and_get_peer(server_1, server_2) + peer = await connect_and_get_peer(server_1, server_2, self_hostname) # First get two blocks in the same sub slot blocks = await full_node_1.get_all_full_blocks() - saved_seed = b"" blocks = bt.get_consecutive_blocks(1, block_list_input=blocks) await full_node_1.full_node.respond_block(fnp.RespondBlock(blocks[-1]), peer) @@ -643,16 +638,16 @@ async def test_respond_end_of_sub_slot_race(self, wallet_nodes): await full_node_1.respond_end_of_sub_slot(fnp.RespondEndOfSubSlot(slot), peer) @pytest.mark.asyncio - async def test_respond_unfinished(self, wallet_nodes): + async def test_respond_unfinished(self, wallet_nodes, bt, self_hostname): full_node_1, full_node_2, server_1, server_2, wallet_a, wallet_receiver = wallet_nodes - incoming_queue, dummy_node_id = await add_dummy_connection(server_1, 12312) + incoming_queue, dummy_node_id = await add_dummy_connection(server_1, self_hostname, 12312) expected_requests = 0 if await full_node_1.full_node.synced(): expected_requests = 1 await time_out_assert(10, time_out_messages(incoming_queue, "request_mempool_transactions", expected_requests)) - peer = await connect_and_get_peer(server_1, server_2) + peer = await connect_and_get_peer(server_1, server_2, self_hostname) blocks = await full_node_1.get_all_full_blocks() # Create empty slots @@ -791,16 +786,16 @@ async def test_respond_unfinished(self, wallet_nodes): assert full_node_1.full_node.blockchain.contains_block(block.header_hash) @pytest.mark.asyncio - async def test_new_peak(self, wallet_nodes): + async def test_new_peak(self, wallet_nodes, bt, self_hostname): full_node_1, full_node_2, server_1, server_2, wallet_a, wallet_receiver = wallet_nodes - incoming_queue, dummy_node_id = await add_dummy_connection(server_1, 12312) + incoming_queue, dummy_node_id = await add_dummy_connection(server_1, self_hostname, 12312) dummy_peer = server_1.all_connections[dummy_node_id] expected_requests = 0 if await full_node_1.full_node.synced(): expected_requests = 1 await time_out_assert(10, time_out_messages(incoming_queue, "request_mempool_transactions", expected_requests)) - peer = await connect_and_get_peer(server_1, server_2) + peer = await connect_and_get_peer(server_1, server_2, self_hostname) blocks = await full_node_1.get_all_full_blocks() blocks = bt.get_consecutive_blocks(3, block_list_input=blocks) # Alternate chain @@ -847,7 +842,7 @@ async def test_new_peak(self, wallet_nodes): await time_out_assert(10, time_out_messages(incoming_queue, "request_block", 1)) @pytest.mark.asyncio - async def test_new_transaction_and_mempool(self, wallet_nodes): + async def test_new_transaction_and_mempool(self, wallet_nodes, bt, self_hostname): full_node_1, full_node_2, server_1, server_2, wallet_a, wallet_receiver = wallet_nodes blocks = await full_node_1.get_all_full_blocks() @@ -867,14 +862,12 @@ async def test_new_transaction_and_mempool(self, wallet_nodes): if full_node_1.full_node.blockchain.get_peak() is not None else -1 ) - peer = await connect_and_get_peer(server_1, server_2) - incoming_queue, node_id = await add_dummy_connection(server_1, 12312) + peer = await connect_and_get_peer(server_1, server_2, self_hostname) + incoming_queue, node_id = await add_dummy_connection(server_1, self_hostname, 12312) fake_peer = server_1.all_connections[node_id] - # Mempool has capacity of 100, make 110 unspents that we can use + # Mempool has capacity of 100, make 110 unspent coins that we can use puzzle_hashes = [] - block_buffer_count = full_node_1.full_node.constants.MEMPOOL_BLOCK_BUFFER - # Makes a bunch of coins for i in range(5): conditions_dict: Dict = {ConditionOpcode.CREATE_COIN: []} @@ -1021,7 +1014,7 @@ async def test_new_transaction_and_mempool(self, wallet_nodes): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_request_respond_transaction(self, wallet_nodes): + async def test_request_respond_transaction(self, wallet_nodes, bt, self_hostname): full_node_1, full_node_2, server_1, server_2, wallet_a, wallet_receiver = wallet_nodes wallet_ph = wallet_a.get_new_puzzlehash() blocks = await full_node_1.get_all_full_blocks() @@ -1034,9 +1027,9 @@ async def test_request_respond_transaction(self, wallet_nodes): pool_reward_puzzle_hash=wallet_ph, ) - incoming_queue, dummy_node_id = await add_dummy_connection(server_1, 12312) + incoming_queue, dummy_node_id = await add_dummy_connection(server_1, self_hostname, 12312) - peer = await connect_and_get_peer(server_1, server_2) + peer = await connect_and_get_peer(server_1, server_2, self_hostname) for block in blocks[-3:]: await full_node_1.full_node.respond_block(fnp.RespondBlock(block), peer) @@ -1069,13 +1062,13 @@ async def test_request_respond_transaction(self, wallet_nodes): assert msg.data == bytes(fnp.RespondTransaction(spend_bundle)) @pytest.mark.asyncio - async def test_respond_transaction_fail(self, wallet_nodes): + async def test_respond_transaction_fail(self, wallet_nodes, bt, self_hostname): full_node_1, full_node_2, server_1, server_2, wallet_a, wallet_receiver = wallet_nodes blocks = await full_node_1.get_all_full_blocks() cb_ph = wallet_a.get_new_puzzlehash() - incoming_queue, dummy_node_id = await add_dummy_connection(server_1, 12312) - peer = await connect_and_get_peer(server_1, server_2) + incoming_queue, dummy_node_id = await add_dummy_connection(server_1, self_hostname, 12312) + peer = await connect_and_get_peer(server_1, server_2, self_hostname) tx_id = token_bytes(32) request_transaction = fnp.RequestTransaction(tx_id) @@ -1116,7 +1109,7 @@ async def test_respond_transaction_fail(self, wallet_nodes): assert incoming_queue.qsize() == 0 @pytest.mark.asyncio - async def test_request_block(self, wallet_nodes): + async def test_request_block(self, wallet_nodes, bt): full_node_1, full_node_2, server_1, server_2, wallet_a, wallet_receiver = wallet_nodes blocks = await full_node_1.get_all_full_blocks() @@ -1158,7 +1151,7 @@ async def test_request_block(self, wallet_nodes): assert res.type != ProtocolMessageTypes.reject_block.value @pytest.mark.asyncio - async def test_request_blocks(self, wallet_nodes): + async def test_request_blocks(self, wallet_nodes, bt): full_node_1, full_node_2, server_1, server_2, wallet_a, wallet_receiver = wallet_nodes blocks = await full_node_1.get_all_full_blocks() @@ -1219,11 +1212,11 @@ async def test_request_blocks(self, wallet_nodes): assert std_hash(fetched_blocks[-1]) == std_hash(blocks_t[-1]) @pytest.mark.asyncio - async def test_new_unfinished_block(self, wallet_nodes): + async def test_new_unfinished_block(self, wallet_nodes, bt, self_hostname): full_node_1, full_node_2, server_1, server_2, wallet_a, wallet_receiver = wallet_nodes blocks = await full_node_1.get_all_full_blocks() - peer = await connect_and_get_peer(server_1, server_2) + peer = await connect_and_get_peer(server_1, server_2, self_hostname) blocks = bt.get_consecutive_blocks(1, block_list_input=blocks) block: FullBlock = blocks[-1] @@ -1250,12 +1243,12 @@ async def test_new_unfinished_block(self, wallet_nodes): assert res is None @pytest.mark.asyncio - async def test_double_blocks_same_pospace(self, wallet_nodes): + async def test_double_blocks_same_pospace(self, wallet_nodes, bt, self_hostname): full_node_1, full_node_2, server_1, server_2, wallet_a, wallet_receiver = wallet_nodes - incoming_queue, dummy_node_id = await add_dummy_connection(server_1, 12315) + incoming_queue, dummy_node_id = await add_dummy_connection(server_1, self_hostname, 12315) dummy_peer = server_1.all_connections[dummy_node_id] - _ = await connect_and_get_peer(server_1, server_2) + _ = await connect_and_get_peer(server_1, server_2, self_hostname) ph = wallet_a.get_new_puzzlehash() @@ -1299,10 +1292,10 @@ async def test_double_blocks_same_pospace(self, wallet_nodes): await time_out_assert(10, time_out_messages(incoming_queue, "request_block", 1)) @pytest.mark.asyncio - async def test_request_unfinished_block(self, wallet_nodes): + async def test_request_unfinished_block(self, wallet_nodes, bt, self_hostname): full_node_1, full_node_2, server_1, server_2, wallet_a, wallet_receiver = wallet_nodes blocks = await full_node_1.get_all_full_blocks() - peer = await connect_and_get_peer(server_1, server_2) + peer = await connect_and_get_peer(server_1, server_2, self_hostname) blocks = bt.get_consecutive_blocks(10, block_list_input=blocks, seed=b"12345") for block in blocks[:-1]: await full_node_1.full_node.respond_block(fnp.RespondBlock(block)) @@ -1329,7 +1322,7 @@ async def test_request_unfinished_block(self, wallet_nodes): assert res is not None @pytest.mark.asyncio - async def test_new_signage_point_or_end_of_sub_slot(self, wallet_nodes): + async def test_new_signage_point_or_end_of_sub_slot(self, wallet_nodes, bt, self_hostname): full_node_1, full_node_2, server_1, server_2, wallet_a, wallet_receiver = wallet_nodes blocks = await full_node_1.get_all_full_blocks() @@ -1351,7 +1344,7 @@ async def test_new_signage_point_or_end_of_sub_slot(self, wallet_nodes): peak.sub_slot_iters, ) - peer = await connect_and_get_peer(server_1, server_2) + peer = await connect_and_get_peer(server_1, server_2, self_hostname) res = await full_node_1.new_signage_point_or_end_of_sub_slot( fnp.NewSignagePointOrEndOfSubSlot(None, sp.cc_vdf.challenge, uint8(11), sp.rc_vdf.challenge), peer ) @@ -1372,7 +1365,7 @@ async def test_new_signage_point_or_end_of_sub_slot(self, wallet_nodes): await full_node_1.respond_end_of_sub_slot(fnp.RespondEndOfSubSlot(slot), peer) assert len(full_node_1.full_node.full_node_store.finished_sub_slots) >= num_slots - 1 - incoming_queue, dummy_node_id = await add_dummy_connection(server_1, 12315) + incoming_queue, dummy_node_id = await add_dummy_connection(server_1, self_hostname, 12315) dummy_peer = server_1.all_connections[dummy_node_id] await full_node_1.respond_end_of_sub_slot(fnp.RespondEndOfSubSlot(slots[-1]), dummy_peer) @@ -1384,11 +1377,11 @@ def caught_up_slots(): await time_out_assert(20, caught_up_slots) @pytest.mark.asyncio - async def test_new_signage_point_caching(self, wallet_nodes, empty_blockchain): + async def test_new_signage_point_caching(self, wallet_nodes, empty_blockchain, bt, self_hostname): full_node_1, full_node_2, server_1, server_2, wallet_a, wallet_receiver = wallet_nodes blocks = await full_node_1.get_all_full_blocks() - peer = await connect_and_get_peer(server_1, server_2) + peer = await connect_and_get_peer(server_1, server_2, self_hostname) blocks = bt.get_consecutive_blocks(3, block_list_input=blocks, skip_slots=2) await full_node_1.full_node.respond_block(fnp.RespondBlock(blocks[-3])) await full_node_1.full_node.respond_block(fnp.RespondBlock(blocks[-2])) @@ -1435,14 +1428,14 @@ async def test_new_signage_point_caching(self, wallet_nodes, empty_blockchain): assert full_node_1.full_node.full_node_store.get_signage_point(sp.cc_vdf.output.get_hash()) is not None @pytest.mark.asyncio - async def test_slot_catch_up_genesis(self, setup_two_nodes): + async def test_slot_catch_up_genesis(self, setup_two_nodes, bt, self_hostname): nodes, _ = setup_two_nodes server_1 = nodes[0].full_node.server server_2 = nodes[1].full_node.server full_node_1 = nodes[0] full_node_2 = nodes[1] - peer = await connect_and_get_peer(server_1, server_2) + peer = await connect_and_get_peer(server_1, server_2, self_hostname) num_slots = 20 blocks = bt.get_consecutive_blocks(1, skip_slots=num_slots) slots = blocks[-1].finished_sub_slots @@ -1454,7 +1447,7 @@ async def test_slot_catch_up_genesis(self, setup_two_nodes): await full_node_1.respond_end_of_sub_slot(fnp.RespondEndOfSubSlot(slot), peer) assert len(full_node_1.full_node.full_node_store.finished_sub_slots) >= num_slots - 1 - incoming_queue, dummy_node_id = await add_dummy_connection(server_1, 12315) + incoming_queue, dummy_node_id = await add_dummy_connection(server_1, self_hostname, 12315) dummy_peer = server_1.all_connections[dummy_node_id] await full_node_1.respond_end_of_sub_slot(fnp.RespondEndOfSubSlot(slots[-1]), dummy_peer) @@ -1467,7 +1460,7 @@ def caught_up_slots(): @pytest.mark.skip("a timebomb causes mainnet to stop after transactions start, so this test doesn't work yet") @pytest.mark.asyncio - async def test_mainnet_softfork(self, wallet_nodes_mainnet): + async def test_mainnet_softfork(self, wallet_nodes_mainnet, bt): full_node_1, full_node_2, server_1, server_2, wallet_a, wallet_receiver = wallet_nodes_mainnet blocks = await full_node_1.get_all_full_blocks() @@ -1525,7 +1518,7 @@ async def test_mainnet_softfork(self, wallet_nodes_mainnet): await _validate_and_add_block(full_node_1.full_node.blockchain, valid_block) @pytest.mark.asyncio - async def test_compact_protocol(self, setup_two_nodes): + async def test_compact_protocol(self, setup_two_nodes, bt): nodes, _ = setup_two_nodes full_node_1 = nodes[0] full_node_2 = nodes[1] @@ -1643,7 +1636,7 @@ async def test_compact_protocol(self, setup_two_nodes): assert full_node_2.full_node.blockchain.get_peak().height == height @pytest.mark.asyncio - async def test_compact_protocol_invalid_messages(self, setup_two_nodes): + async def test_compact_protocol_invalid_messages(self, setup_two_nodes, bt, self_hostname): nodes, _ = setup_two_nodes full_node_1 = nodes[0] full_node_2 = nodes[1] @@ -1854,7 +1847,7 @@ async def test_compact_protocol_invalid_messages(self, setup_two_nodes): ) server_1 = full_node_1.full_node.server server_2 = full_node_2.full_node.server - peer = await connect_and_get_peer(server_1, server_2) + peer = await connect_and_get_peer(server_1, server_2, self_hostname) for invalid_compact_proof in timelord_protocol_invalid_messages: await full_node_1.full_node.respond_compact_proof_of_time(invalid_compact_proof) for invalid_compact_proof in full_node_protocol_invalid_messaages: diff --git a/tests/core/full_node/test_mempool.py b/tests/core/full_node/test_mempool.py index 6b0437c8061d..c58040593adf 100644 --- a/tests/core/full_node/test_mempool.py +++ b/tests/core/full_node/test_mempool.py @@ -40,7 +40,7 @@ from tests.blockchain.blockchain_test_utils import _validate_and_add_block from tests.connection_utils import connect_and_get_peer from tests.core.node_height import node_height_at_least -from tests.setup_nodes import bt, setup_simulators_and_wallets +from tests.setup_nodes import setup_simulators_and_wallets from tests.time_out_assert import time_out_assert from chia.types.blockchain_format.program import Program, INFINITE_COST from chia.consensus.cost_calculator import NPCResult @@ -51,15 +51,22 @@ from clvm.casts import int_from_bytes from blspy import G1Element -BURN_PUZZLE_HASH = b"0" * 32 -BURN_PUZZLE_HASH_2 = b"1" * 32 +from tests.wallet_tools import WalletTool + +BURN_PUZZLE_HASH = bytes32(b"0" * 32) +BURN_PUZZLE_HASH_2 = bytes32(b"1" * 32) + + +@pytest.fixture(scope="module") +def wallet_a(bt): + return bt.get_pool_wallet_tool() -WALLET_A = bt.get_pool_wallet_tool() log = logging.getLogger(__name__) def generate_test_spend_bundle( + wallet_a: WalletTool, coin: Coin, condition_dic: Dict[ConditionOpcode, List[ConditionWithArgs]] = None, fee: uint64 = uint64(0), @@ -68,7 +75,7 @@ def generate_test_spend_bundle( ) -> SpendBundle: if condition_dic is None: condition_dic = {} - transaction = WALLET_A.generate_signed_transaction(amount, new_puzzle_hash, coin, condition_dic, fee) + transaction = wallet_a.generate_signed_transaction(amount, new_puzzle_hash, coin, condition_dic, fee) assert transaction is not None return transaction @@ -88,7 +95,7 @@ def event_loop(): # means you can't instantiate more than one per process, so this is a hack until # that is fixed. For now, our tests are not independent @pytest_asyncio.fixture(scope="module") -async def two_nodes(): +async def two_nodes(bt, wallet_a): async_gen = setup_simulators_and_wallets(2, 1, {}) nodes, _ = await async_gen.__anext__() full_node_1 = nodes[0] @@ -96,7 +103,7 @@ async def two_nodes(): server_1 = full_node_1.full_node.server server_2 = full_node_2.full_node.server - reward_ph = WALLET_A.get_new_puzzlehash() + reward_ph = wallet_a.get_new_puzzlehash() blocks = bt.get_consecutive_blocks( 3, guarantee_transaction_block=True, @@ -192,7 +199,7 @@ def test_cost(self): class TestMempool: @pytest.mark.asyncio - async def test_basic_mempool(self, two_nodes): + async def test_basic_mempool(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes @@ -203,8 +210,8 @@ async def test_basic_mempool(self, two_nodes): with pytest.raises(ValueError): mempool.get_min_fee_rate(max_mempool_cost + 1) - coin = await next_block(full_node_1, WALLET_A, bt) - spend_bundle = generate_test_spend_bundle(coin) + coin = await next_block(full_node_1, wallet_a, bt) + spend_bundle = generate_test_spend_bundle(wallet_a, coin) assert spend_bundle is not None @@ -254,13 +261,13 @@ async def next_block(full_node_1, wallet_a, bt) -> Coin: class TestMempoolManager: @pytest.mark.asyncio - async def test_basic_mempool_manager(self, two_nodes): + async def test_basic_mempool_manager(self, bt, two_nodes, wallet_a, self_hostname): full_node_1, full_node_2, server_1, server_2 = two_nodes - peer = await connect_and_get_peer(server_1, server_2) + peer = await connect_and_get_peer(server_1, server_2, self_hostname) - coin = await next_block(full_node_1, WALLET_A, bt) - spend_bundle = generate_test_spend_bundle(coin) + coin = await next_block(full_node_1, wallet_a, bt) + spend_bundle = generate_test_spend_bundle(wallet_a, coin) assert spend_bundle is not None tx: full_node_protocol.RespondTransaction = full_node_protocol.RespondTransaction(spend_bundle) await full_node_1.respond_transaction(tx, peer) @@ -298,24 +305,24 @@ async def test_basic_mempool_manager(self, two_nodes): # (ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, 10032, MempoolInclusionStatus.FAILED), ], ) - async def test_ephemeral_timelock(self, two_nodes, opcode, lock_value, expected): + async def test_ephemeral_timelock(self, bt, two_nodes, wallet_a, opcode, lock_value, expected): def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: conditions = {opcode: [ConditionWithArgs(opcode, [int_to_bytes(lock_value)])]} - tx1 = WALLET_A.generate_signed_transaction( - uint64(1000000), WALLET_A.get_new_puzzlehash(), coin_2, conditions.copy(), uint64(0) + tx1 = wallet_a.generate_signed_transaction( + uint64(1000000), wallet_a.get_new_puzzlehash(), coin_2, conditions.copy(), uint64(0) ) ephemeral_coin: Coin = tx1.additions()[0] - tx2 = WALLET_A.generate_signed_transaction( - uint64(1000000), WALLET_A.get_new_puzzlehash(), ephemeral_coin, conditions.copy(), uint64(0) + tx2 = wallet_a.generate_signed_transaction( + uint64(1000000), wallet_a.get_new_puzzlehash(), ephemeral_coin, conditions.copy(), uint64(0) ) bundle = SpendBundle.aggregate([tx1, tx2]) return bundle full_node_1, _, server_1, _ = two_nodes - blocks, bundle, status, err = await self.condition_tester2(two_nodes, test_fun) + blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes, wallet_a, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) print(f"status: {status}") @@ -332,7 +339,7 @@ def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: # this test makes sure that one spend successfully asserts the announce from # another spend, even though the assert condition is duplicated 100 times @pytest.mark.asyncio - async def test_coin_announcement_duplicate_consumed(self, two_nodes): + async def test_coin_announcement_duplicate_consumed(self, bt, two_nodes, wallet_a): def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: announce = Announcement(coin_2.name(), b"test") cvp = ConditionWithArgs(ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT, [announce.name()]) @@ -340,13 +347,13 @@ def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: cvp2 = ConditionWithArgs(ConditionOpcode.CREATE_COIN_ANNOUNCEMENT, [b"test"]) dic2 = {cvp.opcode: [cvp2]} - spend_bundle1 = generate_test_spend_bundle(coin_1, dic) - spend_bundle2 = generate_test_spend_bundle(coin_2, dic2) + spend_bundle1 = generate_test_spend_bundle(wallet_a, coin_1, dic) + spend_bundle2 = generate_test_spend_bundle(wallet_a, coin_2, dic2) bundle = SpendBundle.aggregate([spend_bundle1, spend_bundle2]) return bundle full_node_1, full_node_2, server_1, server_2 = two_nodes - blocks, bundle, status, err = await self.condition_tester2(two_nodes, test_fun) + blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes, wallet_a, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) assert err is None @@ -356,7 +363,7 @@ def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: # this test makes sure that one spend successfully asserts the announce from # another spend, even though the create announcement is duplicated 100 times @pytest.mark.asyncio - async def test_coin_duplicate_announcement_consumed(self, two_nodes): + async def test_coin_duplicate_announcement_consumed(self, bt, two_nodes, wallet_a): def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: announce = Announcement(coin_2.name(), b"test") cvp = ConditionWithArgs(ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT, [announce.name()]) @@ -364,13 +371,13 @@ def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: cvp2 = ConditionWithArgs(ConditionOpcode.CREATE_COIN_ANNOUNCEMENT, [b"test"]) dic2 = {cvp.opcode: [cvp2] * 100} - spend_bundle1 = generate_test_spend_bundle(coin_1, dic) - spend_bundle2 = generate_test_spend_bundle(coin_2, dic2) + spend_bundle1 = generate_test_spend_bundle(wallet_a, coin_1, dic) + spend_bundle2 = generate_test_spend_bundle(wallet_a, coin_2, dic2) bundle = SpendBundle.aggregate([spend_bundle1, spend_bundle2]) return bundle full_node_1, full_node_2, server_1, server_2 = two_nodes - blocks, bundle, status, err = await self.condition_tester2(two_nodes, test_fun) + blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes, wallet_a, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) assert err is None @@ -378,8 +385,8 @@ def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_double_spend(self, two_nodes): - reward_ph = WALLET_A.get_new_puzzlehash() + async def test_double_spend(self, bt, two_nodes, wallet_a, self_hostname): + reward_ph = wallet_a.get_new_puzzlehash() full_node_1, full_node_2, server_1, server_2 = two_nodes blocks = await full_node_1.get_all_full_blocks() start_height = blocks[-1].height @@ -390,13 +397,13 @@ async def test_double_spend(self, two_nodes): farmer_reward_puzzle_hash=reward_ph, pool_reward_puzzle_hash=reward_ph, ) - peer = await connect_and_get_peer(server_1, server_2) + peer = await connect_and_get_peer(server_1, server_2, self_hostname) 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) - spend_bundle1 = generate_test_spend_bundle(list(blocks[-1].get_included_reward_coins())[0]) + spend_bundle1 = generate_test_spend_bundle(wallet_a, list(blocks[-1].get_included_reward_coins())[0]) assert spend_bundle1 is not None tx1: full_node_protocol.RespondTransaction = full_node_protocol.RespondTransaction(spend_bundle1) @@ -405,6 +412,7 @@ async def test_double_spend(self, two_nodes): assert status == MempoolInclusionStatus.SUCCESS spend_bundle2 = generate_test_spend_bundle( + wallet_a, list(blocks[-1].get_included_reward_coins())[0], new_puzzle_hash=BURN_PUZZLE_HASH_2, ) @@ -438,8 +446,8 @@ def assert_sb_not_in_pool(self, node, sb): assert node.full_node.mempool_manager.get_spendbundle(sb.name()) is None @pytest.mark.asyncio - async def test_double_spend_with_higher_fee(self, two_nodes): - reward_ph = WALLET_A.get_new_puzzlehash() + async def test_double_spend_with_higher_fee(self, bt, two_nodes, wallet_a, self_hostname): + reward_ph = wallet_a.get_new_puzzlehash() full_node_1, full_node_2, server_1, server_2 = two_nodes blocks = await full_node_1.get_all_full_blocks() @@ -451,7 +459,7 @@ async def test_double_spend_with_higher_fee(self, two_nodes): farmer_reward_puzzle_hash=reward_ph, pool_reward_puzzle_hash=reward_ph, ) - peer = await connect_and_get_peer(server_1, server_2) + peer = await connect_and_get_peer(server_1, server_2, self_hostname) for block in blocks: await full_node_1.full_node.respond_block(full_node_protocol.RespondBlock(block)) @@ -462,8 +470,8 @@ async def test_double_spend_with_higher_fee(self, two_nodes): coins = iter(blocks[-2].get_included_reward_coins()) coin3, coin4 = next(coins), next(coins) - sb1_1 = await self.gen_and_send_sb(full_node_1, peer, coin1) - sb1_2 = await self.gen_and_send_sb(full_node_1, peer, coin1, fee=uint64(1)) + sb1_1 = await self.gen_and_send_sb(full_node_1, peer, wallet_a, coin1) + sb1_2 = await self.gen_and_send_sb(full_node_1, peer, wallet_a, coin1, fee=uint64(1)) # Fee increase is insufficient, the old spendbundle must stay self.assert_sb_in_pool(full_node_1, sb1_1) @@ -471,13 +479,13 @@ async def test_double_spend_with_higher_fee(self, two_nodes): min_fee_increase = full_node_1.full_node.mempool_manager.get_min_fee_increase() - sb1_3 = await self.gen_and_send_sb(full_node_1, peer, coin1, fee=uint64(min_fee_increase)) + sb1_3 = await self.gen_and_send_sb(full_node_1, peer, wallet_a, coin1, fee=uint64(min_fee_increase)) # Fee increase is sufficiently high, sb1_1 gets replaced with sb1_3 self.assert_sb_not_in_pool(full_node_1, sb1_1) self.assert_sb_in_pool(full_node_1, sb1_3) - sb2 = generate_test_spend_bundle(coin2, fee=uint64(min_fee_increase)) + sb2 = generate_test_spend_bundle(wallet_a, coin2, fee=uint64(min_fee_increase)) sb12 = SpendBundle.aggregate((sb2, sb1_3)) await self.send_sb(full_node_1, sb12) @@ -486,7 +494,7 @@ async def test_double_spend_with_higher_fee(self, two_nodes): self.assert_sb_in_pool(full_node_1, sb12) self.assert_sb_not_in_pool(full_node_1, sb1_3) - sb3 = generate_test_spend_bundle(coin3, fee=uint64(min_fee_increase * 2)) + sb3 = generate_test_spend_bundle(wallet_a, coin3, fee=uint64(min_fee_increase * 2)) sb23 = SpendBundle.aggregate((sb2, sb3)) await self.send_sb(full_node_1, sb23) @@ -499,13 +507,13 @@ async def test_double_spend_with_higher_fee(self, two_nodes): # Adding non-conflicting sb3 should succeed self.assert_sb_in_pool(full_node_1, sb3) - sb4_1 = generate_test_spend_bundle(coin4, fee=uint64(min_fee_increase)) + sb4_1 = generate_test_spend_bundle(wallet_a, coin4, fee=uint64(min_fee_increase)) sb1234_1 = SpendBundle.aggregate((sb12, sb3, sb4_1)) await self.send_sb(full_node_1, sb1234_1) # sb1234_1 should not be in pool as it decreases total fees per cost self.assert_sb_not_in_pool(full_node_1, sb1234_1) - sb4_2 = generate_test_spend_bundle(coin4, fee=uint64(min_fee_increase * 2)) + sb4_2 = generate_test_spend_bundle(wallet_a, coin4, fee=uint64(min_fee_increase * 2)) sb1234_2 = SpendBundle.aggregate((sb12, sb3, sb4_2)) await self.send_sb(full_node_1, sb1234_2) # sb1234_2 has a higher fee per cost than its conflicts and should get @@ -515,8 +523,8 @@ async def test_double_spend_with_higher_fee(self, two_nodes): self.assert_sb_not_in_pool(full_node_1, sb3) @pytest.mark.asyncio - async def test_invalid_signature(self, two_nodes): - reward_ph = WALLET_A.get_new_puzzlehash() + async def test_invalid_signature(self, bt, two_nodes, wallet_a): + reward_ph = wallet_a.get_new_puzzlehash() full_node_1, full_node_2, server_1, server_2 = two_nodes blocks = await full_node_1.get_all_full_blocks() @@ -537,7 +545,7 @@ async def test_invalid_signature(self, two_nodes): coin1 = next(coins) coins = iter(blocks[-2].get_included_reward_coins()) - sb: SpendBundle = generate_test_spend_bundle(coin1) + sb: SpendBundle = generate_test_spend_bundle(wallet_a, coin1) assert sb.aggregated_signature != G2Element.generator() sb = dataclasses.replace(sb, aggregated_signature=G2Element.generator()) res: Optional[Message] = await self.send_sb(full_node_1, sb) @@ -548,13 +556,15 @@ async def test_invalid_signature(self, two_nodes): async def condition_tester( self, + bt, two_nodes, + wallet_a, dic: Dict[ConditionOpcode, List[ConditionWithArgs]], fee: int = 0, num_blocks: int = 3, coin: Optional[Coin] = None, ): - reward_ph = WALLET_A.get_new_puzzlehash() + reward_ph = wallet_a.get_new_puzzlehash() full_node_1, full_node_2, server_1, server_2 = two_nodes blocks = await full_node_1.get_all_full_blocks() start_height = blocks[-1].height @@ -565,7 +575,7 @@ async def condition_tester( farmer_reward_puzzle_hash=reward_ph, pool_reward_puzzle_hash=reward_ph, ) - peer = await connect_and_get_peer(server_1, server_2) + peer = await connect_and_get_peer(server_1, server_2, bt.config["self_hostname"]) for block in blocks: await full_node_1.full_node.respond_block(full_node_protocol.RespondBlock(block)) @@ -573,7 +583,7 @@ async def condition_tester( await time_out_assert(60, node_height_at_least, True, full_node_1, start_height + num_blocks) spend_bundle1 = generate_test_spend_bundle( - coin or list(blocks[-num_blocks + 2].get_included_reward_coins())[0], dic, uint64(fee) + wallet_a, coin or list(blocks[-num_blocks + 2].get_included_reward_coins())[0], dic, uint64(fee) ) assert spend_bundle1 is not None @@ -584,8 +594,8 @@ async def condition_tester( return blocks, spend_bundle1, peer, status, err @pytest.mark.asyncio - async def condition_tester2(self, two_nodes, test_fun: Callable[[Coin, Coin], SpendBundle]): - reward_ph = WALLET_A.get_new_puzzlehash() + async def condition_tester2(self, bt, two_nodes, wallet_a, test_fun: Callable[[Coin, Coin], SpendBundle]): + reward_ph = wallet_a.get_new_puzzlehash() full_node_1, full_node_2, server_1, server_2 = two_nodes blocks = await full_node_1.get_all_full_blocks() start_height = blocks[-1].height if len(blocks) > 0 else -1 @@ -596,7 +606,7 @@ async def condition_tester2(self, two_nodes, test_fun: Callable[[Coin, Coin], Sp farmer_reward_puzzle_hash=reward_ph, pool_reward_puzzle_hash=reward_ph, ) - peer = await connect_and_get_peer(server_1, server_2) + peer = await connect_and_get_peer(server_1, server_2, bt.config["self_hostname"]) for block in blocks: await full_node_1.full_node.respond_block(full_node_protocol.RespondBlock(block)) @@ -614,7 +624,7 @@ async def condition_tester2(self, two_nodes, test_fun: Callable[[Coin, Coin], Sp return blocks, bundle, status, err @pytest.mark.asyncio - async def test_invalid_block_index(self, two_nodes): + async def test_invalid_block_index(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes blocks = await full_node_1.get_all_full_blocks() @@ -624,7 +634,7 @@ async def test_invalid_block_index(self, two_nodes): [int_to_bytes(start_height + 5)], ) dic = {ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert sb1 is None # the transaction may become valid later @@ -632,13 +642,13 @@ async def test_invalid_block_index(self, two_nodes): assert status == MempoolInclusionStatus.PENDING @pytest.mark.asyncio - async def test_block_index_missing_arg(self, two_nodes): + async def test_block_index_missing_arg(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes blocks = await full_node_1.get_all_full_blocks() cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, []) dic = {ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert sb1 is None # the transaction may become valid later @@ -646,49 +656,49 @@ async def test_block_index_missing_arg(self, two_nodes): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_correct_block_index(self, two_nodes): + async def test_correct_block_index(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, [int_to_bytes(1)]) dic = {ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err is None assert sb1 is spend_bundle1 assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_block_index_garbage(self, two_nodes): + async def test_block_index_garbage(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes # garbage at the end of the argument list is ignored cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, [int_to_bytes(1), b"garbage"]) dic = {ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err is None assert sb1 is spend_bundle1 assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_negative_block_index(self, two_nodes): + async def test_negative_block_index(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, [int_to_bytes(-1)]) dic = {ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err is None assert sb1 is spend_bundle1 assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_invalid_block_age(self, two_nodes): + async def test_invalid_block_age(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_RELATIVE, [int_to_bytes(5)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err == Err.ASSERT_HEIGHT_RELATIVE_FAILED assert sb1 is None @@ -696,12 +706,12 @@ async def test_invalid_block_age(self, two_nodes): assert status == MempoolInclusionStatus.PENDING @pytest.mark.asyncio - async def test_block_age_missing_arg(self, two_nodes): + async def test_block_age_missing_arg(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_RELATIVE, []) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err == Err.INVALID_CONDITION assert sb1 is None @@ -709,12 +719,14 @@ async def test_block_age_missing_arg(self, two_nodes): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_correct_block_age(self, two_nodes): + async def test_correct_block_age(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_RELATIVE, [int_to_bytes(1)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, num_blocks=4) + blocks, spend_bundle1, peer, status, err = await self.condition_tester( + bt, two_nodes, wallet_a, dic, num_blocks=4 + ) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err is None @@ -722,13 +734,15 @@ async def test_correct_block_age(self, two_nodes): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_block_age_garbage(self, two_nodes): + async def test_block_age_garbage(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes # garbage at the end of the argument list is ignored cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_RELATIVE, [int_to_bytes(1), b"garbage"]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, num_blocks=4) + blocks, spend_bundle1, peer, status, err = await self.condition_tester( + bt, two_nodes, wallet_a, dic, num_blocks=4 + ) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err is None @@ -736,12 +750,14 @@ async def test_block_age_garbage(self, two_nodes): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_negative_block_age(self, two_nodes): + async def test_negative_block_age(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_RELATIVE, [int_to_bytes(-1)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, num_blocks=4) + blocks, spend_bundle1, peer, status, err = await self.condition_tester( + bt, two_nodes, wallet_a, dic, num_blocks=4 + ) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err is None @@ -749,13 +765,13 @@ async def test_negative_block_age(self, two_nodes): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_correct_my_id(self, two_nodes): + async def test_correct_my_id(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes - coin = await next_block(full_node_1, WALLET_A, bt) + coin = await next_block(full_node_1, wallet_a, bt) cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_COIN_ID, [coin.name()]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, coin=coin) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic, coin=coin) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err is None @@ -763,14 +779,14 @@ async def test_correct_my_id(self, two_nodes): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_my_id_garbage(self, two_nodes): + async def test_my_id_garbage(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes - coin = await next_block(full_node_1, WALLET_A, bt) + coin = await next_block(full_node_1, wallet_a, bt) # garbage at the end of the argument list is ignored cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_COIN_ID, [coin.name(), b"garbage"]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, coin=coin) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic, coin=coin) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err is None @@ -778,14 +794,14 @@ async def test_my_id_garbage(self, two_nodes): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_invalid_my_id(self, two_nodes): + async def test_invalid_my_id(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes - coin = await next_block(full_node_1, WALLET_A, bt) - coin_2 = await next_block(full_node_1, WALLET_A, bt) + coin = await next_block(full_node_1, wallet_a, bt) + coin_2 = await next_block(full_node_1, wallet_a, bt) cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_COIN_ID, [coin_2.name()]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, coin=coin) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic, coin=coin) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err == Err.ASSERT_MY_COIN_ID_FAILED @@ -793,13 +809,13 @@ async def test_invalid_my_id(self, two_nodes): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_my_id_missing_arg(self, two_nodes): + async def test_my_id_missing_arg(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes blocks = await full_node_1.get_all_full_blocks() cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_COIN_ID, []) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err == Err.INVALID_CONDITION @@ -807,7 +823,7 @@ async def test_my_id_missing_arg(self, two_nodes): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_assert_time_exceeds(self, two_nodes): + async def test_assert_time_exceeds(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes # 5 seconds should be before the next block @@ -815,28 +831,28 @@ async def test_assert_time_exceeds(self, two_nodes): cvp = ConditionWithArgs(ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, [int_to_bytes(time_now)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err is None assert sb1 is spend_bundle1 assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_assert_time_fail(self, two_nodes): + async def test_assert_time_fail(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes time_now = full_node_1.full_node.blockchain.get_peak().timestamp + 1000 cvp = ConditionWithArgs(ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, [int_to_bytes(time_now)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err == Err.ASSERT_SECONDS_ABSOLUTE_FAILED assert sb1 is None assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_assert_height_pending(self, two_nodes): + async def test_assert_height_pending(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes print(full_node_1.full_node.blockchain.get_peak()) @@ -844,41 +860,41 @@ async def test_assert_height_pending(self, two_nodes): cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, [int_to_bytes(current_height + 4)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err == Err.ASSERT_HEIGHT_ABSOLUTE_FAILED assert sb1 is None assert status == MempoolInclusionStatus.PENDING @pytest.mark.asyncio - async def test_assert_time_negative(self, two_nodes): + async def test_assert_time_negative(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes time_now = -1 cvp = ConditionWithArgs(ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, [int_to_bytes(time_now)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err is None assert sb1 is spend_bundle1 assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_assert_time_missing_arg(self, two_nodes): + async def test_assert_time_missing_arg(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes cvp = ConditionWithArgs(ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, []) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err == Err.INVALID_CONDITION assert sb1 is None assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_assert_time_garbage(self, two_nodes): + async def test_assert_time_garbage(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes time_now = full_node_1.full_node.blockchain.get_peak().timestamp + 5 @@ -886,21 +902,21 @@ async def test_assert_time_garbage(self, two_nodes): # garbage at the end of the argument list is ignored cvp = ConditionWithArgs(ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, [int_to_bytes(time_now), b"garbage"]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err is None assert sb1 is spend_bundle1 assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_assert_time_relative_exceeds(self, two_nodes): + async def test_assert_time_relative_exceeds(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes time_relative = 3 cvp = ConditionWithArgs(ConditionOpcode.ASSERT_SECONDS_RELATIVE, [int_to_bytes(time_relative)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err == Err.ASSERT_SECONDS_RELATIVE_FAILED @@ -908,7 +924,7 @@ async def test_assert_time_relative_exceeds(self, two_nodes): assert status == MempoolInclusionStatus.FAILED for i in range(0, 4): - await full_node_1.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0")) + await full_node_1.farm_new_transaction_block(FarmNewBlockProtocol(bytes32(32 * b"0"))) tx2: full_node_protocol.RespondTransaction = full_node_protocol.RespondTransaction(spend_bundle1) @@ -920,7 +936,7 @@ async def test_assert_time_relative_exceeds(self, two_nodes): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_assert_time_relative_garbage(self, two_nodes): + async def test_assert_time_relative_garbage(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes time_relative = 0 @@ -928,7 +944,7 @@ async def test_assert_time_relative_garbage(self, two_nodes): # garbage at the end of the arguments is ignored cvp = ConditionWithArgs(ConditionOpcode.ASSERT_SECONDS_RELATIVE, [int_to_bytes(time_relative), b"garbage"]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err is None @@ -936,13 +952,13 @@ async def test_assert_time_relative_garbage(self, two_nodes): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_assert_time_relative_missing_arg(self, two_nodes): + async def test_assert_time_relative_missing_arg(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes cvp = ConditionWithArgs(ConditionOpcode.ASSERT_SECONDS_RELATIVE, []) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err == Err.INVALID_CONDITION @@ -950,14 +966,14 @@ async def test_assert_time_relative_missing_arg(self, two_nodes): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_assert_time_relative_negative(self, two_nodes): + async def test_assert_time_relative_negative(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes time_relative = -3 cvp = ConditionWithArgs(ConditionOpcode.ASSERT_SECONDS_RELATIVE, [int_to_bytes(time_relative)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err is None @@ -966,7 +982,7 @@ async def test_assert_time_relative_negative(self, two_nodes): # ensure one spend can assert a coin announcement from another spend @pytest.mark.asyncio - async def test_correct_coin_announcement_consumed(self, two_nodes): + async def test_correct_coin_announcement_consumed(self, bt, two_nodes, wallet_a): def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: announce = Announcement(coin_2.name(), b"test") cvp = ConditionWithArgs(ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT, [announce.name()]) @@ -974,13 +990,13 @@ def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: cvp2 = ConditionWithArgs(ConditionOpcode.CREATE_COIN_ANNOUNCEMENT, [b"test"]) dic2 = {cvp.opcode: [cvp2]} - spend_bundle1 = generate_test_spend_bundle(coin_1, dic) - spend_bundle2 = generate_test_spend_bundle(coin_2, dic2) + spend_bundle1 = generate_test_spend_bundle(wallet_a, coin_1, dic) + spend_bundle2 = generate_test_spend_bundle(wallet_a, coin_2, dic2) bundle = SpendBundle.aggregate([spend_bundle1, spend_bundle2]) return bundle full_node_1, full_node_2, server_1, server_2 = two_nodes - blocks, bundle, status, err = await self.condition_tester2(two_nodes, test_fun) + blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes, wallet_a, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) assert err is None @@ -990,7 +1006,7 @@ def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: # ensure one spend can assert a coin announcement from another spend, even # though the conditions have garbage (ignored) at the end @pytest.mark.asyncio - async def test_coin_announcement_garbage(self, two_nodes): + async def test_coin_announcement_garbage(self, bt, two_nodes, wallet_a): def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: announce = Announcement(coin_2.name(), b"test") # garbage at the end is ignored @@ -1000,13 +1016,13 @@ def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: # garbage at the end is ignored cvp2 = ConditionWithArgs(ConditionOpcode.CREATE_COIN_ANNOUNCEMENT, [b"test", b"garbage"]) dic2 = {cvp.opcode: [cvp2]} - spend_bundle1 = generate_test_spend_bundle(coin_1, dic) - spend_bundle2 = generate_test_spend_bundle(coin_2, dic2) + spend_bundle1 = generate_test_spend_bundle(wallet_a, coin_1, dic) + spend_bundle2 = generate_test_spend_bundle(wallet_a, coin_2, dic2) bundle = SpendBundle.aggregate([spend_bundle1, spend_bundle2]) return bundle full_node_1, full_node_2, server_1, server_2 = two_nodes - blocks, bundle, status, err = await self.condition_tester2(two_nodes, test_fun) + blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes, wallet_a, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) assert err is None @@ -1014,7 +1030,7 @@ def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_coin_announcement_missing_arg(self, two_nodes): + async def test_coin_announcement_missing_arg(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes def test_fun(coin_1: Coin, coin_2: Coin): @@ -1023,19 +1039,19 @@ def test_fun(coin_1: Coin, coin_2: Coin): dic = {cvp.opcode: [cvp]} cvp2 = ConditionWithArgs(ConditionOpcode.CREATE_COIN_ANNOUNCEMENT, [b"test"]) dic2 = {cvp.opcode: [cvp2]} - spend_bundle1 = generate_test_spend_bundle(coin_1, dic) - spend_bundle2 = generate_test_spend_bundle(coin_2, dic2) + spend_bundle1 = generate_test_spend_bundle(wallet_a, coin_1, dic) + spend_bundle2 = generate_test_spend_bundle(wallet_a, coin_2, dic2) return SpendBundle.aggregate([spend_bundle1, spend_bundle2]) - blocks, bundle, status, err = await self.condition_tester2(two_nodes, test_fun) + blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes, wallet_a, test_fun) assert err == Err.INVALID_CONDITION assert full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) is None assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_coin_announcement_missing_arg2(self, two_nodes): + async def test_coin_announcement_missing_arg2(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes def test_fun(coin_1: Coin, coin_2: Coin): @@ -1045,19 +1061,19 @@ def test_fun(coin_1: Coin, coin_2: Coin): # missing arg here cvp2 = ConditionWithArgs(ConditionOpcode.CREATE_COIN_ANNOUNCEMENT, []) dic2 = {cvp.opcode: [cvp2]} - spend_bundle1 = generate_test_spend_bundle(coin_1, dic) - spend_bundle2 = generate_test_spend_bundle(coin_2, dic2) + spend_bundle1 = generate_test_spend_bundle(wallet_a, coin_1, dic) + spend_bundle2 = generate_test_spend_bundle(wallet_a, coin_2, dic2) return SpendBundle.aggregate([spend_bundle1, spend_bundle2]) - blocks, bundle, status, err = await self.condition_tester2(two_nodes, test_fun) + blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes, wallet_a, test_fun) assert err == Err.INVALID_CONDITION assert full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) is None assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_coin_announcement_too_big(self, two_nodes): + async def test_coin_announcement_too_big(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes def test_fun(coin_1: Coin, coin_2: Coin): @@ -1069,12 +1085,12 @@ def test_fun(coin_1: Coin, coin_2: Coin): cvp2 = ConditionWithArgs(ConditionOpcode.CREATE_COIN_ANNOUNCEMENT, [b"test"]) dic2 = {cvp.opcode: [cvp2]} - spend_bundle1 = generate_test_spend_bundle(coin_1, dic) - spend_bundle2 = generate_test_spend_bundle(coin_2, dic2) + spend_bundle1 = generate_test_spend_bundle(wallet_a, coin_1, dic) + spend_bundle2 = generate_test_spend_bundle(wallet_a, coin_2, dic2) return SpendBundle.aggregate([spend_bundle1, spend_bundle2]) - blocks, bundle, status, err = await self.condition_tester2(two_nodes, test_fun) + blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes, wallet_a, test_fun) assert err == Err.ASSERT_ANNOUNCE_CONSUMED_FAILED assert full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) is None @@ -1092,7 +1108,7 @@ def test_fun(coin_1: Coin, coin_2: Coin): # ensure an assert coin announcement is rejected if it doesn't match the # create announcement @pytest.mark.asyncio - async def test_invalid_coin_announcement_rejected(self, two_nodes): + async def test_invalid_coin_announcement_rejected(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes def test_fun(coin_1: Coin, coin_2: Coin): @@ -1107,12 +1123,12 @@ def test_fun(coin_1: Coin, coin_2: Coin): [b"wrong test"], ) dic2 = {cvp.opcode: [cvp2]} - spend_bundle1 = generate_test_spend_bundle(coin_1, dic) - spend_bundle2 = generate_test_spend_bundle(coin_2, dic2) + spend_bundle1 = generate_test_spend_bundle(wallet_a, coin_1, dic) + spend_bundle2 = generate_test_spend_bundle(wallet_a, coin_2, dic2) return SpendBundle.aggregate([spend_bundle1, spend_bundle2]) - blocks, bundle, status, err = await self.condition_tester2(two_nodes, test_fun) + blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes, wallet_a, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) @@ -1121,7 +1137,7 @@ def test_fun(coin_1: Coin, coin_2: Coin): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_invalid_coin_announcement_rejected_two(self, two_nodes): + async def test_invalid_coin_announcement_rejected_two(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes def test_fun(coin_1: Coin, coin_2: Coin): @@ -1133,13 +1149,13 @@ def test_fun(coin_1: Coin, coin_2: Coin): cvp2 = ConditionWithArgs(ConditionOpcode.CREATE_COIN_ANNOUNCEMENT, [b"test"]) dic2 = {cvp.opcode: [cvp2]} - spend_bundle1 = generate_test_spend_bundle(coin_1, dic) + spend_bundle1 = generate_test_spend_bundle(wallet_a, coin_1, dic) # coin 2 is making the announcement, right message wrong coin - spend_bundle2 = generate_test_spend_bundle(coin_2, dic2) + spend_bundle2 = generate_test_spend_bundle(wallet_a, coin_2, dic2) return SpendBundle.aggregate([spend_bundle1, spend_bundle2]) - blocks, bundle, status, err = await self.condition_tester2(two_nodes, test_fun) + blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes, wallet_a, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) assert err == Err.ASSERT_ANNOUNCE_CONSUMED_FAILED @@ -1147,7 +1163,7 @@ def test_fun(coin_1: Coin, coin_2: Coin): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_correct_puzzle_announcement(self, two_nodes): + async def test_correct_puzzle_announcement(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes def test_fun(coin_1: Coin, coin_2: Coin): @@ -1159,12 +1175,12 @@ def test_fun(coin_1: Coin, coin_2: Coin): cvp2 = ConditionWithArgs(ConditionOpcode.CREATE_PUZZLE_ANNOUNCEMENT, [bytes(0x80)]) dic2 = {cvp.opcode: [cvp2]} - spend_bundle1 = generate_test_spend_bundle(coin_1, dic) - spend_bundle2 = generate_test_spend_bundle(coin_2, dic2) + spend_bundle1 = generate_test_spend_bundle(wallet_a, coin_1, dic) + spend_bundle2 = generate_test_spend_bundle(wallet_a, coin_2, dic2) return SpendBundle.aggregate([spend_bundle1, spend_bundle2]) - blocks, bundle, status, err = await self.condition_tester2(two_nodes, test_fun) + blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes, wallet_a, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) @@ -1173,7 +1189,7 @@ def test_fun(coin_1: Coin, coin_2: Coin): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_puzzle_announcement_garbage(self, two_nodes): + async def test_puzzle_announcement_garbage(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes def test_fun(coin_1: Coin, coin_2: Coin): @@ -1185,12 +1201,12 @@ def test_fun(coin_1: Coin, coin_2: Coin): # garbage at the end is ignored cvp2 = ConditionWithArgs(ConditionOpcode.CREATE_PUZZLE_ANNOUNCEMENT, [bytes(0x80), b"garbage"]) dic2 = {cvp.opcode: [cvp2]} - spend_bundle1 = generate_test_spend_bundle(coin_1, dic) - spend_bundle2 = generate_test_spend_bundle(coin_2, dic2) + spend_bundle1 = generate_test_spend_bundle(wallet_a, coin_1, dic) + spend_bundle2 = generate_test_spend_bundle(wallet_a, coin_2, dic2) return SpendBundle.aggregate([spend_bundle1, spend_bundle2]) - blocks, bundle, status, err = await self.condition_tester2(two_nodes, test_fun) + blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes, wallet_a, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) assert err is None @@ -1198,7 +1214,7 @@ def test_fun(coin_1: Coin, coin_2: Coin): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_puzzle_announcement_missing_arg(self, two_nodes): + async def test_puzzle_announcement_missing_arg(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes def test_fun(coin_1: Coin, coin_2: Coin): @@ -1210,12 +1226,12 @@ def test_fun(coin_1: Coin, coin_2: Coin): [b"test"], ) dic2 = {cvp.opcode: [cvp2]} - spend_bundle1 = generate_test_spend_bundle(coin_1, dic) - spend_bundle2 = generate_test_spend_bundle(coin_2, dic2) + spend_bundle1 = generate_test_spend_bundle(wallet_a, coin_1, dic) + spend_bundle2 = generate_test_spend_bundle(wallet_a, coin_2, dic2) return SpendBundle.aggregate([spend_bundle1, spend_bundle2]) - blocks, bundle, status, err = await self.condition_tester2(two_nodes, test_fun) + blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes, wallet_a, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) @@ -1224,7 +1240,7 @@ def test_fun(coin_1: Coin, coin_2: Coin): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_puzzle_announcement_missing_arg2(self, two_nodes): + async def test_puzzle_announcement_missing_arg2(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes def test_fun(coin_1: Coin, coin_2: Coin): @@ -1238,12 +1254,12 @@ def test_fun(coin_1: Coin, coin_2: Coin): [], ) dic2 = {cvp.opcode: [cvp2]} - spend_bundle1 = generate_test_spend_bundle(coin_1, dic) - spend_bundle2 = generate_test_spend_bundle(coin_2, dic2) + spend_bundle1 = generate_test_spend_bundle(wallet_a, coin_1, dic) + spend_bundle2 = generate_test_spend_bundle(wallet_a, coin_2, dic2) return SpendBundle.aggregate([spend_bundle1, spend_bundle2]) - blocks, bundle, status, err = await self.condition_tester2(two_nodes, test_fun) + blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes, wallet_a, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) @@ -1252,7 +1268,7 @@ def test_fun(coin_1: Coin, coin_2: Coin): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_invalid_puzzle_announcement_rejected(self, two_nodes): + async def test_invalid_puzzle_announcement_rejected(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes def test_fun(coin_1: Coin, coin_2: Coin): @@ -1267,12 +1283,12 @@ def test_fun(coin_1: Coin, coin_2: Coin): [b"wrong test"], ) dic2 = {cvp.opcode: [cvp2]} - spend_bundle1 = generate_test_spend_bundle(coin_1, dic) - spend_bundle2 = generate_test_spend_bundle(coin_2, dic2) + spend_bundle1 = generate_test_spend_bundle(wallet_a, coin_1, dic) + spend_bundle2 = generate_test_spend_bundle(wallet_a, coin_2, dic2) return SpendBundle.aggregate([spend_bundle1, spend_bundle2]) - blocks, bundle, status, err = await self.condition_tester2(two_nodes, test_fun) + blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes, wallet_a, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) @@ -1281,7 +1297,7 @@ def test_fun(coin_1: Coin, coin_2: Coin): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_invalid_puzzle_announcement_rejected_two(self, two_nodes): + async def test_invalid_puzzle_announcement_rejected_two(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes def test_fun(coin_1: Coin, coin_2: Coin): @@ -1296,12 +1312,12 @@ def test_fun(coin_1: Coin, coin_2: Coin): [b"test"], ) dic2 = {cvp.opcode: [cvp2]} - spend_bundle1 = generate_test_spend_bundle(coin_1, dic) - spend_bundle2 = generate_test_spend_bundle(coin_2, dic2) + spend_bundle1 = generate_test_spend_bundle(wallet_a, coin_1, dic) + spend_bundle2 = generate_test_spend_bundle(wallet_a, coin_2, dic2) return SpendBundle.aggregate([spend_bundle1, spend_bundle2]) - blocks, bundle, status, err = await self.condition_tester2(two_nodes, test_fun) + blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes, wallet_a, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) @@ -1310,12 +1326,12 @@ def test_fun(coin_1: Coin, coin_2: Coin): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_assert_fee_condition(self, two_nodes): + async def test_assert_fee_condition(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes cvp = ConditionWithArgs(ConditionOpcode.RESERVE_FEE, [int_to_bytes(10)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, fee=10) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic, fee=10) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err is None @@ -1323,13 +1339,13 @@ async def test_assert_fee_condition(self, two_nodes): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_assert_fee_condition_garbage(self, two_nodes): + async def test_assert_fee_condition_garbage(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes # garbage at the end of the arguments is ignored cvp = ConditionWithArgs(ConditionOpcode.RESERVE_FEE, [int_to_bytes(10), b"garbage"]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, fee=10) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic, fee=10) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err is None @@ -1337,20 +1353,20 @@ async def test_assert_fee_condition_garbage(self, two_nodes): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_assert_fee_condition_missing_arg(self, two_nodes): + async def test_assert_fee_condition_missing_arg(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes cvp = ConditionWithArgs(ConditionOpcode.RESERVE_FEE, []) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, fee=10) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic, fee=10) assert err == Err.INVALID_CONDITION assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_assert_fee_condition_negative_fee(self, two_nodes): + async def test_assert_fee_condition_negative_fee(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes cvp = ConditionWithArgs(ConditionOpcode.RESERVE_FEE, [int_to_bytes(-1)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, fee=10) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic, fee=10) assert err == Err.RESERVE_FEE_CONDITION_FAILED assert status == MempoolInclusionStatus.FAILED blocks = bt.get_consecutive_blocks( @@ -1362,11 +1378,11 @@ async def test_assert_fee_condition_negative_fee(self, two_nodes): ) @pytest.mark.asyncio - async def test_assert_fee_condition_fee_too_large(self, two_nodes): + async def test_assert_fee_condition_fee_too_large(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes cvp = ConditionWithArgs(ConditionOpcode.RESERVE_FEE, [int_to_bytes(2 ** 64)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, fee=10) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic, fee=10) assert err == Err.RESERVE_FEE_CONDITION_FAILED assert status == MempoolInclusionStatus.FAILED blocks = bt.get_consecutive_blocks( @@ -1378,13 +1394,13 @@ async def test_assert_fee_condition_fee_too_large(self, two_nodes): ) @pytest.mark.asyncio - async def test_assert_fee_condition_wrong_fee(self, two_nodes): + async def test_assert_fee_condition_wrong_fee(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes cvp = ConditionWithArgs(ConditionOpcode.RESERVE_FEE, [int_to_bytes(10)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, fee=9) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic, fee=9) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err == Err.RESERVE_FEE_CONDITION_FAILED @@ -1392,8 +1408,8 @@ async def test_assert_fee_condition_wrong_fee(self, two_nodes): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_stealing_fee(self, two_nodes): - reward_ph = WALLET_A.get_new_puzzlehash() + async def test_stealing_fee(self, bt, two_nodes, wallet_a): + reward_ph = wallet_a.get_new_puzzlehash() full_node_1, full_node_2, server_1, server_2 = two_nodes blocks = await full_node_1.get_all_full_blocks() start_height = blocks[-1].height @@ -1406,7 +1422,7 @@ async def test_stealing_fee(self, two_nodes): ) full_node_1, full_node_2, server_1, server_2 = two_nodes - peer = await connect_and_get_peer(server_1, server_2) + peer = await connect_and_get_peer(server_1, server_2, bt.config["self_hostname"]) for block in blocks: await full_node_1.full_node.respond_block(full_node_protocol.RespondBlock(block)) @@ -1425,10 +1441,10 @@ async def test_stealing_fee(self, two_nodes): for coin in list(blocks[-1].get_included_reward_coins()): if coin.amount == coin_1.amount: coin_2 = coin - spend_bundle1 = generate_test_spend_bundle(coin_1, dic, uint64(fee)) + spend_bundle1 = generate_test_spend_bundle(wallet_a, coin_1, dic, uint64(fee)) - steal_fee_spendbundle = WALLET_A.generate_signed_transaction( - coin_1.amount + fee - 4, receiver_puzzlehash, coin_2 + steal_fee_spendbundle = wallet_a.generate_signed_transaction( + uint64(coin_1.amount + fee - 4), receiver_puzzlehash, coin_2 ) assert spend_bundle1 is not None @@ -1449,15 +1465,33 @@ async def test_stealing_fee(self, two_nodes): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_double_spend_same_bundle(self, two_nodes): + async def test_double_spend_same_bundle(self, bt, two_nodes, wallet_a): + reward_ph = wallet_a.get_new_puzzlehash() full_node_1, full_node_2, server_1, server_2 = two_nodes + blocks = await full_node_1.get_all_full_blocks() + start_height = blocks[-1].height + 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, + ) + peer = await connect_and_get_peer(server_1, server_2, bt.config["self_hostname"]) - coin = await next_block(full_node_1, WALLET_A, bt) - spend_bundle1 = generate_test_spend_bundle(coin) + 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) + # coin = list(blocks[-1].get_included_reward_coins())[0] + # spend_bundle1 = generate_test_spend_bundle(wallet_a, coin) + coin = await next_block(full_node_1, wallet_a, bt) + spend_bundle1 = generate_test_spend_bundle(wallet_a, coin) assert spend_bundle1 is not None spend_bundle2 = generate_test_spend_bundle( + wallet_a, coin, new_puzzle_hash=BURN_PUZZLE_HASH_2, ) @@ -1468,7 +1502,7 @@ async def test_double_spend_same_bundle(self, two_nodes): tx: full_node_protocol.RespondTransaction = full_node_protocol.RespondTransaction(spend_bundle_combined) - peer = await connect_and_get_peer(server_1, server_2) + peer = await connect_and_get_peer(server_1, server_2, bt.config["self_hostname"]) status, err = await respond_transaction(full_node_1, tx, peer) sb = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle_combined.name()) @@ -1477,12 +1511,27 @@ async def test_double_spend_same_bundle(self, two_nodes): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_agg_sig_condition(self, two_nodes): + async def test_agg_sig_condition(self, bt, two_nodes, wallet_a): + reward_ph = wallet_a.get_new_puzzlehash() full_node_1, full_node_2, server_1, server_2 = two_nodes + blocks = await full_node_1.get_all_full_blocks() + start_height = blocks[-1].height + 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, + ) - coin = await next_block(full_node_1, WALLET_A, bt) + 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) - spend_bundle_0 = generate_test_spend_bundle(coin) + coin = await next_block(full_node_1, wallet_a, bt) + # coin = list(blocks[-1].get_included_reward_coins())[0] + spend_bundle_0 = generate_test_spend_bundle(wallet_a, coin) unsigned: List[CoinSpend] = spend_bundle_0.coin_spends assert len(unsigned) == 1 @@ -1500,7 +1549,7 @@ async def test_agg_sig_condition(self, two_nodes): # # assert pkm_pairs[0][1] == solution.rest().first().get_tree_hash() + coin_spend.coin.name() # - # spend_bundle = WALLET_A.sign_transaction(unsigned) + # spend_bundle = wallet_a.sign_transaction(unsigned) # assert spend_bundle is not None # # tx: full_node_protocol.RespondTransaction = full_node_protocol.RespondTransaction(spend_bundle) @@ -1510,13 +1559,13 @@ async def test_agg_sig_condition(self, two_nodes): # assert sb is spend_bundle @pytest.mark.asyncio - async def test_correct_my_parent(self, two_nodes): + async def test_correct_my_parent(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes - coin = await next_block(full_node_1, WALLET_A, bt) + coin = await next_block(full_node_1, wallet_a, bt) cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_PARENT_ID, [coin.parent_coin_info]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, coin=coin) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic, coin=coin) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1525,14 +1574,14 @@ async def test_correct_my_parent(self, two_nodes): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_my_parent_garbage(self, two_nodes): + async def test_my_parent_garbage(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes - coin = await next_block(full_node_1, WALLET_A, bt) + coin = await next_block(full_node_1, wallet_a, bt) # garbage at the end of the arguments list is allowed but stripped cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_PARENT_ID, [coin.parent_coin_info, b"garbage"]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, coin=coin) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic, coin=coin) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1541,13 +1590,13 @@ async def test_my_parent_garbage(self, two_nodes): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_my_parent_missing_arg(self, two_nodes): + async def test_my_parent_missing_arg(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes blocks = await full_node_1.get_all_full_blocks() cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_PARENT_ID, []) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1556,14 +1605,14 @@ async def test_my_parent_missing_arg(self, two_nodes): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_invalid_my_parent(self, two_nodes): + async def test_invalid_my_parent(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes - coin = await next_block(full_node_1, WALLET_A, bt) - coin_2 = await next_block(full_node_1, WALLET_A, bt) + coin = await next_block(full_node_1, wallet_a, bt) + coin_2 = await next_block(full_node_1, wallet_a, bt) cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_PARENT_ID, [coin_2.parent_coin_info]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, coin=coin) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic, coin=coin) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1572,13 +1621,13 @@ async def test_invalid_my_parent(self, two_nodes): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_correct_my_puzhash(self, two_nodes): + async def test_correct_my_puzhash(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes - coin = await next_block(full_node_1, WALLET_A, bt) + coin = await next_block(full_node_1, wallet_a, bt) cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_PUZZLEHASH, [coin.puzzle_hash]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, coin=coin) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic, coin=coin) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1587,14 +1636,14 @@ async def test_correct_my_puzhash(self, two_nodes): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_my_puzhash_garbage(self, two_nodes): + async def test_my_puzhash_garbage(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes - coin = await next_block(full_node_1, WALLET_A, bt) + coin = await next_block(full_node_1, wallet_a, bt) # garbage at the end of the arguments list is allowed but stripped cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_PUZZLEHASH, [coin.puzzle_hash, b"garbage"]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, coin=coin) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic, coin=coin) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1603,13 +1652,13 @@ async def test_my_puzhash_garbage(self, two_nodes): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_my_puzhash_missing_arg(self, two_nodes): + async def test_my_puzhash_missing_arg(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes blocks = await full_node_1.get_all_full_blocks() cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_PUZZLEHASH, []) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1618,13 +1667,13 @@ async def test_my_puzhash_missing_arg(self, two_nodes): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_invalid_my_puzhash(self, two_nodes): + async def test_invalid_my_puzhash(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes - coin = await next_block(full_node_1, WALLET_A, bt) + coin = await next_block(full_node_1, wallet_a, bt) cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_PUZZLEHASH, [Program.to([]).get_tree_hash()]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, coin=coin) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic, coin=coin) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1633,13 +1682,13 @@ async def test_invalid_my_puzhash(self, two_nodes): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_correct_my_amount(self, two_nodes): + async def test_correct_my_amount(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes - coin = await next_block(full_node_1, WALLET_A, bt) + coin = await next_block(full_node_1, wallet_a, bt) cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_AMOUNT, [int_to_bytes(coin.amount)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, coin=coin) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic, coin=coin) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1648,14 +1697,14 @@ async def test_correct_my_amount(self, two_nodes): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_my_amount_garbage(self, two_nodes): + async def test_my_amount_garbage(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes - coin = await next_block(full_node_1, WALLET_A, bt) + coin = await next_block(full_node_1, wallet_a, bt) # garbage at the end of the arguments list is allowed but stripped cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_AMOUNT, [int_to_bytes(coin.amount), b"garbage"]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic, coin=coin) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic, coin=coin) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1664,13 +1713,13 @@ async def test_my_amount_garbage(self, two_nodes): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_my_amount_missing_arg(self, two_nodes): + async def test_my_amount_missing_arg(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes blocks = await full_node_1.get_all_full_blocks() cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_AMOUNT, []) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1679,13 +1728,13 @@ async def test_my_amount_missing_arg(self, two_nodes): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_invalid_my_amount(self, two_nodes): + async def test_invalid_my_amount(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes blocks = await full_node_1.get_all_full_blocks() cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_AMOUNT, [int_to_bytes(1000)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1694,13 +1743,13 @@ async def test_invalid_my_amount(self, two_nodes): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_negative_my_amount(self, two_nodes): + async def test_negative_my_amount(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes blocks = await full_node_1.get_all_full_blocks() cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_AMOUNT, [int_to_bytes(-1)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1709,13 +1758,13 @@ async def test_negative_my_amount(self, two_nodes): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_my_amount_too_large(self, two_nodes): + async def test_my_amount_too_large(self, bt, two_nodes, wallet_a): full_node_1, full_node_2, server_1, server_2 = two_nodes blocks = await full_node_1.get_all_full_blocks() cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_AMOUNT, [int_to_bytes(2 ** 64)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(two_nodes, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1776,7 +1825,7 @@ def test_invalid_condition_args_terminator(self, softfork_height): (False, 2300000, 1, None), ], ) - def test_div(self, mempool, height, operand, expected): + def test_div(self, mempool, height: uint32, operand, expected): # op_div is disallowed on negative numbers in the mempool, and after the # softfork @@ -2011,7 +2060,7 @@ def test_create_coin_with_hint(self, softfork_height): (False, 2300000), ], ) - def test_unknown_condition(self, mempool, height): + def test_unknown_condition(self, mempool: bool, height: uint32): for c in ['(1 100 "foo" "bar")', "(100)", "(1 1) (2 2) (3 3)", '("foobar")']: npc_result = generator_condition_tester(c, mempool_mode=mempool, height=height) print(npc_result) @@ -2352,8 +2401,8 @@ def test_many_create_coin(self, softfork_height): print(f"run time:{run_time}") @pytest.mark.asyncio - async def test_invalid_coin_spend_coin(self, two_nodes): - reward_ph = WALLET_A.get_new_puzzlehash() + async def test_invalid_coin_spend_coin(self, bt, two_nodes, wallet_a): + reward_ph = wallet_a.get_new_puzzlehash() blocks = bt.get_consecutive_blocks( 5, guarantee_transaction_block=True, @@ -2367,7 +2416,7 @@ async def test_invalid_coin_spend_coin(self, two_nodes): await time_out_assert(60, node_height_at_least, True, full_node_1, blocks[-1].height) - spend_bundle = generate_test_spend_bundle(list(blocks[-1].get_included_reward_coins())[0]) + spend_bundle = generate_test_spend_bundle(wallet_a, list(blocks[-1].get_included_reward_coins())[0]) coin_spend_0 = recursive_replace(spend_bundle.coin_spends[0], "coin.puzzle_hash", bytes32([1] * 32)) new_bundle = recursive_replace(spend_bundle, "coin_spends", [coin_spend_0] + spend_bundle.coin_spends[1:]) assert spend_bundle is not None @@ -2377,10 +2426,10 @@ async def test_invalid_coin_spend_coin(self, two_nodes): class TestPkmPairs: - h1 = b"a" * 32 - h2 = b"b" * 32 - h3 = b"c" * 32 - h4 = b"d" * 32 + h1 = bytes32(b"a" * 32) + h2 = bytes32(b"b" * 32) + h3 = bytes32(b"c" * 32) + h4 = bytes32(b"d" * 32) pk1 = G1Element.generator() pk2 = G1Element.generator() diff --git a/tests/core/full_node/test_mempool_performance.py b/tests/core/full_node/test_mempool_performance.py index 3b438b3abe16..f5b6ab8855f8 100644 --- a/tests/core/full_node/test_mempool_performance.py +++ b/tests/core/full_node/test_mempool_performance.py @@ -13,7 +13,7 @@ from chia.wallet.transaction_record import TransactionRecord from chia.wallet.wallet_node import WalletNode from tests.connection_utils import connect_and_get_peer -from tests.setup_nodes import bt, self_hostname, setup_simulators_and_wallets +from tests.setup_nodes import setup_simulators_and_wallets from tests.time_out_assert import time_out_assert @@ -43,13 +43,13 @@ def event_loop(): class TestMempoolPerformance: @pytest_asyncio.fixture(scope="module") - async def wallet_nodes(self): + async def wallet_nodes(self, bt): key_seed = bt.farmer_master_sk_entropy async for _ in setup_simulators_and_wallets(2, 1, {}, key_seed=key_seed): yield _ @pytest.mark.asyncio - async def test_mempool_update_performance(self, wallet_nodes, default_400_blocks): + async def test_mempool_update_performance(self, bt, wallet_nodes, default_400_blocks, self_hostname): blocks = default_400_blocks full_nodes, wallets = wallet_nodes wallet_node = wallets[0][0] @@ -72,7 +72,7 @@ async def test_mempool_update_performance(self, wallet_nodes, default_400_blocks big_transaction: TransactionRecord = await wallet.generate_signed_transaction(send_amount, ph, fee_amount) - peer = await connect_and_get_peer(server_1, server_2) + peer = await connect_and_get_peer(server_1, server_2, self_hostname) await full_node_api_1.respond_transaction( full_node_protocol.RespondTransaction(big_transaction.spend_bundle), peer, test=True ) diff --git a/tests/core/full_node/test_node_load.py b/tests/core/full_node/test_node_load.py index f4bc3737f391..95d6010f7a7c 100644 --- a/tests/core/full_node/test_node_load.py +++ b/tests/core/full_node/test_node_load.py @@ -8,7 +8,7 @@ from chia.types.peer_info import PeerInfo from chia.util.ints import uint16 from tests.connection_utils import connect_and_get_peer -from tests.setup_nodes import bt, self_hostname, setup_two_nodes, test_constants +from tests.setup_nodes import setup_two_nodes, test_constants from tests.time_out_assert import time_out_assert @@ -20,16 +20,16 @@ def event_loop(): class TestNodeLoad: @pytest_asyncio.fixture(scope="function") - async def two_nodes(self, db_version): - async for _ in setup_two_nodes(test_constants, db_version=db_version): + async def two_nodes(self, db_version, self_hostname): + async for _ in setup_two_nodes(test_constants, db_version=db_version, self_hostname=self_hostname): yield _ @pytest.mark.asyncio - async def test_blocks_load(self, two_nodes): + async def test_blocks_load(self, bt, two_nodes, self_hostname): num_blocks = 50 full_node_1, full_node_2, server_1, server_2 = two_nodes blocks = bt.get_consecutive_blocks(num_blocks) - peer = await connect_and_get_peer(server_1, server_2) + peer = await connect_and_get_peer(server_1, server_2, self_hostname) await full_node_1.full_node.respond_block(full_node_protocol.RespondBlock(blocks[0]), peer) await server_2.start_client(PeerInfo(self_hostname, uint16(server_1._port)), None) diff --git a/tests/core/full_node/test_performance.py b/tests/core/full_node/test_performance.py index e50bd3891bd1..9799616e5e15 100644 --- a/tests/core/full_node/test_performance.py +++ b/tests/core/full_node/test_performance.py @@ -20,11 +20,11 @@ from chia.util.ints import uint64 from tests.wallet_tools import WalletTool -from tests.connection_utils import add_dummy_connection, connect_and_get_peer +from tests.connection_utils import add_dummy_connection from tests.core.full_node.stores.test_coin_store import get_future_reward_coins from tests.core.node_height import node_height_at_least -from tests.setup_nodes import bt, setup_simulators_and_wallets, test_constants -from tests.time_out_assert import time_out_assert, time_out_assert_custom_interval, time_out_messages +from tests.setup_nodes import setup_simulators_and_wallets +from tests.time_out_assert import time_out_assert log = logging.getLogger(__name__) @@ -46,7 +46,7 @@ def event_loop(): @pytest_asyncio.fixture(scope="module") -async def wallet_nodes(): +async def wallet_nodes(bt): async_gen = setup_simulators_and_wallets(1, 1, {"MEMPOOL_BLOCK_BUFFER": 1, "MAX_BLOCK_COST_CLVM": 11000000000}) nodes, wallets = await async_gen.__anext__() full_node_1 = nodes[0] @@ -61,7 +61,7 @@ async def wallet_nodes(): class TestPerformance: @pytest.mark.asyncio - async def test_full_block_performance(self, wallet_nodes): + async def test_full_block_performance(self, bt, wallet_nodes, self_hostname): full_node_1, server_1, wallet_a, wallet_receiver = wallet_nodes blocks = await full_node_1.get_all_full_blocks() full_node_1.full_node.mempool_manager.limit_factor = 1 @@ -82,7 +82,7 @@ async def test_full_block_performance(self, wallet_nodes): if full_node_1.full_node.blockchain.get_peak() is not None else -1 ) - incoming_queue, node_id = await add_dummy_connection(server_1, 12312) + incoming_queue, node_id = await add_dummy_connection(server_1, self_hostname, 12312) fake_peer = server_1.all_connections[node_id] # Mempool has capacity of 100, make 110 unspents that we can use puzzle_hashes = [] diff --git a/tests/core/full_node/test_transactions.py b/tests/core/full_node/test_transactions.py index e0a2f5a0f894..37051af8a567 100644 --- a/tests/core/full_node/test_transactions.py +++ b/tests/core/full_node/test_transactions.py @@ -12,7 +12,7 @@ from chia.simulator.simulator_protocol import FarmNewBlockProtocol from chia.types.peer_info import PeerInfo from chia.util.ints import uint16, uint32 -from tests.setup_nodes import self_hostname, setup_simulators_and_wallets +from tests.setup_nodes import setup_simulators_and_wallets from tests.time_out_assert import time_out_assert @@ -39,7 +39,7 @@ async def three_nodes_two_wallets(self): yield _ @pytest.mark.asyncio - async def test_wallet_coinbase(self, wallet_node): + async def test_wallet_coinbase(self, wallet_node, self_hostname): num_blocks = 5 full_nodes, wallets = wallet_node full_node_api = full_nodes[0] @@ -61,7 +61,7 @@ async def test_wallet_coinbase(self, wallet_node): await time_out_assert(10, wallet.get_confirmed_balance, funds) @pytest.mark.asyncio - async def test_tx_propagation(self, three_nodes_two_wallets): + async def test_tx_propagation(self, three_nodes_two_wallets, self_hostname): num_blocks = 5 full_nodes, wallets = three_nodes_two_wallets @@ -143,7 +143,7 @@ async def peak_height(fna: FullNodeAPI): await time_out_assert(15, wallet_1.wallet_state_manager.main_wallet.get_confirmed_balance, 10) @pytest.mark.asyncio - async def test_mempool_tx_sync(self, three_nodes_two_wallets): + async def test_mempool_tx_sync(self, three_nodes_two_wallets, self_hostname): num_blocks = 5 full_nodes, wallets = three_nodes_two_wallets diff --git a/tests/core/server/test_dos.py b/tests/core/server/test_dos.py index 59a31d3f7f5b..84fa981def90 100644 --- a/tests/core/server/test_dos.py +++ b/tests/core/server/test_dos.py @@ -17,7 +17,7 @@ from chia.types.peer_info import PeerInfo from chia.util.ints import uint16, uint64 from chia.util.errors import Err -from tests.setup_nodes import self_hostname, setup_simulators_and_wallets +from tests.setup_nodes import setup_simulators_and_wallets from tests.time_out_assert import time_out_assert log = logging.getLogger(__name__) @@ -52,7 +52,7 @@ def process_msg_and_check(self, msg): class TestDos: @pytest.mark.asyncio - async def test_large_message_disconnect_and_ban(self, setup_two_nodes): + async def test_large_message_disconnect_and_ban(self, setup_two_nodes, self_hostname): nodes, _ = setup_two_nodes server_1 = nodes[0].full_node.server server_2 = nodes[1].full_node.server @@ -100,7 +100,7 @@ async def test_large_message_disconnect_and_ban(self, setup_two_nodes): await session.close() @pytest.mark.asyncio - async def test_bad_handshake_and_ban(self, setup_two_nodes): + async def test_bad_handshake_and_ban(self, setup_two_nodes, self_hostname): nodes, _ = setup_two_nodes server_1 = nodes[0].full_node.server server_2 = nodes[1].full_node.server @@ -146,7 +146,7 @@ async def test_bad_handshake_and_ban(self, setup_two_nodes): await session.close() @pytest.mark.asyncio - async def test_invalid_protocol_handshake(self, setup_two_nodes): + async def test_invalid_protocol_handshake(self, setup_two_nodes, self_hostname): nodes, _ = setup_two_nodes server_1 = nodes[0].full_node.server server_2 = nodes[1].full_node.server @@ -179,7 +179,7 @@ async def test_invalid_protocol_handshake(self, setup_two_nodes): await asyncio.sleep(1) # give some time for cleanup to work @pytest.mark.asyncio - async def test_spam_tx(self, setup_two_nodes): + async def test_spam_tx(self, setup_two_nodes, self_hostname): nodes, _ = setup_two_nodes full_node_1, full_node_2 = nodes server_1 = nodes[0].full_node.server @@ -232,7 +232,7 @@ def is_banned(): await time_out_assert(15, is_banned) @pytest.mark.asyncio - async def test_spam_message_non_tx(self, setup_two_nodes): + async def test_spam_message_non_tx(self, setup_two_nodes, self_hostname): nodes, _ = setup_two_nodes full_node_1, full_node_2 = nodes server_1 = nodes[0].full_node.server @@ -281,7 +281,7 @@ def is_banned(): await time_out_assert(15, is_banned) @pytest.mark.asyncio - async def test_spam_message_too_large(self, setup_two_nodes): + async def test_spam_message_too_large(self, setup_two_nodes, self_hostname): nodes, _ = setup_two_nodes full_node_1, full_node_2 = nodes server_1 = nodes[0].full_node.server diff --git a/tests/core/ssl/test_ssl.py b/tests/core/ssl/test_ssl.py index b983fc0266fc..66e7a381ee10 100644 --- a/tests/core/ssl/test_ssl.py +++ b/tests/core/ssl/test_ssl.py @@ -13,8 +13,6 @@ from tests.block_tools import test_constants from chia.util.ints import uint16 from tests.setup_nodes import ( - bt, - self_hostname, setup_farmer_harvester, setup_introducer, setup_simulators_and_wallets, @@ -23,7 +21,7 @@ from tests.util.socket import find_available_listen_port -async def establish_connection(server: ChiaServer, ssl_context) -> bool: +async def establish_connection(server: ChiaServer, self_hostname: str, ssl_context) -> bool: timeout = aiohttp.ClientTimeout(total=10) session = aiohttp.ClientSession(timeout=timeout) dummy_port = 5 # this does not matter @@ -55,8 +53,8 @@ async def establish_connection(server: ChiaServer, ssl_context) -> bool: class TestSSL: @pytest_asyncio.fixture(scope="function") - async def harvester_farmer(self): - async for _ in setup_farmer_harvester(test_constants): + async def harvester_farmer(self, bt): + async for _ in setup_farmer_harvester(bt, test_constants): yield _ @pytest_asyncio.fixture(scope="function") @@ -65,13 +63,13 @@ async def wallet_node(self): yield _ @pytest_asyncio.fixture(scope="function") - async def introducer(self): + async def introducer(self, bt): introducer_port = find_available_listen_port("introducer") - async for _ in setup_introducer(introducer_port): + async for _ in setup_introducer(bt, introducer_port): yield _ @pytest_asyncio.fixture(scope="function") - async def timelord(self): + async def timelord(self, bt): timelord_port = find_available_listen_port("timelord") node_port = find_available_listen_port("node") rpc_port = find_available_listen_port("rpc") @@ -80,7 +78,7 @@ async def timelord(self): yield _ @pytest.mark.asyncio - async def test_public_connections(self, wallet_node): + async def test_public_connections(self, wallet_node, self_hostname): full_nodes, wallets = wallet_node full_node_api = full_nodes[0] server_1: ChiaServer = full_node_api.full_node.server @@ -90,7 +88,7 @@ async def test_public_connections(self, wallet_node): assert success is True @pytest.mark.asyncio - async def test_farmer(self, harvester_farmer): + async def test_farmer(self, harvester_farmer, self_hostname): harvester_service, farmer_service = harvester_farmer farmer_api = farmer_service._api @@ -107,7 +105,7 @@ async def test_farmer(self, harvester_farmer): ssl_context = ssl_context_for_client( farmer_server.ca_private_crt_path, farmer_server.ca_private_key_path, priv_crt, priv_key ) - connected = await establish_connection(farmer_server, ssl_context) + connected = await establish_connection(farmer_server, self_hostname, ssl_context) assert connected is True # Create not authenticated cert @@ -119,16 +117,16 @@ async def test_farmer(self, harvester_farmer): ssl_context = ssl_context_for_client( farmer_server.chia_ca_crt_path, farmer_server.chia_ca_key_path, pub_crt, pub_key ) - connected = await establish_connection(farmer_server, ssl_context) + connected = await establish_connection(farmer_server, self_hostname, ssl_context) assert connected is False ssl_context = ssl_context_for_client( farmer_server.ca_private_crt_path, farmer_server.ca_private_key_path, pub_crt, pub_key ) - connected = await establish_connection(farmer_server, ssl_context) + connected = await establish_connection(farmer_server, self_hostname, ssl_context) assert connected is False @pytest.mark.asyncio - async def test_full_node(self, wallet_node): + async def test_full_node(self, wallet_node, self_hostname): full_nodes, wallets = wallet_node full_node_api = full_nodes[0] full_node_server = full_node_api.full_node.server @@ -145,11 +143,11 @@ async def test_full_node(self, wallet_node): ssl_context = ssl_context_for_client( full_node_server.chia_ca_crt_path, full_node_server.chia_ca_key_path, pub_crt, pub_key ) - connected = await establish_connection(full_node_server, ssl_context) + connected = await establish_connection(full_node_server, self_hostname, ssl_context) assert connected is True @pytest.mark.asyncio - async def test_wallet(self, wallet_node): + async def test_wallet(self, wallet_node, self_hostname): full_nodes, wallets = wallet_node wallet_node, wallet_server = wallets[0] @@ -162,7 +160,7 @@ async def test_wallet(self, wallet_node): ssl_context = ssl_context_for_client( wallet_server.chia_ca_crt_path, wallet_server.chia_ca_key_path, pub_crt, pub_key ) - connected = await establish_connection(wallet_server, ssl_context) + connected = await establish_connection(wallet_server, self_hostname, ssl_context) assert connected is False # Not even signed by private cert @@ -177,11 +175,11 @@ async def test_wallet(self, wallet_node): ssl_context = ssl_context_for_client( wallet_server.ca_private_crt_path, wallet_server.ca_private_key_path, priv_crt, priv_key ) - connected = await establish_connection(wallet_server, ssl_context) + connected = await establish_connection(wallet_server, self_hostname, ssl_context) assert connected is False @pytest.mark.asyncio - async def test_harvester(self, harvester_farmer): + async def test_harvester(self, harvester_farmer, self_hostname): harvester, farmer_api = harvester_farmer harvester_server = harvester._server @@ -197,7 +195,7 @@ async def test_harvester(self, harvester_farmer): ssl_context = ssl_context_for_client( harvester_server.chia_ca_crt_path, harvester_server.chia_ca_key_path, pub_crt, pub_key ) - connected = await establish_connection(harvester_server, ssl_context) + connected = await establish_connection(harvester_server, self_hostname, ssl_context) assert connected is False # Not even signed by private cert @@ -212,11 +210,11 @@ async def test_harvester(self, harvester_farmer): ssl_context = ssl_context_for_client( harvester_server.ca_private_crt_path, harvester_server.ca_private_key_path, priv_crt, priv_key ) - connected = await establish_connection(harvester_server, ssl_context) + connected = await establish_connection(harvester_server, self_hostname, ssl_context) assert connected is False @pytest.mark.asyncio - async def test_introducer(self, introducer): + async def test_introducer(self, introducer, self_hostname): introducer_api, introducer_server = introducer # Create not authenticated cert @@ -231,11 +229,11 @@ async def test_introducer(self, introducer): ssl_context = ssl_context_for_client( introducer_server.chia_ca_crt_path, introducer_server.chia_ca_key_path, pub_crt, pub_key ) - connected = await establish_connection(introducer_server, ssl_context) + connected = await establish_connection(introducer_server, self_hostname, ssl_context) assert connected is True @pytest.mark.asyncio - async def test_timelord(self, timelord): + async def test_timelord(self, timelord, self_hostname): timelord_api, timelord_server = timelord # timelord should not accept incoming connections @@ -250,7 +248,7 @@ async def test_timelord(self, timelord): ssl_context = ssl_context_for_client( timelord_server.chia_ca_crt_path, timelord_server.chia_ca_key_path, pub_crt, pub_key ) - connected = await establish_connection(timelord_server, ssl_context) + connected = await establish_connection(timelord_server, self_hostname, ssl_context) assert connected is False # Not even signed by private cert @@ -265,5 +263,5 @@ async def test_timelord(self, timelord): ssl_context = ssl_context_for_client( timelord_server.ca_private_crt_path, timelord_server.ca_private_key_path, priv_crt, priv_key ) - connected = await establish_connection(timelord_server, ssl_context) + connected = await establish_connection(timelord_server, self_hostname, ssl_context) assert connected is False diff --git a/tests/core/test_cost_calculation.py b/tests/core/test_cost_calculation.py index bbd018b6d4a5..955e3a05119f 100644 --- a/tests/core/test_cost_calculation.py +++ b/tests/core/test_cost_calculation.py @@ -13,7 +13,7 @@ from chia.types.blockchain_format.program import Program, SerializedProgram from chia.types.generator_types import BlockGenerator from chia.wallet.puzzles import p2_delegated_puzzle_or_hidden_puzzle -from tests.setup_nodes import bt, test_constants +from tests.setup_nodes import test_constants from .make_block_generator import make_block_generator @@ -54,7 +54,7 @@ def large_block_generator(size): class TestCostCalculation: @pytest.mark.asyncio - async def test_basics(self, softfork_height): + async def test_basics(self, softfork_height, bt): wallet_tool = bt.get_pool_wallet_tool() ph = wallet_tool.get_new_puzzlehash() num_blocks = 3 @@ -110,7 +110,7 @@ async def test_basics(self, softfork_height): ) @pytest.mark.asyncio - async def test_mempool_mode(self, softfork_height): + async def test_mempool_mode(self, softfork_height, bt): wallet_tool = bt.get_pool_wallet_tool() ph = wallet_tool.get_new_puzzlehash() diff --git a/tests/core/test_daemon_rpc.py b/tests/core/test_daemon_rpc.py index e28814aa56ae..0fa9f7f48999 100644 --- a/tests/core/test_daemon_rpc.py +++ b/tests/core/test_daemon_rpc.py @@ -3,27 +3,22 @@ from tests.setup_nodes import setup_daemon from chia.daemon.client import connect_to_daemon -from tests.setup_nodes import bt from chia import __version__ -from tests.util.socket import find_available_listen_port -from chia.util.config import save_config class TestDaemonRpc: @pytest_asyncio.fixture(scope="function") - async def get_daemon(self): - bt._config["daemon_port"] = find_available_listen_port() - # unfortunately, the daemon's WebSocketServer loads the configs from - # disk, so the only way to configure its port is to write it to disk - save_config(bt.root_path, "config.yaml", bt._config) + async def get_daemon(self, bt): async for _ in setup_daemon(btools=bt): yield _ @pytest.mark.asyncio - async def test_get_version_rpc(self, get_daemon): + async def test_get_version_rpc(self, get_daemon, bt): + ws_server = get_daemon config = bt.config client = await connect_to_daemon(config["self_hostname"], config["daemon_port"], bt.get_daemon_ssl_context()) response = await client.get_version() assert response["data"]["success"] assert response["data"]["version"] == __version__ + ws_server.stop() diff --git a/tests/core/test_farmer_harvester_rpc.py b/tests/core/test_farmer_harvester_rpc.py index a9bb0697701d..d6eafb04c5a6 100644 --- a/tests/core/test_farmer_harvester_rpc.py +++ b/tests/core/test_farmer_harvester_rpc.py @@ -18,7 +18,7 @@ from chia.util.hash import std_hash from chia.util.ints import uint8, uint16, uint32, uint64 from chia.wallet.derive_keys import master_sk_to_wallet_sk -from tests.setup_nodes import bt, self_hostname, setup_farmer_harvester, test_constants +from tests.setup_nodes import setup_farmer_harvester, test_constants from tests.time_out_assert import time_out_assert, time_out_assert_custom_interval from tests.util.rpc import validate_get_routes from tests.util.socket import find_available_listen_port @@ -27,13 +27,13 @@ @pytest_asyncio.fixture(scope="function") -async def simulation(): - async for _ in setup_farmer_harvester(test_constants): +async def simulation(bt): + async for _ in setup_farmer_harvester(bt, test_constants): yield _ @pytest_asyncio.fixture(scope="function") -async def environment(simulation): +async def environment(bt, simulation, self_hostname): harvester_service, farmer_service = simulation def stop_node_cb(): @@ -171,7 +171,7 @@ async def have_signage_points(): @pytest.mark.asyncio -async def test_farmer_reward_target_endpoints(environment): +async def test_farmer_reward_target_endpoints(bt, environment): ( farmer_service, farmer_rpc_api, @@ -220,7 +220,7 @@ async def test_farmer_reward_target_endpoints(environment): @pytest.mark.asyncio -async def test_farmer_get_pool_state(environment): +async def test_farmer_get_pool_state(environment, self_hostname): ( farmer_service, farmer_rpc_api, diff --git a/tests/core/test_filter.py b/tests/core/test_filter.py index abac26aa7c07..40717d397433 100644 --- a/tests/core/test_filter.py +++ b/tests/core/test_filter.py @@ -5,7 +5,7 @@ import pytest_asyncio from chiabip158 import PyBIP158 -from tests.setup_nodes import setup_simulators_and_wallets, bt +from tests.setup_nodes import setup_simulators_and_wallets @pytest.fixture(scope="module") @@ -21,7 +21,7 @@ async def wallet_and_node(self): yield _ @pytest.mark.asyncio - async def test_basic_filter_test(self, wallet_and_node): + async def test_basic_filter_test(self, wallet_and_node, bt): full_nodes, wallets = wallet_and_node wallet_node, server_2 = wallets[0] wallet = wallet_node.wallet_state_manager.main_wallet diff --git a/tests/core/test_full_node_rpc.py b/tests/core/test_full_node_rpc.py index 5ae06e37f0f7..be7fe86337a5 100644 --- a/tests/core/test_full_node_rpc.py +++ b/tests/core/test_full_node_rpc.py @@ -22,7 +22,7 @@ from tests.blockchain.blockchain_test_utils import _validate_and_add_block from tests.wallet_tools import WalletTool from tests.connection_utils import connect_and_get_peer -from tests.setup_nodes import bt, self_hostname, setup_simulators_and_wallets, test_constants +from tests.setup_nodes import setup_simulators_and_wallets, test_constants from tests.time_out_assert import time_out_assert from tests.util.rpc import validate_get_routes from tests.util.socket import find_available_listen_port @@ -35,7 +35,7 @@ async def two_nodes(self): yield _ @pytest.mark.asyncio - async def test1(self, two_nodes): + async def test1(self, two_nodes, bt, self_hostname): num_blocks = 5 test_rpc_port = find_available_listen_port() nodes, _ = two_nodes @@ -231,14 +231,18 @@ async def num_connections(): await rpc_cleanup() @pytest.mark.asyncio - async def test_signage_points(self, two_nodes, empty_blockchain): + async def test_signage_points(self, two_nodes, empty_blockchain, bt): test_rpc_port = find_available_listen_port() nodes, _ = two_nodes full_node_api_1, full_node_api_2 = nodes server_1 = full_node_api_1.full_node.server server_2 = full_node_api_2.full_node.server - peer = await connect_and_get_peer(server_1, server_2) + config = bt.config + self_hostname = config["self_hostname"] + daemon_port = config["daemon_port"] + + peer = await connect_and_get_peer(server_1, server_2, self_hostname) def stop_node_cb(): full_node_api_1._close() @@ -246,13 +250,9 @@ def stop_node_cb(): full_node_rpc_api = FullNodeRpcApi(full_node_api_1.full_node) - config = bt.config - hostname = config["self_hostname"] - daemon_port = config["daemon_port"] - rpc_cleanup = await start_rpc_server( full_node_rpc_api, - hostname, + self_hostname, daemon_port, test_rpc_port, stop_node_cb, diff --git a/tests/core/test_merkle_set.py b/tests/core/test_merkle_set.py index 3a00e13d56a6..0f56fa6ce8ce 100644 --- a/tests/core/test_merkle_set.py +++ b/tests/core/test_merkle_set.py @@ -4,7 +4,6 @@ import pytest from chia.util.merkle_set import MerkleSet, confirm_included_already_hashed -from tests.setup_nodes import bt @pytest.fixture(scope="module") @@ -15,7 +14,7 @@ def event_loop(): class TestMerkleSet: @pytest.mark.asyncio - async def test_basics(self): + async def test_basics(self, bt): num_blocks = 20 blocks = bt.get_consecutive_blocks(num_blocks) diff --git a/tests/core/util/test_streamable.py b/tests/core/util/test_streamable.py index 0d029eace28b..7b945d502c67 100644 --- a/tests/core/util/test_streamable.py +++ b/tests/core/util/test_streamable.py @@ -11,7 +11,7 @@ from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.full_block import FullBlock from chia.types.weight_proof import SubEpochChallengeSegment -from chia.util.ints import uint8, uint32 +from chia.util.ints import uint8, uint32, uint64 from chia.util.streamable import ( Streamable, streamable, @@ -25,7 +25,7 @@ parse_size_hints, parse_str, ) -from tests.setup_nodes import bt, test_constants +from tests.setup_nodes import test_constants def test_basic(): @@ -65,9 +65,8 @@ class TestClass3(Streamable): a: int -def test_json(): - block = bt.create_genesis_block(test_constants, bytes([0] * 32), b"0") - +def test_json(bt): + block = bt.create_genesis_block(test_constants, bytes32([0] * 32), uint64(0)) dict_block = block.to_json_dict() assert FullBlock.from_json_dict(dict_block) == block diff --git a/tests/farmer_harvester/test_farmer_harvester.py b/tests/farmer_harvester/test_farmer_harvester.py index 645b645a1439..698d128d2799 100644 --- a/tests/farmer_harvester/test_farmer_harvester.py +++ b/tests/farmer_harvester/test_farmer_harvester.py @@ -5,7 +5,7 @@ from chia.farmer.farmer import Farmer from chia.util.keychain import generate_mnemonic -from tests.setup_nodes import bt, setup_farmer_harvester, test_constants +from tests.setup_nodes import setup_farmer_harvester, test_constants from tests.time_out_assert import time_out_assert @@ -14,13 +14,13 @@ def farmer_is_started(farmer): @pytest_asyncio.fixture(scope="function") -async def environment(): - async for _ in setup_farmer_harvester(test_constants, False): +async def environment(bt): + async for _ in setup_farmer_harvester(bt, test_constants, False): yield _ @pytest.mark.asyncio -async def test_start_with_empty_keychain(environment): +async def test_start_with_empty_keychain(environment, bt): _, farmer_service = environment farmer: Farmer = farmer_service._node # First remove all keys from the keychain @@ -41,7 +41,7 @@ async def test_start_with_empty_keychain(environment): @pytest.mark.asyncio -async def test_harvester_handshake(environment): +async def test_harvester_handshake(environment, bt): harvester_service, farmer_service = environment harvester = harvester_service._node farmer = farmer_service._node diff --git a/tests/plotting/test_plot_manager.py b/tests/plotting/test_plot_manager.py index 9e46637780b2..686c14651c15 100644 --- a/tests/plotting/test_plot_manager.py +++ b/tests/plotting/test_plot_manager.py @@ -22,7 +22,6 @@ from chia.plotting.manager import PlotManager from tests.block_tools import get_plot_dir from tests.plotting.util import get_test_plots -from tests.setup_nodes import bt from tests.time_out_assert import time_out_assert log = logging.getLogger(__name__) @@ -139,7 +138,7 @@ class TestEnvironment: @pytest.fixture(scope="function") -def test_environment(tmp_path) -> Iterator[TestEnvironment]: +def test_environment(tmp_path, bt) -> Iterator[TestEnvironment]: dir_1_count: int = 7 dir_2_count: int = 3 plots: List[Path] = get_test_plots() @@ -448,7 +447,7 @@ async def test_keys_missing(test_environment: TestEnvironment) -> None: @pytest.mark.asyncio -async def test_plot_info_caching(test_environment): +async def test_plot_info_caching(test_environment, bt): env: TestEnvironment = test_environment expected_result = PlotRefreshResult() add_plot_directory(env.root_path, str(env.dir_1.path)) diff --git a/tests/pools/test_pool_rpc.py b/tests/pools/test_pool_rpc.py index 1a573cc4b728..36b10939dfb6 100644 --- a/tests/pools/test_pool_rpc.py +++ b/tests/pools/test_pool_rpc.py @@ -25,12 +25,12 @@ from chia.util.byte_types import hexstr_to_bytes from chia.wallet.derive_keys import find_authentication_sk, find_owner_sk from chia.wallet.wallet_node import WalletNode -from tests.block_tools import get_plot_dir +from tests.block_tools import get_plot_dir, BlockTools from chia.util.config import load_config from chia.util.ints import uint16, uint32 from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.wallet_types import WalletType -from tests.setup_nodes import self_hostname, setup_simulators_and_wallets, bt +from tests.setup_nodes import setup_simulators_and_wallets from tests.time_out_assert import time_out_assert from tests.util.socket import find_available_listen_port @@ -45,21 +45,22 @@ def get_pool_plot_dir(): @dataclass class TemporaryPoolPlot: + bt: BlockTools p2_singleton_puzzle_hash: bytes32 plot_id: Optional[bytes32] = None async def __aenter__(self): - plot_id: bytes32 = await bt.new_plot(self.p2_singleton_puzzle_hash, get_pool_plot_dir()) + plot_id: bytes32 = await self.bt.new_plot(self.p2_singleton_puzzle_hash, get_pool_plot_dir()) assert plot_id is not None - await bt.refresh_plots() + await self.bt.refresh_plots() self.plot_id = plot_id return self async def __aexit__(self, exc_type, exc_value, exc_traceback): - await bt.delete_plot(self.plot_id) + await self.bt.delete_plot(self.plot_id) -async def create_pool_plot(p2_singleton_puzzle_hash: bytes32) -> Optional[bytes32]: +async def create_pool_plot(bt: BlockTools, p2_singleton_puzzle_hash: bytes32) -> Optional[bytes32]: plot_id = await bt.new_plot(p2_singleton_puzzle_hash, get_pool_plot_dir()) await bt.refresh_plots() return plot_id @@ -89,7 +90,7 @@ async def two_wallet_nodes(self): yield _ @pytest_asyncio.fixture(scope="function") - async def one_wallet_node_and_rpc(self): + async def one_wallet_node_and_rpc(self, bt, self_hostname): rmtree(get_pool_plot_dir(), ignore_errors=True) async for nodes in setup_simulators_and_wallets(1, 1, {}): full_nodes, wallets = nodes @@ -124,7 +125,7 @@ async def one_wallet_node_and_rpc(self): await rpc_cleanup() @pytest_asyncio.fixture(scope="function") - async def setup(self, two_wallet_nodes): + async def setup(self, two_wallet_nodes, bt, self_hostname): rmtree(get_pool_plot_dir(), ignore_errors=True) full_nodes, wallets = two_wallet_nodes wallet_node_0, wallet_server_0 = wallets[0] @@ -173,7 +174,7 @@ async def farm_blocks(self, full_node_api, ph: bytes32, num_blocks: int): @pytest.mark.asyncio @pytest.mark.parametrize("trusted", [True, False]) @pytest.mark.parametrize("fee", [0, FEE_AMOUNT]) - async def test_create_new_pool_wallet_self_farm(self, one_wallet_node_and_rpc, fee, trusted): + async def test_create_new_pool_wallet_self_farm(self, one_wallet_node_and_rpc, fee, trusted, self_hostname): client, wallet_node_0, full_node_api = one_wallet_node_and_rpc wallet_0 = wallet_node_0.wallet_state_manager.main_wallet if trusted: @@ -249,7 +250,7 @@ async def test_create_new_pool_wallet_self_farm(self, one_wallet_node_and_rpc, f @pytest.mark.asyncio @pytest.mark.parametrize("trusted", [True, False]) @pytest.mark.parametrize("fee", [0, FEE_AMOUNT]) - async def test_create_new_pool_wallet_farm_to_pool(self, one_wallet_node_and_rpc, fee, trusted): + async def test_create_new_pool_wallet_farm_to_pool(self, one_wallet_node_and_rpc, fee, trusted, self_hostname): client, wallet_node_0, full_node_api = one_wallet_node_and_rpc wallet_0 = wallet_node_0.wallet_state_manager.main_wallet if trusted: @@ -328,7 +329,7 @@ async def test_create_new_pool_wallet_farm_to_pool(self, one_wallet_node_and_rpc @pytest.mark.asyncio @pytest.mark.parametrize("trusted", [True, False]) @pytest.mark.parametrize("fee", [0, FEE_AMOUNT]) - async def test_create_multiple_pool_wallets(self, one_wallet_node_and_rpc, fee, trusted): + async def test_create_multiple_pool_wallets(self, one_wallet_node_and_rpc, fee, trusted, self_hostname): client, wallet_node_0, full_node_api = one_wallet_node_and_rpc if trusted: wallet_node_0.config["trusted_peers"] = { @@ -458,7 +459,7 @@ async def test_create_multiple_pool_wallets(self, one_wallet_node_and_rpc, fee, @pytest.mark.asyncio @pytest.mark.parametrize("trusted", [True, False]) @pytest.mark.parametrize("fee", [0, FEE_AMOUNT]) - async def test_absorb_self(self, one_wallet_node_and_rpc, fee, trusted): + async def test_absorb_self(self, one_wallet_node_and_rpc, fee, trusted, bt, self_hostname): client, wallet_node_0, full_node_api = one_wallet_node_and_rpc if trusted: wallet_node_0.config["trusted_peers"] = { @@ -497,7 +498,7 @@ async def test_absorb_self(self, one_wallet_node_and_rpc, fee, trusted): status: PoolWalletInfo = (await client.pw_status(2))[0] assert status.current.state == PoolSingletonState.SELF_POOLING.value - async with TemporaryPoolPlot(status.p2_singleton_puzzle_hash) as pool_plot: + async with TemporaryPoolPlot(bt, status.p2_singleton_puzzle_hash) as pool_plot: all_blocks = await full_node_api.get_all_full_blocks() blocks = bt.get_consecutive_blocks( 3, @@ -574,7 +575,7 @@ async def test_absorb_self(self, one_wallet_node_and_rpc, fee, trusted): @pytest.mark.asyncio @pytest.mark.parametrize("trusted", [True, False]) @pytest.mark.parametrize("fee", [0, FEE_AMOUNT]) - async def test_absorb_pooling(self, one_wallet_node_and_rpc, fee, trusted): + async def test_absorb_pooling(self, one_wallet_node_and_rpc, fee, trusted, bt, self_hostname): client, wallet_node_0, full_node_api = one_wallet_node_and_rpc if trusted: wallet_node_0.config["trusted_peers"] = { @@ -614,7 +615,7 @@ async def test_absorb_pooling(self, one_wallet_node_and_rpc, fee, trusted): status: PoolWalletInfo = (await client.pw_status(2))[0] assert status.current.state == PoolSingletonState.FARMING_TO_POOL.value - async with TemporaryPoolPlot(status.p2_singleton_puzzle_hash) as pool_plot: + async with TemporaryPoolPlot(bt, status.p2_singleton_puzzle_hash) as pool_plot: all_blocks = await full_node_api.get_all_full_blocks() blocks = bt.get_consecutive_blocks( 3, @@ -722,7 +723,7 @@ async def test_absorb_pooling(self, one_wallet_node_and_rpc, fee, trusted): @pytest.mark.asyncio @pytest.mark.parametrize("trusted", [True]) @pytest.mark.parametrize("fee", [0]) - async def test_self_pooling_to_pooling(self, setup, fee, trusted): + async def test_self_pooling_to_pooling(self, setup, fee, trusted, self_hostname): """This tests self-pooling -> pooling""" num_blocks = 4 # Num blocks to farm at a time total_blocks = 0 # Total blocks farmed so far @@ -859,7 +860,7 @@ async def status_is_farming_to_pool(w_id: int): "fee", [0, FEE_AMOUNT], ) - async def test_leave_pool(self, setup, fee, trusted): + async def test_leave_pool(self, setup, fee, trusted, self_hostname): """This tests self-pooling -> pooling -> escaping -> self pooling""" full_nodes, wallet_nodes, receive_address, client, rpc_cleanup = setup our_ph = receive_address[0] @@ -981,7 +982,7 @@ async def status_is_self_pooling(): @pytest.mark.asyncio @pytest.mark.parametrize("trusted", [True, False]) @pytest.mark.parametrize("fee", [0, FEE_AMOUNT]) - async def test_change_pools(self, setup, fee, trusted): + async def test_change_pools(self, setup, fee, trusted, self_hostname): """This tests Pool A -> escaping -> Pool B""" full_nodes, wallet_nodes, receive_address, client, rpc_cleanup = setup our_ph = receive_address[0] @@ -1085,7 +1086,7 @@ async def status_is_leaving(): @pytest.mark.asyncio @pytest.mark.parametrize("trusted", [True, False]) @pytest.mark.parametrize("fee", [0, FEE_AMOUNT]) - async def test_change_pools_reorg(self, setup, fee, trusted): + async def test_change_pools_reorg(self, setup, fee, trusted, bt, self_hostname): """This tests Pool A -> escaping -> reorg -> escaping -> Pool B""" full_nodes, wallet_nodes, receive_address, client, rpc_cleanup = setup our_ph = receive_address[0] diff --git a/tests/setup_nodes.py b/tests/setup_nodes.py index 3c65c834387a..6fc0233043a0 100644 --- a/tests/setup_nodes.py +++ b/tests/setup_nodes.py @@ -1,10 +1,10 @@ +import logging import asyncio -import atexit import signal import sqlite3 from secrets import token_bytes -from typing import Dict, List, Optional +from typing import Dict, List, Optional, AsyncGenerator from chia.consensus.constants import ConsensusConstants from chia.daemon.server import WebSocketServer, create_server_for_daemon, daemon_launch_lock_path, singleton @@ -20,7 +20,7 @@ from chia.timelord.timelord_launcher import kill_processes, spawn_process from chia.types.peer_info import PeerInfo from chia.util.bech32m import encode_puzzle_hash -from tests.block_tools import create_block_tools, create_block_tools_async, test_constants +from tests.block_tools import create_block_tools_async, test_constants, BlockTools from tests.util.keyring import TempKeyring from tests.util.socket import find_available_listen_port from chia.util.hash import std_hash @@ -33,14 +33,7 @@ def cleanup_keyring(keyring: TempKeyring): keyring.cleanup() -temp_keyring = TempKeyring() -keychain = temp_keyring.get_keychain() -atexit.register(cleanup_keyring, temp_keyring) # Attempt to cleanup the temp keychain -bt = create_block_tools(constants=test_constants, keychain=keychain) - -# if you have a system that has an unusual hostname for localhost and you want -# to run the tests, change this constant -self_hostname = "localhost" +log = logging.getLogger(__name__) def constants_for_dic(dic): @@ -56,9 +49,10 @@ async def _teardown_nodes(node_aiters: List) -> None: pass -async def setup_daemon(btools): +async def setup_daemon(btools: BlockTools) -> AsyncGenerator[WebSocketServer, None]: root_path = btools.root_path config = btools.config + assert "daemon_port" in config lockfile = singleton(daemon_launch_lock_path(root_path)) crt_path = root_path / config["daemon_ssl"]["private_crt"] key_path = root_path / config["daemon_ssl"]["private_key"] @@ -77,9 +71,10 @@ async def setup_daemon(btools): async def setup_full_node( consensus_constants: ConsensusConstants, db_name, + self_hostname: str, port, rpc_port, - local_bt, + local_bt: BlockTools, introducer_port=None, simulator=False, send_uncompact_interval=0, @@ -97,7 +92,10 @@ async def setup_full_node( connection.execute("INSERT INTO database_version VALUES (?)", (db_version,)) connection.commit() + if connect_to_daemon: + assert local_bt.config["daemon_port"] is not None config = local_bt.config["full_node"] + config["database_path"] = db_name config["send_uncompact_interval"] = send_uncompact_interval config["target_uncompact_proofs"] = 30 @@ -136,19 +134,22 @@ async def setup_full_node( db_path.unlink() +# Note: convert these setup functions to fixtures, or push it one layer up, +# keeping these usable independently? async def setup_wallet_node( + self_hostname: str, port, rpc_port, consensus_constants: ConsensusConstants, - local_bt, + local_bt: BlockTools, full_node_port=None, introducer_port=None, key_seed=None, starting_height=None, initial_num_public_keys=5, ): - with TempKeyring() as keychain: - config = bt.config["wallet"] + with TempKeyring(populate=True) as keychain: + config = local_bt.config["wallet"] config["port"] = port config["rpc_port"] = rpc_port if starting_height is not None: @@ -164,7 +165,7 @@ async def setup_wallet_node( db_path_key_suffix = str(first_pk.get_fingerprint()) db_name = f"test-wallet-db-{port}-KEY.sqlite" db_path_replaced: str = db_name.replace("KEY", db_path_key_suffix) - db_path = bt.root_path / db_path_replaced + db_path = local_bt.root_path / db_path_replaced if db_path.exists(): db_path.unlink() @@ -206,10 +207,16 @@ async def setup_wallet_node( async def setup_harvester( - port, rpc_port, farmer_port, consensus_constants: ConsensusConstants, b_tools, start_service: bool = True + b_tools: BlockTools, + self_hostname: str, + port, + rpc_port, + farmer_port, + consensus_constants: ConsensusConstants, + start_service: bool = True, ): - config = bt.config["harvester"] + config = b_tools.config["harvester"] config["port"] = port config["rpc_port"] = rpc_port kwargs = service_kwargs_for_harvester(b_tools.root_path, config, consensus_constants) @@ -234,15 +241,16 @@ async def setup_harvester( async def setup_farmer( + b_tools: BlockTools, + self_hostname: str, port, rpc_port, consensus_constants: ConsensusConstants, - b_tools, full_node_port: Optional[uint16] = None, start_service: bool = True, ): - config = bt.config["farmer"] - config_pool = bt.config["pool"] + config = b_tools.config["farmer"] + config_pool = b_tools.config["pool"] config["xch_target_address"] = encode_puzzle_hash(b_tools.farmer_ph, "xch") config["pool_public_keys"] = [bytes(pk).hex() for pk in b_tools.pool_pubkeys] @@ -276,7 +284,7 @@ async def setup_farmer( await service.wait_closed() -async def setup_introducer(port): +async def setup_introducer(bt: BlockTools, port): kwargs = service_kwargs_for_introducer( bt.root_path, bt.config["introducer"], @@ -298,7 +306,7 @@ async def setup_introducer(port): await service.wait_closed() -async def setup_vdf_client(port): +async def setup_vdf_client(bt: BlockTools, self_hostname: str, port): vdf_task_1 = asyncio.create_task(spawn_process(self_hostname, port, 1, bt.config.get("prefer_ipv6"))) def stop(): @@ -311,7 +319,7 @@ def stop(): await kill_processes() -async def setup_vdf_clients(port): +async def setup_vdf_clients(bt: BlockTools, self_hostname: str, port): vdf_task_1 = asyncio.create_task(spawn_process(self_hostname, port, 1, bt.config.get("prefer_ipv6"))) vdf_task_2 = asyncio.create_task(spawn_process(self_hostname, port, 2, bt.config.get("prefer_ipv6"))) vdf_task_3 = asyncio.create_task(spawn_process(self_hostname, port, 3, bt.config.get("prefer_ipv6"))) @@ -328,7 +336,7 @@ def stop(): async def setup_timelord( - port, full_node_port, rpc_port, vdf_port, sanitizer, consensus_constants: ConsensusConstants, b_tools + port, full_node_port, rpc_port, vdf_port, sanitizer, consensus_constants: ConsensusConstants, b_tools: BlockTools ): config = b_tools.config["timelord"] config["port"] = port @@ -356,16 +364,17 @@ async def setup_timelord( await service.wait_closed() -async def setup_two_nodes(consensus_constants: ConsensusConstants, db_version: int): +async def setup_two_nodes(consensus_constants: ConsensusConstants, db_version: int, self_hostname: str): """ Setup and teardown of two full nodes, with blockchains and separate DBs. """ - with TempKeyring() as keychain1, TempKeyring() as keychain2: + with TempKeyring(populate=True) as keychain1, TempKeyring(populate=True) as keychain2: node_iters = [ setup_full_node( consensus_constants, "blockchain_test.db", + self_hostname, find_available_listen_port("node1"), find_available_listen_port("node1 rpc"), await create_block_tools_async(constants=test_constants, keychain=keychain1), @@ -375,6 +384,7 @@ async def setup_two_nodes(consensus_constants: ConsensusConstants, db_version: i setup_full_node( consensus_constants, "blockchain_test_2.db", + self_hostname, find_available_listen_port("node2"), find_available_listen_port("node2 rpc"), await create_block_tools_async(constants=test_constants, keychain=keychain2), @@ -391,19 +401,20 @@ async def setup_two_nodes(consensus_constants: ConsensusConstants, db_version: i await _teardown_nodes(node_iters) -async def setup_n_nodes(consensus_constants: ConsensusConstants, n: int, db_version: int): +async def setup_n_nodes(consensus_constants: ConsensusConstants, n: int, db_version: int, self_hostname: str): """ Setup and teardown of n full nodes, with blockchains and separate DBs. """ node_iters = [] keyrings_to_cleanup = [] for i in range(n): - keyring = TempKeyring() + keyring = TempKeyring(populate=True) keyrings_to_cleanup.append(keyring) node_iters.append( setup_full_node( consensus_constants, f"blockchain_test_{i}.db", + self_hostname, find_available_listen_port(f"node{i}"), find_available_listen_port(f"node{i} rpc"), await create_block_tools_async(constants=test_constants, keychain=keyring.get_keychain()), @@ -424,14 +435,15 @@ async def setup_n_nodes(consensus_constants: ConsensusConstants, n: int, db_vers async def setup_node_and_wallet( - consensus_constants: ConsensusConstants, starting_height=None, key_seed=None, db_version=1 + consensus_constants: ConsensusConstants, self_hostname: str, starting_height=None, key_seed=None, db_version=1 ): - with TempKeyring() as keychain: + with TempKeyring(populate=True) as keychain: btools = await create_block_tools_async(constants=test_constants, keychain=keychain) node_iters = [ setup_full_node( consensus_constants, "blockchain_test.db", + self_hostname, find_available_listen_port("node1"), find_available_listen_port("node1 rpc"), btools, @@ -439,6 +451,7 @@ async def setup_node_and_wallet( db_version=db_version, ), setup_wallet_node( + btools.config["self_hostname"], find_available_listen_port("node2"), find_available_listen_port("node2 rpc"), consensus_constants, @@ -466,7 +479,7 @@ async def setup_simulators_and_wallets( initial_num_public_keys=5, db_version=1, ): - with TempKeyring() as keychain1, TempKeyring() as keychain2: + with TempKeyring(populate=True) as keychain1, TempKeyring(populate=True) as keychain2: simulators: List[FullNodeAPI] = [] wallets = [] node_iters = [] @@ -481,6 +494,7 @@ async def setup_simulators_and_wallets( ) # block tools modifies constants sim = setup_full_node( bt_tools.constants, + bt_tools.config["self_hostname"], db_name, port, rpc_port, @@ -502,6 +516,7 @@ async def setup_simulators_and_wallets( consensus_constants, const_dict=dic, keychain=keychain2 ) # block tools modifies constants wlt = setup_wallet_node( + bt_tools.config["self_hostname"], port, rpc_port, bt_tools.constants, @@ -519,14 +534,29 @@ async def setup_simulators_and_wallets( await _teardown_nodes(node_iters) -async def setup_farmer_harvester(consensus_constants: ConsensusConstants, start_services: bool = True): +async def setup_farmer_harvester(bt: BlockTools, consensus_constants: ConsensusConstants, start_services: bool = True): farmer_port = find_available_listen_port("farmer") farmer_rpc_port = find_available_listen_port("farmer rpc") harvester_port = find_available_listen_port("harvester") harvester_rpc_port = find_available_listen_port("harvester rpc") node_iters = [ - setup_harvester(harvester_port, harvester_rpc_port, farmer_port, consensus_constants, bt, start_services), - setup_farmer(farmer_port, farmer_rpc_port, consensus_constants, bt, start_service=start_services), + setup_harvester( + bt, + bt.config["self_hostname"], + harvester_port, + harvester_rpc_port, + farmer_port, + consensus_constants, + start_services, + ), + setup_farmer( + bt, + bt.config["self_hostname"], + farmer_port, + farmer_rpc_port, + consensus_constants, + start_service=start_services, + ), ] harvester_service = await node_iters[0].__anext__() @@ -538,13 +568,19 @@ async def setup_farmer_harvester(consensus_constants: ConsensusConstants, start_ async def setup_full_system( - consensus_constants: ConsensusConstants, b_tools=None, b_tools_1=None, connect_to_daemon=False, db_version=1 + consensus_constants: ConsensusConstants, + shared_b_tools: BlockTools, + b_tools: BlockTools = None, + b_tools_1: BlockTools = None, + db_version=1, + connect_to_daemon=False, ): - with TempKeyring() as keychain1, TempKeyring() as keychain2: + with TempKeyring(populate=True) as keychain1, TempKeyring(populate=True) as keychain2: if b_tools is None: b_tools = await create_block_tools_async(constants=test_constants, keychain=keychain1) if b_tools_1 is None: b_tools_1 = await create_block_tools_async(constants=test_constants, keychain=keychain2) + introducer_port = find_available_listen_port("introducer") farmer_port = find_available_listen_port("farmer") farmer_rpc_port = find_available_listen_port("farmer rpc") @@ -562,16 +598,31 @@ async def setup_full_system( harvester_rpc_port = find_available_listen_port("harvester rpc") node_iters = [ - setup_introducer(introducer_port), - setup_harvester(harvester_port, harvester_rpc_port, farmer_port, consensus_constants, b_tools), - setup_farmer(farmer_port, farmer_rpc_port, consensus_constants, b_tools, uint16(node1_port)), - setup_vdf_clients(vdf1_port), + setup_introducer(shared_b_tools, introducer_port), + setup_harvester( + shared_b_tools, + shared_b_tools.config["self_hostname"], + harvester_port, + harvester_rpc_port, + farmer_port, + consensus_constants, + ), + setup_farmer( + shared_b_tools, + shared_b_tools.config["self_hostname"], + farmer_port, + farmer_rpc_port, + consensus_constants, + uint16(node1_port), + ), + setup_vdf_clients(shared_b_tools, shared_b_tools.config["self_hostname"], vdf1_port), setup_timelord( timelord2_port, node1_port, timelord2_rpc_port, vdf1_port, False, consensus_constants, b_tools ), setup_full_node( consensus_constants, "blockchain_test.db", + shared_b_tools.config["self_hostname"], node1_port, rpc1_port, b_tools, @@ -579,28 +630,29 @@ async def setup_full_system( False, 10, True, - connect_to_daemon, + connect_to_daemon=connect_to_daemon, db_version=db_version, ), setup_full_node( consensus_constants, "blockchain_test_2.db", + shared_b_tools.config["self_hostname"], node2_port, rpc2_port, b_tools_1, - introducer_port, - False, - 10, - True, - False, # connect_to_daemon, + introducer_port=introducer_port, + simulator=False, + send_uncompact_interval=10, + sanitize_weight_proof_only=True, db_version=db_version, ), - setup_vdf_client(vdf2_port), + setup_vdf_client(shared_b_tools, shared_b_tools.config["self_hostname"], vdf2_port), setup_timelord(timelord1_port, 1000, timelord1_rpc_port, vdf2_port, True, consensus_constants, b_tools_1), ] if connect_to_daemon: node_iters.append(setup_daemon(btools=b_tools)) + daemon_ws = await node_iters[9].__anext__() introducer, introducer_server = await node_iters[0].__anext__() harvester_service = await node_iters[1].__anext__() @@ -636,9 +688,12 @@ async def num_connections(): ) if connect_to_daemon: - daemon1 = await node_iters[9].__anext__() - yield ret + (daemon1,) + yield ret + (daemon_ws,) else: yield ret - await _teardown_nodes(node_iters) + if connect_to_daemon: + await _teardown_nodes(node_iters[:-1]) + await _teardown_nodes([node_iters[-1]]) + else: + await _teardown_nodes(node_iters) diff --git a/tests/simulation/test_simulation.py b/tests/simulation/test_simulation.py index 31d877db398f..6fe1e88b51b1 100644 --- a/tests/simulation/test_simulation.py +++ b/tests/simulation/test_simulation.py @@ -5,7 +5,7 @@ from tests.block_tools import create_block_tools_async from chia.util.ints import uint16 from tests.core.node_height import node_height_at_least -from tests.setup_nodes import self_hostname, setup_full_node, setup_full_system, test_constants +from tests.setup_nodes import setup_full_node, setup_full_system, test_constants from tests.time_out_assert import time_out_assert from tests.util.keyring import TempKeyring from tests.util.socket import find_available_listen_port @@ -33,12 +33,13 @@ class TestSimulation: # because of a hack in shutting down the full node, which means you cannot run # more than one simulations per process. @pytest_asyncio.fixture(scope="function") - async def extra_node(self): + async def extra_node(self, self_hostname): with TempKeyring() as keychain: b_tools = await create_block_tools_async(constants=test_constants_modified, keychain=keychain) async for _ in setup_full_node( test_constants_modified, "blockchain_test_3.db", + self_hostname, find_available_listen_port(), find_available_listen_port(), b_tools, @@ -47,12 +48,12 @@ async def extra_node(self): yield _ @pytest_asyncio.fixture(scope="function") - async def simulation(self): - async for _ in setup_full_system(test_constants_modified, db_version=1): + async def simulation(self, bt): + async for _ in setup_full_system(test_constants_modified, bt, db_version=1): yield _ @pytest.mark.asyncio - async def test_simulation_1(self, simulation, extra_node): + async def test_simulation_1(self, simulation, extra_node, self_hostname): node1, node2, _, _, _, _, _, _, _, sanitizer_server, server1 = simulation node1_port = node1.full_node.config["port"] diff --git a/tests/util/blockchain.py b/tests/util/blockchain.py index f18fcf8c8131..0b75f123f8d4 100644 --- a/tests/util/blockchain.py +++ b/tests/util/blockchain.py @@ -14,8 +14,7 @@ from chia.types.full_block import FullBlock from chia.util.db_wrapper import DBWrapper from chia.util.path import mkdir - -from tests.setup_nodes import bt +from tests.block_tools import BlockTools async def create_blockchain(constants: ConsensusConstants, db_version: int): @@ -36,6 +35,7 @@ async def create_blockchain(constants: ConsensusConstants, db_version: int): def persistent_blocks( num_of_blocks: int, db_name: str, + bt: BlockTools, seed: bytes = b"", empty_sub_slots=0, normalized_to_identity_cc_eos: bool = False, @@ -69,6 +69,7 @@ def persistent_blocks( num_of_blocks, seed, empty_sub_slots, + bt, normalized_to_identity_cc_eos, normalized_to_identity_icc_eos, normalized_to_identity_cc_sp, @@ -81,6 +82,7 @@ def new_test_db( num_of_blocks: int, seed: bytes, empty_sub_slots: int, + bt: BlockTools, normalized_to_identity_cc_eos: bool = False, # CC_EOS, normalized_to_identity_icc_eos: bool = False, # ICC_EOS normalized_to_identity_cc_sp: bool = False, # CC_SP, diff --git a/tests/wallet/cat_wallet/test_cat_wallet.py b/tests/wallet/cat_wallet/test_cat_wallet.py index 26c96ebdf51d..99c5acbfd6f4 100644 --- a/tests/wallet/cat_wallet/test_cat_wallet.py +++ b/tests/wallet/cat_wallet/test_cat_wallet.py @@ -19,7 +19,7 @@ from chia.wallet.transaction_record import TransactionRecord from chia.wallet.wallet_info import WalletInfo from tests.pools.test_pool_rpc import wallet_is_synced -from tests.setup_nodes import self_hostname, setup_simulators_and_wallets +from tests.setup_nodes import setup_simulators_and_wallets from tests.time_out_assert import time_out_assert @@ -51,7 +51,7 @@ async def three_wallet_nodes(self): [True, False], ) @pytest.mark.asyncio - async def test_cat_creation(self, two_wallet_nodes, trusted): + async def test_cat_creation(self, self_hostname, two_wallet_nodes, trusted): num_blocks = 3 full_nodes, wallets = two_wallet_nodes full_node_api = full_nodes[0] @@ -120,7 +120,7 @@ async def test_cat_creation(self, two_wallet_nodes, trusted): [True, False], ) @pytest.mark.asyncio - async def test_cat_spend(self, two_wallet_nodes, trusted): + async def test_cat_spend(self, self_hostname, two_wallet_nodes, trusted): num_blocks = 3 full_nodes, wallets = two_wallet_nodes full_node_api = full_nodes[0] @@ -221,7 +221,7 @@ async def test_cat_spend(self, two_wallet_nodes, trusted): [True, False], ) @pytest.mark.asyncio - async def test_get_wallet_for_asset_id(self, two_wallet_nodes, trusted): + async def test_get_wallet_for_asset_id(self, self_hostname, two_wallet_nodes, trusted): num_blocks = 3 full_nodes, wallets = two_wallet_nodes full_node_api = full_nodes[0] @@ -274,7 +274,7 @@ async def test_get_wallet_for_asset_id(self, two_wallet_nodes, trusted): [True, False], ) @pytest.mark.asyncio - async def test_cat_doesnt_see_eve(self, two_wallet_nodes, trusted): + async def test_cat_doesnt_see_eve(self, self_hostname, two_wallet_nodes, trusted): num_blocks = 3 full_nodes, wallets = two_wallet_nodes full_node_api = full_nodes[0] @@ -377,7 +377,7 @@ async def query_and_assert_transactions(wsm, id): [True, False], ) @pytest.mark.asyncio - async def test_cat_spend_multiple(self, three_wallet_nodes, trusted): + async def test_cat_spend_multiple(self, self_hostname, three_wallet_nodes, trusted): num_blocks = 3 full_nodes, wallets = three_wallet_nodes full_node_api = full_nodes[0] @@ -518,7 +518,7 @@ async def test_cat_spend_multiple(self, three_wallet_nodes, trusted): [True, False], ) @pytest.mark.asyncio - async def test_cat_max_amount_send(self, two_wallet_nodes, trusted): + async def test_cat_max_amount_send(self, self_hostname, two_wallet_nodes, trusted): num_blocks = 3 full_nodes, wallets = two_wallet_nodes full_node_api = full_nodes[0] @@ -650,7 +650,7 @@ async def check_all_there(): [True, False], ) @pytest.mark.asyncio - async def test_cat_hint(self, two_wallet_nodes, trusted, autodiscovery): + async def test_cat_hint(self, self_hostname, two_wallet_nodes, trusted, autodiscovery): num_blocks = 3 full_nodes, wallets = two_wallet_nodes full_node_api = full_nodes[0] diff --git a/tests/wallet/cat_wallet/test_offer_lifecycle.py b/tests/wallet/cat_wallet/test_offer_lifecycle.py index e63ce73754dd..b4d7542a00ea 100644 --- a/tests/wallet/cat_wallet/test_offer_lifecycle.py +++ b/tests/wallet/cat_wallet/test_offer_lifecycle.py @@ -86,7 +86,7 @@ async def generate_coins( else: payments.append(Payment(acs_ph, amount, [])) - # This bundle create all of the initial coins + # This bundle creates all of the initial coins parent_bundle = SpendBundle( [ CoinSpend( diff --git a/tests/wallet/cat_wallet/test_trades.py b/tests/wallet/cat_wallet/test_trades.py index 817a311f3e2a..0c348fe707d7 100644 --- a/tests/wallet/cat_wallet/test_trades.py +++ b/tests/wallet/cat_wallet/test_trades.py @@ -15,7 +15,7 @@ from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.transaction_type import TransactionType from tests.pools.test_pool_rpc import wallet_is_synced -from tests.setup_nodes import self_hostname, setup_simulators_and_wallets +from tests.setup_nodes import setup_simulators_and_wallets from tests.time_out_assert import time_out_assert @@ -42,7 +42,7 @@ async def two_wallet_nodes(): @pytest_asyncio.fixture(scope="function") -async def wallets_prefarm(two_wallet_nodes, trusted): +async def wallets_prefarm(two_wallet_nodes, self_hostname, trusted): """ Sets up the node with 10 blocks, and returns a payer and payee wallet. """ diff --git a/tests/wallet/did_wallet/test_did.py b/tests/wallet/did_wallet/test_did.py index f717ce924036..dc8f5f1a6d95 100644 --- a/tests/wallet/did_wallet/test_did.py +++ b/tests/wallet/did_wallet/test_did.py @@ -4,7 +4,7 @@ from chia.simulator.simulator_protocol import FarmNewBlockProtocol from chia.types.peer_info import PeerInfo from chia.util.ints import uint16, uint32, uint64 -from tests.setup_nodes import self_hostname, setup_simulators_and_wallets +from tests.setup_nodes import setup_simulators_and_wallets from chia.wallet.did_wallet.did_wallet import DIDWallet from chia.types.blockchain_format.program import Program from blspy import AugSchemeMPL @@ -48,7 +48,7 @@ async def three_sim_two_wallets(self): yield _ @pytest.mark.asyncio - async def test_creation_from_backup_file(self, three_wallet_nodes): + async def test_creation_from_backup_file(self, self_hostname, three_wallet_nodes): num_blocks = 5 full_nodes, wallets = three_wallet_nodes full_node_api = full_nodes[0] @@ -173,7 +173,7 @@ async def get_coins_with_ph(): await time_out_assert(45, did_wallet_2.get_unconfirmed_balance, 0) @pytest.mark.asyncio - async def test_did_recovery_with_multiple_backup_dids(self, two_wallet_nodes): + async def test_did_recovery_with_multiple_backup_dids(self, self_hostname, two_wallet_nodes): num_blocks = 5 full_nodes, wallets = two_wallet_nodes full_node_1 = full_nodes[0] @@ -281,7 +281,7 @@ async def test_did_recovery_with_multiple_backup_dids(self, two_wallet_nodes): await time_out_assert(15, did_wallet_3.get_unconfirmed_balance, 0) @pytest.mark.asyncio - async def test_did_recovery_with_empty_set(self, two_wallet_nodes): + async def test_did_recovery_with_empty_set(self, self_hostname, two_wallet_nodes): num_blocks = 5 full_nodes, wallets = two_wallet_nodes full_node_1 = full_nodes[0] @@ -328,7 +328,7 @@ async def test_did_recovery_with_empty_set(self, two_wallet_nodes): assert additions == [] @pytest.mark.asyncio - async def test_did_attest_after_recovery(self, two_wallet_nodes): + async def test_did_attest_after_recovery(self, self_hostname, two_wallet_nodes): num_blocks = 5 full_nodes, wallets = two_wallet_nodes full_node_1 = full_nodes[0] diff --git a/tests/wallet/did_wallet/test_did_rpc.py b/tests/wallet/did_wallet/test_did_rpc.py index af5473fb01d3..eba47e2d7ff0 100644 --- a/tests/wallet/did_wallet/test_did_rpc.py +++ b/tests/wallet/did_wallet/test_did_rpc.py @@ -10,7 +10,7 @@ from chia.types.peer_info import PeerInfo from chia.util.ints import uint16, uint64 from chia.wallet.util.wallet_types import WalletType -from tests.setup_nodes import self_hostname, setup_simulators_and_wallets, bt +from tests.setup_nodes import setup_simulators_and_wallets from tests.time_out_assert import time_out_assert from chia.wallet.did_wallet.did_wallet import DIDWallet from tests.util.socket import find_available_listen_port @@ -34,7 +34,7 @@ async def three_wallet_nodes(self): yield _ @pytest.mark.asyncio - async def test_create_did(self, three_wallet_nodes): + async def test_create_did(self, bt, three_wallet_nodes, self_hostname): num_blocks = 4 full_nodes, wallets = three_wallet_nodes full_node_api = full_nodes[0] diff --git a/tests/wallet/rl_wallet/test_rl_rpc.py b/tests/wallet/rl_wallet/test_rl_rpc.py index d3aeceed7ed4..d0df6826ea65 100644 --- a/tests/wallet/rl_wallet/test_rl_rpc.py +++ b/tests/wallet/rl_wallet/test_rl_rpc.py @@ -13,7 +13,7 @@ from chia.util.ints import uint16 from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.wallet_types import WalletType -from tests.setup_nodes import self_hostname, setup_simulators_and_wallets +from tests.setup_nodes import setup_simulators_and_wallets from tests.time_out_assert import time_out_assert from tests.wallet.sync.test_wallet_sync import wallet_height_at_least @@ -60,7 +60,7 @@ async def three_wallet_nodes(self): @pytest.mark.asyncio @pytest.mark.skip - async def test_create_rl_coin(self, three_wallet_nodes): + async def test_create_rl_coin(self, three_wallet_nodes, self_hostname): num_blocks = 4 full_nodes, wallets = three_wallet_nodes full_node_api = full_nodes[0] diff --git a/tests/wallet/rl_wallet/test_rl_wallet.py b/tests/wallet/rl_wallet/test_rl_wallet.py index e4dce95ce286..bddc9d1bf590 100644 --- a/tests/wallet/rl_wallet/test_rl_wallet.py +++ b/tests/wallet/rl_wallet/test_rl_wallet.py @@ -7,7 +7,7 @@ from chia.types.peer_info import PeerInfo from chia.util.ints import uint16, uint64 from chia.wallet.rl_wallet.rl_wallet import RLWallet -from tests.setup_nodes import self_hostname, setup_simulators_and_wallets +from tests.setup_nodes import setup_simulators_and_wallets from tests.time_out_assert import time_out_assert @@ -25,7 +25,7 @@ async def two_wallet_nodes(self): @pytest.mark.asyncio @pytest.mark.skip - async def test_create_rl_coin(self, two_wallet_nodes): + async def test_create_rl_coin(self, two_wallet_nodes, self_hostname): num_blocks = 4 full_nodes, wallets = two_wallet_nodes full_node_api = full_nodes[0] diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index 90b0866c0f53..735cc556655e 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -35,7 +35,7 @@ from chia.wallet.transaction_sorting import SortKey from chia.wallet.util.compute_memos import compute_memos from tests.pools.test_pool_rpc import wallet_is_synced -from tests.setup_nodes import bt, setup_simulators_and_wallets, self_hostname +from tests.setup_nodes import setup_simulators_and_wallets from tests.time_out_assert import time_out_assert from tests.util.socket import find_available_listen_port @@ -53,7 +53,7 @@ async def two_wallet_nodes(self): [True, False], ) @pytest.mark.asyncio - async def test_wallet_rpc(self, two_wallet_nodes, trusted): + async def test_wallet_rpc(self, two_wallet_nodes, trusted, bt, self_hostname): test_rpc_port = find_available_listen_port() test_rpc_port_2 = find_available_listen_port() test_rpc_port_node = find_available_listen_port() @@ -137,9 +137,9 @@ def stop_node_cb(): await time_out_assert(5, wallet.get_confirmed_balance, initial_funds) await time_out_assert(5, wallet.get_unconfirmed_balance, initial_funds) - client = await WalletRpcClient.create(self_hostname, test_rpc_port, bt.root_path, config) - client_2 = await WalletRpcClient.create(self_hostname, test_rpc_port_2, bt.root_path, config) - client_node = await FullNodeRpcClient.create(self_hostname, test_rpc_port_node, bt.root_path, config) + client = await WalletRpcClient.create(hostname, test_rpc_port, bt.root_path, config) + client_2 = await WalletRpcClient.create(hostname, test_rpc_port_2, bt.root_path, config) + client_node = await FullNodeRpcClient.create(hostname, test_rpc_port_node, bt.root_path, config) try: await time_out_assert(5, client.get_synced) addr = encode_puzzle_hash(await wallet_node_2.wallet_state_manager.main_wallet.get_new_puzzlehash(), "xch") diff --git a/tests/wallet/simple_sync/test_simple_sync_protocol.py b/tests/wallet/simple_sync/test_simple_sync_protocol.py index c71c92ef592a..0ab1b2de6f18 100644 --- a/tests/wallet/simple_sync/test_simple_sync_protocol.py +++ b/tests/wallet/simple_sync/test_simple_sync_protocol.py @@ -25,7 +25,7 @@ from chia.wallet.wallet_state_manager import WalletStateManager from tests.connection_utils import add_dummy_connection from tests.pools.test_pool_rpc import wallet_is_synced -from tests.setup_nodes import self_hostname, setup_simulators_and_wallets, bt +from tests.setup_nodes import setup_simulators_and_wallets from tests.time_out_assert import time_out_assert from tests.wallet.cat_wallet.test_cat_wallet import tx_in_pool from tests.wallet_tools import WalletTool @@ -67,7 +67,7 @@ async def get_all_messages_in_queue(self, queue): return all_messages @pytest.mark.asyncio - async def test_subscribe_for_ph(self, wallet_node_simulator): + async def test_subscribe_for_ph(self, wallet_node_simulator, self_hostname): num_blocks = 4 full_nodes, wallets = wallet_node_simulator full_node_api = full_nodes[0] @@ -76,7 +76,7 @@ async def test_subscribe_for_ph(self, wallet_node_simulator): wsm: WalletStateManager = wallet_node.wallet_state_manager await server_2.start_client(PeerInfo(self_hostname, uint16(fn_server._port)), None) - incoming_queue, peer_id = await add_dummy_connection(fn_server, 12312, NodeType.WALLET) + incoming_queue, peer_id = await add_dummy_connection(fn_server, self_hostname, 12312, NodeType.WALLET) zero_ph = 32 * b"\0" junk_ph = 32 * b"\a" @@ -255,7 +255,7 @@ async def test_subscribe_for_ph(self, wallet_node_simulator): assert notified_state.spent_height is not None @pytest.mark.asyncio - async def test_subscribe_for_coin_id(self, wallet_node_simulator): + async def test_subscribe_for_coin_id(self, wallet_node_simulator, self_hostname): num_blocks = 4 full_nodes, wallets = wallet_node_simulator full_node_api = full_nodes[0] @@ -266,7 +266,7 @@ async def test_subscribe_for_coin_id(self, wallet_node_simulator): puzzle_hash = await standard_wallet.get_new_puzzlehash() await server_2.start_client(PeerInfo(self_hostname, uint16(fn_server._port)), None) - incoming_queue, peer_id = await add_dummy_connection(fn_server, 12312, NodeType.WALLET) + incoming_queue, peer_id = await add_dummy_connection(fn_server, self_hostname, 12312, NodeType.WALLET) fake_wallet_peer = fn_server.all_connections[peer_id] @@ -362,7 +362,7 @@ async def test_subscribe_for_coin_id(self, wallet_node_simulator): assert notified_state.spent_height is None @pytest.mark.asyncio - async def test_subscribe_for_ph_reorg(self, wallet_node_simulator): + async def test_subscribe_for_ph_reorg(self, wallet_node_simulator, self_hostname): num_blocks = 4 long_blocks = 20 full_nodes, wallets = wallet_node_simulator @@ -374,7 +374,7 @@ async def test_subscribe_for_ph_reorg(self, wallet_node_simulator): puzzle_hash = await standard_wallet.get_new_puzzlehash() await server_2.start_client(PeerInfo(self_hostname, uint16(fn_server._port)), None) - incoming_queue, peer_id = await add_dummy_connection(fn_server, 12312, NodeType.WALLET) + incoming_queue, peer_id = await add_dummy_connection(fn_server, self_hostname, 12312, NodeType.WALLET) fake_wallet_peer = fn_server.all_connections[peer_id] zero_ph = 32 * b"\0" @@ -437,7 +437,7 @@ async def test_subscribe_for_ph_reorg(self, wallet_node_simulator): assert second_state_coin_2.created_height is None @pytest.mark.asyncio - async def test_subscribe_for_coin_id_reorg(self, wallet_node_simulator): + async def test_subscribe_for_coin_id_reorg(self, wallet_node_simulator, self_hostname): num_blocks = 4 long_blocks = 20 full_nodes, wallets = wallet_node_simulator @@ -449,7 +449,7 @@ async def test_subscribe_for_coin_id_reorg(self, wallet_node_simulator): puzzle_hash = await standard_wallet.get_new_puzzlehash() await server_2.start_client(PeerInfo(self_hostname, uint16(fn_server._port)), None) - incoming_queue, peer_id = await add_dummy_connection(fn_server, 12312, NodeType.WALLET) + incoming_queue, peer_id = await add_dummy_connection(fn_server, self_hostname, 12312, NodeType.WALLET) fake_wallet_peer = fn_server.all_connections[peer_id] zero_ph = 32 * b"\0" @@ -504,7 +504,7 @@ async def test_subscribe_for_coin_id_reorg(self, wallet_node_simulator): assert second_coin.created_height is None @pytest.mark.asyncio - async def test_subscribe_for_hint(self, wallet_node_simulator): + async def test_subscribe_for_hint(self, bt, wallet_node_simulator, self_hostname): num_blocks = 4 full_nodes, wallets = wallet_node_simulator full_node_api = full_nodes[0] @@ -513,7 +513,7 @@ async def test_subscribe_for_hint(self, wallet_node_simulator): wsm: WalletStateManager = wallet_node.wallet_state_manager await server_2.start_client(PeerInfo(self_hostname, uint16(fn_server._port)), None) - incoming_queue, peer_id = await add_dummy_connection(fn_server, 12312, NodeType.WALLET) + incoming_queue, peer_id = await add_dummy_connection(fn_server, self_hostname, 12312, NodeType.WALLET) wt: WalletTool = bt.get_pool_wallet_tool() ph = wt.get_new_puzzlehash() @@ -579,7 +579,7 @@ async def test_subscribe_for_hint(self, wallet_node_simulator): assert data_response.coin_states[0] == coin_records[0].coin_state @pytest.mark.asyncio - async def test_subscribe_for_hint_long_sync(self, wallet_two_node_simulator): + async def test_subscribe_for_hint_long_sync(self, wallet_two_node_simulator, bt, self_hostname): num_blocks = 4 full_nodes, wallets = wallet_two_node_simulator full_node_api = full_nodes[0] @@ -592,8 +592,8 @@ async def test_subscribe_for_hint_long_sync(self, wallet_two_node_simulator): wsm: WalletStateManager = wallet_node.wallet_state_manager await server_2.start_client(PeerInfo(self_hostname, uint16(fn_server._port)), None) - incoming_queue, peer_id = await add_dummy_connection(fn_server, 12312, NodeType.WALLET) - incoming_queue_1, peer_id_1 = await add_dummy_connection(fn_server_1, 12313, NodeType.WALLET) + incoming_queue, peer_id = await add_dummy_connection(fn_server, self_hostname, 12312, NodeType.WALLET) + incoming_queue_1, peer_id_1 = await add_dummy_connection(fn_server_1, self_hostname, 12313, NodeType.WALLET) wt: WalletTool = bt.get_pool_wallet_tool() ph = wt.get_new_puzzlehash() diff --git a/tests/wallet/sync/test_wallet_sync.py b/tests/wallet/sync/test_wallet_sync.py index 1b80a4bb79c8..340dd442018d 100644 --- a/tests/wallet/sync/test_wallet_sync.py +++ b/tests/wallet/sync/test_wallet_sync.py @@ -13,7 +13,7 @@ from chia.wallet.wallet_state_manager import WalletStateManager from tests.connection_utils import disconnect_all_and_reconnect from tests.pools.test_pool_rpc import wallet_is_synced -from tests.setup_nodes import bt, self_hostname, setup_node_and_wallet, setup_simulators_and_wallets, test_constants +from tests.setup_nodes import setup_node_and_wallet, setup_simulators_and_wallets, test_constants from tests.time_out_assert import time_out_assert @@ -35,8 +35,8 @@ def event_loop(): class TestWalletSync: @pytest_asyncio.fixture(scope="function") - async def wallet_node(self): - async for _ in setup_node_and_wallet(test_constants): + async def wallet_node(self, self_hostname): + async for _ in setup_node_and_wallet(test_constants, self_hostname): yield _ @pytest_asyncio.fixture(scope="function") @@ -45,8 +45,8 @@ async def wallet_node_simulator(self): yield _ @pytest_asyncio.fixture(scope="function") - async def wallet_node_starting_height(self): - async for _ in setup_node_and_wallet(test_constants, starting_height=100): + async def wallet_node_starting_height(self, self_hostname): + async for _ in setup_node_and_wallet(test_constants, self_hostname, starting_height=100): yield _ @pytest.mark.parametrize( @@ -54,7 +54,7 @@ async def wallet_node_starting_height(self): [True, False], ) @pytest.mark.asyncio - async def test_basic_sync_wallet(self, wallet_node, default_400_blocks, trusted): + async def test_basic_sync_wallet(self, bt, wallet_node, default_400_blocks, trusted, self_hostname): full_node_api, wallet_node, full_node_server, wallet_server = wallet_node @@ -77,7 +77,7 @@ async def test_basic_sync_wallet(self, wallet_node, default_400_blocks, trusted) for i in range(1, len(blocks_reorg)): await full_node_api.full_node.respond_block(full_node_protocol.RespondBlock(blocks_reorg[i])) - await disconnect_all_and_reconnect(wallet_server, full_node_server) + await disconnect_all_and_reconnect(wallet_server, full_node_server, self_hostname) await time_out_assert( 100, wallet_height_at_least, True, wallet_node, len(default_400_blocks) + num_blocks - 5 - 1 @@ -88,7 +88,7 @@ async def test_basic_sync_wallet(self, wallet_node, default_400_blocks, trusted) [True, False], ) @pytest.mark.asyncio - async def test_almost_recent(self, wallet_node, default_1000_blocks, trusted): + async def test_almost_recent(self, bt, wallet_node, default_1000_blocks, trusted, self_hostname): # Tests the edge case of receiving funds right before the recent blocks in weight proof full_node_api, wallet_node, full_node_server, wallet_server = wallet_node @@ -119,14 +119,14 @@ async def test_almost_recent(self, wallet_node, default_1000_blocks, trusted): await wallet_server.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) - await time_out_assert(30, wallet.get_confirmed_balance, 20 * calculate_pool_reward(1000)) + await time_out_assert(30, wallet.get_confirmed_balance, 20 * calculate_pool_reward(uint32(1000))) @pytest.mark.parametrize( "trusted", [True, False], ) @pytest.mark.asyncio - async def test_backtrack_sync_wallet(self, wallet_node, default_400_blocks, trusted): + async def test_backtrack_sync_wallet(self, wallet_node, default_400_blocks, trusted, self_hostname): full_node_api, wallet_node, full_node_server, wallet_server = wallet_node for block in default_400_blocks[:20]: await full_node_api.full_node.respond_block(full_node_protocol.RespondBlock(block)) @@ -147,7 +147,7 @@ async def test_backtrack_sync_wallet(self, wallet_node, default_400_blocks, trus [True, False], ) @pytest.mark.asyncio - async def test_short_batch_sync_wallet(self, wallet_node, default_400_blocks, trusted): + async def test_short_batch_sync_wallet(self, wallet_node, default_400_blocks, trusted, self_hostname): full_node_api, wallet_node, full_node_server, wallet_server = wallet_node for block in default_400_blocks[:200]: @@ -169,7 +169,9 @@ async def test_short_batch_sync_wallet(self, wallet_node, default_400_blocks, tr [True, False], ) @pytest.mark.asyncio - async def test_long_sync_wallet(self, wallet_node, default_1000_blocks, default_400_blocks, trusted): + async def test_long_sync_wallet( + self, bt, wallet_node, default_1000_blocks, default_400_blocks, trusted, self_hostname + ): full_node_api, wallet_node, full_node_server, wallet_server = wallet_node @@ -190,12 +192,12 @@ async def test_long_sync_wallet(self, wallet_node, default_1000_blocks, default_ for block in default_1000_blocks: await full_node_api.full_node.respond_block(full_node_protocol.RespondBlock(block)) - await disconnect_all_and_reconnect(wallet_server, full_node_server) + await disconnect_all_and_reconnect(wallet_server, full_node_server, self_hostname) log.info(f"wallet node height is {wallet_node.wallet_state_manager.blockchain.get_peak_height()}") await time_out_assert(600, wallet_height_at_least, True, wallet_node, len(default_1000_blocks) - 1) - await disconnect_all_and_reconnect(wallet_server, full_node_server) + await disconnect_all_and_reconnect(wallet_server, full_node_server, self_hostname) # Tests a short reorg num_blocks = 30 @@ -213,7 +215,7 @@ async def test_long_sync_wallet(self, wallet_node, default_1000_blocks, default_ [True, False], ) @pytest.mark.asyncio - async def test_wallet_reorg_sync(self, wallet_node_simulator, default_400_blocks, trusted): + async def test_wallet_reorg_sync(self, bt, wallet_node_simulator, default_400_blocks, trusted, self_hostname): num_blocks = 5 full_nodes, wallets = wallet_node_simulator full_node_api = full_nodes[0] @@ -266,7 +268,9 @@ async def get_tx_count(wallet_id): [False], ) @pytest.mark.asyncio - async def test_wallet_reorg_get_coinbase(self, wallet_node_simulator, default_400_blocks, trusted): + async def test_wallet_reorg_get_coinbase( + self, bt, wallet_node_simulator, default_400_blocks, trusted, self_hostname + ): full_nodes, wallets = wallet_node_simulator full_node_api = full_nodes[0] wallet_node, server_2 = wallets[0] @@ -310,7 +314,7 @@ async def get_tx_count(wallet_id): await asyncio.sleep(0.4) await full_node_api.full_node.respond_block(full_node_protocol.RespondBlock(block)) - await disconnect_all_and_reconnect(server_2, fn_server) + await disconnect_all_and_reconnect(server_2, fn_server, self_hostname) # Confirm we have the funds funds = calculate_pool_reward(uint32(len(blocks_reorg_1))) + calculate_base_farmer_reward( diff --git a/tests/wallet/test_wallet.py b/tests/wallet/test_wallet.py index 887d74c4840c..4902e759f3d9 100644 --- a/tests/wallet/test_wallet.py +++ b/tests/wallet/test_wallet.py @@ -16,7 +16,7 @@ from chia.wallet.transaction_record import TransactionRecord from chia.wallet.wallet_node import WalletNode from chia.wallet.wallet_state_manager import WalletStateManager -from tests.setup_nodes import self_hostname, setup_simulators_and_wallets +from tests.setup_nodes import setup_simulators_and_wallets from tests.time_out_assert import time_out_assert, time_out_assert_not_none from tests.wallet.cat_wallet.test_cat_wallet import tx_in_pool @@ -52,7 +52,7 @@ async def three_sim_two_wallets(self): [True, False], ) @pytest.mark.asyncio - async def test_wallet_coinbase(self, wallet_node, trusted): + async def test_wallet_coinbase(self, wallet_node, trusted, self_hostname): num_blocks = 10 full_nodes, wallets = wallet_node full_node_api = full_nodes[0] @@ -108,7 +108,7 @@ async def check_tx_are_pool_farm_rewards(): [True, False], ) @pytest.mark.asyncio - async def test_wallet_make_transaction(self, two_wallet_nodes, trusted): + async def test_wallet_make_transaction(self, two_wallet_nodes, trusted, self_hostname): num_blocks = 5 full_nodes, wallets = two_wallet_nodes full_node_api = full_nodes[0] @@ -165,7 +165,7 @@ async def test_wallet_make_transaction(self, two_wallet_nodes, trusted): [True, False], ) @pytest.mark.asyncio - async def test_wallet_coinbase_reorg(self, wallet_node, trusted): + async def test_wallet_coinbase_reorg(self, wallet_node, trusted, self_hostname): num_blocks = 5 full_nodes, wallets = wallet_node full_node_api = full_nodes[0] @@ -204,7 +204,7 @@ async def test_wallet_coinbase_reorg(self, wallet_node, trusted): [True, False], ) @pytest.mark.asyncio - async def test_wallet_send_to_three_peers(self, three_sim_two_wallets, trusted): + async def test_wallet_send_to_three_peers(self, three_sim_two_wallets, trusted, self_hostname): num_blocks = 10 full_nodes, wallets = three_sim_two_wallets @@ -270,7 +270,7 @@ async def test_wallet_send_to_three_peers(self, three_sim_two_wallets, trusted): [True, False], ) @pytest.mark.asyncio - async def test_wallet_make_transaction_hop(self, two_wallet_nodes_five_freeze, trusted): + async def test_wallet_make_transaction_hop(self, two_wallet_nodes_five_freeze, trusted, self_hostname): num_blocks = 10 full_nodes, wallets = two_wallet_nodes_five_freeze full_node_api_0 = full_nodes[0] @@ -390,7 +390,7 @@ async def test_wallet_make_transaction_hop(self, two_wallet_nodes_five_freeze, t [True, False], ) @pytest.mark.asyncio - async def test_wallet_make_transaction_with_fee(self, two_wallet_nodes, trusted): + async def test_wallet_make_transaction_with_fee(self, two_wallet_nodes, trusted, self_hostname): num_blocks = 5 full_nodes, wallets = two_wallet_nodes full_node_1 = full_nodes[0] @@ -457,7 +457,7 @@ async def test_wallet_make_transaction_with_fee(self, two_wallet_nodes, trusted) [True, False], ) @pytest.mark.asyncio - async def test_wallet_create_hit_max_send_amount(self, two_wallet_nodes, trusted): + async def test_wallet_create_hit_max_send_amount(self, two_wallet_nodes, trusted, self_hostname): num_blocks = 5 full_nodes, wallets = two_wallet_nodes full_node_1 = full_nodes[0] @@ -553,7 +553,7 @@ async def test_wallet_create_hit_max_send_amount(self, two_wallet_nodes, trusted [True, False], ) @pytest.mark.asyncio - async def test_wallet_prevent_fee_theft(self, two_wallet_nodes, trusted): + async def test_wallet_prevent_fee_theft(self, two_wallet_nodes, trusted, self_hostname): num_blocks = 5 full_nodes, wallets = two_wallet_nodes full_node_1 = full_nodes[0] @@ -638,7 +638,7 @@ async def test_wallet_prevent_fee_theft(self, two_wallet_nodes, trusted): [True, False], ) @pytest.mark.asyncio - async def test_wallet_tx_reorg(self, two_wallet_nodes, trusted): + async def test_wallet_tx_reorg(self, two_wallet_nodes, trusted, self_hostname): num_blocks = 5 full_nodes, wallets = two_wallet_nodes full_node_api = full_nodes[0] @@ -732,7 +732,7 @@ async def test_wallet_tx_reorg(self, two_wallet_nodes, trusted): [False], ) @pytest.mark.asyncio - async def test_address_sliding_window(self, wallet_node_100_pk, trusted): + async def test_address_sliding_window(self, wallet_node_100_pk, trusted, self_hostname): full_nodes, wallets = wallet_node_100_pk full_node_api = full_nodes[0] server_1: ChiaServer = full_node_api.full_node.server @@ -775,7 +775,7 @@ async def test_address_sliding_window(self, wallet_node_100_pk, trusted): [True, False], ) @pytest.mark.asyncio - async def test_wallet_transaction_options(self, two_wallet_nodes, trusted): + async def test_wallet_transaction_options(self, two_wallet_nodes, trusted, self_hostname): num_blocks = 5 full_nodes, wallets = two_wallet_nodes full_node_api = full_nodes[0] diff --git a/tests/wallet/test_wallet_blockchain.py b/tests/wallet/test_wallet_blockchain.py index c4737b984078..1afc723d5daf 100644 --- a/tests/wallet/test_wallet_blockchain.py +++ b/tests/wallet/test_wallet_blockchain.py @@ -25,8 +25,8 @@ def event_loop(): class TestWalletBlockchain: @pytest_asyncio.fixture(scope="function") - async def wallet_node(self): - async for _ in setup_node_and_wallet(test_constants): + async def wallet_node(self, self_hostname): + async for _ in setup_node_and_wallet(test_constants, self_hostname): yield _ @pytest.mark.asyncio diff --git a/tests/wallet/test_wallet_key_val_store.py b/tests/wallet/test_wallet_key_val_store.py index b65309151164..ecaaf0bf2f26 100644 --- a/tests/wallet/test_wallet_key_val_store.py +++ b/tests/wallet/test_wallet_key_val_store.py @@ -7,7 +7,6 @@ from chia.types.header_block import HeaderBlock from chia.util.db_wrapper import DBWrapper from chia.wallet.key_val_store import KeyValStore -from tests.setup_nodes import bt @pytest.fixture(scope="module") @@ -18,7 +17,7 @@ def event_loop(): class TestWalletKeyValStore: @pytest.mark.asyncio - async def test_store(self): + async def test_store(self, bt): db_filename = Path("wallet_store_test.db") if db_filename.exists(): diff --git a/tests/weight_proof/test_weight_proof.py b/tests/weight_proof/test_weight_proof.py index 0309d438f936..284578d9cb92 100644 --- a/tests/weight_proof/test_weight_proof.py +++ b/tests/weight_proof/test_weight_proof.py @@ -22,7 +22,7 @@ from chia.util.config import load_config from chia.util.default_root import DEFAULT_ROOT_PATH from chia.util.generator_tools import get_block_header -from tests.setup_nodes import bt + try: from reprlib import repr @@ -201,7 +201,7 @@ async def test_weight_proof_from_genesis(self, default_400_blocks): assert wp is not None @pytest.mark.asyncio - async def test_weight_proof_edge_cases(self, default_400_blocks): + async def test_weight_proof_edge_cases(self, bt, default_400_blocks): blocks: List[FullBlock] = default_400_blocks blocks: List[FullBlock] = bt.get_consecutive_blocks( @@ -377,7 +377,7 @@ async def test_weight_proof10000__blocks_compact(self, default_10000_blocks_comp assert fork_point == 0 @pytest.mark.asyncio - async def test_weight_proof1000_partial_blocks_compact(self, default_10000_blocks_compact): + async def test_weight_proof1000_partial_blocks_compact(self, bt, default_10000_blocks_compact): blocks: List[FullBlock] = bt.get_consecutive_blocks( 100, block_list_input=default_10000_blocks_compact, From 5b0fb70d9795e5af310405213a4956baef856a5a Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Thu, 10 Mar 2022 20:08:30 +0100 Subject: [PATCH 182/378] a problem with using random.randint() to pick a port is that all processes (running in parallel) are seeded the same, and so pick the same ports at the same time, causing conflicts. This uses proper entropy instead. (#10621) --- tests/util/socket.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/util/socket.py b/tests/util/socket.py index 39077e0c650b..727e7e270df7 100644 --- a/tests/util/socket.py +++ b/tests/util/socket.py @@ -1,15 +1,18 @@ import random +import secrets import socket from typing import Set recent_ports: Set[int] = set() +prng = random.Random() +prng.seed(secrets.randbits(32)) def find_available_listen_port(name: str = "free") -> int: global recent_ports while True: - port = random.randint(2000, 65535) + port = prng.randint(2000, 65535) if port in recent_ports: continue @@ -17,6 +20,7 @@ def find_available_listen_port(name: str = "free") -> int: try: s.bind(("127.0.0.1", port)) except OSError: + recent_ports.add(port) continue recent_ports.add(port) From 88f7c6bd834896f9b132f30089b7ef8bce6f2240 Mon Sep 17 00:00:00 2001 From: Jeff Date: Thu, 10 Mar 2022 11:09:29 -0800 Subject: [PATCH 183/378] `chia keys show` will default to displaying the first observer-derived wallet address. With the addition of the `-d` option, the non-observer derived wallet address can be displayed. (#10615) --- chia/cmds/keys.py | 19 +++++++++++++++---- chia/cmds/keys_funcs.py | 11 +++++++---- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/chia/cmds/keys.py b/chia/cmds/keys.py index 243f1aca6d18..227ffabb8297 100644 --- a/chia/cmds/keys.py +++ b/chia/cmds/keys.py @@ -28,10 +28,21 @@ def generate_cmd(ctx: click.Context): @click.option( "--show-mnemonic-seed", help="Show the mnemonic seed of the keys", default=False, show_default=True, is_flag=True ) -def show_cmd(show_mnemonic_seed): +@click.option( + "--non-observer-derivation", + "-d", + help=( + "Show the first wallet address using non-observer derivation. Older Chia versions use " + "non-observer derivation when generating wallet addresses." + ), + default=False, + show_default=True, + is_flag=True, +) +def show_cmd(show_mnemonic_seed, non_observer_derivation): from .keys_funcs import show_all_keys - show_all_keys(show_mnemonic_seed) + show_all_keys(show_mnemonic_seed, non_observer_derivation) @keys_cmd.command("add", short_help="Add a private key by mnemonic") @@ -201,7 +212,7 @@ def derive_cmd(ctx: click.Context, fingerprint: Optional[int], filename: Optiona "--derive-from-hd-path", "-p", help="Search for items derived from a specific HD path. Indices ending in an 'n' indicate that " - "non-observer derivation should used at that index. Example HD path: m/12381n/8444n/2/", + "non-observer derivation should be used at that index. Example HD path: m/12381n/8444n/2/", type=str, ) @click.pass_context @@ -289,7 +300,7 @@ def wallet_address_cmd( "--derive-from-hd-path", "-p", help="Derive child keys rooted from a specific HD path. Indices ending in an 'n' indicate that " - "non-observer derivation should used at that index. Example HD path: m/12381n/8444n/2/", + "non-observer derivation should be used at that index. Example HD path: m/12381n/8444n/2/", type=str, ) @click.option( diff --git a/chia/cmds/keys_funcs.py b/chia/cmds/keys_funcs.py index deb22d75b8f2..1d9af0580756 100644 --- a/chia/cmds/keys_funcs.py +++ b/chia/cmds/keys_funcs.py @@ -67,7 +67,7 @@ def add_private_key_seed(mnemonic: str): @unlocks_keyring(use_passphrase_cache=True) -def show_all_keys(show_mnemonic: bool): +def show_all_keys(show_mnemonic: bool, non_observer_derivation: bool): """ Prints all keys and mnemonics (if available). """ @@ -92,10 +92,13 @@ def show_all_keys(show_mnemonic: bool): master_sk_to_farmer_sk(sk).get_g1(), ) print("Pool public key (m/12381/8444/1/0):", master_sk_to_pool_sk(sk).get_g1()) - print( - "First wallet address:", - encode_puzzle_hash(create_puzzlehash_for_pk(master_sk_to_wallet_sk(sk, uint32(0)).get_g1()), prefix), + first_wallet_sk: PrivateKey = ( + master_sk_to_wallet_sk(sk, uint32(0)) + if non_observer_derivation + else master_sk_to_wallet_sk_unhardened(sk, uint32(0)) ) + wallet_address: str = encode_puzzle_hash(create_puzzlehash_for_pk(first_wallet_sk.get_g1()), prefix) + print(f"First wallet address{' (non-observer)' if non_observer_derivation else ''}: {wallet_address}") assert seed is not None if show_mnemonic: print("Master private key (m):", bytes(sk).hex()) From 7a8cae29361c6b91668669d7a2ea34f3dfc27a0d Mon Sep 17 00:00:00 2001 From: dustinface <35775977+xdustinface@users.noreply.github.com> Date: Thu, 10 Mar 2022 20:10:03 +0100 Subject: [PATCH 184/378] pools: Fix `plotnft claim` command's output (#10609) If you currently claim rewards `claim_cmd` fails to print the txhash with the lookup hint in `submit_tx_with_confirmation` ``` Error performing operation on Plot NFT -f 172057028 wallet id: 12: 'dict' object has no attribute 'name' ``` Because `submit_tx_with_confirmation` expects a `TransactionRecord` as result from its callable parameter `func` but `pw_absorb_rewards` returns a dict which includes the `TransactionRecord` as value for the key `transaction`. This PR makes sure all other methods used as `func` callable have the same return behaviour as `pw_absorb_rewards`. We could have adjusted it the other way around (only return `TransactionRecord` in `pw_absorb_rewards`) but then we would drop information in the RPC client. With this PR you get: ``` Do chia wallet get_transaction -f 172057028 -tx 0x34f74a1ffd9da9a493b78463e635996fd03d4f805ade583acb9764df73355f9c to get status ``` --- chia/cmds/plotnft_funcs.py | 3 +- chia/rpc/wallet_rpc_client.py | 25 +++++----- tests/pools/test_pool_rpc.py | 88 +++++++++++++++++++---------------- 3 files changed, 65 insertions(+), 51 deletions(-) diff --git a/chia/cmds/plotnft_funcs.py b/chia/cmds/plotnft_funcs.py index 5fa7d7c52d8b..b70f202a5dd8 100644 --- a/chia/cmds/plotnft_funcs.py +++ b/chia/cmds/plotnft_funcs.py @@ -279,7 +279,8 @@ async def submit_tx_with_confirmation( if user_input.lower() == "y" or user_input.lower() == "yes": try: - tx_record: TransactionRecord = await func() + result: Dict = await func() + tx_record: TransactionRecord = result["transaction"] start = time.time() while time.time() - start < 10: await asyncio.sleep(0.1) diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index 78c9e7839fe2..62729fe17690 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -12,6 +12,13 @@ from chia.wallet.transaction_sorting import SortKey +def parse_result_transactions(result: Dict[str, Any]) -> Dict[str, Any]: + result["transaction"] = TransactionRecord.from_json_dict(result["transaction"]) + if result["fee_transaction"]: + result["fee_transaction"] = TransactionRecord.from_json_dict(result["fee_transaction"]) + return result + + class WalletRpcClient(RpcClient): """ Client to Chia RPC, connects to a local wallet. Uses HTTP/JSON, and converts back from @@ -291,10 +298,10 @@ async def create_new_pool_wallet( res = await self.fetch("create_new_wallet", request) return TransactionRecord.from_json_dict(res["transaction"]) - async def pw_self_pool(self, wallet_id: str, fee: uint64) -> TransactionRecord: - return TransactionRecord.from_json_dict( - (await self.fetch("pw_self_pool", {"wallet_id": wallet_id, "fee": fee}))["transaction"] - ) + async def pw_self_pool(self, wallet_id: str, fee: uint64) -> Dict: + reply = await self.fetch("pw_self_pool", {"wallet_id": wallet_id, "fee": fee}) + reply = parse_result_transactions(reply) + return reply async def pw_join_pool( self, wallet_id: str, target_puzzlehash: bytes32, pool_url: str, relative_lock_height: uint32, fee: uint64 @@ -308,17 +315,13 @@ async def pw_join_pool( } reply = await self.fetch("pw_join_pool", request) - reply["transaction"] = TransactionRecord.from_json_dict(reply["transaction"]) - if reply["fee_transaction"]: - reply["fee_transaction"] = TransactionRecord.from_json_dict(reply["fee_transaction"]) - return reply["transaction"] + reply = parse_result_transactions(reply) + return reply async def pw_absorb_rewards(self, wallet_id: str, fee: uint64 = uint64(0)) -> Dict: reply = await self.fetch("pw_absorb_rewards", {"wallet_id": wallet_id, "fee": fee}) reply["state"] = PoolWalletInfo.from_json_dict(reply["state"]) - reply["transaction"] = TransactionRecord.from_json_dict(reply["transaction"]) - if reply["fee_transaction"]: - reply["fee_transaction"] = TransactionRecord.from_json_dict(reply["fee_transaction"]) + reply = parse_result_transactions(reply) return reply async def pw_status(self, wallet_id: str) -> Tuple[PoolWalletInfo, List[TransactionRecord]]: diff --git a/tests/pools/test_pool_rpc.py b/tests/pools/test_pool_rpc.py index 36b10939dfb6..7e61fb2146af 100644 --- a/tests/pools/test_pool_rpc.py +++ b/tests/pools/test_pool_rpc.py @@ -3,7 +3,7 @@ from dataclasses import dataclass from pathlib import Path from shutil import rmtree -from typing import Optional, List, Dict +from typing import Any, Optional, List, Dict import pytest import pytest_asyncio @@ -803,20 +803,24 @@ async def test_self_pooling_to_pooling(self, setup, fee, trusted, self_hostname) assert status.target is None assert status_2.target is None - join_pool_tx: TransactionRecord = await client.pw_join_pool( - wallet_id, - pool_ph, - "https://pool.example.com", - 10, - fee, - ) - join_pool_tx_2: TransactionRecord = await client.pw_join_pool( - wallet_id_2, - pool_ph, - "https://pool.example.com", - 10, - fee, - ) + join_pool_tx: TransactionRecord = ( + await client.pw_join_pool( + wallet_id, + pool_ph, + "https://pool.example.com", + 10, + fee, + ) + )["transaction"] + join_pool_tx_2: TransactionRecord = ( + await client.pw_join_pool( + wallet_id_2, + pool_ph, + "https://pool.example.com", + 10, + fee, + ) + )["transaction"] assert join_pool_tx is not None assert join_pool_tx_2 is not None @@ -920,13 +924,15 @@ async def have_chia(): assert status.current.state == PoolSingletonState.SELF_POOLING.value assert status.target is None - join_pool_tx: TransactionRecord = await client.pw_join_pool( - wallet_id, - pool_ph, - "https://pool.example.com", - 5, - fee, - ) + join_pool_tx: TransactionRecord = ( + await client.pw_join_pool( + wallet_id, + pool_ph, + "https://pool.example.com", + 5, + fee, + ) + )["transaction"] assert join_pool_tx is not None status: PoolWalletInfo = (await client.pw_status(wallet_id))[0] @@ -954,9 +960,9 @@ async def status_is_farming_to_pool(): status: PoolWalletInfo = (await client.pw_status(wallet_id))[0] - leave_pool_tx: TransactionRecord = await client.pw_self_pool(wallet_id, fee) - assert leave_pool_tx.wallet_id == wallet_id - assert leave_pool_tx.amount == 1 + leave_pool_tx: Dict[str, Any] = await client.pw_self_pool(wallet_id, fee) + assert leave_pool_tx["transaction"].wallet_id == wallet_id + assert leave_pool_tx["transaction"].amount == 1 async def status_is_leaving(): await self.farm_blocks(full_node_api, our_ph, 1) @@ -1055,13 +1061,15 @@ async def status_is_farming_to_pool(): assert pw_info.current.relative_lock_height == 5 status: PoolWalletInfo = (await client.pw_status(wallet_id))[0] - join_pool_tx: TransactionRecord = await client.pw_join_pool( - wallet_id, - pool_b_ph, - "https://pool-b.org", - 10, - fee, - ) + join_pool_tx: TransactionRecord = ( + await client.pw_join_pool( + wallet_id, + pool_b_ph, + "https://pool-b.org", + 10, + fee, + ) + )["transaction"] assert join_pool_tx is not None async def status_is_leaving(): @@ -1157,13 +1165,15 @@ async def status_is_farming_to_pool(): assert pw_info.current.pool_url == "https://pool-a.org" assert pw_info.current.relative_lock_height == 5 - join_pool_tx: TransactionRecord = await client.pw_join_pool( - wallet_id, - pool_b_ph, - "https://pool-b.org", - 10, - fee, - ) + join_pool_tx: TransactionRecord = ( + await client.pw_join_pool( + wallet_id, + pool_b_ph, + "https://pool-b.org", + 10, + fee, + ) + )["transaction"] assert join_pool_tx is not None await time_out_assert( 10, From e8ff5a32a7745cba23d57cd2fc59fc42da393281 Mon Sep 17 00:00:00 2001 From: Johannes Tysiak Date: Thu, 10 Mar 2022 20:10:28 +0100 Subject: [PATCH 185/378] fix initial-config typo - log_maxbytesrotation (#10598) --- chia/util/initial-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia/util/initial-config.yaml b/chia/util/initial-config.yaml index edaf821f481c..600222592edb 100644 --- a/chia/util/initial-config.yaml +++ b/chia/util/initial-config.yaml @@ -126,7 +126,7 @@ logging: &logging log_filename: "log/debug.log" log_level: "WARNING" # Can be CRITICAL, ERROR, WARNING, INFO, DEBUG, NOTSET log_maxfilesrotation: 7 # Max files in rotation. Default value 7 if the key is not set - log_maxbytessrotation: 52428800 # Max bytes logged before rotating logs + log_maxbytesrotation: 52428800 # Max bytes logged before rotating logs log_syslog: False # If True, outputs to SysLog host and port specified log_syslog_host: "localhost" # Send logging messages to a remote or local Unix syslog log_syslog_port: 514 # UDP port of the remote or local Unix syslog From fe964a48bddb2f1c31e68d16d226deb44a51fb17 Mon Sep 17 00:00:00 2001 From: Mariano Sorgente <3069354+mariano54@users.noreply.github.com> Date: Thu, 10 Mar 2022 14:11:04 -0500 Subject: [PATCH 186/378] Fix invalid DB commit (#10594) * Fix invalid DB commit * More fixes * Add raise * Fix test --- chia/pools/pool_wallet.py | 4 +- chia/wallet/wallet_action_store.py | 23 ---------- chia/wallet/wallet_pool_store.py | 34 ++++++++------ chia/wallet/wallet_state_manager.py | 61 +++++++++++++++---------- chia/wallet/wallet_transaction_store.py | 4 +- tests/pools/test_wallet_pool_store.py | 6 +-- 6 files changed, 64 insertions(+), 68 deletions(-) diff --git a/chia/pools/pool_wallet.py b/chia/pools/pool_wallet.py index a748f1320fa7..5f6698625cb4 100644 --- a/chia/pools/pool_wallet.py +++ b/chia/pools/pool_wallet.py @@ -296,7 +296,7 @@ async def apply_state_transition(self, new_state: CoinSpend, block_height: uint3 await self.update_pool_config() return True - async def rewind(self, block_height: int) -> bool: + async def rewind(self, block_height: int, in_transaction: bool) -> bool: """ Rolls back all transactions after block_height, and if creation was after block_height, deletes the wallet. Returns True if the wallet should be removed. @@ -306,7 +306,7 @@ async def rewind(self, block_height: int) -> bool: self.wallet_id ).copy() prev_state: PoolWalletInfo = await self.get_current_state() - await self.wallet_state_manager.pool_store.rollback(block_height, self.wallet_id) + await self.wallet_state_manager.pool_store.rollback(block_height, self.wallet_id, in_transaction) if len(history) > 0 and history[0][0] > block_height: return True diff --git a/chia/wallet/wallet_action_store.py b/chia/wallet/wallet_action_store.py index f50af3229b8d..1abecbeb87fb 100644 --- a/chia/wallet/wallet_action_store.py +++ b/chia/wallet/wallet_action_store.py @@ -84,29 +84,6 @@ async def create_action( await self.db_connection.commit() self.db_wrapper.lock.release() - async def action_done(self, action_id: int): - """ - Marks action as done - """ - action: Optional[WalletAction] = await self.get_wallet_action(action_id) - assert action is not None - async with self.db_wrapper.lock: - cursor = await self.db_connection.execute( - "Replace INTO action_queue VALUES(?, ?, ?, ?, ?, ?, ?)", - ( - action.id, - action.name, - action.wallet_id, - action.type.value, - action.wallet_callback, - True, - action.data, - ), - ) - - await cursor.close() - await self.db_connection.commit() - async def get_all_pending_actions(self) -> List[WalletAction]: """ Returns list of all pending action diff --git a/chia/wallet/wallet_pool_store.py b/chia/wallet/wallet_pool_store.py index 26285ba73178..2523dd61c0ff 100644 --- a/chia/wallet/wallet_pool_store.py +++ b/chia/wallet/wallet_pool_store.py @@ -103,21 +103,29 @@ async def rebuild_cache(self) -> None: self._state_transitions_cache[wallet_id] = [] self._state_transitions_cache[wallet_id].append((height, coin_spend)) - async def rollback(self, height: int, wallet_id_arg: int) -> None: + async def rollback(self, height: int, wallet_id_arg: int, in_transaction: bool) -> None: """ Rollback removes all entries which have entry_height > height passed in. Note that this is not committed to the DB until db_wrapper.commit() is called. However it is written to the cache, so it can be fetched with get_all_state_transitions. """ - for wallet_id, items in self._state_transitions_cache.items(): - remove_index_start: Optional[int] = None - for i, (item_block_height, _) in enumerate(items): - if item_block_height > height and wallet_id == wallet_id_arg: - remove_index_start = i - break - if remove_index_start is not None: - del items[remove_index_start:] - cursor = await self.db_connection.execute( - "DELETE FROM pool_state_transitions WHERE height>? AND wallet_id=?", (height, wallet_id_arg) - ) - await cursor.close() + + if not in_transaction: + await self.db_wrapper.lock.acquire() + try: + for wallet_id, items in self._state_transitions_cache.items(): + remove_index_start: Optional[int] = None + for i, (item_block_height, _) in enumerate(items): + if item_block_height > height and wallet_id == wallet_id_arg: + remove_index_start = i + break + if remove_index_start is not None: + del items[remove_index_start:] + cursor = await self.db_connection.execute( + "DELETE FROM pool_state_transitions WHERE height>? AND wallet_id=?", (height, wallet_id_arg) + ) + await cursor.close() + finally: + if not in_transaction: + await self.db_connection.commit() + self.db_wrapper.lock.release() diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 53de1422764a..1b091e1408b4 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -4,6 +4,7 @@ import multiprocessing import multiprocessing.context import time +import traceback from collections import defaultdict from pathlib import Path from secrets import token_bytes @@ -797,7 +798,6 @@ async def new_coin_state( for tx_record in rem_tx_records: await self.tx_store.set_confirmed(tx_record.name, coin_state.spent_height) - await self.coin_store.db_connection.commit() for unconfirmed_record in all_unconfirmed: for rem_coin in unconfirmed_record.removals: if rem_coin.name() == coin_state.coin.name(): @@ -1103,30 +1103,41 @@ async def reorg_rollback(self, height: int): Rolls back and updates the coin_store and transaction store. It's possible this height is the tip, or even beyond the tip. """ - await self.coin_store.rollback_to_block(height) - - reorged: List[TransactionRecord] = await self.tx_store.get_transaction_above(height) - await self.tx_store.rollback_to_block(height) - await self.coin_store.db_wrapper.commit_transaction() - for record in reorged: - if record.type in [ - TransactionType.OUTGOING_TX, - TransactionType.OUTGOING_TRADE, - TransactionType.INCOMING_TRADE, - ]: - await self.tx_store.tx_reorged(record) - self.tx_pending_changed() - - # Removes wallets that were created from a blockchain transaction which got reorged. - remove_ids = [] - for wallet_id, wallet in self.wallets.items(): - if wallet.type() == WalletType.POOLING_WALLET.value: - remove: bool = await wallet.rewind(height) - if remove: - remove_ids.append(wallet_id) - for wallet_id in remove_ids: - await self.user_store.delete_wallet(wallet_id, in_transaction=False) - self.wallets.pop(wallet_id) + try: + await self.db_wrapper.commit_transaction() + await self.db_wrapper.begin_transaction() + + await self.coin_store.rollback_to_block(height) + reorged: List[TransactionRecord] = await self.tx_store.get_transaction_above(height) + await self.tx_store.rollback_to_block(height) + for record in reorged: + if record.type in [ + TransactionType.OUTGOING_TX, + TransactionType.OUTGOING_TRADE, + TransactionType.INCOMING_TRADE, + ]: + await self.tx_store.tx_reorged(record, in_transaction=True) + self.tx_pending_changed() + + # Removes wallets that were created from a blockchain transaction which got reorged. + remove_ids = [] + for wallet_id, wallet in self.wallets.items(): + if wallet.type() == WalletType.POOLING_WALLET.value: + remove: bool = await wallet.rewind(height, in_transaction=True) + if remove: + remove_ids.append(wallet_id) + for wallet_id in remove_ids: + await self.user_store.delete_wallet(wallet_id, in_transaction=True) + self.wallets.pop(wallet_id) + await self.db_wrapper.commit_transaction() + except Exception as e: + tb = traceback.format_exc() + self.log.error(f"Exception while rolling back: {e} {tb}") + await self.db_wrapper.rollback_transaction() + await self.coin_store.rebuild_wallet_cache() + await self.tx_store.rebuild_tx_cache() + await self.pool_store.rebuild_cache() + raise async def _await_closed(self) -> None: await self.db_connection.close() diff --git a/chia/wallet/wallet_transaction_store.py b/chia/wallet/wallet_transaction_store.py index 4c44e26c26fe..74a6216139d5 100644 --- a/chia/wallet/wallet_transaction_store.py +++ b/chia/wallet/wallet_transaction_store.py @@ -235,7 +235,7 @@ async def increment_sent( await self.add_transaction_record(tx, False) return True - async def tx_reorged(self, record: TransactionRecord): + async def tx_reorged(self, record: TransactionRecord, in_transaction: bool): """ Updates transaction sent count to 0 and resets confirmation data """ @@ -257,7 +257,7 @@ async def tx_reorged(self, record: TransactionRecord): name=record.name, memos=record.memos, ) - await self.add_transaction_record(tx, False) + await self.add_transaction_record(tx, in_transaction=in_transaction) async def get_transaction_record(self, tx_id: bytes32) -> Optional[TransactionRecord]: """ diff --git a/tests/pools/test_wallet_pool_store.py b/tests/pools/test_wallet_pool_store.py index bed22440c9aa..206493bc4695 100644 --- a/tests/pools/test_wallet_pool_store.py +++ b/tests/pools/test_wallet_pool_store.py @@ -103,7 +103,7 @@ async def test_store(self): await store.rebuild_cache() await store.add_spend(1, solution_4, 101) await store.rebuild_cache() - await store.rollback(101, 1) + await store.rollback(101, 1, False) await store.rebuild_cache() assert store.get_spends_for_wallet(1) == [ (100, solution_1), @@ -112,7 +112,7 @@ async def test_store(self): (101, solution_4), ] await store.rebuild_cache() - await store.rollback(100, 1) + await store.rollback(100, 1, False) await store.rebuild_cache() assert store.get_spends_for_wallet(1) == [ (100, solution_1), @@ -125,7 +125,7 @@ async def test_store(self): await store.add_spend(1, solution_4, 105) solution_5: CoinSpend = make_child_solution(solution_4) await store.add_spend(1, solution_5, 105) - await store.rollback(99, 1) + await store.rollback(99, 1, False) assert store.get_spends_for_wallet(1) == [] finally: From 7c717853a1ad55b87e29c1f8597018deea7a1a29 Mon Sep 17 00:00:00 2001 From: Don Kackman Date: Thu, 10 Mar 2022 13:11:30 -0600 Subject: [PATCH 187/378] correct spelling of genrated (#10653) --- chia/cmds/wallet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia/cmds/wallet.py b/chia/cmds/wallet.py index bd1dff88cb3e..de24d8ba1527 100644 --- a/chia/cmds/wallet.py +++ b/chia/cmds/wallet.py @@ -240,7 +240,7 @@ def add_token_cmd(wallet_rpc_port: Optional[int], asset_id: str, token_name: str required=True, multiple=True, ) -@click.option("-p", "--filepath", help="The path to write the genrated offer file to", required=True) +@click.option("-p", "--filepath", help="The path to write the generated offer file to", required=True) @click.option("-m", "--fee", help="A fee to add to the offer when it gets taken", default="0") def make_offer_cmd( wallet_rpc_port: Optional[int], fingerprint: int, offer: Tuple[str], request: Tuple[str], filepath: str, fee: str From ef4224d9e496e202f2f7fb7237ddc35003f89816 Mon Sep 17 00:00:00 2001 From: Don Kackman Date: Thu, 10 Mar 2022 13:25:28 -0600 Subject: [PATCH 188/378] fix [Bug] #10569 (#10571) --- chia/wallet/trading/trade_store.py | 41 ++++++++++++------------------ 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/chia/wallet/trading/trade_store.py b/chia/wallet/trading/trade_store.py index 8c0b8cf9533f..6a170c76c2ed 100644 --- a/chia/wallet/trading/trade_store.py +++ b/chia/wallet/trading/trade_store.py @@ -32,10 +32,7 @@ async def migrate_is_my_offer(log: logging.Logger, db_connection: aiosqlite.Conn updates.append((is_my_offer, row[1])) try: - await db_connection.executemany( - "UPDATE trade_records SET is_my_offer=? WHERE trade_id=?", - updates, - ) + await db_connection.executemany("UPDATE trade_records SET is_my_offer=? WHERE trade_id=?", updates) except (aiosqlite.OperationalError, aiosqlite.IntegrityError): log.exception("Failed to migrate is_my_offer property in trade_records") raise @@ -55,12 +52,7 @@ class TradeStore: log: logging.Logger @classmethod - async def create( - cls, - db_wrapper: DBWrapper, - cache_size: uint32 = uint32(600000), - name: str = None, - ) -> "TradeStore": + async def create(cls, db_wrapper: DBWrapper, cache_size: uint32 = uint32(600000), name: str = None) -> "TradeStore": self = cls() if name: @@ -163,11 +155,7 @@ async def set_status(self, trade_id: bytes32, status: TradeStatus, in_transactio await self.add_trade_record(tx, in_transaction) async def increment_sent( - self, - id: bytes32, - name: str, - send_status: MempoolInclusionStatus, - err: Optional[Err], + self, id: bytes32, name: str, send_status: MempoolInclusionStatus, err: Optional[Err] ) -> bool: """ Updates trade sent count (Full Node has received spend_bundle and sent ack). @@ -217,10 +205,19 @@ async def get_trades_count(self) -> Tuple[int, int, int]: row = await cursor.fetchone() await cursor.close() - if row is None: - return 0, 0, 0 + total = 0 + my_offers_count = 0 + taken_offers_count = 0 + + if row is not None: + if row[0] is not None: + total = int(row[0]) + if row[1] is not None: + my_offers_count = int(row[1]) + if row[2] is not None: + taken_offers_count = int(row[2]) - return int(row[0]), int(row[1]), int(row[2]) + return total, my_offers_count, taken_offers_count async def get_trade_record(self, trade_id: bytes32) -> Optional[TradeRecord]: """ @@ -253,13 +250,7 @@ async def get_not_sent(self) -> List[TradeRecord]: Returns the list of trades that have not been received by full node yet. """ - cursor = await self.db_connection.execute( - "SELECT * from trade_records WHERE sent Date: Fri, 11 Mar 2022 17:57:35 -0500 Subject: [PATCH 189/378] Version control (#10479) * Added version control enforcement to macOS m1 * Added enforced version control * Added enforce version compliance * Added enforced version compliance * Added enforced versioning * Updating this to include DRY internal action. * Removed some unintended whitespace. * Removed some unintended whitespace. * CI re-run * Trying to figure out why it's failing one test. * Trying to figure out why it's failing one test. * Trying to figure out why it's failing one test. --- .github/workflows/build-linux-installer-deb.yml | 3 +++ .github/workflows/build-linux-installer-rpm.yml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/build-linux-installer-deb.yml b/.github/workflows/build-linux-installer-deb.yml index e3929e741157..3cb64637a209 100644 --- a/.github/workflows/build-linux-installer-deb.yml +++ b/.github/workflows/build-linux-installer-deb.yml @@ -40,6 +40,9 @@ jobs: - name: Cleanup any leftovers that exist from previous runs run: bash build_scripts/clean-runner.sh || true + - uses: Chia-Network/actions/enforce-semver@main + if: startsWith(github.ref, 'refs/tags/') + - name: Setup Python environment uses: actions/setup-python@v2 with: diff --git a/.github/workflows/build-linux-installer-rpm.yml b/.github/workflows/build-linux-installer-rpm.yml index 96e580524e27..e2fabeff2892 100644 --- a/.github/workflows/build-linux-installer-rpm.yml +++ b/.github/workflows/build-linux-installer-rpm.yml @@ -41,6 +41,9 @@ jobs: - name: Cleanup any leftovers that exist from previous runs run: bash build_scripts/clean-runner.sh || true + - uses: Chia-Network/actions/enforce-semver@main + if: startsWith(github.ref, 'refs/tags/') + # Create our own venv outside of the git directory JUST for getting the ACTUAL version so that install can't break it - name: Get version number id: version_number From 0bf370c642007608a2e64b3970b7f7d19768125d Mon Sep 17 00:00:00 2001 From: dustinface <35775977+xdustinface@users.noreply.github.com> Date: Sat, 12 Mar 2022 01:23:01 +0100 Subject: [PATCH 190/378] plotting: Only lock while actually accessing `PlotManager.plots` (#10675) --- chia/plotting/manager.py | 42 ++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/chia/plotting/manager.py b/chia/plotting/manager.py index aed8921c61dc..6fcd84982eef 100644 --- a/chia/plotting/manager.py +++ b/chia/plotting/manager.py @@ -244,30 +244,30 @@ def _refresh_task(self): if path not in plot_paths: self.no_key_filenames.remove(path) - with self: - filenames_to_remove: List[str] = [] - for plot_filename, paths_entry in self.plot_filename_paths.items(): - loaded_path, duplicated_paths = paths_entry - loaded_plot = Path(loaded_path) / Path(plot_filename) - if loaded_plot not in plot_paths: - filenames_to_remove.append(plot_filename) + filenames_to_remove: List[str] = [] + for plot_filename, paths_entry in self.plot_filename_paths.items(): + loaded_path, duplicated_paths = paths_entry + loaded_plot = Path(loaded_path) / Path(plot_filename) + if loaded_plot not in plot_paths: + filenames_to_remove.append(plot_filename) + with self: if loaded_plot in self.plots: del self.plots[loaded_plot] + total_result.removed.append(loaded_plot) + # No need to check the duplicates here since we drop the whole entry + continue + + paths_to_remove: List[str] = [] + for path in duplicated_paths: + loaded_plot = Path(path) / Path(plot_filename) + if loaded_plot not in plot_paths: + paths_to_remove.append(path) total_result.removed.append(loaded_plot) - # No need to check the duplicates here since we drop the whole entry - continue - - paths_to_remove: List[str] = [] - for path in duplicated_paths: - loaded_plot = Path(path) / Path(plot_filename) - if loaded_plot not in plot_paths: - paths_to_remove.append(path) - total_result.removed.append(loaded_plot) - for path in paths_to_remove: - duplicated_paths.remove(path) - - for filename in filenames_to_remove: - del self.plot_filename_paths[filename] + for path in paths_to_remove: + duplicated_paths.remove(path) + + for filename in filenames_to_remove: + del self.plot_filename_paths[filename] for remaining, batch in list_to_batches(plot_paths, self.refresh_parameter.batch_size): batch_result: PlotRefreshResult = self.refresh_batch(batch, plot_directories) From 75ed1307e59bae20e7e52d7527d53ccf904681f1 Mon Sep 17 00:00:00 2001 From: Jeff Date: Fri, 11 Mar 2022 16:23:24 -0800 Subject: [PATCH 191/378] Fix exception when `chia keys migrate` is run without needing migration (#10655) * Fix exception when `chia keys migrate` is run without needing migration * Linter fixes --- chia/util/keychain.py | 27 +++++++++++++++++---------- chia/util/keyring_wrapper.py | 9 +++++++-- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/chia/util/keychain.py b/chia/util/keychain.py index 6c59b0546db5..b701f3191cd7 100644 --- a/chia/util/keychain.py +++ b/chia/util/keychain.py @@ -43,6 +43,10 @@ class KeyringMaxUnlockAttempts(Exception): pass +class KeyringNotSet(Exception): + pass + + def supports_keyring_passphrase() -> bool: # Support can be disabled by setting CHIA_PASSPHRASE_SUPPORT to 0/false return os.environ.get("CHIA_PASSPHRASE_SUPPORT", "true").lower() in ["1", "true"] @@ -238,14 +242,15 @@ class Keychain: def __init__(self, user: Optional[str] = None, service: Optional[str] = None, force_legacy: bool = False): self.user = user if user is not None else default_keychain_user() self.service = service if service is not None else default_keychain_service() - if force_legacy: - legacy_keyring_wrapper = KeyringWrapper.get_legacy_instance() - if legacy_keyring_wrapper is not None: - self.keyring_wrapper = legacy_keyring_wrapper - else: - return None - else: - self.keyring_wrapper = KeyringWrapper.get_shared_instance() + + keyring_wrapper: Optional[KeyringWrapper] = ( + KeyringWrapper.get_legacy_instance() if force_legacy else KeyringWrapper.get_shared_instance() + ) + + if keyring_wrapper is None: + raise KeyringNotSet(f"KeyringWrapper not set: force_legacy={force_legacy}") + + self.keyring_wrapper = keyring_wrapper @unlocks_keyring(use_passphrase_cache=True) def _get_pk_and_entropy(self, user: str) -> Optional[Tuple[G1Element, bytes]]: @@ -561,8 +566,10 @@ def migrate_legacy_keyring( @staticmethod def get_keys_needing_migration() -> Tuple[List[Tuple[PrivateKey, bytes]], Optional["Keychain"]]: - legacy_keyring: Optional[Keychain] = Keychain(force_legacy=True) - if legacy_keyring is None: + try: + legacy_keyring: Keychain = Keychain(force_legacy=True) + except KeyringNotSet: + # No legacy keyring available, so no keys need to be migrated return [], None keychain = Keychain() all_legacy_sks = legacy_keyring.get_all_private_keys() diff --git a/chia/util/keyring_wrapper.py b/chia/util/keyring_wrapper.py index 642d773c5aa6..0cb3c162b0ae 100644 --- a/chia/util/keyring_wrapper.py +++ b/chia/util/keyring_wrapper.py @@ -110,16 +110,21 @@ def __init__(self, keys_root_path: Path = DEFAULT_KEYS_ROOT_PATH, force_legacy: used CryptFileKeyring. We now use our own FileKeyring backend and migrate the data from the legacy CryptFileKeyring (on write). """ + from chia.util.keychain import KeyringNotSet + self.keys_root_path = keys_root_path if force_legacy: legacy_keyring = get_legacy_keyring_instance() if check_legacy_keyring_keys_present(legacy_keyring): self.keyring = legacy_keyring - else: - return None else: self.refresh_keyrings() + if self.keyring is None: + raise KeyringNotSet( + f"Unable to initialize keyring backend: keys_root_path={keys_root_path}, force_legacy={force_legacy}" + ) + def refresh_keyrings(self): self.keyring = None self.keyring = self._configure_backend() From 230b785d3a7d36ad35c719b5dc55e2320f5aca04 Mon Sep 17 00:00:00 2001 From: Chris Marslender Date: Fri, 11 Mar 2022 18:24:58 -0600 Subject: [PATCH 192/378] Slightly different query for V2 DBs for getting compact/uncompact block counts, to ensure the available index is used / avoid a table scan (#10661) --- chia/full_node/block_store.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/chia/full_node/block_store.py b/chia/full_node/block_store.py index 1bb180f00b67..418e0b576036 100644 --- a/chia/full_node/block_store.py +++ b/chia/full_node/block_store.py @@ -576,8 +576,15 @@ async def get_random_not_compactified(self, number: int) -> List[int]: return heights async def count_compactified_blocks(self) -> int: - async with self.db.execute("select count(*) from full_blocks where is_fully_compactified=1") as cursor: - row = await cursor.fetchone() + if self.db_wrapper.db_version == 2: + # DB V2 has an index on is_fully_compactified only for blocks in the main chain + async with self.db.execute( + "select count(*) from full_blocks where is_fully_compactified=1 and in_main_chain=1" + ) as cursor: + row = await cursor.fetchone() + else: + async with self.db.execute("select count(*) from full_blocks where is_fully_compactified=1") as cursor: + row = await cursor.fetchone() assert row is not None @@ -585,8 +592,15 @@ async def count_compactified_blocks(self) -> int: return int(count) async def count_uncompactified_blocks(self) -> int: - async with self.db.execute("select count(*) from full_blocks where is_fully_compactified=0") as cursor: - row = await cursor.fetchone() + if self.db_wrapper.db_version == 2: + # DB V2 has an index on is_fully_compactified only for blocks in the main chain + async with self.db.execute( + "select count(*) from full_blocks where is_fully_compactified=0 and in_main_chain=1" + ) as cursor: + row = await cursor.fetchone() + else: + async with self.db.execute("select count(*) from full_blocks where is_fully_compactified=0") as cursor: + row = await cursor.fetchone() assert row is not None From efdff0adba6b7257725f03b31a85c81e372d3604 Mon Sep 17 00:00:00 2001 From: Chris Marslender Date: Fri, 11 Mar 2022 18:25:55 -0600 Subject: [PATCH 193/378] Check prefix on send_transaction before sending (#10566) * Check prefix on send_transaction * Fix failing RPC tests - xch -> txch since tests are a testnet and we enforce valid prefixes with this PR --- chia/rpc/wallet_rpc_api.py | 7 ++++++- tests/wallet/rpc/test_wallet_rpc.py | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index af5efbc4c72e..79357f56a554 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -756,7 +756,12 @@ async def send_transaction(self, request): if not isinstance(request["amount"], int) or not isinstance(request["fee"], int): raise ValueError("An integer amount or fee is required (too many decimals)") amount: uint64 = uint64(request["amount"]) - puzzle_hash: bytes32 = decode_puzzle_hash(request["address"]) + address = request["address"] + selected_network = self.service.config["selected_network"] + expected_prefix = self.service.config["network_overrides"]["config"][selected_network]["address_prefix"] + if address[0 : len(expected_prefix)] != expected_prefix: + raise ValueError("Unexpected Address Prefix") + puzzle_hash: bytes32 = decode_puzzle_hash(address) memos: List[bytes] = [] if "memos" in request: diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index 735cc556655e..cf7dec8144f6 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -142,7 +142,7 @@ def stop_node_cb(): client_node = await FullNodeRpcClient.create(hostname, test_rpc_port_node, bt.root_path, config) try: await time_out_assert(5, client.get_synced) - addr = encode_puzzle_hash(await wallet_node_2.wallet_state_manager.main_wallet.get_new_puzzlehash(), "xch") + addr = encode_puzzle_hash(await wallet_node_2.wallet_state_manager.main_wallet.get_new_puzzlehash(), "txch") tx_amount = 15600000 try: await client.send_transaction("1", 100000000000000001, addr) @@ -263,7 +263,7 @@ async def eventual_balance_det(c, wallet_id: str): ] == initial_funds_eventually - tx_amount for i in range(0, 5): - await client.farm_block(encode_puzzle_hash(ph_2, "xch")) + await client.farm_block(encode_puzzle_hash(ph_2, "txch")) await asyncio.sleep(0.5) await time_out_assert(5, eventual_balance, initial_funds_eventually - tx_amount - signed_tx_amount) @@ -290,7 +290,7 @@ async def eventual_balance_det(c, wallet_id: str): push_res = await client_node.push_tx(tx_res.spend_bundle) assert push_res["success"] for i in range(0, 5): - await client.farm_block(encode_puzzle_hash(ph_2, "xch")) + await client.farm_block(encode_puzzle_hash(ph_2, "txch")) await asyncio.sleep(0.5) found: bool = False @@ -330,7 +330,7 @@ async def eventual_balance_det(c, wallet_id: str): await asyncio.sleep(3) for i in range(0, 5): - await client.farm_block(encode_puzzle_hash(ph_2, "xch")) + await client.farm_block(encode_puzzle_hash(ph_2, "txch")) await asyncio.sleep(0.5) new_balance = new_balance - 555 - 666 - 200 @@ -356,7 +356,7 @@ async def eventual_balance_det(c, wallet_id: str): assert all_transactions == sorted(all_transactions, key=attrgetter("confirmed_at_height"), reverse=True) # Test RELEVANCE - await client.send_transaction("1", 1, encode_puzzle_hash(ph_2, "xch")) # Create a pending tx + await client.send_transaction("1", 1, encode_puzzle_hash(ph_2, "txch")) # Create a pending tx all_transactions = await client.get_transactions("1", sort_key=SortKey.RELEVANCE) sorted_transactions = sorted(all_transactions, key=attrgetter("created_at_time"), reverse=True) @@ -386,11 +386,11 @@ async def eventual_balance_det(c, wallet_id: str): # Test get_transactions to address ph_by_addr = await wallet.get_new_puzzlehash() - await client.send_transaction("1", 1, encode_puzzle_hash(ph_by_addr, "xch")) - await client.farm_block(encode_puzzle_hash(ph_by_addr, "xch")) + await client.send_transaction("1", 1, encode_puzzle_hash(ph_by_addr, "txch")) + await client.farm_block(encode_puzzle_hash(ph_by_addr, "txch")) await time_out_assert(10, wallet_is_synced, True, wallet_node, full_node_api) tx_for_address = await wallet_rpc_api.get_transactions( - {"wallet_id": "1", "to_address": encode_puzzle_hash(ph_by_addr, "xch")} + {"wallet_id": "1", "to_address": encode_puzzle_hash(ph_by_addr, "txch")} ) assert len(tx_for_address["transactions"]) == 1 assert decode_puzzle_hash(tx_for_address["transactions"][0]["to_address"]) == ph_by_addr @@ -428,7 +428,7 @@ async def eventual_balance_det(c, wallet_id: str): await asyncio.sleep(1) for i in range(0, 5): - await client.farm_block(encode_puzzle_hash(ph_2, "xch")) + await client.farm_block(encode_puzzle_hash(ph_2, "txch")) await asyncio.sleep(0.5) await time_out_assert(10, eventual_balance_det, 20, client, cat_0_id) @@ -445,7 +445,7 @@ async def eventual_balance_det(c, wallet_id: str): await asyncio.sleep(1) for i in range(0, 5): - await client.farm_block(encode_puzzle_hash(ph_2, "xch")) + await client.farm_block(encode_puzzle_hash(ph_2, "txch")) await asyncio.sleep(0.5) bal_1 = await client_2.get_wallet_balance(cat_1_id) assert bal_1["confirmed_wallet_balance"] == 0 @@ -459,7 +459,7 @@ async def eventual_balance_det(c, wallet_id: str): await asyncio.sleep(1) for i in range(0, 5): - await client.farm_block(encode_puzzle_hash(ph_2, "xch")) + await client.farm_block(encode_puzzle_hash(ph_2, "txch")) await asyncio.sleep(0.5) await time_out_assert(10, eventual_balance_det, 16, client, cat_0_id) @@ -507,7 +507,7 @@ async def eventual_balance_det(c, wallet_id: str): await asyncio.sleep(1) for i in range(0, 5): - await client.farm_block(encode_puzzle_hash(ph_2, "xch")) + await client.farm_block(encode_puzzle_hash(ph_2, "txch")) await asyncio.sleep(0.5) async def is_trade_confirmed(client, trade) -> bool: From 70639be9065ae4034495577ca05ecbe691dc257e Mon Sep 17 00:00:00 2001 From: Adam Kelly <338792+aqk@users.noreply.github.com> Date: Fri, 11 Mar 2022 16:26:54 -0800 Subject: [PATCH 194/378] Ak.convert fixtures (#10612) * Use bt fixture * rebase * Use local_hostname * flake8 * Remove set_shared_instance * Remove unneeded comments * Revert unrelated change * Add back type signature - rebase issue * Correct type for _configure_legacy_backend * See what's going on during CI mypy run * github workflows * mypy typing * Remove legacy Keyring create method * Start daemon first * Set chia-blockchain-gui to hash in main * Fix two test files that were not importing self_hostname * self_hostname fixture * Convert all class fixtures to top level functions --- .../test_blockchain_transactions.py | 11 +- tests/core/cmds/test_keys.py | 83 +++--- tests/core/daemon/test_daemon.py | 81 +++--- .../full_node/full_sync/test_full_sync.py | 44 +-- tests/core/full_node/test_mempool.py | 3 +- .../full_node/test_mempool_performance.py | 13 +- tests/core/full_node/test_node_load.py | 11 +- tests/core/full_node/test_transactions.py | 29 +- tests/core/ssl/test_ssl.py | 48 ++-- tests/core/test_daemon_rpc.py | 11 +- tests/core/test_filter.py | 11 +- tests/core/test_full_node_rpc.py | 11 +- tests/core/util/test_config.py | 40 +-- .../util/test_file_keyring_synchronization.py | 24 +- tests/core/util/test_keyring_wrapper.py | 13 +- tests/pools/test_pool_rpc.py | 207 +++++++------- tests/simulation/test_simulation.py | 47 ++-- tests/wallet/cat_wallet/test_cat_lifecycle.py | 15 +- tests/wallet/cat_wallet/test_cat_wallet.py | 29 +- .../wallet/cat_wallet/test_offer_lifecycle.py | 253 +++++++++--------- tests/wallet/did_wallet/test_did.py | 55 ++-- tests/wallet/did_wallet/test_did_rpc.py | 11 +- tests/wallet/rl_wallet/test_rl_rpc.py | 11 +- tests/wallet/rl_wallet/test_rl_wallet.py | 11 +- tests/wallet/rpc/test_wallet_rpc.py | 11 +- .../simple_sync/test_simple_sync_protocol.py | 59 ++-- tests/wallet/sync/test_wallet_sync.py | 29 +- tests/wallet/test_wallet.py | 55 ++-- tests/wallet/test_wallet_blockchain.py | 11 +- 29 files changed, 648 insertions(+), 589 deletions(-) diff --git a/tests/blockchain/test_blockchain_transactions.py b/tests/blockchain/test_blockchain_transactions.py index 9f9c4f361406..00ead35c1867 100644 --- a/tests/blockchain/test_blockchain_transactions.py +++ b/tests/blockchain/test_blockchain_transactions.py @@ -31,12 +31,13 @@ def event_loop(): yield loop -class TestBlockchainTransactions: - @pytest_asyncio.fixture(scope="function") - async def two_nodes(self, db_version, self_hostname): - async for _ in setup_two_nodes(test_constants, db_version=db_version, self_hostname=self_hostname): - yield _ +@pytest_asyncio.fixture(scope="function") +async def two_nodes(db_version, self_hostname): + async for _ in setup_two_nodes(test_constants, db_version=db_version, self_hostname=self_hostname): + yield _ + +class TestBlockchainTransactions: @pytest.mark.asyncio async def test_basic_blockchain_tx(self, two_nodes, bt): num_blocks = 10 diff --git a/tests/core/cmds/test_keys.py b/tests/core/cmds/test_keys.py index 25b9fd7100de..2cdedbda1d31 100644 --- a/tests/core/cmds/test_keys.py +++ b/tests/core/cmds/test_keys.py @@ -65,50 +65,55 @@ def delete_password(self, service, username): del self.service_dict[service][username] -class TestKeysCommands: - @pytest.fixture(scope="function") - def empty_keyring(self): - with TempKeyring(user="user-chia-1.8", service="chia-user-chia-1.8") as keychain: - yield keychain - KeyringWrapper.cleanup_shared_instance() - - @pytest.fixture(scope="function") - def keyring_with_one_key(self, empty_keyring): - keychain = empty_keyring - keychain.add_private_key(TEST_MNEMONIC_SEED, "") - return keychain - - @pytest.fixture(scope="function") - def mnemonic_seed_file(self, tmp_path): - seed_file = Path(tmp_path) / "seed.txt" - with open(seed_file, "w") as f: - f.write(TEST_MNEMONIC_SEED) - return seed_file - - @pytest.fixture(scope="function") - def setup_keyringwrapper(self, tmp_path): - KeyringWrapper.cleanup_shared_instance() - KeyringWrapper.set_keys_root_path(tmp_path) - _ = KeyringWrapper.get_shared_instance() - yield +@pytest.fixture(scope="function") +def empty_keyring(): + with TempKeyring(user="user-chia-1.8", service="chia-user-chia-1.8") as keychain: + yield keychain KeyringWrapper.cleanup_shared_instance() - KeyringWrapper.set_keys_root_path(DEFAULT_KEYS_ROOT_PATH) - @pytest.fixture(scope="function") - def setup_legacy_keyringwrapper(self, tmp_path, monkeypatch): - def mock_setup_keyring_file_watcher(_): - pass - # Silence errors in the watchdog module during testing - monkeypatch.setattr(FileKeyring, "setup_keyring_file_watcher", mock_setup_keyring_file_watcher) +@pytest.fixture(scope="function") +def keyring_with_one_key(empty_keyring): + keychain = empty_keyring + keychain.add_private_key(TEST_MNEMONIC_SEED, "") + return keychain - KeyringWrapper.cleanup_shared_instance() - KeyringWrapper.set_keys_root_path(tmp_path) - KeyringWrapper.get_shared_instance().legacy_keyring = DummyLegacyKeyring() - yield - KeyringWrapper.cleanup_shared_instance() - KeyringWrapper.set_keys_root_path(DEFAULT_KEYS_ROOT_PATH) +@pytest.fixture(scope="function") +def mnemonic_seed_file(tmp_path): + seed_file = Path(tmp_path) / "seed.txt" + with open(seed_file, "w") as f: + f.write(TEST_MNEMONIC_SEED) + return seed_file + + +@pytest.fixture(scope="function") +def setup_keyringwrapper(tmp_path): + KeyringWrapper.cleanup_shared_instance() + KeyringWrapper.set_keys_root_path(tmp_path) + _ = KeyringWrapper.get_shared_instance() + yield + KeyringWrapper.cleanup_shared_instance() + KeyringWrapper.set_keys_root_path(DEFAULT_KEYS_ROOT_PATH) + + +@pytest.fixture(scope="function") +def setup_legacy_keyringwrapper(tmp_path, monkeypatch): + def mock_setup_keyring_file_watcher(_): + pass + + # Silence errors in the watchdog module during testing + monkeypatch.setattr(FileKeyring, "setup_keyring_file_watcher", mock_setup_keyring_file_watcher) + + KeyringWrapper.cleanup_shared_instance() + KeyringWrapper.set_keys_root_path(tmp_path) + KeyringWrapper.get_shared_instance().legacy_keyring = DummyLegacyKeyring() + yield + KeyringWrapper.cleanup_shared_instance() + KeyringWrapper.set_keys_root_path(DEFAULT_KEYS_ROOT_PATH) + + +class TestKeysCommands: def test_generate_with_new_config(self, tmp_path, empty_keyring): """ Generate a new config and a new key. Verify that the config has diff --git a/tests/core/daemon/test_daemon.py b/tests/core/daemon/test_daemon.py index 3225c474b524..797727e5377e 100644 --- a/tests/core/daemon/test_daemon.py +++ b/tests/core/daemon/test_daemon.py @@ -19,44 +19,49 @@ from tests.util.keyring import TempKeyring -class TestDaemon: - @pytest_asyncio.fixture(scope="function") - async def get_temp_keyring(self): - with TempKeyring() as keychain: - yield keychain - - @pytest_asyncio.fixture(scope="function") - async def get_b_tools_1(self, get_temp_keyring): - return await create_block_tools_async(constants=test_constants_modified, keychain=get_temp_keyring) - - @pytest_asyncio.fixture(scope="function") - async def get_b_tools(self, get_temp_keyring): - local_b_tools = await create_block_tools_async(constants=test_constants_modified, keychain=get_temp_keyring) - new_config = local_b_tools._config - local_b_tools.change_config(new_config) - return local_b_tools - - @pytest_asyncio.fixture(scope="function") - async def get_daemon_with_temp_keyring(self, get_b_tools): - async for daemon in setup_daemon(btools=get_b_tools): - yield get_b_tools, daemon - - # TODO: Ideally, the db_version should be the (parameterized) db_version - # fixture, to test all versions of the database schema. This doesn't work - # because of a hack in shutting down the full node, which means you cannot run - # more than one simulations per process. - @pytest_asyncio.fixture(scope="function") - async def simulation(self, bt, get_b_tools, get_b_tools_1): - async for _ in setup_full_system( - test_constants_modified, - bt, - b_tools=get_b_tools, - b_tools_1=get_b_tools_1, - connect_to_daemon=True, - db_version=1, - ): - yield _ +@pytest_asyncio.fixture(scope="function") +async def get_temp_keyring(): + with TempKeyring() as keychain: + yield keychain + + +@pytest_asyncio.fixture(scope="function") +async def get_b_tools_1(get_temp_keyring): + return await create_block_tools_async(constants=test_constants_modified, keychain=get_temp_keyring) + + +@pytest_asyncio.fixture(scope="function") +async def get_b_tools(get_temp_keyring): + local_b_tools = await create_block_tools_async(constants=test_constants_modified, keychain=get_temp_keyring) + new_config = local_b_tools._config + local_b_tools.change_config(new_config) + return local_b_tools + +@pytest_asyncio.fixture(scope="function") +async def get_daemon_with_temp_keyring(get_b_tools): + async for daemon in setup_daemon(btools=get_b_tools): + yield get_b_tools, daemon + + +# TODO: Ideally, the db_version should be the (parameterized) db_version +# fixture, to test all versions of the database schema. This doesn't work +# because of a hack in shutting down the full node, which means you cannot run +# more than one simulations per process. +@pytest_asyncio.fixture(scope="function") +async def simulation(bt, get_b_tools, get_b_tools_1): + async for _ in setup_full_system( + test_constants_modified, + bt, + b_tools=get_b_tools, + b_tools_1=get_b_tools_1, + connect_to_daemon=True, + db_version=1, + ): + yield _ + + +class TestDaemon: @pytest.mark.asyncio async def test_daemon_simulation(self, self_hostname, simulation, bt, get_b_tools, get_b_tools_1): node1, node2, _, _, _, _, _, _, _, _, server1, daemon1 = simulation @@ -332,7 +337,7 @@ async def check_invalid_mnemonic_case(response: aiohttp.http_websocket.WSMessage # Expect: Failure due to invalid mnemonic await check_invalid_mnemonic_length_case(await ws.receive()) - # When: using using an incorrect mnemnonic + # When: using an incorrect mnemnonic await ws.send_str( create_payload( "add_private_key", {"mnemonic": " ".join(["abandon"] * 24), "passphrase": ""}, "test", "daemon" diff --git a/tests/core/full_node/full_sync/test_full_sync.py b/tests/core/full_node/full_sync/test_full_sync.py index a0715978a99e..10ed699baf47 100644 --- a/tests/core/full_node/full_sync/test_full_sync.py +++ b/tests/core/full_node/full_sync/test_full_sync.py @@ -28,27 +28,31 @@ def event_loop(): log = logging.getLogger(__name__) -class TestFullSync: - @pytest_asyncio.fixture(scope="function") - async def two_nodes(self, db_version, self_hostname): - async for _ in setup_two_nodes(test_constants, db_version=db_version, self_hostname=self_hostname): - yield _ - - @pytest_asyncio.fixture(scope="function") - async def three_nodes(self, db_version, self_hostname): - async for _ in setup_n_nodes(test_constants, 3, db_version=db_version, self_hostname=self_hostname): - yield _ - - @pytest_asyncio.fixture(scope="function") - async def four_nodes(self, db_version, self_hostname): - async for _ in setup_n_nodes(test_constants, 4, db_version=db_version, self_hostname=self_hostname): - yield _ - - @pytest_asyncio.fixture(scope="function") - async def five_nodes(self, db_version, self_hostname): - async for _ in setup_n_nodes(test_constants, 5, db_version=db_version, self_hostname=self_hostname): - yield _ +@pytest_asyncio.fixture(scope="function") +async def two_nodes(db_version, self_hostname): + async for _ in setup_two_nodes(test_constants, db_version=db_version, self_hostname=self_hostname): + yield _ + + +@pytest_asyncio.fixture(scope="function") +async def three_nodes(db_version, self_hostname): + async for _ in setup_n_nodes(test_constants, 3, db_version=db_version, self_hostname=self_hostname): + yield _ + + +@pytest_asyncio.fixture(scope="function") +async def four_nodes(db_version, self_hostname): + async for _ in setup_n_nodes(test_constants, 4, db_version=db_version, self_hostname=self_hostname): + yield _ + +@pytest_asyncio.fixture(scope="function") +async def five_nodes(db_version, self_hostname): + async for _ in setup_n_nodes(test_constants, 5, db_version=db_version, self_hostname=self_hostname): + yield _ + + +class TestFullSync: @pytest.mark.asyncio async def test_long_sync_from_zero(self, five_nodes, default_400_blocks, bt, self_hostname): # Must be larger than "sync_block_behind_threshold" in the config diff --git a/tests/core/full_node/test_mempool.py b/tests/core/full_node/test_mempool.py index c58040593adf..ce883c5b592e 100644 --- a/tests/core/full_node/test_mempool.py +++ b/tests/core/full_node/test_mempool.py @@ -89,8 +89,7 @@ def event_loop(): yield loop -# TODO: this fixture should really be at function scope, to make all tests -# independent. +# TODO: this fixture should really be at function scope, to make all tests independent. # The reason it isn't is that our simulators can't be destroyed correctly, which # means you can't instantiate more than one per process, so this is a hack until # that is fixed. For now, our tests are not independent diff --git a/tests/core/full_node/test_mempool_performance.py b/tests/core/full_node/test_mempool_performance.py index f5b6ab8855f8..1de87a30f23f 100644 --- a/tests/core/full_node/test_mempool_performance.py +++ b/tests/core/full_node/test_mempool_performance.py @@ -41,13 +41,14 @@ def event_loop(): yield loop -class TestMempoolPerformance: - @pytest_asyncio.fixture(scope="module") - async def wallet_nodes(self, bt): - key_seed = bt.farmer_master_sk_entropy - async for _ in setup_simulators_and_wallets(2, 1, {}, key_seed=key_seed): - yield _ +@pytest_asyncio.fixture(scope="module") +async def wallet_nodes(bt): + key_seed = bt.farmer_master_sk_entropy + async for _ in setup_simulators_and_wallets(2, 1, {}, key_seed=key_seed): + yield _ + +class TestMempoolPerformance: @pytest.mark.asyncio async def test_mempool_update_performance(self, bt, wallet_nodes, default_400_blocks, self_hostname): blocks = default_400_blocks diff --git a/tests/core/full_node/test_node_load.py b/tests/core/full_node/test_node_load.py index 95d6010f7a7c..437789110bcb 100644 --- a/tests/core/full_node/test_node_load.py +++ b/tests/core/full_node/test_node_load.py @@ -18,12 +18,13 @@ def event_loop(): yield loop -class TestNodeLoad: - @pytest_asyncio.fixture(scope="function") - async def two_nodes(self, db_version, self_hostname): - async for _ in setup_two_nodes(test_constants, db_version=db_version, self_hostname=self_hostname): - yield _ +@pytest_asyncio.fixture(scope="function") +async def two_nodes(db_version, self_hostname): + async for _ in setup_two_nodes(test_constants, db_version=db_version, self_hostname=self_hostname): + yield _ + +class TestNodeLoad: @pytest.mark.asyncio async def test_blocks_load(self, bt, two_nodes, self_hostname): num_blocks = 50 diff --git a/tests/core/full_node/test_transactions.py b/tests/core/full_node/test_transactions.py index 37051af8a567..643dd1868bec 100644 --- a/tests/core/full_node/test_transactions.py +++ b/tests/core/full_node/test_transactions.py @@ -22,22 +22,25 @@ def event_loop(): yield loop -class TestTransactions: - @pytest_asyncio.fixture(scope="function") - async def wallet_node(self): - async for _ in setup_simulators_and_wallets(1, 1, {}): - yield _ +@pytest_asyncio.fixture(scope="function") +async def wallet_node(): + async for _ in setup_simulators_and_wallets(1, 1, {}): + yield _ + + +@pytest_asyncio.fixture(scope="function") +async def two_wallet_nodes(): + async for _ in setup_simulators_and_wallets(1, 2, {}): + yield _ - @pytest_asyncio.fixture(scope="function") - async def two_wallet_nodes(self): - async for _ in setup_simulators_and_wallets(1, 2, {}): - yield _ - @pytest_asyncio.fixture(scope="function") - async def three_nodes_two_wallets(self): - async for _ in setup_simulators_and_wallets(3, 2, {}): - yield _ +@pytest_asyncio.fixture(scope="function") +async def three_nodes_two_wallets(): + async for _ in setup_simulators_and_wallets(3, 2, {}): + yield _ + +class TestTransactions: @pytest.mark.asyncio async def test_wallet_coinbase(self, wallet_node, self_hostname): num_blocks = 5 diff --git a/tests/core/ssl/test_ssl.py b/tests/core/ssl/test_ssl.py index 66e7a381ee10..666a467e7e1a 100644 --- a/tests/core/ssl/test_ssl.py +++ b/tests/core/ssl/test_ssl.py @@ -51,32 +51,36 @@ async def establish_connection(server: ChiaServer, self_hostname: str, ssl_conte return False -class TestSSL: - @pytest_asyncio.fixture(scope="function") - async def harvester_farmer(self, bt): - async for _ in setup_farmer_harvester(bt, test_constants): - yield _ +@pytest_asyncio.fixture(scope="function") +async def harvester_farmer(bt): + async for _ in setup_farmer_harvester(bt, test_constants): + yield _ + + +@pytest_asyncio.fixture(scope="function") +async def wallet_node(): + async for _ in setup_simulators_and_wallets(1, 1, {}): + yield _ - @pytest_asyncio.fixture(scope="function") - async def wallet_node(self): - async for _ in setup_simulators_and_wallets(1, 1, {}): - yield _ - @pytest_asyncio.fixture(scope="function") - async def introducer(self, bt): - introducer_port = find_available_listen_port("introducer") - async for _ in setup_introducer(bt, introducer_port): - yield _ +@pytest_asyncio.fixture(scope="function") +async def introducer(bt): + introducer_port = find_available_listen_port("introducer") + async for _ in setup_introducer(bt, introducer_port): + yield _ - @pytest_asyncio.fixture(scope="function") - async def timelord(self, bt): - timelord_port = find_available_listen_port("timelord") - node_port = find_available_listen_port("node") - rpc_port = find_available_listen_port("rpc") - vdf_port = find_available_listen_port("vdf") - async for _ in setup_timelord(timelord_port, node_port, rpc_port, vdf_port, False, test_constants, bt): - yield _ +@pytest_asyncio.fixture(scope="function") +async def timelord(bt): + timelord_port = find_available_listen_port("timelord") + node_port = find_available_listen_port("node") + rpc_port = find_available_listen_port("rpc") + vdf_port = find_available_listen_port("vdf") + async for _ in setup_timelord(timelord_port, node_port, rpc_port, vdf_port, False, test_constants, bt): + yield _ + + +class TestSSL: @pytest.mark.asyncio async def test_public_connections(self, wallet_node, self_hostname): full_nodes, wallets = wallet_node diff --git a/tests/core/test_daemon_rpc.py b/tests/core/test_daemon_rpc.py index 0fa9f7f48999..f5100eed8990 100644 --- a/tests/core/test_daemon_rpc.py +++ b/tests/core/test_daemon_rpc.py @@ -6,12 +6,13 @@ from chia import __version__ -class TestDaemonRpc: - @pytest_asyncio.fixture(scope="function") - async def get_daemon(self, bt): - async for _ in setup_daemon(btools=bt): - yield _ +@pytest_asyncio.fixture(scope="function") +async def get_daemon(bt): + async for _ in setup_daemon(btools=bt): + yield _ + +class TestDaemonRpc: @pytest.mark.asyncio async def test_get_version_rpc(self, get_daemon, bt): ws_server = get_daemon diff --git a/tests/core/test_filter.py b/tests/core/test_filter.py index 40717d397433..13e3263df199 100644 --- a/tests/core/test_filter.py +++ b/tests/core/test_filter.py @@ -14,12 +14,13 @@ def event_loop(): yield loop -class TestFilter: - @pytest_asyncio.fixture(scope="function") - async def wallet_and_node(self): - async for _ in setup_simulators_and_wallets(1, 1, {}): - yield _ +@pytest_asyncio.fixture(scope="function") +async def wallet_and_node(): + async for _ in setup_simulators_and_wallets(1, 1, {}): + yield _ + +class TestFilter: @pytest.mark.asyncio async def test_basic_filter_test(self, wallet_and_node, bt): full_nodes, wallets = wallet_and_node diff --git a/tests/core/test_full_node_rpc.py b/tests/core/test_full_node_rpc.py index be7fe86337a5..fb3107a2a1c8 100644 --- a/tests/core/test_full_node_rpc.py +++ b/tests/core/test_full_node_rpc.py @@ -28,12 +28,13 @@ from tests.util.socket import find_available_listen_port -class TestRpc: - @pytest_asyncio.fixture(scope="function") - async def two_nodes(self): - async for _ in setup_simulators_and_wallets(2, 0, {}): - yield _ +@pytest_asyncio.fixture(scope="function") +async def two_nodes(): + async for _ in setup_simulators_and_wallets(2, 0, {}): + yield _ + +class TestRpc: @pytest.mark.asyncio async def test1(self, two_nodes, bt, self_hostname): num_blocks = 5 diff --git a/tests/core/util/test_config.py b/tests/core/util/test_config.py index e0f38ade3867..c19c180d4506 100644 --- a/tests/core/util/test_config.py +++ b/tests/core/util/test_config.py @@ -12,7 +12,7 @@ from time import sleep from typing import Dict -# Commented-out lines are preserved to aide in debugging the multiprocessing tests +# Commented-out lines are preserved to aid in debugging the multiprocessing tests # import logging # import os # import threading @@ -71,26 +71,28 @@ def run_reader_and_writer_tasks(root_path: Path, default_config: Dict): asyncio.get_event_loop().run_until_complete(create_reader_and_writer_tasks(root_path, default_config)) -class TestConfig: - @pytest.fixture(scope="function") - def root_path_populated_with_config(self, tmpdir) -> Path: - """ - Create a temp directory and populate it with a default config.yaml. - Returns the root path containing the config. - """ - root_path: Path = Path(tmpdir) - create_default_chia_config(root_path) - return Path(root_path) +@pytest.fixture(scope="function") +def root_path_populated_with_config(tmpdir) -> Path: + """ + Create a temp directory and populate it with a default config.yaml. + Returns the root path containing the config. + """ + root_path: Path = Path(tmpdir) + create_default_chia_config(root_path) + return Path(root_path) - @pytest.fixture(scope="function") - def default_config_dict(self) -> Dict: - """ - Returns a dictionary containing the default config.yaml contents - """ - content: str = initial_config_file("config.yaml") - config: Dict = yaml.safe_load(content) - return config +@pytest.fixture(scope="function") +def default_config_dict() -> Dict: + """ + Returns a dictionary containing the default config.yaml contents + """ + content: str = initial_config_file("config.yaml") + config: Dict = yaml.safe_load(content) + return config + + +class TestConfig: def test_create_config_new(self, tmpdir): """ Test create_default_chia_config() as in a first run scenario diff --git a/tests/core/util/test_file_keyring_synchronization.py b/tests/core/util/test_file_keyring_synchronization.py index ccaddf9cd149..9035baf33e6d 100644 --- a/tests/core/util/test_file_keyring_synchronization.py +++ b/tests/core/util/test_file_keyring_synchronization.py @@ -147,19 +147,21 @@ def poll_directory(dir: Path, expected_entries: int, max_attempts: int, interval return found_all -class TestFileKeyringSynchronization: - @pytest.fixture(scope="function") - def ready_dir(self, tmp_path: Path): - ready_dir: Path = tmp_path / "ready" - mkdir(ready_dir) - return ready_dir +@pytest.fixture(scope="function") +def ready_dir(tmp_path: Path): + ready_dir: Path = tmp_path / "ready" + mkdir(ready_dir) + return ready_dir - @pytest.fixture(scope="function") - def finished_dir(self, tmp_path: Path): - finished_dir: Path = tmp_path / "finished" - mkdir(finished_dir) - return finished_dir +@pytest.fixture(scope="function") +def finished_dir(tmp_path: Path): + finished_dir: Path = tmp_path / "finished" + mkdir(finished_dir) + return finished_dir + + +class TestFileKeyringSynchronization: # When: using a new empty keyring @using_temp_file_keyring() def test_multiple_writers(self): diff --git a/tests/core/util/test_keyring_wrapper.py b/tests/core/util/test_keyring_wrapper.py index ff738b9b40c8..77cfe3a38e50 100644 --- a/tests/core/util/test_keyring_wrapper.py +++ b/tests/core/util/test_keyring_wrapper.py @@ -9,13 +9,14 @@ log = logging.getLogger(__name__) -class TestKeyringWrapper: - @pytest.fixture(autouse=True, scope="function") - def setup_keyring_wrapper(self): - yield - KeyringWrapper.cleanup_shared_instance() - assert KeyringWrapper.get_shared_instance(create_if_necessary=False) is None +@pytest.fixture(autouse=True, scope="function") +def setup_keyring_wrapper(): + yield + KeyringWrapper.cleanup_shared_instance() + assert KeyringWrapper.get_shared_instance(create_if_necessary=False) is None + +class TestKeyringWrapper: def test_shared_instance(self): """ Using KeyringWrapper's get_shared_instance() method should return the same diff --git a/tests/pools/test_pool_rpc.py b/tests/pools/test_pool_rpc.py index 7e61fb2146af..d9fa9f660233 100644 --- a/tests/pools/test_pool_rpc.py +++ b/tests/pools/test_pool_rpc.py @@ -43,6 +43,20 @@ def get_pool_plot_dir(): return get_plot_dir() / Path("pool_tests") +async def get_total_block_rewards(num_blocks): + funds = sum( + [calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i)) for i in range(1, num_blocks)] + ) + return funds + + +async def farm_blocks(full_node_api, ph: bytes32, num_blocks: int): + for i in range(num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) + return num_blocks + # TODO also return calculated block rewards + + @dataclass class TemporaryPoolPlot: bt: BlockTools @@ -83,57 +97,24 @@ def event_loop(): PREFARMED_BLOCKS = 4 -class TestPoolWalletRpc: - @pytest_asyncio.fixture(scope="function") - async def two_wallet_nodes(self): - async for _ in setup_simulators_and_wallets(1, 2, {}): - yield _ - - @pytest_asyncio.fixture(scope="function") - async def one_wallet_node_and_rpc(self, bt, self_hostname): - rmtree(get_pool_plot_dir(), ignore_errors=True) - async for nodes in setup_simulators_and_wallets(1, 1, {}): - full_nodes, wallets = nodes - full_node_api = full_nodes[0] - wallet_node_0, wallet_server_0 = wallets[0] - - wallet_0 = wallet_node_0.wallet_state_manager.main_wallet - our_ph = await wallet_0.get_new_puzzlehash() - await self.farm_blocks(full_node_api, our_ph, PREFARMED_BLOCKS) - - api_user = WalletRpcApi(wallet_node_0) - config = bt.config - daemon_port = config["daemon_port"] - test_rpc_port = find_available_listen_port("rpc_port") - - rpc_cleanup = await start_rpc_server( - api_user, - self_hostname, - daemon_port, - test_rpc_port, - lambda x: None, - bt.root_path, - config, - connect_to_daemon=False, - ) - client = await WalletRpcClient.create(self_hostname, test_rpc_port, bt.root_path, config) - - yield client, wallet_node_0, full_node_api +@pytest_asyncio.fixture(scope="function") +async def two_wallet_nodes(): + async for _ in setup_simulators_and_wallets(1, 2, {}): + yield _ - client.close() - await client.await_closed() - await rpc_cleanup() - @pytest_asyncio.fixture(scope="function") - async def setup(self, two_wallet_nodes, bt, self_hostname): - rmtree(get_pool_plot_dir(), ignore_errors=True) - full_nodes, wallets = two_wallet_nodes +@pytest_asyncio.fixture(scope="function") +async def one_wallet_node_and_rpc(bt, self_hostname): + rmtree(get_pool_plot_dir(), ignore_errors=True) + async for nodes in setup_simulators_and_wallets(1, 1, {}): + full_nodes, wallets = nodes + full_node_api = full_nodes[0] wallet_node_0, wallet_server_0 = wallets[0] - wallet_node_1, wallet_server_1 = wallets[1] - our_ph_record = await wallet_node_0.wallet_state_manager.get_unused_derivation_record(1, False, True) - pool_ph_record = await wallet_node_1.wallet_state_manager.get_unused_derivation_record(1, False, True) - our_ph = our_ph_record.puzzle_hash - pool_ph = pool_ph_record.puzzle_hash + + wallet_0 = wallet_node_0.wallet_state_manager.main_wallet + our_ph = await wallet_0.get_new_puzzlehash() + await farm_blocks(full_node_api, our_ph, PREFARMED_BLOCKS) + api_user = WalletRpcApi(wallet_node_0) config = bt.config daemon_port = config["daemon_port"] @@ -151,26 +132,50 @@ async def setup(self, two_wallet_nodes, bt, self_hostname): ) client = await WalletRpcClient.create(self_hostname, test_rpc_port, bt.root_path, config) - return ( - full_nodes, - [wallet_node_0, wallet_node_1], - [our_ph, pool_ph], - client, # wallet rpc client - rpc_cleanup, - ) + yield client, wallet_node_0, full_node_api + + client.close() + await client.await_closed() + await rpc_cleanup() + + +@pytest_asyncio.fixture(scope="function") +async def setup(two_wallet_nodes, bt, self_hostname): + rmtree(get_pool_plot_dir(), ignore_errors=True) + full_nodes, wallets = two_wallet_nodes + wallet_node_0, wallet_server_0 = wallets[0] + wallet_node_1, wallet_server_1 = wallets[1] + our_ph_record = await wallet_node_0.wallet_state_manager.get_unused_derivation_record(1, False, True) + pool_ph_record = await wallet_node_1.wallet_state_manager.get_unused_derivation_record(1, False, True) + our_ph = our_ph_record.puzzle_hash + pool_ph = pool_ph_record.puzzle_hash + api_user = WalletRpcApi(wallet_node_0) + config = bt.config + daemon_port = config["daemon_port"] + test_rpc_port = find_available_listen_port("rpc_port") + + rpc_cleanup = await start_rpc_server( + api_user, + self_hostname, + daemon_port, + test_rpc_port, + lambda x: None, + bt.root_path, + config, + connect_to_daemon=False, + ) + client = await WalletRpcClient.create(self_hostname, test_rpc_port, bt.root_path, config) - async def get_total_block_rewards(self, num_blocks): - funds = sum( - [calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i)) for i in range(1, num_blocks)] - ) - return funds + return ( + full_nodes, + [wallet_node_0, wallet_node_1], + [our_ph, pool_ph], + client, # wallet rpc client + rpc_cleanup, + ) - async def farm_blocks(self, full_node_api, ph: bytes32, num_blocks: int): - for i in range(num_blocks): - await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) - return num_blocks - # TODO also return calculated block rewards +class TestPoolWalletRpc: @pytest.mark.asyncio @pytest.mark.parametrize("trusted", [True, False]) @pytest.mark.parametrize("fee", [0, FEE_AMOUNT]) @@ -187,7 +192,7 @@ async def test_create_new_pool_wallet_self_farm(self, one_wallet_node_and_rpc, f await wallet_node_0.server.start_client( PeerInfo(self_hostname, uint16(full_node_api.full_node.server._port)), None ) - total_block_rewards = await self.get_total_block_rewards(PREFARMED_BLOCKS) + total_block_rewards = await get_total_block_rewards(PREFARMED_BLOCKS) await time_out_assert(10, wallet_0.get_confirmed_balance, total_block_rewards) await time_out_assert(10, wallet_node_0.wallet_state_manager.blockchain.get_peak_height, PREFARMED_BLOCKS) @@ -207,7 +212,7 @@ async def test_create_new_pool_wallet_self_farm(self, one_wallet_node_and_rpc, f creation_tx.name, ) - await self.farm_blocks(full_node_api, our_ph, 6) + await farm_blocks(full_node_api, our_ph, 6) assert full_node_api.full_node.mempool_manager.get_spendbundle(creation_tx.name) is None await time_out_assert(10, wallet_is_synced, True, wallet_node_0, full_node_api) @@ -263,7 +268,7 @@ async def test_create_new_pool_wallet_farm_to_pool(self, one_wallet_node_and_rpc await wallet_node_0.server.start_client( PeerInfo(self_hostname, uint16(full_node_api.full_node.server._port)), None ) - total_block_rewards = await self.get_total_block_rewards(PREFARMED_BLOCKS) + total_block_rewards = await get_total_block_rewards(PREFARMED_BLOCKS) await time_out_assert(10, wallet_node_0.wallet_state_manager.blockchain.get_peak_height, PREFARMED_BLOCKS) await time_out_assert(10, wallet_0.get_confirmed_balance, total_block_rewards) @@ -286,7 +291,7 @@ async def test_create_new_pool_wallet_farm_to_pool(self, one_wallet_node_and_rpc creation_tx.name, ) - await self.farm_blocks(full_node_api, our_ph, 6) + await farm_blocks(full_node_api, our_ph, 6) assert full_node_api.full_node.mempool_manager.get_spendbundle(creation_tx.name) is None await time_out_assert(5, wallet_is_synced, True, wallet_node_0, full_node_api) @@ -341,7 +346,7 @@ async def test_create_multiple_pool_wallets(self, one_wallet_node_and_rpc, fee, await wallet_node_0.server.start_client( PeerInfo(self_hostname, uint16(full_node_api.full_node.server._port)), None ) - total_block_rewards = await self.get_total_block_rewards(PREFARMED_BLOCKS) + total_block_rewards = await get_total_block_rewards(PREFARMED_BLOCKS) wallet_0 = wallet_node_0.wallet_state_manager.main_wallet await time_out_assert(10, wallet_0.get_confirmed_balance, total_block_rewards) await time_out_assert(10, wallet_node_0.wallet_state_manager.blockchain.get_peak_height, PREFARMED_BLOCKS) @@ -374,7 +379,7 @@ async def test_create_multiple_pool_wallets(self, one_wallet_node_and_rpc, fee, creation_tx_2.name, ) - await self.farm_blocks(full_node_api, our_ph_2, 6) + await farm_blocks(full_node_api, our_ph_2, 6) assert full_node_api.full_node.mempool_manager.get_spendbundle(creation_tx.name) is None assert full_node_api.full_node.mempool_manager.get_spendbundle(creation_tx_2.name) is None @@ -415,7 +420,7 @@ async def test_create_multiple_pool_wallets(self, one_wallet_node_and_rpc, fee, cat_0_id = res["wallet_id"] asset_id = bytes.fromhex(res["asset_id"]) assert len(asset_id) > 0 - await self.farm_blocks(full_node_api, our_ph_2, 6) + await farm_blocks(full_node_api, our_ph_2, 6) await time_out_assert(20, wallet_is_synced, True, wallet_node_0, full_node_api) bal_0 = await client.get_wallet_balance(cat_0_id) assert bal_0["confirmed_wallet_balance"] == 20 @@ -434,7 +439,7 @@ async def test_create_multiple_pool_wallets(self, one_wallet_node_and_rpc, fee, creation_tx_3.spend_bundle, creation_tx_3.name, ) - await self.farm_blocks(full_node_api, our_ph_2, 2) + await farm_blocks(full_node_api, our_ph_2, 2) await time_out_assert(20, wallet_is_synced, True, wallet_node_0, full_node_api) full_config: Dict = load_config(wallet_0.wallet_state_manager.root_path, "config.yaml") @@ -472,7 +477,7 @@ async def test_absorb_self(self, one_wallet_node_and_rpc, fee, trusted, bt, self PeerInfo(self_hostname, uint16(full_node_api.full_node.server._port)), None ) wallet_0 = wallet_node_0.wallet_state_manager.main_wallet - total_block_rewards = await self.get_total_block_rewards(PREFARMED_BLOCKS) + total_block_rewards = await get_total_block_rewards(PREFARMED_BLOCKS) await time_out_assert(10, wallet_0.get_confirmed_balance, total_block_rewards) await time_out_assert(10, wallet_node_0.wallet_state_manager.blockchain.get_peak_height, PREFARMED_BLOCKS) @@ -493,7 +498,7 @@ async def test_absorb_self(self, one_wallet_node_and_rpc, fee, trusted, bt, self creation_tx.spend_bundle, creation_tx.name, ) - await self.farm_blocks(full_node_api, our_ph, 1) + await farm_blocks(full_node_api, our_ph, 1) await asyncio.sleep(2) status: PoolWalletInfo = (await client.pw_status(2))[0] @@ -524,7 +529,7 @@ async def test_absorb_self(self, one_wallet_node_and_rpc, fee, trusted, bt, self absorb_tx.spend_bundle, absorb_tx.name, ) - await self.farm_blocks(full_node_api, our_ph, 2) + await farm_blocks(full_node_api, our_ph, 2) await asyncio.sleep(2) new_status: PoolWalletInfo = (await client.pw_status(2))[0] assert status.current == new_status.current @@ -543,7 +548,7 @@ async def test_absorb_self(self, one_wallet_node_and_rpc, fee, trusted, bt, self absorb_tx1.name, ) - await self.farm_blocks(full_node_api, our_ph, 2) + await farm_blocks(full_node_api, our_ph, 2) await asyncio.sleep(2) bal = await client.get_wallet_balance(2) assert bal["confirmed_wallet_balance"] == 0 @@ -560,7 +565,7 @@ async def test_absorb_self(self, one_wallet_node_and_rpc, fee, trusted, bt, self tr.spend_bundle, tr.name, ) - await self.farm_blocks(full_node_api, our_ph, 2) + await farm_blocks(full_node_api, our_ph, 2) # Balance ignores non coinbase TX bal = await client.get_wallet_balance(2) assert bal["confirmed_wallet_balance"] == 0 @@ -588,7 +593,7 @@ async def test_absorb_pooling(self, one_wallet_node_and_rpc, fee, trusted, bt, s PeerInfo(self_hostname, uint16(full_node_api.full_node.server._port)), None ) wallet_0 = wallet_node_0.wallet_state_manager.main_wallet - total_block_rewards = await self.get_total_block_rewards(PREFARMED_BLOCKS) + total_block_rewards = await get_total_block_rewards(PREFARMED_BLOCKS) await time_out_assert(10, wallet_0.get_confirmed_balance, total_block_rewards) await time_out_assert(10, wallet_node_0.wallet_state_manager.blockchain.get_peak_height, PREFARMED_BLOCKS) @@ -610,7 +615,7 @@ async def test_absorb_pooling(self, one_wallet_node_and_rpc, fee, trusted, bt, s creation_tx.spend_bundle, creation_tx.name, ) - await self.farm_blocks(full_node_api, our_ph, 1) + await farm_blocks(full_node_api, our_ph, 1) await asyncio.sleep(2) status: PoolWalletInfo = (await client.pw_status(2))[0] @@ -641,7 +646,7 @@ async def test_absorb_pooling(self, one_wallet_node_and_rpc, fee, trusted, bt, s absorb_tx.spend_bundle, absorb_tx.name, ) - await self.farm_blocks(full_node_api, our_ph, 2) + await farm_blocks(full_node_api, our_ph, 2) await asyncio.sleep(5) new_status: PoolWalletInfo = (await client.pw_status(2))[0] assert status.current == new_status.current @@ -666,7 +671,7 @@ async def test_absorb_pooling(self, one_wallet_node_and_rpc, fee, trusted, bt, s assert ret["fee_transaction"].fee_amount == fee assert absorb_tx.fee_amount == fee - await self.farm_blocks(full_node_api, our_ph, 2) + await farm_blocks(full_node_api, our_ph, 2) await asyncio.sleep(5) bal = await client.get_wallet_balance(2) assert bal["confirmed_wallet_balance"] == 0 @@ -705,7 +710,7 @@ async def test_absorb_pooling(self, one_wallet_node_and_rpc, fee, trusted, bt, s absorb_tx.name, ) - await self.farm_blocks(full_node_api, our_ph, 2) + await farm_blocks(full_node_api, our_ph, 2) await asyncio.sleep(2) new_status: PoolWalletInfo = (await client.pw_status(2))[0] assert status.current == new_status.current @@ -745,8 +750,8 @@ async def test_self_pooling_to_pooling(self, setup, fee, trusted, self_hostname) ) try: - total_blocks += await self.farm_blocks(full_node_api, our_ph, num_blocks) - total_block_rewards = await self.get_total_block_rewards(total_blocks) + total_blocks += await farm_blocks(full_node_api, our_ph, num_blocks) + total_block_rewards = await get_total_block_rewards(total_blocks) await time_out_assert(10, wallets[0].get_unconfirmed_balance, total_block_rewards) await time_out_assert(10, wallets[0].get_confirmed_balance, total_block_rewards) @@ -779,7 +784,7 @@ async def test_self_pooling_to_pooling(self, setup, fee, trusted, self_hostname) creation_tx_2.name, ) - await self.farm_blocks(full_node_api, our_ph, 6) + await farm_blocks(full_node_api, our_ph, 6) assert full_node_api.full_node.mempool_manager.get_spendbundle(creation_tx.name) is None await time_out_assert(10, wallet_is_synced, True, wallet_node_0, full_node_api) @@ -841,9 +846,9 @@ async def tx_is_in_mempool(wid, tx: TransactionRecord): assert status_2.target is not None assert status_2.target.state == PoolSingletonState.FARMING_TO_POOL.value - await self.farm_blocks(full_node_api, our_ph, 6) + await farm_blocks(full_node_api, our_ph, 6) - total_blocks += await self.farm_blocks(full_node_api, our_ph, num_blocks) + total_blocks += await farm_blocks(full_node_api, our_ph, num_blocks) async def status_is_farming_to_pool(w_id: int): pw_status: PoolWalletInfo = (await client.pw_status(w_id))[0] @@ -891,7 +896,7 @@ async def test_leave_pool(self, setup, fee, trusted, self_hostname): assert False async def have_chia(): - await self.farm_blocks(full_node_api, our_ph, 1) + await farm_blocks(full_node_api, our_ph, 1) return (await wallets[0].get_confirmed_balance()) > 0 await time_out_assert(timeout=WAIT_SECS, function=have_chia) @@ -908,7 +913,7 @@ async def have_chia(): creation_tx.name, ) - await self.farm_blocks(full_node_api, our_ph, 6) + await farm_blocks(full_node_api, our_ph, 6) assert full_node_api.full_node.mempool_manager.get_spendbundle(creation_tx.name) is None await time_out_assert(10, wallet_is_synced, True, wallet_nodes[0], full_node_api) @@ -950,7 +955,7 @@ async def have_chia(): assert status.target.version == 1 async def status_is_farming_to_pool(): - await self.farm_blocks(full_node_api, our_ph, 1) + await farm_blocks(full_node_api, our_ph, 1) pw_status: PoolWalletInfo = (await client.pw_status(wallet_id))[0] return pw_status.current.state == PoolSingletonState.FARMING_TO_POOL.value @@ -965,7 +970,7 @@ async def status_is_farming_to_pool(): assert leave_pool_tx["transaction"].amount == 1 async def status_is_leaving(): - await self.farm_blocks(full_node_api, our_ph, 1) + await farm_blocks(full_node_api, our_ph, 1) pw_status: PoolWalletInfo = (await client.pw_status(wallet_id))[0] return pw_status.current.state == PoolSingletonState.LEAVING_POOL.value @@ -973,7 +978,7 @@ async def status_is_leaving(): async def status_is_self_pooling(): # Farm enough blocks to wait for relative_lock_height - await self.farm_blocks(full_node_api, our_ph, 1) + await farm_blocks(full_node_api, our_ph, 1) pw_status: PoolWalletInfo = (await client.pw_status(wallet_id))[0] return pw_status.current.state == PoolSingletonState.SELF_POOLING.value @@ -1016,7 +1021,7 @@ async def test_change_pools(self, setup, fee, trusted, self_hostname): assert False async def have_chia(): - await self.farm_blocks(full_node_api, our_ph, 1) + await farm_blocks(full_node_api, our_ph, 1) return (await wallets[0].get_confirmed_balance()) > 0 await time_out_assert(timeout=WAIT_SECS, function=have_chia) @@ -1033,7 +1038,7 @@ async def have_chia(): creation_tx.name, ) - await self.farm_blocks(full_node_api, our_ph, 6) + await farm_blocks(full_node_api, our_ph, 6) assert full_node_api.full_node.mempool_manager.get_spendbundle(creation_tx.name) is None await time_out_assert(10, wallet_is_synced, True, wallet_nodes[0], full_node_api) @@ -1050,7 +1055,7 @@ async def have_chia(): assert status.target is None async def status_is_farming_to_pool(): - await self.farm_blocks(full_node_api, our_ph, 1) + await farm_blocks(full_node_api, our_ph, 1) pw_status: PoolWalletInfo = (await client.pw_status(wallet_id))[0] return pw_status.current.state == PoolSingletonState.FARMING_TO_POOL.value @@ -1073,7 +1078,7 @@ async def status_is_farming_to_pool(): assert join_pool_tx is not None async def status_is_leaving(): - await self.farm_blocks(full_node_api, our_ph, 1) + await farm_blocks(full_node_api, our_ph, 1) pw_status: PoolWalletInfo = (await client.pw_status(wallet_id))[0] return pw_status.current.state == PoolSingletonState.LEAVING_POOL.value @@ -1121,7 +1126,7 @@ async def test_change_pools_reorg(self, setup, fee, trusted, bt, self_hostname): assert False async def have_chia(): - await self.farm_blocks(full_node_api, our_ph, 1) + await farm_blocks(full_node_api, our_ph, 1) return (await wallets[0].get_confirmed_balance()) > 0 await time_out_assert(timeout=WAIT_SECS, function=have_chia) @@ -1138,7 +1143,7 @@ async def have_chia(): creation_tx.name, ) - await self.farm_blocks(full_node_api, our_ph, 6) + await farm_blocks(full_node_api, our_ph, 6) assert full_node_api.full_node.mempool_manager.get_spendbundle(creation_tx.name) is None await time_out_assert(5, wallet_is_synced, True, wallet_nodes[0], full_node_api) @@ -1155,7 +1160,7 @@ async def have_chia(): assert status.target is None async def status_is_farming_to_pool(): - await self.farm_blocks(full_node_api, our_ph, 1) + await farm_blocks(full_node_api, our_ph, 1) pw_status: PoolWalletInfo = (await client.pw_status(wallet_id))[0] return pw_status.current.state == PoolSingletonState.FARMING_TO_POOL.value @@ -1181,7 +1186,7 @@ async def status_is_farming_to_pool(): join_pool_tx.spend_bundle, join_pool_tx.name, ) - await self.farm_blocks(full_node_api, our_ph, 1) + await farm_blocks(full_node_api, our_ph, 1) async def status_is_leaving_no_blocks(): pw_status: PoolWalletInfo = (await client.pw_status(wallet_id))[0] diff --git a/tests/simulation/test_simulation.py b/tests/simulation/test_simulation.py index 6fe1e88b51b1..903984155a81 100644 --- a/tests/simulation/test_simulation.py +++ b/tests/simulation/test_simulation.py @@ -26,32 +26,33 @@ ) -class TestSimulation: +# TODO: Ideally, the db_version should be the (parameterized) db_version +# fixture, to test all versions of the database schema. This doesn't work +# because of a hack in shutting down the full node, which means you cannot run +# more than one simulations per process. +@pytest_asyncio.fixture(scope="function") +async def extra_node(self_hostname): + with TempKeyring() as keychain: + b_tools = await create_block_tools_async(constants=test_constants_modified, keychain=keychain) + async for _ in setup_full_node( + test_constants_modified, + "blockchain_test_3.db", + self_hostname, + find_available_listen_port(), + find_available_listen_port(), + b_tools, + db_version=1, + ): + yield _ - # TODO: Ideally, the db_version should be the (parameterized) db_version - # fixture, to test all versions of the database schema. This doesn't work - # because of a hack in shutting down the full node, which means you cannot run - # more than one simulations per process. - @pytest_asyncio.fixture(scope="function") - async def extra_node(self, self_hostname): - with TempKeyring() as keychain: - b_tools = await create_block_tools_async(constants=test_constants_modified, keychain=keychain) - async for _ in setup_full_node( - test_constants_modified, - "blockchain_test_3.db", - self_hostname, - find_available_listen_port(), - find_available_listen_port(), - b_tools, - db_version=1, - ): - yield _ - @pytest_asyncio.fixture(scope="function") - async def simulation(self, bt): - async for _ in setup_full_system(test_constants_modified, bt, db_version=1): - yield _ +@pytest_asyncio.fixture(scope="function") +async def simulation(bt): + async for _ in setup_full_system(test_constants_modified, bt, db_version=1): + yield _ + +class TestSimulation: @pytest.mark.asyncio async def test_simulation_1(self, simulation, extra_node, self_hostname): node1, node2, _, _, _, _, _, _, _, sanitizer_server, server1 = simulation diff --git a/tests/wallet/cat_wallet/test_cat_lifecycle.py b/tests/wallet/cat_wallet/test_cat_lifecycle.py index fa1f708613a3..c940c1b419c4 100644 --- a/tests/wallet/cat_wallet/test_cat_lifecycle.py +++ b/tests/wallet/cat_wallet/test_cat_lifecycle.py @@ -36,16 +36,17 @@ NO_LINEAGE_PROOF = LineageProof() +@pytest_asyncio.fixture(scope="function") +async def setup_sim(): + sim = await SpendSim.create() + sim_client = SimClient(sim) + await sim.farm_block() + return sim, sim_client + + class TestCATLifecycle: cost: Dict[str, int] = {} - @pytest_asyncio.fixture(scope="function") - async def setup_sim(self): - sim = await SpendSim.create() - sim_client = SimClient(sim) - await sim.farm_block() - return sim, sim_client - async def do_spend( self, sim: SpendSim, diff --git a/tests/wallet/cat_wallet/test_cat_wallet.py b/tests/wallet/cat_wallet/test_cat_wallet.py index 99c5acbfd6f4..74179a2ceff1 100644 --- a/tests/wallet/cat_wallet/test_cat_wallet.py +++ b/tests/wallet/cat_wallet/test_cat_wallet.py @@ -30,22 +30,25 @@ async def tx_in_pool(mempool: MempoolManager, tx_id: bytes32): return True -class TestCATWallet: - @pytest_asyncio.fixture(scope="function") - async def wallet_node(self): - async for _ in setup_simulators_and_wallets(1, 1, {}): - yield _ +@pytest_asyncio.fixture(scope="function") +async def wallet_node(): + async for _ in setup_simulators_and_wallets(1, 1, {}): + yield _ + + +@pytest_asyncio.fixture(scope="function") +async def two_wallet_nodes(): + async for _ in setup_simulators_and_wallets(1, 2, {}): + yield _ - @pytest_asyncio.fixture(scope="function") - async def two_wallet_nodes(self): - async for _ in setup_simulators_and_wallets(1, 2, {}): - yield _ - @pytest_asyncio.fixture(scope="function") - async def three_wallet_nodes(self): - async for _ in setup_simulators_and_wallets(1, 3, {}): - yield _ +@pytest_asyncio.fixture(scope="function") +async def three_wallet_nodes(): + async for _ in setup_simulators_and_wallets(1, 3, {}): + yield _ + +class TestCATWallet: @pytest.mark.parametrize( "trusted", [True, False], diff --git a/tests/wallet/cat_wallet/test_offer_lifecycle.py b/tests/wallet/cat_wallet/test_offer_lifecycle.py index b4d7542a00ea..7f1e9272e39c 100644 --- a/tests/wallet/cat_wallet/test_offer_lifecycle.py +++ b/tests/wallet/cat_wallet/test_offer_lifecycle.py @@ -41,139 +41,138 @@ def str_to_cat_hash(tail_str: str) -> bytes32: return construct_cat_puzzle(CAT_MOD, str_to_tail_hash(tail_str), acs).get_tree_hash() -class TestOfferLifecycle: - cost: Dict[str, int] = {} - - @pytest_asyncio.fixture(scope="function") - async def setup_sim(self): - sim = await SpendSim.create() - sim_client = SimClient(sim) - await sim.farm_block() - return sim, sim_client - - # This method takes a dictionary of strings mapping to amounts and generates the appropriate CAT/XCH coins - async def generate_coins( - self, - sim, - sim_client, - requested_coins: Dict[Optional[str], List[uint64]], - ) -> Dict[Optional[str], List[Coin]]: - await sim.farm_block(acs_ph) - parent_coin: Coin = [cr.coin for cr in await (sim_client.get_coin_records_by_puzzle_hash(acs_ph))][0] - - # We need to gather a list of initial coins to create as well as spends that do the eve spend for every CAT - payments: List[Payment] = [] - cat_bundles: List[SpendBundle] = [] - for tail_str, amounts in requested_coins.items(): - for amount in amounts: - if tail_str: - tail: Program = str_to_tail(tail_str) # Making a fake but unique TAIL - cat_puzzle: Program = construct_cat_puzzle(CAT_MOD, tail.get_tree_hash(), acs) - payments.append(Payment(cat_puzzle.get_tree_hash(), amount, [])) - cat_bundles.append( - unsigned_spend_bundle_for_spendable_cats( - CAT_MOD, - [ - SpendableCAT( - Coin(parent_coin.name(), cat_puzzle.get_tree_hash(), amount), - tail.get_tree_hash(), - acs, - Program.to([[51, acs_ph, amount], [51, 0, -113, tail, []]]), - ) - ], - ) +@pytest_asyncio.fixture(scope="function") +async def setup_sim(): + sim = await SpendSim.create() + sim_client = SimClient(sim) + await sim.farm_block() + return sim, sim_client + + +# This method takes a dictionary of strings mapping to amounts and generates the appropriate CAT/XCH coins +async def generate_coins( + sim, + sim_client, + requested_coins: Dict[Optional[str], List[uint64]], +) -> Dict[Optional[str], List[Coin]]: + await sim.farm_block(acs_ph) + parent_coin: Coin = [cr.coin for cr in await (sim_client.get_coin_records_by_puzzle_hash(acs_ph))][0] + + # We need to gather a list of initial coins to create as well as spends that do the eve spend for every CAT + payments: List[Payment] = [] + cat_bundles: List[SpendBundle] = [] + for tail_str, amounts in requested_coins.items(): + for amount in amounts: + if tail_str: + tail: Program = str_to_tail(tail_str) # Making a fake but unique TAIL + cat_puzzle: Program = construct_cat_puzzle(CAT_MOD, tail.get_tree_hash(), acs) + payments.append(Payment(cat_puzzle.get_tree_hash(), amount, [])) + cat_bundles.append( + unsigned_spend_bundle_for_spendable_cats( + CAT_MOD, + [ + SpendableCAT( + Coin(parent_coin.name(), cat_puzzle.get_tree_hash(), amount), + tail.get_tree_hash(), + acs, + Program.to([[51, acs_ph, amount], [51, 0, -113, tail, []]]), + ) + ], ) - else: - payments.append(Payment(acs_ph, amount, [])) + ) + else: + payments.append(Payment(acs_ph, amount, [])) + + # This bundle creates all of the initial coins + parent_bundle = SpendBundle( + [ + CoinSpend( + parent_coin, + acs, + Program.to([[51, p.puzzle_hash, p.amount] for p in payments]), + ) + ], + G2Element(), + ) + + # Then we aggregate it with all of the eve spends + await sim_client.push_tx(SpendBundle.aggregate([parent_bundle, *cat_bundles])) + await sim.farm_block() + + # Search for all of the coins and put them into a dictionary + coin_dict: Dict[Optional[str], List[Coin]] = {} + for tail_str, _ in requested_coins.items(): + if tail_str: + tail_hash: bytes32 = str_to_tail_hash(tail_str) + cat_ph: bytes32 = construct_cat_puzzle(CAT_MOD, tail_hash, acs).get_tree_hash() + coin_dict[tail_str] = [ + cr.coin for cr in await (sim_client.get_coin_records_by_puzzle_hash(cat_ph, include_spent_coins=False)) + ] + else: + coin_dict[None] = list( + filter( + lambda c: c.amount < 250000000000, + [ + cr.coin + for cr in await (sim_client.get_coin_records_by_puzzle_hash(acs_ph, include_spent_coins=False)) + ], + ) + ) - # This bundle creates all of the initial coins - parent_bundle = SpendBundle( + return coin_dict + + +# `generate_secure_bundle` simulates a wallet's `generate_signed_transaction` +# but doesn't bother with non-offer announcements +def generate_secure_bundle( + selected_coins: List[Coin], + announcements: List[Announcement], + offered_amount: uint64, + tail_str: Optional[str] = None, +) -> SpendBundle: + announcement_assertions: List[List] = [[63, a.name()] for a in announcements] + selected_coin_amount: int = sum([c.amount for c in selected_coins]) + non_primaries: List[Coin] = [] if len(selected_coins) < 2 else selected_coins[1:] + inner_solution: List[List] = [ + [51, Offer.ph(), offered_amount], # Offered coin + [51, acs_ph, uint64(selected_coin_amount - offered_amount)], # Change + *announcement_assertions, + ] + + if tail_str is None: + bundle = SpendBundle( [ CoinSpend( - parent_coin, + selected_coins[0], acs, - Program.to([[51, p.puzzle_hash, p.amount] for p in payments]), - ) + Program.to(inner_solution), + ), + *[CoinSpend(c, acs, Program.to([])) for c in non_primaries], ], G2Element(), ) - - # Then we aggregate it with all of the eve spends - await sim_client.push_tx(SpendBundle.aggregate([parent_bundle, *cat_bundles])) - await sim.farm_block() - - # Search for all of the coins and put them into a dictionary - coin_dict: Dict[Optional[str], List[Coin]] = {} - for tail_str, _ in requested_coins.items(): - if tail_str: - tail_hash: bytes32 = str_to_tail_hash(tail_str) - cat_ph: bytes32 = construct_cat_puzzle(CAT_MOD, tail_hash, acs).get_tree_hash() - coin_dict[tail_str] = [ - cr.coin - for cr in await (sim_client.get_coin_records_by_puzzle_hash(cat_ph, include_spent_coins=False)) - ] - else: - coin_dict[None] = list( - filter( - lambda c: c.amount < 250000000000, - [ - cr.coin - for cr in await ( - sim_client.get_coin_records_by_puzzle_hash(acs_ph, include_spent_coins=False) - ) - ], - ) - ) - - return coin_dict - - # This method simulates a wallet's `generate_signed_transaction` but doesn't bother with non-offer announcements - def generate_secure_bundle( - self, - selected_coins: List[Coin], - announcements: List[Announcement], - offered_amount: uint64, - tail_str: Optional[str] = None, - ) -> SpendBundle: - announcement_assertions: List[List] = [[63, a.name()] for a in announcements] - selected_coin_amount: int = sum([c.amount for c in selected_coins]) - non_primaries: List[Coin] = [] if len(selected_coins) < 2 else selected_coins[1:] - inner_solution: List[List] = [ - [51, Offer.ph(), offered_amount], # Offered coin - [51, acs_ph, uint64(selected_coin_amount - offered_amount)], # Change - *announcement_assertions, + else: + spendable_cats: List[SpendableCAT] = [ + SpendableCAT( + c, + str_to_tail_hash(tail_str), + acs, + Program.to( + [ + [51, 0, -113, str_to_tail(tail_str), Program.to([])], # Use the TAIL rather than lineage + *(inner_solution if c == selected_coins[0] else []), + ] + ), + ) + for c in selected_coins ] + bundle = unsigned_spend_bundle_for_spendable_cats(CAT_MOD, spendable_cats) - if tail_str is None: - bundle = SpendBundle( - [ - CoinSpend( - selected_coins[0], - acs, - Program.to(inner_solution), - ), - *[CoinSpend(c, acs, Program.to([])) for c in non_primaries], - ], - G2Element(), - ) - else: - spendable_cats: List[SpendableCAT] = [ - SpendableCAT( - c, - str_to_tail_hash(tail_str), - acs, - Program.to( - [ - [51, 0, -113, str_to_tail(tail_str), Program.to([])], # Use the TAIL rather than lineage - *(inner_solution if c == selected_coins[0] else []), - ] - ), - ) - for c in selected_coins - ] - bundle = unsigned_spend_bundle_for_spendable_cats(CAT_MOD, spendable_cats) + return bundle - return bundle + +class TestOfferLifecycle: + cost: Dict[str, int] = {} @pytest.mark.asyncio() async def test_complex_offer(self, setup_sim): @@ -185,7 +184,7 @@ async def test_complex_offer(self, setup_sim): "red": [250, 100], "blue": [3000], } - all_coins: Dict[Optional[str], List[Coin]] = await self.generate_coins(sim, sim_client, coins_needed) + all_coins: Dict[Optional[str], List[Coin]] = await generate_coins(sim, sim_client, coins_needed) chia_coins: List[Coin] = all_coins[None] red_coins: List[Coin] = all_coins["red"] blue_coins: List[Coin] = all_coins["blue"] @@ -202,7 +201,7 @@ async def test_complex_offer(self, setup_sim): chia_requested_payments, chia_coins ) chia_announcements: List[Announcement] = Offer.calculate_announcements(chia_requested_payments) - chia_secured_bundle: SpendBundle = self.generate_secure_bundle(chia_coins, chia_announcements, 1000) + chia_secured_bundle: SpendBundle = generate_secure_bundle(chia_coins, chia_announcements, 1000) chia_offer = Offer(chia_requested_payments, chia_secured_bundle) assert not chia_offer.is_valid() @@ -218,9 +217,7 @@ async def test_complex_offer(self, setup_sim): red_requested_payments, red_coins ) red_announcements: List[Announcement] = Offer.calculate_announcements(red_requested_payments) - red_secured_bundle: SpendBundle = self.generate_secure_bundle( - red_coins, red_announcements, 350, tail_str="red" - ) + red_secured_bundle: SpendBundle = generate_secure_bundle(red_coins, red_announcements, 350, tail_str="red") red_offer = Offer(red_requested_payments, red_secured_bundle) assert not red_offer.is_valid() @@ -244,7 +241,7 @@ async def test_complex_offer(self, setup_sim): blue_requested_payments, blue_coins ) blue_announcements: List[Announcement] = Offer.calculate_announcements(blue_requested_payments) - blue_secured_bundle: SpendBundle = self.generate_secure_bundle( + blue_secured_bundle: SpendBundle = generate_secure_bundle( blue_coins, blue_announcements, 2000, tail_str="blue" ) blue_offer = Offer(blue_requested_payments, blue_secured_bundle) diff --git a/tests/wallet/did_wallet/test_did.py b/tests/wallet/did_wallet/test_did.py index dc8f5f1a6d95..d15b2bacb898 100644 --- a/tests/wallet/did_wallet/test_did.py +++ b/tests/wallet/did_wallet/test_did.py @@ -21,32 +21,37 @@ def event_loop(): yield loop -class TestDIDWallet: - @pytest_asyncio.fixture(scope="function") - async def wallet_node(self): - async for _ in setup_simulators_and_wallets(1, 1, {}): - yield _ - - @pytest_asyncio.fixture(scope="function") - async def two_wallet_nodes(self): - async for _ in setup_simulators_and_wallets(1, 2, {}): - yield _ - - @pytest_asyncio.fixture(scope="function") - async def three_wallet_nodes(self): - async for _ in setup_simulators_and_wallets(1, 3, {}): - yield _ - - @pytest_asyncio.fixture(scope="function") - async def two_wallet_nodes_five_freeze(self): - async for _ in setup_simulators_and_wallets(1, 2, {}): - yield _ - - @pytest_asyncio.fixture(scope="function") - async def three_sim_two_wallets(self): - async for _ in setup_simulators_and_wallets(3, 2, {}): - yield _ +@pytest_asyncio.fixture(scope="function") +async def wallet_node(): + async for _ in setup_simulators_and_wallets(1, 1, {}): + yield _ + + +@pytest_asyncio.fixture(scope="function") +async def two_wallet_nodes(): + async for _ in setup_simulators_and_wallets(1, 2, {}): + yield _ + + +@pytest_asyncio.fixture(scope="function") +async def three_wallet_nodes(): + async for _ in setup_simulators_and_wallets(1, 3, {}): + yield _ + +@pytest_asyncio.fixture(scope="function") +async def two_wallet_nodes_five_freeze(): + async for _ in setup_simulators_and_wallets(1, 2, {}): + yield _ + + +@pytest_asyncio.fixture(scope="function") +async def three_sim_two_wallets(): + async for _ in setup_simulators_and_wallets(3, 2, {}): + yield _ + + +class TestDIDWallet: @pytest.mark.asyncio async def test_creation_from_backup_file(self, self_hostname, three_wallet_nodes): num_blocks = 5 diff --git a/tests/wallet/did_wallet/test_did_rpc.py b/tests/wallet/did_wallet/test_did_rpc.py index eba47e2d7ff0..328094d4b802 100644 --- a/tests/wallet/did_wallet/test_did_rpc.py +++ b/tests/wallet/did_wallet/test_did_rpc.py @@ -27,12 +27,13 @@ def event_loop(): yield loop -class TestDIDWallet: - @pytest_asyncio.fixture(scope="function") - async def three_wallet_nodes(self): - async for _ in setup_simulators_and_wallets(1, 3, {}): - yield _ +@pytest_asyncio.fixture(scope="function") +async def three_wallet_nodes(): + async for _ in setup_simulators_and_wallets(1, 3, {}): + yield _ + +class TestDIDWallet: @pytest.mark.asyncio async def test_create_did(self, bt, three_wallet_nodes, self_hostname): num_blocks = 4 diff --git a/tests/wallet/rl_wallet/test_rl_rpc.py b/tests/wallet/rl_wallet/test_rl_rpc.py index d0df6826ea65..686bbc1e6422 100644 --- a/tests/wallet/rl_wallet/test_rl_rpc.py +++ b/tests/wallet/rl_wallet/test_rl_rpc.py @@ -52,12 +52,13 @@ async def check_balance(api, wallet_id): return balance -class TestRLWallet: - @pytest_asyncio.fixture(scope="function") - async def three_wallet_nodes(self): - async for _ in setup_simulators_and_wallets(1, 3, {}): - yield _ +@pytest_asyncio.fixture(scope="function") +async def three_wallet_nodes(): + async for _ in setup_simulators_and_wallets(1, 3, {}): + yield _ + +class TestRLWallet: @pytest.mark.asyncio @pytest.mark.skip async def test_create_rl_coin(self, three_wallet_nodes, self_hostname): diff --git a/tests/wallet/rl_wallet/test_rl_wallet.py b/tests/wallet/rl_wallet/test_rl_wallet.py index bddc9d1bf590..c3226fe18299 100644 --- a/tests/wallet/rl_wallet/test_rl_wallet.py +++ b/tests/wallet/rl_wallet/test_rl_wallet.py @@ -17,12 +17,13 @@ def event_loop(): yield loop -class TestCATWallet: - @pytest_asyncio.fixture(scope="function") - async def two_wallet_nodes(self): - async for _ in setup_simulators_and_wallets(1, 2, {}): - yield _ +@pytest_asyncio.fixture(scope="function") +async def two_wallet_nodes(): + async for _ in setup_simulators_and_wallets(1, 2, {}): + yield _ + +class TestCATWallet: @pytest.mark.asyncio @pytest.mark.skip async def test_create_rl_coin(self, two_wallet_nodes, self_hostname): diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index cf7dec8144f6..8aa7ea32b383 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -42,12 +42,13 @@ log = logging.getLogger(__name__) -class TestWalletRpc: - @pytest_asyncio.fixture(scope="function") - async def two_wallet_nodes(self): - async for _ in setup_simulators_and_wallets(1, 2, {}): - yield _ +@pytest_asyncio.fixture(scope="function") +async def two_wallet_nodes(): + async for _ in setup_simulators_and_wallets(1, 2, {}): + yield _ + +class TestWalletRpc: @pytest.mark.parametrize( "trusted", [True, False], diff --git a/tests/wallet/simple_sync/test_simple_sync_protocol.py b/tests/wallet/simple_sync/test_simple_sync_protocol.py index 0ab1b2de6f18..b4a647cfce2f 100644 --- a/tests/wallet/simple_sync/test_simple_sync_protocol.py +++ b/tests/wallet/simple_sync/test_simple_sync_protocol.py @@ -47,25 +47,28 @@ def event_loop(): yield loop -class TestSimpleSyncProtocol: - @pytest_asyncio.fixture(scope="function") - async def wallet_node_simulator(self): - async for _ in setup_simulators_and_wallets(1, 1, {}): - yield _ - - @pytest_asyncio.fixture(scope="function") - async def wallet_two_node_simulator(self): - async for _ in setup_simulators_and_wallets(2, 1, {}): - yield _ - - async def get_all_messages_in_queue(self, queue): - all_messages = [] - await asyncio.sleep(2) - while not queue.empty(): - message, peer = await queue.get() - all_messages.append(message) - return all_messages +@pytest_asyncio.fixture(scope="function") +async def wallet_node_simulator(): + async for _ in setup_simulators_and_wallets(1, 1, {}): + yield _ + + +@pytest_asyncio.fixture(scope="function") +async def wallet_two_node_simulator(): + async for _ in setup_simulators_and_wallets(2, 1, {}): + yield _ + +async def get_all_messages_in_queue(queue): + all_messages = [] + await asyncio.sleep(2) + while not queue.empty(): + message, peer = await queue.get() + all_messages.append(message) + return all_messages + + +class TestSimpleSyncProtocol: @pytest.mark.asyncio async def test_subscribe_for_ph(self, wallet_node_simulator, self_hostname): num_blocks = 4 @@ -110,7 +113,7 @@ async def test_subscribe_for_ph(self, wallet_node_simulator, self_hostname): else: await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(zero_ph)) - all_messages = await self.get_all_messages_in_queue(incoming_queue) + all_messages = await get_all_messages_in_queue(incoming_queue) zero_coin = await full_node_api.full_node.coin_store.get_coin_states_by_puzzle_hashes(True, [zero_ph]) all_zero_coin = set(zero_coin) @@ -153,7 +156,7 @@ async def test_subscribe_for_ph(self, wallet_node_simulator, self_hostname): all_coins = set(zero_coins) all_coins.update(one_coins) - all_messages = await self.get_all_messages_in_queue(incoming_queue) + all_messages = await get_all_messages_in_queue(incoming_queue) notified_all_coins = set() @@ -239,7 +242,7 @@ async def test_subscribe_for_ph(self, wallet_node_simulator, self_hostname): for i in range(0, num_blocks): await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(puzzle_hash)) - all_messages = await self.get_all_messages_in_queue(incoming_queue) + all_messages = await get_all_messages_in_queue(incoming_queue) notified_state = None @@ -305,7 +308,7 @@ async def test_subscribe_for_coin_id(self, wallet_node_simulator, self_hostname) for i in range(0, num_blocks): await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(puzzle_hash)) - all_messages = await self.get_all_messages_in_queue(incoming_queue) + all_messages = await get_all_messages_in_queue(incoming_queue) notified_coins = set() for message in all_messages: @@ -346,7 +349,7 @@ async def test_subscribe_for_coin_id(self, wallet_node_simulator, self_hostname) for i in range(0, num_blocks): await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(puzzle_hash)) - all_messages = await self.get_all_messages_in_queue(incoming_queue) + all_messages = await get_all_messages_in_queue(incoming_queue) notified_state = None @@ -406,7 +409,7 @@ async def test_subscribe_for_ph_reorg(self, wallet_node_simulator, self_hostname coin_records = await full_node_api.full_node.coin_store.get_coin_records_by_puzzle_hash(True, puzzle_hash) assert coin_records == [] - all_messages = await self.get_all_messages_in_queue(incoming_queue) + all_messages = await get_all_messages_in_queue(incoming_queue) coin_update_messages = [] for message in all_messages: @@ -484,7 +487,7 @@ async def test_subscribe_for_coin_id_reorg(self, wallet_node_simulator, self_hos coin_records = await full_node_api.full_node.coin_store.get_coin_records_by_puzzle_hash(True, puzzle_hash) assert coin_records == [] - all_messages = await self.get_all_messages_in_queue(incoming_queue) + all_messages = await get_all_messages_in_queue(incoming_queue) coin_update_messages = [] for message in all_messages: @@ -554,7 +557,7 @@ async def test_subscribe_for_hint(self, bt, wallet_node_simulator, self_hostname for i in range(0, num_blocks): await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) - all_messages = await self.get_all_messages_in_queue(incoming_queue) + all_messages = await get_all_messages_in_queue(incoming_queue) notified_state = None @@ -645,8 +648,8 @@ async def test_subscribe_for_hint_long_sync(self, wallet_two_node_simulator, bt, node0_height = full_node_api.full_node.blockchain.get_peak_height() await time_out_assert(15, full_node_api_1.full_node.blockchain.get_peak_height, node0_height) - all_messages = await self.get_all_messages_in_queue(incoming_queue) - all_messages_1 = await self.get_all_messages_in_queue(incoming_queue_1) + all_messages = await get_all_messages_in_queue(incoming_queue) + all_messages_1 = await get_all_messages_in_queue(incoming_queue_1) def check_messages_for_hint(messages): notified_state = None diff --git a/tests/wallet/sync/test_wallet_sync.py b/tests/wallet/sync/test_wallet_sync.py index 340dd442018d..73c0aedb4d88 100644 --- a/tests/wallet/sync/test_wallet_sync.py +++ b/tests/wallet/sync/test_wallet_sync.py @@ -33,22 +33,25 @@ def event_loop(): yield loop -class TestWalletSync: - @pytest_asyncio.fixture(scope="function") - async def wallet_node(self, self_hostname): - async for _ in setup_node_and_wallet(test_constants, self_hostname): - yield _ +@pytest_asyncio.fixture(scope="function") +async def wallet_node(self_hostname): + async for _ in setup_node_and_wallet(test_constants, self_hostname): + yield _ + + +@pytest_asyncio.fixture(scope="function") +async def wallet_node_simulator(): + async for _ in setup_simulators_and_wallets(1, 1, {}): + yield _ - @pytest_asyncio.fixture(scope="function") - async def wallet_node_simulator(self): - async for _ in setup_simulators_and_wallets(1, 1, {}): - yield _ - @pytest_asyncio.fixture(scope="function") - async def wallet_node_starting_height(self, self_hostname): - async for _ in setup_node_and_wallet(test_constants, self_hostname, starting_height=100): - yield _ +@pytest_asyncio.fixture(scope="function") +async def wallet_node_starting_height(self_hostname): + async for _ in setup_node_and_wallet(test_constants, self_hostname, starting_height=100): + yield _ + +class TestWalletSync: @pytest.mark.parametrize( "trusted", [True, False], diff --git a/tests/wallet/test_wallet.py b/tests/wallet/test_wallet.py index 4902e759f3d9..62ff22020f63 100644 --- a/tests/wallet/test_wallet.py +++ b/tests/wallet/test_wallet.py @@ -21,32 +21,37 @@ from tests.wallet.cat_wallet.test_cat_wallet import tx_in_pool -class TestWalletSimulator: - @pytest_asyncio.fixture(scope="function") - async def wallet_node(self): - async for _ in setup_simulators_and_wallets(1, 1, {}, True): - yield _ - - @pytest_asyncio.fixture(scope="function") - async def wallet_node_100_pk(self): - async for _ in setup_simulators_and_wallets(1, 1, {}, initial_num_public_keys=100): - yield _ - - @pytest_asyncio.fixture(scope="function") - async def two_wallet_nodes(self): - async for _ in setup_simulators_and_wallets(1, 2, {}, True): - yield _ - - @pytest_asyncio.fixture(scope="function") - async def two_wallet_nodes_five_freeze(self): - async for _ in setup_simulators_and_wallets(1, 2, {}, True): - yield _ - - @pytest_asyncio.fixture(scope="function") - async def three_sim_two_wallets(self): - async for _ in setup_simulators_and_wallets(3, 2, {}, True): - yield _ +@pytest_asyncio.fixture(scope="function") +async def wallet_node(): + async for _ in setup_simulators_and_wallets(1, 1, {}, True): + yield _ + + +@pytest_asyncio.fixture(scope="function") +async def wallet_node_100_pk(): + async for _ in setup_simulators_and_wallets(1, 1, {}, initial_num_public_keys=100): + yield _ + + +@pytest_asyncio.fixture(scope="function") +async def two_wallet_nodes(): + async for _ in setup_simulators_and_wallets(1, 2, {}, True): + yield _ + +@pytest_asyncio.fixture(scope="function") +async def two_wallet_nodes_five_freeze(): + async for _ in setup_simulators_and_wallets(1, 2, {}, True): + yield _ + + +@pytest_asyncio.fixture(scope="function") +async def three_sim_two_wallets(): + async for _ in setup_simulators_and_wallets(3, 2, {}, True): + yield _ + + +class TestWalletSimulator: @pytest.mark.parametrize( "trusted", [True, False], diff --git a/tests/wallet/test_wallet_blockchain.py b/tests/wallet/test_wallet_blockchain.py index 1afc723d5daf..ef8c198a3aa6 100644 --- a/tests/wallet/test_wallet_blockchain.py +++ b/tests/wallet/test_wallet_blockchain.py @@ -23,12 +23,13 @@ def event_loop(): yield loop -class TestWalletBlockchain: - @pytest_asyncio.fixture(scope="function") - async def wallet_node(self, self_hostname): - async for _ in setup_node_and_wallet(test_constants, self_hostname): - yield _ +@pytest_asyncio.fixture(scope="function") +async def wallet_node(self_hostname): + async for _ in setup_node_and_wallet(test_constants, self_hostname): + yield _ + +class TestWalletBlockchain: @pytest.mark.asyncio async def test_wallet_blockchain(self, wallet_node, default_1000_blocks): full_node_api, wallet_node, full_node_server, wallet_server = wallet_node From 917fb5db4c1107fc96d68ce0df94476df47dbdb9 Mon Sep 17 00:00:00 2001 From: William Blanke Date: Fri, 11 Mar 2022 18:04:07 -0800 Subject: [PATCH 195/378] updated gui to cdfa2b98217fa8755c0da4f7409e6f90032c4c4c --- chia-blockchain-gui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia-blockchain-gui b/chia-blockchain-gui index 672cf2a74ade..cdfa2b98217f 160000 --- a/chia-blockchain-gui +++ b/chia-blockchain-gui @@ -1 +1 @@ -Subproject commit 672cf2a74ade67a868df232772bd6358bce8dedf +Subproject commit cdfa2b98217fa8755c0da4f7409e6f90032c4c4c From 184920a55c93dba0c7d648484764452a3379dc05 Mon Sep 17 00:00:00 2001 From: Jeff Date: Fri, 11 Mar 2022 18:48:45 -0800 Subject: [PATCH 196/378] Better management of KeyringWrapper's keys_root_path when using TempKeyring for tests (#10636) * Better management of KeyringWrapper's keys_root_path when using TempKeyring for tests. * Move keys_root_path restoration code into `cleanup()` Added an assert to detect if an unexpected shared KeyringWrapper is injected during a test. * Conditionally restore keys_root_path for testing --- tests/core/cmds/test_keys.py | 168 +++++++++++++++++++++++++---------- tests/util/keyring.py | 18 +++- 2 files changed, 140 insertions(+), 46 deletions(-) diff --git a/tests/core/cmds/test_keys.py b/tests/core/cmds/test_keys.py index 2cdedbda1d31..a874541df431 100644 --- a/tests/core/cmds/test_keys.py +++ b/tests/core/cmds/test_keys.py @@ -120,19 +120,23 @@ def test_generate_with_new_config(self, tmp_path, empty_keyring): the correct xch_target_address entries. """ + keychain = empty_keyring + keys_root_path = keychain.keyring_wrapper.keys_root_path + # Generate the new config runner = CliRunner() - init_result: Result = runner.invoke(cli, ["--root-path", os.fspath(tmp_path), "init"]) + init_result: Result = runner.invoke( + cli, ["--root-path", os.fspath(tmp_path), "--keys-root-path", os.fspath(keys_root_path), "init"] + ) assert init_result.exit_code == 0 - - keychain = empty_keyring - assert len(keychain.get_all_private_keys()) == 0 # Generate a new key runner = CliRunner() - result: Result = runner.invoke(cli, ["--root-path", os.fspath(tmp_path), "keys", "generate"]) + result: Result = runner.invoke( + cli, ["--root-path", os.fspath(tmp_path), "--keys-root-path", os.fspath(keys_root_path), "keys", "generate"] + ) assert result.exit_code == 0 assert len(keychain.get_all_private_keys()) == 1 @@ -152,19 +156,23 @@ def test_generate_with_existing_config(self, tmp_path, empty_keyring): the original xch_target_address entries. """ + keychain = empty_keyring + keys_root_path = keychain.keyring_wrapper.keys_root_path + # Generate the new config runner = CliRunner() - init_result: Result = runner.invoke(cli, ["--root-path", os.fspath(tmp_path), "init"]) + init_result: Result = runner.invoke( + cli, ["--root-path", os.fspath(tmp_path), "--keys-root-path", os.fspath(keys_root_path), "init"] + ) assert init_result.exit_code == 0 - - keychain = empty_keyring - assert len(keychain.get_all_private_keys()) == 0 # Generate the first key runner = CliRunner() - generate_result: Result = runner.invoke(cli, ["--root-path", os.fspath(tmp_path), "keys", "generate"]) + generate_result: Result = runner.invoke( + cli, ["--root-path", os.fspath(tmp_path), "--keys-root-path", os.fspath(keys_root_path), "keys", "generate"] + ) assert generate_result.exit_code == 0 assert len(keychain.get_all_private_keys()) == 1 @@ -180,7 +188,9 @@ def test_generate_with_existing_config(self, tmp_path, empty_keyring): # Generate the second key runner = CliRunner() - result: Result = runner.invoke(cli, ["--root-path", os.fspath(tmp_path), "keys", "generate"]) + result: Result = runner.invoke( + cli, ["--root-path", os.fspath(tmp_path), "--keys-root-path", os.fspath(keys_root_path), "keys", "generate"] + ) assert result.exit_code == 0 assert len(keychain.get_all_private_keys()) == 2 @@ -227,18 +237,22 @@ def test_add_interactive(self, tmp_path, empty_keyring): Test adding a key from mnemonic seed using the interactive prompt. """ + keychain = empty_keyring + keys_root_path = keychain.keyring_wrapper.keys_root_path + runner = CliRunner() - init_result: Result = runner.invoke(cli, ["--root-path", os.fspath(tmp_path), "init"]) + init_result: Result = runner.invoke( + cli, ["--root-path", os.fspath(tmp_path), "--keys-root-path", os.fspath(keys_root_path), "init"] + ) assert init_result.exit_code == 0 - - keychain = empty_keyring - assert len(keychain.get_all_private_keys()) == 0 runner = CliRunner() result: Result = runner.invoke( - cli, ["--root-path", os.fspath(tmp_path), "keys", "add"], input=f"{TEST_MNEMONIC_SEED}\n" + cli, + ["--root-path", os.fspath(tmp_path), "--keys-root-path", os.fspath(keys_root_path), "keys", "add"], + input=f"{TEST_MNEMONIC_SEED}\n", ) assert result.exit_code == 0 @@ -249,18 +263,30 @@ def test_add_from_mnemonic_seed(self, tmp_path, empty_keyring, mnemonic_seed_fil Test adding a key from a mnemonic seed file using the `--filename` flag. """ + keychain = empty_keyring + keys_root_path = keychain.keyring_wrapper.keys_root_path + runner = CliRunner() - init_result: Result = runner.invoke(cli, ["--root-path", os.fspath(tmp_path), "init"]) + init_result: Result = runner.invoke( + cli, ["--root-path", os.fspath(tmp_path), "--keys-root-path", os.fspath(keys_root_path), "init"] + ) assert init_result.exit_code == 0 - - keychain = empty_keyring - assert len(keychain.get_all_private_keys()) == 0 runner = CliRunner() result: Result = runner.invoke( - cli, ["--root-path", os.fspath(tmp_path), "keys", "add", "--filename", os.fspath(mnemonic_seed_file)] + cli, + [ + "--root-path", + os.fspath(tmp_path), + "--keys-root-path", + os.fspath(keys_root_path), + "keys", + "add", + "--filename", + os.fspath(mnemonic_seed_file), + ], ) assert result.exit_code == 0 @@ -271,18 +297,30 @@ def test_delete(self, tmp_path, empty_keyring, mnemonic_seed_file): Test deleting a key using the `--fingerprint` option. """ + keychain = empty_keyring + keys_root_path = keychain.keyring_wrapper.keys_root_path + runner = CliRunner() - init_result: Result = runner.invoke(cli, ["--root-path", os.fspath(tmp_path), "init"]) + init_result: Result = runner.invoke( + cli, ["--root-path", os.fspath(tmp_path), "--keys-root-path", os.fspath(keys_root_path), "init"] + ) assert init_result.exit_code == 0 - - keychain = empty_keyring - assert len(keychain.get_all_private_keys()) == 0 runner = CliRunner() add_result: Result = runner.invoke( - cli, ["--root-path", os.fspath(tmp_path), "keys", "add", "--filename", os.fspath(mnemonic_seed_file)] + cli, + [ + "--root-path", + os.fspath(tmp_path), + "--keys-root-path", + os.fspath(keys_root_path), + "keys", + "add", + "--filename", + os.fspath(mnemonic_seed_file), + ], ) assert add_result.exit_code == 0 @@ -290,7 +328,17 @@ def test_delete(self, tmp_path, empty_keyring, mnemonic_seed_file): runner = CliRunner() result: Result = runner.invoke( - cli, ["--root-path", os.fspath(tmp_path), "keys", "delete", "--fingerprint", TEST_FINGERPRINT] + cli, + [ + "--root-path", + os.fspath(tmp_path), + "--keys-root-path", + os.fspath(keys_root_path), + "keys", + "delete", + "--fingerprint", + TEST_FINGERPRINT, + ], ) assert result.exit_code == 0 @@ -459,12 +507,15 @@ def test_derive_search(self, tmp_path, keyring_with_one_key): Test the `chia keys derive search` command, searching a public and private key """ + keychain = keyring_with_one_key + keys_root_path = keychain.keyring_wrapper.keys_root_path + runner = CliRunner() - init_result: Result = runner.invoke(cli, ["--root-path", os.fspath(tmp_path), "init"]) + init_result: Result = runner.invoke( + cli, ["--root-path", os.fspath(tmp_path), "--keys-root-path", os.fspath(keys_root_path), "init"] + ) assert init_result.exit_code == 0 - - keychain = keyring_with_one_key assert len(keychain.get_all_private_keys()) == 1 runner = CliRunner() @@ -473,6 +524,8 @@ def test_derive_search(self, tmp_path, keyring_with_one_key): [ "--root-path", os.fspath(tmp_path), + "--keys-root-path", + os.fspath(keys_root_path), "keys", "derive", "--fingerprint", @@ -512,12 +565,15 @@ def test_derive_search_wallet_address(self, tmp_path, keyring_with_one_key): Test the `chia keys derive search` command, searching for a wallet address """ + keychain = keyring_with_one_key + keys_root_path = keychain.keyring_wrapper.keys_root_path + runner = CliRunner() - init_result: Result = runner.invoke(cli, ["--root-path", os.fspath(tmp_path), "init"]) + init_result: Result = runner.invoke( + cli, ["--root-path", os.fspath(tmp_path), "--keys-root-path", os.fspath(keys_root_path), "init"] + ) assert init_result.exit_code == 0 - - keychain = keyring_with_one_key assert len(keychain.get_all_private_keys()) == 1 runner = CliRunner() @@ -526,6 +582,8 @@ def test_derive_search_wallet_address(self, tmp_path, keyring_with_one_key): [ "--root-path", os.fspath(tmp_path), + "--keys-root-path", + os.fspath(keys_root_path), "keys", "derive", "--fingerprint", @@ -555,12 +613,15 @@ def test_derive_search_failure(self, tmp_path, keyring_with_one_key): Test the `chia keys derive search` command with a failing search. """ + keychain = keyring_with_one_key + keys_root_path = keychain.keyring_wrapper.keys_root_path + runner = CliRunner() - init_result: Result = runner.invoke(cli, ["--root-path", os.fspath(tmp_path), "init"]) + init_result: Result = runner.invoke( + cli, ["--root-path", os.fspath(tmp_path), "--keys-root-path", os.fspath(keys_root_path), "init"] + ) assert init_result.exit_code == 0 - - keychain = keyring_with_one_key assert len(keychain.get_all_private_keys()) == 1 runner = CliRunner() @@ -569,6 +630,8 @@ def test_derive_search_failure(self, tmp_path, keyring_with_one_key): [ "--root-path", os.fspath(tmp_path), + "--keys-root-path", + os.fspath(keys_root_path), "keys", "derive", "--fingerprint", @@ -589,12 +652,15 @@ def test_derive_search_hd_path(self, tmp_path, empty_keyring, mnemonic_seed_file Test the `chia keys derive search` command, searching under a provided HD path. """ + keychain = empty_keyring + keys_root_path = keychain.keyring_wrapper.keys_root_path + runner = CliRunner() - init_result: Result = runner.invoke(cli, ["--root-path", os.fspath(tmp_path), "init"]) + init_result: Result = runner.invoke( + cli, ["--root-path", os.fspath(tmp_path), "--keys-root-path", os.fspath(keys_root_path), "init"] + ) assert init_result.exit_code == 0 - - keychain = empty_keyring assert len(keychain.get_all_private_keys()) == 0 runner = CliRunner() @@ -603,6 +669,8 @@ def test_derive_search_hd_path(self, tmp_path, empty_keyring, mnemonic_seed_file [ "--root-path", os.fspath(tmp_path), + "--keys-root-path", + os.fspath(keys_root_path), "keys", "derive", "--mnemonic-seed-filename", @@ -634,12 +702,15 @@ def test_derive_wallet_address(self, tmp_path, keyring_with_one_key): Test the `chia keys derive wallet-address` command, generating a couple of wallet addresses. """ + keychain = keyring_with_one_key + keys_root_path = keychain.keyring_wrapper.keys_root_path + runner = CliRunner() - init_result: Result = runner.invoke(cli, ["--root-path", os.fspath(tmp_path), "init"]) + init_result: Result = runner.invoke( + cli, ["--root-path", os.fspath(tmp_path), "--keys-root-path", os.fspath(keys_root_path), "init"] + ) assert init_result.exit_code == 0 - - keychain = keyring_with_one_key assert len(keychain.get_all_private_keys()) == 1 runner = CliRunner() @@ -648,6 +719,8 @@ def test_derive_wallet_address(self, tmp_path, keyring_with_one_key): [ "--root-path", os.fspath(tmp_path), + "--keys-root-path", + os.fspath(keys_root_path), "keys", "derive", "--fingerprint", @@ -687,12 +760,15 @@ def test_derive_child_keys(self, tmp_path, keyring_with_one_key): Test the `chia keys derive child-keys` command, generating a couple of derived keys. """ + keychain = keyring_with_one_key + keys_root_path = keychain.keyring_wrapper.keys_root_path + runner = CliRunner() - init_result: Result = runner.invoke(cli, ["--root-path", os.fspath(tmp_path), "init"]) + init_result: Result = runner.invoke( + cli, ["--root-path", os.fspath(tmp_path), "--keys-root-path", os.fspath(keys_root_path), "init"] + ) assert init_result.exit_code == 0 - - keychain = keyring_with_one_key assert len(keychain.get_all_private_keys()) == 1 runner = CliRunner() @@ -701,6 +777,8 @@ def test_derive_child_keys(self, tmp_path, keyring_with_one_key): [ "--root-path", os.fspath(tmp_path), + "--keys-root-path", + os.fspath(keys_root_path), "keys", "derive", "--fingerprint", diff --git a/tests/util/keyring.py b/tests/util/keyring.py index e25d73df6986..3321d45e6f18 100644 --- a/tests/util/keyring.py +++ b/tests/util/keyring.py @@ -115,6 +115,7 @@ def __init__( use_os_credential_store=use_os_credential_store, setup_cryptfilekeyring=setup_cryptfilekeyring, ) + self.old_keys_root_path = None self.delete_on_cleanup = delete_on_cleanup self.cleaned_up = False @@ -181,7 +182,12 @@ def _patch_and_create_keychain( def __enter__(self): assert not self.cleaned_up - return self.get_keychain() + if KeyringWrapper.get_shared_instance(create_if_necessary=False) is not None: + self.old_keys_root_path = KeyringWrapper.get_shared_instance().keys_root_path + KeyringWrapper.cleanup_shared_instance() + kc = self.get_keychain() + KeyringWrapper.set_keys_root_path(kc.keyring_wrapper.keys_root_path) + return kc def __exit__(self, exc_type, exc_value, exc_tb): self.cleanup() @@ -192,6 +198,8 @@ def get_keychain(self): def cleanup(self): assert not self.cleaned_up + keys_root_path = self.keychain.keyring_wrapper.keys_root_path + if self.delete_on_cleanup: self.keychain.keyring_wrapper.keyring.cleanup_keyring_file_watcher() shutil.rmtree(self.keychain._temp_dir) @@ -203,4 +211,12 @@ def cleanup(self): self.keychain._mock_configure_legacy_backend_patch.stop() self.keychain._mock_data_root_patch.stop() + if self.old_keys_root_path is not None: + if KeyringWrapper.get_shared_instance(create_if_necessary=False) is not None: + shared_keys_root_path = KeyringWrapper.get_shared_instance().keys_root_path + if shared_keys_root_path == keys_root_path: + KeyringWrapper.cleanup_shared_instance() + KeyringWrapper.set_keys_root_path(self.old_keys_root_path) + KeyringWrapper.get_shared_instance() + self.cleaned_up = True From 19ee652d73ed34c2c6a43123cafc35d613017e42 Mon Sep 17 00:00:00 2001 From: Earle Lowe <30607889+emlowe@users.noreply.github.com> Date: Fri, 11 Mar 2022 18:52:00 -0800 Subject: [PATCH 197/378] better TLS1.3 check (#10552) * better TLS1.3 check * catch ValueError instead of Exception * Code simplification and cleanup * a few nits in comments --- chia/daemon/server.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/chia/daemon/server.py b/chia/daemon/server.py index 1216d66de4c1..1c032f57667b 100644 --- a/chia/daemon/server.py +++ b/chia/daemon/server.py @@ -156,18 +156,24 @@ def __init__( async def start(self): self.log.info("Starting Daemon Server") - if ssl.OPENSSL_VERSION_NUMBER < 0x10101000: + # Note: the minimum_version has been already set to TLSv1_2 + # in ssl_context_for_server() + # Daemon is internal connections, so override to TLSv1_3 only + if ssl.HAS_TLSv1_3: + try: + self.ssl_context.minimum_version = ssl.TLSVersion.TLSv1_3 + except ValueError: + # in case the attempt above confused the config, set it again (likely not needed but doesn't hurt) + self.ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2 + + if self.ssl_context.minimum_version is not ssl.TLSVersion.TLSv1_3: self.log.warning( ( - "Deprecation Warning: Your version of openssl (%s) does not support TLS1.3. " + "Deprecation Warning: Your version of SSL (%s) does not support TLS1.3. " "A future version of Chia will require TLS1.3." ), ssl.OPENSSL_VERSION, ) - else: - if self.ssl_context is not None: - # Daemon is internal connections, so override to TLS1.3 only - self.ssl_context.minimum_version = ssl.TLSVersion.TLSv1_3 def master_close_cb(): asyncio.create_task(self.stop()) From dbeff36ae0bc7e2411f378faec22d489d5a5fefd Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Sat, 12 Mar 2022 09:52:47 -0500 Subject: [PATCH 198/378] Add configuration locking (#10680) * Add configuration locking Extracted from https://github.com/Chia-Network/chia-blockchain/pull/10631 * note that fasteners will likely be replaced by filelock * Fix test_multiple_writers on macOS * create_all_ssl() doesn't need to be inside the config access lock * add warnings about not using async within get_config_lock() get lock contexts * no need to pre-touch the lock file * .yaml.lock instead of just .lock * test_multiple_writers() is sync * Revert "add warnings about not using async within get_config_lock() get lock contexts" This reverts commit 681af3835bbc8ab0ff6e1cca286d8b23fcbd0983. * reduce lock context size in chia_init() * use an exit stack in load_config() * avoid config existence precheck * only lock around the read in load_config() * do not raise e, just raise * tidy new imports * fix queue empty check in test_config.py * remove commented out code in test_config.py * remove unused import Co-authored-by: Jeff Cruikshank --- .pre-commit-config.yaml | 2 +- chia/cmds/configure.py | 309 +++++++++++++++++---------------- chia/cmds/db_upgrade_func.py | 13 +- chia/cmds/init_funcs.py | 179 ++++++++++--------- chia/farmer/farmer.py | 52 +++--- chia/plotting/util.py | 40 +++-- chia/pools/pool_config.py | 46 ++--- chia/util/config.py | 59 +++++-- chia/util/ssl_check.py | 2 +- setup.py | 3 +- tests/core/util/test_config.py | 141 ++++++++++++--- 11 files changed, 495 insertions(+), 351 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c0891660bd13..10f1ae5f0e73 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -57,7 +57,7 @@ repos: rev: v0.930 hooks: - id: mypy - additional_dependencies: [pytest, pytest-asyncio, types-aiofiles, types-click, types-setuptools, types-PyYAML] + additional_dependencies: [filelock, pytest, pytest-asyncio, types-aiofiles, types-click, types-setuptools, types-PyYAML] # This intentionally counters the settings in mypy.ini to allow a loose local # check and a strict CI check. This difference may or may not be retained long # term. diff --git a/chia/cmds/configure.py b/chia/cmds/configure.py index e54da32234cc..793207c45fb5 100644 --- a/chia/cmds/configure.py +++ b/chia/cmds/configure.py @@ -3,7 +3,7 @@ import click -from chia.util.config import load_config, save_config, str2bool +from chia.util.config import get_config_lock, load_config, save_config, str2bool from chia.util.default_root import DEFAULT_ROOT_PATH @@ -24,172 +24,173 @@ def configure( seeder_domain_name: str, seeder_nameserver: str, ): - config: Dict = load_config(DEFAULT_ROOT_PATH, "config.yaml") - change_made = False - if set_node_introducer: - try: - if set_node_introducer.index(":"): - host, port = ( - ":".join(set_node_introducer.split(":")[:-1]), - set_node_introducer.split(":")[-1], - ) - config["full_node"]["introducer_peer"]["host"] = host - config["full_node"]["introducer_peer"]["port"] = int(port) - config["introducer"]["port"] = int(port) - print("Node introducer updated") - change_made = True - except ValueError: - print("Node introducer address must be in format [IP:Port]") - if set_farmer_peer: - try: - if set_farmer_peer.index(":"): - host, port = ( - ":".join(set_farmer_peer.split(":")[:-1]), - set_farmer_peer.split(":")[-1], - ) - config["full_node"]["farmer_peer"]["host"] = host - config["full_node"]["farmer_peer"]["port"] = int(port) - config["harvester"]["farmer_peer"]["host"] = host - config["harvester"]["farmer_peer"]["port"] = int(port) - print("Farmer peer updated, make sure your harvester has the proper cert installed") + with get_config_lock(root_path, "config.yaml"): + config: Dict = load_config(DEFAULT_ROOT_PATH, "config.yaml", acquire_lock=False) + change_made = False + if set_node_introducer: + try: + if set_node_introducer.index(":"): + host, port = ( + ":".join(set_node_introducer.split(":")[:-1]), + set_node_introducer.split(":")[-1], + ) + config["full_node"]["introducer_peer"]["host"] = host + config["full_node"]["introducer_peer"]["port"] = int(port) + config["introducer"]["port"] = int(port) + print("Node introducer updated") + change_made = True + except ValueError: + print("Node introducer address must be in format [IP:Port]") + if set_farmer_peer: + try: + if set_farmer_peer.index(":"): + host, port = ( + ":".join(set_farmer_peer.split(":")[:-1]), + set_farmer_peer.split(":")[-1], + ) + config["full_node"]["farmer_peer"]["host"] = host + config["full_node"]["farmer_peer"]["port"] = int(port) + config["harvester"]["farmer_peer"]["host"] = host + config["harvester"]["farmer_peer"]["port"] = int(port) + print("Farmer peer updated, make sure your harvester has the proper cert installed") + change_made = True + except ValueError: + print("Farmer address must be in format [IP:Port]") + if set_fullnode_port: + config["full_node"]["port"] = int(set_fullnode_port) + config["full_node"]["introducer_peer"]["port"] = int(set_fullnode_port) + config["farmer"]["full_node_peer"]["port"] = int(set_fullnode_port) + config["timelord"]["full_node_peer"]["port"] = int(set_fullnode_port) + config["wallet"]["full_node_peer"]["port"] = int(set_fullnode_port) + config["wallet"]["introducer_peer"]["port"] = int(set_fullnode_port) + config["introducer"]["port"] = int(set_fullnode_port) + print("Default full node port updated") + change_made = True + if set_harvester_port: + config["harvester"]["port"] = int(set_harvester_port) + config["farmer"]["harvester_peer"]["port"] = int(set_harvester_port) + print("Default harvester port updated") + change_made = True + if set_log_level: + levels = ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "NOTSET"] + if set_log_level in levels: + config["logging"]["log_level"] = set_log_level + print(f"Logging level updated. Check {DEFAULT_ROOT_PATH}/log/debug.log") change_made = True - except ValueError: - print("Farmer address must be in format [IP:Port]") - if set_fullnode_port: - config["full_node"]["port"] = int(set_fullnode_port) - config["full_node"]["introducer_peer"]["port"] = int(set_fullnode_port) - config["farmer"]["full_node_peer"]["port"] = int(set_fullnode_port) - config["timelord"]["full_node_peer"]["port"] = int(set_fullnode_port) - config["wallet"]["full_node_peer"]["port"] = int(set_fullnode_port) - config["wallet"]["introducer_peer"]["port"] = int(set_fullnode_port) - config["introducer"]["port"] = int(set_fullnode_port) - print("Default full node port updated") - change_made = True - if set_harvester_port: - config["harvester"]["port"] = int(set_harvester_port) - config["farmer"]["harvester_peer"]["port"] = int(set_harvester_port) - print("Default harvester port updated") - change_made = True - if set_log_level: - levels = ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "NOTSET"] - if set_log_level in levels: - config["logging"]["log_level"] = set_log_level - print(f"Logging level updated. Check {DEFAULT_ROOT_PATH}/log/debug.log") + else: + print(f"Logging level not updated. Use one of: {levels}") + if enable_upnp: + config["full_node"]["enable_upnp"] = str2bool(enable_upnp) + if str2bool(enable_upnp): + print("uPnP enabled") + else: + print("uPnP disabled") + change_made = True + if set_outbound_peer_count: + config["full_node"]["target_outbound_peer_count"] = int(set_outbound_peer_count) + print("Target outbound peer count updated") change_made = True - else: - print(f"Logging level not updated. Use one of: {levels}") - if enable_upnp: - config["full_node"]["enable_upnp"] = str2bool(enable_upnp) - if str2bool(enable_upnp): - print("uPnP enabled") - else: - print("uPnP disabled") - change_made = True - if set_outbound_peer_count: - config["full_node"]["target_outbound_peer_count"] = int(set_outbound_peer_count) - print("Target outbound peer count updated") - change_made = True - if set_peer_count: - config["full_node"]["target_peer_count"] = int(set_peer_count) - print("Target peer count updated") - change_made = True - if testnet: - if testnet == "true" or testnet == "t": - print("Setting Testnet") - testnet_port = "58444" - testnet_introducer = "introducer-testnet10.chia.net" - testnet_dns_introducer = "dns-introducer-testnet10.chia.net" - bootstrap_peers = ["testnet10-node.chia.net"] - testnet = "testnet10" - config["full_node"]["port"] = int(testnet_port) - config["full_node"]["introducer_peer"]["port"] = int(testnet_port) - config["farmer"]["full_node_peer"]["port"] = int(testnet_port) - config["timelord"]["full_node_peer"]["port"] = int(testnet_port) - config["wallet"]["full_node_peer"]["port"] = int(testnet_port) - config["wallet"]["introducer_peer"]["port"] = int(testnet_port) - config["introducer"]["port"] = int(testnet_port) - config["full_node"]["introducer_peer"]["host"] = testnet_introducer - config["full_node"]["dns_servers"] = [testnet_dns_introducer] - config["wallet"]["dns_servers"] = [testnet_dns_introducer] - config["selected_network"] = testnet - config["harvester"]["selected_network"] = testnet - config["pool"]["selected_network"] = testnet - config["farmer"]["selected_network"] = testnet - config["timelord"]["selected_network"] = testnet - config["full_node"]["selected_network"] = testnet - config["ui"]["selected_network"] = testnet - config["introducer"]["selected_network"] = testnet - config["wallet"]["selected_network"] = testnet + if set_peer_count: + config["full_node"]["target_peer_count"] = int(set_peer_count) + print("Target peer count updated") + change_made = True + if testnet: + if testnet == "true" or testnet == "t": + print("Setting Testnet") + testnet_port = "58444" + testnet_introducer = "introducer-testnet10.chia.net" + testnet_dns_introducer = "dns-introducer-testnet10.chia.net" + bootstrap_peers = ["testnet10-node.chia.net"] + testnet = "testnet10" + config["full_node"]["port"] = int(testnet_port) + config["full_node"]["introducer_peer"]["port"] = int(testnet_port) + config["farmer"]["full_node_peer"]["port"] = int(testnet_port) + config["timelord"]["full_node_peer"]["port"] = int(testnet_port) + config["wallet"]["full_node_peer"]["port"] = int(testnet_port) + config["wallet"]["introducer_peer"]["port"] = int(testnet_port) + config["introducer"]["port"] = int(testnet_port) + config["full_node"]["introducer_peer"]["host"] = testnet_introducer + config["full_node"]["dns_servers"] = [testnet_dns_introducer] + config["wallet"]["dns_servers"] = [testnet_dns_introducer] + config["selected_network"] = testnet + config["harvester"]["selected_network"] = testnet + config["pool"]["selected_network"] = testnet + config["farmer"]["selected_network"] = testnet + config["timelord"]["selected_network"] = testnet + config["full_node"]["selected_network"] = testnet + config["ui"]["selected_network"] = testnet + config["introducer"]["selected_network"] = testnet + config["wallet"]["selected_network"] = testnet - if "seeder" in config: - config["seeder"]["port"] = int(testnet_port) - config["seeder"]["other_peers_port"] = int(testnet_port) - config["seeder"]["selected_network"] = testnet - config["seeder"]["bootstrap_peers"] = bootstrap_peers + if "seeder" in config: + config["seeder"]["port"] = int(testnet_port) + config["seeder"]["other_peers_port"] = int(testnet_port) + config["seeder"]["selected_network"] = testnet + config["seeder"]["bootstrap_peers"] = bootstrap_peers - print("Default full node port, introducer and network setting updated") - change_made = True + print("Default full node port, introducer and network setting updated") + change_made = True - elif testnet == "false" or testnet == "f": - print("Setting Mainnet") - mainnet_port = "8444" - mainnet_introducer = "introducer.chia.net" - mainnet_dns_introducer = "dns-introducer.chia.net" - bootstrap_peers = ["node.chia.net"] - net = "mainnet" - config["full_node"]["port"] = int(mainnet_port) - config["full_node"]["introducer_peer"]["port"] = int(mainnet_port) - config["farmer"]["full_node_peer"]["port"] = int(mainnet_port) - config["timelord"]["full_node_peer"]["port"] = int(mainnet_port) - config["wallet"]["full_node_peer"]["port"] = int(mainnet_port) - config["wallet"]["introducer_peer"]["port"] = int(mainnet_port) - config["introducer"]["port"] = int(mainnet_port) - config["full_node"]["introducer_peer"]["host"] = mainnet_introducer - config["full_node"]["dns_servers"] = [mainnet_dns_introducer] - config["selected_network"] = net - config["harvester"]["selected_network"] = net - config["pool"]["selected_network"] = net - config["farmer"]["selected_network"] = net - config["timelord"]["selected_network"] = net - config["full_node"]["selected_network"] = net - config["ui"]["selected_network"] = net - config["introducer"]["selected_network"] = net - config["wallet"]["selected_network"] = net + elif testnet == "false" or testnet == "f": + print("Setting Mainnet") + mainnet_port = "8444" + mainnet_introducer = "introducer.chia.net" + mainnet_dns_introducer = "dns-introducer.chia.net" + bootstrap_peers = ["node.chia.net"] + net = "mainnet" + config["full_node"]["port"] = int(mainnet_port) + config["full_node"]["introducer_peer"]["port"] = int(mainnet_port) + config["farmer"]["full_node_peer"]["port"] = int(mainnet_port) + config["timelord"]["full_node_peer"]["port"] = int(mainnet_port) + config["wallet"]["full_node_peer"]["port"] = int(mainnet_port) + config["wallet"]["introducer_peer"]["port"] = int(mainnet_port) + config["introducer"]["port"] = int(mainnet_port) + config["full_node"]["introducer_peer"]["host"] = mainnet_introducer + config["full_node"]["dns_servers"] = [mainnet_dns_introducer] + config["selected_network"] = net + config["harvester"]["selected_network"] = net + config["pool"]["selected_network"] = net + config["farmer"]["selected_network"] = net + config["timelord"]["selected_network"] = net + config["full_node"]["selected_network"] = net + config["ui"]["selected_network"] = net + config["introducer"]["selected_network"] = net + config["wallet"]["selected_network"] = net - if "seeder" in config: - config["seeder"]["port"] = int(mainnet_port) - config["seeder"]["other_peers_port"] = int(mainnet_port) - config["seeder"]["selected_network"] = net - config["seeder"]["bootstrap_peers"] = bootstrap_peers + if "seeder" in config: + config["seeder"]["port"] = int(mainnet_port) + config["seeder"]["other_peers_port"] = int(mainnet_port) + config["seeder"]["selected_network"] = net + config["seeder"]["bootstrap_peers"] = bootstrap_peers - print("Default full node port, introducer and network setting updated") - change_made = True - else: - print("Please choose True or False") + print("Default full node port, introducer and network setting updated") + change_made = True + else: + print("Please choose True or False") - if peer_connect_timeout: - config["full_node"]["peer_connect_timeout"] = int(peer_connect_timeout) - change_made = True + if peer_connect_timeout: + config["full_node"]["peer_connect_timeout"] = int(peer_connect_timeout) + change_made = True - if crawler_db_path is not None and "seeder" in config: - config["seeder"]["crawler_db_path"] = crawler_db_path - change_made = True + if crawler_db_path is not None and "seeder" in config: + config["seeder"]["crawler_db_path"] = crawler_db_path + change_made = True - if crawler_minimum_version_count is not None and "seeder" in config: - config["seeder"]["minimum_version_count"] = crawler_minimum_version_count - change_made = True + if crawler_minimum_version_count is not None and "seeder" in config: + config["seeder"]["minimum_version_count"] = crawler_minimum_version_count + change_made = True - if seeder_domain_name is not None and "seeder" in config: - config["seeder"]["domain_name"] = seeder_domain_name - change_made = True + if seeder_domain_name is not None and "seeder" in config: + config["seeder"]["domain_name"] = seeder_domain_name + change_made = True - if seeder_nameserver is not None and "seeder" in config: - config["seeder"]["nameserver"] = seeder_nameserver - change_made = True + if seeder_nameserver is not None and "seeder" in config: + config["seeder"]["nameserver"] = seeder_nameserver + change_made = True - if change_made: - print("Restart any running chia services for changes to take effect") - save_config(root_path, "config.yaml", config) + if change_made: + print("Restart any running chia services for changes to take effect") + save_config(root_path, "config.yaml", config) @click.command("configure", short_help="Modify configuration") diff --git a/chia/cmds/db_upgrade_func.py b/chia/cmds/db_upgrade_func.py index 0a2f6a3a23ff..898c241d02cf 100644 --- a/chia/cmds/db_upgrade_func.py +++ b/chia/cmds/db_upgrade_func.py @@ -3,7 +3,7 @@ import sys from time import time -from chia.util.config import load_config, save_config +from chia.util.config import load_config, save_config, get_config_lock from chia.util.path import mkdir, path_from_root from chia.util.ints import uint32 from chia.types.blockchain_format.sized_bytes import bytes32 @@ -44,11 +44,12 @@ def db_upgrade_func( if update_config: print("updating config.yaml") - config = load_config(root_path, "config.yaml") - new_db_path = db_pattern.replace("_v1_", "_v2_") - config["full_node"]["database_path"] = new_db_path - print(f"database_path: {new_db_path}") - save_config(root_path, "config.yaml", config) + with get_config_lock(root_path, "config.yaml"): + config = load_config(root_path, "config.yaml", acquire_lock=False) + new_db_path = db_pattern.replace("_v1_", "_v2_") + config["full_node"]["database_path"] = new_db_path + print(f"database_path: {new_db_path}") + save_config(root_path, "config.yaml", config) print(f"\n\nLEAVING PREVIOUS DB FILE UNTOUCHED {in_db_path}\n") diff --git a/chia/cmds/init_funcs.py b/chia/cmds/init_funcs.py index 3fa6702432be..467df9143068 100644 --- a/chia/cmds/init_funcs.py +++ b/chia/cmds/init_funcs.py @@ -21,6 +21,7 @@ load_config, save_config, unflatten_properties, + get_config_lock, ) from chia.util.keychain import Keychain from chia.util.path import mkdir, path_from_root @@ -74,88 +75,90 @@ def check_keys(new_root: Path, keychain: Optional[Keychain] = None) -> None: print("No keys are present in the keychain. Generate them with 'chia keys generate'") return None - config: Dict = load_config(new_root, "config.yaml") - pool_child_pubkeys = [master_sk_to_pool_sk(sk).get_g1() for sk, _ in all_sks] - all_targets = [] - stop_searching_for_farmer = "xch_target_address" not in config["farmer"] - stop_searching_for_pool = "xch_target_address" not in config["pool"] - number_of_ph_to_search = 50 - selected = config["selected_network"] - prefix = config["network_overrides"]["config"][selected]["address_prefix"] - - intermediates = {} - for sk, _ in all_sks: - intermediates[bytes(sk)] = { - "observer": master_sk_to_wallet_sk_unhardened_intermediate(sk), - "non-observer": master_sk_to_wallet_sk_intermediate(sk), - } - - for i in range(number_of_ph_to_search): - if stop_searching_for_farmer and stop_searching_for_pool and i > 0: - break + with get_config_lock(new_root, "config.yaml"): + config: Dict = load_config(new_root, "config.yaml", acquire_lock=False) + pool_child_pubkeys = [master_sk_to_pool_sk(sk).get_g1() for sk, _ in all_sks] + all_targets = [] + stop_searching_for_farmer = "xch_target_address" not in config["farmer"] + stop_searching_for_pool = "xch_target_address" not in config["pool"] + number_of_ph_to_search = 50 + selected = config["selected_network"] + prefix = config["network_overrides"]["config"][selected]["address_prefix"] + + intermediates = {} for sk, _ in all_sks: - intermediate_n = intermediates[bytes(sk)]["non-observer"] - intermediate_o = intermediates[bytes(sk)]["observer"] - - all_targets.append( - encode_puzzle_hash( - create_puzzlehash_for_pk(_derive_path_unhardened(intermediate_o, [i]).get_g1()), prefix + intermediates[bytes(sk)] = { + "observer": master_sk_to_wallet_sk_unhardened_intermediate(sk), + "non-observer": master_sk_to_wallet_sk_intermediate(sk), + } + + for i in range(number_of_ph_to_search): + if stop_searching_for_farmer and stop_searching_for_pool and i > 0: + break + for sk, _ in all_sks: + intermediate_n = intermediates[bytes(sk)]["non-observer"] + intermediate_o = intermediates[bytes(sk)]["observer"] + + all_targets.append( + encode_puzzle_hash( + create_puzzlehash_for_pk(_derive_path_unhardened(intermediate_o, [i]).get_g1()), prefix + ) + ) + all_targets.append( + encode_puzzle_hash(create_puzzlehash_for_pk(_derive_path(intermediate_n, [i]).get_g1()), prefix) ) + if all_targets[-1] == config["farmer"].get("xch_target_address") or all_targets[-2] == config[ + "farmer" + ].get("xch_target_address"): + stop_searching_for_farmer = True + if all_targets[-1] == config["pool"].get("xch_target_address") or all_targets[-2] == config["pool"].get( + "xch_target_address" + ): + stop_searching_for_pool = True + + # Set the destinations, if necessary + updated_target: bool = False + if "xch_target_address" not in config["farmer"]: + print( + f"Setting the xch destination for the farmer reward (1/8 plus fees, solo and pooling)" + f" to {all_targets[0]}" ) - all_targets.append( - encode_puzzle_hash(create_puzzlehash_for_pk(_derive_path(intermediate_n, [i]).get_g1()), prefix) + config["farmer"]["xch_target_address"] = all_targets[0] + updated_target = True + elif config["farmer"]["xch_target_address"] not in all_targets: + print( + f"WARNING: using a farmer address which we might not have the private" + f" keys for. We searched the first {number_of_ph_to_search} addresses. Consider overriding " + f"{config['farmer']['xch_target_address']} with {all_targets[0]}" ) - if all_targets[-1] == config["farmer"].get("xch_target_address") or all_targets[-2] == config["farmer"].get( - "xch_target_address" - ): - stop_searching_for_farmer = True - if all_targets[-1] == config["pool"].get("xch_target_address") or all_targets[-2] == config["pool"].get( - "xch_target_address" - ): - stop_searching_for_pool = True - - # Set the destinations, if necessary - updated_target: bool = False - if "xch_target_address" not in config["farmer"]: - print( - f"Setting the xch destination for the farmer reward (1/8 plus fees, solo and pooling) to {all_targets[0]}" - ) - config["farmer"]["xch_target_address"] = all_targets[0] - updated_target = True - elif config["farmer"]["xch_target_address"] not in all_targets: - print( - f"WARNING: using a farmer address which we might not have the private" - f" keys for. We searched the first {number_of_ph_to_search} addresses. Consider overriding " - f"{config['farmer']['xch_target_address']} with {all_targets[0]}" - ) - if "pool" not in config: - config["pool"] = {} - if "xch_target_address" not in config["pool"]: - print(f"Setting the xch destination address for pool reward (7/8 for solo only) to {all_targets[0]}") - config["pool"]["xch_target_address"] = all_targets[0] - updated_target = True - elif config["pool"]["xch_target_address"] not in all_targets: - print( - f"WARNING: using a pool address which we might not have the private" - f" keys for. We searched the first {number_of_ph_to_search} addresses. Consider overriding " - f"{config['pool']['xch_target_address']} with {all_targets[0]}" - ) - if updated_target: - print( - f"To change the XCH destination addresses, edit the `xch_target_address` entries in" - f" {(new_root / 'config' / 'config.yaml').absolute()}." - ) + if "pool" not in config: + config["pool"] = {} + if "xch_target_address" not in config["pool"]: + print(f"Setting the xch destination address for pool reward (7/8 for solo only) to {all_targets[0]}") + config["pool"]["xch_target_address"] = all_targets[0] + updated_target = True + elif config["pool"]["xch_target_address"] not in all_targets: + print( + f"WARNING: using a pool address which we might not have the private" + f" keys for. We searched the first {number_of_ph_to_search} addresses. Consider overriding " + f"{config['pool']['xch_target_address']} with {all_targets[0]}" + ) + if updated_target: + print( + f"To change the XCH destination addresses, edit the `xch_target_address` entries in" + f" {(new_root / 'config' / 'config.yaml').absolute()}." + ) - # Set the pool pks in the farmer - pool_pubkeys_hex = set(bytes(pk).hex() for pk in pool_child_pubkeys) - if "pool_public_keys" in config["farmer"]: - for pk_hex in config["farmer"]["pool_public_keys"]: - # Add original ones in config - pool_pubkeys_hex.add(pk_hex) + # Set the pool pks in the farmer + pool_pubkeys_hex = set(bytes(pk).hex() for pk in pool_child_pubkeys) + if "pool_public_keys" in config["farmer"]: + for pk_hex in config["farmer"]["pool_public_keys"]: + # Add original ones in config + pool_pubkeys_hex.add(pk_hex) - config["farmer"]["pool_public_keys"] = pool_pubkeys_hex - save_config(new_root, "config.yaml", config) + config["farmer"]["pool_public_keys"] = pool_pubkeys_hex + save_config(new_root, "config.yaml", config) def copy_files_rec(old_path: Path, new_path: Path): @@ -193,13 +196,15 @@ def migrate_from( copy_files_rec(old_path, new_path) # update config yaml with new keys - config: Dict = load_config(new_root, "config.yaml") - config_str: str = initial_config_file("config.yaml") - default_config: Dict = yaml.safe_load(config_str) - flattened_keys = unflatten_properties({k: "" for k in do_not_migrate_settings}) - dict_add_new_default(config, default_config, flattened_keys) - save_config(new_root, "config.yaml", config) + with get_config_lock(new_root, "config.yaml"): + config: Dict = load_config(new_root, "config.yaml", acquire_lock=False) + config_str: str = initial_config_file("config.yaml") + default_config: Dict = yaml.safe_load(config_str) + flattened_keys = unflatten_properties({k: "" for k in do_not_migrate_settings}) + dict_add_new_default(config, default_config, flattened_keys) + + save_config(new_root, "config.yaml", config) create_all_ssl(new_root) @@ -457,12 +462,14 @@ def chia_init( check_keys(root_path) config: Dict + if v1_db: - config = load_config(root_path, "config.yaml") - db_pattern = config["full_node"]["database_path"] - new_db_path = db_pattern.replace("_v2_", "_v1_") - config["full_node"]["database_path"] = new_db_path - save_config(root_path, "config.yaml", config) + with get_config_lock(root_path, "config.yaml"): + config = load_config(root_path, "config.yaml", acquire_lock=False) + db_pattern = config["full_node"]["database_path"] + new_db_path = db_pattern.replace("_v2_", "_v1_") + config["full_node"]["database_path"] = new_db_path + save_config(root_path, "config.yaml", config) else: config = load_config(root_path, "config.yaml")["full_node"] db_path_replaced: str = config["database_path"].replace("CHALLENGE", config["selected_network"]) diff --git a/chia/farmer/farmer.py b/chia/farmer/farmer.py index 43ffeba13611..1a63befe07d0 100644 --- a/chia/farmer/farmer.py +++ b/chia/farmer/farmer.py @@ -40,7 +40,7 @@ from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.bech32m import decode_puzzle_hash from chia.util.byte_types import hexstr_to_bytes -from chia.util.config import load_config, save_config, config_path_for_filename +from chia.util.config import load_config, save_config, config_path_for_filename, get_config_lock from chia.util.hash import std_hash from chia.util.ints import uint8, uint16, uint32, uint64 from chia.util.keychain import Keychain @@ -581,34 +581,36 @@ async def get_reward_targets(self, search_for_private_key: bool) -> Dict: } def set_reward_targets(self, farmer_target_encoded: Optional[str], pool_target_encoded: Optional[str]): - config = load_config(self._root_path, "config.yaml") - if farmer_target_encoded is not None: - self.farmer_target_encoded = farmer_target_encoded - self.farmer_target = decode_puzzle_hash(farmer_target_encoded) - config["farmer"]["xch_target_address"] = farmer_target_encoded - if pool_target_encoded is not None: - self.pool_target_encoded = pool_target_encoded - self.pool_target = decode_puzzle_hash(pool_target_encoded) - config["pool"]["xch_target_address"] = pool_target_encoded - save_config(self._root_path, "config.yaml", config) + with get_config_lock(self._root_path, "config.yaml"): + config = load_config(self._root_path, "config.yaml", acquire_lock=False) + if farmer_target_encoded is not None: + self.farmer_target_encoded = farmer_target_encoded + self.farmer_target = decode_puzzle_hash(farmer_target_encoded) + config["farmer"]["xch_target_address"] = farmer_target_encoded + if pool_target_encoded is not None: + self.pool_target_encoded = pool_target_encoded + self.pool_target = decode_puzzle_hash(pool_target_encoded) + config["pool"]["xch_target_address"] = pool_target_encoded + save_config(self._root_path, "config.yaml", config) async def set_payout_instructions(self, launcher_id: bytes32, payout_instructions: str): for p2_singleton_puzzle_hash, pool_state_dict in self.pool_state.items(): if launcher_id == pool_state_dict["pool_config"].launcher_id: - config = load_config(self._root_path, "config.yaml") - new_list = [] - pool_list = config["pool"].get("pool_list", []) - if pool_list is not None: - for list_element in pool_list: - if hexstr_to_bytes(list_element["launcher_id"]) == bytes(launcher_id): - list_element["payout_instructions"] = payout_instructions - new_list.append(list_element) - - config["pool"]["pool_list"] = new_list - save_config(self._root_path, "config.yaml", config) - # Force a GET /farmer which triggers the PUT /farmer if it detects the changed instructions - pool_state_dict["next_farmer_update"] = 0 - return + with get_config_lock(self._root_path, "config.yaml"): + config = load_config(self._root_path, "config.yaml", acquire_lock=False) + new_list = [] + pool_list = config["pool"].get("pool_list", []) + if pool_list is not None: + for list_element in pool_list: + if hexstr_to_bytes(list_element["launcher_id"]) == bytes(launcher_id): + list_element["payout_instructions"] = payout_instructions + new_list.append(list_element) + + config["pool"]["pool_list"] = new_list + save_config(self._root_path, "config.yaml", config) + # Force a GET /farmer which triggers the PUT /farmer if it detects the changed instructions + pool_state_dict["next_farmer_update"] = 0 + return self.log.warning(f"Launcher id: {launcher_id} not found") diff --git a/chia/plotting/util.py b/chia/plotting/util.py index 18617465fe43..64852201c3da 100644 --- a/chia/plotting/util.py +++ b/chia/plotting/util.py @@ -9,7 +9,7 @@ from chiapos import DiskProver from chia.types.blockchain_format.sized_bytes import bytes32 -from chia.util.config import load_config, save_config +from chia.util.config import load_config, save_config, get_config_lock log = logging.getLogger(__name__) @@ -79,28 +79,30 @@ def get_plot_filenames(root_path: Path) -> Dict[Path, List[Path]]: def add_plot_directory(root_path: Path, str_path: str) -> Dict: log.debug(f"add_plot_directory {str_path}") - config = load_config(root_path, "config.yaml") - if str(Path(str_path).resolve()) not in get_plot_directories(root_path, config): - config["harvester"]["plot_directories"].append(str(Path(str_path).resolve())) - save_config(root_path, "config.yaml", config) - return config + with get_config_lock(root_path, "config.yaml"): + config = load_config(root_path, "config.yaml", acquire_lock=False) + if str(Path(str_path).resolve()) not in get_plot_directories(root_path, config): + config["harvester"]["plot_directories"].append(str(Path(str_path).resolve())) + save_config(root_path, "config.yaml", config) + return config def remove_plot_directory(root_path: Path, str_path: str) -> None: log.debug(f"remove_plot_directory {str_path}") - config = load_config(root_path, "config.yaml") - str_paths: List[str] = get_plot_directories(root_path, config) - # If path str matches exactly, remove - if str_path in str_paths: - str_paths.remove(str_path) - - # If path matches full path, remove - new_paths = [Path(sp).resolve() for sp in str_paths] - if Path(str_path).resolve() in new_paths: - new_paths.remove(Path(str_path).resolve()) - - config["harvester"]["plot_directories"] = [str(np) for np in new_paths] - save_config(root_path, "config.yaml", config) + with get_config_lock(root_path, "config.yaml"): + config = load_config(root_path, "config.yaml", acquire_lock=False) + str_paths: List[str] = get_plot_directories(root_path, config) + # If path str matches exactly, remove + if str_path in str_paths: + str_paths.remove(str_path) + + # If path matches full path, remove + new_paths = [Path(sp).resolve() for sp in str_paths] + if Path(str_path).resolve() in new_paths: + new_paths.remove(Path(str_path).resolve()) + + config["harvester"]["plot_directories"] = [str(np) for np in new_paths] + save_config(root_path, "config.yaml", config) def remove_plot(path: Path): diff --git a/chia/pools/pool_config.py b/chia/pools/pool_config.py index 07d2bd05f065..d9d15733872b 100644 --- a/chia/pools/pool_config.py +++ b/chia/pools/pool_config.py @@ -7,7 +7,7 @@ from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.byte_types import hexstr_to_bytes -from chia.util.config import load_config, save_config +from chia.util.config import get_config_lock, load_config, save_config from chia.util.streamable import Streamable, streamable """ @@ -61,28 +61,28 @@ def load_pool_config(root_path: Path) -> List[PoolWalletConfig]: # TODO: remove this a few versions after 1.3, since authentication_public_key is deprecated. This is here to support # downgrading to versions older than 1.3. def add_auth_key(root_path: Path, config_entry: PoolWalletConfig, auth_key: G1Element): - config = load_config(root_path, "config.yaml") - pool_list = config["pool"].get("pool_list", []) - updated = False - if pool_list is not None: - for pool_config_dict in pool_list: - try: - if ( - G1Element.from_bytes(hexstr_to_bytes(pool_config_dict["owner_public_key"])) - == config_entry.owner_public_key - ): - auth_key_hex = bytes(auth_key).hex() - if pool_config_dict.get("authentication_public_key", "") != auth_key_hex: - pool_config_dict["authentication_public_key"] = auth_key_hex - updated = True - except Exception as e: - log.error(f"Exception updating config: {pool_config_dict} {e}") - if updated: - config["pool"]["pool_list"] = pool_list - save_config(root_path, "config.yaml", config) + with get_config_lock(root_path, "config.yaml"): + config = load_config(root_path, "config.yaml", acquire_lock=False) + pool_list = config["pool"].get("pool_list", []) + updated = False + if pool_list is not None: + for pool_config_dict in pool_list: + try: + if hexstr_to_bytes(pool_config_dict["owner_public_key"]) == bytes(config_entry.owner_public_key): + auth_key_hex = bytes(auth_key).hex() + if pool_config_dict.get("authentication_public_key", "") != auth_key_hex: + pool_config_dict["authentication_public_key"] = auth_key_hex + updated = True + except Exception as e: + log.error(f"Exception updating config: {pool_config_dict} {e}") + if updated: + log.info(f"Updating pool config for auth key: {auth_key}") + config["pool"]["pool_list"] = pool_list + save_config(root_path, "config.yaml", config) async def update_pool_config(root_path: Path, pool_config_list: List[PoolWalletConfig]): - full_config = load_config(root_path, "config.yaml") - full_config["pool"]["pool_list"] = [c.to_json_dict() for c in pool_config_list] - save_config(root_path, "config.yaml", full_config) + with get_config_lock(root_path, "config.yaml"): + full_config = load_config(root_path, "config.yaml", acquire_lock=False) + full_config["pool"]["pool_list"] = [c.to_json_dict() for c in pool_config_list] + save_config(root_path, "config.yaml", full_config) diff --git a/chia/util/config.py b/chia/util/config.py index 589d7383a49a..60c14a18a626 100644 --- a/chia/util/config.py +++ b/chia/util/config.py @@ -1,13 +1,18 @@ import argparse +import contextlib import logging import os import shutil import sys +import tempfile +import time +import traceback from pathlib import Path from typing import Any, Callable, Dict, Optional, Union import pkg_resources import yaml +from filelock import BaseFileLock, FileLock from typing_extensions import Literal from chia.util.path import mkdir @@ -15,6 +20,8 @@ PEER_DB_PATH_KEY_DEPRECATED = "peer_db_path" # replaced by "peers_file_path" WALLET_PEERS_PATH_KEY_DEPRECATED = "wallet_peers_path" # replaced by "wallet_peers_file_path" +log = logging.getLogger(__name__) + def initial_config_file(filename: Union[str, Path]) -> str: return pkg_resources.resource_string(__name__, f"initial-{filename}").decode() @@ -41,24 +48,36 @@ def config_path_for_filename(root_path: Path, filename: Union[str, Path]) -> Pat return root_path / "config" / filename +def get_config_lock(root_path: Path, filename: Union[str, Path]) -> BaseFileLock: + config_path = config_path_for_filename(root_path, filename) + lock_path: Path = config_path.with_name(config_path.name + ".lock") + return FileLock(lock_path) + + def save_config(root_path: Path, filename: Union[str, Path], config_data: Any): + # This must be called under an acquired config lock path: Path = config_path_for_filename(root_path, filename) - tmp_path: Path = path.with_suffix("." + str(os.getpid())) - with open(tmp_path, "w") as f: - yaml.safe_dump(config_data, f) - try: - os.replace(str(tmp_path), path) - except PermissionError: - shutil.move(str(tmp_path), str(path)) + with tempfile.TemporaryDirectory(dir=path.parent) as tmp_dir: + tmp_path: Path = Path(tmp_dir) / Path(filename) + with open(tmp_path, "w") as f: + yaml.safe_dump(config_data, f) + try: + os.replace(str(tmp_path), path) + except PermissionError: + shutil.move(str(tmp_path), str(path)) def load_config( root_path: Path, filename: Union[str, Path], sub_config: Optional[str] = None, - exit_on_error=True, + exit_on_error: bool = True, + acquire_lock: bool = True, ) -> Dict: + # This must be called under an acquired config lock, or acquire_lock should be True + path = config_path_for_filename(root_path, filename) + if not path.is_file(): if not exit_on_error: raise ValueError("Config not found") @@ -66,10 +85,26 @@ def load_config( print("** please run `chia init` to migrate or create new config files **") # TODO: fix this hack sys.exit(-1) - r = yaml.safe_load(open(path, "r")) - if sub_config is not None: - r = r.get(sub_config) - return r + # This loop should not be necessary due to the config lock, but it's kept here just in case + for i in range(10): + try: + with contextlib.ExitStack() as exit_stack: + if acquire_lock: + exit_stack.enter_context(get_config_lock(root_path, filename)) + with open(path, "r") as opened_config_file: + r = yaml.safe_load(opened_config_file) + if r is None: + log.error(f"yaml.safe_load returned None: {path}") + time.sleep(i * 0.1) + continue + if sub_config is not None: + r = r.get(sub_config) + return r + except Exception as e: + tb = traceback.format_exc() + log.error(f"Error loading file: {tb} {e} Retrying {i}") + time.sleep(i * 0.1) + raise RuntimeError("Was not able to read config file successfully") def load_config_cli(root_path: Path, filename: str, sub_config: Optional[str] = None) -> Dict: diff --git a/chia/util/ssl_check.py b/chia/util/ssl_check.py index b100bf393802..3e4c1184cca2 100644 --- a/chia/util/ssl_check.py +++ b/chia/util/ssl_check.py @@ -79,7 +79,7 @@ def get_all_ssl_file_paths(root_path: Path) -> Tuple[List[Path], List[Path]]: # Check the Mozilla Root CAs as well all_certs.append(Path(get_mozilla_ca_crt())) - except ValueError: + except (FileNotFoundError, ValueError): pass return all_certs, all_keys diff --git a/setup.py b/setup.py index 65b344b1d620..11b73f736964 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,8 @@ "colorlog==5.0.1", # Adds color to logs "concurrent-log-handler==0.9.19", # Concurrently log and rotate logs "cryptography==3.4.7", # Python cryptography library for TLS - keyring conflict - "fasteners==0.16.3", # For interprocess file locking + "fasteners==0.16.3", # For interprocess file locking, expected to be replaced by filelock + "filelock==3.4.2", # For reading and writing config multiprocess and multithread safely (non-reentrant locks) "keyring==23.0.1", # Store keys in MacOS Keychain, Windows Credential Locker "keyrings.cryptfile==1.3.4", # Secure storage for keys on Linux (Will be replaced) # "keyrings.cryptfile==1.3.8", # Secure storage for keys on Linux (Will be replaced) diff --git a/tests/core/util/test_config.py b/tests/core/util/test_config.py index c19c180d4506..41462067743f 100644 --- a/tests/core/util/test_config.py +++ b/tests/core/util/test_config.py @@ -1,16 +1,28 @@ import asyncio import copy +import shutil +import tempfile +from concurrent.futures import ProcessPoolExecutor + import pytest import random import yaml -from chia.util.config import create_default_chia_config, initial_config_file, load_config, save_config +from chia.util.config import ( + config_path_for_filename, + create_default_chia_config, + get_config_lock, + initial_config_file, + load_config, + save_config, +) from chia.util.path import mkdir -from multiprocessing import Pool, TimeoutError +from multiprocessing import Pool, Queue, TimeoutError from pathlib import Path from threading import Thread from time import sleep -from typing import Dict +from typing import Dict, Optional + # Commented-out lines are preserved to aid in debugging the multiprocessing tests # import logging @@ -20,47 +32,105 @@ # log = logging.getLogger(__name__) -def write_config(root_path: Path, config: Dict): +def write_config( + root_path: Path, + config: Dict, + atomic_write: bool, + do_sleep: bool, + iterations: int, + error_queue: Optional[Queue] = None, +): """ Wait for a random amount of time and write out the config data. With a large config, we expect save_config() to require multiple writes. """ - sleep(random.random()) - # log.warning(f"[pid:{os.getpid()}:{threading.get_ident()}] write_config") - # save_config(root_path=root_path, filename="config.yaml", config_data=modified_config) - save_config(root_path=root_path, filename="config.yaml", config_data=config) + try: + for i in range(iterations): + # This is a small sleep to get interweaving reads and writes + sleep(0.05) + if do_sleep: + sleep(random.random()) + if atomic_write: + # Note that this is usually atomic but in certain circumstances in Windows it can copy the file, + # leading to a non-atomic operation. + with get_config_lock(root_path, "config.yaml"): + save_config(root_path=root_path, filename="config.yaml", config_data=config) + else: + path: Path = config_path_for_filename(root_path, filename="config.yaml") + with get_config_lock(root_path, "config.yaml"): + with tempfile.TemporaryDirectory(dir=path.parent) as tmp_dir: + tmp_path: Path = Path(tmp_dir) / Path("config.yaml") + with open(tmp_path, "w") as f: + yaml.safe_dump(config, f) + shutil.copy2(str(tmp_path), str(path)) + except Exception as e: + if error_queue is not None: + error_queue.put(e) + raise -def read_and_compare_config(root_path: Path, default_config: Dict): + +def read_and_compare_config( + root_path: Path, default_config: Dict, do_sleep: bool, iterations: int, error_queue: Optional[Queue] = None +): """ Wait for a random amount of time, read the config and compare with the default config data. If the config file is partially-written or corrupt, load_config should fail or return bad data """ - # Wait a moment. The read and write threads are delayed by a random amount - # in an attempt to interleave their execution. - sleep(random.random()) - # log.warning(f"[pid:{os.getpid()}:{threading.get_ident()}] read_and_compare_config") - config: Dict = load_config(root_path=root_path, filename="config.yaml") - assert len(config) > 0 - # if config != default_config: - # log.error(f"[pid:{os.getpid()}:{threading.get_ident()}] bad config: {config}") - # log.error(f"[pid:{os.getpid()}:{threading.get_ident()}] default config: {default_config}") - assert config == default_config + try: + for i in range(iterations): + # This is a small sleep to get interweaving reads and writes + sleep(0.05) + + # Wait a moment. The read and write threads are delayed by a random amount + # in an attempt to interleave their execution. + if do_sleep: + sleep(random.random()) + + with get_config_lock(root_path, "config.yaml"): + config: Dict = load_config(root_path=root_path, filename="config.yaml", acquire_lock=False) + assert config == default_config + except Exception as e: + if error_queue is not None: + error_queue.put(e) + raise async def create_reader_and_writer_tasks(root_path: Path, default_config: Dict): """ Spin-off reader and writer threads and wait for completion """ - thread1 = Thread(target=write_config, kwargs={"root_path": root_path, "config": default_config}) - thread2 = Thread(target=read_and_compare_config, kwargs={"root_path": root_path, "default_config": default_config}) + error_queue: Queue = Queue() + thread1 = Thread( + target=write_config, + kwargs={ + "root_path": root_path, + "config": default_config, + "atomic_write": False, + "do_sleep": True, + "iterations": 1, + "error_queue": error_queue, + }, + ) + thread2 = Thread( + target=read_and_compare_config, + kwargs={ + "root_path": root_path, + "default_config": default_config, + "do_sleep": True, + "iterations": 1, + "error_queue": error_queue, + }, + ) thread1.start() thread2.start() thread1.join() thread2.join() + if not error_queue.empty(): + raise error_queue.get() def run_reader_and_writer_tasks(root_path: Path, default_config: Dict): @@ -197,8 +267,6 @@ def test_multiple_writers(self, root_path_populated_with_config, default_config_ """ Test whether multiple readers/writers encounter data corruption. When using non-atomic operations to write to the config, partial/incomplete writes can cause readers to yield bad/corrupt data. - Access to config.yaml isn't currently synchronized, so the best we can currently hope for is that - the file contents are written-to as a whole. """ # Artifically inflate the size of the default config. This is done to (hopefully) force # save_config() to require multiple writes. When save_config() was using shutil.move() @@ -216,3 +284,30 @@ def test_multiple_writers(self, root_path_populated_with_config, default_config_ res.get(timeout=60) except TimeoutError: pytest.skip("Timed out waiting for reader/writer processes to complete") + + @pytest.mark.asyncio + async def test_non_atomic_writes(self, root_path_populated_with_config, default_config_dict): + """ + Test whether one continuous writer (writing constantly, but not atomically) will interfere with many + concurrent readers. + """ + + default_config_dict["xyz"] = "x" * 32768 + root_path: Path = root_path_populated_with_config + save_config(root_path=root_path, filename="config.yaml", config_data=default_config_dict) + + with ProcessPoolExecutor(max_workers=4) as pool: + all_tasks = [] + for i in range(10): + all_tasks.append( + asyncio.get_running_loop().run_in_executor( + pool, read_and_compare_config, root_path, default_config_dict, False, 100, None + ) + ) + if i % 2 == 0: + all_tasks.append( + asyncio.get_running_loop().run_in_executor( + pool, write_config, root_path, default_config_dict, False, False, 100, None + ) + ) + await asyncio.gather(*all_tasks) From 773d692fc5a7ee539392c78902857c3c03e00560 Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Sat, 12 Mar 2022 19:40:52 +0100 Subject: [PATCH 199/378] fix usage of the bt fixture in conftest fixtures (#10688) --- tests/conftest.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 13bb572b302b..e8bd84bf9ad9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,7 +21,7 @@ def get_keychain(): @pytest.fixture(scope="session", name="bt") -def bt(get_keychain) -> BlockTools: +def block_tools_fixture(get_keychain) -> BlockTools: # Note that this causes a lot of CPU and disk traffic - disk, DB, ports, process creation ... _shared_block_tools = create_block_tools(constants=test_constants, keychain=get_keychain) return _shared_block_tools @@ -73,21 +73,21 @@ def softfork_height(request): @pytest.fixture(scope="session") -def default_400_blocks(): +def default_400_blocks(bt): from tests.util.blockchain import persistent_blocks return persistent_blocks(400, f"test_blocks_400_{block_format_version}.db", bt, seed=b"alternate2") @pytest.fixture(scope="session") -def default_1000_blocks(): +def default_1000_blocks(bt): from tests.util.blockchain import persistent_blocks return persistent_blocks(1000, f"test_blocks_1000_{block_format_version}.db", bt) @pytest.fixture(scope="session") -def pre_genesis_empty_slots_1000_blocks(): +def pre_genesis_empty_slots_1000_blocks(bt): from tests.util.blockchain import persistent_blocks return persistent_blocks( @@ -96,21 +96,21 @@ def pre_genesis_empty_slots_1000_blocks(): @pytest.fixture(scope="session") -def default_10000_blocks(): +def default_10000_blocks(bt): from tests.util.blockchain import persistent_blocks return persistent_blocks(10000, f"test_blocks_10000_{block_format_version}.db", bt) @pytest.fixture(scope="session") -def default_20000_blocks(): +def default_20000_blocks(bt): from tests.util.blockchain import persistent_blocks return persistent_blocks(20000, f"test_blocks_20000_{block_format_version}.db", bt) @pytest.fixture(scope="session") -def default_10000_blocks_compact(): +def default_10000_blocks_compact(bt): from tests.util.blockchain import persistent_blocks return persistent_blocks( From 4915477fcbf1156435b5387f60f63efec14b36e3 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Mon, 14 Mar 2022 09:08:05 -0400 Subject: [PATCH 200/378] bump pre-commit mypy to 0.940 (#10672) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 10f1ae5f0e73..1492ce452cc0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -54,7 +54,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.930 + rev: v0.940 hooks: - id: mypy additional_dependencies: [filelock, pytest, pytest-asyncio, types-aiofiles, types-click, types-setuptools, types-PyYAML] From c63324abe9e6734c4e5382c7da463e21573c9919 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Mon, 14 Mar 2022 09:08:35 -0400 Subject: [PATCH 201/378] remove some event_loop() fixtures (#10420) * remove event_loop() fixtures * flake8 * flake8 * remove sys.exit() from daemon shutdown * bump full node test timeout. a lot... to see. * fixup some tests * back to module scope event loop fixture for test_full_node.py * Update test_full_node.py * Iterator... * for the whole directory * some fixtures back to module scope for reduced runtime * back to 40 minute workflow timeouts * these are being addressed separately --- tests/blockchain/test_blockchain.py | 6 ------ tests/blockchain/test_blockchain_transactions.py | 7 ------- tests/core/full_node/conftest.py | 15 +++++++++++++++ tests/core/full_node/full_sync/test_full_sync.py | 6 ------ tests/core/full_node/stores/test_block_store.py | 6 ------ tests/core/full_node/stores/test_coin_store.py | 7 ------- .../core/full_node/stores/test_full_node_store.py | 6 ------ tests/core/full_node/stores/test_hint_store.py | 7 ------- tests/core/full_node/stores/test_sync_store.py | 8 -------- tests/core/full_node/test_address_manager.py | 7 ------- tests/core/full_node/test_full_node.py | 6 ------ tests/core/full_node/test_mempool.py | 14 -------------- tests/core/full_node/test_mempool_performance.py | 6 ------ tests/core/full_node/test_node_load.py | 7 ------- tests/core/full_node/test_performance.py | 6 ------ tests/core/full_node/test_transactions.py | 6 ------ tests/core/server/test_dos.py | 6 ------ tests/core/server/test_rate_limits.py | 6 ------ tests/core/test_cost_calculation.py | 7 ------- tests/core/test_db_conversion.py | 8 -------- tests/core/test_filter.py | 7 ------- tests/core/test_merkle_set.py | 7 ------- tests/pools/test_pool_rpc.py | 6 ------ tests/pools/test_wallet_pool_store.py | 7 ------- tests/util/test_lock_queue.py | 6 ------ tests/wallet/cat_wallet/test_trades.py | 6 ------ tests/wallet/did_wallet/test_did.py | 7 ------- tests/wallet/did_wallet/test_did_rpc.py | 7 ------- tests/wallet/rl_wallet/test_rl_rpc.py | 6 ------ tests/wallet/rl_wallet/test_rl_wallet.py | 8 -------- .../simple_sync/test_simple_sync_protocol.py | 6 ------ tests/wallet/sync/test_wallet_sync.py | 6 ------ tests/wallet/test_puzzle_store.py | 7 ------- tests/wallet/test_wallet_blockchain.py | 7 ------- tests/wallet/test_wallet_interested_store.py | 7 ------- tests/wallet/test_wallet_key_val_store.py | 7 ------- tests/weight_proof/test_weight_proof.py | 6 ------ 37 files changed, 15 insertions(+), 245 deletions(-) create mode 100644 tests/core/full_node/conftest.py diff --git a/tests/blockchain/test_blockchain.py b/tests/blockchain/test_blockchain.py index c9490261231e..b72d3a4607bf 100644 --- a/tests/blockchain/test_blockchain.py +++ b/tests/blockchain/test_blockchain.py @@ -62,12 +62,6 @@ bad_element = ClassgroupElement.from_bytes(b"\x00") -@pytest.fixture(scope="session") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - class TestGenesisBlock: @pytest.mark.asyncio async def test_block_tools_proofs_400(self, default_400_blocks): diff --git a/tests/blockchain/test_blockchain_transactions.py b/tests/blockchain/test_blockchain_transactions.py index 00ead35c1867..895d5d2b1212 100644 --- a/tests/blockchain/test_blockchain_transactions.py +++ b/tests/blockchain/test_blockchain_transactions.py @@ -1,4 +1,3 @@ -import asyncio import logging import pytest @@ -25,12 +24,6 @@ log = logging.getLogger(__name__) -@pytest.fixture(scope="session") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - @pytest_asyncio.fixture(scope="function") async def two_nodes(db_version, self_hostname): async for _ in setup_two_nodes(test_constants, db_version=db_version, self_hostname=self_hostname): diff --git a/tests/core/full_node/conftest.py b/tests/core/full_node/conftest.py new file mode 100644 index 000000000000..d2ed3b296db0 --- /dev/null +++ b/tests/core/full_node/conftest.py @@ -0,0 +1,15 @@ +import asyncio +from typing import Iterator + +import pytest + + +# This is an optimization to reduce runtime by reducing setup and teardown on the +# wallet nodes fixture below. +# https://github.com/pytest-dev/pytest-asyncio/blob/v0.18.1/pytest_asyncio/plugin.py#L479-L484 +@pytest.fixture(scope="module") +def event_loop(request: "pytest.FixtureRequest") -> Iterator[asyncio.AbstractEventLoop]: + """Create an instance of the default event loop for each test case.""" + loop = asyncio.get_event_loop_policy().new_event_loop() + yield loop + loop.close() diff --git a/tests/core/full_node/full_sync/test_full_sync.py b/tests/core/full_node/full_sync/test_full_sync.py index 10ed699baf47..e13b1489cd83 100644 --- a/tests/core/full_node/full_sync/test_full_sync.py +++ b/tests/core/full_node/full_sync/test_full_sync.py @@ -19,12 +19,6 @@ from tests.time_out_assert import time_out_assert -@pytest.fixture(scope="session") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - log = logging.getLogger(__name__) diff --git a/tests/core/full_node/stores/test_block_store.py b/tests/core/full_node/stores/test_block_store.py index 15ac9ad2f947..620f79889605 100644 --- a/tests/core/full_node/stores/test_block_store.py +++ b/tests/core/full_node/stores/test_block_store.py @@ -24,12 +24,6 @@ log = logging.getLogger(__name__) -@pytest.fixture(scope="module") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - class TestBlockStore: @pytest.mark.asyncio async def test_block_store(self, tmp_dir, db_version, bt): diff --git a/tests/core/full_node/stores/test_coin_store.py b/tests/core/full_node/stores/test_coin_store.py index 8f9a0b2b7f6c..883aa7d9735b 100644 --- a/tests/core/full_node/stores/test_coin_store.py +++ b/tests/core/full_node/stores/test_coin_store.py @@ -1,4 +1,3 @@ -import asyncio import logging from typing import List, Optional, Set, Tuple @@ -25,12 +24,6 @@ from tests.util.db_connection import DBConnection -@pytest.fixture(scope="module") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - constants = test_constants WALLET_A = WalletTool(constants) diff --git a/tests/core/full_node/stores/test_full_node_store.py b/tests/core/full_node/stores/test_full_node_store.py index 748bc33718d8..e1149f188433 100644 --- a/tests/core/full_node/stores/test_full_node_store.py +++ b/tests/core/full_node/stores/test_full_node_store.py @@ -44,12 +44,6 @@ def cleanup_keyring(keyring: TempKeyring): bt = create_block_tools(constants=test_constants, keychain=keychain) -@pytest.fixture(scope="session") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - log = logging.getLogger(__name__) diff --git a/tests/core/full_node/stores/test_hint_store.py b/tests/core/full_node/stores/test_hint_store.py index 69d4431113f1..edd097664fd9 100644 --- a/tests/core/full_node/stores/test_hint_store.py +++ b/tests/core/full_node/stores/test_hint_store.py @@ -1,4 +1,3 @@ -import asyncio import logging import pytest from clvm.casts import int_to_bytes @@ -14,12 +13,6 @@ from tests.wallet_tools import WalletTool -@pytest.fixture(scope="module") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - log = logging.getLogger(__name__) diff --git a/tests/core/full_node/stores/test_sync_store.py b/tests/core/full_node/stores/test_sync_store.py index 6f818e21af7f..e219ebbb70f4 100644 --- a/tests/core/full_node/stores/test_sync_store.py +++ b/tests/core/full_node/stores/test_sync_store.py @@ -1,17 +1,9 @@ -import asyncio - import pytest from chia.full_node.sync_store import SyncStore from chia.util.hash import std_hash -@pytest.fixture(scope="module") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - class TestStore: @pytest.mark.asyncio async def test_basic_store(self): diff --git a/tests/core/full_node/test_address_manager.py b/tests/core/full_node/test_address_manager.py index e71497feb511..de1e40f71341 100644 --- a/tests/core/full_node/test_address_manager.py +++ b/tests/core/full_node/test_address_manager.py @@ -1,4 +1,3 @@ -import asyncio import math import time from pathlib import Path @@ -11,12 +10,6 @@ from chia.util.ints import uint16, uint64 -@pytest.fixture(scope="module") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - class AddressManagerTest(AddressManager): def __init__(self, make_deterministic=True): super().__init__() diff --git a/tests/core/full_node/test_full_node.py b/tests/core/full_node/test_full_node.py index 2c72be5c3ef8..81fed5beb660 100644 --- a/tests/core/full_node/test_full_node.py +++ b/tests/core/full_node/test_full_node.py @@ -98,12 +98,6 @@ async def get_block_path(full_node: FullNodeAPI): return blocks_list -@pytest.fixture(scope="session") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - @pytest_asyncio.fixture(scope="module") async def wallet_nodes(bt): async_gen = setup_simulators_and_wallets(2, 1, {"MEMPOOL_BLOCK_BUFFER": 2, "MAX_BLOCK_COST_CLVM": 400000000}) diff --git a/tests/core/full_node/test_mempool.py b/tests/core/full_node/test_mempool.py index ce883c5b592e..eabfd9ba0025 100644 --- a/tests/core/full_node/test_mempool.py +++ b/tests/core/full_node/test_mempool.py @@ -1,4 +1,3 @@ -import asyncio import dataclasses import logging from time import time @@ -80,19 +79,6 @@ def generate_test_spend_bundle( return transaction -# this is here to avoid this error: -# ScopeMismatch: You tried to access the 'function' scoped fixture 'event_loop' -# with a 'module' scoped request object, involved factories -@pytest.fixture(scope="module") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - -# TODO: this fixture should really be at function scope, to make all tests independent. -# The reason it isn't is that our simulators can't be destroyed correctly, which -# means you can't instantiate more than one per process, so this is a hack until -# that is fixed. For now, our tests are not independent @pytest_asyncio.fixture(scope="module") async def two_nodes(bt, wallet_a): async_gen = setup_simulators_and_wallets(2, 1, {}) diff --git a/tests/core/full_node/test_mempool_performance.py b/tests/core/full_node/test_mempool_performance.py index 1de87a30f23f..408178463f40 100644 --- a/tests/core/full_node/test_mempool_performance.py +++ b/tests/core/full_node/test_mempool_performance.py @@ -35,12 +35,6 @@ async def wallet_balance_at_least(wallet_node: WalletNode, balance): log = logging.getLogger(__name__) -@pytest.fixture(scope="session") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - @pytest_asyncio.fixture(scope="module") async def wallet_nodes(bt): key_seed = bt.farmer_master_sk_entropy diff --git a/tests/core/full_node/test_node_load.py b/tests/core/full_node/test_node_load.py index 437789110bcb..94e500e18729 100644 --- a/tests/core/full_node/test_node_load.py +++ b/tests/core/full_node/test_node_load.py @@ -1,4 +1,3 @@ -import asyncio import time import pytest @@ -12,12 +11,6 @@ from tests.time_out_assert import time_out_assert -@pytest.fixture(scope="session") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - @pytest_asyncio.fixture(scope="function") async def two_nodes(db_version, self_hostname): async for _ in setup_two_nodes(test_constants, db_version=db_version, self_hostname=self_hostname): diff --git a/tests/core/full_node/test_performance.py b/tests/core/full_node/test_performance.py index 9799616e5e15..0cd0331abecc 100644 --- a/tests/core/full_node/test_performance.py +++ b/tests/core/full_node/test_performance.py @@ -39,12 +39,6 @@ async def get_block_path(full_node: FullNodeAPI): return blocks_list -@pytest.fixture(scope="session") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - @pytest_asyncio.fixture(scope="module") async def wallet_nodes(bt): async_gen = setup_simulators_and_wallets(1, 1, {"MEMPOOL_BLOCK_BUFFER": 1, "MAX_BLOCK_COST_CLVM": 11000000000}) diff --git a/tests/core/full_node/test_transactions.py b/tests/core/full_node/test_transactions.py index 643dd1868bec..97c8dfa43ecb 100644 --- a/tests/core/full_node/test_transactions.py +++ b/tests/core/full_node/test_transactions.py @@ -16,12 +16,6 @@ from tests.time_out_assert import time_out_assert -@pytest.fixture(scope="module") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - @pytest_asyncio.fixture(scope="function") async def wallet_node(): async for _ in setup_simulators_and_wallets(1, 1, {}): diff --git a/tests/core/server/test_dos.py b/tests/core/server/test_dos.py index 84fa981def90..72ddccccd825 100644 --- a/tests/core/server/test_dos.py +++ b/tests/core/server/test_dos.py @@ -33,12 +33,6 @@ async def get_block_path(full_node: FullNodeAPI): return blocks_list -@pytest.fixture(scope="session") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - @pytest_asyncio.fixture(scope="function") async def setup_two_nodes(db_version): async for _ in setup_simulators_and_wallets(2, 0, {}, db_version=db_version): diff --git a/tests/core/server/test_rate_limits.py b/tests/core/server/test_rate_limits.py index 7833a04ba2a5..31f41ab2e99e 100644 --- a/tests/core/server/test_rate_limits.py +++ b/tests/core/server/test_rate_limits.py @@ -8,12 +8,6 @@ from tests.setup_nodes import test_constants -@pytest.fixture(scope="module") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - constants = test_constants diff --git a/tests/core/test_cost_calculation.py b/tests/core/test_cost_calculation.py index 955e3a05119f..af62892f7052 100644 --- a/tests/core/test_cost_calculation.py +++ b/tests/core/test_cost_calculation.py @@ -1,4 +1,3 @@ -import asyncio import logging import pathlib import time @@ -23,12 +22,6 @@ log = logging.getLogger(__name__) -@pytest.fixture(scope="module") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - def large_block_generator(size): # make a small block and hash it # use this in the name for the cached big block diff --git a/tests/core/test_db_conversion.py b/tests/core/test_db_conversion.py index e6dd6e2d94cd..4a9e94366605 100644 --- a/tests/core/test_db_conversion.py +++ b/tests/core/test_db_conversion.py @@ -1,8 +1,6 @@ import pytest -import pytest_asyncio import aiosqlite import random -import asyncio from pathlib import Path from typing import List, Tuple @@ -27,12 +25,6 @@ def rand_bytes(num) -> bytes: return bytes(ret) -@pytest_asyncio.fixture(scope="session") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - class TestDbUpgrade: @pytest.mark.asyncio @pytest.mark.parametrize("with_hints", [True, False]) diff --git a/tests/core/test_filter.py b/tests/core/test_filter.py index 13e3263df199..3448b4b42e66 100644 --- a/tests/core/test_filter.py +++ b/tests/core/test_filter.py @@ -1,4 +1,3 @@ -import asyncio from typing import List import pytest @@ -8,12 +7,6 @@ from tests.setup_nodes import setup_simulators_and_wallets -@pytest.fixture(scope="module") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - @pytest_asyncio.fixture(scope="function") async def wallet_and_node(): async for _ in setup_simulators_and_wallets(1, 1, {}): diff --git a/tests/core/test_merkle_set.py b/tests/core/test_merkle_set.py index 0f56fa6ce8ce..02072ed46f1e 100644 --- a/tests/core/test_merkle_set.py +++ b/tests/core/test_merkle_set.py @@ -1,4 +1,3 @@ -import asyncio import itertools import pytest @@ -6,12 +5,6 @@ from chia.util.merkle_set import MerkleSet, confirm_included_already_hashed -@pytest.fixture(scope="module") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - class TestMerkleSet: @pytest.mark.asyncio async def test_basics(self, bt): diff --git a/tests/pools/test_pool_rpc.py b/tests/pools/test_pool_rpc.py index d9fa9f660233..34c3b44e790f 100644 --- a/tests/pools/test_pool_rpc.py +++ b/tests/pools/test_pool_rpc.py @@ -88,12 +88,6 @@ async def wallet_is_synced(wallet_node: WalletNode, full_node_api): ) -@pytest.fixture(scope="session") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - PREFARMED_BLOCKS = 4 diff --git a/tests/pools/test_wallet_pool_store.py b/tests/pools/test_wallet_pool_store.py index 206493bc4695..a35e285933ee 100644 --- a/tests/pools/test_wallet_pool_store.py +++ b/tests/pools/test_wallet_pool_store.py @@ -1,4 +1,3 @@ -import asyncio from pathlib import Path from secrets import token_bytes from typing import Optional @@ -17,12 +16,6 @@ from chia.wallet.wallet_pool_store import WalletPoolStore -@pytest.fixture(scope="module") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - def make_child_solution(coin_spend: CoinSpend, new_coin: Optional[Coin] = None) -> CoinSpend: # TODO: address hint error and remove ignore # error: Incompatible types in assignment (expression has type "bytes", variable has type "bytes32") diff --git a/tests/util/test_lock_queue.py b/tests/util/test_lock_queue.py index e9705bd86262..e725b75074c9 100644 --- a/tests/util/test_lock_queue.py +++ b/tests/util/test_lock_queue.py @@ -10,12 +10,6 @@ log = logging.getLogger(__name__) -@pytest.fixture(scope="module") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - class TestLockQueue: @pytest.mark.asyncio async def test_lock_queue(self): diff --git a/tests/wallet/cat_wallet/test_trades.py b/tests/wallet/cat_wallet/test_trades.py index 0c348fe707d7..f2366362a8a5 100644 --- a/tests/wallet/cat_wallet/test_trades.py +++ b/tests/wallet/cat_wallet/test_trades.py @@ -26,12 +26,6 @@ async def tx_in_pool(mempool: MempoolManager, tx_id): return True -@pytest.fixture(scope="module") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - @pytest_asyncio.fixture(scope="function") async def two_wallet_nodes(): async for _ in setup_simulators_and_wallets(1, 2, {}): diff --git a/tests/wallet/did_wallet/test_did.py b/tests/wallet/did_wallet/test_did.py index d15b2bacb898..0ba514fd3e7f 100644 --- a/tests/wallet/did_wallet/test_did.py +++ b/tests/wallet/did_wallet/test_did.py @@ -1,4 +1,3 @@ -import asyncio import pytest import pytest_asyncio from chia.simulator.simulator_protocol import FarmNewBlockProtocol @@ -15,12 +14,6 @@ pytestmark = pytest.mark.skip("TODO: Fix tests") -@pytest.fixture(scope="module") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - @pytest_asyncio.fixture(scope="function") async def wallet_node(): async for _ in setup_simulators_and_wallets(1, 1, {}): diff --git a/tests/wallet/did_wallet/test_did_rpc.py b/tests/wallet/did_wallet/test_did_rpc.py index 328094d4b802..f393c3164aae 100644 --- a/tests/wallet/did_wallet/test_did_rpc.py +++ b/tests/wallet/did_wallet/test_did_rpc.py @@ -1,4 +1,3 @@ -import asyncio import logging import pytest import pytest_asyncio @@ -21,12 +20,6 @@ pytestmark = pytest.mark.skip("TODO: Fix tests") -@pytest.fixture(scope="module") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - @pytest_asyncio.fixture(scope="function") async def three_wallet_nodes(): async for _ in setup_simulators_and_wallets(1, 3, {}): diff --git a/tests/wallet/rl_wallet/test_rl_rpc.py b/tests/wallet/rl_wallet/test_rl_rpc.py index 686bbc1e6422..e387646e79cc 100644 --- a/tests/wallet/rl_wallet/test_rl_rpc.py +++ b/tests/wallet/rl_wallet/test_rl_rpc.py @@ -18,12 +18,6 @@ from tests.wallet.sync.test_wallet_sync import wallet_height_at_least -@pytest.fixture(scope="module") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - async def is_transaction_in_mempool(user_wallet_id, api, tx_id: bytes32) -> bool: try: val = await api.get_transaction({"wallet_id": user_wallet_id, "transaction_id": tx_id.hex()}) diff --git a/tests/wallet/rl_wallet/test_rl_wallet.py b/tests/wallet/rl_wallet/test_rl_wallet.py index c3226fe18299..cee43b28fcb6 100644 --- a/tests/wallet/rl_wallet/test_rl_wallet.py +++ b/tests/wallet/rl_wallet/test_rl_wallet.py @@ -1,5 +1,3 @@ -import asyncio - import pytest import pytest_asyncio @@ -11,12 +9,6 @@ from tests.time_out_assert import time_out_assert -@pytest.fixture(scope="module") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - @pytest_asyncio.fixture(scope="function") async def two_wallet_nodes(): async for _ in setup_simulators_and_wallets(1, 2, {}): diff --git a/tests/wallet/simple_sync/test_simple_sync_protocol.py b/tests/wallet/simple_sync/test_simple_sync_protocol.py index b4a647cfce2f..2e45359f5730 100644 --- a/tests/wallet/simple_sync/test_simple_sync_protocol.py +++ b/tests/wallet/simple_sync/test_simple_sync_protocol.py @@ -41,12 +41,6 @@ def wallet_height_at_least(wallet_node, h): log = getLogger(__name__) -@pytest.fixture(scope="session") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - @pytest_asyncio.fixture(scope="function") async def wallet_node_simulator(): async for _ in setup_simulators_and_wallets(1, 1, {}): diff --git a/tests/wallet/sync/test_wallet_sync.py b/tests/wallet/sync/test_wallet_sync.py index 73c0aedb4d88..e904a1a8d11d 100644 --- a/tests/wallet/sync/test_wallet_sync.py +++ b/tests/wallet/sync/test_wallet_sync.py @@ -27,12 +27,6 @@ def wallet_height_at_least(wallet_node, h): log = getLogger(__name__) -@pytest.fixture(scope="session") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - @pytest_asyncio.fixture(scope="function") async def wallet_node(self_hostname): async for _ in setup_node_and_wallet(test_constants, self_hostname): diff --git a/tests/wallet/test_puzzle_store.py b/tests/wallet/test_puzzle_store.py index 94fa358c70e7..bd90a4f107e0 100644 --- a/tests/wallet/test_puzzle_store.py +++ b/tests/wallet/test_puzzle_store.py @@ -1,4 +1,3 @@ -import asyncio from pathlib import Path from secrets import token_bytes @@ -13,12 +12,6 @@ from chia.wallet.wallet_puzzle_store import WalletPuzzleStore -@pytest.fixture(scope="module") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - class TestPuzzleStore: @pytest.mark.asyncio async def test_puzzle_store(self): diff --git a/tests/wallet/test_wallet_blockchain.py b/tests/wallet/test_wallet_blockchain.py index ef8c198a3aa6..f2dd424f09a6 100644 --- a/tests/wallet/test_wallet_blockchain.py +++ b/tests/wallet/test_wallet_blockchain.py @@ -1,4 +1,3 @@ -import asyncio import dataclasses from pathlib import Path @@ -17,12 +16,6 @@ from tests.setup_nodes import test_constants, setup_node_and_wallet -@pytest.fixture(scope="session") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - @pytest_asyncio.fixture(scope="function") async def wallet_node(self_hostname): async for _ in setup_node_and_wallet(test_constants, self_hostname): diff --git a/tests/wallet/test_wallet_interested_store.py b/tests/wallet/test_wallet_interested_store.py index 0f2ae9926190..11b4b8bb0bcf 100644 --- a/tests/wallet/test_wallet_interested_store.py +++ b/tests/wallet/test_wallet_interested_store.py @@ -1,4 +1,3 @@ -import asyncio from pathlib import Path from secrets import token_bytes import aiosqlite @@ -11,12 +10,6 @@ from chia.wallet.wallet_interested_store import WalletInterestedStore -@pytest.fixture(scope="module") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - class TestWalletInterestedStore: @pytest.mark.asyncio async def test_store(self): diff --git a/tests/wallet/test_wallet_key_val_store.py b/tests/wallet/test_wallet_key_val_store.py index ecaaf0bf2f26..9ae34e0e9314 100644 --- a/tests/wallet/test_wallet_key_val_store.py +++ b/tests/wallet/test_wallet_key_val_store.py @@ -1,4 +1,3 @@ -import asyncio from pathlib import Path import aiosqlite import pytest @@ -9,12 +8,6 @@ from chia.wallet.key_val_store import KeyValStore -@pytest.fixture(scope="module") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - class TestWalletKeyValStore: @pytest.mark.asyncio async def test_store(self, bt): diff --git a/tests/weight_proof/test_weight_proof.py b/tests/weight_proof/test_weight_proof.py index 284578d9cb92..8bc04a001af1 100644 --- a/tests/weight_proof/test_weight_proof.py +++ b/tests/weight_proof/test_weight_proof.py @@ -42,12 +42,6 @@ from chia.util.ints import uint32, uint64 -@pytest.fixture(scope="session") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - - def count_sub_epochs(blockchain, last_hash) -> int: curr = blockchain._sub_blocks[last_hash] count = 0 From 622cdeb3968563c38570454da1599523179168de Mon Sep 17 00:00:00 2001 From: William Blanke Date: Mon, 14 Mar 2022 13:10:16 -0700 Subject: [PATCH 202/378] updated gui to c992d07c956501f92e84ead80127c6b1e882fc21 --- chia-blockchain-gui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia-blockchain-gui b/chia-blockchain-gui index cdfa2b98217f..c992d07c9565 160000 --- a/chia-blockchain-gui +++ b/chia-blockchain-gui @@ -1 +1 @@ -Subproject commit cdfa2b98217fa8755c0da4f7409e6f90032c4c4c +Subproject commit c992d07c956501f92e84ead80127c6b1e882fc21 From 0036118177d632bce341db26ce678ab3f64b187b Mon Sep 17 00:00:00 2001 From: dustinface <35775977+xdustinface@users.noreply.github.com> Date: Tue, 15 Mar 2022 14:08:29 +0100 Subject: [PATCH 203/378] tests: Add `_PYTEST_RAISE` to fix exception breakpoints with pytest (#10487) It's currently not possible to have the debuger stop on an uncaucht exception when debugging tests. With this patch, adding `_PYTEST_RAISE=1` to the environment variables in the pytest configuration template fixes this. --- tests/conftest.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index e8bd84bf9ad9..eb1c32d6cac2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,6 @@ # flake8: noqa E402 # See imports after multiprocessing.set_start_method import multiprocessing +import os import pytest import pytest_asyncio import tempfile @@ -128,3 +129,15 @@ def default_10000_blocks_compact(bt): def tmp_dir(): with tempfile.TemporaryDirectory() as folder: yield Path(folder) + + +# For the below see https://stackoverflow.com/a/62563106/15133773 +if os.getenv("_PYTEST_RAISE", "0") != "0": + + @pytest.hookimpl(tryfirst=True) + def pytest_exception_interact(call): + raise call.excinfo.value + + @pytest.hookimpl(tryfirst=True) + def pytest_internalerror(excinfo): + raise excinfo.value From fc453e637c3fd97ae017f2f9b03cc5b6e7750200 Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 15 Mar 2022 09:59:27 -0700 Subject: [PATCH 204/378] Fixed test failures on Windows. (#10740) --- tests/core/full_node/test_peer_store_resolver.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/core/full_node/test_peer_store_resolver.py b/tests/core/full_node/test_peer_store_resolver.py index cf615673025f..b01c97721f1a 100644 --- a/tests/core/full_node/test_peer_store_resolver.py +++ b/tests/core/full_node/test_peer_store_resolver.py @@ -1,3 +1,4 @@ +import os from pathlib import Path from typing import Dict @@ -25,7 +26,7 @@ def test_resolve_unmodified_legacy_peer_db_path(self, tmp_path: Path): # Expect: peers.dat path has the same directory as the legacy db assert resolver.peers_file_path == root_path / Path("db/peers.dat") # Expect: the config is updated with the new value - assert config["peers_file_path"] == "db/peers.dat" + assert config["peers_file_path"] == os.fspath(Path("db/peers.dat")) # Expect: the config retains the legacy peer_db_path value assert config["peer_db_path"] == "db/peer_table_node.sqlite" @@ -49,7 +50,7 @@ def test_resolve_modified_legacy_peer_db_path(self, tmp_path: Path): # Expect: peers.dat path has the same directory as the legacy db assert resolver.peers_file_path == root_path / Path("some/modified/db/path/peers.dat") # Expect: the config is updated with the new value - assert config["peers_file_path"] == "some/modified/db/path/peers.dat" + assert config["peers_file_path"] == os.fspath(Path("some/modified/db/path/peers.dat")) # Expect: the config retains the legacy peer_db_path value assert config["peer_db_path"] == "some/modified/db/path/peer_table_node.sqlite" @@ -173,7 +174,7 @@ def test_resolve_missing_keys(self, tmp_path: Path): # Expect: peers.dat Path is set to the default location assert resolver.peers_file_path == root_path / Path("db/peers.dat") # Expect: the config is updated with the new value - assert config["peers_file_path"] == "db/peers.dat" + assert config["peers_file_path"] == os.fspath(Path("db/peers.dat")) # Expect: the config doesn't add a legacy peer_db_path value assert config.get("peer_db_path") is None @@ -197,7 +198,7 @@ def test_resolve_with_testnet(self, tmp_path: Path): # Expect: resolved file path has testnet in the name assert resolver.peers_file_path == root_path / Path("db/peers_testnet123.dat") # Expect: the config is updated with the new value - assert config["peers_file_path"] == "db/peers_testnet123.dat" + assert config["peers_file_path"] == os.fspath(Path("db/peers_testnet123.dat")) # Expect: the config doesn't add a legacy peer_db_path value assert config.get("peer_db_path") is None From aa7123053bf495868f80cbca0403237873a6fedf Mon Sep 17 00:00:00 2001 From: Adam Kelly <338792+aqk@users.noreply.github.com> Date: Tue, 15 Mar 2022 10:33:05 -0700 Subject: [PATCH 205/378] Convert helper method do_spend from a class method to a function (#10613) --- tests/wallet/cat_wallet/test_cat_lifecycle.py | 150 ++++++++---------- 1 file changed, 68 insertions(+), 82 deletions(-) diff --git a/tests/wallet/cat_wallet/test_cat_lifecycle.py b/tests/wallet/cat_wallet/test_cat_lifecycle.py index c940c1b419c4..b4bdf889b126 100644 --- a/tests/wallet/cat_wallet/test_cat_lifecycle.py +++ b/tests/wallet/cat_wallet/test_cat_lifecycle.py @@ -44,64 +44,64 @@ async def setup_sim(): return sim, sim_client -class TestCATLifecycle: - cost: Dict[str, int] = {} - - async def do_spend( - self, - sim: SpendSim, - sim_client: SimClient, - tail: Program, - coins: List[Coin], - lineage_proofs: List[Program], - inner_solutions: List[Program], - expected_result: Tuple[MempoolInclusionStatus, Err], - reveal_limitations_program: bool = True, - signatures: List[G2Element] = [], - extra_deltas: Optional[List[int]] = None, - additional_spends: List[SpendBundle] = [], - limitations_solutions: Optional[List[Program]] = None, - cost_str: str = "", +async def do_spend( + sim: SpendSim, + sim_client: SimClient, + tail: Program, + coins: List[Coin], + lineage_proofs: List[Program], + inner_solutions: List[Program], + expected_result: Tuple[MempoolInclusionStatus, Err], + reveal_limitations_program: bool = True, + signatures: List[G2Element] = [], + extra_deltas: Optional[List[int]] = None, + additional_spends: List[SpendBundle] = [], + limitations_solutions: Optional[List[Program]] = None, +) -> int: + if limitations_solutions is None: + limitations_solutions = [Program.to([])] * len(coins) + if extra_deltas is None: + extra_deltas = [0] * len(coins) + + spendable_cat_list: List[SpendableCAT] = [] + for coin, innersol, proof, limitations_solution, extra_delta in zip( + coins, inner_solutions, lineage_proofs, limitations_solutions, extra_deltas ): - if limitations_solutions is None: - limitations_solutions = [Program.to([])] * len(coins) - if extra_deltas is None: - extra_deltas = [0] * len(coins) - - spendable_cat_list: List[SpendableCAT] = [] - for coin, innersol, proof, limitations_solution, extra_delta in zip( - coins, inner_solutions, lineage_proofs, limitations_solutions, extra_deltas - ): - spendable_cat_list.append( - SpendableCAT( - coin, - tail.get_tree_hash(), - acs, - innersol, - limitations_solution=limitations_solution, - lineage_proof=proof, - extra_delta=extra_delta, - limitations_program_reveal=tail if reveal_limitations_program else Program.to([]), - ) + spendable_cat_list.append( + SpendableCAT( + coin, + tail.get_tree_hash(), + acs, + innersol, + limitations_solution=limitations_solution, + lineage_proof=proof, + extra_delta=extra_delta, + limitations_program_reveal=tail if reveal_limitations_program else Program.to([]), ) - - spend_bundle: SpendBundle = unsigned_spend_bundle_for_spendable_cats( - CAT_MOD, - spendable_cat_list, ) - agg_sig = AugSchemeMPL.aggregate(signatures) - result = await sim_client.push_tx( - SpendBundle.aggregate( - [ - *additional_spends, - spend_bundle, - SpendBundle([], agg_sig), # "Signing" the spend bundle - ] - ) + + spend_bundle: SpendBundle = unsigned_spend_bundle_for_spendable_cats( + CAT_MOD, + spendable_cat_list, + ) + agg_sig = AugSchemeMPL.aggregate(signatures) + result = await sim_client.push_tx( + SpendBundle.aggregate( + [ + *additional_spends, + spend_bundle, + SpendBundle([], agg_sig), # "Signing" the spend bundle + ] ) - assert result == expected_result - self.cost[cost_str] = cost_of_spend_bundle(spend_bundle) - await sim.farm_block() + ) + assert result == expected_result + cost = cost_of_spend_bundle(spend_bundle) + await sim.farm_block() + return cost + + +class TestCATLifecycle: + cost: Dict[str, int] = {} @pytest.mark.asyncio() async def test_cat_mod(self, setup_sim): @@ -116,7 +116,7 @@ async def test_cat_mod(self, setup_sim): starting_coin: Coin = (await sim_client.get_coin_records_by_puzzle_hash(cat_ph))[0].coin # Testing the eve spend - await self.do_spend( + self.cost["Eve Spend"] = await do_spend( sim, sim_client, tail, @@ -134,7 +134,6 @@ async def test_cat_mod(self, setup_sim): ], (MempoolInclusionStatus.SUCCESS, None), limitations_solutions=[checker_solution], - cost_str="Eve Spend", ) # There's 4 total coins at this point. A farming reward and the three children of the spend above. @@ -145,7 +144,7 @@ async def test_cat_mod(self, setup_sim): for record in (await sim_client.get_coin_records_by_puzzle_hash(cat_ph, include_spent_coins=False)) ] coins = [coins[0], coins[1]] - await self.do_spend( + self.cost["Two CATs"] = await do_spend( sim, sim_client, tail, @@ -162,7 +161,6 @@ async def test_cat_mod(self, setup_sim): ], (MempoolInclusionStatus.SUCCESS, None), limitations_solutions=[checker_solution] * 2, - cost_str="Two CATs", ) # Testing a combination of three @@ -171,7 +169,7 @@ async def test_cat_mod(self, setup_sim): for record in (await sim_client.get_coin_records_by_puzzle_hash(cat_ph, include_spent_coins=False)) ] total_amount: uint64 = uint64(sum([c.amount for c in coins])) - await self.do_spend( + self.cost["Three CATs"] = await do_spend( sim, sim_client, tail, @@ -189,7 +187,6 @@ async def test_cat_mod(self, setup_sim): ], (MempoolInclusionStatus.SUCCESS, None), limitations_solutions=[checker_solution] * 3, - cost_str="Three CATs", ) # Spend with a standard lineage proof @@ -197,7 +194,7 @@ async def test_cat_mod(self, setup_sim): _, curried_args = cat_puzzle.uncurry() _, _, innerpuzzle = curried_args.as_iter() lineage_proof = LineageProof(parent_coin.parent_coin_info, innerpuzzle.get_tree_hash(), parent_coin.amount) - await self.do_spend( + self.cost["Standard Lineage Check"] = await do_spend( sim, sim_client, tail, @@ -206,11 +203,10 @@ async def test_cat_mod(self, setup_sim): [Program.to([[51, acs.get_tree_hash(), total_amount]])], (MempoolInclusionStatus.SUCCESS, None), reveal_limitations_program=False, - cost_str="Standard Lineage Check", ) # Melt some value - await self.do_spend( + self.cost["Melting Value"] = await do_spend( sim, sim_client, tail, @@ -227,7 +223,6 @@ async def test_cat_mod(self, setup_sim): (MempoolInclusionStatus.SUCCESS, None), extra_deltas=[-1], limitations_solutions=[checker_solution], - cost_str="Melting Value", ) # Mint some value @@ -247,7 +242,7 @@ async def test_cat_mod(self, setup_sim): ], G2Element(), ) - await self.do_spend( + self.cost["Mint Value"] = await do_spend( sim, sim_client, tail, @@ -265,7 +260,6 @@ async def test_cat_mod(self, setup_sim): extra_deltas=[1], additional_spends=[acs_bundle], limitations_solutions=[checker_solution], - cost_str="Mint Value", ) finally: @@ -290,7 +284,7 @@ async def test_complex_spend(self, setup_sim): eve_to_melt = cat_records[3].coin # Spend two of them to make them non-eve - await self.do_spend( + self.cost["Spend two eves"] = await do_spend( sim, sim_client, tail, @@ -312,7 +306,6 @@ async def test_complex_spend(self, setup_sim): ], (MempoolInclusionStatus.SUCCESS, None), limitations_solutions=[checker_solution] * 2, - cost_str="Spend two eves", ) # Make the lineage proofs for the non-eves @@ -327,7 +320,7 @@ async def test_complex_spend(self, setup_sim): # Do the complex spend # We have both and eve and non-eve doing both minting and melting - await self.do_spend( + self.cost["Complex Spend"] = await do_spend( sim, sim_client, tail, @@ -362,7 +355,6 @@ async def test_complex_spend(self, setup_sim): (MempoolInclusionStatus.SUCCESS, None), limitations_solutions=[checker_solution] * 4, extra_deltas=[13, -21, 21, -13], - cost_str="Complex Spend", ) finally: await sim.close() @@ -390,7 +382,7 @@ async def test_genesis_by_id(self, setup_sim): ) await sim.farm_block() - await self.do_spend( + self.cost["Genesis by ID"] = await do_spend( sim, sim_client, tail, @@ -406,7 +398,6 @@ async def test_genesis_by_id(self, setup_sim): ], (MempoolInclusionStatus.SUCCESS, None), limitations_solutions=[checker_solution], - cost_str="Genesis by ID", ) finally: @@ -435,7 +426,7 @@ async def test_genesis_by_puzhash(self, setup_sim): ) await sim.farm_block() - await self.do_spend( + self.cost["Genesis by Puzhash"] = await do_spend( sim, sim_client, tail, @@ -451,7 +442,6 @@ async def test_genesis_by_puzhash(self, setup_sim): ], (MempoolInclusionStatus.SUCCESS, None), limitations_solutions=[checker_solution], - cost_str="Genesis by Puzhash", ) finally: @@ -476,7 +466,7 @@ async def test_everything_with_signature(self, setup_sim): sk, (starting_coin.name() + sim.defaults.AGG_SIG_ME_ADDITIONAL_DATA) ) - await self.do_spend( + self.cost["Signature Issuance"] = await do_spend( sim, sim_client, tail, @@ -493,7 +483,6 @@ async def test_everything_with_signature(self, setup_sim): (MempoolInclusionStatus.SUCCESS, None), limitations_solutions=[checker_solution], signatures=[signature], - cost_str="Signature Issuance", ) # Test melting value @@ -502,7 +491,7 @@ async def test_everything_with_signature(self, setup_sim): sk, (int_to_bytes(-1) + coin.name() + sim.defaults.AGG_SIG_ME_ADDITIONAL_DATA) ) - await self.do_spend( + self.cost["Signature Melt"] = await do_spend( sim, sim_client, tail, @@ -520,7 +509,6 @@ async def test_everything_with_signature(self, setup_sim): extra_deltas=[-1], limitations_solutions=[checker_solution], signatures=[signature], - cost_str="Signature Melt", ) # Test minting value @@ -545,7 +533,7 @@ async def test_everything_with_signature(self, setup_sim): G2Element(), ) - await self.do_spend( + self.cost["Signature Mint"] = await do_spend( sim, sim_client, tail, @@ -564,7 +552,6 @@ async def test_everything_with_signature(self, setup_sim): limitations_solutions=[checker_solution], signatures=[signature], additional_spends=[acs_bundle], - cost_str="Signature Mint", ) finally: @@ -608,7 +595,7 @@ async def test_delegated_tail(self, setup_sim): ) signature: G2Element = AugSchemeMPL.sign(sk, new_tail.get_tree_hash()) - await self.do_spend( + self.cost["Delegated Genesis"] = await do_spend( sim, sim_client, tail, @@ -625,7 +612,6 @@ async def test_delegated_tail(self, setup_sim): (MempoolInclusionStatus.SUCCESS, None), signatures=[signature], limitations_solutions=[checker_solution], - cost_str="Delegated Genesis", ) finally: From 185a483d0daeaa7e60734de66e4547ee76db1eac Mon Sep 17 00:00:00 2001 From: Adam Kelly <338792+aqk@users.noreply.github.com> Date: Tue, 15 Mar 2022 10:33:33 -0700 Subject: [PATCH 206/378] Remove unused test code (#10614) --- tests/core/test_crawler_rpc.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/tests/core/test_crawler_rpc.py b/tests/core/test_crawler_rpc.py index 5fcde02874cb..285a8b84fe3e 100644 --- a/tests/core/test_crawler_rpc.py +++ b/tests/core/test_crawler_rpc.py @@ -1,26 +1,12 @@ -import atexit - import pytest from chia.rpc.crawler_rpc_api import CrawlerRpcApi from chia.seeder.crawler import Crawler -from tests.block_tools import create_block_tools, test_constants -from tests.util.keyring import TempKeyring - - -def cleanup_keyring(keyring: TempKeyring): - keyring.cleanup() - - -temp_keyring = TempKeyring() -keychain = temp_keyring.get_keychain() -atexit.register(cleanup_keyring, temp_keyring) # Attempt to cleanup the temp keychain -bt = create_block_tools(constants=test_constants, keychain=keychain) class TestCrawlerRpc: @pytest.mark.asyncio - async def test_get_ips_after_timestamp(self): + async def test_get_ips_after_timestamp(self, bt): crawler = Crawler(bt.config.get("seeder", {}), bt.root_path, consensus_constants=bt.constants) crawler_rpc_api = CrawlerRpcApi(crawler) From 7bbfa30a24e5e87c2cc356c35b4e4a7fc7ee6e60 Mon Sep 17 00:00:00 2001 From: Adam Kelly <338792+aqk@users.noreply.github.com> Date: Wed, 16 Mar 2022 11:47:54 -0700 Subject: [PATCH 207/378] Ak.setup nodes (#10619) * Remove unused test code * Centralize fixture uses of setup_n_nodes * Centralize fixure uses of setup_two_nodes * Break up setup_nodes into setup_services, for individial services, and setup_nodes, for initializing different simulator configurations * Sort imports --- mypy.ini | 2 +- .../test_blockchain_transactions.py | 9 +- tests/conftest.py | 32 ++ .../full_node/full_sync/test_full_sync.py | 27 +- tests/core/full_node/test_node_load.py | 8 - tests/setup_nodes.py | 269 +------------- tests/setup_services.py | 341 ++++++++++++++++++ 7 files changed, 387 insertions(+), 301 deletions(-) create mode 100644 tests/setup_services.py diff --git a/mypy.ini b/mypy.ini index 8dd59a99a268..ec9389612df1 100644 --- a/mypy.ini +++ b/mypy.ini @@ -17,7 +17,7 @@ no_implicit_reexport = True strict_equality = True # list created by: venv/bin/mypy | sed -n 's/.py:.*//p' | sort | uniq | tr '/' '.' | tr '\n' ',' -[mypy-benchmarks.block_ref,benchmarks.block_store,benchmarks.coin_store,benchmarks.utils,build_scripts.installer-version,chia.clvm.spend_sim,chia.cmds.configure,chia.cmds.db,chia.cmds.db_upgrade_func,chia.cmds.farm_funcs,chia.cmds.init,chia.cmds.init_funcs,chia.cmds.keys,chia.cmds.keys_funcs,chia.cmds.passphrase,chia.cmds.passphrase_funcs,chia.cmds.plotnft,chia.cmds.plotnft_funcs,chia.cmds.plots,chia.cmds.plotters,chia.cmds.show,chia.cmds.start_funcs,chia.cmds.wallet,chia.cmds.wallet_funcs,chia.consensus.block_body_validation,chia.consensus.blockchain,chia.consensus.blockchain_interface,chia.consensus.block_creation,chia.consensus.block_header_validation,chia.consensus.block_record,chia.consensus.block_root_validation,chia.consensus.coinbase,chia.consensus.constants,chia.consensus.difficulty_adjustment,chia.consensus.get_block_challenge,chia.consensus.multiprocess_validation,chia.consensus.pos_quality,chia.consensus.vdf_info_computation,chia.daemon.client,chia.daemon.keychain_proxy,chia.daemon.keychain_server,chia.daemon.server,chia.farmer.farmer,chia.farmer.farmer_api,chia.full_node.block_height_map,chia.full_node.block_store,chia.full_node.bundle_tools,chia.full_node.coin_store,chia.full_node.full_node,chia.full_node.full_node_api,chia.full_node.full_node_store,chia.full_node.generator,chia.full_node.hint_store,chia.full_node.lock_queue,chia.full_node.mempool,chia.full_node.mempool_check_conditions,chia.full_node.mempool_manager,chia.full_node.pending_tx_cache,chia.full_node.sync_store,chia.full_node.weight_proof,chia.harvester.harvester,chia.harvester.harvester_api,chia.introducer.introducer,chia.introducer.introducer_api,chia.plotters.bladebit,chia.plotters.chiapos,chia.plotters.install_plotter,chia.plotters.madmax,chia.plotters.plotters,chia.plotters.plotters_util,chia.plotting.check_plots,chia.plotting.create_plots,chia.plotting.manager,chia.plotting.util,chia.pools.pool_config,chia.pools.pool_puzzles,chia.pools.pool_wallet,chia.pools.pool_wallet_info,chia.protocols.pool_protocol,chia.rpc.crawler_rpc_api,chia.rpc.farmer_rpc_api,chia.rpc.farmer_rpc_client,chia.rpc.full_node_rpc_api,chia.rpc.full_node_rpc_client,chia.rpc.harvester_rpc_api,chia.rpc.harvester_rpc_client,chia.rpc.rpc_client,chia.rpc.rpc_server,chia.rpc.timelord_rpc_api,chia.rpc.util,chia.rpc.wallet_rpc_api,chia.rpc.wallet_rpc_client,chia.seeder.crawler,chia.seeder.crawler_api,chia.seeder.crawl_store,chia.seeder.dns_server,chia.seeder.peer_record,chia.seeder.start_crawler,chia.server.address_manager,chia.server.address_manager_store,chia.server.connection_utils,chia.server.introducer_peers,chia.server.node_discovery,chia.server.peer_store_resolver,chia.server.rate_limits,chia.server.reconnect_task,chia.server.server,chia.server.ssl_context,chia.server.start_farmer,chia.server.start_full_node,chia.server.start_harvester,chia.server.start_introducer,chia.server.start_service,chia.server.start_timelord,chia.server.start_wallet,chia.server.upnp,chia.server.ws_connection,chia.simulator.full_node_simulator,chia.simulator.start_simulator,chia.ssl.create_ssl,chia.timelord.iters_from_block,chia.timelord.timelord,chia.timelord.timelord_api,chia.timelord.timelord_launcher,chia.timelord.timelord_state,chia.types.announcement,chia.types.blockchain_format.classgroup,chia.types.blockchain_format.coin,chia.types.blockchain_format.program,chia.types.blockchain_format.proof_of_space,chia.types.blockchain_format.tree_hash,chia.types.blockchain_format.vdf,chia.types.full_block,chia.types.header_block,chia.types.mempool_item,chia.types.name_puzzle_condition,chia.types.peer_info,chia.types.spend_bundle,chia.types.transaction_queue_entry,chia.types.unfinished_block,chia.types.unfinished_header_block,chia.util.api_decorators,chia.util.block_cache,chia.util.byte_types,chia.util.cached_bls,chia.util.check_fork_next_block,chia.util.chia_logging,chia.util.config,chia.util.db_wrapper,chia.util.dump_keyring,chia.util.file_keyring,chia.util.files,chia.util.hash,chia.util.ints,chia.util.json_util,chia.util.keychain,chia.util.keyring_wrapper,chia.util.log_exceptions,chia.util.lru_cache,chia.util.make_test_constants,chia.util.merkle_set,chia.util.network,chia.util.partial_func,chia.util.pip_import,chia.util.profiler,chia.util.safe_cancel_task,chia.util.service_groups,chia.util.ssl_check,chia.util.streamable,chia.util.struct_stream,chia.util.type_checking,chia.util.validate_alert,chia.wallet.block_record,chia.wallet.cat_wallet.cat_utils,chia.wallet.cat_wallet.cat_wallet,chia.wallet.cat_wallet.lineage_store,chia.wallet.chialisp,chia.wallet.did_wallet.did_wallet,chia.wallet.did_wallet.did_wallet_puzzles,chia.wallet.key_val_store,chia.wallet.lineage_proof,chia.wallet.payment,chia.wallet.puzzles.genesis_checkers,chia.wallet.puzzles.load_clvm,chia.wallet.puzzles.p2_conditions,chia.wallet.puzzles.p2_delegated_conditions,chia.wallet.puzzles.p2_delegated_puzzle,chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle,chia.wallet.puzzles.p2_m_of_n_delegate_direct,chia.wallet.puzzles.p2_puzzle_hash,chia.wallet.puzzles.prefarm.spend_prefarm,chia.wallet.puzzles.puzzle_utils,chia.wallet.puzzles.rom_bootstrap_generator,chia.wallet.puzzles.singleton_top_layer,chia.wallet.puzzles.tails,chia.wallet.rl_wallet.rl_wallet,chia.wallet.rl_wallet.rl_wallet_puzzles,chia.wallet.secret_key_store,chia.wallet.settings.user_settings,chia.wallet.trade_manager,chia.wallet.trade_record,chia.wallet.trading.offer,chia.wallet.trading.trade_store,chia.wallet.transaction_record,chia.wallet.util.debug_spend_bundle,chia.wallet.util.new_peak_queue,chia.wallet.util.peer_request_cache,chia.wallet.util.wallet_sync_utils,chia.wallet.wallet,chia.wallet.wallet_action_store,chia.wallet.wallet_blockchain,chia.wallet.wallet_coin_store,chia.wallet.wallet_interested_store,chia.wallet.wallet_node,chia.wallet.wallet_node_api,chia.wallet.wallet_pool_store,chia.wallet.wallet_puzzle_store,chia.wallet.wallet_state_manager,chia.wallet.wallet_sync_store,chia.wallet.wallet_transaction_store,chia.wallet.wallet_user_store,chia.wallet.wallet_weight_proof_handler,installhelper,tests.blockchain.blockchain_test_utils,tests.blockchain.test_blockchain,tests.blockchain.test_blockchain_transactions,tests.block_tools,tests.build-init-files,tests.build-workflows,tests.clvm.coin_store,tests.clvm.test_chialisp_deserialization,tests.clvm.test_clvm_compilation,tests.clvm.test_program,tests.clvm.test_puzzle_compression,tests.clvm.test_puzzles,tests.clvm.test_serialized_program,tests.clvm.test_singletons,tests.clvm.test_spend_sim,tests.conftest,tests.connection_utils,tests.core.cmds.test_keys,tests.core.consensus.test_pot_iterations,tests.core.custom_types.test_coin,tests.core.custom_types.test_proof_of_space,tests.core.custom_types.test_spend_bundle,tests.core.daemon.test_daemon,tests.core.full_node.full_sync.test_full_sync,tests.core.full_node.stores.test_block_store,tests.core.full_node.stores.test_coin_store,tests.core.full_node.stores.test_full_node_store,tests.core.full_node.stores.test_hint_store,tests.core.full_node.stores.test_sync_store,tests.core.full_node.test_address_manager,tests.core.full_node.test_block_height_map,tests.core.full_node.test_conditions,tests.core.full_node.test_full_node,tests.core.full_node.test_mempool,tests.core.full_node.test_mempool_performance,tests.core.full_node.test_node_load,tests.core.full_node.test_peer_store_resolver,tests.core.full_node.test_performance,tests.core.full_node.test_transactions,tests.core.make_block_generator,tests.core.node_height,tests.core.server.test_dos,tests.core.server.test_rate_limits,tests.core.ssl.test_ssl,tests.core.test_cost_calculation,tests.core.test_crawler_rpc,tests.core.test_daemon_rpc,tests.core.test_db_conversion,tests.core.test_farmer_harvester_rpc,tests.core.test_filter,tests.core.test_full_node_rpc,tests.core.test_merkle_set,tests.core.test_setproctitle,tests.core.util.test_cached_bls,tests.core.util.test_config,tests.core.util.test_file_keyring_synchronization,tests.core.util.test_files,tests.core.util.test_keychain,tests.core.util.test_keyring_wrapper,tests.core.util.test_lru_cache,tests.core.util.test_significant_bits,tests.core.util.test_streamable,tests.core.util.test_type_checking,tests.farmer_harvester.test_farmer_harvester,tests.generator.test_compression,tests.generator.test_generator_types,tests.generator.test_list_to_batches,tests.generator.test_rom,tests.generator.test_scan,tests.plotting.test_plot_manager,tests.pools.test_pool_cmdline,tests.pools.test_pool_config,tests.pools.test_pool_puzzles_lifecycle,tests.pools.test_pool_rpc,tests.pools.test_wallet_pool_store,tests.setup_nodes,tests.simulation.test_simulation,tests.time_out_assert,tests.tools.test_full_sync,tests.tools.test_run_block,tests.util.alert_server,tests.util.benchmark_cost,tests.util.blockchain,tests.util.build_network_protocol_files,tests.util.db_connection,tests.util.generator_tools_testing,tests.util.keyring,tests.util.key_tool,tests.util.misc,tests.util.network,tests.util.rpc,tests.util.test_full_block_utils,tests.util.test_lock_queue,tests.util.test_network_protocol_files,tests.util.test_struct_stream,tests.wallet.cat_wallet.test_cat_lifecycle,tests.wallet.cat_wallet.test_cat_wallet,tests.wallet.cat_wallet.test_offer_lifecycle,tests.wallet.cat_wallet.test_trades,tests.wallet.did_wallet.test_did,tests.wallet.did_wallet.test_did_rpc,tests.wallet.rl_wallet.test_rl_rpc,tests.wallet.rl_wallet.test_rl_wallet,tests.wallet.rpc.test_wallet_rpc,tests.wallet.simple_sync.test_simple_sync_protocol,tests.wallet.sync.test_wallet_sync,tests.wallet.test_bech32m,tests.wallet.test_chialisp,tests.wallet.test_puzzle_store,tests.wallet.test_singleton,tests.wallet.test_singleton_lifecycle,tests.wallet.test_singleton_lifecycle_fast,tests.wallet.test_taproot,tests.wallet.test_wallet,tests.wallet.test_wallet_blockchain,tests.wallet.test_wallet_interested_store,tests.wallet.test_wallet_key_val_store,tests.wallet.test_wallet_user_store,tests.wallet_tools,tests.weight_proof.test_weight_proof,tools.analyze-chain,tools.run_block,tools.test_full_sync] +[mypy-benchmarks.block_ref,benchmarks.block_store,benchmarks.coin_store,benchmarks.utils,build_scripts.installer-version,chia.clvm.spend_sim,chia.cmds.configure,chia.cmds.db,chia.cmds.db_upgrade_func,chia.cmds.farm_funcs,chia.cmds.init,chia.cmds.init_funcs,chia.cmds.keys,chia.cmds.keys_funcs,chia.cmds.passphrase,chia.cmds.passphrase_funcs,chia.cmds.plotnft,chia.cmds.plotnft_funcs,chia.cmds.plots,chia.cmds.plotters,chia.cmds.show,chia.cmds.start_funcs,chia.cmds.wallet,chia.cmds.wallet_funcs,chia.consensus.block_body_validation,chia.consensus.blockchain,chia.consensus.blockchain_interface,chia.consensus.block_creation,chia.consensus.block_header_validation,chia.consensus.block_record,chia.consensus.block_root_validation,chia.consensus.coinbase,chia.consensus.constants,chia.consensus.difficulty_adjustment,chia.consensus.get_block_challenge,chia.consensus.multiprocess_validation,chia.consensus.pos_quality,chia.consensus.vdf_info_computation,chia.daemon.client,chia.daemon.keychain_proxy,chia.daemon.keychain_server,chia.daemon.server,chia.farmer.farmer,chia.farmer.farmer_api,chia.full_node.block_height_map,chia.full_node.block_store,chia.full_node.bundle_tools,chia.full_node.coin_store,chia.full_node.full_node,chia.full_node.full_node_api,chia.full_node.full_node_store,chia.full_node.generator,chia.full_node.hint_store,chia.full_node.lock_queue,chia.full_node.mempool,chia.full_node.mempool_check_conditions,chia.full_node.mempool_manager,chia.full_node.pending_tx_cache,chia.full_node.sync_store,chia.full_node.weight_proof,chia.harvester.harvester,chia.harvester.harvester_api,chia.introducer.introducer,chia.introducer.introducer_api,chia.plotters.bladebit,chia.plotters.chiapos,chia.plotters.install_plotter,chia.plotters.madmax,chia.plotters.plotters,chia.plotters.plotters_util,chia.plotting.check_plots,chia.plotting.create_plots,chia.plotting.manager,chia.plotting.util,chia.pools.pool_config,chia.pools.pool_puzzles,chia.pools.pool_wallet,chia.pools.pool_wallet_info,chia.protocols.pool_protocol,chia.rpc.crawler_rpc_api,chia.rpc.farmer_rpc_api,chia.rpc.farmer_rpc_client,chia.rpc.full_node_rpc_api,chia.rpc.full_node_rpc_client,chia.rpc.harvester_rpc_api,chia.rpc.harvester_rpc_client,chia.rpc.rpc_client,chia.rpc.rpc_server,chia.rpc.timelord_rpc_api,chia.rpc.util,chia.rpc.wallet_rpc_api,chia.rpc.wallet_rpc_client,chia.seeder.crawler,chia.seeder.crawler_api,chia.seeder.crawl_store,chia.seeder.dns_server,chia.seeder.peer_record,chia.seeder.start_crawler,chia.server.address_manager,chia.server.address_manager_store,chia.server.connection_utils,chia.server.introducer_peers,chia.server.node_discovery,chia.server.peer_store_resolver,chia.server.rate_limits,chia.server.reconnect_task,chia.server.server,chia.server.ssl_context,chia.server.start_farmer,chia.server.start_full_node,chia.server.start_harvester,chia.server.start_introducer,chia.server.start_service,chia.server.start_timelord,chia.server.start_wallet,chia.server.upnp,chia.server.ws_connection,chia.simulator.full_node_simulator,chia.simulator.start_simulator,chia.ssl.create_ssl,chia.timelord.iters_from_block,chia.timelord.timelord,chia.timelord.timelord_api,chia.timelord.timelord_launcher,chia.timelord.timelord_state,chia.types.announcement,chia.types.blockchain_format.classgroup,chia.types.blockchain_format.coin,chia.types.blockchain_format.program,chia.types.blockchain_format.proof_of_space,chia.types.blockchain_format.tree_hash,chia.types.blockchain_format.vdf,chia.types.full_block,chia.types.header_block,chia.types.mempool_item,chia.types.name_puzzle_condition,chia.types.peer_info,chia.types.spend_bundle,chia.types.transaction_queue_entry,chia.types.unfinished_block,chia.types.unfinished_header_block,chia.util.api_decorators,chia.util.block_cache,chia.util.byte_types,chia.util.cached_bls,chia.util.check_fork_next_block,chia.util.chia_logging,chia.util.config,chia.util.db_wrapper,chia.util.dump_keyring,chia.util.file_keyring,chia.util.files,chia.util.hash,chia.util.ints,chia.util.json_util,chia.util.keychain,chia.util.keyring_wrapper,chia.util.log_exceptions,chia.util.lru_cache,chia.util.make_test_constants,chia.util.merkle_set,chia.util.network,chia.util.partial_func,chia.util.pip_import,chia.util.profiler,chia.util.safe_cancel_task,chia.util.service_groups,chia.util.ssl_check,chia.util.streamable,chia.util.struct_stream,chia.util.type_checking,chia.util.validate_alert,chia.wallet.block_record,chia.wallet.cat_wallet.cat_utils,chia.wallet.cat_wallet.cat_wallet,chia.wallet.cat_wallet.lineage_store,chia.wallet.chialisp,chia.wallet.did_wallet.did_wallet,chia.wallet.did_wallet.did_wallet_puzzles,chia.wallet.key_val_store,chia.wallet.lineage_proof,chia.wallet.payment,chia.wallet.puzzles.genesis_checkers,chia.wallet.puzzles.load_clvm,chia.wallet.puzzles.p2_conditions,chia.wallet.puzzles.p2_delegated_conditions,chia.wallet.puzzles.p2_delegated_puzzle,chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle,chia.wallet.puzzles.p2_m_of_n_delegate_direct,chia.wallet.puzzles.p2_puzzle_hash,chia.wallet.puzzles.prefarm.spend_prefarm,chia.wallet.puzzles.puzzle_utils,chia.wallet.puzzles.rom_bootstrap_generator,chia.wallet.puzzles.singleton_top_layer,chia.wallet.puzzles.tails,chia.wallet.rl_wallet.rl_wallet,chia.wallet.rl_wallet.rl_wallet_puzzles,chia.wallet.secret_key_store,chia.wallet.settings.user_settings,chia.wallet.trade_manager,chia.wallet.trade_record,chia.wallet.trading.offer,chia.wallet.trading.trade_store,chia.wallet.transaction_record,chia.wallet.util.debug_spend_bundle,chia.wallet.util.new_peak_queue,chia.wallet.util.peer_request_cache,chia.wallet.util.wallet_sync_utils,chia.wallet.wallet,chia.wallet.wallet_action_store,chia.wallet.wallet_blockchain,chia.wallet.wallet_coin_store,chia.wallet.wallet_interested_store,chia.wallet.wallet_node,chia.wallet.wallet_node_api,chia.wallet.wallet_pool_store,chia.wallet.wallet_puzzle_store,chia.wallet.wallet_state_manager,chia.wallet.wallet_sync_store,chia.wallet.wallet_transaction_store,chia.wallet.wallet_user_store,chia.wallet.wallet_weight_proof_handler,installhelper,tests.blockchain.blockchain_test_utils,tests.blockchain.test_blockchain,tests.blockchain.test_blockchain_transactions,tests.block_tools,tests.build-init-files,tests.build-workflows,tests.clvm.coin_store,tests.clvm.test_chialisp_deserialization,tests.clvm.test_clvm_compilation,tests.clvm.test_program,tests.clvm.test_puzzle_compression,tests.clvm.test_puzzles,tests.clvm.test_serialized_program,tests.clvm.test_singletons,tests.clvm.test_spend_sim,tests.conftest,tests.connection_utils,tests.core.cmds.test_keys,tests.core.consensus.test_pot_iterations,tests.core.custom_types.test_coin,tests.core.custom_types.test_proof_of_space,tests.core.custom_types.test_spend_bundle,tests.core.daemon.test_daemon,tests.core.full_node.full_sync.test_full_sync,tests.core.full_node.stores.test_block_store,tests.core.full_node.stores.test_coin_store,tests.core.full_node.stores.test_full_node_store,tests.core.full_node.stores.test_hint_store,tests.core.full_node.stores.test_sync_store,tests.core.full_node.test_address_manager,tests.core.full_node.test_block_height_map,tests.core.full_node.test_conditions,tests.core.full_node.test_full_node,tests.core.full_node.test_mempool,tests.core.full_node.test_mempool_performance,tests.core.full_node.test_node_load,tests.core.full_node.test_peer_store_resolver,tests.core.full_node.test_performance,tests.core.full_node.test_transactions,tests.core.make_block_generator,tests.core.node_height,tests.core.server.test_dos,tests.core.server.test_rate_limits,tests.core.ssl.test_ssl,tests.core.test_cost_calculation,tests.core.test_crawler_rpc,tests.core.test_daemon_rpc,tests.core.test_db_conversion,tests.core.test_farmer_harvester_rpc,tests.core.test_filter,tests.core.test_full_node_rpc,tests.core.test_merkle_set,tests.core.test_setproctitle,tests.core.util.test_cached_bls,tests.core.util.test_config,tests.core.util.test_file_keyring_synchronization,tests.core.util.test_files,tests.core.util.test_keychain,tests.core.util.test_keyring_wrapper,tests.core.util.test_lru_cache,tests.core.util.test_significant_bits,tests.core.util.test_streamable,tests.core.util.test_type_checking,tests.farmer_harvester.test_farmer_harvester,tests.generator.test_compression,tests.generator.test_generator_types,tests.generator.test_list_to_batches,tests.generator.test_rom,tests.generator.test_scan,tests.plotting.test_plot_manager,tests.pools.test_pool_cmdline,tests.pools.test_pool_config,tests.pools.test_pool_puzzles_lifecycle,tests.pools.test_pool_rpc,tests.pools.test_wallet_pool_store,tests.setup_nodes,tests.setup_services,tests.simulation.test_simulation,tests.time_out_assert,tests.tools.test_full_sync,tests.tools.test_run_block,tests.util.alert_server,tests.util.benchmark_cost,tests.util.blockchain,tests.util.build_network_protocol_files,tests.util.db_connection,tests.util.generator_tools_testing,tests.util.keyring,tests.util.key_tool,tests.util.misc,tests.util.network,tests.util.rpc,tests.util.test_full_block_utils,tests.util.test_lock_queue,tests.util.test_network_protocol_files,tests.util.test_struct_stream,tests.wallet.cat_wallet.test_cat_lifecycle,tests.wallet.cat_wallet.test_cat_wallet,tests.wallet.cat_wallet.test_offer_lifecycle,tests.wallet.cat_wallet.test_trades,tests.wallet.did_wallet.test_did,tests.wallet.did_wallet.test_did_rpc,tests.wallet.rl_wallet.test_rl_rpc,tests.wallet.rl_wallet.test_rl_wallet,tests.wallet.rpc.test_wallet_rpc,tests.wallet.simple_sync.test_simple_sync_protocol,tests.wallet.sync.test_wallet_sync,tests.wallet.test_bech32m,tests.wallet.test_chialisp,tests.wallet.test_puzzle_store,tests.wallet.test_singleton,tests.wallet.test_singleton_lifecycle,tests.wallet.test_singleton_lifecycle_fast,tests.wallet.test_taproot,tests.wallet.test_wallet,tests.wallet.test_wallet_blockchain,tests.wallet.test_wallet_interested_store,tests.wallet.test_wallet_key_val_store,tests.wallet.test_wallet_user_store,tests.wallet_tools,tests.weight_proof.test_weight_proof,tools.analyze-chain,tools.run_block,tools.test_full_sync] disallow_any_generics = False disallow_subclassing_any = False disallow_untyped_calls = False diff --git a/tests/blockchain/test_blockchain_transactions.py b/tests/blockchain/test_blockchain_transactions.py index 895d5d2b1212..1d0c830ac096 100644 --- a/tests/blockchain/test_blockchain_transactions.py +++ b/tests/blockchain/test_blockchain_transactions.py @@ -1,7 +1,6 @@ import logging import pytest -import pytest_asyncio from clvm.casts import int_to_bytes from chia.protocols import full_node_protocol, wallet_protocol @@ -13,7 +12,7 @@ from chia.util.ints import uint64 from tests.blockchain.blockchain_test_utils import _validate_and_add_block from tests.wallet_tools import WalletTool -from tests.setup_nodes import setup_two_nodes, test_constants +from tests.setup_nodes import test_constants from tests.util.generator_tools_testing import run_and_get_removals_and_additions BURN_PUZZLE_HASH = b"0" * 32 @@ -24,12 +23,6 @@ log = logging.getLogger(__name__) -@pytest_asyncio.fixture(scope="function") -async def two_nodes(db_version, self_hostname): - async for _ in setup_two_nodes(test_constants, db_version=db_version, self_hostname=self_hostname): - yield _ - - class TestBlockchainTransactions: @pytest.mark.asyncio async def test_basic_blockchain_tx(self, two_nodes, bt): diff --git a/tests/conftest.py b/tests/conftest.py index eb1c32d6cac2..6a0ee5f4d071 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,6 +5,8 @@ import pytest_asyncio import tempfile +from tests.setup_nodes import setup_node_and_wallet, setup_n_nodes, setup_two_nodes + # Set spawn after stdlib imports, but before other imports multiprocessing.set_start_method("spawn") @@ -141,3 +143,33 @@ def pytest_exception_interact(call): @pytest.hookimpl(tryfirst=True) def pytest_internalerror(excinfo): raise excinfo.value + + +@pytest_asyncio.fixture(scope="function") +async def wallet_node(self_hostname): + async for _ in setup_node_and_wallet(test_constants, self_hostname): + yield _ + + +@pytest_asyncio.fixture(scope="function") +async def two_nodes(db_version, self_hostname): + async for _ in setup_two_nodes(test_constants, db_version=db_version, self_hostname=self_hostname): + yield _ + + +@pytest_asyncio.fixture(scope="function") +async def three_nodes(db_version, self_hostname): + async for _ in setup_n_nodes(test_constants, 3, db_version=db_version, self_hostname=self_hostname): + yield _ + + +@pytest_asyncio.fixture(scope="function") +async def four_nodes(db_version, self_hostname): + async for _ in setup_n_nodes(test_constants, 4, db_version=db_version, self_hostname=self_hostname): + yield _ + + +@pytest_asyncio.fixture(scope="function") +async def five_nodes(db_version, self_hostname): + async for _ in setup_n_nodes(test_constants, 5, db_version=db_version, self_hostname=self_hostname): + yield _ diff --git a/tests/core/full_node/full_sync/test_full_sync.py b/tests/core/full_node/full_sync/test_full_sync.py index e13b1489cd83..0444f827a10c 100644 --- a/tests/core/full_node/full_sync/test_full_sync.py +++ b/tests/core/full_node/full_sync/test_full_sync.py @@ -5,7 +5,6 @@ from typing import List import pytest -import pytest_asyncio from chia.full_node.weight_proof import _validate_sub_epoch_summaries from chia.protocols import full_node_protocol @@ -15,37 +14,13 @@ from chia.util.hash import std_hash from chia.util.ints import uint16 from tests.core.node_height import node_height_exactly, node_height_between -from tests.setup_nodes import setup_n_nodes, setup_two_nodes, test_constants +from tests.setup_nodes import test_constants from tests.time_out_assert import time_out_assert log = logging.getLogger(__name__) -@pytest_asyncio.fixture(scope="function") -async def two_nodes(db_version, self_hostname): - async for _ in setup_two_nodes(test_constants, db_version=db_version, self_hostname=self_hostname): - yield _ - - -@pytest_asyncio.fixture(scope="function") -async def three_nodes(db_version, self_hostname): - async for _ in setup_n_nodes(test_constants, 3, db_version=db_version, self_hostname=self_hostname): - yield _ - - -@pytest_asyncio.fixture(scope="function") -async def four_nodes(db_version, self_hostname): - async for _ in setup_n_nodes(test_constants, 4, db_version=db_version, self_hostname=self_hostname): - yield _ - - -@pytest_asyncio.fixture(scope="function") -async def five_nodes(db_version, self_hostname): - async for _ in setup_n_nodes(test_constants, 5, db_version=db_version, self_hostname=self_hostname): - yield _ - - class TestFullSync: @pytest.mark.asyncio async def test_long_sync_from_zero(self, five_nodes, default_400_blocks, bt, self_hostname): diff --git a/tests/core/full_node/test_node_load.py b/tests/core/full_node/test_node_load.py index 94e500e18729..bcbd7de412a1 100644 --- a/tests/core/full_node/test_node_load.py +++ b/tests/core/full_node/test_node_load.py @@ -1,22 +1,14 @@ import time import pytest -import pytest_asyncio from chia.protocols import full_node_protocol from chia.types.peer_info import PeerInfo from chia.util.ints import uint16 from tests.connection_utils import connect_and_get_peer -from tests.setup_nodes import setup_two_nodes, test_constants from tests.time_out_assert import time_out_assert -@pytest_asyncio.fixture(scope="function") -async def two_nodes(db_version, self_hostname): - async for _ in setup_two_nodes(test_constants, db_version=db_version, self_hostname=self_hostname): - yield _ - - class TestNodeLoad: @pytest.mark.asyncio async def test_blocks_load(self, bt, two_nodes, self_hostname): diff --git a/tests/setup_nodes.py b/tests/setup_nodes.py index 6fc0233043a0..e1de55f010d3 100644 --- a/tests/setup_nodes.py +++ b/tests/setup_nodes.py @@ -1,26 +1,24 @@ import logging import asyncio -import signal -import sqlite3 from secrets import token_bytes -from typing import Dict, List, Optional, AsyncGenerator +from typing import Dict, List from chia.consensus.constants import ConsensusConstants -from chia.daemon.server import WebSocketServer, create_server_for_daemon, daemon_launch_lock_path, singleton from chia.full_node.full_node_api import FullNodeAPI -from chia.server.start_farmer import service_kwargs_for_farmer -from chia.server.start_full_node import service_kwargs_for_full_node -from chia.server.start_harvester import service_kwargs_for_harvester -from chia.server.start_introducer import service_kwargs_for_introducer from chia.server.start_service import Service -from chia.server.start_timelord import service_kwargs_for_timelord from chia.server.start_wallet import service_kwargs_for_wallet -from chia.simulator.start_simulator import service_kwargs_for_full_node_simulator -from chia.timelord.timelord_launcher import kill_processes, spawn_process -from chia.types.peer_info import PeerInfo -from chia.util.bech32m import encode_puzzle_hash from tests.block_tools import create_block_tools_async, test_constants, BlockTools +from tests.setup_services import ( + setup_full_node, + setup_harvester, + setup_farmer, + setup_introducer, + setup_vdf_clients, + setup_timelord, + setup_vdf_client, + setup_daemon, +) from tests.util.keyring import TempKeyring from tests.util.socket import find_available_listen_port from chia.util.hash import std_hash @@ -49,93 +47,6 @@ async def _teardown_nodes(node_aiters: List) -> None: pass -async def setup_daemon(btools: BlockTools) -> AsyncGenerator[WebSocketServer, None]: - root_path = btools.root_path - config = btools.config - assert "daemon_port" in config - lockfile = singleton(daemon_launch_lock_path(root_path)) - crt_path = root_path / config["daemon_ssl"]["private_crt"] - key_path = root_path / config["daemon_ssl"]["private_key"] - ca_crt_path = root_path / config["private_ssl_ca"]["crt"] - ca_key_path = root_path / config["private_ssl_ca"]["key"] - assert lockfile is not None - create_server_for_daemon(btools.root_path) - ws_server = WebSocketServer(root_path, ca_crt_path, ca_key_path, crt_path, key_path) - await ws_server.start() - - yield ws_server - - await ws_server.stop() - - -async def setup_full_node( - consensus_constants: ConsensusConstants, - db_name, - self_hostname: str, - port, - rpc_port, - local_bt: BlockTools, - introducer_port=None, - simulator=False, - send_uncompact_interval=0, - sanitize_weight_proof_only=False, - connect_to_daemon=False, - db_version=1, -): - db_path = local_bt.root_path / f"{db_name}" - if db_path.exists(): - db_path.unlink() - - if db_version > 1: - with sqlite3.connect(db_path) as connection: - connection.execute("CREATE TABLE database_version(version int)") - connection.execute("INSERT INTO database_version VALUES (?)", (db_version,)) - connection.commit() - - if connect_to_daemon: - assert local_bt.config["daemon_port"] is not None - config = local_bt.config["full_node"] - - config["database_path"] = db_name - config["send_uncompact_interval"] = send_uncompact_interval - config["target_uncompact_proofs"] = 30 - config["peer_connect_interval"] = 50 - config["sanitize_weight_proof_only"] = sanitize_weight_proof_only - if introducer_port is not None: - config["introducer_peer"]["host"] = self_hostname - config["introducer_peer"]["port"] = introducer_port - else: - config["introducer_peer"] = None - config["dns_servers"] = [] - config["port"] = port - config["rpc_port"] = rpc_port - overrides = config["network_overrides"]["constants"][config["selected_network"]] - updated_constants = consensus_constants.replace_str_to_bytes(**overrides) - if simulator: - kwargs = service_kwargs_for_full_node_simulator(local_bt.root_path, config, local_bt) - else: - kwargs = service_kwargs_for_full_node(local_bt.root_path, config, updated_constants) - - kwargs.update( - parse_cli_args=False, - connect_to_daemon=connect_to_daemon, - service_name_prefix="test_", - ) - - service = Service(**kwargs, handle_signals=False) - - await service.start() - - yield service._api - - service.stop() - await service.wait_closed() - if db_path.exists(): - db_path.unlink() - - -# Note: convert these setup functions to fixtures, or push it one layer up, -# keeping these usable independently? async def setup_wallet_node( self_hostname: str, port, @@ -206,164 +117,6 @@ async def setup_wallet_node( keychain.delete_all_keys() -async def setup_harvester( - b_tools: BlockTools, - self_hostname: str, - port, - rpc_port, - farmer_port, - consensus_constants: ConsensusConstants, - start_service: bool = True, -): - - config = b_tools.config["harvester"] - config["port"] = port - config["rpc_port"] = rpc_port - kwargs = service_kwargs_for_harvester(b_tools.root_path, config, consensus_constants) - kwargs.update( - server_listen_ports=[port], - advertised_port=port, - connect_peers=[PeerInfo(self_hostname, farmer_port)], - parse_cli_args=False, - connect_to_daemon=False, - service_name_prefix="test_", - ) - - service = Service(**kwargs, handle_signals=False) - - if start_service: - await service.start() - - yield service - - service.stop() - await service.wait_closed() - - -async def setup_farmer( - b_tools: BlockTools, - self_hostname: str, - port, - rpc_port, - consensus_constants: ConsensusConstants, - full_node_port: Optional[uint16] = None, - start_service: bool = True, -): - config = b_tools.config["farmer"] - config_pool = b_tools.config["pool"] - - config["xch_target_address"] = encode_puzzle_hash(b_tools.farmer_ph, "xch") - config["pool_public_keys"] = [bytes(pk).hex() for pk in b_tools.pool_pubkeys] - config["port"] = port - config["rpc_port"] = rpc_port - config_pool["xch_target_address"] = encode_puzzle_hash(b_tools.pool_ph, "xch") - - if full_node_port: - config["full_node_peer"]["host"] = self_hostname - config["full_node_peer"]["port"] = full_node_port - else: - del config["full_node_peer"] - - kwargs = service_kwargs_for_farmer( - b_tools.root_path, config, config_pool, consensus_constants, b_tools.local_keychain - ) - kwargs.update( - parse_cli_args=False, - connect_to_daemon=False, - service_name_prefix="test_", - ) - - service = Service(**kwargs, handle_signals=False) - - if start_service: - await service.start() - - yield service - - service.stop() - await service.wait_closed() - - -async def setup_introducer(bt: BlockTools, port): - kwargs = service_kwargs_for_introducer( - bt.root_path, - bt.config["introducer"], - ) - kwargs.update( - advertised_port=port, - parse_cli_args=False, - connect_to_daemon=False, - service_name_prefix="test_", - ) - - service = Service(**kwargs, handle_signals=False) - - await service.start() - - yield service._api, service._node.server - - service.stop() - await service.wait_closed() - - -async def setup_vdf_client(bt: BlockTools, self_hostname: str, port): - vdf_task_1 = asyncio.create_task(spawn_process(self_hostname, port, 1, bt.config.get("prefer_ipv6"))) - - def stop(): - asyncio.create_task(kill_processes()) - - asyncio.get_running_loop().add_signal_handler(signal.SIGTERM, stop) - asyncio.get_running_loop().add_signal_handler(signal.SIGINT, stop) - - yield vdf_task_1 - await kill_processes() - - -async def setup_vdf_clients(bt: BlockTools, self_hostname: str, port): - vdf_task_1 = asyncio.create_task(spawn_process(self_hostname, port, 1, bt.config.get("prefer_ipv6"))) - vdf_task_2 = asyncio.create_task(spawn_process(self_hostname, port, 2, bt.config.get("prefer_ipv6"))) - vdf_task_3 = asyncio.create_task(spawn_process(self_hostname, port, 3, bt.config.get("prefer_ipv6"))) - - def stop(): - asyncio.create_task(kill_processes()) - - asyncio.get_running_loop().add_signal_handler(signal.SIGTERM, stop) - asyncio.get_running_loop().add_signal_handler(signal.SIGINT, stop) - - yield vdf_task_1, vdf_task_2, vdf_task_3 - - await kill_processes() - - -async def setup_timelord( - port, full_node_port, rpc_port, vdf_port, sanitizer, consensus_constants: ConsensusConstants, b_tools: BlockTools -): - config = b_tools.config["timelord"] - config["port"] = port - config["full_node_peer"]["port"] = full_node_port - config["bluebox_mode"] = sanitizer - config["fast_algorithm"] = False - config["vdf_server"]["port"] = vdf_port - config["start_rpc_server"] = True - config["rpc_port"] = rpc_port - - kwargs = service_kwargs_for_timelord(b_tools.root_path, config, consensus_constants) - kwargs.update( - parse_cli_args=False, - connect_to_daemon=False, - service_name_prefix="test_", - ) - - service = Service(**kwargs, handle_signals=False) - - await service.start() - - yield service._api, service._node.server - - service.stop() - await service.wait_closed() - - async def setup_two_nodes(consensus_constants: ConsensusConstants, db_version: int, self_hostname: str): """ Setup and teardown of two full nodes, with blockchains and separate DBs. diff --git a/tests/setup_services.py b/tests/setup_services.py new file mode 100644 index 000000000000..2043ab26c149 --- /dev/null +++ b/tests/setup_services.py @@ -0,0 +1,341 @@ +import asyncio +import logging +import signal +import sqlite3 +from secrets import token_bytes +from typing import AsyncGenerator, Optional + +from chia.consensus.constants import ConsensusConstants +from chia.daemon.server import WebSocketServer, create_server_for_daemon, daemon_launch_lock_path, singleton +from chia.server.start_farmer import service_kwargs_for_farmer +from chia.server.start_full_node import service_kwargs_for_full_node +from chia.server.start_harvester import service_kwargs_for_harvester +from chia.server.start_introducer import service_kwargs_for_introducer +from chia.server.start_service import Service +from chia.server.start_timelord import service_kwargs_for_timelord +from chia.server.start_wallet import service_kwargs_for_wallet +from chia.simulator.start_simulator import service_kwargs_for_full_node_simulator +from chia.timelord.timelord_launcher import kill_processes, spawn_process +from chia.types.peer_info import PeerInfo +from chia.util.bech32m import encode_puzzle_hash +from chia.util.ints import uint16 +from chia.util.keychain import bytes_to_mnemonic +from tests.block_tools import BlockTools +from tests.util.keyring import TempKeyring + +log = logging.getLogger(__name__) + + +async def setup_daemon(btools: BlockTools) -> AsyncGenerator[WebSocketServer, None]: + root_path = btools.root_path + config = btools.config + assert "daemon_port" in config + lockfile = singleton(daemon_launch_lock_path(root_path)) + crt_path = root_path / config["daemon_ssl"]["private_crt"] + key_path = root_path / config["daemon_ssl"]["private_key"] + ca_crt_path = root_path / config["private_ssl_ca"]["crt"] + ca_key_path = root_path / config["private_ssl_ca"]["key"] + assert lockfile is not None + create_server_for_daemon(btools.root_path) + ws_server = WebSocketServer(root_path, ca_crt_path, ca_key_path, crt_path, key_path) + await ws_server.start() + + yield ws_server + + await ws_server.stop() + + +async def setup_full_node( + consensus_constants: ConsensusConstants, + db_name, + self_hostname: str, + port, + rpc_port, + local_bt: BlockTools, + introducer_port=None, + simulator=False, + send_uncompact_interval=0, + sanitize_weight_proof_only=False, + connect_to_daemon=False, + db_version=1, +): + db_path = local_bt.root_path / f"{db_name}" + if db_path.exists(): + db_path.unlink() + + if db_version > 1: + with sqlite3.connect(db_path) as connection: + connection.execute("CREATE TABLE database_version(version int)") + connection.execute("INSERT INTO database_version VALUES (?)", (db_version,)) + connection.commit() + + if connect_to_daemon: + assert local_bt.config["daemon_port"] is not None + config = local_bt.config["full_node"] + + config["database_path"] = db_name + config["send_uncompact_interval"] = send_uncompact_interval + config["target_uncompact_proofs"] = 30 + config["peer_connect_interval"] = 50 + config["sanitize_weight_proof_only"] = sanitize_weight_proof_only + if introducer_port is not None: + config["introducer_peer"]["host"] = self_hostname + config["introducer_peer"]["port"] = introducer_port + else: + config["introducer_peer"] = None + config["dns_servers"] = [] + config["port"] = port + config["rpc_port"] = rpc_port + overrides = config["network_overrides"]["constants"][config["selected_network"]] + updated_constants = consensus_constants.replace_str_to_bytes(**overrides) + if simulator: + kwargs = service_kwargs_for_full_node_simulator(local_bt.root_path, config, local_bt) + else: + kwargs = service_kwargs_for_full_node(local_bt.root_path, config, updated_constants) + + kwargs.update( + parse_cli_args=False, + connect_to_daemon=connect_to_daemon, + service_name_prefix="test_", + ) + + service = Service(**kwargs, handle_signals=False) + + await service.start() + + yield service._api + + service.stop() + await service.wait_closed() + if db_path.exists(): + db_path.unlink() + + +# Note: convert these setup functions to fixtures, or push it one layer up, +# keeping these usable independently? +async def setup_wallet_node( + self_hostname: str, + port, + rpc_port, + consensus_constants: ConsensusConstants, + local_bt: BlockTools, + full_node_port=None, + introducer_port=None, + key_seed=None, + starting_height=None, + initial_num_public_keys=5, +): + with TempKeyring(populate=True) as keychain: + config = local_bt.config["wallet"] + config["port"] = port + config["rpc_port"] = rpc_port + if starting_height is not None: + config["starting_height"] = starting_height + config["initial_num_public_keys"] = initial_num_public_keys + + entropy = token_bytes(32) + if key_seed is None: + key_seed = entropy + keychain.add_private_key(bytes_to_mnemonic(key_seed), "") + first_pk = keychain.get_first_public_key() + assert first_pk is not None + db_path_key_suffix = str(first_pk.get_fingerprint()) + db_name = f"test-wallet-db-{port}-KEY.sqlite" + db_path_replaced: str = db_name.replace("KEY", db_path_key_suffix) + db_path = local_bt.root_path / db_path_replaced + + if db_path.exists(): + db_path.unlink() + config["database_path"] = str(db_name) + config["testing"] = True + + config["introducer_peer"]["host"] = self_hostname + if introducer_port is not None: + config["introducer_peer"]["port"] = introducer_port + config["peer_connect_interval"] = 10 + else: + config["introducer_peer"] = None + + if full_node_port is not None: + config["full_node_peer"] = {} + config["full_node_peer"]["host"] = self_hostname + config["full_node_peer"]["port"] = full_node_port + else: + del config["full_node_peer"] + + kwargs = service_kwargs_for_wallet(local_bt.root_path, config, consensus_constants, keychain) + kwargs.update( + parse_cli_args=False, + connect_to_daemon=False, + service_name_prefix="test_", + ) + + service = Service(**kwargs, handle_signals=False) + + await service.start() + + yield service._node, service._node.server + + service.stop() + await service.wait_closed() + if db_path.exists(): + db_path.unlink() + keychain.delete_all_keys() + + +async def setup_harvester( + b_tools: BlockTools, + self_hostname: str, + port, + rpc_port, + farmer_port, + consensus_constants: ConsensusConstants, + start_service: bool = True, +): + + config = b_tools.config["harvester"] + config["port"] = port + config["rpc_port"] = rpc_port + kwargs = service_kwargs_for_harvester(b_tools.root_path, config, consensus_constants) + kwargs.update( + server_listen_ports=[port], + advertised_port=port, + connect_peers=[PeerInfo(self_hostname, farmer_port)], + parse_cli_args=False, + connect_to_daemon=False, + service_name_prefix="test_", + ) + + service = Service(**kwargs, handle_signals=False) + + if start_service: + await service.start() + + yield service + + service.stop() + await service.wait_closed() + + +async def setup_farmer( + b_tools: BlockTools, + self_hostname: str, + port, + rpc_port, + consensus_constants: ConsensusConstants, + full_node_port: Optional[uint16] = None, + start_service: bool = True, +): + config = b_tools.config["farmer"] + config_pool = b_tools.config["pool"] + + config["xch_target_address"] = encode_puzzle_hash(b_tools.farmer_ph, "xch") + config["pool_public_keys"] = [bytes(pk).hex() for pk in b_tools.pool_pubkeys] + config["port"] = port + config["rpc_port"] = rpc_port + config_pool["xch_target_address"] = encode_puzzle_hash(b_tools.pool_ph, "xch") + + if full_node_port: + config["full_node_peer"]["host"] = self_hostname + config["full_node_peer"]["port"] = full_node_port + else: + del config["full_node_peer"] + + kwargs = service_kwargs_for_farmer( + b_tools.root_path, config, config_pool, consensus_constants, b_tools.local_keychain + ) + kwargs.update( + parse_cli_args=False, + connect_to_daemon=False, + service_name_prefix="test_", + ) + + service = Service(**kwargs, handle_signals=False) + + if start_service: + await service.start() + + yield service + + service.stop() + await service.wait_closed() + + +async def setup_introducer(bt: BlockTools, port): + kwargs = service_kwargs_for_introducer( + bt.root_path, + bt.config["introducer"], + ) + kwargs.update( + advertised_port=port, + parse_cli_args=False, + connect_to_daemon=False, + service_name_prefix="test_", + ) + + service = Service(**kwargs, handle_signals=False) + + await service.start() + + yield service._api, service._node.server + + service.stop() + await service.wait_closed() + + +async def setup_vdf_client(bt: BlockTools, self_hostname: str, port): + vdf_task_1 = asyncio.create_task(spawn_process(self_hostname, port, 1, bt.config.get("prefer_ipv6"))) + + def stop(): + asyncio.create_task(kill_processes()) + + asyncio.get_running_loop().add_signal_handler(signal.SIGTERM, stop) + asyncio.get_running_loop().add_signal_handler(signal.SIGINT, stop) + + yield vdf_task_1 + await kill_processes() + + +async def setup_vdf_clients(bt: BlockTools, self_hostname: str, port): + vdf_task_1 = asyncio.create_task(spawn_process(self_hostname, port, 1, bt.config.get("prefer_ipv6"))) + vdf_task_2 = asyncio.create_task(spawn_process(self_hostname, port, 2, bt.config.get("prefer_ipv6"))) + vdf_task_3 = asyncio.create_task(spawn_process(self_hostname, port, 3, bt.config.get("prefer_ipv6"))) + + def stop(): + asyncio.create_task(kill_processes()) + + asyncio.get_running_loop().add_signal_handler(signal.SIGTERM, stop) + asyncio.get_running_loop().add_signal_handler(signal.SIGINT, stop) + + yield vdf_task_1, vdf_task_2, vdf_task_3 + + await kill_processes() + + +async def setup_timelord( + port, full_node_port, rpc_port, vdf_port, sanitizer, consensus_constants: ConsensusConstants, b_tools: BlockTools +): + config = b_tools.config["timelord"] + config["port"] = port + config["full_node_peer"]["port"] = full_node_port + config["bluebox_mode"] = sanitizer + config["fast_algorithm"] = False + config["vdf_server"]["port"] = vdf_port + config["start_rpc_server"] = True + config["rpc_port"] = rpc_port + + kwargs = service_kwargs_for_timelord(b_tools.root_path, config, consensus_constants) + kwargs.update( + parse_cli_args=False, + connect_to_daemon=False, + service_name_prefix="test_", + ) + + service = Service(**kwargs, handle_signals=False) + + await service.start() + + yield service._api, service._node.server + + service.stop() + await service.wait_closed() From 7bab39161f2516e6bf1974f6a54262626b69454e Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Wed, 16 Mar 2022 20:03:56 +0100 Subject: [PATCH 208/378] more entropy in random listen ports (#10743) --- tests/util/socket.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/util/socket.py b/tests/util/socket.py index 727e7e270df7..84630c6cad91 100644 --- a/tests/util/socket.py +++ b/tests/util/socket.py @@ -1,18 +1,15 @@ -import random import secrets import socket from typing import Set recent_ports: Set[int] = set() -prng = random.Random() -prng.seed(secrets.randbits(32)) def find_available_listen_port(name: str = "free") -> int: global recent_ports while True: - port = prng.randint(2000, 65535) + port = secrets.randbits(15) + 2000 if port in recent_ports: continue From da6f668f0c4416238c7991fbb53ea3b007fb9600 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Wed, 16 Mar 2022 19:21:46 -0400 Subject: [PATCH 209/378] update chia-blockchain-gui one commit for npm build fix (#10776) --- chia-blockchain-gui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia-blockchain-gui b/chia-blockchain-gui index c992d07c9565..80e8bcb83c8d 160000 --- a/chia-blockchain-gui +++ b/chia-blockchain-gui @@ -1 +1 @@ -Subproject commit c992d07c956501f92e84ead80127c6b1e882fc21 +Subproject commit 80e8bcb83c8dac3c2e37e40a7f701ad9842bb120 From bf2976a2a9dfdd1b71db48030ef79a4a571feae0 Mon Sep 17 00:00:00 2001 From: William Allen Date: Wed, 16 Mar 2022 19:08:33 -0500 Subject: [PATCH 210/378] Updating Changelog for point release (#10781) * Updating Changelog for point release * Adding missing changelog items --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e3d3c07a15f..35248b96e168 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,34 @@ for setuptools_scm/PEP 440 reasons. ## [Unreleased] +## 1.3.1 Chia blockchain 2022-3-16 + +### Fixed + +- Improved config.yaml update concurrency to prevent some cases of the wrong pool being used for a PlotNFT. +- Fixed `chia keys show` displaying non-observer-derived wallet address. +- Fixed `plotnft claim` returning an error. +- Fixed invalid DB commit that prevented rollback of coin store changes. +- Fixed locking issue with `PlotManager.plots` that caused high lookup times on plots. +- Fixed exception when `chia keys migrate` is run without needing migration. +- Fixed farmer rewards dialog (GUI). +- Fixed display of pool payout address (GUI). +- Fixed display of harvesters status when harvesters are restarted (GUI). +- Fixed wallet RPC `get_offers_count` returning an error when there are no trades (Thanks, @dkackman!) +- Fixed spelling of "genrated" (Thanks again, @dkackman!) +- Fixed typo "log_maxbytessrotation" in initial-config (@skweee made their first contribution!) + +### Added + +- Added checks to ensure wallet address prefixes are either `xch` or `txch`. +- Added a better TLS1.3 check to handle cases where python is using a non-openssl TLS library. + +### Changed + +- Update the database queries for the `block_count_metrics` RPC endpoint to utilize indexes effectively for V2 DBs. +- Several improvements to tests. + + ## 1.3.0 Chia blockchain 2022-3-07 ### Added: From 0e956c8b5a7f61fafbc9382e1e0e2f91bbe3ae77 Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Thu, 17 Mar 2022 16:36:51 +0100 Subject: [PATCH 211/378] run benchmarks separately (#10754) * run benchmarks separately * only run benchmarks once, with the most recent python version we support --- .github/workflows/benchmarks.yml | 69 +++++++++++++++++++ .../workflows/build-test-macos-blockchain.yml | 2 +- .github/workflows/build-test-macos-clvm.yml | 2 +- .../workflows/build-test-macos-core-cmds.yml | 2 +- .../build-test-macos-core-consensus.yml | 2 +- .../build-test-macos-core-custom_types.yml | 2 +- .../build-test-macos-core-daemon.yml | 2 +- ...ld-test-macos-core-full_node-full_sync.yml | 2 +- ...build-test-macos-core-full_node-stores.yml | 2 +- .../build-test-macos-core-full_node.yml | 2 +- .../build-test-macos-core-server.yml | 2 +- .../workflows/build-test-macos-core-ssl.yml | 2 +- .../workflows/build-test-macos-core-util.yml | 2 +- .github/workflows/build-test-macos-core.yml | 2 +- .../build-test-macos-farmer_harvester.yml | 2 +- .../workflows/build-test-macos-generator.yml | 2 +- .../workflows/build-test-macos-plotting.yml | 2 +- .github/workflows/build-test-macos-pools.yml | 2 +- .../workflows/build-test-macos-simulation.yml | 2 +- .github/workflows/build-test-macos-tools.yml | 2 +- .github/workflows/build-test-macos-util.yml | 2 +- .../build-test-macos-wallet-cat_wallet.yml | 2 +- .../build-test-macos-wallet-did_wallet.yml | 2 +- .../build-test-macos-wallet-rl_wallet.yml | 2 +- .../workflows/build-test-macos-wallet-rpc.yml | 2 +- .../build-test-macos-wallet-simple_sync.yml | 2 +- .../build-test-macos-wallet-sync.yml | 2 +- .github/workflows/build-test-macos-wallet.yml | 2 +- .../build-test-macos-weight_proof.yml | 2 +- .../build-test-ubuntu-blockchain.yml | 2 +- .github/workflows/build-test-ubuntu-clvm.yml | 2 +- .../workflows/build-test-ubuntu-core-cmds.yml | 2 +- .../build-test-ubuntu-core-consensus.yml | 2 +- .../build-test-ubuntu-core-custom_types.yml | 2 +- .../build-test-ubuntu-core-daemon.yml | 2 +- ...d-test-ubuntu-core-full_node-full_sync.yml | 2 +- ...uild-test-ubuntu-core-full_node-stores.yml | 2 +- .../build-test-ubuntu-core-full_node.yml | 2 +- .../build-test-ubuntu-core-server.yml | 2 +- .../workflows/build-test-ubuntu-core-ssl.yml | 2 +- .../workflows/build-test-ubuntu-core-util.yml | 2 +- .github/workflows/build-test-ubuntu-core.yml | 2 +- .../build-test-ubuntu-farmer_harvester.yml | 2 +- .../workflows/build-test-ubuntu-generator.yml | 2 +- .../workflows/build-test-ubuntu-plotting.yml | 2 +- .github/workflows/build-test-ubuntu-pools.yml | 2 +- .../build-test-ubuntu-simulation.yml | 2 +- .github/workflows/build-test-ubuntu-tools.yml | 2 +- .github/workflows/build-test-ubuntu-util.yml | 2 +- .../build-test-ubuntu-wallet-cat_wallet.yml | 2 +- .../build-test-ubuntu-wallet-did_wallet.yml | 2 +- .../build-test-ubuntu-wallet-rl_wallet.yml | 2 +- .../build-test-ubuntu-wallet-rpc.yml | 2 +- .../build-test-ubuntu-wallet-simple_sync.yml | 2 +- .../build-test-ubuntu-wallet-sync.yml | 2 +- .../workflows/build-test-ubuntu-wallet.yml | 2 +- .../build-test-ubuntu-weight_proof.yml | 2 +- tests/core/full_node/test_mempool.py | 30 +++++--- .../full_node/test_mempool_performance.py | 10 ++- tests/core/full_node/test_performance.py | 13 +++- tests/pytest.ini | 1 + tests/runner_templates/build-test-macos | 2 +- tests/runner_templates/build-test-ubuntu | 2 +- 63 files changed, 166 insertions(+), 73 deletions(-) create mode 100644 .github/workflows/benchmarks.yml diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml new file mode 100644 index 000000000000..bbcbf914566e --- /dev/null +++ b/.github/workflows/benchmarks.yml @@ -0,0 +1,69 @@ +name: Benchmarks + +on: + push: + branches: + - main + tags: + - '**' + pull_request: + branches: + - '**' + +concurrency: + # SHA is added to the end if on `main` to let all main workflows run + group: ${{ github.ref }}-${{ github.workflow }}-${{ github.event_name }}-${{ github.ref == 'refs/heads/main' && github.sha || '' }} + cancel-in-progress: true + +jobs: + build: + name: Benchmarks + runs-on: benchmark + timeout-minutes: 30 + strategy: + fail-fast: false + max-parallel: 4 + matrix: + python-version: [ 3.9 ] + + steps: + - name: Checkout Code + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Setup Python environment + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Get pip cache dir + id: pip-cache + run: | + echo "::set-output name=dir::$(pip cache dir)" + + - name: Cache pip + uses: actions/cache@v2.1.6 + with: + path: ${{ steps.pip-cache.outputs.dir }} + key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Install ubuntu dependencies + run: | + sudo apt-get install software-properties-common + sudo add-apt-repository ppa:deadsnakes/ppa + sudo apt-get update + sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y + + - name: Run install script + env: + INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} + run: | + sh install.sh -d + + - name: pytest + run: | + . ./activate + ./venv/bin/py.test -n 0 -m benchmark tests diff --git a/.github/workflows/build-test-macos-blockchain.yml b/.github/workflows/build-test-macos-blockchain.yml index c6bb2c7f27f6..3544e088deb1 100644 --- a/.github/workflows/build-test-macos-blockchain.yml +++ b/.github/workflows/build-test-macos-blockchain.yml @@ -94,7 +94,7 @@ jobs: - name: Test blockchain code with pytest run: | . ./activate - ./venv/bin/py.test tests/blockchain/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/blockchain/test_*.py -s -v --durations 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-clvm.yml b/.github/workflows/build-test-macos-clvm.yml index adf8d8100c51..62f6e8b94245 100644 --- a/.github/workflows/build-test-macos-clvm.yml +++ b/.github/workflows/build-test-macos-clvm.yml @@ -77,7 +77,7 @@ jobs: - name: Test clvm code with pytest run: | . ./activate - ./venv/bin/py.test tests/clvm/test_*.py -s -v --durations 0 -n auto + ./venv/bin/py.test tests/clvm/test_*.py -s -v --durations 0 -n auto -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-core-cmds.yml b/.github/workflows/build-test-macos-core-cmds.yml index 2faab8f8b9e6..f59195f6ebbe 100644 --- a/.github/workflows/build-test-macos-core-cmds.yml +++ b/.github/workflows/build-test-macos-core-cmds.yml @@ -94,7 +94,7 @@ jobs: - name: Test core-cmds code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/cmds/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/core/cmds/test_*.py -s -v --durations 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-core-consensus.yml b/.github/workflows/build-test-macos-core-consensus.yml index 6f02f78582f8..e3e7a3c8fa47 100644 --- a/.github/workflows/build-test-macos-core-consensus.yml +++ b/.github/workflows/build-test-macos-core-consensus.yml @@ -94,7 +94,7 @@ jobs: - name: Test core-consensus code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/consensus/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/core/consensus/test_*.py -s -v --durations 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-core-custom_types.yml b/.github/workflows/build-test-macos-core-custom_types.yml index 9cf67c6b7aab..a3935cfa82f6 100644 --- a/.github/workflows/build-test-macos-core-custom_types.yml +++ b/.github/workflows/build-test-macos-core-custom_types.yml @@ -94,7 +94,7 @@ jobs: - name: Test core-custom_types code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/custom_types/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/core/custom_types/test_*.py -s -v --durations 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-core-daemon.yml b/.github/workflows/build-test-macos-core-daemon.yml index c1725dee3e64..cc0b08fec987 100644 --- a/.github/workflows/build-test-macos-core-daemon.yml +++ b/.github/workflows/build-test-macos-core-daemon.yml @@ -94,7 +94,7 @@ jobs: - name: Test core-daemon code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/daemon/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/core/daemon/test_*.py -s -v --durations 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-core-full_node-full_sync.yml b/.github/workflows/build-test-macos-core-full_node-full_sync.yml index f801f20c1e58..f6de039e80ef 100644 --- a/.github/workflows/build-test-macos-core-full_node-full_sync.yml +++ b/.github/workflows/build-test-macos-core-full_node-full_sync.yml @@ -94,7 +94,7 @@ jobs: - name: Test core-full_node-full_sync code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/full_node/full_sync/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/core/full_node/full_sync/test_*.py -s -v --durations 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-core-full_node-stores.yml b/.github/workflows/build-test-macos-core-full_node-stores.yml index 27f54068e4d0..23bbde959313 100644 --- a/.github/workflows/build-test-macos-core-full_node-stores.yml +++ b/.github/workflows/build-test-macos-core-full_node-stores.yml @@ -94,7 +94,7 @@ jobs: - name: Test core-full_node-stores code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/full_node/stores/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/core/full_node/stores/test_*.py -s -v --durations 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-core-full_node.yml b/.github/workflows/build-test-macos-core-full_node.yml index 486377fbf5fa..68df7eb106dc 100644 --- a/.github/workflows/build-test-macos-core-full_node.yml +++ b/.github/workflows/build-test-macos-core-full_node.yml @@ -94,7 +94,7 @@ jobs: - name: Test core-full_node code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/full_node/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/core/full_node/test_*.py -s -v --durations 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-core-server.yml b/.github/workflows/build-test-macos-core-server.yml index c806200e7440..d94f8f91804b 100644 --- a/.github/workflows/build-test-macos-core-server.yml +++ b/.github/workflows/build-test-macos-core-server.yml @@ -94,7 +94,7 @@ jobs: - name: Test core-server code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/server/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/core/server/test_*.py -s -v --durations 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-core-ssl.yml b/.github/workflows/build-test-macos-core-ssl.yml index fcd56193e29c..4d5978f11202 100644 --- a/.github/workflows/build-test-macos-core-ssl.yml +++ b/.github/workflows/build-test-macos-core-ssl.yml @@ -94,7 +94,7 @@ jobs: - name: Test core-ssl code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/ssl/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/core/ssl/test_*.py -s -v --durations 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-core-util.yml b/.github/workflows/build-test-macos-core-util.yml index d481e26ab52b..3238ee35dcd4 100644 --- a/.github/workflows/build-test-macos-core-util.yml +++ b/.github/workflows/build-test-macos-core-util.yml @@ -94,7 +94,7 @@ jobs: - name: Test core-util code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/util/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/core/util/test_*.py -s -v --durations 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-core.yml b/.github/workflows/build-test-macos-core.yml index ea722cc6f7d6..3a1f0b87cd91 100644 --- a/.github/workflows/build-test-macos-core.yml +++ b/.github/workflows/build-test-macos-core.yml @@ -94,7 +94,7 @@ jobs: - name: Test core code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/core/test_*.py -s -v --durations 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-farmer_harvester.yml b/.github/workflows/build-test-macos-farmer_harvester.yml index a57032f46e46..8cc0e061dec6 100644 --- a/.github/workflows/build-test-macos-farmer_harvester.yml +++ b/.github/workflows/build-test-macos-farmer_harvester.yml @@ -94,7 +94,7 @@ jobs: - name: Test farmer_harvester code with pytest run: | . ./activate - ./venv/bin/py.test tests/farmer_harvester/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/farmer_harvester/test_*.py -s -v --durations 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-generator.yml b/.github/workflows/build-test-macos-generator.yml index 61ae6cd09d31..b199ec301f9c 100644 --- a/.github/workflows/build-test-macos-generator.yml +++ b/.github/workflows/build-test-macos-generator.yml @@ -94,7 +94,7 @@ jobs: - name: Test generator code with pytest run: | . ./activate - ./venv/bin/py.test tests/generator/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/generator/test_*.py -s -v --durations 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-plotting.yml b/.github/workflows/build-test-macos-plotting.yml index 822386e52f1b..1c66bc4f61c0 100644 --- a/.github/workflows/build-test-macos-plotting.yml +++ b/.github/workflows/build-test-macos-plotting.yml @@ -90,7 +90,7 @@ jobs: - name: Test plotting code with pytest run: | . ./activate - ./venv/bin/py.test tests/plotting/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/plotting/test_*.py -s -v --durations 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-pools.yml b/.github/workflows/build-test-macos-pools.yml index 88745eb56c3f..75ec2a74432f 100644 --- a/.github/workflows/build-test-macos-pools.yml +++ b/.github/workflows/build-test-macos-pools.yml @@ -94,7 +94,7 @@ jobs: - name: Test pools code with pytest run: | . ./activate - ./venv/bin/py.test tests/pools/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/pools/test_*.py -s -v --durations 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-simulation.yml b/.github/workflows/build-test-macos-simulation.yml index b7d1144f3108..ed3549d4b988 100644 --- a/.github/workflows/build-test-macos-simulation.yml +++ b/.github/workflows/build-test-macos-simulation.yml @@ -94,7 +94,7 @@ jobs: - name: Test simulation code with pytest run: | . ./activate - ./venv/bin/py.test tests/simulation/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/simulation/test_*.py -s -v --durations 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-tools.yml b/.github/workflows/build-test-macos-tools.yml index 39650a472bf4..c47c0607f446 100644 --- a/.github/workflows/build-test-macos-tools.yml +++ b/.github/workflows/build-test-macos-tools.yml @@ -94,7 +94,7 @@ jobs: - name: Test tools code with pytest run: | . ./activate - ./venv/bin/py.test tests/tools/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/tools/test_*.py -s -v --durations 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-util.yml b/.github/workflows/build-test-macos-util.yml index 22fe4d8fe25f..fce1cb87177a 100644 --- a/.github/workflows/build-test-macos-util.yml +++ b/.github/workflows/build-test-macos-util.yml @@ -94,7 +94,7 @@ jobs: - name: Test util code with pytest run: | . ./activate - ./venv/bin/py.test tests/util/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/util/test_*.py -s -v --durations 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-wallet-cat_wallet.yml b/.github/workflows/build-test-macos-wallet-cat_wallet.yml index 7c31d0c14f22..3f09eb94eaf7 100644 --- a/.github/workflows/build-test-macos-wallet-cat_wallet.yml +++ b/.github/workflows/build-test-macos-wallet-cat_wallet.yml @@ -94,7 +94,7 @@ jobs: - name: Test wallet-cat_wallet code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/cat_wallet/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/wallet/cat_wallet/test_*.py -s -v --durations 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-wallet-did_wallet.yml b/.github/workflows/build-test-macos-wallet-did_wallet.yml index 8a0d7989dc55..891d22e7a4fa 100644 --- a/.github/workflows/build-test-macos-wallet-did_wallet.yml +++ b/.github/workflows/build-test-macos-wallet-did_wallet.yml @@ -94,7 +94,7 @@ jobs: - name: Test wallet-did_wallet code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/did_wallet/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/wallet/did_wallet/test_*.py -s -v --durations 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-wallet-rl_wallet.yml b/.github/workflows/build-test-macos-wallet-rl_wallet.yml index d4fb6aef30b3..5e71af654633 100644 --- a/.github/workflows/build-test-macos-wallet-rl_wallet.yml +++ b/.github/workflows/build-test-macos-wallet-rl_wallet.yml @@ -94,7 +94,7 @@ jobs: - name: Test wallet-rl_wallet code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/rl_wallet/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/wallet/rl_wallet/test_*.py -s -v --durations 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-wallet-rpc.yml b/.github/workflows/build-test-macos-wallet-rpc.yml index 8260adfa87d7..61e70d4a7e13 100644 --- a/.github/workflows/build-test-macos-wallet-rpc.yml +++ b/.github/workflows/build-test-macos-wallet-rpc.yml @@ -94,7 +94,7 @@ jobs: - name: Test wallet-rpc code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/rpc/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/wallet/rpc/test_*.py -s -v --durations 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-wallet-simple_sync.yml b/.github/workflows/build-test-macos-wallet-simple_sync.yml index 749d43a4d573..821d7ebf3bb5 100644 --- a/.github/workflows/build-test-macos-wallet-simple_sync.yml +++ b/.github/workflows/build-test-macos-wallet-simple_sync.yml @@ -94,7 +94,7 @@ jobs: - name: Test wallet-simple_sync code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/simple_sync/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/wallet/simple_sync/test_*.py -s -v --durations 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-wallet-sync.yml b/.github/workflows/build-test-macos-wallet-sync.yml index 2cd5adc07dd8..6cd37b769dce 100644 --- a/.github/workflows/build-test-macos-wallet-sync.yml +++ b/.github/workflows/build-test-macos-wallet-sync.yml @@ -94,7 +94,7 @@ jobs: - name: Test wallet-sync code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/sync/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/wallet/sync/test_*.py -s -v --durations 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-wallet.yml b/.github/workflows/build-test-macos-wallet.yml index e49cff03afb8..52909bbf052f 100644 --- a/.github/workflows/build-test-macos-wallet.yml +++ b/.github/workflows/build-test-macos-wallet.yml @@ -94,7 +94,7 @@ jobs: - name: Test wallet code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/wallet/test_*.py -s -v --durations 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-weight_proof.yml b/.github/workflows/build-test-macos-weight_proof.yml index 3a937c25f7a5..b02acf646d56 100644 --- a/.github/workflows/build-test-macos-weight_proof.yml +++ b/.github/workflows/build-test-macos-weight_proof.yml @@ -94,7 +94,7 @@ jobs: - name: Test weight_proof code with pytest run: | . ./activate - ./venv/bin/py.test tests/weight_proof/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/weight_proof/test_*.py -s -v --durations 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-ubuntu-blockchain.yml b/.github/workflows/build-test-ubuntu-blockchain.yml index de87c3cbd58f..11cf3e722779 100644 --- a/.github/workflows/build-test-ubuntu-blockchain.yml +++ b/.github/workflows/build-test-ubuntu-blockchain.yml @@ -99,7 +99,7 @@ jobs: - name: Test blockchain code with pytest run: | . ./activate - ./venv/bin/py.test tests/blockchain/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/blockchain/test_*.py -s -v --durations 0 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-clvm.yml b/.github/workflows/build-test-ubuntu-clvm.yml index a216d18268bd..0056f77758af 100644 --- a/.github/workflows/build-test-ubuntu-clvm.yml +++ b/.github/workflows/build-test-ubuntu-clvm.yml @@ -82,7 +82,7 @@ jobs: - name: Test clvm code with pytest run: | . ./activate - ./venv/bin/py.test tests/clvm/test_*.py -s -v --durations 0 -n auto + ./venv/bin/py.test tests/clvm/test_*.py -s -v --durations 0 -n auto -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-core-cmds.yml b/.github/workflows/build-test-ubuntu-core-cmds.yml index 5d7cb372bcf8..f15c15411f96 100644 --- a/.github/workflows/build-test-ubuntu-core-cmds.yml +++ b/.github/workflows/build-test-ubuntu-core-cmds.yml @@ -99,7 +99,7 @@ jobs: - name: Test core-cmds code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/cmds/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/core/cmds/test_*.py -s -v --durations 0 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-core-consensus.yml b/.github/workflows/build-test-ubuntu-core-consensus.yml index 1899af939c86..ea62f3833cd6 100644 --- a/.github/workflows/build-test-ubuntu-core-consensus.yml +++ b/.github/workflows/build-test-ubuntu-core-consensus.yml @@ -99,7 +99,7 @@ jobs: - name: Test core-consensus code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/consensus/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/core/consensus/test_*.py -s -v --durations 0 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-core-custom_types.yml b/.github/workflows/build-test-ubuntu-core-custom_types.yml index 05be1fdda458..c969454d3d8e 100644 --- a/.github/workflows/build-test-ubuntu-core-custom_types.yml +++ b/.github/workflows/build-test-ubuntu-core-custom_types.yml @@ -99,7 +99,7 @@ jobs: - name: Test core-custom_types code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/custom_types/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/core/custom_types/test_*.py -s -v --durations 0 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-core-daemon.yml b/.github/workflows/build-test-ubuntu-core-daemon.yml index 93b5ebc48ec0..acc5d55b4011 100644 --- a/.github/workflows/build-test-ubuntu-core-daemon.yml +++ b/.github/workflows/build-test-ubuntu-core-daemon.yml @@ -99,7 +99,7 @@ jobs: - name: Test core-daemon code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/daemon/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/core/daemon/test_*.py -s -v --durations 0 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml b/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml index 7f69dd81fe39..cef93fea7173 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml @@ -99,7 +99,7 @@ jobs: - name: Test core-full_node-full_sync code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/full_node/full_sync/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/core/full_node/full_sync/test_*.py -s -v --durations 0 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-core-full_node-stores.yml b/.github/workflows/build-test-ubuntu-core-full_node-stores.yml index 43377a417133..edb7a655f0bd 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node-stores.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node-stores.yml @@ -99,7 +99,7 @@ jobs: - name: Test core-full_node-stores code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/full_node/stores/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/core/full_node/stores/test_*.py -s -v --durations 0 -m "not benchmark" - name: Check resource usage diff --git a/.github/workflows/build-test-ubuntu-core-full_node.yml b/.github/workflows/build-test-ubuntu-core-full_node.yml index 558c1df426ad..931f1949e85f 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node.yml @@ -99,7 +99,7 @@ jobs: - name: Test core-full_node code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/full_node/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/core/full_node/test_*.py -s -v --durations 0 -m "not benchmark" - name: Check resource usage diff --git a/.github/workflows/build-test-ubuntu-core-server.yml b/.github/workflows/build-test-ubuntu-core-server.yml index f29f2ee39d37..2f203b126e11 100644 --- a/.github/workflows/build-test-ubuntu-core-server.yml +++ b/.github/workflows/build-test-ubuntu-core-server.yml @@ -99,7 +99,7 @@ jobs: - name: Test core-server code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/server/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/core/server/test_*.py -s -v --durations 0 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-core-ssl.yml b/.github/workflows/build-test-ubuntu-core-ssl.yml index ea6c3298e609..ec908a60db15 100644 --- a/.github/workflows/build-test-ubuntu-core-ssl.yml +++ b/.github/workflows/build-test-ubuntu-core-ssl.yml @@ -99,7 +99,7 @@ jobs: - name: Test core-ssl code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/ssl/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/core/ssl/test_*.py -s -v --durations 0 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-core-util.yml b/.github/workflows/build-test-ubuntu-core-util.yml index b755c6e5cb32..8480a86c4e87 100644 --- a/.github/workflows/build-test-ubuntu-core-util.yml +++ b/.github/workflows/build-test-ubuntu-core-util.yml @@ -99,7 +99,7 @@ jobs: - name: Test core-util code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/util/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/core/util/test_*.py -s -v --durations 0 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-core.yml b/.github/workflows/build-test-ubuntu-core.yml index 70e2d15c3e04..c5794a6c708c 100644 --- a/.github/workflows/build-test-ubuntu-core.yml +++ b/.github/workflows/build-test-ubuntu-core.yml @@ -99,7 +99,7 @@ jobs: - name: Test core code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/core/test_*.py -s -v --durations 0 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-farmer_harvester.yml b/.github/workflows/build-test-ubuntu-farmer_harvester.yml index 204c6a7decf2..e83b86fd360a 100644 --- a/.github/workflows/build-test-ubuntu-farmer_harvester.yml +++ b/.github/workflows/build-test-ubuntu-farmer_harvester.yml @@ -99,7 +99,7 @@ jobs: - name: Test farmer_harvester code with pytest run: | . ./activate - ./venv/bin/py.test tests/farmer_harvester/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/farmer_harvester/test_*.py -s -v --durations 0 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-generator.yml b/.github/workflows/build-test-ubuntu-generator.yml index 5dbae29186e2..ab75eeb93656 100644 --- a/.github/workflows/build-test-ubuntu-generator.yml +++ b/.github/workflows/build-test-ubuntu-generator.yml @@ -99,7 +99,7 @@ jobs: - name: Test generator code with pytest run: | . ./activate - ./venv/bin/py.test tests/generator/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/generator/test_*.py -s -v --durations 0 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-plotting.yml b/.github/workflows/build-test-ubuntu-plotting.yml index 6368ca5a3529..8388efe74987 100644 --- a/.github/workflows/build-test-ubuntu-plotting.yml +++ b/.github/workflows/build-test-ubuntu-plotting.yml @@ -95,7 +95,7 @@ jobs: - name: Test plotting code with pytest run: | . ./activate - ./venv/bin/py.test tests/plotting/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/plotting/test_*.py -s -v --durations 0 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-pools.yml b/.github/workflows/build-test-ubuntu-pools.yml index 4fa27f077c90..56f1d0a0e98a 100644 --- a/.github/workflows/build-test-ubuntu-pools.yml +++ b/.github/workflows/build-test-ubuntu-pools.yml @@ -99,7 +99,7 @@ jobs: - name: Test pools code with pytest run: | . ./activate - ./venv/bin/py.test tests/pools/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/pools/test_*.py -s -v --durations 0 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-simulation.yml b/.github/workflows/build-test-ubuntu-simulation.yml index f17beda38279..9b8a221db590 100644 --- a/.github/workflows/build-test-ubuntu-simulation.yml +++ b/.github/workflows/build-test-ubuntu-simulation.yml @@ -99,7 +99,7 @@ jobs: - name: Test simulation code with pytest run: | . ./activate - ./venv/bin/py.test tests/simulation/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/simulation/test_*.py -s -v --durations 0 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-tools.yml b/.github/workflows/build-test-ubuntu-tools.yml index 9d2dd0738839..bea355f3cfca 100644 --- a/.github/workflows/build-test-ubuntu-tools.yml +++ b/.github/workflows/build-test-ubuntu-tools.yml @@ -99,7 +99,7 @@ jobs: - name: Test tools code with pytest run: | . ./activate - ./venv/bin/py.test tests/tools/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/tools/test_*.py -s -v --durations 0 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-util.yml b/.github/workflows/build-test-ubuntu-util.yml index 3473110fe511..01f55a6fb56d 100644 --- a/.github/workflows/build-test-ubuntu-util.yml +++ b/.github/workflows/build-test-ubuntu-util.yml @@ -99,7 +99,7 @@ jobs: - name: Test util code with pytest run: | . ./activate - ./venv/bin/py.test tests/util/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/util/test_*.py -s -v --durations 0 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml index a1392ae82787..dea76d7b1d61 100644 --- a/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml @@ -99,7 +99,7 @@ jobs: - name: Test wallet-cat_wallet code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/cat_wallet/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/wallet/cat_wallet/test_*.py -s -v --durations 0 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml index 799671937d4f..f7f311148cfc 100644 --- a/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml @@ -99,7 +99,7 @@ jobs: - name: Test wallet-did_wallet code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/did_wallet/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/wallet/did_wallet/test_*.py -s -v --durations 0 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml index 31943dc2d5ea..3e1ddf7287cf 100644 --- a/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml @@ -99,7 +99,7 @@ jobs: - name: Test wallet-rl_wallet code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/rl_wallet/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/wallet/rl_wallet/test_*.py -s -v --durations 0 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-wallet-rpc.yml b/.github/workflows/build-test-ubuntu-wallet-rpc.yml index ee48532a7893..76a1b30a6ad5 100644 --- a/.github/workflows/build-test-ubuntu-wallet-rpc.yml +++ b/.github/workflows/build-test-ubuntu-wallet-rpc.yml @@ -99,7 +99,7 @@ jobs: - name: Test wallet-rpc code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/rpc/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/wallet/rpc/test_*.py -s -v --durations 0 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml b/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml index 151dfec41120..34b6de85e407 100644 --- a/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml +++ b/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml @@ -99,7 +99,7 @@ jobs: - name: Test wallet-simple_sync code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/simple_sync/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/wallet/simple_sync/test_*.py -s -v --durations 0 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-wallet-sync.yml b/.github/workflows/build-test-ubuntu-wallet-sync.yml index 656e3c62a722..73d8c8e3ae76 100644 --- a/.github/workflows/build-test-ubuntu-wallet-sync.yml +++ b/.github/workflows/build-test-ubuntu-wallet-sync.yml @@ -99,7 +99,7 @@ jobs: - name: Test wallet-sync code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/sync/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/wallet/sync/test_*.py -s -v --durations 0 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-wallet.yml b/.github/workflows/build-test-ubuntu-wallet.yml index ebe8d2b490d3..35db826fb560 100644 --- a/.github/workflows/build-test-ubuntu-wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet.yml @@ -99,7 +99,7 @@ jobs: - name: Test wallet code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/wallet/test_*.py -s -v --durations 0 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-weight_proof.yml b/.github/workflows/build-test-ubuntu-weight_proof.yml index 619e33833944..d4d028f60377 100644 --- a/.github/workflows/build-test-ubuntu-weight_proof.yml +++ b/.github/workflows/build-test-ubuntu-weight_proof.yml @@ -99,7 +99,7 @@ jobs: - name: Test weight_proof code with pytest run: | . ./activate - ./venv/bin/py.test tests/weight_proof/test_*.py -s -v --durations 0 + ./venv/bin/py.test tests/weight_proof/test_*.py -s -v --durations 0 -m "not benchmark" # diff --git a/tests/core/full_node/test_mempool.py b/tests/core/full_node/test_mempool.py index eabfd9ba0025..64571fefb7d0 100644 --- a/tests/core/full_node/test_mempool.py +++ b/tests/core/full_node/test_mempool.py @@ -2183,6 +2183,7 @@ class TestMaliciousGenerators: ConditionOpcode.ASSERT_SECONDS_RELATIVE, ], ) + @pytest.mark.benchmark def test_duplicate_large_integer_ladder(self, opcode, softfork_height): condition = SINGLE_ARG_INT_LADDER_COND.format(opcode=opcode.value[0], num=28, filler="0x00") start_time = time() @@ -2199,8 +2200,8 @@ def test_duplicate_large_integer_ladder(self, opcode, softfork_height): [ConditionWithArgs(opcode, [int_to_bytes(28)])], ) ] - assert run_time < 1.5 print(f"run time:{run_time}") + assert run_time < 0.7 @pytest.mark.parametrize( "opcode", @@ -2211,6 +2212,7 @@ def test_duplicate_large_integer_ladder(self, opcode, softfork_height): ConditionOpcode.ASSERT_SECONDS_RELATIVE, ], ) + @pytest.mark.benchmark def test_duplicate_large_integer(self, opcode, softfork_height): condition = SINGLE_ARG_INT_COND.format(opcode=opcode.value[0], num=280000, val=100, filler="0x00") start_time = time() @@ -2227,8 +2229,8 @@ def test_duplicate_large_integer(self, opcode, softfork_height): [ConditionWithArgs(opcode, [bytes([100])])], ) ] - assert run_time < 2.5 print(f"run time:{run_time}") + assert run_time < 1.1 @pytest.mark.parametrize( "opcode", @@ -2239,6 +2241,7 @@ def test_duplicate_large_integer(self, opcode, softfork_height): ConditionOpcode.ASSERT_SECONDS_RELATIVE, ], ) + @pytest.mark.benchmark def test_duplicate_large_integer_substr(self, opcode, softfork_height): condition = SINGLE_ARG_INT_SUBSTR_COND.format(opcode=opcode.value[0], num=280000, val=100, filler="0x00") start_time = time() @@ -2255,8 +2258,8 @@ def test_duplicate_large_integer_substr(self, opcode, softfork_height): [ConditionWithArgs(opcode, [bytes([100])])], ) ] - assert run_time < 3 print(f"run time:{run_time}") + assert run_time < 1.1 @pytest.mark.parametrize( "opcode", @@ -2267,6 +2270,7 @@ def test_duplicate_large_integer_substr(self, opcode, softfork_height): ConditionOpcode.ASSERT_SECONDS_RELATIVE, ], ) + @pytest.mark.benchmark def test_duplicate_large_integer_substr_tail(self, opcode, softfork_height): condition = SINGLE_ARG_INT_SUBSTR_TAIL_COND.format( opcode=opcode.value[0], num=280, val="0xffffffff", filler="0x00" @@ -2282,8 +2286,8 @@ def test_duplicate_large_integer_substr_tail(self, opcode, softfork_height): print(npc_result.npc_list[0].conditions[0][1]) assert ConditionWithArgs(opcode, [int_to_bytes(0xFFFFFFFF)]) in npc_result.npc_list[0].conditions[0][1] - assert run_time < 1 print(f"run time:{run_time}") + assert run_time < 0.3 @pytest.mark.parametrize( "opcode", @@ -2294,6 +2298,7 @@ def test_duplicate_large_integer_substr_tail(self, opcode, softfork_height): ConditionOpcode.ASSERT_SECONDS_RELATIVE, ], ) + @pytest.mark.benchmark def test_duplicate_large_integer_negative(self, opcode, softfork_height): condition = SINGLE_ARG_INT_COND.format(opcode=opcode.value[0], num=280000, val=100, filler="0xff") start_time = time() @@ -2302,9 +2307,10 @@ def test_duplicate_large_integer_negative(self, opcode, softfork_height): assert npc_result.error is None assert len(npc_result.npc_list) == 1 assert npc_result.npc_list[0].conditions == [] - assert run_time < 2 print(f"run time:{run_time}") + assert run_time < 1 + @pytest.mark.benchmark def test_duplicate_reserve_fee(self, softfork_height): opcode = ConditionOpcode.RESERVE_FEE condition = SINGLE_ARG_INT_COND.format(opcode=opcode.value[0], num=280000, val=100, filler="0x00") @@ -2322,9 +2328,10 @@ def test_duplicate_reserve_fee(self, softfork_height): [ConditionWithArgs(opcode, [int_to_bytes(100 * 280000)])], ) ] - assert run_time < 2 print(f"run time:{run_time}") + assert run_time < 1 + @pytest.mark.benchmark def test_duplicate_reserve_fee_negative(self, softfork_height): opcode = ConditionOpcode.RESERVE_FEE condition = SINGLE_ARG_INT_COND.format(opcode=opcode.value[0], num=200000, val=100, filler="0xff") @@ -2335,12 +2342,13 @@ def test_duplicate_reserve_fee_negative(self, softfork_height): # amount assert npc_result.error == Err.RESERVE_FEE_CONDITION_FAILED.value assert len(npc_result.npc_list) == 0 - assert run_time < 1.5 print(f"run time:{run_time}") + assert run_time < 0.8 @pytest.mark.parametrize( "opcode", [ConditionOpcode.CREATE_COIN_ANNOUNCEMENT, ConditionOpcode.CREATE_PUZZLE_ANNOUNCEMENT] ) + @pytest.mark.benchmark def test_duplicate_coin_announces(self, opcode, softfork_height): condition = CREATE_ANNOUNCE_COND.format(opcode=opcode.value[0], num=5950000) start_time = time() @@ -2351,9 +2359,10 @@ def test_duplicate_coin_announces(self, opcode, softfork_height): # coin announcements are not propagated to python, but validated in rust assert len(npc_result.npc_list[0].conditions) == 0 # TODO: optimize clvm to make this run in < 1 second - assert run_time < 21 print(f"run time:{run_time}") + assert run_time < 7 + @pytest.mark.benchmark def test_create_coin_duplicates(self, softfork_height): # CREATE_COIN # this program will emit 6000 identical CREATE_COIN conditions. However, @@ -2365,9 +2374,10 @@ def test_create_coin_duplicates(self, softfork_height): run_time = time() - start_time assert npc_result.error == Err.DUPLICATE_OUTPUT.value assert len(npc_result.npc_list) == 0 - assert run_time < 2 print(f"run time:{run_time}") + assert run_time < 0.8 + @pytest.mark.benchmark def test_many_create_coin(self, softfork_height): # CREATE_COIN # this program will emit many CREATE_COIN conditions, all with different @@ -2382,8 +2392,8 @@ def test_many_create_coin(self, softfork_height): assert len(npc_result.npc_list[0].conditions) == 1 assert npc_result.npc_list[0].conditions[0][0] == ConditionOpcode.CREATE_COIN.value assert len(npc_result.npc_list[0].conditions[0][1]) == 6094 - assert run_time < 1 print(f"run time:{run_time}") + assert run_time < 0.2 @pytest.mark.asyncio async def test_invalid_coin_spend_coin(self, bt, two_nodes, wallet_a): diff --git a/tests/core/full_node/test_mempool_performance.py b/tests/core/full_node/test_mempool_performance.py index 408178463f40..6cc53b239210 100644 --- a/tests/core/full_node/test_mempool_performance.py +++ b/tests/core/full_node/test_mempool_performance.py @@ -44,6 +44,7 @@ async def wallet_nodes(bt): class TestMempoolPerformance: @pytest.mark.asyncio + @pytest.mark.benchmark async def test_mempool_update_performance(self, bt, wallet_nodes, default_400_blocks, self_hostname): blocks = default_400_blocks full_nodes, wallets = wallet_nodes @@ -78,7 +79,12 @@ async def test_mempool_update_performance(self, bt, wallet_nodes, default_400_bl blocks = bt.get_consecutive_blocks(3, blocks) await full_node_api_1.full_node.respond_block(full_node_protocol.RespondBlock(blocks[-3])) - for block in blocks[-2:]: + for idx, block in enumerate(blocks): start_t_2 = time.time() await full_node_api_1.full_node.respond_block(full_node_protocol.RespondBlock(block)) - assert time.time() - start_t_2 < 1 + end_t_2 = time.time() + duration = end_t_2 - start_t_2 + if idx >= len(blocks) - 3: + assert duration < 0.1 + else: + assert duration < 0.0002 diff --git a/tests/core/full_node/test_performance.py b/tests/core/full_node/test_performance.py index 0cd0331abecc..be8d1a9905f0 100644 --- a/tests/core/full_node/test_performance.py +++ b/tests/core/full_node/test_performance.py @@ -55,6 +55,7 @@ async def wallet_nodes(bt): class TestPerformance: @pytest.mark.asyncio + @pytest.mark.benchmark async def test_full_block_performance(self, bt, wallet_nodes, self_hostname): full_node_1, server_1, wallet_a, wallet_receiver = wallet_nodes blocks = await full_node_1.get_all_full_blocks() @@ -146,8 +147,10 @@ async def test_full_block_performance(self, bt, wallet_nodes, self_hostname): if req is None: break + end = time.time() log.warning(f"Num Tx: {num_tx}") - log.warning(f"Time for mempool: {time.time() - start}") + log.warning(f"Time for mempool: {end - start:f}") + assert end - start < 0.001 pr.create_stats() pr.dump_stats("./mempool-benchmark.pstats") @@ -188,8 +191,10 @@ async def test_full_block_performance(self, bt, wallet_nodes, self_hostname): start = time.time() res = await full_node_1.respond_unfinished_block(fnp.RespondUnfinishedBlock(unfinished), fake_peer) + end = time.time() log.warning(f"Res: {res}") - log.warning(f"Time for unfinished: {time.time() - start}") + log.warning(f"Time for unfinished: {end - start:f}") + assert end - start < 0.1 pr.create_stats() pr.dump_stats("./unfinished-benchmark.pstats") @@ -201,8 +206,10 @@ async def test_full_block_performance(self, bt, wallet_nodes, self_hostname): # No transactions generator, the full node already cached it from the unfinished block block_small = dataclasses.replace(block, transactions_generator=None) res = await full_node_1.full_node.respond_block(fnp.RespondBlock(block_small)) + end = time.time() log.warning(f"Res: {res}") - log.warning(f"Time for full block: {time.time() - start}") + log.warning(f"Time for full block: {end - start:f}") + assert end - start < 0.1 pr.create_stats() pr.dump_stats("./full-block-benchmark.pstats") diff --git a/tests/pytest.ini b/tests/pytest.ini index d0b86d3b8f2b..cbf633bd24b2 100644 --- a/tests/pytest.ini +++ b/tests/pytest.ini @@ -6,6 +6,7 @@ log_level = WARNING console_output_style = count log_format = %(asctime)s %(name)s: %(levelname)s %(message)s asyncio_mode = strict +markers=benchmark filterwarnings = error ignore:ssl_context is deprecated:DeprecationWarning diff --git a/tests/runner_templates/build-test-macos b/tests/runner_templates/build-test-macos index 4a6bbf00cc07..6f26b845d05d 100644 --- a/tests/runner_templates/build-test-macos +++ b/tests/runner_templates/build-test-macos @@ -77,7 +77,7 @@ INSTALL_TIMELORD - name: Test TEST_NAME code with pytest run: | . ./activate - ./venv/bin/py.test TEST_DIR -s -v --durations 0PYTEST_PARALLEL_ARGS + ./venv/bin/py.test TEST_DIR -s -v --durations 0PYTEST_PARALLEL_ARGS -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/tests/runner_templates/build-test-ubuntu b/tests/runner_templates/build-test-ubuntu index 94a75eaedcca..7f9d2c896a30 100644 --- a/tests/runner_templates/build-test-ubuntu +++ b/tests/runner_templates/build-test-ubuntu @@ -82,7 +82,7 @@ INSTALL_TIMELORD - name: Test TEST_NAME code with pytest run: | . ./activate - ./venv/bin/py.test TEST_DIR -s -v --durations 0PYTEST_PARALLEL_ARGS + ./venv/bin/py.test TEST_DIR -s -v --durations 0PYTEST_PARALLEL_ARGS -m "not benchmark" CHECK_RESOURCE_USAGE # From 37843ee0448f6d81397b1d9fdde8faee847a45aa Mon Sep 17 00:00:00 2001 From: Adam Kelly <338792+aqk@users.noreply.github.com> Date: Thu, 17 Mar 2022 08:39:30 -0700 Subject: [PATCH 212/378] Change name to order of returned values. Enforce mandatory naming and inclusion of start_services parameter (#10769) --- tests/core/ssl/test_ssl.py | 4 +-- tests/core/test_farmer_harvester_rpc.py | 30 +++++++++---------- .../farmer_harvester/test_farmer_harvester.py | 14 ++++----- tests/setup_nodes.py | 2 +- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/tests/core/ssl/test_ssl.py b/tests/core/ssl/test_ssl.py index 666a467e7e1a..246bce7b4885 100644 --- a/tests/core/ssl/test_ssl.py +++ b/tests/core/ssl/test_ssl.py @@ -13,7 +13,7 @@ from tests.block_tools import test_constants from chia.util.ints import uint16 from tests.setup_nodes import ( - setup_farmer_harvester, + setup_harvester_farmer, setup_introducer, setup_simulators_and_wallets, setup_timelord, @@ -53,7 +53,7 @@ async def establish_connection(server: ChiaServer, self_hostname: str, ssl_conte @pytest_asyncio.fixture(scope="function") async def harvester_farmer(bt): - async for _ in setup_farmer_harvester(bt, test_constants): + async for _ in setup_harvester_farmer(bt, test_constants, start_services=True): yield _ diff --git a/tests/core/test_farmer_harvester_rpc.py b/tests/core/test_farmer_harvester_rpc.py index d6eafb04c5a6..6287d3530b87 100644 --- a/tests/core/test_farmer_harvester_rpc.py +++ b/tests/core/test_farmer_harvester_rpc.py @@ -18,7 +18,7 @@ from chia.util.hash import std_hash from chia.util.ints import uint8, uint16, uint32, uint64 from chia.wallet.derive_keys import master_sk_to_wallet_sk -from tests.setup_nodes import setup_farmer_harvester, test_constants +from tests.setup_nodes import setup_harvester_farmer, test_constants from tests.time_out_assert import time_out_assert, time_out_assert_custom_interval from tests.util.rpc import validate_get_routes from tests.util.socket import find_available_listen_port @@ -27,14 +27,14 @@ @pytest_asyncio.fixture(scope="function") -async def simulation(bt): - async for _ in setup_farmer_harvester(bt, test_constants): +async def harvester_farmer_simulation(bt): + async for _ in setup_harvester_farmer(bt, test_constants, start_services=True): yield _ @pytest_asyncio.fixture(scope="function") -async def environment(bt, simulation, self_hostname): - harvester_service, farmer_service = simulation +async def harvester_farmer_environment(bt, harvester_farmer_simulation, self_hostname): + harvester_service, farmer_service = harvester_farmer_simulation def stop_node_cb(): pass @@ -89,7 +89,7 @@ async def have_connections(): @pytest.mark.asyncio -async def test_get_routes(environment): +async def test_get_routes(harvester_farmer_environment): ( farmer_service, farmer_rpc_api, @@ -97,13 +97,13 @@ async def test_get_routes(environment): harvester_service, harvester_rpc_api, harvester_rpc_client, - ) = environment + ) = harvester_farmer_environment await validate_get_routes(farmer_rpc_client, farmer_rpc_api) await validate_get_routes(harvester_rpc_client, harvester_rpc_api) @pytest.mark.asyncio -async def test_farmer_get_harvesters(environment): +async def test_farmer_get_harvesters(harvester_farmer_environment): ( farmer_service, farmer_rpc_api, @@ -111,7 +111,7 @@ async def test_farmer_get_harvesters(environment): harvester_service, harvester_rpc_api, harvester_rpc_client, - ) = environment + ) = harvester_farmer_environment farmer_api = farmer_service._api harvester = harvester_service._node @@ -144,7 +144,7 @@ async def test_get_harvesters(): @pytest.mark.asyncio -async def test_farmer_signage_point_endpoints(environment): +async def test_farmer_signage_point_endpoints(harvester_farmer_environment): ( farmer_service, farmer_rpc_api, @@ -152,7 +152,7 @@ async def test_farmer_signage_point_endpoints(environment): harvester_service, harvester_rpc_api, harvester_rpc_client, - ) = environment + ) = harvester_farmer_environment farmer_api = farmer_service._api assert (await farmer_rpc_client.get_signage_point(std_hash(b"2"))) is None @@ -171,7 +171,7 @@ async def have_signage_points(): @pytest.mark.asyncio -async def test_farmer_reward_target_endpoints(bt, environment): +async def test_farmer_reward_target_endpoints(bt, harvester_farmer_environment): ( farmer_service, farmer_rpc_api, @@ -179,7 +179,7 @@ async def test_farmer_reward_target_endpoints(bt, environment): harvester_service, harvester_rpc_api, harvester_rpc_client, - ) = environment + ) = harvester_farmer_environment farmer_api = farmer_service._api targets_1 = await farmer_rpc_client.get_reward_targets(False) @@ -220,7 +220,7 @@ async def test_farmer_reward_target_endpoints(bt, environment): @pytest.mark.asyncio -async def test_farmer_get_pool_state(environment, self_hostname): +async def test_farmer_get_pool_state(harvester_farmer_environment, self_hostname): ( farmer_service, farmer_rpc_api, @@ -228,7 +228,7 @@ async def test_farmer_get_pool_state(environment, self_hostname): harvester_service, harvester_rpc_api, harvester_rpc_client, - ) = environment + ) = harvester_farmer_environment farmer_api = farmer_service._api assert len((await farmer_rpc_client.get_pool_state())["pool_state"]) == 0 diff --git a/tests/farmer_harvester/test_farmer_harvester.py b/tests/farmer_harvester/test_farmer_harvester.py index 698d128d2799..ca494cf54ae4 100644 --- a/tests/farmer_harvester/test_farmer_harvester.py +++ b/tests/farmer_harvester/test_farmer_harvester.py @@ -5,7 +5,7 @@ from chia.farmer.farmer import Farmer from chia.util.keychain import generate_mnemonic -from tests.setup_nodes import setup_farmer_harvester, test_constants +from tests.setup_nodes import setup_harvester_farmer, test_constants from tests.time_out_assert import time_out_assert @@ -14,14 +14,14 @@ def farmer_is_started(farmer): @pytest_asyncio.fixture(scope="function") -async def environment(bt): - async for _ in setup_farmer_harvester(bt, test_constants, False): +async def harvester_farmer_environment_no_start(bt): + async for _ in setup_harvester_farmer(bt, test_constants, start_services=False): yield _ @pytest.mark.asyncio -async def test_start_with_empty_keychain(environment, bt): - _, farmer_service = environment +async def test_start_with_empty_keychain(harvester_farmer_environment_no_start, bt): + _, farmer_service = harvester_farmer_environment_no_start farmer: Farmer = farmer_service._node # First remove all keys from the keychain bt.local_keychain.delete_all_keys() @@ -41,8 +41,8 @@ async def test_start_with_empty_keychain(environment, bt): @pytest.mark.asyncio -async def test_harvester_handshake(environment, bt): - harvester_service, farmer_service = environment +async def test_harvester_handshake(harvester_farmer_environment_no_start, bt): + harvester_service, farmer_service = harvester_farmer_environment_no_start harvester = harvester_service._node farmer = farmer_service._node diff --git a/tests/setup_nodes.py b/tests/setup_nodes.py index e1de55f010d3..b4122d55d0b3 100644 --- a/tests/setup_nodes.py +++ b/tests/setup_nodes.py @@ -287,7 +287,7 @@ async def setup_simulators_and_wallets( await _teardown_nodes(node_iters) -async def setup_farmer_harvester(bt: BlockTools, consensus_constants: ConsensusConstants, start_services: bool = True): +async def setup_harvester_farmer(bt: BlockTools, consensus_constants: ConsensusConstants, *, start_services: bool): farmer_port = find_available_listen_port("farmer") farmer_rpc_port = find_available_listen_port("farmer rpc") harvester_port = find_available_listen_port("harvester") From d127898ce86dd3cd98c84f99ec0722b5c91ed16f Mon Sep 17 00:00:00 2001 From: dustinface <35775977+xdustinface@users.noreply.github.com> Date: Thu, 17 Mar 2022 16:40:54 +0100 Subject: [PATCH 213/378] cmds: Implement `chia rpc` command (#10763) * cmds: Implement `chia rpc` command * Enable "timelord" client + some refactoring to enable "crawler" client --- chia/cmds/chia.py | 2 + chia/cmds/rpc.py | 140 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 chia/cmds/rpc.py diff --git a/chia/cmds/chia.py b/chia/cmds/chia.py index 9825964f649f..1a02749ef848 100644 --- a/chia/cmds/chia.py +++ b/chia/cmds/chia.py @@ -9,6 +9,7 @@ from chia.cmds.netspace import netspace_cmd from chia.cmds.passphrase import passphrase_cmd from chia.cmds.plots import plots_cmd +from chia.cmds.rpc import rpc_cmd from chia.cmds.show import show_cmd from chia.cmds.start import start_cmd from chia.cmds.stop import stop_cmd @@ -131,6 +132,7 @@ def run_daemon_cmd(ctx: click.Context, wait_for_unlock: bool) -> None: cli.add_command(plotnft_cmd) cli.add_command(configure_cmd) cli.add_command(init_cmd) +cli.add_command(rpc_cmd) cli.add_command(show_cmd) cli.add_command(start_cmd) cli.add_command(stop_cmd) diff --git a/chia/cmds/rpc.py b/chia/cmds/rpc.py new file mode 100644 index 000000000000..ddb43f47c6ef --- /dev/null +++ b/chia/cmds/rpc.py @@ -0,0 +1,140 @@ +import asyncio +import json +import sys +from typing import Any, Dict, List, Optional, TextIO + +import click +from aiohttp import ClientResponseError + +from chia.util.config import load_config +from chia.util.default_root import DEFAULT_ROOT_PATH +from chia.util.ints import uint16 + +services: List[str] = ["crawler", "farmer", "full_node", "harvester", "timelord", "wallet"] + + +async def call_endpoint(service: str, endpoint: str, request: Dict[str, Any], config: Dict[str, Any]) -> Dict[str, Any]: + from chia.rpc.rpc_client import RpcClient + + port: uint16 + if service == "crawler": + # crawler config is inside the seeder config + port = uint16(config["seeder"][service]["rpc_port"]) + else: + port = uint16(config[service]["rpc_port"]) + + try: + client = await RpcClient.create(config["self_hostname"], port, DEFAULT_ROOT_PATH, config) + except Exception as e: + raise Exception(f"Failed to create RPC client {service}: {e}") + result: Dict[str, Any] + try: + result = await client.fetch(endpoint, request) + except ClientResponseError as e: + if e.code == 404: + raise Exception(f"Invalid endpoint for {service}: {endpoint}") + raise e + except Exception as e: + raise Exception(f"Request failed: {e}") + finally: + client.close() + await client.await_closed() + return result + + +def print_result(json_dict: Dict[str, Any]) -> None: + print(json.dumps(json_dict, indent=4, sort_keys=True)) + + +def get_routes(service: str, config: Dict[str, Any]) -> Dict[str, Any]: + return asyncio.run(call_endpoint(service, "get_routes", {}, config)) + + +@click.group("rpc", short_help="RPC Client") +def rpc_cmd() -> None: + pass + + +@rpc_cmd.command("endpoints", help="Print all endpoints of a service") +@click.argument("service", type=click.Choice(services)) +def endpoints_cmd(service: str) -> None: + config = load_config(DEFAULT_ROOT_PATH, "config.yaml") + try: + routes = get_routes(service, config) + for route in routes["routes"]: + print(route[1:]) + except Exception as e: + print(e) + + +@rpc_cmd.command("status", help="Print the status of all available RPC services") +def status_cmd() -> None: + config = load_config(DEFAULT_ROOT_PATH, "config.yaml") + + def print_row(c0: str, c1: str) -> None: + c0 = "{0:<12}".format(f"{c0}") + c1 = "{0:<9}".format(f"{c1}") + print(f"{c0} | {c1}") + + print_row("SERVICE", "STATUS") + print_row("------------", "---------") + for service in services: + status = "ACTIVE" + try: + if not get_routes(service, config)["success"]: + raise Exception() + except Exception: + status = "INACTIVE" + print_row(service, status) + + +def create_commands() -> None: + for service in services: + + @rpc_cmd.command( + service, + short_help=f"RPC client for the {service} RPC API", + help=( + f"Call ENDPOINT (RPC endpoint as as string) of the {service} " + "RPC API with REQUEST (must be a JSON string) as request data." + ), + ) + @click.argument("endpoint", type=str) + @click.argument("request", type=str, required=False) + @click.option( + "-j", + "--json-file", + help="Optionally instead of REQUEST you can provide a json file containing the request data", + type=click.File("r"), + default=None, + ) + def rpc_client_cmd( + endpoint: str, request: Optional[str], json_file: Optional[TextIO], service: str = service + ) -> None: + config = load_config(DEFAULT_ROOT_PATH, "config.yaml") + if request is not None and json_file is not None: + sys.exit( + "Can only use one request source: REQUEST argument OR -j/--json-file option. See the help with -h" + ) + + request_json: Dict[str, Any] = {} + if json_file is not None: + try: + request_json = json.load(json_file) + except Exception as e: + sys.exit(f"Invalid JSON file: {e}") + if request is not None: + try: + request_json = json.loads(request) + except Exception as e: + sys.exit(f"Invalid REQUEST JSON: {e}") + + try: + if endpoint[0] == "/": + endpoint = endpoint[1:] + print_result(asyncio.run(call_endpoint(service, endpoint, request_json, config))) + except Exception as e: + sys.exit(e) + + +create_commands() From 25630d3e7f4b48f57d7c294d362b0fd0ce9967c7 Mon Sep 17 00:00:00 2001 From: dustinface <35775977+xdustinface@users.noreply.github.com> Date: Thu, 17 Mar 2022 16:41:54 +0100 Subject: [PATCH 214/378] wallet: Fix `STANDARD_WALLET` creation for `wallet_info.id != 1` (#10757) --- chia/wallet/wallet_state_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 1b091e1408b4..ced4b35d65f0 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -182,7 +182,7 @@ async def create( if wallet_info.type == WalletType.STANDARD_WALLET: if wallet_info.id == 1: continue - wallet = await Wallet.create(config, wallet_info) + wallet = await Wallet.create(self, wallet_info) elif wallet_info.type == WalletType.CAT: wallet = await CATWallet.create( self, From 66c7427622399dc26dc332a6def38873b66cc996 Mon Sep 17 00:00:00 2001 From: dustinface <35775977+xdustinface@users.noreply.github.com> Date: Thu, 17 Mar 2022 16:42:48 +0100 Subject: [PATCH 215/378] wallet: Optional wallet type parameter for `get_wallets` and `wallet show` (#10739) * wallet: Add optional `type` parameter to `get_wallets` and `wallet show` * tests: Use the `type` parameter for `get_wallets` in pool rpc tests * cmds: Ask for the name of the wallet type in CLI --- chia/cmds/wallet.py | 18 ++++- chia/cmds/wallet_funcs.py | 11 ++- chia/rpc/wallet_rpc_api.py | 6 +- chia/rpc/wallet_rpc_client.py | 8 ++- chia/wallet/wallet_state_manager.py | 4 +- chia/wallet/wallet_user_store.py | 11 ++- tests/pools/test_pool_rpc.py | 105 ++++++++-------------------- tests/wallet/rpc/test_wallet_rpc.py | 25 ++++++- 8 files changed, 96 insertions(+), 92 deletions(-) diff --git a/chia/cmds/wallet.py b/chia/cmds/wallet.py index de24d8ba1527..2d0f85b7ef88 100644 --- a/chia/cmds/wallet.py +++ b/chia/cmds/wallet.py @@ -1,8 +1,10 @@ import sys -from typing import Optional, Tuple +from typing import Any, Dict, Optional, Tuple import click +from chia.wallet.util.wallet_types import WalletType + @click.group("wallet", short_help="Manage your wallet") def wallet_cmd() -> None: @@ -138,11 +140,21 @@ def send_cmd( default=None, ) @click.option("-f", "--fingerprint", help="Set the fingerprint to specify which wallet to use", type=int) -def show_cmd(wallet_rpc_port: Optional[int], fingerprint: int) -> None: +@click.option( + "-w", + "--wallet_type", + help="Choose a specific wallet type to return", + type=click.Choice([x.name.lower() for x in WalletType]), + default=None, +) +def show_cmd(wallet_rpc_port: Optional[int], fingerprint: int, wallet_type: Optional[str]) -> None: import asyncio from .wallet_funcs import execute_with_wallet, print_balances - asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, {}, print_balances)) + args: Dict[str, Any] = {} + if wallet_type is not None: + args["type"] = WalletType[wallet_type.upper()] + asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, args, print_balances)) @wallet_cmd.command("get_address", short_help="Get a wallet receive address") diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index 25877fffda92..e6105ed14fb5 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -457,7 +457,10 @@ def print_balance(amount: int, scale: int, address_prefix: str) -> str: async def print_balances(args: dict, wallet_client: WalletRpcClient, fingerprint: int) -> None: - summaries_response = await wallet_client.get_wallets() + wallet_type: Optional[WalletType] = None + if "type" in args: + wallet_type = WalletType(args["type"]) + summaries_response = await wallet_client.get_wallets(wallet_type) config = load_config(DEFAULT_ROOT_PATH, "config.yaml") address_prefix = config["network_overrides"]["config"][config["selected_network"]]["address_prefix"] @@ -473,7 +476,11 @@ async def print_balances(args: dict, wallet_client: WalletRpcClient, fingerprint print("Sync status: Not synced") if not is_syncing and is_synced: - print(f"Balances, fingerprint: {fingerprint}") + if len(summaries_response) == 0: + type_hint = " " if wallet_type is None else f" from type {wallet_type.name} " + print(f"\nNo wallets{type_hint}available for fingerprint: {fingerprint}") + else: + print(f"Balances, fingerprint: {fingerprint}") for summary in summaries_response: wallet_id = summary["id"] balances = await wallet_client.get_wallet_balance(wallet_id) diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 79357f56a554..277acc7f436c 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -415,7 +415,11 @@ async def farm_block(self, request): async def get_wallets(self, request: Dict): assert self.service.wallet_state_manager is not None - wallets: List[WalletInfo] = await self.service.wallet_state_manager.get_all_wallet_info_entries() + wallet_type: Optional[WalletType] = None + if "type" in request: + wallet_type = WalletType(request["type"]) + + wallets: List[WalletInfo] = await self.service.wallet_state_manager.get_all_wallet_info_entries(wallet_type) return {"wallets": wallets} diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index 62729fe17690..8e03bbd2daf5 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -10,6 +10,7 @@ from chia.wallet.trading.offer import Offer from chia.wallet.transaction_record import TransactionRecord from chia.wallet.transaction_sorting import SortKey +from chia.wallet.util.wallet_types import WalletType def parse_result_transactions(result: Dict[str, Any]) -> Dict[str, Any]: @@ -80,8 +81,11 @@ async def farm_block(self, address: str) -> None: return await self.fetch("farm_block", {"address": address}) # Wallet Management APIs - async def get_wallets(self) -> Dict: - return (await self.fetch("get_wallets", {}))["wallets"] + async def get_wallets(self, wallet_type: Optional[WalletType] = None) -> Dict: + request: Dict[str, Any] = {} + if wallet_type is not None: + request["type"] = wallet_type + return (await self.fetch("get_wallets", request))["wallets"] # Wallet APIs async def get_wallet_balance(self, wallet_id: str) -> Dict: diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index ced4b35d65f0..96874ac0fe25 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -1147,8 +1147,8 @@ async def _await_closed(self) -> None: def unlink_db(self): Path(self.db_path).unlink() - async def get_all_wallet_info_entries(self) -> List[WalletInfo]: - return await self.user_store.get_all_wallet_info_entries() + async def get_all_wallet_info_entries(self, wallet_type: Optional[WalletType] = None) -> List[WalletInfo]: + return await self.user_store.get_all_wallet_info_entries(wallet_type) async def get_start_height(self): """ diff --git a/chia/wallet/wallet_user_store.py b/chia/wallet/wallet_user_store.py index dd541fa95c38..7f1eb1455526 100644 --- a/chia/wallet/wallet_user_store.py +++ b/chia/wallet/wallet_user_store.py @@ -112,12 +112,17 @@ async def get_last_wallet(self) -> Optional[WalletInfo]: return await self.get_wallet_by_id(row[0]) - async def get_all_wallet_info_entries(self) -> List[WalletInfo]: + async def get_all_wallet_info_entries(self, wallet_type: Optional[WalletType] = None) -> List[WalletInfo]: """ - Return a set containing all wallets + Return a set containing all wallets, optionally with a specific WalletType """ + if wallet_type is None: + cursor = await self.db_connection.execute("SELECT * from users_wallets") + else: + cursor = await self.db_connection.execute( + "SELECT * from users_wallets WHERE wallet_type=?", (wallet_type.value,) + ) - cursor = await self.db_connection.execute("SELECT * from users_wallets") rows = await cursor.fetchall() await cursor.close() result = [] diff --git a/tests/pools/test_pool_rpc.py b/tests/pools/test_pool_rpc.py index 34c3b44e790f..20f627bc3124 100644 --- a/tests/pools/test_pool_rpc.py +++ b/tests/pools/test_pool_rpc.py @@ -191,10 +191,7 @@ async def test_create_new_pool_wallet_self_farm(self, one_wallet_node_and_rpc, f await time_out_assert(10, wallet_node_0.wallet_state_manager.blockchain.get_peak_height, PREFARMED_BLOCKS) our_ph = await wallet_0.get_new_puzzlehash() - summaries_response = await client.get_wallets() - for summary in summaries_response: - if WalletType(int(summary["type"])) == WalletType.POOLING_WALLET: - assert False + assert len(await client.get_wallets(WalletType.POOLING_WALLET)) == 0 await time_out_assert(10, wallet_is_synced, True, wallet_node_0, full_node_api) creation_tx: TransactionRecord = await client.create_new_pool_wallet( our_ph, "", 0, f"{self_hostname}:5000", "new", "SELF_POOLING", fee @@ -210,12 +207,9 @@ async def test_create_new_pool_wallet_self_farm(self, one_wallet_node_and_rpc, f assert full_node_api.full_node.mempool_manager.get_spendbundle(creation_tx.name) is None await time_out_assert(10, wallet_is_synced, True, wallet_node_0, full_node_api) - summaries_response = await client.get_wallets() - wallet_id: Optional[int] = None - for summary in summaries_response: - if WalletType(int(summary["type"])) == WalletType.POOLING_WALLET: - wallet_id = summary["id"] - assert wallet_id is not None + summaries_response = await client.get_wallets(WalletType.POOLING_WALLET) + assert len(summaries_response) == 1 + wallet_id: int = summaries_response[0]["id"] status: PoolWalletInfo = (await client.pw_status(wallet_id))[0] assert status.current.state == PoolSingletonState.SELF_POOLING.value @@ -270,10 +264,7 @@ async def test_create_new_pool_wallet_farm_to_pool(self, one_wallet_node_and_rpc await time_out_assert(10, wallet_is_synced, True, wallet_node_0, full_node_api) our_ph = await wallet_0.get_new_puzzlehash() - summaries_response = await client.get_wallets() - for summary in summaries_response: - if WalletType(int(summary["type"])) == WalletType.POOLING_WALLET: - assert False + assert len(await client.get_wallets(WalletType.POOLING_WALLET)) == 0 creation_tx: TransactionRecord = await client.create_new_pool_wallet( our_ph, "http://pool.example.com", 10, f"{self_hostname}:5000", "new", "FARMING_TO_POOL", fee @@ -289,12 +280,9 @@ async def test_create_new_pool_wallet_farm_to_pool(self, one_wallet_node_and_rpc assert full_node_api.full_node.mempool_manager.get_spendbundle(creation_tx.name) is None await time_out_assert(5, wallet_is_synced, True, wallet_node_0, full_node_api) - summaries_response = await client.get_wallets() - wallet_id: Optional[int] = None - for summary in summaries_response: - if WalletType(int(summary["type"])) == WalletType.POOLING_WALLET: - wallet_id = summary["id"] - assert wallet_id is not None + summaries_response = await client.get_wallets(WalletType.POOLING_WALLET) + assert len(summaries_response) == 1 + wallet_id: int = summaries_response[0]["id"] status: PoolWalletInfo = (await client.pw_status(wallet_id))[0] assert status.current.state == PoolSingletonState.FARMING_TO_POOL.value @@ -348,10 +336,7 @@ async def test_create_multiple_pool_wallets(self, one_wallet_node_and_rpc, fee, our_ph_1 = await wallet_0.get_new_puzzlehash() our_ph_2 = await wallet_0.get_new_puzzlehash() - summaries_response = await client.get_wallets() - for summary in summaries_response: - if WalletType(int(summary["type"])) == WalletType.POOLING_WALLET: - assert False + assert len(await client.get_wallets(WalletType.POOLING_WALLET)) == 0 creation_tx: TransactionRecord = await client.create_new_pool_wallet( our_ph_1, "", 0, f"{self_hostname}:5000", "new", "SELF_POOLING", fee @@ -476,10 +461,7 @@ async def test_absorb_self(self, one_wallet_node_and_rpc, fee, trusted, bt, self await time_out_assert(10, wallet_node_0.wallet_state_manager.blockchain.get_peak_height, PREFARMED_BLOCKS) our_ph = await wallet_0.get_new_puzzlehash() - summaries_response = await client.get_wallets() - for summary in summaries_response: - if WalletType(int(summary["type"])) == WalletType.POOLING_WALLET: - assert False + assert len(await client.get_wallets(WalletType.POOLING_WALLET)) == 0 await time_out_assert(10, wallet_is_synced, True, wallet_node_0, full_node_api) creation_tx: TransactionRecord = await client.create_new_pool_wallet( @@ -593,10 +575,7 @@ async def test_absorb_pooling(self, one_wallet_node_and_rpc, fee, trusted, bt, s await time_out_assert(10, wallet_is_synced, True, wallet_node_0, full_node_api) our_ph = await wallet_0.get_new_puzzlehash() - summaries_response = await client.get_wallets() - for summary in summaries_response: - if WalletType(int(summary["type"])) == WalletType.POOLING_WALLET: - assert False + assert len(await client.get_wallets(WalletType.POOLING_WALLET)) == 0 # Balance stars at 6 XCH assert (await wallet_0.get_confirmed_balance()) == 6000000000000 creation_tx: TransactionRecord = await client.create_new_pool_wallet( @@ -753,10 +732,7 @@ async def test_self_pooling_to_pooling(self, setup, fee, trusted, self_hostname) await time_out_assert(10, wallet_is_synced, True, wallet_node_0, full_node_api) assert total_block_rewards > 0 - summaries_response = await client.get_wallets() - for summary in summaries_response: - if WalletType(int(summary["type"])) == WalletType.POOLING_WALLET: - assert False + assert len(await client.get_wallets(WalletType.POOLING_WALLET)) == 0 creation_tx: TransactionRecord = await client.create_new_pool_wallet( our_ph, "", 0, f"{self_hostname}:5000", "new", "SELF_POOLING", fee @@ -782,18 +758,11 @@ async def test_self_pooling_to_pooling(self, setup, fee, trusted, self_hostname) assert full_node_api.full_node.mempool_manager.get_spendbundle(creation_tx.name) is None await time_out_assert(10, wallet_is_synced, True, wallet_node_0, full_node_api) - summaries_response = await client.get_wallets() - wallet_id: Optional[int] = None - wallet_id_2: Optional[int] = None - for summary in summaries_response: - if WalletType(int(summary["type"])) == WalletType.POOLING_WALLET: - if wallet_id is not None: - wallet_id_2 = summary["id"] - else: - wallet_id = summary["id"] + summaries_response = await client.get_wallets(WalletType.POOLING_WALLET) + assert len(summaries_response) == 2 + wallet_id: int = summaries_response[0]["id"] + wallet_id_2: int = summaries_response[1]["id"] await asyncio.sleep(1) - assert wallet_id is not None - assert wallet_id_2 is not None status: PoolWalletInfo = (await client.pw_status(wallet_id))[0] status_2: PoolWalletInfo = (await client.pw_status(wallet_id_2))[0] @@ -884,10 +853,7 @@ async def test_leave_pool(self, setup, fee, trusted, self_hostname): WAIT_SECS = 200 try: - summaries_response = await client.get_wallets() - for summary in summaries_response: - if WalletType(int(summary["type"])) == WalletType.POOLING_WALLET: - assert False + assert len(await client.get_wallets(WalletType.POOLING_WALLET)) == 0 async def have_chia(): await farm_blocks(full_node_api, our_ph, 1) @@ -912,12 +878,9 @@ async def have_chia(): await time_out_assert(10, wallet_is_synced, True, wallet_nodes[0], full_node_api) - summaries_response = await client.get_wallets() - wallet_id: Optional[int] = None - for summary in summaries_response: - if WalletType(int(summary["type"])) == WalletType.POOLING_WALLET: - wallet_id = summary["id"] - assert wallet_id is not None + summaries_response = await client.get_wallets(WalletType.POOLING_WALLET) + assert len(summaries_response) == 1 + wallet_id: int = summaries_response[0]["id"] status: PoolWalletInfo = (await client.pw_status(wallet_id))[0] assert status.current.state == PoolSingletonState.SELF_POOLING.value @@ -1009,10 +972,7 @@ async def test_change_pools(self, setup, fee, trusted, self_hostname): WAIT_SECS = 200 try: - summaries_response = await client.get_wallets() - for summary in summaries_response: - if WalletType(int(summary["type"])) == WalletType.POOLING_WALLET: - assert False + assert len(await client.get_wallets(WalletType.POOLING_WALLET)) == 0 async def have_chia(): await farm_blocks(full_node_api, our_ph, 1) @@ -1037,12 +997,9 @@ async def have_chia(): await time_out_assert(10, wallet_is_synced, True, wallet_nodes[0], full_node_api) - summaries_response = await client.get_wallets() - wallet_id: Optional[int] = None - for summary in summaries_response: - if WalletType(int(summary["type"])) == WalletType.POOLING_WALLET: - wallet_id = summary["id"] - assert wallet_id is not None + summaries_response = await client.get_wallets(WalletType.POOLING_WALLET) + assert len(summaries_response) == 1 + wallet_id: int = summaries_response[0]["id"] status: PoolWalletInfo = (await client.pw_status(wallet_id))[0] assert status.current.state == PoolSingletonState.FARMING_TO_POOL.value @@ -1114,10 +1071,7 @@ async def test_change_pools_reorg(self, setup, fee, trusted, bt, self_hostname): ) try: - summaries_response = await client.get_wallets() - for summary in summaries_response: - if WalletType(int(summary["type"])) == WalletType.POOLING_WALLET: - assert False + assert len(await client.get_wallets(WalletType.POOLING_WALLET)) == 0 async def have_chia(): await farm_blocks(full_node_api, our_ph, 1) @@ -1142,12 +1096,9 @@ async def have_chia(): await time_out_assert(5, wallet_is_synced, True, wallet_nodes[0], full_node_api) - summaries_response = await client.get_wallets() - wallet_id: Optional[int] = None - for summary in summaries_response: - if WalletType(int(summary["type"])) == WalletType.POOLING_WALLET: - wallet_id = summary["id"] - assert wallet_id is not None + summaries_response = await client.get_wallets(WalletType.POOLING_WALLET) + assert len(summaries_response) == 1 + wallet_id: int = summaries_response[0]["id"] status: PoolWalletInfo = (await client.pw_status(wallet_id))[0] assert status.current.state == PoolSingletonState.FARMING_TO_POOL.value diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index 8aa7ea32b383..962c8d5963cf 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -1,5 +1,5 @@ import asyncio -from typing import Optional +from typing import Dict, Optional from blspy import G2Element @@ -34,6 +34,7 @@ from chia.wallet.transaction_record import TransactionRecord from chia.wallet.transaction_sorting import SortKey from chia.wallet.util.compute_memos import compute_memos +from chia.wallet.util.wallet_types import WalletType from tests.pools.test_pool_rpc import wallet_is_synced from tests.setup_nodes import setup_simulators_and_wallets from tests.time_out_assert import time_out_assert @@ -48,6 +49,16 @@ async def two_wallet_nodes(): yield _ +async def assert_wallet_types(client: WalletRpcClient, expected: Dict[WalletType, int]) -> None: + for wallet_type in WalletType: + wallets = await client.get_wallets(wallet_type) + wallet_count = len(wallets) + if wallet_type in expected: + assert wallet_count == expected.get(wallet_type, 0) + for wallet in wallets: + assert wallet["type"] == wallet_type.value + + class TestWalletRpc: @pytest.mark.parametrize( "trusted", @@ -142,6 +153,9 @@ def stop_node_cb(): client_2 = await WalletRpcClient.create(hostname, test_rpc_port_2, bt.root_path, config) client_node = await FullNodeRpcClient.create(hostname, test_rpc_port_node, bt.root_path, config) try: + await assert_wallet_types(client, {WalletType.STANDARD_WALLET: 1}) + await assert_wallet_types(client_2, {WalletType.STANDARD_WALLET: 1}) + await time_out_assert(5, client.get_synced) addr = encode_puzzle_hash(await wallet_node_2.wallet_state_manager.main_wallet.get_new_puzzlehash(), "txch") tx_amount = 15600000 @@ -400,13 +414,17 @@ async def eventual_balance_det(c, wallet_id: str): # CATS # ############## - # Creates a wallet and a CAT with 20 mojos + # Creates a CAT wallet with 100 mojos and a CAT with 20 mojos + await client.create_new_cat_and_wallet(100) res = await client.create_new_cat_and_wallet(20) assert res["success"] cat_0_id = res["wallet_id"] asset_id = bytes.fromhex(res["asset_id"]) assert len(asset_id) > 0 + await assert_wallet_types(client, {WalletType.STANDARD_WALLET: 1, WalletType.CAT: 2}) + await assert_wallet_types(client_2, {WalletType.STANDARD_WALLET: 1}) + bal_0 = await client.get_wallet_balance(cat_0_id) assert bal_0["confirmed_wallet_balance"] == 0 assert bal_0["pending_coin_removal_count"] == 1 @@ -444,6 +462,9 @@ async def eventual_balance_det(c, wallet_id: str): cat_1_asset_id = bytes.fromhex(res["asset_id"]) assert cat_1_asset_id == asset_id + await assert_wallet_types(client, {WalletType.STANDARD_WALLET: 1, WalletType.CAT: 2}) + await assert_wallet_types(client_2, {WalletType.STANDARD_WALLET: 1, WalletType.CAT: 1}) + await asyncio.sleep(1) for i in range(0, 5): await client.farm_block(encode_puzzle_hash(ph_2, "txch")) From e0c53864f5ea76012743a1c91fc5df779fe3bd42 Mon Sep 17 00:00:00 2001 From: dustinface <35775977+xdustinface@users.noreply.github.com> Date: Thu, 17 Mar 2022 16:43:53 +0100 Subject: [PATCH 216/378] harvester: Reuse legacy refresh interval if new params aren't available (#10729) --- chia/harvester/harvester.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/chia/harvester/harvester.py b/chia/harvester/harvester.py index 385d15fa6e80..d2dcd1eed5ca 100644 --- a/chia/harvester/harvester.py +++ b/chia/harvester/harvester.py @@ -1,5 +1,6 @@ import asyncio import concurrent +import dataclasses import logging from concurrent.futures.thread import ThreadPoolExecutor from pathlib import Path @@ -43,6 +44,9 @@ def __init__(self, root_path: Path, config: Dict, constants: ConsensusConstants) "`harvester.plot_loading_frequency_seconds` is deprecated. Consider replacing it with the new section " "`harvester.plots_refresh_parameter`. See `initial-config.yaml`." ) + refresh_parameter = dataclasses.replace( + refresh_parameter, interval_seconds=config["plot_loading_frequency_seconds"] + ) if "plots_refresh_parameter" in config: refresh_parameter = dataclass_from_dict(PlotsRefreshParameter, config["plots_refresh_parameter"]) From 7850f7355a8843640b3ea4ba58eb9749617e6c83 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 17 Mar 2022 11:44:28 -0400 Subject: [PATCH 217/378] mypy 0.941 for pre-commit (#10728) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1492ce452cc0..86e5236837ba 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -54,7 +54,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.940 + rev: v0.941 hooks: - id: mypy additional_dependencies: [filelock, pytest, pytest-asyncio, types-aiofiles, types-click, types-setuptools, types-PyYAML] From 82e8b7dea5365a529e826b4cf34018b1cb655e0a Mon Sep 17 00:00:00 2001 From: Jeff Date: Thu, 17 Mar 2022 08:45:06 -0700 Subject: [PATCH 218/378] Add maker fee to remaining offer RPCs (#10726) --- chia/wallet/trade_record.py | 1 + 1 file changed, 1 insertion(+) diff --git a/chia/wallet/trade_record.py b/chia/wallet/trade_record.py index 5e0dd9656528..278181b81d85 100644 --- a/chia/wallet/trade_record.py +++ b/chia/wallet/trade_record.py @@ -37,6 +37,7 @@ def to_json_dict_convenience(self) -> Dict[str, Any]: formatted["summary"] = { "offered": offered, "requested": requested, + "fees": offer.bundle.fees(), } formatted["pending"] = offer.get_pending_amounts() del formatted["offer"] From 6c5d7e7e8926d010fdfea235daa9e0ed2376019f Mon Sep 17 00:00:00 2001 From: Brandon Butler Date: Thu, 17 Mar 2022 08:45:48 -0700 Subject: [PATCH 219/378] Add healthcheck endpoint to rpc services (#10718) * Add healthcheck endpoint to rpc services * Trailing whitespace ding --- chia/rpc/rpc_client.py | 3 +++ chia/rpc/rpc_server.py | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/chia/rpc/rpc_client.py b/chia/rpc/rpc_client.py index 733e3e748cf9..4e7b8f94def2 100644 --- a/chia/rpc/rpc_client.py +++ b/chia/rpc/rpc_client.py @@ -67,6 +67,9 @@ async def close_connection(self, node_id: bytes32) -> Dict: async def stop_node(self) -> Dict: return await self.fetch("stop_node", {}) + async def healthz(self) -> Dict: + return await self.fetch("healthz", {}) + def close(self): self.closing_task = asyncio.create_task(self.session.close()) diff --git a/chia/rpc/rpc_server.py b/chia/rpc/rpc_server.py index 0995240d3df5..dbabcea5fa38 100644 --- a/chia/rpc/rpc_server.py +++ b/chia/rpc/rpc_server.py @@ -87,6 +87,7 @@ def get_routes(self) -> Dict[str, Callable]: "/close_connection": self.close_connection, "/stop_node": self.stop_node, "/get_routes": self._get_routes, + "/healthz": self.healthz, } async def _get_routes(self, request: Dict) -> Dict: @@ -183,6 +184,11 @@ async def stop_node(self, request): self.stop_cb() return {} + async def healthz(self, request: Dict) -> Dict: + return { + "success": "true", + } + async def ws_api(self, message): """ This function gets called when new message is received via websocket. From 073dc941f09a62b18a241beca9fd4a5415c20fd4 Mon Sep 17 00:00:00 2001 From: Dave <72020697+daverof@users.noreply.github.com> Date: Thu, 17 Mar 2022 15:47:02 +0000 Subject: [PATCH 220/378] Fix typos lastest > latest (#10720) --- chia/consensus/blockchain.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/chia/consensus/blockchain.py b/chia/consensus/blockchain.py index 7af1cbb8ca34..06e440c921e0 100644 --- a/chia/consensus/blockchain.py +++ b/chia/consensus/blockchain.py @@ -330,7 +330,7 @@ async def _reconsider_peak( None if there was no update to the heaviest chain. """ peak = self.get_peak() - lastest_coin_state: Dict[bytes32, CoinRecord] = {} + latest_coin_state: Dict[bytes32, CoinRecord] = {} hint_coin_state: Dict[bytes, Dict[bytes32, CoinRecord]] = {} if genesis: @@ -372,7 +372,7 @@ async def _reconsider_peak( if block_record.prev_hash != peak.header_hash: roll_changes: List[CoinRecord] = await self.coin_store.rollback_to_block(fork_height) for coin_record in roll_changes: - lastest_coin_state[coin_record.name] = coin_record + latest_coin_state[coin_record.name] = coin_record # Rollback sub_epoch_summaries self.__height_map.rollback(fork_height) @@ -423,10 +423,10 @@ async def _reconsider_peak( record: Optional[CoinRecord] for record in added_rec: assert record - lastest_coin_state[record.name] = record + latest_coin_state[record.name] = record for record in removed_rec: assert record - lastest_coin_state[record.name] = record + latest_coin_state[record.name] = record if npc_res is not None: hint_list: List[Tuple[bytes32, bytes]] = self.get_hint_list(npc_res) @@ -436,7 +436,7 @@ async def _reconsider_peak( key = hint if key not in hint_coin_state: hint_coin_state[key] = {} - hint_coin_state[key][coin_id] = lastest_coin_state[coin_id] + hint_coin_state[key][coin_id] = latest_coin_state[coin_id] await self.block_store.set_in_chain([(br.header_hash,) for br in records_to_add]) @@ -446,7 +446,7 @@ async def _reconsider_peak( uint32(max(fork_height, 0)), block_record.height, records_to_add, - (list(lastest_coin_state.values()), hint_coin_state), + (list(latest_coin_state.values()), hint_coin_state), ) # This is not a heavier block than the heaviest we have seen, so we don't change the coin set From cd83a9ecde2a9161fd5f4f13492e267a670799ce Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Thu, 17 Mar 2022 16:47:31 +0100 Subject: [PATCH 221/378] fix typo in command line argument parsing for chia db validate (#10716) --- chia/cmds/db.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia/cmds/db.py b/chia/cmds/db.py index 671254e68f57..b35d7049084e 100644 --- a/chia/cmds/db.py +++ b/chia/cmds/db.py @@ -46,7 +46,7 @@ def db_upgrade_cmd(ctx: click.Context, no_update_config: bool, **kwargs) -> None @click.pass_context def db_validate_cmd(ctx: click.Context, validate_blocks: bool, **kwargs) -> None: try: - in_db_path = kwargs.get("input") + in_db_path = kwargs.get("db") db_validate_func( Path(ctx.obj["root_path"]), None if in_db_path is None else Path(in_db_path), From 687ea720e3f3ef8a20a5df4de5cc97b0ec6945ae Mon Sep 17 00:00:00 2001 From: Freddie Coleman Date: Thu, 17 Mar 2022 15:48:09 +0000 Subject: [PATCH 222/378] New RPC get_coin_records_by_hint - Get coins for a given hint (#10715) * RPC endpoint to retrieve coins by hint * RPC client update for get_coin_records_by_hint * start writing tests for get_coin_records_by_hint * Address linting concerns. * Address flake8. Fix the get_coin_ids() call. * convert hint to bytes32 * tests for get_coin_records_by_hint Co-authored-by: Amine Khaldi --- chia/rpc/full_node_rpc_api.py | 30 ++++++++++++++++++++++++ chia/rpc/full_node_rpc_client.py | 16 +++++++++++++ tests/core/test_full_node_rpc.py | 40 ++++++++++++++++++++++++++++++++ tests/wallet_tools.py | 9 +++++-- 4 files changed, 93 insertions(+), 2 deletions(-) diff --git a/chia/rpc/full_node_rpc_api.py b/chia/rpc/full_node_rpc_api.py index beca3eacdb56..3f6ec6577b17 100644 --- a/chia/rpc/full_node_rpc_api.py +++ b/chia/rpc/full_node_rpc_api.py @@ -54,6 +54,7 @@ def get_routes(self) -> Dict[str, Callable]: "/get_coin_record_by_name": self.get_coin_record_by_name, "/get_coin_records_by_names": self.get_coin_records_by_names, "/get_coin_records_by_parent_ids": self.get_coin_records_by_parent_ids, + "/get_coin_records_by_hint": self.get_coin_records_by_hint, "/push_tx": self.push_tx, "/get_puzzle_and_solution": self.get_puzzle_and_solution, # Mempool @@ -586,6 +587,35 @@ async def get_coin_records_by_parent_ids(self, request: Dict) -> Optional[Dict]: return {"coin_records": [coin_record_dict_backwards_compat(cr.to_json_dict()) for cr in coin_records]} + async def get_coin_records_by_hint(self, request: Dict) -> Optional[Dict]: + """ + Retrieves coins by hint, by default returns unspent coins. + """ + if "hint" not in request: + raise ValueError("Hint not in request") + + if self.service.hint_store is None: + return {"coin_records": []} + + names: List[bytes32] = await self.service.hint_store.get_coin_ids(bytes32.from_hexstr(request["hint"])) + + kwargs: Dict[str, Any] = { + "include_spent_coins": False, + "names": names, + } + + if "start_height" in request: + kwargs["start_height"] = uint32(request["start_height"]) + if "end_height" in request: + kwargs["end_height"] = uint32(request["end_height"]) + + if "include_spent_coins" in request: + kwargs["include_spent_coins"] = request["include_spent_coins"] + + coin_records = await self.service.blockchain.coin_store.get_coin_records_by_names(**kwargs) + + return {"coin_records": [coin_record_dict_backwards_compat(cr.to_json_dict()) for cr in coin_records]} + async def push_tx(self, request: Dict) -> Optional[Dict]: if "spend_bundle" not in request: raise ValueError("Spend bundle not in request") diff --git a/chia/rpc/full_node_rpc_client.py b/chia/rpc/full_node_rpc_client.py index a5d59dd213c3..89f412259126 100644 --- a/chia/rpc/full_node_rpc_client.py +++ b/chia/rpc/full_node_rpc_client.py @@ -161,6 +161,22 @@ async def get_coin_records_by_parent_ids( response = await self.fetch("get_coin_records_by_parent_ids", d) return [CoinRecord.from_json_dict(coin_record_dict_backwards_compat(coin)) for coin in response["coin_records"]] + async def get_coin_records_by_hint( + self, + hint: bytes32, + include_spent_coins: bool = True, + start_height: Optional[int] = None, + end_height: Optional[int] = None, + ) -> List: + d = {"hint": hint.hex(), "include_spent_coins": include_spent_coins} + if start_height is not None: + d["start_height"] = start_height + if end_height is not None: + d["end_height"] = end_height + + response = await self.fetch("get_coin_records_by_hint", d) + return [CoinRecord.from_json_dict(coin_record_dict_backwards_compat(coin)) for coin in response["coin_records"]] + async def get_additions_and_removals(self, header_hash: bytes32) -> Tuple[List[CoinRecord], List[CoinRecord]]: try: response = await self.fetch("get_additions_and_removals", {"header_hash": header_hash.hex()}) diff --git a/tests/core/test_full_node_rpc.py b/tests/core/test_full_node_rpc.py index fb3107a2a1c8..704499e4df68 100644 --- a/tests/core/test_full_node_rpc.py +++ b/tests/core/test_full_node_rpc.py @@ -196,6 +196,46 @@ def stop_node_cb(): assert len(await client.get_coin_records_by_puzzle_hash(ph, True, 0, blocks[-1].height + 1)) == 2 assert len(await client.get_coin_records_by_puzzle_hash(ph, True, 0, 1)) == 0 + memo = 32 * b"\f" + + for i in range(2): + await full_node_api_1.farm_new_transaction_block(FarmNewBlockProtocol(ph_2)) + + state = await client.get_blockchain_state() + block = await client.get_block(state["peak"].header_hash) + + coin_to_spend = list(block.get_included_reward_coins())[0] + + spend_bundle = wallet.generate_signed_transaction(coin_to_spend.amount, ph_2, coin_to_spend, memo=memo) + await client.push_tx(spend_bundle) + + await full_node_api_1.farm_new_transaction_block(FarmNewBlockProtocol(ph_2)) + + coin_to_spend = (await client.get_coin_records_by_hint(memo))[0].coin + + # Spend the most recent coin so we can test including spent coins later + spend_bundle = wallet.generate_signed_transaction(coin_to_spend.amount, ph_2, coin_to_spend, memo=memo) + await client.push_tx(spend_bundle) + + await full_node_api_1.farm_new_transaction_block(FarmNewBlockProtocol(ph_2)) + + coin_records = await client.get_coin_records_by_hint(memo) + + assert len(coin_records) == 3 + + coin_records = await client.get_coin_records_by_hint(memo, include_spent_coins=False) + + assert len(coin_records) == 2 + + state = await client.get_blockchain_state() + + # Get coin records by hint + coin_records = await client.get_coin_records_by_hint( + memo, start_height=state["peak"].height - 1, end_height=state["peak"].height + ) + + assert len(coin_records) == 1 + assert len(await client.get_connections()) == 0 await client.open_connection(self_hostname, server_2._port) diff --git a/tests/wallet_tools.py b/tests/wallet_tools.py index dc3aba79ec2d..e1df8bb8792a 100644 --- a/tests/wallet_tools.py +++ b/tests/wallet_tools.py @@ -106,6 +106,7 @@ def generate_unsigned_transaction( fee: int = 0, secret_key: Optional[PrivateKey] = None, additional_outputs: Optional[List[Tuple[bytes32, int]]] = None, + memo: Optional[bytes32] = None, ) -> List[CoinSpend]: spends = [] @@ -116,7 +117,10 @@ def generate_unsigned_transaction( if ConditionOpcode.CREATE_COIN_ANNOUNCEMENT not in condition_dic: condition_dic[ConditionOpcode.CREATE_COIN_ANNOUNCEMENT] = [] - output = ConditionWithArgs(ConditionOpcode.CREATE_COIN, [new_puzzle_hash, int_to_bytes(amount)]) + coin_create = [new_puzzle_hash, int_to_bytes(amount)] + if memo is not None: + coin_create.append(memo) + output = ConditionWithArgs(ConditionOpcode.CREATE_COIN, coin_create) condition_dic[output.opcode].append(output) if additional_outputs is not None: for o in additional_outputs: @@ -199,11 +203,12 @@ def generate_signed_transaction( condition_dic: Dict[ConditionOpcode, List[ConditionWithArgs]] = None, fee: int = 0, additional_outputs: Optional[List[Tuple[bytes32, int]]] = None, + memo: Optional[bytes32] = None, ) -> SpendBundle: if condition_dic is None: condition_dic = {} transaction = self.generate_unsigned_transaction( - amount, new_puzzle_hash, [coin], condition_dic, fee, additional_outputs=additional_outputs + amount, new_puzzle_hash, [coin], condition_dic, fee, additional_outputs=additional_outputs, memo=memo ) assert transaction is not None return self.sign_transaction(transaction) From f5d692f5c38a433825e836384561013c8b140e72 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 17 Mar 2022 12:02:41 -0400 Subject: [PATCH 223/378] require test-cache repo is found in CI (#10711) --- tests/block_tools.py | 5 +++++ tests/util/blockchain.py | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/tests/block_tools.py b/tests/block_tools.py index d364cbcaf14e..2de8be522a76 100644 --- a/tests/block_tools.py +++ b/tests/block_tools.py @@ -1374,6 +1374,11 @@ def get_challenges( def get_plot_dir() -> Path: cache_path = Path(os.path.expanduser(os.getenv("CHIA_ROOT", "~/.chia/"))) / "test-plots" + + ci = os.environ.get("CI") + if ci is not None and not cache_path.exists(): + raise Exception(f"Running in CI and expected path not found: {cache_path!r}") + mkdir(cache_path) return cache_path diff --git a/tests/util/blockchain.py b/tests/util/blockchain.py index 0b75f123f8d4..c63b75579e3b 100644 --- a/tests/util/blockchain.py +++ b/tests/util/blockchain.py @@ -1,3 +1,4 @@ +import os import pickle from os import path from pathlib import Path @@ -47,6 +48,11 @@ def persistent_blocks( # TODO hash fixtures.py and blocktool.py, add to path, delete if the files changed block_path_dir = Path("~/.chia/blocks").expanduser() file_path = Path(f"~/.chia/blocks/{db_name}").expanduser() + + ci = os.environ.get("CI") + if ci is not None and not file_path.exists(): + raise Exception(f"Running in CI and expected path not found: {file_path!r}") + if not path.exists(block_path_dir): mkdir(block_path_dir.parent) mkdir(block_path_dir) From 42fff8e773d6d83308608c9bab759d8b3ddd4ab2 Mon Sep 17 00:00:00 2001 From: ChiaMineJP Date: Fri, 18 Mar 2022 01:03:30 +0900 Subject: [PATCH 224/378] Issues found in RPC API review (#10702) * Removed unnecessary substitution * Recovered property which was accidentally removed in full node RPC API * Added backward compatibility to `get_additions_and_removals` full_node RPC API * Fixed full_node rpc client --- chia/daemon/server.py | 1 - chia/rpc/full_node_rpc_api.py | 6 +++++- chia/rpc/full_node_rpc_client.py | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/chia/daemon/server.py b/chia/daemon/server.py index 1c032f57667b..e9cda8fdffce 100644 --- a/chia/daemon/server.py +++ b/chia/daemon/server.py @@ -296,7 +296,6 @@ async def handle_message( command = message["command"] destination = message["destination"] if destination != "daemon": - destination = message["destination"] if destination in self.connections: sockets = self.connections[destination] return dict_to_json_str(message), sockets diff --git a/chia/rpc/full_node_rpc_api.py b/chia/rpc/full_node_rpc_api.py index 3f6ec6577b17..4b843fa0bb99 100644 --- a/chia/rpc/full_node_rpc_api.py +++ b/chia/rpc/full_node_rpc_api.py @@ -118,6 +118,7 @@ async def get_blockchain_state(self, _request: Dict): "difficulty": 0, "sub_slot_iters": 0, "space": 0, + "mempool_size": 0, "mempool_cost": 0, "mempool_min_fees": { "cost_5000000": 0, @@ -682,7 +683,10 @@ async def get_additions_and_removals(self, request: Dict) -> Optional[Dict]: additions: List[CoinRecord] = await self.service.coin_store.get_coins_added_at_height(block.height) removals: List[CoinRecord] = await self.service.coin_store.get_coins_removed_at_height(block.height) - return {"additions": additions, "removals": removals} + return { + "additions": [coin_record_dict_backwards_compat(cr.to_json_dict()) for cr in additions], + "removals": [coin_record_dict_backwards_compat(cr.to_json_dict()) for cr in removals], + } async def get_all_mempool_tx_ids(self, request: Dict) -> Optional[Dict]: ids = list(self.service.mempool_manager.mempool.spends.keys()) diff --git a/chia/rpc/full_node_rpc_client.py b/chia/rpc/full_node_rpc_client.py index 89f412259126..5c4dd0d29a34 100644 --- a/chia/rpc/full_node_rpc_client.py +++ b/chia/rpc/full_node_rpc_client.py @@ -185,9 +185,9 @@ async def get_additions_and_removals(self, header_hash: bytes32) -> Tuple[List[C removals = [] additions = [] for coin_record in response["removals"]: - removals.append(CoinRecord.from_json_dict(coin_record)) + removals.append(CoinRecord.from_json_dict(coin_record_dict_backwards_compat(coin_record))) for coin_record in response["additions"]: - additions.append(CoinRecord.from_json_dict(coin_record)) + additions.append(CoinRecord.from_json_dict(coin_record_dict_backwards_compat(coin_record))) return additions, removals async def get_block_records(self, start: int, end: int) -> List: From 48cd97cf47c1165b30830d56b5d6b1bc15661d3c Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 17 Mar 2022 12:03:58 -0400 Subject: [PATCH 225/378] minor followup to config locking (#10696) * minor lock scope reduction * use the lock in tests --- chia/farmer/farmer.py | 6 +++--- chia/plotting/util.py | 2 +- tests/block_tools.py | 8 +++++--- tests/core/test_farmer_harvester_rpc.py | 9 +++++---- tests/core/util/test_config.py | 9 ++++++--- tests/pools/test_pool_config.py | 8 +++++--- tests/wallet/rpc/test_wallet_rpc.py | 17 +++++++++-------- 7 files changed, 34 insertions(+), 25 deletions(-) diff --git a/chia/farmer/farmer.py b/chia/farmer/farmer.py index 1a63befe07d0..8eede4e6e6a5 100644 --- a/chia/farmer/farmer.py +++ b/chia/farmer/farmer.py @@ -608,9 +608,9 @@ async def set_payout_instructions(self, launcher_id: bytes32, payout_instruction config["pool"]["pool_list"] = new_list save_config(self._root_path, "config.yaml", config) - # Force a GET /farmer which triggers the PUT /farmer if it detects the changed instructions - pool_state_dict["next_farmer_update"] = 0 - return + # Force a GET /farmer which triggers the PUT /farmer if it detects the changed instructions + pool_state_dict["next_farmer_update"] = 0 + return self.log.warning(f"Launcher id: {launcher_id} not found") diff --git a/chia/plotting/util.py b/chia/plotting/util.py index 64852201c3da..c777c2c418a1 100644 --- a/chia/plotting/util.py +++ b/chia/plotting/util.py @@ -84,7 +84,7 @@ def add_plot_directory(root_path: Path, str_path: str) -> Dict: if str(Path(str_path).resolve()) not in get_plot_directories(root_path, config): config["harvester"]["plot_directories"].append(str(Path(str_path).resolve())) save_config(root_path, "config.yaml", config) - return config + return config def remove_plot_directory(root_path: Path, str_path: str) -> None: diff --git a/tests/block_tools.py b/tests/block_tools.py index 2de8be522a76..183422750fe9 100644 --- a/tests/block_tools.py +++ b/tests/block_tools.py @@ -74,7 +74,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, save_config +from chia.util.config import get_config_lock, load_config, save_config from chia.util.hash import std_hash from chia.util.ints import uint8, uint16, uint32, uint64, uint128 from chia.util.keychain import Keychain, bytes_to_mnemonic @@ -155,7 +155,8 @@ 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") - save_config(self.root_path, "config.yaml", self._config) + with get_config_lock(self.root_path, "config.yaml"): + save_config(self.root_path, "config.yaml", self._config) overrides = self._config["network_overrides"]["constants"][self._config["selected_network"]] updated_constants = constants.replace_str_to_bytes(**overrides) if const_dict is not None: @@ -233,7 +234,8 @@ def change_config(self, new_config: Dict): overrides = self._config["network_overrides"]["constants"][self._config["selected_network"]] updated_constants = self.constants.replace_str_to_bytes(**overrides) self.constants = updated_constants - save_config(self.root_path, "config.yaml", self._config) + with get_config_lock(self.root_path, "config.yaml"): + save_config(self.root_path, "config.yaml", self._config) async def setup_plots(self): assert self.created_plots == 0 diff --git a/tests/core/test_farmer_harvester_rpc.py b/tests/core/test_farmer_harvester_rpc.py index 6287d3530b87..0b81bdb4654b 100644 --- a/tests/core/test_farmer_harvester_rpc.py +++ b/tests/core/test_farmer_harvester_rpc.py @@ -14,7 +14,7 @@ from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.bech32m import decode_puzzle_hash, encode_puzzle_hash from chia.util.byte_types import hexstr_to_bytes -from chia.util.config import load_config, save_config +from chia.util.config import get_config_lock, load_config, save_config from chia.util.hash import std_hash from chia.util.ints import uint8, uint16, uint32, uint64 from chia.wallet.derive_keys import master_sk_to_wallet_sk @@ -244,9 +244,10 @@ async def test_farmer_get_pool_state(harvester_farmer_environment, self_hostname ] root_path = farmer_api.farmer._root_path - config = load_config(root_path, "config.yaml") - config["pool"]["pool_list"] = pool_list - save_config(root_path, "config.yaml", config) + with get_config_lock(root_path, "config.yaml"): + config = load_config(root_path, "config.yaml", acquire_lock=False) + config["pool"]["pool_list"] = pool_list + save_config(root_path, "config.yaml", config) await farmer_api.farmer.update_pool_state() pool_state = (await farmer_rpc_client.get_pool_state())["pool_state"] diff --git a/tests/core/util/test_config.py b/tests/core/util/test_config.py index 41462067743f..c3484ad650e6 100644 --- a/tests/core/util/test_config.py +++ b/tests/core/util/test_config.py @@ -257,7 +257,8 @@ def test_save_config(self, root_path_populated_with_config, default_config_dict) # Sanity check that we didn't modify the default config assert config["harvester"]["farmer_peer"]["host"] != default_config_dict["harvester"]["farmer_peer"]["host"] # When: saving the modified config - save_config(root_path=root_path, filename="config.yaml", config_data=config) + with get_config_lock(root_path, "config.yaml"): + save_config(root_path=root_path, filename="config.yaml", config_data=config) # Expect: modifications should be preserved in the config read from disk loaded: Dict = load_config(root_path=root_path, filename="config.yaml") @@ -273,7 +274,8 @@ def test_multiple_writers(self, root_path_populated_with_config, default_config_ # multiple writes were observed, leading to read failures when data was partially written. default_config_dict["xyz"] = "x" * 32768 root_path: Path = root_path_populated_with_config - save_config(root_path=root_path, filename="config.yaml", config_data=default_config_dict) + with get_config_lock(root_path, "config.yaml"): + save_config(root_path=root_path, filename="config.yaml", config_data=default_config_dict) num_workers: int = 30 args = list(map(lambda _: (root_path, default_config_dict), range(num_workers))) # Spin-off several processes (not threads) to read and write config data. If any @@ -294,7 +296,8 @@ async def test_non_atomic_writes(self, root_path_populated_with_config, default_ default_config_dict["xyz"] = "x" * 32768 root_path: Path = root_path_populated_with_config - save_config(root_path=root_path, filename="config.yaml", config_data=default_config_dict) + with get_config_lock(root_path, "config.yaml"): + save_config(root_path=root_path, filename="config.yaml", config_data=default_config_dict) with ProcessPoolExecutor(max_workers=4) as pool: all_tasks = [] diff --git a/tests/pools/test_pool_config.py b/tests/pools/test_pool_config.py index 0ecd49371e1b..5dde4aec0424 100644 --- a/tests/pools/test_pool_config.py +++ b/tests/pools/test_pool_config.py @@ -4,7 +4,7 @@ from blspy import AugSchemeMPL, PrivateKey from chia.pools.pool_config import PoolWalletConfig -from chia.util.config import load_config, save_config, create_default_chia_config +from chia.util.config import get_config_lock, load_config, save_config, create_default_chia_config def test_pool_config(): @@ -37,6 +37,8 @@ def test_pool_config(): config_b["wallet"]["pool_list"] = [pwc.to_json_dict()] print(config["wallet"]["pool_list"]) - save_config(test_root, "test_pool_config_a.yaml", config_a) - save_config(test_root, "test_pool_config_b.yaml", config_b) + with get_config_lock(test_root, "test_pool_config_a.yaml"): + save_config(test_root, "test_pool_config_a.yaml", config_a) + with get_config_lock(test_root, "test_pool_config_b.yaml"): + save_config(test_root, "test_pool_config_b.yaml", config_b) assert config_a == config_b diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index 962c8d5963cf..5e7277b5d37d 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -6,7 +6,7 @@ from chia.types.coin_record import CoinRecord from chia.types.coin_spend import CoinSpend from chia.types.spend_bundle import SpendBundle -from chia.util.config import load_config, save_config +from chia.util.config import get_config_lock, load_config, save_config from operator import attrgetter import logging @@ -618,13 +618,14 @@ async def tx_in_mempool_2(): # set farmer to first private key sk = await wallet_node.get_key_for_fingerprint(pks[0]) test_ph = create_puzzlehash_for_pk(master_sk_to_wallet_sk(sk, uint32(0)).get_g1()) - test_config = load_config(wallet_node.root_path, "config.yaml") - test_config["farmer"]["xch_target_address"] = encode_puzzle_hash(test_ph, "txch") - # set pool to second private key - sk = await wallet_node.get_key_for_fingerprint(pks[1]) - test_ph = create_puzzlehash_for_pk(master_sk_to_wallet_sk(sk, uint32(0)).get_g1()) - test_config["pool"]["xch_target_address"] = encode_puzzle_hash(test_ph, "txch") - save_config(wallet_node.root_path, "config.yaml", test_config) + with get_config_lock(wallet_node.root_path, "config.yaml"): + test_config = load_config(wallet_node.root_path, "config.yaml", acquire_lock=False) + test_config["farmer"]["xch_target_address"] = encode_puzzle_hash(test_ph, "txch") + # set pool to second private key + sk = await wallet_node.get_key_for_fingerprint(pks[1]) + test_ph = create_puzzlehash_for_pk(master_sk_to_wallet_sk(sk, uint32(0)).get_g1()) + test_config["pool"]["xch_target_address"] = encode_puzzle_hash(test_ph, "txch") + save_config(wallet_node.root_path, "config.yaml", test_config) # Check first key sk_dict = await client.check_delete_key(pks[0]) From 1c8e4aef10ba7dec2f66833d139ef6e7e20426d5 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 17 Mar 2022 12:04:47 -0400 Subject: [PATCH 226/378] Use the passed root_path in configure CLI command (#10694) --- chia/cmds/configure.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/chia/cmds/configure.py b/chia/cmds/configure.py index 793207c45fb5..0123fb439683 100644 --- a/chia/cmds/configure.py +++ b/chia/cmds/configure.py @@ -4,7 +4,6 @@ import click from chia.util.config import get_config_lock, load_config, save_config, str2bool -from chia.util.default_root import DEFAULT_ROOT_PATH def configure( @@ -25,7 +24,7 @@ def configure( seeder_nameserver: str, ): with get_config_lock(root_path, "config.yaml"): - config: Dict = load_config(DEFAULT_ROOT_PATH, "config.yaml", acquire_lock=False) + config: Dict = load_config(root_path, "config.yaml", acquire_lock=False) change_made = False if set_node_introducer: try: @@ -75,7 +74,7 @@ def configure( levels = ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "NOTSET"] if set_log_level in levels: config["logging"]["log_level"] = set_log_level - print(f"Logging level updated. Check {DEFAULT_ROOT_PATH}/log/debug.log") + print(f"Logging level updated. Check {root_path}/log/debug.log") change_made = True else: print(f"Logging level not updated. Use one of: {levels}") From 6e499e0e625218f76ed2a18d1357a021d85b68f5 Mon Sep 17 00:00:00 2001 From: Amine Khaldi Date: Thu, 17 Mar 2022 17:05:33 +0100 Subject: [PATCH 227/378] Improve the CI runs w.r.t. timelord installation (#10673) * Superficial analysis showed that only two test groups require (for now) installing the timelord. This change aims to save us hours of CI running time by simply running the install timelord script only for those test groups, with everything else having it omitted. Dedicated to @hoffmang9 * We don't need these anymore. --- .github/workflows/build-test-macos-blockchain.yml | 7 +------ .github/workflows/build-test-macos-clvm.yml | 1 - .github/workflows/build-test-macos-core-cmds.yml | 7 +------ .github/workflows/build-test-macos-core-consensus.yml | 7 +------ .github/workflows/build-test-macos-core-custom_types.yml | 7 +------ .github/workflows/build-test-macos-core-daemon.yml | 1 - .../build-test-macos-core-full_node-full_sync.yml | 7 +------ .../workflows/build-test-macos-core-full_node-stores.yml | 7 +------ .github/workflows/build-test-macos-core-full_node.yml | 7 +------ .github/workflows/build-test-macos-core-server.yml | 7 +------ .github/workflows/build-test-macos-core-ssl.yml | 7 +------ .github/workflows/build-test-macos-core-util.yml | 7 +------ .github/workflows/build-test-macos-core.yml | 7 +------ .github/workflows/build-test-macos-farmer_harvester.yml | 7 +------ .github/workflows/build-test-macos-generator.yml | 7 +------ .github/workflows/build-test-macos-plotting.yml | 1 - .github/workflows/build-test-macos-pools.yml | 7 +------ .github/workflows/build-test-macos-simulation.yml | 1 - .github/workflows/build-test-macos-tools.yml | 7 +------ .github/workflows/build-test-macos-util.yml | 7 +------ .github/workflows/build-test-macos-wallet-cat_wallet.yml | 7 +------ .github/workflows/build-test-macos-wallet-did_wallet.yml | 7 +------ .github/workflows/build-test-macos-wallet-rl_wallet.yml | 7 +------ .github/workflows/build-test-macos-wallet-rpc.yml | 7 +------ .github/workflows/build-test-macos-wallet-simple_sync.yml | 7 +------ .github/workflows/build-test-macos-wallet-sync.yml | 7 +------ .github/workflows/build-test-macos-wallet.yml | 7 +------ .github/workflows/build-test-macos-weight_proof.yml | 7 +------ .github/workflows/build-test-ubuntu-blockchain.yml | 6 +----- .github/workflows/build-test-ubuntu-core-cmds.yml | 6 +----- .github/workflows/build-test-ubuntu-core-consensus.yml | 6 +----- .github/workflows/build-test-ubuntu-core-custom_types.yml | 6 +----- .../build-test-ubuntu-core-full_node-full_sync.yml | 6 +----- .../workflows/build-test-ubuntu-core-full_node-stores.yml | 6 +----- .github/workflows/build-test-ubuntu-core-full_node.yml | 6 +----- .github/workflows/build-test-ubuntu-core-server.yml | 6 +----- .github/workflows/build-test-ubuntu-core-ssl.yml | 6 +----- .github/workflows/build-test-ubuntu-core-util.yml | 6 +----- .github/workflows/build-test-ubuntu-core.yml | 6 +----- .github/workflows/build-test-ubuntu-farmer_harvester.yml | 6 +----- .github/workflows/build-test-ubuntu-generator.yml | 6 +----- .github/workflows/build-test-ubuntu-pools.yml | 6 +----- .github/workflows/build-test-ubuntu-tools.yml | 6 +----- .github/workflows/build-test-ubuntu-util.yml | 6 +----- .github/workflows/build-test-ubuntu-wallet-cat_wallet.yml | 6 +----- .github/workflows/build-test-ubuntu-wallet-did_wallet.yml | 6 +----- .github/workflows/build-test-ubuntu-wallet-rl_wallet.yml | 6 +----- .github/workflows/build-test-ubuntu-wallet-rpc.yml | 6 +----- .github/workflows/build-test-ubuntu-wallet-simple_sync.yml | 6 +----- .github/workflows/build-test-ubuntu-wallet-sync.yml | 6 +----- .github/workflows/build-test-ubuntu-wallet.yml | 6 +----- .github/workflows/build-test-ubuntu-weight_proof.yml | 6 +----- tests/clvm/config.py | 1 - tests/core/daemon/config.py | 1 + tests/plotting/config.py | 1 - tests/runner_templates/build-test-macos | 1 - tests/simulation/config.py | 1 + tests/testconfig.py | 2 +- 58 files changed, 51 insertions(+), 272 deletions(-) create mode 100644 tests/core/daemon/config.py delete mode 100644 tests/plotting/config.py diff --git a/.github/workflows/build-test-macos-blockchain.yml b/.github/workflows/build-test-macos-blockchain.yml index 3544e088deb1..a4bed0c5c345 100644 --- a/.github/workflows/build-test-macos-blockchain.yml +++ b/.github/workflows/build-test-macos-blockchain.yml @@ -80,16 +80,11 @@ jobs: - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} - BUILD_VDF_CLIENT: "N" run: | brew install boost sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test blockchain code with pytest run: | diff --git a/.github/workflows/build-test-macos-clvm.yml b/.github/workflows/build-test-macos-clvm.yml index 62f6e8b94245..e71ade151b30 100644 --- a/.github/workflows/build-test-macos-clvm.yml +++ b/.github/workflows/build-test-macos-clvm.yml @@ -67,7 +67,6 @@ jobs: - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} - BUILD_VDF_CLIENT: "N" run: | brew install boost sh install.sh -d diff --git a/.github/workflows/build-test-macos-core-cmds.yml b/.github/workflows/build-test-macos-core-cmds.yml index f59195f6ebbe..a29a1dcceff5 100644 --- a/.github/workflows/build-test-macos-core-cmds.yml +++ b/.github/workflows/build-test-macos-core-cmds.yml @@ -80,16 +80,11 @@ jobs: - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} - BUILD_VDF_CLIENT: "N" run: | brew install boost sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test core-cmds code with pytest run: | diff --git a/.github/workflows/build-test-macos-core-consensus.yml b/.github/workflows/build-test-macos-core-consensus.yml index e3e7a3c8fa47..6c3ebbc27be2 100644 --- a/.github/workflows/build-test-macos-core-consensus.yml +++ b/.github/workflows/build-test-macos-core-consensus.yml @@ -80,16 +80,11 @@ jobs: - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} - BUILD_VDF_CLIENT: "N" run: | brew install boost sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test core-consensus code with pytest run: | diff --git a/.github/workflows/build-test-macos-core-custom_types.yml b/.github/workflows/build-test-macos-core-custom_types.yml index a3935cfa82f6..0222c2ce00d3 100644 --- a/.github/workflows/build-test-macos-core-custom_types.yml +++ b/.github/workflows/build-test-macos-core-custom_types.yml @@ -80,16 +80,11 @@ jobs: - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} - BUILD_VDF_CLIENT: "N" run: | brew install boost sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test core-custom_types code with pytest run: | diff --git a/.github/workflows/build-test-macos-core-daemon.yml b/.github/workflows/build-test-macos-core-daemon.yml index cc0b08fec987..3050e274e34c 100644 --- a/.github/workflows/build-test-macos-core-daemon.yml +++ b/.github/workflows/build-test-macos-core-daemon.yml @@ -80,7 +80,6 @@ jobs: - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} - BUILD_VDF_CLIENT: "N" run: | brew install boost sh install.sh -d diff --git a/.github/workflows/build-test-macos-core-full_node-full_sync.yml b/.github/workflows/build-test-macos-core-full_node-full_sync.yml index f6de039e80ef..a978c428490d 100644 --- a/.github/workflows/build-test-macos-core-full_node-full_sync.yml +++ b/.github/workflows/build-test-macos-core-full_node-full_sync.yml @@ -80,16 +80,11 @@ jobs: - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} - BUILD_VDF_CLIENT: "N" run: | brew install boost sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test core-full_node-full_sync code with pytest run: | diff --git a/.github/workflows/build-test-macos-core-full_node-stores.yml b/.github/workflows/build-test-macos-core-full_node-stores.yml index 23bbde959313..81114297b741 100644 --- a/.github/workflows/build-test-macos-core-full_node-stores.yml +++ b/.github/workflows/build-test-macos-core-full_node-stores.yml @@ -80,16 +80,11 @@ jobs: - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} - BUILD_VDF_CLIENT: "N" run: | brew install boost sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test core-full_node-stores code with pytest run: | diff --git a/.github/workflows/build-test-macos-core-full_node.yml b/.github/workflows/build-test-macos-core-full_node.yml index 68df7eb106dc..cba830984ad3 100644 --- a/.github/workflows/build-test-macos-core-full_node.yml +++ b/.github/workflows/build-test-macos-core-full_node.yml @@ -80,16 +80,11 @@ jobs: - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} - BUILD_VDF_CLIENT: "N" run: | brew install boost sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test core-full_node code with pytest run: | diff --git a/.github/workflows/build-test-macos-core-server.yml b/.github/workflows/build-test-macos-core-server.yml index d94f8f91804b..f6fb4537b17b 100644 --- a/.github/workflows/build-test-macos-core-server.yml +++ b/.github/workflows/build-test-macos-core-server.yml @@ -80,16 +80,11 @@ jobs: - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} - BUILD_VDF_CLIENT: "N" run: | brew install boost sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test core-server code with pytest run: | diff --git a/.github/workflows/build-test-macos-core-ssl.yml b/.github/workflows/build-test-macos-core-ssl.yml index 4d5978f11202..55f54619d184 100644 --- a/.github/workflows/build-test-macos-core-ssl.yml +++ b/.github/workflows/build-test-macos-core-ssl.yml @@ -80,16 +80,11 @@ jobs: - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} - BUILD_VDF_CLIENT: "N" run: | brew install boost sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test core-ssl code with pytest run: | diff --git a/.github/workflows/build-test-macos-core-util.yml b/.github/workflows/build-test-macos-core-util.yml index 3238ee35dcd4..d3c35befdc77 100644 --- a/.github/workflows/build-test-macos-core-util.yml +++ b/.github/workflows/build-test-macos-core-util.yml @@ -80,16 +80,11 @@ jobs: - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} - BUILD_VDF_CLIENT: "N" run: | brew install boost sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test core-util code with pytest run: | diff --git a/.github/workflows/build-test-macos-core.yml b/.github/workflows/build-test-macos-core.yml index 3a1f0b87cd91..5ad96190f25d 100644 --- a/.github/workflows/build-test-macos-core.yml +++ b/.github/workflows/build-test-macos-core.yml @@ -80,16 +80,11 @@ jobs: - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} - BUILD_VDF_CLIENT: "N" run: | brew install boost sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test core code with pytest run: | diff --git a/.github/workflows/build-test-macos-farmer_harvester.yml b/.github/workflows/build-test-macos-farmer_harvester.yml index 8cc0e061dec6..d976167d6e64 100644 --- a/.github/workflows/build-test-macos-farmer_harvester.yml +++ b/.github/workflows/build-test-macos-farmer_harvester.yml @@ -80,16 +80,11 @@ jobs: - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} - BUILD_VDF_CLIENT: "N" run: | brew install boost sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test farmer_harvester code with pytest run: | diff --git a/.github/workflows/build-test-macos-generator.yml b/.github/workflows/build-test-macos-generator.yml index b199ec301f9c..af9677d0c1e9 100644 --- a/.github/workflows/build-test-macos-generator.yml +++ b/.github/workflows/build-test-macos-generator.yml @@ -80,16 +80,11 @@ jobs: - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} - BUILD_VDF_CLIENT: "N" run: | brew install boost sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test generator code with pytest run: | diff --git a/.github/workflows/build-test-macos-plotting.yml b/.github/workflows/build-test-macos-plotting.yml index 1c66bc4f61c0..e7af72bbe566 100644 --- a/.github/workflows/build-test-macos-plotting.yml +++ b/.github/workflows/build-test-macos-plotting.yml @@ -80,7 +80,6 @@ jobs: - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} - BUILD_VDF_CLIENT: "N" run: | brew install boost sh install.sh -d diff --git a/.github/workflows/build-test-macos-pools.yml b/.github/workflows/build-test-macos-pools.yml index 75ec2a74432f..15623c2c41db 100644 --- a/.github/workflows/build-test-macos-pools.yml +++ b/.github/workflows/build-test-macos-pools.yml @@ -80,16 +80,11 @@ jobs: - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} - BUILD_VDF_CLIENT: "N" run: | brew install boost sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test pools code with pytest run: | diff --git a/.github/workflows/build-test-macos-simulation.yml b/.github/workflows/build-test-macos-simulation.yml index ed3549d4b988..f4002ef43a9c 100644 --- a/.github/workflows/build-test-macos-simulation.yml +++ b/.github/workflows/build-test-macos-simulation.yml @@ -80,7 +80,6 @@ jobs: - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} - BUILD_VDF_CLIENT: "N" run: | brew install boost sh install.sh -d diff --git a/.github/workflows/build-test-macos-tools.yml b/.github/workflows/build-test-macos-tools.yml index c47c0607f446..3190020bd959 100644 --- a/.github/workflows/build-test-macos-tools.yml +++ b/.github/workflows/build-test-macos-tools.yml @@ -80,16 +80,11 @@ jobs: - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} - BUILD_VDF_CLIENT: "N" run: | brew install boost sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test tools code with pytest run: | diff --git a/.github/workflows/build-test-macos-util.yml b/.github/workflows/build-test-macos-util.yml index fce1cb87177a..efaf870eb5b7 100644 --- a/.github/workflows/build-test-macos-util.yml +++ b/.github/workflows/build-test-macos-util.yml @@ -80,16 +80,11 @@ jobs: - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} - BUILD_VDF_CLIENT: "N" run: | brew install boost sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test util code with pytest run: | diff --git a/.github/workflows/build-test-macos-wallet-cat_wallet.yml b/.github/workflows/build-test-macos-wallet-cat_wallet.yml index 3f09eb94eaf7..a7e05deaf8a3 100644 --- a/.github/workflows/build-test-macos-wallet-cat_wallet.yml +++ b/.github/workflows/build-test-macos-wallet-cat_wallet.yml @@ -80,16 +80,11 @@ jobs: - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} - BUILD_VDF_CLIENT: "N" run: | brew install boost sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test wallet-cat_wallet code with pytest run: | diff --git a/.github/workflows/build-test-macos-wallet-did_wallet.yml b/.github/workflows/build-test-macos-wallet-did_wallet.yml index 891d22e7a4fa..bb453c2c7b1e 100644 --- a/.github/workflows/build-test-macos-wallet-did_wallet.yml +++ b/.github/workflows/build-test-macos-wallet-did_wallet.yml @@ -80,16 +80,11 @@ jobs: - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} - BUILD_VDF_CLIENT: "N" run: | brew install boost sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test wallet-did_wallet code with pytest run: | diff --git a/.github/workflows/build-test-macos-wallet-rl_wallet.yml b/.github/workflows/build-test-macos-wallet-rl_wallet.yml index 5e71af654633..090d3ad9365e 100644 --- a/.github/workflows/build-test-macos-wallet-rl_wallet.yml +++ b/.github/workflows/build-test-macos-wallet-rl_wallet.yml @@ -80,16 +80,11 @@ jobs: - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} - BUILD_VDF_CLIENT: "N" run: | brew install boost sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test wallet-rl_wallet code with pytest run: | diff --git a/.github/workflows/build-test-macos-wallet-rpc.yml b/.github/workflows/build-test-macos-wallet-rpc.yml index 61e70d4a7e13..11d2e3b6e7ab 100644 --- a/.github/workflows/build-test-macos-wallet-rpc.yml +++ b/.github/workflows/build-test-macos-wallet-rpc.yml @@ -80,16 +80,11 @@ jobs: - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} - BUILD_VDF_CLIENT: "N" run: | brew install boost sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test wallet-rpc code with pytest run: | diff --git a/.github/workflows/build-test-macos-wallet-simple_sync.yml b/.github/workflows/build-test-macos-wallet-simple_sync.yml index 821d7ebf3bb5..6688f3224ef8 100644 --- a/.github/workflows/build-test-macos-wallet-simple_sync.yml +++ b/.github/workflows/build-test-macos-wallet-simple_sync.yml @@ -80,16 +80,11 @@ jobs: - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} - BUILD_VDF_CLIENT: "N" run: | brew install boost sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test wallet-simple_sync code with pytest run: | diff --git a/.github/workflows/build-test-macos-wallet-sync.yml b/.github/workflows/build-test-macos-wallet-sync.yml index 6cd37b769dce..e24edb51cecc 100644 --- a/.github/workflows/build-test-macos-wallet-sync.yml +++ b/.github/workflows/build-test-macos-wallet-sync.yml @@ -80,16 +80,11 @@ jobs: - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} - BUILD_VDF_CLIENT: "N" run: | brew install boost sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test wallet-sync code with pytest run: | diff --git a/.github/workflows/build-test-macos-wallet.yml b/.github/workflows/build-test-macos-wallet.yml index 52909bbf052f..fee9f5c09935 100644 --- a/.github/workflows/build-test-macos-wallet.yml +++ b/.github/workflows/build-test-macos-wallet.yml @@ -80,16 +80,11 @@ jobs: - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} - BUILD_VDF_CLIENT: "N" run: | brew install boost sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test wallet code with pytest run: | diff --git a/.github/workflows/build-test-macos-weight_proof.yml b/.github/workflows/build-test-macos-weight_proof.yml index b02acf646d56..5c443caee4bf 100644 --- a/.github/workflows/build-test-macos-weight_proof.yml +++ b/.github/workflows/build-test-macos-weight_proof.yml @@ -80,16 +80,11 @@ jobs: - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} - BUILD_VDF_CLIENT: "N" run: | brew install boost sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test weight_proof code with pytest run: | diff --git a/.github/workflows/build-test-ubuntu-blockchain.yml b/.github/workflows/build-test-ubuntu-blockchain.yml index 11cf3e722779..ade564bd98d0 100644 --- a/.github/workflows/build-test-ubuntu-blockchain.yml +++ b/.github/workflows/build-test-ubuntu-blockchain.yml @@ -90,11 +90,7 @@ jobs: run: | sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test blockchain code with pytest run: | diff --git a/.github/workflows/build-test-ubuntu-core-cmds.yml b/.github/workflows/build-test-ubuntu-core-cmds.yml index f15c15411f96..f1eacf48e9ab 100644 --- a/.github/workflows/build-test-ubuntu-core-cmds.yml +++ b/.github/workflows/build-test-ubuntu-core-cmds.yml @@ -90,11 +90,7 @@ jobs: run: | sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test core-cmds code with pytest run: | diff --git a/.github/workflows/build-test-ubuntu-core-consensus.yml b/.github/workflows/build-test-ubuntu-core-consensus.yml index ea62f3833cd6..7449816ed5d3 100644 --- a/.github/workflows/build-test-ubuntu-core-consensus.yml +++ b/.github/workflows/build-test-ubuntu-core-consensus.yml @@ -90,11 +90,7 @@ jobs: run: | sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test core-consensus code with pytest run: | diff --git a/.github/workflows/build-test-ubuntu-core-custom_types.yml b/.github/workflows/build-test-ubuntu-core-custom_types.yml index c969454d3d8e..cd49263314c2 100644 --- a/.github/workflows/build-test-ubuntu-core-custom_types.yml +++ b/.github/workflows/build-test-ubuntu-core-custom_types.yml @@ -90,11 +90,7 @@ jobs: run: | sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test core-custom_types code with pytest run: | diff --git a/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml b/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml index cef93fea7173..1228588f3ea0 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml @@ -90,11 +90,7 @@ jobs: run: | sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test core-full_node-full_sync code with pytest run: | diff --git a/.github/workflows/build-test-ubuntu-core-full_node-stores.yml b/.github/workflows/build-test-ubuntu-core-full_node-stores.yml index edb7a655f0bd..9851c4dcdfbb 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node-stores.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node-stores.yml @@ -90,11 +90,7 @@ jobs: run: | sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test core-full_node-stores code with pytest run: | diff --git a/.github/workflows/build-test-ubuntu-core-full_node.yml b/.github/workflows/build-test-ubuntu-core-full_node.yml index 931f1949e85f..cfd3eb81ddb0 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node.yml @@ -90,11 +90,7 @@ jobs: run: | sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test core-full_node code with pytest run: | diff --git a/.github/workflows/build-test-ubuntu-core-server.yml b/.github/workflows/build-test-ubuntu-core-server.yml index 2f203b126e11..5a94428d33f1 100644 --- a/.github/workflows/build-test-ubuntu-core-server.yml +++ b/.github/workflows/build-test-ubuntu-core-server.yml @@ -90,11 +90,7 @@ jobs: run: | sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test core-server code with pytest run: | diff --git a/.github/workflows/build-test-ubuntu-core-ssl.yml b/.github/workflows/build-test-ubuntu-core-ssl.yml index ec908a60db15..69c7c52d36e8 100644 --- a/.github/workflows/build-test-ubuntu-core-ssl.yml +++ b/.github/workflows/build-test-ubuntu-core-ssl.yml @@ -90,11 +90,7 @@ jobs: run: | sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test core-ssl code with pytest run: | diff --git a/.github/workflows/build-test-ubuntu-core-util.yml b/.github/workflows/build-test-ubuntu-core-util.yml index 8480a86c4e87..2ce686704c8b 100644 --- a/.github/workflows/build-test-ubuntu-core-util.yml +++ b/.github/workflows/build-test-ubuntu-core-util.yml @@ -90,11 +90,7 @@ jobs: run: | sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test core-util code with pytest run: | diff --git a/.github/workflows/build-test-ubuntu-core.yml b/.github/workflows/build-test-ubuntu-core.yml index c5794a6c708c..8cff8cf197a1 100644 --- a/.github/workflows/build-test-ubuntu-core.yml +++ b/.github/workflows/build-test-ubuntu-core.yml @@ -90,11 +90,7 @@ jobs: run: | sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test core code with pytest run: | diff --git a/.github/workflows/build-test-ubuntu-farmer_harvester.yml b/.github/workflows/build-test-ubuntu-farmer_harvester.yml index e83b86fd360a..ab92205c4a0b 100644 --- a/.github/workflows/build-test-ubuntu-farmer_harvester.yml +++ b/.github/workflows/build-test-ubuntu-farmer_harvester.yml @@ -90,11 +90,7 @@ jobs: run: | sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test farmer_harvester code with pytest run: | diff --git a/.github/workflows/build-test-ubuntu-generator.yml b/.github/workflows/build-test-ubuntu-generator.yml index ab75eeb93656..6e250275ee87 100644 --- a/.github/workflows/build-test-ubuntu-generator.yml +++ b/.github/workflows/build-test-ubuntu-generator.yml @@ -90,11 +90,7 @@ jobs: run: | sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test generator code with pytest run: | diff --git a/.github/workflows/build-test-ubuntu-pools.yml b/.github/workflows/build-test-ubuntu-pools.yml index 56f1d0a0e98a..fcef4625f85f 100644 --- a/.github/workflows/build-test-ubuntu-pools.yml +++ b/.github/workflows/build-test-ubuntu-pools.yml @@ -90,11 +90,7 @@ jobs: run: | sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test pools code with pytest run: | diff --git a/.github/workflows/build-test-ubuntu-tools.yml b/.github/workflows/build-test-ubuntu-tools.yml index bea355f3cfca..23c1cc94dfb2 100644 --- a/.github/workflows/build-test-ubuntu-tools.yml +++ b/.github/workflows/build-test-ubuntu-tools.yml @@ -90,11 +90,7 @@ jobs: run: | sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test tools code with pytest run: | diff --git a/.github/workflows/build-test-ubuntu-util.yml b/.github/workflows/build-test-ubuntu-util.yml index 01f55a6fb56d..5fe3a2e0042c 100644 --- a/.github/workflows/build-test-ubuntu-util.yml +++ b/.github/workflows/build-test-ubuntu-util.yml @@ -90,11 +90,7 @@ jobs: run: | sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test util code with pytest run: | diff --git a/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml index dea76d7b1d61..6a288dc4b9a0 100644 --- a/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml @@ -90,11 +90,7 @@ jobs: run: | sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test wallet-cat_wallet code with pytest run: | diff --git a/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml index f7f311148cfc..50a77868c4b1 100644 --- a/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml @@ -90,11 +90,7 @@ jobs: run: | sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test wallet-did_wallet code with pytest run: | diff --git a/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml index 3e1ddf7287cf..183214e867d9 100644 --- a/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml @@ -90,11 +90,7 @@ jobs: run: | sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test wallet-rl_wallet code with pytest run: | diff --git a/.github/workflows/build-test-ubuntu-wallet-rpc.yml b/.github/workflows/build-test-ubuntu-wallet-rpc.yml index 76a1b30a6ad5..e10e0eab8766 100644 --- a/.github/workflows/build-test-ubuntu-wallet-rpc.yml +++ b/.github/workflows/build-test-ubuntu-wallet-rpc.yml @@ -90,11 +90,7 @@ jobs: run: | sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test wallet-rpc code with pytest run: | diff --git a/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml b/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml index 34b6de85e407..2a4e371e77eb 100644 --- a/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml +++ b/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml @@ -90,11 +90,7 @@ jobs: run: | sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test wallet-simple_sync code with pytest run: | diff --git a/.github/workflows/build-test-ubuntu-wallet-sync.yml b/.github/workflows/build-test-ubuntu-wallet-sync.yml index 73d8c8e3ae76..16fa2784dd6e 100644 --- a/.github/workflows/build-test-ubuntu-wallet-sync.yml +++ b/.github/workflows/build-test-ubuntu-wallet-sync.yml @@ -90,11 +90,7 @@ jobs: run: | sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test wallet-sync code with pytest run: | diff --git a/.github/workflows/build-test-ubuntu-wallet.yml b/.github/workflows/build-test-ubuntu-wallet.yml index 35db826fb560..baf709d7efa4 100644 --- a/.github/workflows/build-test-ubuntu-wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet.yml @@ -90,11 +90,7 @@ jobs: run: | sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test wallet code with pytest run: | diff --git a/.github/workflows/build-test-ubuntu-weight_proof.yml b/.github/workflows/build-test-ubuntu-weight_proof.yml index d4d028f60377..cbbcb06170ea 100644 --- a/.github/workflows/build-test-ubuntu-weight_proof.yml +++ b/.github/workflows/build-test-ubuntu-weight_proof.yml @@ -90,11 +90,7 @@ jobs: run: | sh install.sh -d - - name: Install timelord - run: | - . ./activate - sh install-timelord.sh - ./vdf_bench square_asm 400000 +# Omitted installing Timelord - name: Test weight_proof code with pytest run: | diff --git a/tests/clvm/config.py b/tests/clvm/config.py index b49fecc1fd32..031b82365b77 100644 --- a/tests/clvm/config.py +++ b/tests/clvm/config.py @@ -1,3 +1,2 @@ parallel = True checkout_blocks_and_plots = False -install_timelord = False diff --git a/tests/core/daemon/config.py b/tests/core/daemon/config.py new file mode 100644 index 000000000000..685e5b3a49e7 --- /dev/null +++ b/tests/core/daemon/config.py @@ -0,0 +1 @@ +install_timelord = True diff --git a/tests/plotting/config.py b/tests/plotting/config.py deleted file mode 100644 index db98a0ba2222..000000000000 --- a/tests/plotting/config.py +++ /dev/null @@ -1 +0,0 @@ -install_timelord = False diff --git a/tests/runner_templates/build-test-macos b/tests/runner_templates/build-test-macos index 6f26b845d05d..b8d42f4a5fa9 100644 --- a/tests/runner_templates/build-test-macos +++ b/tests/runner_templates/build-test-macos @@ -67,7 +67,6 @@ CHECKOUT_TEST_BLOCKS_AND_PLOTS - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} - BUILD_VDF_CLIENT: "N" run: | brew install boost sh install.sh -d diff --git a/tests/simulation/config.py b/tests/simulation/config.py index d9b815b24cb2..4ef7da0588bd 100644 --- a/tests/simulation/config.py +++ b/tests/simulation/config.py @@ -1 +1,2 @@ job_timeout = 60 +install_timelord = True diff --git a/tests/testconfig.py b/tests/testconfig.py index e331dcebdd82..8e0cdf0e985a 100644 --- a/tests/testconfig.py +++ b/tests/testconfig.py @@ -4,6 +4,6 @@ # Defaults are conservative. parallel = False checkout_blocks_and_plots = True -install_timelord = True +install_timelord = False job_timeout = 30 custom_vars = ["CHECK_RESOURCE_USAGE"] From aead84a346371dae587b3c88182e23752ef4dc5f Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 17 Mar 2022 12:06:22 -0400 Subject: [PATCH 228/378] less optional around ssl (#10558) * less optional * clean up cruft * more * more * just a little less optional --- chia/daemon/client.py | 2 +- chia/server/server.py | 6 +++--- tests/block_tools.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/chia/daemon/client.py b/chia/daemon/client.py index 20cdef991649..c8ca93019a7c 100644 --- a/chia/daemon/client.py +++ b/chia/daemon/client.py @@ -124,7 +124,7 @@ async def exit(self) -> WsRpcMessage: return await self._get(request) -async def connect_to_daemon(self_hostname: str, daemon_port: int, ssl_context: Optional[ssl.SSLContext]) -> DaemonProxy: +async def connect_to_daemon(self_hostname: str, daemon_port: int, ssl_context: ssl.SSLContext) -> DaemonProxy: """ Connect to the local daemon. """ diff --git a/chia/server/server.py b/chia/server/server.py index 6fc8f34912ec..0a8983951c5a 100644 --- a/chia/server/server.py +++ b/chia/server/server.py @@ -42,7 +42,7 @@ def ssl_context_for_server( *, check_permissions: bool = True, log: Optional[logging.Logger] = None, -) -> Optional[ssl.SSLContext]: +) -> ssl.SSLContext: if check_permissions: verify_ssl_certs_and_keys([ca_cert, private_cert_path], [ca_key, private_key_path], log) @@ -70,7 +70,7 @@ def ssl_context_for_server( def ssl_context_for_root( ca_cert_file: str, *, check_permissions: bool = True, log: Optional[logging.Logger] = None -) -> Optional[ssl.SSLContext]: +) -> ssl.SSLContext: if check_permissions: verify_ssl_certs_and_keys([Path(ca_cert_file)], [], log) @@ -86,7 +86,7 @@ def ssl_context_for_client( *, check_permissions: bool = True, log: Optional[logging.Logger] = None, -) -> Optional[ssl.SSLContext]: +) -> ssl.SSLContext: if check_permissions: verify_ssl_certs_and_keys([ca_cert, private_cert_path], [ca_key, private_key_path], log) diff --git a/tests/block_tools.py b/tests/block_tools.py index 183422750fe9..6906710501a8 100644 --- a/tests/block_tools.py +++ b/tests/block_tools.py @@ -353,7 +353,7 @@ async def delete_plot(self, plot_id: bytes32): def config(self) -> Dict: return copy.deepcopy(self._config) - def get_daemon_ssl_context(self) -> Optional[ssl.SSLContext]: + def get_daemon_ssl_context(self) -> ssl.SSLContext: crt_path = self.root_path / self.config["daemon_ssl"]["private_crt"] key_path = self.root_path / self.config["daemon_ssl"]["private_key"] ca_cert_path = self.root_path / self.config["private_ssl_ca"]["crt"] From 0abaea3393bf39379251e5d81c0ca9c76a139577 Mon Sep 17 00:00:00 2001 From: dustinface <35775977+xdustinface@users.noreply.github.com> Date: Thu, 17 Mar 2022 17:07:05 +0100 Subject: [PATCH 229/378] cmds: Fix trusted peer hint in `chia wallet show` (#10695) `config` is the root config here. --- chia/cmds/wallet_funcs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index e6105ed14fb5..2b957ccd9016 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -495,7 +495,7 @@ async def print_balances(args: dict, wallet_client: WalletRpcClient, fingerprint print(f" -Spendable: {print_balance(balances['spendable_balance'], scale, address_prefix)}") print(" ") - trusted_peers: Dict = config.get("trusted_peers", {}) + trusted_peers: Dict = config["wallet"].get("trusted_peers", {}) await print_connections(wallet_client, time, NodeType, trusted_peers) From 4bd8498e5a0c00cd9cf53d1980de71c5a9c8123e Mon Sep 17 00:00:00 2001 From: arty Date: Thu, 17 Mar 2022 09:08:36 -0700 Subject: [PATCH 230/378] Enable clvm_tools_rs by default (#10554) * Enable clvm_tools_rs by default * Re-add clvm_tools dep for now as it provides python idioms for interacting with clvm data * Take lint formatting * Adam: Try making this non-parallel * Try fix for threading issue in tests * Test whether turning off parallel runs causes things to work (temp) * Test use of temp files in clvm_tools_rs as a candidate solution for atomic replacement of hex output * Use proper git+https url scheme (oops) * Update to candidate 0.1.6 so we can test * Revert version bump to re-test * Test whether we can re-enable parallelism * Attempt to mitigate concurrent test running: return own conception of the compiled output. This will work if the failing path is downstream of recompilation * fix path to hex file * Probe for source of 0-length data * Further exploration: more assertions, hopefully to trigger in the test on ci * Do an even more paranoid check to verify that we observe a file whose filesystem reported size is much smaller than expected * Try a heavier handed approach, using heavyweight lockfiles on the filesystem * Import a simple lockfile implementation and use it to enforce mutual exclusion. Simplify it and remove the unwanted os traffic * Take lint, precommit advice, bump to clvm_tools_rs 0.1.6 now that it's released * Fix lint * While i was working on this, -n auto was on the command line so i think this didn't actually do anything, but reverting my change just in case * Lint * label the hashes re: pr * Add a lock.py for spot exclusivity using the filesystem (re: adam in the pr) and a convenience wrapper that hides the details * Formatting warning * Ensure type info is present and do the obvious return of the inner function's result * Use double quotes (lint) * Properly balance blank lines * Lint: alphabetize imports * One line is required here (lint) * Remove unnecessary assignment --- chia/util/lock.py | 45 +++++++++++++++++ chia/wallet/puzzles/load_clvm.py | 83 +++++++++++++++++++++----------- setup.py | 3 +- 3 files changed, 101 insertions(+), 30 deletions(-) create mode 100644 chia/util/lock.py diff --git a/chia/util/lock.py b/chia/util/lock.py new file mode 100644 index 000000000000..dec44b32f995 --- /dev/null +++ b/chia/util/lock.py @@ -0,0 +1,45 @@ +import os +import time +from typing import Callable, Optional, TextIO, TypeVar + +T = TypeVar("T") + + +# Cribbed mostly from chia/daemon/server.py +def create_exclusive_lock(lockfile: str) -> Optional[TextIO]: + """ + Open a lockfile exclusively. + """ + + try: + fd = os.open(lockfile, os.O_CREAT | os.O_EXCL | os.O_RDWR) + f = open(fd, "w") + + f.write("lock") + except IOError: + return None + + return f + + +def with_lock(lock_filename: str, run: Callable[[], T]) -> T: + """ + Ensure that this process and this thread is the only one operating on the + resource associated with lock_filename systemwide. + + Pass through the result of run after exiting the lock. + """ + + lock_file = None + while True: + lock_file = create_exclusive_lock(lock_filename) + if lock_file is not None: + break + + time.sleep(0.1) + + try: + return run() + finally: + lock_file.close() + os.remove(lock_filename) diff --git a/chia/wallet/puzzles/load_clvm.py b/chia/wallet/puzzles/load_clvm.py index e46f51580807..89429a9f8b6f 100644 --- a/chia/wallet/puzzles/load_clvm.py +++ b/chia/wallet/puzzles/load_clvm.py @@ -1,53 +1,76 @@ import importlib import inspect import os + import pathlib import pkg_resources -from clvm_tools.clvmc import compile_clvm as compile_clvm_py from chia.types.blockchain_format.program import Program, SerializedProgram +from chia.util.lock import with_lock +from clvm_tools_rs import compile_clvm as compile_clvm_rust + + +compile_clvm_py = None + -compile_clvm = compile_clvm_py +def translate_path(p_): + p = str(p_) + if os.path.isdir(p): + return p + else: + module_object = importlib.import_module(p) + return os.path.dirname(inspect.getfile(module_object)) -# Handle optional use of clvm_tools_rs if available and requested -if "CLVM_TOOLS_RS" in os.environ: + +# Handle optional use of python clvm_tools if available and requested +if "CLVM_TOOLS" in os.environ: try: + from clvm_tools.clvmc import compile_clvm as compile_clvm_py_candidate + + compile_clvm_py = compile_clvm_py_candidate + finally: + pass + +def compile_clvm_in_lock(full_path, output, search_paths): + # Compile using rust (default) + + # Ensure path translation is done in the idiomatic way currently + # expected. It can use either a filesystem path or name a python + # module. + treated_include_paths = list(map(translate_path, search_paths)) + res = compile_clvm_rust(str(full_path), str(output), treated_include_paths) + + if "CLVM_TOOLS" in os.environ and os.environ["CLVM_TOOLS"] == "check" and compile_clvm_py is not None: + # Simple helper to read the compiled output def sha256file(f): import hashlib m = hashlib.sha256() - m.update(open(f).read().encode("utf8")) + m.update(open(f).read().strip().encode("utf8")) return m.hexdigest() - from clvm_tools_rs import compile_clvm as compile_clvm_rs + orig = "%s.orig" % output - def translate_path(p_): - p = str(p_) - if os.path.isdir(p): - return p - else: - module_object = importlib.import_module(p) - return os.path.dirname(inspect.getfile(module_object)) + compile_clvm_py(full_path, orig, search_paths=search_paths) + orig256 = sha256file(orig) + rs256 = sha256file(output) - def rust_compile_clvm(full_path, output, search_paths=[]): - treated_include_paths = list(map(translate_path, search_paths)) - compile_clvm_rs(str(full_path), str(output), treated_include_paths) + if orig256 != rs256: + print("Compiled original %s: %s vs rust %s\n" % (full_path, orig256, rs256)) + print("Aborting compilation due to mismatch with rust") + assert orig256 == rs256 + else: + print("Compilation match %s: %s\n" % (full_path, orig256)) - if os.environ["CLVM_TOOLS_RS"] == "check": - orig = str(output) + ".orig" - compile_clvm_py(full_path, orig, search_paths=search_paths) - orig256 = sha256file(orig) - rs256 = sha256file(output) + return res - if orig256 != rs256: - print("Compiled %s: %s vs %s\n" % (full_path, orig256, rs256)) - print("Aborting compilation due to mismatch with rust") - assert orig256 == rs256 - compile_clvm = rust_compile_clvm - finally: - pass +def compile_clvm(full_path, output, search_paths=[]): + def do_compile(): + compile_clvm_in_lock(full_path, output, search_paths) + + with_lock(f"{full_path}.lock", do_compile) def load_serialized_clvm(clvm_filename, package_or_requirement=__name__) -> SerializedProgram: @@ -59,20 +82,22 @@ def load_serialized_clvm(clvm_filename, package_or_requirement=__name__) -> Seri clvm_filename: file name package_or_requirement: usually `__name__` if the clvm file is in the same package """ - hex_filename = f"{clvm_filename}.hex" try: if pkg_resources.resource_exists(package_or_requirement, clvm_filename): + # Establish whether the size is zero on entry full_path = pathlib.Path(pkg_resources.resource_filename(package_or_requirement, clvm_filename)) output = full_path.parent / hex_filename compile_clvm(full_path, output, search_paths=[full_path.parent]) + except NotImplementedError: # pyinstaller doesn't support `pkg_resources.resource_exists` # so we just fall through to loading the hex clvm pass clvm_hex = pkg_resources.resource_string(package_or_requirement, hex_filename).decode("utf8") + assert len(clvm_hex.strip()) != 0 clvm_blob = bytes.fromhex(clvm_hex) return SerializedProgram.from_bytes(clvm_blob) diff --git a/setup.py b/setup.py index 11b73f736964..6675ad1f0a11 100644 --- a/setup.py +++ b/setup.py @@ -8,8 +8,9 @@ "chiabip158==1.1", # bip158-style wallet filters "chiapos==1.0.9", # proof of space "clvm==0.9.7", + "clvm_tools==0.4.3", # Currying, Program.to, other conveniences "clvm_rs==0.1.19", - "clvm_tools==0.4.3", + "clvm-tools-rs==0.1.6", # Rust implementation of clvm_tools "aiohttp==3.7.4", # HTTP server for full node rpc "aiosqlite==0.17.0", # asyncio wrapper for sqlite, to store blocks "bitstring==3.1.9", # Binary data management library From ba6eb6af3d70da42b28b17089190731e4193b706 Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Thu, 17 Mar 2022 17:09:26 +0100 Subject: [PATCH 231/378] when creating a new blockchain database implicitly, make it v2 (#10498) * when creating a new blockchain database implicitly, make it v2 * fix config deadlock --- chia/cmds/init_funcs.py | 33 ++++++++++++++++++++------------- chia/full_node/full_node.py | 16 ++++++++++++++-- chia/util/db_version.py | 14 ++++++++++++++ 3 files changed, 48 insertions(+), 15 deletions(-) diff --git a/chia/cmds/init_funcs.py b/chia/cmds/init_funcs.py index 467df9143068..4182c5be3a4b 100644 --- a/chia/cmds/init_funcs.py +++ b/chia/cmds/init_funcs.py @@ -1,5 +1,6 @@ import os import shutil +import sqlite3 from pathlib import Path from typing import Any, Dict, List, Optional, Tuple @@ -23,6 +24,7 @@ unflatten_properties, get_config_lock, ) +from chia.util.db_version import set_db_version from chia.util.keychain import Keychain from chia.util.path import mkdir, path_from_root from chia.util.ssl_check import ( @@ -463,24 +465,29 @@ def chia_init( config: Dict - if v1_db: - with get_config_lock(root_path, "config.yaml"): + with get_config_lock(root_path, "config.yaml"): + db_path_replaced: str + if v1_db: config = load_config(root_path, "config.yaml", acquire_lock=False) db_pattern = config["full_node"]["database_path"] new_db_path = db_pattern.replace("_v2_", "_v1_") config["full_node"]["database_path"] = new_db_path + db_path_replaced = new_db_path.replace("CHALLENGE", config["selected_network"]) + db_path = path_from_root(root_path, db_path_replaced) + + with sqlite3.connect(db_path) as connection: + set_db_version(connection, 1) + save_config(root_path, "config.yaml", config) - else: - config = load_config(root_path, "config.yaml")["full_node"] - db_path_replaced: str = config["database_path"].replace("CHALLENGE", config["selected_network"]) - db_path = path_from_root(root_path, db_path_replaced) - mkdir(db_path.parent) - import sqlite3 - - with sqlite3.connect(db_path) as connection: - connection.execute("CREATE TABLE database_version(version int)") - connection.execute("INSERT INTO database_version VALUES (2)") - connection.commit() + + else: + config = load_config(root_path, "config.yaml", acquire_lock=False)["full_node"] + db_path_replaced = config["database_path"].replace("CHALLENGE", config["selected_network"]) + db_path = path_from_root(root_path, db_path_replaced) + mkdir(db_path.parent) + + with sqlite3.connect(db_path) as connection: + set_db_version(connection, 2) print("") print("To see your keys, run 'chia keys show --show-mnemonic-seed'") diff --git a/chia/full_node/full_node.py b/chia/full_node/full_node.py index bce9aa04b36d..1eb2a454f31f 100644 --- a/chia/full_node/full_node.py +++ b/chia/full_node/full_node.py @@ -11,6 +11,7 @@ from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union import aiosqlite +import sqlite3 from blspy import AugSchemeMPL import chia.server.ws_connection as ws # lgtm [py/import-and-import-from] @@ -74,7 +75,7 @@ from chia.util.profiler import profile_task from datetime import datetime from chia.util.db_synchronous import db_synchronous_on -from chia.util.db_version import lookup_db_version +from chia.util.db_version import lookup_db_version, set_db_version_async class FullNode: @@ -165,11 +166,22 @@ async def _start(self): # create the store (db) and full node instance self.connection = await aiosqlite.connect(self.db_path) await self.connection.execute("pragma journal_mode=wal") - db_sync = db_synchronous_on(self.config.get("db_sync", "auto"), self.db_path) self.log.info(f"opening blockchain DB: synchronous={db_sync}") await self.connection.execute("pragma synchronous={}".format(db_sync)) + async with self.connection.execute( + "SELECT name FROM sqlite_master WHERE type='table' AND name='full_blocks'" + ) as conn: + if len(await conn.fetchall()) == 0: + try: + # this is a new DB file. Make it v2 + await set_db_version_async(self.connection, 2) + except sqlite3.OperationalError: + # it could be a database created with "chia init", which is + # empty except it has the database_version table + pass + if self.config.get("log_sqlite_cmds", False): sql_log_path = path_from_root(self.root_path, "log/sql.log") self.log.info(f"logging SQL commands to {sql_log_path}") diff --git a/chia/util/db_version.py b/chia/util/db_version.py index e6e94f0c2150..379f47267a4f 100644 --- a/chia/util/db_version.py +++ b/chia/util/db_version.py @@ -1,3 +1,5 @@ +import sqlite3 + import aiosqlite @@ -12,3 +14,15 @@ async def lookup_db_version(db: aiosqlite.Connection) -> int: except aiosqlite.OperationalError: # expects OperationalError('no such table: database_version') return 1 + + +async def set_db_version_async(db: aiosqlite.Connection, version: int) -> None: + await db.execute("CREATE TABLE database_version(version int)") + await db.execute("INSERT INTO database_version VALUES (?)", (version,)) + db.commit() + + +def set_db_version(db: sqlite3.Connection, version: int) -> None: + db.execute("CREATE TABLE database_version(version int)") + db.execute("INSERT INTO database_version VALUES (?)", (version,)) + db.commit() From 53da6ab41bdc5872eb15751449eeb125bc6ab649 Mon Sep 17 00:00:00 2001 From: Francesco Truzzi Date: Thu, 17 Mar 2022 17:10:21 +0100 Subject: [PATCH 232/378] add select_coins RPC method (#10495) * add select_coins RPC method * typing fix * fix typing, casts * add RPC coin selection tests * black formatting * fix select_coins tests --- chia/rpc/wallet_rpc_api.py | 16 ++++++++++++++++ chia/rpc/wallet_rpc_client.py | 5 +++++ tests/wallet/rpc/test_wallet_rpc.py | 8 ++++++++ 3 files changed, 29 insertions(+) diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 277acc7f436c..9ea76e3d244c 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -86,6 +86,7 @@ def get_routes(self) -> Dict[str, Callable]: "/get_farmed_amount": self.get_farmed_amount, "/create_signed_transaction": self.create_signed_transaction, "/delete_unconfirmed_transactions": self.delete_unconfirmed_transactions, + "/select_coins": self.select_coins, # CATs and trading "/cat_set_name": self.cat_set_name, "/cat_asset_id_to_name": self.cat_asset_id_to_name, @@ -820,6 +821,21 @@ async def delete_unconfirmed_transactions(self, request): await self.service.wallet_state_manager.tx_store.rebuild_tx_cache() return {} + async def select_coins(self, request) -> Dict[str, List[Dict]]: + assert self.service.wallet_state_manager is not None + + if await self.service.wallet_state_manager.synced() is False: + raise ValueError("Wallet needs to be fully synced before selecting coins") + + amount = uint64(request["amount"]) + wallet_id = uint32(request["wallet_id"]) + + wallet = self.service.wallet_state_manager.wallets[wallet_id] + async with self.service.wallet_state_manager.lock: + selected_coins = await wallet.select_coins(amount=amount) + + return {"coins": [coin.to_json_dict() for coin in selected_coins]} + ########################################################################################## # CATs and Trading ########################################################################################## diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index 8e03bbd2daf5..8d8afb6d4f78 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -230,6 +230,11 @@ async def create_signed_transaction( response: Dict = await self.fetch("create_signed_transaction", request) return TransactionRecord.from_json_dict_convenience(response["signed_tx"]) + async def select_coins(self, *, amount: int, wallet_id: int) -> List[Coin]: + request = {"amount": amount, "wallet_id": wallet_id} + response: Dict[str, List[Dict]] = await self.fetch("select_coins", request) + return [Coin.from_json_dict(coin) for coin in response["coins"]] + # DID wallet async def create_new_did_wallet(self, amount): request: Dict[str, Any] = { diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index 5e7277b5d37d..bcfbb80ecef2 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -410,6 +410,10 @@ async def eventual_balance_det(c, wallet_id: str): assert len(tx_for_address["transactions"]) == 1 assert decode_puzzle_hash(tx_for_address["transactions"][0]["to_address"]) == ph_by_addr + # Test coin selection + selected_coins = await client.select_coins(amount=1, wallet_id=1) + assert len(selected_coins) > 0 + ############## # CATS # ############## @@ -487,6 +491,10 @@ async def eventual_balance_det(c, wallet_id: str): await time_out_assert(10, eventual_balance_det, 16, client, cat_0_id) await time_out_assert(10, eventual_balance_det, 4, client_2, cat_1_id) + # Test CAT coin selection + selected_coins = await client.select_coins(amount=1, wallet_id=cat_0_id) + assert len(selected_coins) > 0 + ########## # Offers # ########## From 144c4d14782f26a6828272d2e1d50b3c4f6fdea1 Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Thu, 17 Mar 2022 17:11:08 +0100 Subject: [PATCH 233/378] improve error messages from chia db upgrade, specifically to help users if the disk is full (#10494) --- chia/cmds/db.py | 11 ++++- chia/cmds/db_upgrade_func.py | 95 +++++++++++++++++++++++++++--------- 2 files changed, 82 insertions(+), 24 deletions(-) diff --git a/chia/cmds/db.py b/chia/cmds/db.py index b35d7049084e..f907d394e0f6 100644 --- a/chia/cmds/db.py +++ b/chia/cmds/db.py @@ -19,8 +19,14 @@ def db_cmd() -> None: help="don't update config file to point to new database. When specifying a " "custom output file, the config will not be updated regardless", ) +@click.option( + "--force", + default=False, + is_flag=True, + help="force conversion despite warnings", +) @click.pass_context -def db_upgrade_cmd(ctx: click.Context, no_update_config: bool, **kwargs) -> None: +def db_upgrade_cmd(ctx: click.Context, no_update_config: bool, force: bool, **kwargs) -> None: try: in_db_path = kwargs.get("input") @@ -29,7 +35,8 @@ def db_upgrade_cmd(ctx: click.Context, no_update_config: bool, **kwargs) -> None Path(ctx.obj["root_path"]), None if in_db_path is None else Path(in_db_path), None if out_db_path is None else Path(out_db_path), - no_update_config, + no_update_config=no_update_config, + force=force, ) except RuntimeError as e: print(f"FAILED: {e}") diff --git a/chia/cmds/db_upgrade_func.py b/chia/cmds/db_upgrade_func.py index 898c241d02cf..e4e6442c1d1f 100644 --- a/chia/cmds/db_upgrade_func.py +++ b/chia/cmds/db_upgrade_func.py @@ -1,7 +1,11 @@ from typing import Dict, Optional +import platform from pathlib import Path +import shutil import sys from time import time +import textwrap +import os from chia.util.config import load_config, save_config, get_config_lock from chia.util.path import mkdir, path_from_root @@ -17,8 +21,10 @@ def db_upgrade_func( root_path: Path, in_db_path: Optional[Path] = None, out_db_path: Optional[Path] = None, + *, no_update_config: bool = False, -): + force: bool = False, +) -> None: update_config: bool = in_db_path is None and out_db_path is None and not no_update_config @@ -40,16 +46,67 @@ def db_upgrade_func( out_db_path = path_from_root(root_path, db_path_replaced) mkdir(out_db_path.parent) - convert_v1_to_v2(in_db_path, out_db_path) - - if update_config: - print("updating config.yaml") - with get_config_lock(root_path, "config.yaml"): - config = load_config(root_path, "config.yaml", acquire_lock=False) - new_db_path = db_pattern.replace("_v1_", "_v2_") - config["full_node"]["database_path"] = new_db_path - print(f"database_path: {new_db_path}") - save_config(root_path, "config.yaml", config) + total, used, free = shutil.disk_usage(out_db_path.parent) + in_db_size = in_db_path.stat().st_size + if free < in_db_size: + no_free: bool = free < in_db_size * 0.6 + strength: str + if no_free: + strength = "probably not enough" + else: + strength = "very little" + print(f"there is {strength} free space on the volume where the output database will be written:") + print(f" {out_db_path}") + print( + f"free space: {free / 1024 / 1024 / 1024:0.2f} GiB expected about " + f"{in_db_size / 1024 / 1024 / 1024:0.2f} GiB" + ) + if no_free and not force: + print("to override this check and convert anyway, pass --force") + return + + try: + convert_v1_to_v2(in_db_path, out_db_path) + + if update_config: + print("updating config.yaml") + with get_config_lock(root_path, "config.yaml"): + config = load_config(root_path, "config.yaml", acquire_lock=False) + new_db_path = db_pattern.replace("_v1_", "_v2_") + config["full_node"]["database_path"] = new_db_path + print(f"database_path: {new_db_path}") + save_config(root_path, "config.yaml", config) + + except RuntimeError as e: + print(f"conversion failed with error: {e}.") + except Exception as e: + + print( + textwrap.dedent( + f"""\ + conversion failed with error: {e}. + The target v2 database is left in place (possibly in an incomplete state) + {out_db_path} + If the failure was caused by a full disk, ensure the volumes of your + temporary- and target directory have sufficient free space.""" + ) + ) + if platform.system() == "Windows": + temp_dir = None + # this is where GetTempPath() looks + # https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-gettemppatha + if "TMP" in os.environ: + temp_dir = os.environ["TMP"] + elif "TEMP" in os.environ: + temp_dir = os.environ["TEMP"] + elif "USERPROFILE" in os.environ: + temp_dir = os.environ["USERPROFILE"] + if temp_dir is not None: + print(f"your temporary directory may be {temp_dir}") + temp_env = "TMP" + else: + temp_env = "SQLITE_TMPDIR" + print(f'you can specify the "{temp_env}" environment variable to control the temporary directory to be used') print(f"\n\nLEAVING PREVIOUS DB FILE UNTOUCHED {in_db_path}\n") @@ -67,16 +124,13 @@ def convert_v1_to_v2(in_path: Path, out_path: Path) -> None: from contextlib import closing if not in_path.exists(): - print(f"input file doesn't exist. {in_path}") - raise RuntimeError(f"can't find {in_path}") + raise RuntimeError(f"input file doesn't exist. {in_path}") if in_path == out_path: - print(f"output file is the same as the input {in_path}") - raise RuntimeError("invalid conversion files") + raise RuntimeError(f"output file is the same as the input {in_path}") if out_path.exists(): - print(f"output file already exists. {out_path}") - raise RuntimeError("already exists") + raise RuntimeError(f"output file already exists. {out_path}") print(f"opening file for reading: {in_path}") with closing(sqlite3.connect(in_path)) as in_db: @@ -84,8 +138,7 @@ def convert_v1_to_v2(in_path: Path, out_path: Path) -> None: with closing(in_db.execute("SELECT * from database_version")) as cursor: row = cursor.fetchone() if row is not None and row[0] != 1: - print(f"blockchain database already version {row[0]}\nDone") - raise RuntimeError("already v2") + raise RuntimeError(f"blockchain database already version {row[0]}. Won't convert") except sqlite3.OperationalError: pass @@ -120,8 +173,7 @@ def convert_v1_to_v2(in_path: Path, out_path: Path) -> None: with closing(in_db.execute("SELECT header_hash, height from block_records WHERE is_peak = 1")) as cursor: peak_row = cursor.fetchone() if peak_row is None: - print("v1 database does not have a peak block, there is no blockchain to convert") - raise RuntimeError("no blockchain") + raise RuntimeError("v1 database does not have a peak block, there is no blockchain to convert") peak_hash = bytes32(bytes.fromhex(peak_row[0])) peak_height = uint32(peak_row[1]) print(f"peak: {peak_hash.hex()} height: {peak_height}") @@ -161,7 +213,6 @@ def convert_v1_to_v2(in_path: Path, out_path: Path) -> None: while True: row_2 = cursor_2.fetchone() if row_2 is None: - print(f"ERROR: could not find block {hh.hex()}") raise RuntimeError(f"block {hh.hex()} not found") if bytes.fromhex(row_2[0]) == hh: break From 161a838b6ad7b5c1abe04e2032b737bff9c43134 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 17 Mar 2022 12:13:27 -0400 Subject: [PATCH 234/378] more set -o errexit (#10468) * more set -o errexit -o pipefail * no pipefail, too fancy for dash at least... --- build_scripts/build_linux_deb.sh | 2 ++ build_scripts/build_linux_rpm.sh | 2 ++ build_scripts/build_macos.sh | 2 +- build_scripts/build_macos_m1.sh | 2 +- build_scripts/clean-runner.sh | 2 ++ chia/wallet/puzzles/recompile-all.sh | 2 ++ install-gui.sh | 4 +++- install-timelord.sh | 2 ++ install.sh | 3 ++- run-py-tests.sh | 2 ++ start-gui.sh | 4 +++- 11 files changed, 22 insertions(+), 5 deletions(-) diff --git a/build_scripts/build_linux_deb.sh b/build_scripts/build_linux_deb.sh index b489d1e70f01..b60b6744ac46 100644 --- a/build_scripts/build_linux_deb.sh +++ b/build_scripts/build_linux_deb.sh @@ -1,5 +1,7 @@ #!/bin/bash +set -o errexit + if [ ! "$1" ]; then echo "This script requires either amd64 of arm64 as an argument" exit 1 diff --git a/build_scripts/build_linux_rpm.sh b/build_scripts/build_linux_rpm.sh index 02641e2eb325..e8fa595fc032 100644 --- a/build_scripts/build_linux_rpm.sh +++ b/build_scripts/build_linux_rpm.sh @@ -1,5 +1,7 @@ #!/bin/bash +set -o errexit + if [ ! "$1" ]; then echo "This script requires either amd64 of arm64 as an argument" exit 1 diff --git a/build_scripts/build_macos.sh b/build_scripts/build_macos.sh index 38758c099fa1..d1831e285f5f 100644 --- a/build_scripts/build_macos.sh +++ b/build_scripts/build_macos.sh @@ -1,6 +1,6 @@ #!/bin/bash -set -euo pipefail +set -o errexit -o nounset pip install setuptools_scm # The environment variable CHIA_INSTALLER_VERSION needs to be defined. diff --git a/build_scripts/build_macos_m1.sh b/build_scripts/build_macos_m1.sh index 5e01580e4e29..8cb006e7b4e1 100644 --- a/build_scripts/build_macos_m1.sh +++ b/build_scripts/build_macos_m1.sh @@ -1,6 +1,6 @@ #!/bin/bash -set -euo pipefail +set -o errexit -o nounset pip install setuptools_scm # The environment variable CHIA_INSTALLER_VERSION needs to be defined. diff --git a/build_scripts/clean-runner.sh b/build_scripts/clean-runner.sh index 2ae9804cd829..6aba280b147e 100644 --- a/build_scripts/clean-runner.sh +++ b/build_scripts/clean-runner.sh @@ -1,6 +1,8 @@ #!/bin/bash # Cleans up files/directories that may be left over from previous runs for a clean slate before starting a new build +set -o errexit + PWD=$(pwd) rm -rf ../venv || true diff --git a/chia/wallet/puzzles/recompile-all.sh b/chia/wallet/puzzles/recompile-all.sh index 1fcd5c7d551c..999f0412c7ff 100755 --- a/chia/wallet/puzzles/recompile-all.sh +++ b/chia/wallet/puzzles/recompile-all.sh @@ -2,6 +2,8 @@ # This hack is a quick way to recompile everything in this directory +set -o errexit + #BASE_DIR=`pwd | dirname` FILES=$(ls ./*.clvm) diff --git a/install-gui.sh b/install-gui.sh index bda599762dfb..278d156ca8e6 100755 --- a/install-gui.sh +++ b/install-gui.sh @@ -1,5 +1,7 @@ #!/bin/bash -set -e + +set -o errexit + export NODE_OPTIONS="--max-old-space-size=3000" SCRIPT_DIR=$(cd -- "$(dirname -- "$0")"; pwd) diff --git a/install-timelord.sh b/install-timelord.sh index 3be18e7f47a8..24fa76004092 100644 --- a/install-timelord.sh +++ b/install-timelord.sh @@ -1,5 +1,7 @@ #!/bin/bash +set -o errexit + if [ -z "$VIRTUAL_ENV" ]; then echo "This requires the chia python virtual environment." echo "Execute '. ./activate' before running." diff --git a/install.sh b/install.sh index 7d5ae61796d6..2f76b859943c 100644 --- a/install.sh +++ b/install.sh @@ -1,5 +1,6 @@ #!/bin/bash -set -e + +set -o errexit USAGE_TEXT="\ Usage: $0 [-d] diff --git a/run-py-tests.sh b/run-py-tests.sh index f830f9134a26..ba76043863bf 100755 --- a/run-py-tests.sh +++ b/run-py-tests.sh @@ -1,5 +1,7 @@ #!/bin/bash +set -o errexit + python3 -m venv venv # shellcheck disable=SC1091 . ./activate diff --git a/start-gui.sh b/start-gui.sh index 0a035898969b..1ff9101caaff 100755 --- a/start-gui.sh +++ b/start-gui.sh @@ -1,5 +1,7 @@ #!/bin/bash -set -e + +set -o errexit + export NODE_OPTIONS="--max-old-space-size=3000" SCRIPT_DIR=$(cd -- "$(dirname -- "$0")"; pwd) From 36ea91420119f3dc3aa7d79bfb804ed79002405b Mon Sep 17 00:00:00 2001 From: arty Date: Thu, 17 Mar 2022 14:57:39 -0700 Subject: [PATCH 235/378] Bump clvm_tools_rs to fix a problem running as daemon caused by old log message that is now eliminated (#10788) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6675ad1f0a11..7677b28ba62f 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ "clvm==0.9.7", "clvm_tools==0.4.3", # Currying, Program.to, other conveniences "clvm_rs==0.1.19", - "clvm-tools-rs==0.1.6", # Rust implementation of clvm_tools + "clvm-tools-rs==0.1.7", # Rust implementation of clvm_tools "aiohttp==3.7.4", # HTTP server for full node rpc "aiosqlite==0.17.0", # asyncio wrapper for sqlite, to store blocks "bitstring==3.1.9", # Binary data management library From 9db85f328405b41795b65e7128a28041d167d75d Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Thu, 17 Mar 2022 22:58:30 +0100 Subject: [PATCH 236/378] when running multiple services in the same process (in tests), don't initialize logging for all of them, and don't set the proctitle of the test (#10686) --- chia/server/start_service.py | 20 ++++++++++++++------ tests/setup_nodes.py | 2 +- tests/setup_services.py | 12 ++++++------ 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/chia/server/start_service.py b/chia/server/start_service.py index 80c46edaa912..0371c02a494a 100644 --- a/chia/server/start_service.py +++ b/chia/server/start_service.py @@ -43,6 +43,7 @@ def __init__( advertised_port: int, service_name: str, network_id: str, + *, upnp_ports: List[int] = [], server_listen_ports: List[int] = [], connect_peers: List[PeerInfo] = [], @@ -51,7 +52,7 @@ def __init__( rpc_info: Optional[Tuple[type, int]] = None, parse_cli_args=True, connect_to_daemon=True, - handle_signals=True, + running_new_process=True, service_name_prefix="", ) -> None: self.root_path = root_path @@ -66,17 +67,24 @@ def __init__( self._rpc_task: Optional[asyncio.Task] = None self._rpc_close_task: Optional[asyncio.Task] = None self._network_id: str = network_id - self._handle_signals = handle_signals + self._running_new_process = running_new_process + + # when we start this service as a component of an existing process, + # don't change its proctitle + if running_new_process: + proctitle_name = f"chia_{service_name_prefix}{service_name}" + setproctitle(proctitle_name) - proctitle_name = f"chia_{service_name_prefix}{service_name}" - setproctitle(proctitle_name) self._log = logging.getLogger(service_name) if parse_cli_args: service_config = load_config_cli(root_path, "config.yaml", service_name) else: service_config = load_config(root_path, "config.yaml", service_name) - initialize_logging(service_name, service_config["logging"], root_path) + + # only initialize logging once per process + if running_new_process: + initialize_logging(service_name, service_config["logging"], root_path) self._rpc_info = rpc_info private_ca_crt, private_ca_key = private_ssl_ca_paths(root_path, self.config) @@ -138,7 +146,7 @@ async def start(self, **kwargs) -> None: self._did_start = True - if self._handle_signals: + if self._running_new_process: self._enable_signals() await self._node._start(**kwargs) diff --git a/tests/setup_nodes.py b/tests/setup_nodes.py index b4122d55d0b3..83ca5601c368 100644 --- a/tests/setup_nodes.py +++ b/tests/setup_nodes.py @@ -104,7 +104,7 @@ async def setup_wallet_node( service_name_prefix="test_", ) - service = Service(**kwargs, handle_signals=False) + service = Service(**kwargs, running_new_process=False) await service.start() diff --git a/tests/setup_services.py b/tests/setup_services.py index 2043ab26c149..ebd1db149c63 100644 --- a/tests/setup_services.py +++ b/tests/setup_services.py @@ -99,7 +99,7 @@ async def setup_full_node( service_name_prefix="test_", ) - service = Service(**kwargs, handle_signals=False) + service = Service(**kwargs, running_new_process=False) await service.start() @@ -170,7 +170,7 @@ async def setup_wallet_node( service_name_prefix="test_", ) - service = Service(**kwargs, handle_signals=False) + service = Service(**kwargs, running_new_process=False) await service.start() @@ -206,7 +206,7 @@ async def setup_harvester( service_name_prefix="test_", ) - service = Service(**kwargs, handle_signals=False) + service = Service(**kwargs, running_new_process=False) if start_service: await service.start() @@ -250,7 +250,7 @@ async def setup_farmer( service_name_prefix="test_", ) - service = Service(**kwargs, handle_signals=False) + service = Service(**kwargs, running_new_process=False) if start_service: await service.start() @@ -273,7 +273,7 @@ async def setup_introducer(bt: BlockTools, port): service_name_prefix="test_", ) - service = Service(**kwargs, handle_signals=False) + service = Service(**kwargs, running_new_process=False) await service.start() @@ -331,7 +331,7 @@ async def setup_timelord( service_name_prefix="test_", ) - service = Service(**kwargs, handle_signals=False) + service = Service(**kwargs, running_new_process=False) await service.start() From 25917d56a55ef4df90ffb8674537b15878d9e7fc Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Mon, 21 Mar 2022 16:22:05 -0700 Subject: [PATCH 237/378] stop helping mkdir() do what it already does (#10802) * stop helping mkdir() do what it already does * flake8 --- tests/util/blockchain.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/util/blockchain.py b/tests/util/blockchain.py index c63b75579e3b..3cf4283aba83 100644 --- a/tests/util/blockchain.py +++ b/tests/util/blockchain.py @@ -1,6 +1,5 @@ import os import pickle -from os import path from pathlib import Path from typing import List @@ -53,9 +52,7 @@ def persistent_blocks( if ci is not None and not file_path.exists(): raise Exception(f"Running in CI and expected path not found: {file_path!r}") - if not path.exists(block_path_dir): - mkdir(block_path_dir.parent) - mkdir(block_path_dir) + mkdir(block_path_dir) if file_path.exists(): try: From aaf949aa8ba7e31b0d499d637f124ef2a677e0d9 Mon Sep 17 00:00:00 2001 From: hugepants Date: Mon, 21 Mar 2022 23:22:37 +0000 Subject: [PATCH 238/378] Capitalize display of Rpc -> RPC in `chia show -s` (#10797) --- chia/cmds/show.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia/cmds/show.py b/chia/cmds/show.py index e3224edb0eb1..47dcef7f9b56 100644 --- a/chia/cmds/show.py +++ b/chia/cmds/show.py @@ -107,7 +107,7 @@ async def show_async( full_node_port = config["full_node"]["port"] full_node_rpc_port = config["full_node"]["rpc_port"] - print(f"Network: {network_name} Port: {full_node_port} Rpc Port: {full_node_rpc_port}") + print(f"Network: {network_name} Port: {full_node_port} RPC Port: {full_node_rpc_port}") print(f"Node ID: {node_id}") print(f"Genesis Challenge: {genesis_challenge}") From 1d6ce4da1242e7e8d32669a600c3ce4a2bcc2bb7 Mon Sep 17 00:00:00 2001 From: Adam Kelly <338792+aqk@users.noreply.github.com> Date: Mon, 21 Mar 2022 16:26:11 -0700 Subject: [PATCH 239/378] Remove accidental parameters from calls to setup_simulators_and_wallets and prevent future mistakes (#10770) --- tests/setup_nodes.py | 1 + tests/wallet/test_wallet.py | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/setup_nodes.py b/tests/setup_nodes.py index 83ca5601c368..abcdc04706b1 100644 --- a/tests/setup_nodes.py +++ b/tests/setup_nodes.py @@ -227,6 +227,7 @@ async def setup_simulators_and_wallets( simulator_count: int, wallet_count: int, dic: Dict, + *, starting_height=None, key_seed=None, initial_num_public_keys=5, diff --git a/tests/wallet/test_wallet.py b/tests/wallet/test_wallet.py index 62ff22020f63..50bbb09b9124 100644 --- a/tests/wallet/test_wallet.py +++ b/tests/wallet/test_wallet.py @@ -23,7 +23,7 @@ @pytest_asyncio.fixture(scope="function") async def wallet_node(): - async for _ in setup_simulators_and_wallets(1, 1, {}, True): + async for _ in setup_simulators_and_wallets(1, 1, {}): yield _ @@ -35,19 +35,19 @@ async def wallet_node_100_pk(): @pytest_asyncio.fixture(scope="function") async def two_wallet_nodes(): - async for _ in setup_simulators_and_wallets(1, 2, {}, True): + async for _ in setup_simulators_and_wallets(1, 2, {}): yield _ @pytest_asyncio.fixture(scope="function") async def two_wallet_nodes_five_freeze(): - async for _ in setup_simulators_and_wallets(1, 2, {}, True): + async for _ in setup_simulators_and_wallets(1, 2, {}): yield _ @pytest_asyncio.fixture(scope="function") async def three_sim_two_wallets(): - async for _ in setup_simulators_and_wallets(3, 2, {}, True): + async for _ in setup_simulators_and_wallets(3, 2, {}): yield _ From 7f84309e70876e227a14a6d2e10eb63a30a393b8 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Mon, 21 Mar 2022 16:27:43 -0700 Subject: [PATCH 240/378] stop using deadsnakes, unless we need it (#10752) * stop using deadsnakes. and see... * only install dead snakes stuff if building the timelord on linux --- .../workflows/build-test-macos-core-daemon.yml | 8 ++++++++ .github/workflows/build-test-macos-simulation.yml | 8 ++++++++ .../workflows/build-test-ubuntu-blockchain.yml | 7 ------- .github/workflows/build-test-ubuntu-clvm.yml | 7 ------- .github/workflows/build-test-ubuntu-core-cmds.yml | 7 ------- .../build-test-ubuntu-core-consensus.yml | 7 ------- .../build-test-ubuntu-core-custom_types.yml | 7 ------- .../workflows/build-test-ubuntu-core-daemon.yml | 15 ++++++++------- ...build-test-ubuntu-core-full_node-full_sync.yml | 7 ------- .../build-test-ubuntu-core-full_node-stores.yml | 7 ------- .../build-test-ubuntu-core-full_node.yml | 7 ------- .../workflows/build-test-ubuntu-core-server.yml | 7 ------- .github/workflows/build-test-ubuntu-core-ssl.yml | 7 ------- .github/workflows/build-test-ubuntu-core-util.yml | 7 ------- .github/workflows/build-test-ubuntu-core.yml | 7 ------- .../build-test-ubuntu-farmer_harvester.yml | 7 ------- .github/workflows/build-test-ubuntu-generator.yml | 7 ------- .github/workflows/build-test-ubuntu-plotting.yml | 7 ------- .github/workflows/build-test-ubuntu-pools.yml | 7 ------- .../workflows/build-test-ubuntu-simulation.yml | 15 ++++++++------- .github/workflows/build-test-ubuntu-tools.yml | 7 ------- .github/workflows/build-test-ubuntu-util.yml | 7 ------- .../build-test-ubuntu-wallet-cat_wallet.yml | 7 ------- .../build-test-ubuntu-wallet-did_wallet.yml | 7 ------- .../build-test-ubuntu-wallet-rl_wallet.yml | 7 ------- .../workflows/build-test-ubuntu-wallet-rpc.yml | 7 ------- .../build-test-ubuntu-wallet-simple_sync.yml | 7 ------- .../workflows/build-test-ubuntu-wallet-sync.yml | 7 ------- .github/workflows/build-test-ubuntu-wallet.yml | 7 ------- .../workflows/build-test-ubuntu-weight_proof.yml | 7 ------- tests/runner_templates/build-test-ubuntu | 7 ------- .../runner_templates/install-timelord.include.yml | 8 ++++++++ 32 files changed, 40 insertions(+), 203 deletions(-) diff --git a/.github/workflows/build-test-macos-core-daemon.yml b/.github/workflows/build-test-macos-core-daemon.yml index 3050e274e34c..84fbfb637598 100644 --- a/.github/workflows/build-test-macos-core-daemon.yml +++ b/.github/workflows/build-test-macos-core-daemon.yml @@ -84,6 +84,14 @@ jobs: brew install boost sh install.sh -d + - name: Install Ubuntu dependencies + if: startsWith(matrix.os, 'ubuntu') + run: | + sudo apt-get install software-properties-common + sudo add-apt-repository ppa:deadsnakes/ppa + sudo apt-get update + sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y + - name: Install timelord run: | . ./activate diff --git a/.github/workflows/build-test-macos-simulation.yml b/.github/workflows/build-test-macos-simulation.yml index f4002ef43a9c..64e59534b9e0 100644 --- a/.github/workflows/build-test-macos-simulation.yml +++ b/.github/workflows/build-test-macos-simulation.yml @@ -84,6 +84,14 @@ jobs: brew install boost sh install.sh -d + - name: Install Ubuntu dependencies + if: startsWith(matrix.os, 'ubuntu') + run: | + sudo apt-get install software-properties-common + sudo add-apt-repository ppa:deadsnakes/ppa + sudo apt-get update + sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y + - name: Install timelord run: | . ./activate diff --git a/.github/workflows/build-test-ubuntu-blockchain.yml b/.github/workflows/build-test-ubuntu-blockchain.yml index ade564bd98d0..a929b884e68e 100644 --- a/.github/workflows/build-test-ubuntu-blockchain.yml +++ b/.github/workflows/build-test-ubuntu-blockchain.yml @@ -77,13 +77,6 @@ jobs: echo "$HOME/.chia" ls -al $HOME/.chia - - name: Install ubuntu dependencies - run: | - sudo apt-get install software-properties-common - sudo add-apt-repository ppa:deadsnakes/ppa - sudo apt-get update - sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-clvm.yml b/.github/workflows/build-test-ubuntu-clvm.yml index 0056f77758af..2de335682de4 100644 --- a/.github/workflows/build-test-ubuntu-clvm.yml +++ b/.github/workflows/build-test-ubuntu-clvm.yml @@ -64,13 +64,6 @@ jobs: # Omitted checking out blocks and plots repo Chia-Network/test-cache - - name: Install ubuntu dependencies - run: | - sudo apt-get install software-properties-common - sudo add-apt-repository ppa:deadsnakes/ppa - sudo apt-get update - sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-core-cmds.yml b/.github/workflows/build-test-ubuntu-core-cmds.yml index f1eacf48e9ab..474044698ce7 100644 --- a/.github/workflows/build-test-ubuntu-core-cmds.yml +++ b/.github/workflows/build-test-ubuntu-core-cmds.yml @@ -77,13 +77,6 @@ jobs: echo "$HOME/.chia" ls -al $HOME/.chia - - name: Install ubuntu dependencies - run: | - sudo apt-get install software-properties-common - sudo add-apt-repository ppa:deadsnakes/ppa - sudo apt-get update - sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-core-consensus.yml b/.github/workflows/build-test-ubuntu-core-consensus.yml index 7449816ed5d3..adcade20830f 100644 --- a/.github/workflows/build-test-ubuntu-core-consensus.yml +++ b/.github/workflows/build-test-ubuntu-core-consensus.yml @@ -77,13 +77,6 @@ jobs: echo "$HOME/.chia" ls -al $HOME/.chia - - name: Install ubuntu dependencies - run: | - sudo apt-get install software-properties-common - sudo add-apt-repository ppa:deadsnakes/ppa - sudo apt-get update - sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-core-custom_types.yml b/.github/workflows/build-test-ubuntu-core-custom_types.yml index cd49263314c2..023ad90bb082 100644 --- a/.github/workflows/build-test-ubuntu-core-custom_types.yml +++ b/.github/workflows/build-test-ubuntu-core-custom_types.yml @@ -77,13 +77,6 @@ jobs: echo "$HOME/.chia" ls -al $HOME/.chia - - name: Install ubuntu dependencies - run: | - sudo apt-get install software-properties-common - sudo add-apt-repository ppa:deadsnakes/ppa - sudo apt-get update - sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-core-daemon.yml b/.github/workflows/build-test-ubuntu-core-daemon.yml index acc5d55b4011..476b681575b6 100644 --- a/.github/workflows/build-test-ubuntu-core-daemon.yml +++ b/.github/workflows/build-test-ubuntu-core-daemon.yml @@ -77,19 +77,20 @@ jobs: echo "$HOME/.chia" ls -al $HOME/.chia - - name: Install ubuntu dependencies - run: | - sudo apt-get install software-properties-common - sudo add-apt-repository ppa:deadsnakes/ppa - sudo apt-get update - sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} run: | sh install.sh -d + - name: Install Ubuntu dependencies + if: startsWith(matrix.os, 'ubuntu') + run: | + sudo apt-get install software-properties-common + sudo add-apt-repository ppa:deadsnakes/ppa + sudo apt-get update + sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y + - name: Install timelord run: | . ./activate diff --git a/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml b/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml index 1228588f3ea0..31080127a9d4 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml @@ -77,13 +77,6 @@ jobs: echo "$HOME/.chia" ls -al $HOME/.chia - - name: Install ubuntu dependencies - run: | - sudo apt-get install software-properties-common - sudo add-apt-repository ppa:deadsnakes/ppa - sudo apt-get update - sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-core-full_node-stores.yml b/.github/workflows/build-test-ubuntu-core-full_node-stores.yml index 9851c4dcdfbb..2c8b50c68aba 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node-stores.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node-stores.yml @@ -77,13 +77,6 @@ jobs: echo "$HOME/.chia" ls -al $HOME/.chia - - name: Install ubuntu dependencies - run: | - sudo apt-get install software-properties-common - sudo add-apt-repository ppa:deadsnakes/ppa - sudo apt-get update - sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-core-full_node.yml b/.github/workflows/build-test-ubuntu-core-full_node.yml index cfd3eb81ddb0..fa58214bd3ff 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node.yml @@ -77,13 +77,6 @@ jobs: echo "$HOME/.chia" ls -al $HOME/.chia - - name: Install ubuntu dependencies - run: | - sudo apt-get install software-properties-common - sudo add-apt-repository ppa:deadsnakes/ppa - sudo apt-get update - sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-core-server.yml b/.github/workflows/build-test-ubuntu-core-server.yml index 5a94428d33f1..d73d1ba4beed 100644 --- a/.github/workflows/build-test-ubuntu-core-server.yml +++ b/.github/workflows/build-test-ubuntu-core-server.yml @@ -77,13 +77,6 @@ jobs: echo "$HOME/.chia" ls -al $HOME/.chia - - name: Install ubuntu dependencies - run: | - sudo apt-get install software-properties-common - sudo add-apt-repository ppa:deadsnakes/ppa - sudo apt-get update - sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-core-ssl.yml b/.github/workflows/build-test-ubuntu-core-ssl.yml index 69c7c52d36e8..fc1ac4c8289f 100644 --- a/.github/workflows/build-test-ubuntu-core-ssl.yml +++ b/.github/workflows/build-test-ubuntu-core-ssl.yml @@ -77,13 +77,6 @@ jobs: echo "$HOME/.chia" ls -al $HOME/.chia - - name: Install ubuntu dependencies - run: | - sudo apt-get install software-properties-common - sudo add-apt-repository ppa:deadsnakes/ppa - sudo apt-get update - sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-core-util.yml b/.github/workflows/build-test-ubuntu-core-util.yml index 2ce686704c8b..93aee80915b6 100644 --- a/.github/workflows/build-test-ubuntu-core-util.yml +++ b/.github/workflows/build-test-ubuntu-core-util.yml @@ -77,13 +77,6 @@ jobs: echo "$HOME/.chia" ls -al $HOME/.chia - - name: Install ubuntu dependencies - run: | - sudo apt-get install software-properties-common - sudo add-apt-repository ppa:deadsnakes/ppa - sudo apt-get update - sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-core.yml b/.github/workflows/build-test-ubuntu-core.yml index 8cff8cf197a1..57a6a3b2d177 100644 --- a/.github/workflows/build-test-ubuntu-core.yml +++ b/.github/workflows/build-test-ubuntu-core.yml @@ -77,13 +77,6 @@ jobs: echo "$HOME/.chia" ls -al $HOME/.chia - - name: Install ubuntu dependencies - run: | - sudo apt-get install software-properties-common - sudo add-apt-repository ppa:deadsnakes/ppa - sudo apt-get update - sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-farmer_harvester.yml b/.github/workflows/build-test-ubuntu-farmer_harvester.yml index ab92205c4a0b..157dbd140273 100644 --- a/.github/workflows/build-test-ubuntu-farmer_harvester.yml +++ b/.github/workflows/build-test-ubuntu-farmer_harvester.yml @@ -77,13 +77,6 @@ jobs: echo "$HOME/.chia" ls -al $HOME/.chia - - name: Install ubuntu dependencies - run: | - sudo apt-get install software-properties-common - sudo add-apt-repository ppa:deadsnakes/ppa - sudo apt-get update - sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-generator.yml b/.github/workflows/build-test-ubuntu-generator.yml index 6e250275ee87..54ada47e1cea 100644 --- a/.github/workflows/build-test-ubuntu-generator.yml +++ b/.github/workflows/build-test-ubuntu-generator.yml @@ -77,13 +77,6 @@ jobs: echo "$HOME/.chia" ls -al $HOME/.chia - - name: Install ubuntu dependencies - run: | - sudo apt-get install software-properties-common - sudo add-apt-repository ppa:deadsnakes/ppa - sudo apt-get update - sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-plotting.yml b/.github/workflows/build-test-ubuntu-plotting.yml index 8388efe74987..8430fa8aaeec 100644 --- a/.github/workflows/build-test-ubuntu-plotting.yml +++ b/.github/workflows/build-test-ubuntu-plotting.yml @@ -77,13 +77,6 @@ jobs: echo "$HOME/.chia" ls -al $HOME/.chia - - name: Install ubuntu dependencies - run: | - sudo apt-get install software-properties-common - sudo add-apt-repository ppa:deadsnakes/ppa - sudo apt-get update - sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-pools.yml b/.github/workflows/build-test-ubuntu-pools.yml index fcef4625f85f..d10655de209b 100644 --- a/.github/workflows/build-test-ubuntu-pools.yml +++ b/.github/workflows/build-test-ubuntu-pools.yml @@ -77,13 +77,6 @@ jobs: echo "$HOME/.chia" ls -al $HOME/.chia - - name: Install ubuntu dependencies - run: | - sudo apt-get install software-properties-common - sudo add-apt-repository ppa:deadsnakes/ppa - sudo apt-get update - sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-simulation.yml b/.github/workflows/build-test-ubuntu-simulation.yml index 9b8a221db590..81c91603f81f 100644 --- a/.github/workflows/build-test-ubuntu-simulation.yml +++ b/.github/workflows/build-test-ubuntu-simulation.yml @@ -77,19 +77,20 @@ jobs: echo "$HOME/.chia" ls -al $HOME/.chia - - name: Install ubuntu dependencies - run: | - sudo apt-get install software-properties-common - sudo add-apt-repository ppa:deadsnakes/ppa - sudo apt-get update - sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} run: | sh install.sh -d + - name: Install Ubuntu dependencies + if: startsWith(matrix.os, 'ubuntu') + run: | + sudo apt-get install software-properties-common + sudo add-apt-repository ppa:deadsnakes/ppa + sudo apt-get update + sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y + - name: Install timelord run: | . ./activate diff --git a/.github/workflows/build-test-ubuntu-tools.yml b/.github/workflows/build-test-ubuntu-tools.yml index 23c1cc94dfb2..8f689c66c6a1 100644 --- a/.github/workflows/build-test-ubuntu-tools.yml +++ b/.github/workflows/build-test-ubuntu-tools.yml @@ -77,13 +77,6 @@ jobs: echo "$HOME/.chia" ls -al $HOME/.chia - - name: Install ubuntu dependencies - run: | - sudo apt-get install software-properties-common - sudo add-apt-repository ppa:deadsnakes/ppa - sudo apt-get update - sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-util.yml b/.github/workflows/build-test-ubuntu-util.yml index 5fe3a2e0042c..74991c734bba 100644 --- a/.github/workflows/build-test-ubuntu-util.yml +++ b/.github/workflows/build-test-ubuntu-util.yml @@ -77,13 +77,6 @@ jobs: echo "$HOME/.chia" ls -al $HOME/.chia - - name: Install ubuntu dependencies - run: | - sudo apt-get install software-properties-common - sudo add-apt-repository ppa:deadsnakes/ppa - sudo apt-get update - sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml index 6a288dc4b9a0..785234c66c05 100644 --- a/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml @@ -77,13 +77,6 @@ jobs: echo "$HOME/.chia" ls -al $HOME/.chia - - name: Install ubuntu dependencies - run: | - sudo apt-get install software-properties-common - sudo add-apt-repository ppa:deadsnakes/ppa - sudo apt-get update - sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml index 50a77868c4b1..ee606bc54df5 100644 --- a/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml @@ -77,13 +77,6 @@ jobs: echo "$HOME/.chia" ls -al $HOME/.chia - - name: Install ubuntu dependencies - run: | - sudo apt-get install software-properties-common - sudo add-apt-repository ppa:deadsnakes/ppa - sudo apt-get update - sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml index 183214e867d9..176bc2bf73a2 100644 --- a/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml @@ -77,13 +77,6 @@ jobs: echo "$HOME/.chia" ls -al $HOME/.chia - - name: Install ubuntu dependencies - run: | - sudo apt-get install software-properties-common - sudo add-apt-repository ppa:deadsnakes/ppa - sudo apt-get update - sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-wallet-rpc.yml b/.github/workflows/build-test-ubuntu-wallet-rpc.yml index e10e0eab8766..51177944a934 100644 --- a/.github/workflows/build-test-ubuntu-wallet-rpc.yml +++ b/.github/workflows/build-test-ubuntu-wallet-rpc.yml @@ -77,13 +77,6 @@ jobs: echo "$HOME/.chia" ls -al $HOME/.chia - - name: Install ubuntu dependencies - run: | - sudo apt-get install software-properties-common - sudo add-apt-repository ppa:deadsnakes/ppa - sudo apt-get update - sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml b/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml index 2a4e371e77eb..03849eb8da14 100644 --- a/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml +++ b/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml @@ -77,13 +77,6 @@ jobs: echo "$HOME/.chia" ls -al $HOME/.chia - - name: Install ubuntu dependencies - run: | - sudo apt-get install software-properties-common - sudo add-apt-repository ppa:deadsnakes/ppa - sudo apt-get update - sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-wallet-sync.yml b/.github/workflows/build-test-ubuntu-wallet-sync.yml index 16fa2784dd6e..7eae2814f67c 100644 --- a/.github/workflows/build-test-ubuntu-wallet-sync.yml +++ b/.github/workflows/build-test-ubuntu-wallet-sync.yml @@ -77,13 +77,6 @@ jobs: echo "$HOME/.chia" ls -al $HOME/.chia - - name: Install ubuntu dependencies - run: | - sudo apt-get install software-properties-common - sudo add-apt-repository ppa:deadsnakes/ppa - sudo apt-get update - sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-wallet.yml b/.github/workflows/build-test-ubuntu-wallet.yml index baf709d7efa4..d55940c9fed0 100644 --- a/.github/workflows/build-test-ubuntu-wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet.yml @@ -77,13 +77,6 @@ jobs: echo "$HOME/.chia" ls -al $HOME/.chia - - name: Install ubuntu dependencies - run: | - sudo apt-get install software-properties-common - sudo add-apt-repository ppa:deadsnakes/ppa - sudo apt-get update - sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-weight_proof.yml b/.github/workflows/build-test-ubuntu-weight_proof.yml index cbbcb06170ea..a9c4a762b411 100644 --- a/.github/workflows/build-test-ubuntu-weight_proof.yml +++ b/.github/workflows/build-test-ubuntu-weight_proof.yml @@ -77,13 +77,6 @@ jobs: echo "$HOME/.chia" ls -al $HOME/.chia - - name: Install ubuntu dependencies - run: | - sudo apt-get install software-properties-common - sudo add-apt-repository ppa:deadsnakes/ppa - sudo apt-get update - sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/tests/runner_templates/build-test-ubuntu b/tests/runner_templates/build-test-ubuntu index 7f9d2c896a30..f53a61b15ca6 100644 --- a/tests/runner_templates/build-test-ubuntu +++ b/tests/runner_templates/build-test-ubuntu @@ -64,13 +64,6 @@ jobs: CHECKOUT_TEST_BLOCKS_AND_PLOTS - - name: Install ubuntu dependencies - run: | - sudo apt-get install software-properties-common - sudo add-apt-repository ppa:deadsnakes/ppa - sudo apt-get update - sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/tests/runner_templates/install-timelord.include.yml b/tests/runner_templates/install-timelord.include.yml index 361a8fe9cf6b..0093f48f6947 100644 --- a/tests/runner_templates/install-timelord.include.yml +++ b/tests/runner_templates/install-timelord.include.yml @@ -1,3 +1,11 @@ + - name: Install Ubuntu dependencies + if: startsWith(matrix.os, 'ubuntu') + run: | + sudo apt-get install software-properties-common + sudo add-apt-repository ppa:deadsnakes/ppa + sudo apt-get update + sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y + - name: Install timelord run: | . ./activate From 3d7fda47708dd92fc3553a58042da59d1b05b1e3 Mon Sep 17 00:00:00 2001 From: Jack Nelson Date: Tue, 22 Mar 2022 13:51:01 -0400 Subject: [PATCH 241/378] small change to fix branch in contributing (#10805) * small change to fix branch in contributing * Update CONTRIBUTING.md --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 96d847948702..df968e63667d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,7 +25,7 @@ We ask that external contributors create a fork of the `main` branch for any fea Members of the Chia organization may create feature branches from the `main` branch. -In the event an emergency fix is required for the release version of Chia, members of the Chia organization will create a feature branch from the current release branch `1.0.0`. +In the event an emergency fix is required for the release version of Chia, members of the Chia organization will create a feature branch from the current release branch `latest`. ## Branching Strategy @@ -93,7 +93,7 @@ workflow. 3. Install BlackConnect plugin 4. Set to run python black on save 5. Set line length to 120 -6. Install these linters https://github.com/Chia-Network/chia-blockchain/tree/main/.github/linters +6. Install the linters in the root directory ## Testnets and review environments From b8e1f3b9a9f7f78196748a489e6494e990ca4268 Mon Sep 17 00:00:00 2001 From: Adam Kelly <338792+aqk@users.noreply.github.com> Date: Tue, 22 Mar 2022 11:24:55 -0700 Subject: [PATCH 242/378] =?UTF-8?q?Rename=20confusing=20fixtures,=20especi?= =?UTF-8?q?ally=20ones=20with=20the=20same=20name=20but=20dif=E2=80=A6=20(?= =?UTF-8?q?#10772)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Rename confusing fixtures, especially ones with the same name but different implementation * rename test_environment to test_plot_environment * Make it so setup_two_nodes is no longer the name of a fixture and a utility function * revert premature fixture rename: two_wallet_nodes_start_height_1 --- tests/core/daemon/test_daemon.py | 6 +- .../full_node/stores/test_full_node_store.py | 6 +- tests/core/full_node/test_full_node.py | 14 +- tests/core/full_node/test_mempool.py | 444 ++++++++++-------- .../full_node/test_mempool_performance.py | 6 +- tests/core/full_node/test_performance.py | 6 +- tests/core/full_node/test_transactions.py | 6 +- tests/core/server/test_dos.py | 26 +- tests/core/ssl/test_ssl.py | 14 +- tests/core/test_full_node_rpc.py | 10 +- tests/plotting/test_plot_manager.py | 22 +- tests/wallet/cat_wallet/test_cat_wallet.py | 2 +- tests/wallet/did_wallet/test_did.py | 6 - tests/wallet/test_wallet.py | 14 +- 14 files changed, 308 insertions(+), 274 deletions(-) diff --git a/tests/core/daemon/test_daemon.py b/tests/core/daemon/test_daemon.py index 797727e5377e..fb6fe11465ed 100644 --- a/tests/core/daemon/test_daemon.py +++ b/tests/core/daemon/test_daemon.py @@ -49,7 +49,7 @@ async def get_daemon_with_temp_keyring(get_b_tools): # because of a hack in shutting down the full node, which means you cannot run # more than one simulations per process. @pytest_asyncio.fixture(scope="function") -async def simulation(bt, get_b_tools, get_b_tools_1): +async def daemon_simulation(bt, get_b_tools, get_b_tools_1): async for _ in setup_full_system( test_constants_modified, bt, @@ -63,8 +63,8 @@ async def simulation(bt, get_b_tools, get_b_tools_1): class TestDaemon: @pytest.mark.asyncio - async def test_daemon_simulation(self, self_hostname, simulation, bt, get_b_tools, get_b_tools_1): - node1, node2, _, _, _, _, _, _, _, _, server1, daemon1 = simulation + async def test_daemon_simulation(self, self_hostname, daemon_simulation, bt, get_b_tools, get_b_tools_1): + node1, node2, _, _, _, _, _, _, _, _, server1, daemon1 = daemon_simulation node2_port = node2.full_node.config["port"] await server1.start_client(PeerInfo(self_hostname, uint16(node2_port))) diff --git a/tests/core/full_node/stores/test_full_node_store.py b/tests/core/full_node/stores/test_full_node_store.py index e1149f188433..f5ff33c36b86 100644 --- a/tests/core/full_node/stores/test_full_node_store.py +++ b/tests/core/full_node/stores/test_full_node_store.py @@ -57,7 +57,7 @@ async def empty_blockchain(request): @pytest_asyncio.fixture(scope="function", params=[1, 2]) -async def empty_blockchain_original(request): +async def empty_blockchain_with_original_constants(request): bc1, connection, db_path = await create_blockchain(test_constants_original, request.param) yield bc1 await connection.close() @@ -739,8 +739,8 @@ async def test_basic_store_compact_blockchain(self, empty_blockchain): await self.test_basic_store(empty_blockchain, True) @pytest.mark.asyncio - async def test_long_chain_slots(self, empty_blockchain_original, default_1000_blocks): - blockchain = empty_blockchain_original + async def test_long_chain_slots(self, empty_blockchain_with_original_constants, default_1000_blocks): + blockchain = empty_blockchain_with_original_constants store = FullNodeStore(test_constants_original) blocks = default_1000_blocks peak = None diff --git a/tests/core/full_node/test_full_node.py b/tests/core/full_node/test_full_node.py index 81fed5beb660..4d02d7a37526 100644 --- a/tests/core/full_node/test_full_node.py +++ b/tests/core/full_node/test_full_node.py @@ -121,7 +121,7 @@ async def setup_four_nodes(db_version): @pytest_asyncio.fixture(scope="function") -async def setup_two_nodes(db_version): +async def setup_two_nodes_fixture(db_version): async for _ in setup_simulators_and_wallets(2, 0, {}, db_version=db_version): yield _ @@ -1422,8 +1422,8 @@ async def test_new_signage_point_caching(self, wallet_nodes, empty_blockchain, b assert full_node_1.full_node.full_node_store.get_signage_point(sp.cc_vdf.output.get_hash()) is not None @pytest.mark.asyncio - async def test_slot_catch_up_genesis(self, setup_two_nodes, bt, self_hostname): - nodes, _ = setup_two_nodes + async def test_slot_catch_up_genesis(self, setup_two_nodes_fixture, bt, self_hostname): + nodes, _ = setup_two_nodes_fixture server_1 = nodes[0].full_node.server server_2 = nodes[1].full_node.server full_node_1 = nodes[0] @@ -1512,8 +1512,8 @@ async def test_mainnet_softfork(self, wallet_nodes_mainnet, bt): await _validate_and_add_block(full_node_1.full_node.blockchain, valid_block) @pytest.mark.asyncio - async def test_compact_protocol(self, setup_two_nodes, bt): - nodes, _ = setup_two_nodes + async def test_compact_protocol(self, setup_two_nodes_fixture, bt): + nodes, _ = setup_two_nodes_fixture full_node_1 = nodes[0] full_node_2 = nodes[1] blocks = bt.get_consecutive_blocks(num_blocks=10, skip_slots=3) @@ -1630,8 +1630,8 @@ async def test_compact_protocol(self, setup_two_nodes, bt): assert full_node_2.full_node.blockchain.get_peak().height == height @pytest.mark.asyncio - async def test_compact_protocol_invalid_messages(self, setup_two_nodes, bt, self_hostname): - nodes, _ = setup_two_nodes + async def test_compact_protocol_invalid_messages(self, setup_two_nodes_fixture, bt, self_hostname): + nodes, _ = setup_two_nodes_fixture full_node_1 = nodes[0] full_node_2 = nodes[1] blocks = bt.get_consecutive_blocks(num_blocks=1, skip_slots=3) diff --git a/tests/core/full_node/test_mempool.py b/tests/core/full_node/test_mempool.py index 64571fefb7d0..c72e490ecabb 100644 --- a/tests/core/full_node/test_mempool.py +++ b/tests/core/full_node/test_mempool.py @@ -80,7 +80,7 @@ def generate_test_spend_bundle( @pytest_asyncio.fixture(scope="module") -async def two_nodes(bt, wallet_a): +async def two_nodes_mempool(bt, wallet_a): async_gen = setup_simulators_and_wallets(2, 1, {}) nodes, _ = await async_gen.__anext__() full_node_1 = nodes[0] @@ -184,9 +184,9 @@ def test_cost(self): class TestMempool: @pytest.mark.asyncio - async def test_basic_mempool(self, bt, two_nodes, wallet_a): + async def test_basic_mempool(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool max_mempool_cost = 40000000 * 5 mempool = Mempool(max_mempool_cost) @@ -246,8 +246,8 @@ async def next_block(full_node_1, wallet_a, bt) -> Coin: class TestMempoolManager: @pytest.mark.asyncio - async def test_basic_mempool_manager(self, bt, two_nodes, wallet_a, self_hostname): - full_node_1, full_node_2, server_1, server_2 = two_nodes + async def test_basic_mempool_manager(self, bt, two_nodes_mempool, wallet_a, self_hostname): + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool peer = await connect_and_get_peer(server_1, server_2, self_hostname) @@ -278,7 +278,7 @@ async def test_basic_mempool_manager(self, bt, two_nodes, wallet_a, self_hostnam (ConditionOpcode.ASSERT_HEIGHT_RELATIVE, 1, MempoolInclusionStatus.PENDING), # the absolute height and seconds tests require fresh full nodes to # run the test on. Right now, we just launch two simulations and all - # tests use the same ones. See comment at the two_nodes fixture + # tests use the same ones. See comment at the two_nodes_mempool fixture # (ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, 2, MempoolInclusionStatus.SUCCESS), # (ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, 3, MempoolInclusionStatus.SUCCESS), # (ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, 4, MempoolInclusionStatus.PENDING), @@ -290,7 +290,7 @@ async def test_basic_mempool_manager(self, bt, two_nodes, wallet_a, self_hostnam # (ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, 10032, MempoolInclusionStatus.FAILED), ], ) - async def test_ephemeral_timelock(self, bt, two_nodes, wallet_a, opcode, lock_value, expected): + async def test_ephemeral_timelock(self, bt, two_nodes_mempool, wallet_a, opcode, lock_value, expected): def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: conditions = {opcode: [ConditionWithArgs(opcode, [int_to_bytes(lock_value)])]} @@ -306,8 +306,8 @@ def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: bundle = SpendBundle.aggregate([tx1, tx2]) return bundle - full_node_1, _, server_1, _ = two_nodes - blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes, wallet_a, test_fun) + full_node_1, _, server_1, _ = two_nodes_mempool + blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes_mempool, wallet_a, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) print(f"status: {status}") @@ -324,7 +324,7 @@ def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: # this test makes sure that one spend successfully asserts the announce from # another spend, even though the assert condition is duplicated 100 times @pytest.mark.asyncio - async def test_coin_announcement_duplicate_consumed(self, bt, two_nodes, wallet_a): + async def test_coin_announcement_duplicate_consumed(self, bt, two_nodes_mempool, wallet_a): def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: announce = Announcement(coin_2.name(), b"test") cvp = ConditionWithArgs(ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT, [announce.name()]) @@ -337,8 +337,8 @@ def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: bundle = SpendBundle.aggregate([spend_bundle1, spend_bundle2]) return bundle - full_node_1, full_node_2, server_1, server_2 = two_nodes - blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes, wallet_a, test_fun) + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes_mempool, wallet_a, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) assert err is None @@ -348,7 +348,7 @@ def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: # this test makes sure that one spend successfully asserts the announce from # another spend, even though the create announcement is duplicated 100 times @pytest.mark.asyncio - async def test_coin_duplicate_announcement_consumed(self, bt, two_nodes, wallet_a): + async def test_coin_duplicate_announcement_consumed(self, bt, two_nodes_mempool, wallet_a): def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: announce = Announcement(coin_2.name(), b"test") cvp = ConditionWithArgs(ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT, [announce.name()]) @@ -361,8 +361,8 @@ def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: bundle = SpendBundle.aggregate([spend_bundle1, spend_bundle2]) return bundle - full_node_1, full_node_2, server_1, server_2 = two_nodes - blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes, wallet_a, test_fun) + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes_mempool, wallet_a, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) assert err is None @@ -370,9 +370,9 @@ def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_double_spend(self, bt, two_nodes, wallet_a, self_hostname): + async def test_double_spend(self, bt, two_nodes_mempool, wallet_a, self_hostname): reward_ph = wallet_a.get_new_puzzlehash() - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool blocks = await full_node_1.get_all_full_blocks() start_height = blocks[-1].height blocks = bt.get_consecutive_blocks( @@ -431,10 +431,10 @@ def assert_sb_not_in_pool(self, node, sb): assert node.full_node.mempool_manager.get_spendbundle(sb.name()) is None @pytest.mark.asyncio - async def test_double_spend_with_higher_fee(self, bt, two_nodes, wallet_a, self_hostname): + async def test_double_spend_with_higher_fee(self, bt, two_nodes_mempool, wallet_a, self_hostname): reward_ph = wallet_a.get_new_puzzlehash() - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool 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( @@ -508,10 +508,10 @@ async def test_double_spend_with_higher_fee(self, bt, two_nodes, wallet_a, self_ self.assert_sb_not_in_pool(full_node_1, sb3) @pytest.mark.asyncio - async def test_invalid_signature(self, bt, two_nodes, wallet_a): + async def test_invalid_signature(self, bt, two_nodes_mempool, wallet_a): reward_ph = wallet_a.get_new_puzzlehash() - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool 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( @@ -542,7 +542,7 @@ async def test_invalid_signature(self, bt, two_nodes, wallet_a): async def condition_tester( self, bt, - two_nodes, + two_nodes_mempool, wallet_a, dic: Dict[ConditionOpcode, List[ConditionWithArgs]], fee: int = 0, @@ -550,7 +550,7 @@ async def condition_tester( coin: Optional[Coin] = None, ): reward_ph = wallet_a.get_new_puzzlehash() - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool blocks = await full_node_1.get_all_full_blocks() start_height = blocks[-1].height blocks = bt.get_consecutive_blocks( @@ -579,9 +579,9 @@ async def condition_tester( return blocks, spend_bundle1, peer, status, err @pytest.mark.asyncio - async def condition_tester2(self, bt, two_nodes, wallet_a, test_fun: Callable[[Coin, Coin], SpendBundle]): + async def condition_tester2(self, bt, two_nodes_mempool, wallet_a, test_fun: Callable[[Coin, Coin], SpendBundle]): reward_ph = wallet_a.get_new_puzzlehash() - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool 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( @@ -609,9 +609,9 @@ async def condition_tester2(self, bt, two_nodes, wallet_a, test_fun: Callable[[C return blocks, bundle, status, err @pytest.mark.asyncio - async def test_invalid_block_index(self, bt, two_nodes, wallet_a): + async def test_invalid_block_index(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool blocks = await full_node_1.get_all_full_blocks() start_height = blocks[-1].height cvp = ConditionWithArgs( @@ -619,7 +619,7 @@ async def test_invalid_block_index(self, bt, two_nodes, wallet_a): [int_to_bytes(start_height + 5)], ) dic = {ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert sb1 is None # the transaction may become valid later @@ -627,13 +627,13 @@ async def test_invalid_block_index(self, bt, two_nodes, wallet_a): assert status == MempoolInclusionStatus.PENDING @pytest.mark.asyncio - async def test_block_index_missing_arg(self, bt, two_nodes, wallet_a): + async def test_block_index_missing_arg(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool blocks = await full_node_1.get_all_full_blocks() cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, []) dic = {ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert sb1 is None # the transaction may become valid later @@ -641,49 +641,49 @@ async def test_block_index_missing_arg(self, bt, two_nodes, wallet_a): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_correct_block_index(self, bt, two_nodes, wallet_a): + async def test_correct_block_index(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, [int_to_bytes(1)]) dic = {ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err is None assert sb1 is spend_bundle1 assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_block_index_garbage(self, bt, two_nodes, wallet_a): + async def test_block_index_garbage(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool # garbage at the end of the argument list is ignored cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, [int_to_bytes(1), b"garbage"]) dic = {ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err is None assert sb1 is spend_bundle1 assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_negative_block_index(self, bt, two_nodes, wallet_a): + async def test_negative_block_index(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, [int_to_bytes(-1)]) dic = {ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err is None assert sb1 is spend_bundle1 assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_invalid_block_age(self, bt, two_nodes, wallet_a): + async def test_invalid_block_age(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_RELATIVE, [int_to_bytes(5)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err == Err.ASSERT_HEIGHT_RELATIVE_FAILED assert sb1 is None @@ -691,12 +691,12 @@ async def test_invalid_block_age(self, bt, two_nodes, wallet_a): assert status == MempoolInclusionStatus.PENDING @pytest.mark.asyncio - async def test_block_age_missing_arg(self, bt, two_nodes, wallet_a): + async def test_block_age_missing_arg(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_RELATIVE, []) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err == Err.INVALID_CONDITION assert sb1 is None @@ -704,13 +704,13 @@ async def test_block_age_missing_arg(self, bt, two_nodes, wallet_a): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_correct_block_age(self, bt, two_nodes, wallet_a): + async def test_correct_block_age(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_RELATIVE, [int_to_bytes(1)]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester( - bt, two_nodes, wallet_a, dic, num_blocks=4 + bt, two_nodes_mempool, wallet_a, dic, num_blocks=4 ) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -719,14 +719,14 @@ async def test_correct_block_age(self, bt, two_nodes, wallet_a): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_block_age_garbage(self, bt, two_nodes, wallet_a): + async def test_block_age_garbage(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool # garbage at the end of the argument list is ignored cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_RELATIVE, [int_to_bytes(1), b"garbage"]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester( - bt, two_nodes, wallet_a, dic, num_blocks=4 + bt, two_nodes_mempool, wallet_a, dic, num_blocks=4 ) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -735,13 +735,13 @@ async def test_block_age_garbage(self, bt, two_nodes, wallet_a): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_negative_block_age(self, bt, two_nodes, wallet_a): + async def test_negative_block_age(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_RELATIVE, [int_to_bytes(-1)]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester( - bt, two_nodes, wallet_a, dic, num_blocks=4 + bt, two_nodes_mempool, wallet_a, dic, num_blocks=4 ) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -750,13 +750,15 @@ async def test_negative_block_age(self, bt, two_nodes, wallet_a): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_correct_my_id(self, bt, two_nodes, wallet_a): + async def test_correct_my_id(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool coin = await next_block(full_node_1, wallet_a, bt) cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_COIN_ID, [coin.name()]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic, coin=coin) + blocks, spend_bundle1, peer, status, err = await self.condition_tester( + bt, two_nodes_mempool, wallet_a, dic, coin=coin + ) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err is None @@ -764,14 +766,16 @@ async def test_correct_my_id(self, bt, two_nodes, wallet_a): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_my_id_garbage(self, bt, two_nodes, wallet_a): + async def test_my_id_garbage(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool coin = await next_block(full_node_1, wallet_a, bt) # garbage at the end of the argument list is ignored cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_COIN_ID, [coin.name(), b"garbage"]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic, coin=coin) + blocks, spend_bundle1, peer, status, err = await self.condition_tester( + bt, two_nodes_mempool, wallet_a, dic, coin=coin + ) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err is None @@ -779,14 +783,16 @@ async def test_my_id_garbage(self, bt, two_nodes, wallet_a): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_invalid_my_id(self, bt, two_nodes, wallet_a): + async def test_invalid_my_id(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool coin = await next_block(full_node_1, wallet_a, bt) coin_2 = await next_block(full_node_1, wallet_a, bt) cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_COIN_ID, [coin_2.name()]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic, coin=coin) + blocks, spend_bundle1, peer, status, err = await self.condition_tester( + bt, two_nodes_mempool, wallet_a, dic, coin=coin + ) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err == Err.ASSERT_MY_COIN_ID_FAILED @@ -794,13 +800,13 @@ async def test_invalid_my_id(self, bt, two_nodes, wallet_a): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_my_id_missing_arg(self, bt, two_nodes, wallet_a): + async def test_my_id_missing_arg(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool blocks = await full_node_1.get_all_full_blocks() cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_COIN_ID, []) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err == Err.INVALID_CONDITION @@ -808,100 +814,100 @@ async def test_my_id_missing_arg(self, bt, two_nodes, wallet_a): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_assert_time_exceeds(self, bt, two_nodes, wallet_a): + async def test_assert_time_exceeds(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool # 5 seconds should be before the next block time_now = full_node_1.full_node.blockchain.get_peak().timestamp + 5 cvp = ConditionWithArgs(ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, [int_to_bytes(time_now)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err is None assert sb1 is spend_bundle1 assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_assert_time_fail(self, bt, two_nodes, wallet_a): + async def test_assert_time_fail(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool time_now = full_node_1.full_node.blockchain.get_peak().timestamp + 1000 cvp = ConditionWithArgs(ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, [int_to_bytes(time_now)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err == Err.ASSERT_SECONDS_ABSOLUTE_FAILED assert sb1 is None assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_assert_height_pending(self, bt, two_nodes, wallet_a): + async def test_assert_height_pending(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool print(full_node_1.full_node.blockchain.get_peak()) current_height = full_node_1.full_node.blockchain.get_peak().height cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, [int_to_bytes(current_height + 4)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err == Err.ASSERT_HEIGHT_ABSOLUTE_FAILED assert sb1 is None assert status == MempoolInclusionStatus.PENDING @pytest.mark.asyncio - async def test_assert_time_negative(self, bt, two_nodes, wallet_a): + async def test_assert_time_negative(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool time_now = -1 cvp = ConditionWithArgs(ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, [int_to_bytes(time_now)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err is None assert sb1 is spend_bundle1 assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_assert_time_missing_arg(self, bt, two_nodes, wallet_a): + async def test_assert_time_missing_arg(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool cvp = ConditionWithArgs(ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, []) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err == Err.INVALID_CONDITION assert sb1 is None assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_assert_time_garbage(self, bt, two_nodes, wallet_a): + async def test_assert_time_garbage(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool time_now = full_node_1.full_node.blockchain.get_peak().timestamp + 5 # garbage at the end of the argument list is ignored cvp = ConditionWithArgs(ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, [int_to_bytes(time_now), b"garbage"]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err is None assert sb1 is spend_bundle1 assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_assert_time_relative_exceeds(self, bt, two_nodes, wallet_a): + async def test_assert_time_relative_exceeds(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool time_relative = 3 cvp = ConditionWithArgs(ConditionOpcode.ASSERT_SECONDS_RELATIVE, [int_to_bytes(time_relative)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err == Err.ASSERT_SECONDS_RELATIVE_FAILED @@ -921,15 +927,15 @@ async def test_assert_time_relative_exceeds(self, bt, two_nodes, wallet_a): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_assert_time_relative_garbage(self, bt, two_nodes, wallet_a): + async def test_assert_time_relative_garbage(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool time_relative = 0 # garbage at the end of the arguments is ignored cvp = ConditionWithArgs(ConditionOpcode.ASSERT_SECONDS_RELATIVE, [int_to_bytes(time_relative), b"garbage"]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err is None @@ -937,13 +943,13 @@ async def test_assert_time_relative_garbage(self, bt, two_nodes, wallet_a): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_assert_time_relative_missing_arg(self, bt, two_nodes, wallet_a): + async def test_assert_time_relative_missing_arg(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool cvp = ConditionWithArgs(ConditionOpcode.ASSERT_SECONDS_RELATIVE, []) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err == Err.INVALID_CONDITION @@ -951,14 +957,14 @@ async def test_assert_time_relative_missing_arg(self, bt, two_nodes, wallet_a): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_assert_time_relative_negative(self, bt, two_nodes, wallet_a): + async def test_assert_time_relative_negative(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool time_relative = -3 cvp = ConditionWithArgs(ConditionOpcode.ASSERT_SECONDS_RELATIVE, [int_to_bytes(time_relative)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err is None @@ -967,7 +973,7 @@ async def test_assert_time_relative_negative(self, bt, two_nodes, wallet_a): # ensure one spend can assert a coin announcement from another spend @pytest.mark.asyncio - async def test_correct_coin_announcement_consumed(self, bt, two_nodes, wallet_a): + async def test_correct_coin_announcement_consumed(self, bt, two_nodes_mempool, wallet_a): def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: announce = Announcement(coin_2.name(), b"test") cvp = ConditionWithArgs(ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT, [announce.name()]) @@ -980,8 +986,8 @@ def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: bundle = SpendBundle.aggregate([spend_bundle1, spend_bundle2]) return bundle - full_node_1, full_node_2, server_1, server_2 = two_nodes - blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes, wallet_a, test_fun) + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes_mempool, wallet_a, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) assert err is None @@ -991,7 +997,7 @@ def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: # ensure one spend can assert a coin announcement from another spend, even # though the conditions have garbage (ignored) at the end @pytest.mark.asyncio - async def test_coin_announcement_garbage(self, bt, two_nodes, wallet_a): + async def test_coin_announcement_garbage(self, bt, two_nodes_mempool, wallet_a): def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: announce = Announcement(coin_2.name(), b"test") # garbage at the end is ignored @@ -1006,8 +1012,8 @@ def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: bundle = SpendBundle.aggregate([spend_bundle1, spend_bundle2]) return bundle - full_node_1, full_node_2, server_1, server_2 = two_nodes - blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes, wallet_a, test_fun) + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes_mempool, wallet_a, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) assert err is None @@ -1015,8 +1021,8 @@ def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_coin_announcement_missing_arg(self, bt, two_nodes, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + async def test_coin_announcement_missing_arg(self, bt, two_nodes_mempool, wallet_a): + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool def test_fun(coin_1: Coin, coin_2: Coin): # missing arg here @@ -1029,15 +1035,15 @@ def test_fun(coin_1: Coin, coin_2: Coin): return SpendBundle.aggregate([spend_bundle1, spend_bundle2]) - blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes, wallet_a, test_fun) + blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes_mempool, wallet_a, test_fun) assert err == Err.INVALID_CONDITION assert full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) is None assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_coin_announcement_missing_arg2(self, bt, two_nodes, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + async def test_coin_announcement_missing_arg2(self, bt, two_nodes_mempool, wallet_a): + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool def test_fun(coin_1: Coin, coin_2: Coin): announce = Announcement(coin_2.name(), b"test") @@ -1051,15 +1057,15 @@ def test_fun(coin_1: Coin, coin_2: Coin): return SpendBundle.aggregate([spend_bundle1, spend_bundle2]) - blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes, wallet_a, test_fun) + blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes_mempool, wallet_a, test_fun) assert err == Err.INVALID_CONDITION assert full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) is None assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_coin_announcement_too_big(self, bt, two_nodes, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + async def test_coin_announcement_too_big(self, bt, two_nodes_mempool, wallet_a): + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool def test_fun(coin_1: Coin, coin_2: Coin): announce = Announcement(coin_2.name(), bytes([1] * 10000)) @@ -1075,7 +1081,7 @@ def test_fun(coin_1: Coin, coin_2: Coin): return SpendBundle.aggregate([spend_bundle1, spend_bundle2]) - blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes, wallet_a, test_fun) + blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes_mempool, wallet_a, test_fun) assert err == Err.ASSERT_ANNOUNCE_CONSUMED_FAILED assert full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) is None @@ -1093,8 +1099,8 @@ def test_fun(coin_1: Coin, coin_2: Coin): # ensure an assert coin announcement is rejected if it doesn't match the # create announcement @pytest.mark.asyncio - async def test_invalid_coin_announcement_rejected(self, bt, two_nodes, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + async def test_invalid_coin_announcement_rejected(self, bt, two_nodes_mempool, wallet_a): + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool def test_fun(coin_1: Coin, coin_2: Coin): announce = Announcement(coin_2.name(), b"test") @@ -1113,7 +1119,7 @@ def test_fun(coin_1: Coin, coin_2: Coin): return SpendBundle.aggregate([spend_bundle1, spend_bundle2]) - blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes, wallet_a, test_fun) + blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes_mempool, wallet_a, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) @@ -1122,8 +1128,8 @@ def test_fun(coin_1: Coin, coin_2: Coin): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_invalid_coin_announcement_rejected_two(self, bt, two_nodes, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + async def test_invalid_coin_announcement_rejected_two(self, bt, two_nodes_mempool, wallet_a): + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool def test_fun(coin_1: Coin, coin_2: Coin): announce = Announcement(coin_1.name(), b"test") @@ -1140,7 +1146,7 @@ def test_fun(coin_1: Coin, coin_2: Coin): return SpendBundle.aggregate([spend_bundle1, spend_bundle2]) - blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes, wallet_a, test_fun) + blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes_mempool, wallet_a, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) assert err == Err.ASSERT_ANNOUNCE_CONSUMED_FAILED @@ -1148,8 +1154,8 @@ def test_fun(coin_1: Coin, coin_2: Coin): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_correct_puzzle_announcement(self, bt, two_nodes, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + async def test_correct_puzzle_announcement(self, bt, two_nodes_mempool, wallet_a): + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool def test_fun(coin_1: Coin, coin_2: Coin): announce = Announcement(coin_2.puzzle_hash, bytes(0x80)) @@ -1165,7 +1171,7 @@ def test_fun(coin_1: Coin, coin_2: Coin): return SpendBundle.aggregate([spend_bundle1, spend_bundle2]) - blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes, wallet_a, test_fun) + blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes_mempool, wallet_a, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) @@ -1174,8 +1180,8 @@ def test_fun(coin_1: Coin, coin_2: Coin): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_puzzle_announcement_garbage(self, bt, two_nodes, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + async def test_puzzle_announcement_garbage(self, bt, two_nodes_mempool, wallet_a): + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool def test_fun(coin_1: Coin, coin_2: Coin): announce = Announcement(coin_2.puzzle_hash, bytes(0x80)) @@ -1191,7 +1197,7 @@ def test_fun(coin_1: Coin, coin_2: Coin): return SpendBundle.aggregate([spend_bundle1, spend_bundle2]) - blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes, wallet_a, test_fun) + blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes_mempool, wallet_a, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) assert err is None @@ -1199,8 +1205,8 @@ def test_fun(coin_1: Coin, coin_2: Coin): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_puzzle_announcement_missing_arg(self, bt, two_nodes, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + async def test_puzzle_announcement_missing_arg(self, bt, two_nodes_mempool, wallet_a): + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool def test_fun(coin_1: Coin, coin_2: Coin): # missing arg here @@ -1216,7 +1222,7 @@ def test_fun(coin_1: Coin, coin_2: Coin): return SpendBundle.aggregate([spend_bundle1, spend_bundle2]) - blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes, wallet_a, test_fun) + blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes_mempool, wallet_a, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) @@ -1225,8 +1231,8 @@ def test_fun(coin_1: Coin, coin_2: Coin): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_puzzle_announcement_missing_arg2(self, bt, two_nodes, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + async def test_puzzle_announcement_missing_arg2(self, bt, two_nodes_mempool, wallet_a): + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool def test_fun(coin_1: Coin, coin_2: Coin): announce = Announcement(coin_2.puzzle_hash, b"test") @@ -1244,7 +1250,7 @@ def test_fun(coin_1: Coin, coin_2: Coin): return SpendBundle.aggregate([spend_bundle1, spend_bundle2]) - blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes, wallet_a, test_fun) + blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes_mempool, wallet_a, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) @@ -1253,8 +1259,8 @@ def test_fun(coin_1: Coin, coin_2: Coin): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_invalid_puzzle_announcement_rejected(self, bt, two_nodes, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + async def test_invalid_puzzle_announcement_rejected(self, bt, two_nodes_mempool, wallet_a): + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool def test_fun(coin_1: Coin, coin_2: Coin): announce = Announcement(coin_2.puzzle_hash, bytes("test", "utf-8")) @@ -1273,7 +1279,7 @@ def test_fun(coin_1: Coin, coin_2: Coin): return SpendBundle.aggregate([spend_bundle1, spend_bundle2]) - blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes, wallet_a, test_fun) + blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes_mempool, wallet_a, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) @@ -1282,8 +1288,8 @@ def test_fun(coin_1: Coin, coin_2: Coin): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_invalid_puzzle_announcement_rejected_two(self, bt, two_nodes, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + async def test_invalid_puzzle_announcement_rejected_two(self, bt, two_nodes_mempool, wallet_a): + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool def test_fun(coin_1: Coin, coin_2: Coin): announce = Announcement(coin_2.puzzle_hash, b"test") @@ -1302,7 +1308,7 @@ def test_fun(coin_1: Coin, coin_2: Coin): return SpendBundle.aggregate([spend_bundle1, spend_bundle2]) - blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes, wallet_a, test_fun) + blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes_mempool, wallet_a, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) @@ -1311,12 +1317,14 @@ def test_fun(coin_1: Coin, coin_2: Coin): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_assert_fee_condition(self, bt, two_nodes, wallet_a): + async def test_assert_fee_condition(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool cvp = ConditionWithArgs(ConditionOpcode.RESERVE_FEE, [int_to_bytes(10)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic, fee=10) + blocks, spend_bundle1, peer, status, err = await self.condition_tester( + bt, two_nodes_mempool, wallet_a, dic, fee=10 + ) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err is None @@ -1324,13 +1332,15 @@ async def test_assert_fee_condition(self, bt, two_nodes, wallet_a): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_assert_fee_condition_garbage(self, bt, two_nodes, wallet_a): + async def test_assert_fee_condition_garbage(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool # garbage at the end of the arguments is ignored cvp = ConditionWithArgs(ConditionOpcode.RESERVE_FEE, [int_to_bytes(10), b"garbage"]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic, fee=10) + blocks, spend_bundle1, peer, status, err = await self.condition_tester( + bt, two_nodes_mempool, wallet_a, dic, fee=10 + ) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err is None @@ -1338,20 +1348,24 @@ async def test_assert_fee_condition_garbage(self, bt, two_nodes, wallet_a): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_assert_fee_condition_missing_arg(self, bt, two_nodes, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + async def test_assert_fee_condition_missing_arg(self, bt, two_nodes_mempool, wallet_a): + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool cvp = ConditionWithArgs(ConditionOpcode.RESERVE_FEE, []) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic, fee=10) + blocks, spend_bundle1, peer, status, err = await self.condition_tester( + bt, two_nodes_mempool, wallet_a, dic, fee=10 + ) assert err == Err.INVALID_CONDITION assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_assert_fee_condition_negative_fee(self, bt, two_nodes, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + async def test_assert_fee_condition_negative_fee(self, bt, two_nodes_mempool, wallet_a): + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool cvp = ConditionWithArgs(ConditionOpcode.RESERVE_FEE, [int_to_bytes(-1)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic, fee=10) + blocks, spend_bundle1, peer, status, err = await self.condition_tester( + bt, two_nodes_mempool, wallet_a, dic, fee=10 + ) assert err == Err.RESERVE_FEE_CONDITION_FAILED assert status == MempoolInclusionStatus.FAILED blocks = bt.get_consecutive_blocks( @@ -1363,11 +1377,13 @@ async def test_assert_fee_condition_negative_fee(self, bt, two_nodes, wallet_a): ) @pytest.mark.asyncio - async def test_assert_fee_condition_fee_too_large(self, bt, two_nodes, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + async def test_assert_fee_condition_fee_too_large(self, bt, two_nodes_mempool, wallet_a): + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool cvp = ConditionWithArgs(ConditionOpcode.RESERVE_FEE, [int_to_bytes(2 ** 64)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic, fee=10) + blocks, spend_bundle1, peer, status, err = await self.condition_tester( + bt, two_nodes_mempool, wallet_a, dic, fee=10 + ) assert err == Err.RESERVE_FEE_CONDITION_FAILED assert status == MempoolInclusionStatus.FAILED blocks = bt.get_consecutive_blocks( @@ -1379,13 +1395,15 @@ async def test_assert_fee_condition_fee_too_large(self, bt, two_nodes, wallet_a) ) @pytest.mark.asyncio - async def test_assert_fee_condition_wrong_fee(self, bt, two_nodes, wallet_a): + async def test_assert_fee_condition_wrong_fee(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool cvp = ConditionWithArgs(ConditionOpcode.RESERVE_FEE, [int_to_bytes(10)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic, fee=9) + blocks, spend_bundle1, peer, status, err = await self.condition_tester( + bt, two_nodes_mempool, wallet_a, dic, fee=9 + ) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err == Err.RESERVE_FEE_CONDITION_FAILED @@ -1393,9 +1411,9 @@ async def test_assert_fee_condition_wrong_fee(self, bt, two_nodes, wallet_a): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_stealing_fee(self, bt, two_nodes, wallet_a): + async def test_stealing_fee(self, bt, two_nodes_mempool, wallet_a): reward_ph = wallet_a.get_new_puzzlehash() - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool blocks = await full_node_1.get_all_full_blocks() start_height = blocks[-1].height blocks = bt.get_consecutive_blocks( @@ -1406,7 +1424,7 @@ async def test_stealing_fee(self, bt, two_nodes, wallet_a): pool_reward_puzzle_hash=reward_ph, ) - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool peer = await connect_and_get_peer(server_1, server_2, bt.config["self_hostname"]) for block in blocks: @@ -1450,9 +1468,9 @@ async def test_stealing_fee(self, bt, two_nodes, wallet_a): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_double_spend_same_bundle(self, bt, two_nodes, wallet_a): + async def test_double_spend_same_bundle(self, bt, two_nodes_mempool, wallet_a): reward_ph = wallet_a.get_new_puzzlehash() - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool blocks = await full_node_1.get_all_full_blocks() start_height = blocks[-1].height blocks = bt.get_consecutive_blocks( @@ -1496,9 +1514,9 @@ async def test_double_spend_same_bundle(self, bt, two_nodes, wallet_a): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_agg_sig_condition(self, bt, two_nodes, wallet_a): + async def test_agg_sig_condition(self, bt, two_nodes_mempool, wallet_a): reward_ph = wallet_a.get_new_puzzlehash() - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool blocks = await full_node_1.get_all_full_blocks() start_height = blocks[-1].height blocks = bt.get_consecutive_blocks( @@ -1544,13 +1562,15 @@ async def test_agg_sig_condition(self, bt, two_nodes, wallet_a): # assert sb is spend_bundle @pytest.mark.asyncio - async def test_correct_my_parent(self, bt, two_nodes, wallet_a): + async def test_correct_my_parent(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool coin = await next_block(full_node_1, wallet_a, bt) cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_PARENT_ID, [coin.parent_coin_info]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic, coin=coin) + blocks, spend_bundle1, peer, status, err = await self.condition_tester( + bt, two_nodes_mempool, wallet_a, dic, coin=coin + ) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1559,14 +1579,16 @@ async def test_correct_my_parent(self, bt, two_nodes, wallet_a): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_my_parent_garbage(self, bt, two_nodes, wallet_a): + async def test_my_parent_garbage(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool coin = await next_block(full_node_1, wallet_a, bt) # garbage at the end of the arguments list is allowed but stripped cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_PARENT_ID, [coin.parent_coin_info, b"garbage"]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic, coin=coin) + blocks, spend_bundle1, peer, status, err = await self.condition_tester( + bt, two_nodes_mempool, wallet_a, dic, coin=coin + ) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1575,13 +1597,13 @@ async def test_my_parent_garbage(self, bt, two_nodes, wallet_a): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_my_parent_missing_arg(self, bt, two_nodes, wallet_a): + async def test_my_parent_missing_arg(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool blocks = await full_node_1.get_all_full_blocks() cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_PARENT_ID, []) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1590,14 +1612,16 @@ async def test_my_parent_missing_arg(self, bt, two_nodes, wallet_a): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_invalid_my_parent(self, bt, two_nodes, wallet_a): + async def test_invalid_my_parent(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool coin = await next_block(full_node_1, wallet_a, bt) coin_2 = await next_block(full_node_1, wallet_a, bt) cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_PARENT_ID, [coin_2.parent_coin_info]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic, coin=coin) + blocks, spend_bundle1, peer, status, err = await self.condition_tester( + bt, two_nodes_mempool, wallet_a, dic, coin=coin + ) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1606,13 +1630,15 @@ async def test_invalid_my_parent(self, bt, two_nodes, wallet_a): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_correct_my_puzhash(self, bt, two_nodes, wallet_a): + async def test_correct_my_puzhash(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool coin = await next_block(full_node_1, wallet_a, bt) cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_PUZZLEHASH, [coin.puzzle_hash]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic, coin=coin) + blocks, spend_bundle1, peer, status, err = await self.condition_tester( + bt, two_nodes_mempool, wallet_a, dic, coin=coin + ) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1621,14 +1647,16 @@ async def test_correct_my_puzhash(self, bt, two_nodes, wallet_a): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_my_puzhash_garbage(self, bt, two_nodes, wallet_a): + async def test_my_puzhash_garbage(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool coin = await next_block(full_node_1, wallet_a, bt) # garbage at the end of the arguments list is allowed but stripped cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_PUZZLEHASH, [coin.puzzle_hash, b"garbage"]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic, coin=coin) + blocks, spend_bundle1, peer, status, err = await self.condition_tester( + bt, two_nodes_mempool, wallet_a, dic, coin=coin + ) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1637,13 +1665,13 @@ async def test_my_puzhash_garbage(self, bt, two_nodes, wallet_a): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_my_puzhash_missing_arg(self, bt, two_nodes, wallet_a): + async def test_my_puzhash_missing_arg(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool blocks = await full_node_1.get_all_full_blocks() cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_PUZZLEHASH, []) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1652,13 +1680,15 @@ async def test_my_puzhash_missing_arg(self, bt, two_nodes, wallet_a): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_invalid_my_puzhash(self, bt, two_nodes, wallet_a): + async def test_invalid_my_puzhash(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool coin = await next_block(full_node_1, wallet_a, bt) cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_PUZZLEHASH, [Program.to([]).get_tree_hash()]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic, coin=coin) + blocks, spend_bundle1, peer, status, err = await self.condition_tester( + bt, two_nodes_mempool, wallet_a, dic, coin=coin + ) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1667,13 +1697,15 @@ async def test_invalid_my_puzhash(self, bt, two_nodes, wallet_a): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_correct_my_amount(self, bt, two_nodes, wallet_a): + async def test_correct_my_amount(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool coin = await next_block(full_node_1, wallet_a, bt) cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_AMOUNT, [int_to_bytes(coin.amount)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic, coin=coin) + blocks, spend_bundle1, peer, status, err = await self.condition_tester( + bt, two_nodes_mempool, wallet_a, dic, coin=coin + ) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1682,14 +1714,16 @@ async def test_correct_my_amount(self, bt, two_nodes, wallet_a): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_my_amount_garbage(self, bt, two_nodes, wallet_a): + async def test_my_amount_garbage(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool coin = await next_block(full_node_1, wallet_a, bt) # garbage at the end of the arguments list is allowed but stripped cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_AMOUNT, [int_to_bytes(coin.amount), b"garbage"]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic, coin=coin) + blocks, spend_bundle1, peer, status, err = await self.condition_tester( + bt, two_nodes_mempool, wallet_a, dic, coin=coin + ) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1698,13 +1732,13 @@ async def test_my_amount_garbage(self, bt, two_nodes, wallet_a): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_my_amount_missing_arg(self, bt, two_nodes, wallet_a): + async def test_my_amount_missing_arg(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool blocks = await full_node_1.get_all_full_blocks() cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_AMOUNT, []) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1713,13 +1747,13 @@ async def test_my_amount_missing_arg(self, bt, two_nodes, wallet_a): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_invalid_my_amount(self, bt, two_nodes, wallet_a): + async def test_invalid_my_amount(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool blocks = await full_node_1.get_all_full_blocks() cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_AMOUNT, [int_to_bytes(1000)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1728,13 +1762,13 @@ async def test_invalid_my_amount(self, bt, two_nodes, wallet_a): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_negative_my_amount(self, bt, two_nodes, wallet_a): + async def test_negative_my_amount(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool blocks = await full_node_1.get_all_full_blocks() cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_AMOUNT, [int_to_bytes(-1)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1743,13 +1777,13 @@ async def test_negative_my_amount(self, bt, two_nodes, wallet_a): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_my_amount_too_large(self, bt, two_nodes, wallet_a): + async def test_my_amount_too_large(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool blocks = await full_node_1.get_all_full_blocks() cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_AMOUNT, [int_to_bytes(2 ** 64)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -2396,7 +2430,7 @@ def test_many_create_coin(self, softfork_height): assert run_time < 0.2 @pytest.mark.asyncio - async def test_invalid_coin_spend_coin(self, bt, two_nodes, wallet_a): + async def test_invalid_coin_spend_coin(self, bt, two_nodes_mempool, wallet_a): reward_ph = wallet_a.get_new_puzzlehash() blocks = bt.get_consecutive_blocks( 5, @@ -2404,7 +2438,7 @@ async def test_invalid_coin_spend_coin(self, bt, two_nodes, wallet_a): farmer_reward_puzzle_hash=reward_ph, pool_reward_puzzle_hash=reward_ph, ) - full_node_1, full_node_2, server_1, server_2 = two_nodes + full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool for block in blocks: await full_node_1.full_node.respond_block(full_node_protocol.RespondBlock(block)) diff --git a/tests/core/full_node/test_mempool_performance.py b/tests/core/full_node/test_mempool_performance.py index 6cc53b239210..a8a7766affbb 100644 --- a/tests/core/full_node/test_mempool_performance.py +++ b/tests/core/full_node/test_mempool_performance.py @@ -36,7 +36,7 @@ async def wallet_balance_at_least(wallet_node: WalletNode, balance): @pytest_asyncio.fixture(scope="module") -async def wallet_nodes(bt): +async def wallet_nodes_mempool_perf(bt): key_seed = bt.farmer_master_sk_entropy async for _ in setup_simulators_and_wallets(2, 1, {}, key_seed=key_seed): yield _ @@ -45,9 +45,9 @@ async def wallet_nodes(bt): class TestMempoolPerformance: @pytest.mark.asyncio @pytest.mark.benchmark - async def test_mempool_update_performance(self, bt, wallet_nodes, default_400_blocks, self_hostname): + async def test_mempool_update_performance(self, bt, wallet_nodes_mempool_perf, default_400_blocks, self_hostname): blocks = default_400_blocks - full_nodes, wallets = wallet_nodes + full_nodes, wallets = wallet_nodes_mempool_perf wallet_node = wallets[0][0] wallet_server = wallets[0][1] full_node_api_1 = full_nodes[0] diff --git a/tests/core/full_node/test_performance.py b/tests/core/full_node/test_performance.py index be8d1a9905f0..4a1ce2260c0e 100644 --- a/tests/core/full_node/test_performance.py +++ b/tests/core/full_node/test_performance.py @@ -40,7 +40,7 @@ async def get_block_path(full_node: FullNodeAPI): @pytest_asyncio.fixture(scope="module") -async def wallet_nodes(bt): +async def wallet_nodes_perf(bt): async_gen = setup_simulators_and_wallets(1, 1, {"MEMPOOL_BLOCK_BUFFER": 1, "MAX_BLOCK_COST_CLVM": 11000000000}) nodes, wallets = await async_gen.__anext__() full_node_1 = nodes[0] @@ -56,8 +56,8 @@ async def wallet_nodes(bt): class TestPerformance: @pytest.mark.asyncio @pytest.mark.benchmark - async def test_full_block_performance(self, bt, wallet_nodes, self_hostname): - full_node_1, server_1, wallet_a, wallet_receiver = wallet_nodes + async def test_full_block_performance(self, bt, wallet_nodes_perf, self_hostname): + full_node_1, server_1, wallet_a, wallet_receiver = wallet_nodes_perf blocks = await full_node_1.get_all_full_blocks() full_node_1.full_node.mempool_manager.limit_factor = 1 diff --git a/tests/core/full_node/test_transactions.py b/tests/core/full_node/test_transactions.py index 97c8dfa43ecb..e00dacd22a09 100644 --- a/tests/core/full_node/test_transactions.py +++ b/tests/core/full_node/test_transactions.py @@ -17,7 +17,7 @@ @pytest_asyncio.fixture(scope="function") -async def wallet_node(): +async def wallet_node_sim_and_wallet(): async for _ in setup_simulators_and_wallets(1, 1, {}): yield _ @@ -36,9 +36,9 @@ async def three_nodes_two_wallets(): class TestTransactions: @pytest.mark.asyncio - async def test_wallet_coinbase(self, wallet_node, self_hostname): + async def test_wallet_coinbase(self, wallet_node_sim_and_wallet, self_hostname): num_blocks = 5 - full_nodes, wallets = wallet_node + full_nodes, wallets = wallet_node_sim_and_wallet full_node_api = full_nodes[0] full_node_server = full_node_api.server wallet_node, server_2 = wallets[0] diff --git a/tests/core/server/test_dos.py b/tests/core/server/test_dos.py index 72ddccccd825..9985e2999317 100644 --- a/tests/core/server/test_dos.py +++ b/tests/core/server/test_dos.py @@ -34,7 +34,7 @@ async def get_block_path(full_node: FullNodeAPI): @pytest_asyncio.fixture(scope="function") -async def setup_two_nodes(db_version): +async def setup_two_nodes_fixture(db_version): async for _ in setup_simulators_and_wallets(2, 0, {}, db_version=db_version): yield _ @@ -46,8 +46,8 @@ def process_msg_and_check(self, msg): class TestDos: @pytest.mark.asyncio - async def test_large_message_disconnect_and_ban(self, setup_two_nodes, self_hostname): - nodes, _ = setup_two_nodes + async def test_large_message_disconnect_and_ban(self, setup_two_nodes_fixture, self_hostname): + nodes, _ = setup_two_nodes_fixture server_1 = nodes[0].full_node.server server_2 = nodes[1].full_node.server @@ -94,8 +94,8 @@ async def test_large_message_disconnect_and_ban(self, setup_two_nodes, self_host await session.close() @pytest.mark.asyncio - async def test_bad_handshake_and_ban(self, setup_two_nodes, self_hostname): - nodes, _ = setup_two_nodes + async def test_bad_handshake_and_ban(self, setup_two_nodes_fixture, self_hostname): + nodes, _ = setup_two_nodes_fixture server_1 = nodes[0].full_node.server server_2 = nodes[1].full_node.server @@ -140,8 +140,8 @@ async def test_bad_handshake_and_ban(self, setup_two_nodes, self_hostname): await session.close() @pytest.mark.asyncio - async def test_invalid_protocol_handshake(self, setup_two_nodes, self_hostname): - nodes, _ = setup_two_nodes + async def test_invalid_protocol_handshake(self, setup_two_nodes_fixture, self_hostname): + nodes, _ = setup_two_nodes_fixture server_1 = nodes[0].full_node.server server_2 = nodes[1].full_node.server @@ -173,8 +173,8 @@ async def test_invalid_protocol_handshake(self, setup_two_nodes, self_hostname): await asyncio.sleep(1) # give some time for cleanup to work @pytest.mark.asyncio - async def test_spam_tx(self, setup_two_nodes, self_hostname): - nodes, _ = setup_two_nodes + async def test_spam_tx(self, setup_two_nodes_fixture, self_hostname): + nodes, _ = setup_two_nodes_fixture full_node_1, full_node_2 = nodes server_1 = nodes[0].full_node.server server_2 = nodes[1].full_node.server @@ -226,8 +226,8 @@ def is_banned(): await time_out_assert(15, is_banned) @pytest.mark.asyncio - async def test_spam_message_non_tx(self, setup_two_nodes, self_hostname): - nodes, _ = setup_two_nodes + async def test_spam_message_non_tx(self, setup_two_nodes_fixture, self_hostname): + nodes, _ = setup_two_nodes_fixture full_node_1, full_node_2 = nodes server_1 = nodes[0].full_node.server server_2 = nodes[1].full_node.server @@ -275,8 +275,8 @@ def is_banned(): await time_out_assert(15, is_banned) @pytest.mark.asyncio - async def test_spam_message_too_large(self, setup_two_nodes, self_hostname): - nodes, _ = setup_two_nodes + async def test_spam_message_too_large(self, setup_two_nodes_fixture, self_hostname): + nodes, _ = setup_two_nodes_fixture full_node_1, full_node_2 = nodes server_1 = nodes[0].full_node.server server_2 = nodes[1].full_node.server diff --git a/tests/core/ssl/test_ssl.py b/tests/core/ssl/test_ssl.py index 246bce7b4885..798df4c64c2f 100644 --- a/tests/core/ssl/test_ssl.py +++ b/tests/core/ssl/test_ssl.py @@ -58,7 +58,7 @@ async def harvester_farmer(bt): @pytest_asyncio.fixture(scope="function") -async def wallet_node(): +async def wallet_node_sim_and_wallet(): async for _ in setup_simulators_and_wallets(1, 1, {}): yield _ @@ -82,8 +82,8 @@ async def timelord(bt): class TestSSL: @pytest.mark.asyncio - async def test_public_connections(self, wallet_node, self_hostname): - full_nodes, wallets = wallet_node + async def test_public_connections(self, wallet_node_sim_and_wallet, self_hostname): + full_nodes, wallets = wallet_node_sim_and_wallet full_node_api = full_nodes[0] server_1: ChiaServer = full_node_api.full_node.server wallet_node, server_2 = wallets[0] @@ -130,8 +130,8 @@ async def test_farmer(self, harvester_farmer, self_hostname): assert connected is False @pytest.mark.asyncio - async def test_full_node(self, wallet_node, self_hostname): - full_nodes, wallets = wallet_node + async def test_full_node(self, wallet_node_sim_and_wallet, self_hostname): + full_nodes, wallets = wallet_node_sim_and_wallet full_node_api = full_nodes[0] full_node_server = full_node_api.full_node.server @@ -151,8 +151,8 @@ async def test_full_node(self, wallet_node, self_hostname): assert connected is True @pytest.mark.asyncio - async def test_wallet(self, wallet_node, self_hostname): - full_nodes, wallets = wallet_node + async def test_wallet(self, wallet_node_sim_and_wallet, self_hostname): + full_nodes, wallets = wallet_node_sim_and_wallet wallet_node, wallet_server = wallets[0] # Wallet should not accept incoming connections diff --git a/tests/core/test_full_node_rpc.py b/tests/core/test_full_node_rpc.py index 704499e4df68..ee6a58c30ecd 100644 --- a/tests/core/test_full_node_rpc.py +++ b/tests/core/test_full_node_rpc.py @@ -29,17 +29,17 @@ @pytest_asyncio.fixture(scope="function") -async def two_nodes(): +async def two_nodes_sim_and_wallets(): async for _ in setup_simulators_and_wallets(2, 0, {}): yield _ class TestRpc: @pytest.mark.asyncio - async def test1(self, two_nodes, bt, self_hostname): + async def test1(self, two_nodes_sim_and_wallets, bt, self_hostname): num_blocks = 5 test_rpc_port = find_available_listen_port() - nodes, _ = two_nodes + nodes, _ = two_nodes_sim_and_wallets full_node_api_1, full_node_api_2 = nodes server_1 = full_node_api_1.full_node.server server_2 = full_node_api_2.full_node.server @@ -272,9 +272,9 @@ async def num_connections(): await rpc_cleanup() @pytest.mark.asyncio - async def test_signage_points(self, two_nodes, empty_blockchain, bt): + async def test_signage_points(self, two_nodes_sim_and_wallets, empty_blockchain, bt): test_rpc_port = find_available_listen_port() - nodes, _ = two_nodes + nodes, _ = two_nodes_sim_and_wallets full_node_api_1, full_node_api_2 = nodes server_1 = full_node_api_1.full_node.server server_2 = full_node_api_2.full_node.server diff --git a/tests/plotting/test_plot_manager.py b/tests/plotting/test_plot_manager.py index 686c14651c15..f2a0e843c255 100644 --- a/tests/plotting/test_plot_manager.py +++ b/tests/plotting/test_plot_manager.py @@ -138,7 +138,7 @@ class TestEnvironment: @pytest.fixture(scope="function") -def test_environment(tmp_path, bt) -> Iterator[TestEnvironment]: +def test_plot_environment(tmp_path, bt) -> Iterator[TestEnvironment]: dir_1_count: int = 7 dir_2_count: int = 3 plots: List[Path] = get_test_plots() @@ -162,8 +162,8 @@ def trigger_remove_plot(_: Path, plot_path: str): @pytest.mark.asyncio -async def test_plot_refreshing(test_environment): - env: TestEnvironment = test_environment +async def test_plot_refreshing(test_plot_environment): + env: TestEnvironment = test_plot_environment expected_result = PlotRefreshResult() dir_duplicates: TestDirectory = TestDirectory(get_plot_dir().resolve() / "duplicates", env.dir_1.plots) @@ -358,8 +358,8 @@ async def run_test_case( @pytest.mark.asyncio -async def test_invalid_plots(test_environment): - env: TestEnvironment = test_environment +async def test_invalid_plots(test_plot_environment): + env: TestEnvironment = test_plot_environment expected_result = PlotRefreshResult() # Test re-trying if processing a plot failed # First create a backup of the plot @@ -409,8 +409,8 @@ async def test_invalid_plots(test_environment): @pytest.mark.asyncio -async def test_keys_missing(test_environment: TestEnvironment) -> None: - env: TestEnvironment = test_environment +async def test_keys_missing(test_plot_environment: TestEnvironment) -> None: + env: TestEnvironment = test_plot_environment not_in_keychain_plots: List[Path] = get_test_plots("not_in_keychain") dir_not_in_keychain: TestDirectory = TestDirectory( env.root_path / "plots" / "not_in_keychain", not_in_keychain_plots @@ -447,8 +447,8 @@ async def test_keys_missing(test_environment: TestEnvironment) -> None: @pytest.mark.asyncio -async def test_plot_info_caching(test_environment, bt): - env: TestEnvironment = test_environment +async def test_plot_info_caching(test_plot_environment, bt): + env: TestEnvironment = test_plot_environment expected_result = PlotRefreshResult() add_plot_directory(env.root_path, str(env.dir_1.path)) expected_result.loaded = env.dir_1.plot_info_list() @@ -513,7 +513,7 @@ async def test_plot_info_caching(test_environment, bt): ], ) @pytest.mark.asyncio -async def test_callback_event_raises(test_environment, event_to_raise: PlotRefreshEvents): +async def test_callback_event_raises(test_plot_environment, event_to_raise: PlotRefreshEvents): last_event_fired: Optional[PlotRefreshEvents] = None def raising_callback(event: PlotRefreshEvents, _: PlotRefreshResult): @@ -522,7 +522,7 @@ def raising_callback(event: PlotRefreshEvents, _: PlotRefreshResult): if event == event_to_raise: raise Exception(f"run_raise_in_callback {event_to_raise}") - env: TestEnvironment = test_environment + env: TestEnvironment = test_plot_environment expected_result = PlotRefreshResult() # Load dir_1 add_plot_directory(env.root_path, str(env.dir_1.path)) diff --git a/tests/wallet/cat_wallet/test_cat_wallet.py b/tests/wallet/cat_wallet/test_cat_wallet.py index 74179a2ceff1..808a76c5291a 100644 --- a/tests/wallet/cat_wallet/test_cat_wallet.py +++ b/tests/wallet/cat_wallet/test_cat_wallet.py @@ -31,7 +31,7 @@ async def tx_in_pool(mempool: MempoolManager, tx_id: bytes32): @pytest_asyncio.fixture(scope="function") -async def wallet_node(): +async def wallet_node_sim_and_wallet(): async for _ in setup_simulators_and_wallets(1, 1, {}): yield _ diff --git a/tests/wallet/did_wallet/test_did.py b/tests/wallet/did_wallet/test_did.py index 0ba514fd3e7f..11b9bdd80055 100644 --- a/tests/wallet/did_wallet/test_did.py +++ b/tests/wallet/did_wallet/test_did.py @@ -14,12 +14,6 @@ pytestmark = pytest.mark.skip("TODO: Fix tests") -@pytest_asyncio.fixture(scope="function") -async def wallet_node(): - async for _ in setup_simulators_and_wallets(1, 1, {}): - yield _ - - @pytest_asyncio.fixture(scope="function") async def two_wallet_nodes(): async for _ in setup_simulators_and_wallets(1, 2, {}): diff --git a/tests/wallet/test_wallet.py b/tests/wallet/test_wallet.py index 50bbb09b9124..bd75233fcbb4 100644 --- a/tests/wallet/test_wallet.py +++ b/tests/wallet/test_wallet.py @@ -27,6 +27,12 @@ async def wallet_node(): yield _ +@pytest_asyncio.fixture(scope="function") +async def wallet_node_sim_and_wallet(): + async for _ in setup_simulators_and_wallets(1, 1, {}): + yield _ + + @pytest_asyncio.fixture(scope="function") async def wallet_node_100_pk(): async for _ in setup_simulators_and_wallets(1, 1, {}, initial_num_public_keys=100): @@ -57,9 +63,9 @@ class TestWalletSimulator: [True, False], ) @pytest.mark.asyncio - async def test_wallet_coinbase(self, wallet_node, trusted, self_hostname): + async def test_wallet_coinbase(self, wallet_node_sim_and_wallet, trusted, self_hostname): num_blocks = 10 - full_nodes, wallets = wallet_node + full_nodes, wallets = wallet_node_sim_and_wallet full_node_api = full_nodes[0] server_1: ChiaServer = full_node_api.full_node.server wallet_node, server_2 = wallets[0] @@ -170,9 +176,9 @@ async def test_wallet_make_transaction(self, two_wallet_nodes, trusted, self_hos [True, False], ) @pytest.mark.asyncio - async def test_wallet_coinbase_reorg(self, wallet_node, trusted, self_hostname): + async def test_wallet_coinbase_reorg(self, wallet_node_sim_and_wallet, trusted, self_hostname): num_blocks = 5 - full_nodes, wallets = wallet_node + full_nodes, wallets = wallet_node_sim_and_wallet full_node_api = full_nodes[0] fn_server = full_node_api.full_node.server wallet_node, server_2 = wallets[0] From 171abb03ee37144654012ac2935dfafed374e0d3 Mon Sep 17 00:00:00 2001 From: wjblanke Date: Tue, 22 Mar 2022 11:35:35 -0700 Subject: [PATCH 243/378] atomic rollback for wallet (#10799) * atomic rollback for wallet --- chia/wallet/util/peer_request_cache.py | 2 +- chia/wallet/wallet_node.py | 53 +++++++++++++++++------ chia/wallet/wallet_state_manager.py | 58 ++++++++++---------------- 3 files changed, 63 insertions(+), 50 deletions(-) diff --git a/chia/wallet/util/peer_request_cache.py b/chia/wallet/util/peer_request_cache.py index 4efc0c02db13..73973341298f 100644 --- a/chia/wallet/util/peer_request_cache.py +++ b/chia/wallet/util/peer_request_cache.py @@ -80,7 +80,7 @@ def clear_after_height(self, height: int): new_states_validated = LRUCache(self._states_validated.capacity) for k, cs_height in self._states_validated.cache.items(): - if cs_height is not None: + if cs_height is not None and cs_height <= height: new_states_validated.put(k, cs_height) self._states_validated = new_states_validated diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index 720ba71b0740..0ad91aa38398 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -467,6 +467,28 @@ async def on_connect(self, peer: WSChiaConnection): if self.wallet_peers is not None: await self.wallet_peers.on_connect(peer) + async def perform_atomic_rollback(self, fork_height: int, cache: Optional[PeerRequestCache] = None): + assert self.wallet_state_manager is not None + self.log.info(f"perform_atomic_rollback to {fork_height}") + async with self.wallet_state_manager.db_wrapper.lock: + try: + await self.wallet_state_manager.db_wrapper.begin_transaction() + await self.wallet_state_manager.reorg_rollback(fork_height) + await self.wallet_state_manager.blockchain.set_finished_sync_up_to(fork_height) + if cache is None: + self.rollback_request_caches(fork_height) + else: + cache.clear_after_height(fork_height) + await self.wallet_state_manager.db_wrapper.commit_transaction() + except Exception as e: + tb = traceback.format_exc() + self.log.error(f"Exception while perform_atomic_rollback: {e} {tb}") + await self.wallet_state_manager.db_wrapper.rollback_transaction() + await self.wallet_state_manager.coin_store.rebuild_wallet_cache() + await self.wallet_state_manager.tx_store.rebuild_tx_cache() + await self.wallet_state_manager.pool_store.rebuild_cache() + raise + async def long_sync( self, target_height: uint32, @@ -500,8 +522,8 @@ def is_new_state_update(cs: CoinState) -> bool: start_time = time.time() if rollback: - await self.wallet_state_manager.reorg_rollback(fork_height) - self.rollback_request_caches(fork_height) + # we should clear all peers since this is a full rollback + await self.perform_atomic_rollback(fork_height) await self.update_ui() # We only process new state updates to avoid slow reprocessing. We set the sync height after adding @@ -590,14 +612,21 @@ async def receive_state_from_peer( if self.validation_semaphore is None: self.validation_semaphore = asyncio.Semaphore(6) + # Rollback is handled in wallet_short_sync_backtrack for untrusted peers, so we don't need to do it here. + # Also it's not safe to rollback, an untrusted peer can give us old fork point and make our TX dissapear. + # wallet_short_sync_backtrack can safely rollback because we validated the weight for the new peak so we + # know the peer is telling the truth about the reorg. + # If there is a fork, we need to ensure that we roll back in trusted mode to properly handle reorgs - if trusted and fork_height is not None and height is not None and fork_height != height - 1: - await self.wallet_state_manager.reorg_rollback(fork_height) - await self.wallet_state_manager.blockchain.set_finished_sync_up_to(fork_height) cache: PeerRequestCache = self.get_cache_for_peer(peer) - if fork_height is not None: - cache.clear_after_height(fork_height) - self.log.info(f"Rolling back to {fork_height}") + if trusted and fork_height is not None and height is not None and fork_height != height - 1: + # only one peer told us to rollback so only clear for that peer + await self.perform_atomic_rollback(fork_height, cache=cache) + else: + if fork_height is not None: + # only one peer told us to rollback so only clear for that peer + cache.clear_after_height(fork_height) + self.log.info(f"clear_after_height {fork_height} for peer {peer}") all_tasks: List[asyncio.Task] = [] target_concurrent_tasks: int = 20 @@ -630,7 +659,6 @@ async def receive_and_validate(inner_states: List[CoinState], inner_idx_start: i if self.wallet_state_manager is None: return try: - await self.wallet_state_manager.db_wrapper.commit_transaction() await self.wallet_state_manager.db_wrapper.begin_transaction() await self.wallet_state_manager.new_coin_state(valid_states, peer, fork_height) @@ -674,13 +702,12 @@ async def receive_and_validate(inner_states: List[CoinState], inner_idx_start: i async with self.wallet_state_manager.db_wrapper.lock: try: self.log.info(f"new coin state received ({idx}-" f"{idx + len(states) - 1}/ {len(items)})") - await self.wallet_state_manager.db_wrapper.commit_transaction() await self.wallet_state_manager.db_wrapper.begin_transaction() await self.wallet_state_manager.new_coin_state(states, peer, fork_height) - await self.wallet_state_manager.db_wrapper.commit_transaction() await self.wallet_state_manager.blockchain.set_finished_sync_up_to( last_change_height_cs(states[-1]) - 1, in_transaction=True ) + await self.wallet_state_manager.db_wrapper.commit_transaction() except Exception as e: await self.wallet_state_manager.db_wrapper.rollback_transaction() await self.wallet_state_manager.coin_store.rebuild_wallet_cache() @@ -1030,9 +1057,9 @@ async def wallet_short_sync_backtrack(self, header_block: HeaderBlock, peer: WSC peak_height = self.wallet_state_manager.blockchain.get_peak_height() if fork_height < peak_height: self.log.info(f"Rolling back to {fork_height}") - await self.wallet_state_manager.reorg_rollback(fork_height) + # we should clear all peers since this is a full rollback + await self.perform_atomic_rollback(fork_height) await self.update_ui() - self.rollback_request_caches(fork_height) if peak is not None: assert header_block.weight >= peak.weight diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 96874ac0fe25..aed351a13be3 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -4,7 +4,6 @@ import multiprocessing import multiprocessing.context import time -import traceback from collections import defaultdict from pathlib import Path from secrets import token_bytes @@ -1103,41 +1102,28 @@ async def reorg_rollback(self, height: int): Rolls back and updates the coin_store and transaction store. It's possible this height is the tip, or even beyond the tip. """ - try: - await self.db_wrapper.commit_transaction() - await self.db_wrapper.begin_transaction() - - await self.coin_store.rollback_to_block(height) - reorged: List[TransactionRecord] = await self.tx_store.get_transaction_above(height) - await self.tx_store.rollback_to_block(height) - for record in reorged: - if record.type in [ - TransactionType.OUTGOING_TX, - TransactionType.OUTGOING_TRADE, - TransactionType.INCOMING_TRADE, - ]: - await self.tx_store.tx_reorged(record, in_transaction=True) - self.tx_pending_changed() - - # Removes wallets that were created from a blockchain transaction which got reorged. - remove_ids = [] - for wallet_id, wallet in self.wallets.items(): - if wallet.type() == WalletType.POOLING_WALLET.value: - remove: bool = await wallet.rewind(height, in_transaction=True) - if remove: - remove_ids.append(wallet_id) - for wallet_id in remove_ids: - await self.user_store.delete_wallet(wallet_id, in_transaction=True) - self.wallets.pop(wallet_id) - await self.db_wrapper.commit_transaction() - except Exception as e: - tb = traceback.format_exc() - self.log.error(f"Exception while rolling back: {e} {tb}") - await self.db_wrapper.rollback_transaction() - await self.coin_store.rebuild_wallet_cache() - await self.tx_store.rebuild_tx_cache() - await self.pool_store.rebuild_cache() - raise + await self.coin_store.rollback_to_block(height) + reorged: List[TransactionRecord] = await self.tx_store.get_transaction_above(height) + await self.tx_store.rollback_to_block(height) + for record in reorged: + if record.type in [ + TransactionType.OUTGOING_TX, + TransactionType.OUTGOING_TRADE, + TransactionType.INCOMING_TRADE, + ]: + await self.tx_store.tx_reorged(record, in_transaction=True) + self.tx_pending_changed() + + # Removes wallets that were created from a blockchain transaction which got reorged. + remove_ids = [] + for wallet_id, wallet in self.wallets.items(): + if wallet.type() == WalletType.POOLING_WALLET.value: + remove: bool = await wallet.rewind(height, in_transaction=True) + if remove: + remove_ids.append(wallet_id) + for wallet_id in remove_ids: + await self.user_store.delete_wallet(wallet_id, in_transaction=True) + self.wallets.pop(wallet_id) async def _await_closed(self) -> None: await self.db_connection.close() From b7a1c9ccb8076f9a2418029d6324b4e0c70a4946 Mon Sep 17 00:00:00 2001 From: Earle Lowe <30607889+emlowe@users.noreply.github.com> Date: Tue, 22 Mar 2022 11:36:28 -0700 Subject: [PATCH 244/378] Handle cases where one node doesn't have the coin we are looking for (#10829) * Continue if one node doesn't have the coin * Pass in coin_state list * Pass in the single coinstate instead of list * more simplifications --- chia/wallet/cat_wallet/cat_wallet.py | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/chia/wallet/cat_wallet/cat_wallet.py b/chia/wallet/cat_wallet/cat_wallet.py index 6d9f78b6bf7e..244849200f22 100644 --- a/chia/wallet/cat_wallet/cat_wallet.py +++ b/chia/wallet/cat_wallet/cat_wallet.py @@ -11,7 +11,6 @@ from chia.consensus.cost_calculator import NPCResult from chia.full_node.bundle_tools import simple_solution_generator from chia.full_node.mempool_check_conditions import get_name_puzzle_conditions -from chia.protocols.wallet_protocol import CoinState from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.program import Program from chia.types.blockchain_format.sized_bytes import bytes32 @@ -328,34 +327,26 @@ async def coin_added(self, coin: Coin, height: uint32): coin_state = await self.wallet_state_manager.wallet_node.get_coin_state( [coin.parent_coin_info], None, node ) + # check for empty list and continue on to next node + if not coin_state: + continue assert coin_state[0].coin.name() == coin.parent_coin_info coin_spend = await self.wallet_state_manager.wallet_node.fetch_puzzle_solution( node, coin_state[0].spent_height, coin_state[0].coin ) - await self.puzzle_solution_received(coin_spend) + await self.puzzle_solution_received(coin_spend, parent_coin=coin_state[0].coin) break except Exception as e: self.log.debug(f"Exception: {e}, traceback: {traceback.format_exc()}") - async def puzzle_solution_received(self, coin_spend: CoinSpend): + async def puzzle_solution_received(self, coin_spend: CoinSpend, parent_coin: Coin): coin_name = coin_spend.coin.name() puzzle: Program = Program.from_bytes(bytes(coin_spend.puzzle_reveal)) matched, curried_args = match_cat_puzzle(puzzle) if matched: mod_hash, genesis_coin_checker_hash, inner_puzzle = curried_args self.log.info(f"parent: {coin_name} inner_puzzle for parent is {inner_puzzle}") - parent_coin = None - coin_record = await self.wallet_state_manager.coin_store.get_coin_record(coin_name) - if coin_record is None: - coin_states: Optional[List[CoinState]] = await self.wallet_state_manager.wallet_node.get_coin_state( - [coin_name] - ) - if coin_states is not None: - parent_coin = coin_states[0].coin - if coin_record is not None: - parent_coin = coin_record.coin - if parent_coin is None: - raise ValueError("Error in finding parent") + await self.add_lineage( coin_name, LineageProof(parent_coin.parent_coin_info, inner_puzzle.get_tree_hash(), parent_coin.amount), From b5d51c38f8440a713904202f2d908ad20f18d4fb Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Tue, 22 Mar 2022 15:04:07 -0700 Subject: [PATCH 245/378] run tests in parallel in CI (#10499) --- .github/workflows/build-test-macos-blockchain.yml | 2 +- .github/workflows/build-test-macos-clvm.yml | 2 +- .github/workflows/build-test-macos-core-cmds.yml | 2 +- .github/workflows/build-test-macos-core-consensus.yml | 2 +- .github/workflows/build-test-macos-core-custom_types.yml | 2 +- .github/workflows/build-test-macos-core-daemon.yml | 2 +- .../workflows/build-test-macos-core-full_node-full_sync.yml | 2 +- .github/workflows/build-test-macos-core-full_node-stores.yml | 2 +- .github/workflows/build-test-macos-core-full_node.yml | 2 +- .github/workflows/build-test-macos-core-server.yml | 2 +- .github/workflows/build-test-macos-core-ssl.yml | 2 +- .github/workflows/build-test-macos-core-util.yml | 2 +- .github/workflows/build-test-macos-core.yml | 2 +- .github/workflows/build-test-macos-farmer_harvester.yml | 2 +- .github/workflows/build-test-macos-generator.yml | 2 +- .github/workflows/build-test-macos-plotting.yml | 2 +- .github/workflows/build-test-macos-pools.yml | 2 +- .github/workflows/build-test-macos-simulation.yml | 2 +- .github/workflows/build-test-macos-tools.yml | 2 +- .github/workflows/build-test-macos-util.yml | 2 +- .github/workflows/build-test-macos-wallet-cat_wallet.yml | 2 +- .github/workflows/build-test-macos-wallet-did_wallet.yml | 2 +- .github/workflows/build-test-macos-wallet-rl_wallet.yml | 2 +- .github/workflows/build-test-macos-wallet-rpc.yml | 2 +- .github/workflows/build-test-macos-wallet-simple_sync.yml | 2 +- .github/workflows/build-test-macos-wallet-sync.yml | 2 +- .github/workflows/build-test-macos-wallet.yml | 2 +- .github/workflows/build-test-macos-weight_proof.yml | 2 +- .github/workflows/build-test-ubuntu-blockchain.yml | 2 +- .github/workflows/build-test-ubuntu-clvm.yml | 2 +- .github/workflows/build-test-ubuntu-core-cmds.yml | 2 +- .github/workflows/build-test-ubuntu-core-consensus.yml | 2 +- .github/workflows/build-test-ubuntu-core-custom_types.yml | 2 +- .github/workflows/build-test-ubuntu-core-daemon.yml | 2 +- .../workflows/build-test-ubuntu-core-full_node-full_sync.yml | 2 +- .github/workflows/build-test-ubuntu-core-full_node-stores.yml | 2 +- .github/workflows/build-test-ubuntu-core-full_node.yml | 2 +- .github/workflows/build-test-ubuntu-core-server.yml | 2 +- .github/workflows/build-test-ubuntu-core-ssl.yml | 2 +- .github/workflows/build-test-ubuntu-core-util.yml | 2 +- .github/workflows/build-test-ubuntu-core.yml | 2 +- .github/workflows/build-test-ubuntu-farmer_harvester.yml | 2 +- .github/workflows/build-test-ubuntu-generator.yml | 2 +- .github/workflows/build-test-ubuntu-plotting.yml | 2 +- .github/workflows/build-test-ubuntu-pools.yml | 2 +- .github/workflows/build-test-ubuntu-simulation.yml | 2 +- .github/workflows/build-test-ubuntu-tools.yml | 2 +- .github/workflows/build-test-ubuntu-util.yml | 2 +- .github/workflows/build-test-ubuntu-wallet-cat_wallet.yml | 2 +- .github/workflows/build-test-ubuntu-wallet-did_wallet.yml | 2 +- .github/workflows/build-test-ubuntu-wallet-rl_wallet.yml | 2 +- .github/workflows/build-test-ubuntu-wallet-rpc.yml | 2 +- .github/workflows/build-test-ubuntu-wallet-simple_sync.yml | 2 +- .github/workflows/build-test-ubuntu-wallet-sync.yml | 2 +- .github/workflows/build-test-ubuntu-wallet.yml | 2 +- .github/workflows/build-test-ubuntu-weight_proof.yml | 2 +- tests/blockchain/config.py | 1 + tests/build-workflows.py | 3 +-- tests/core/cmds/config.py | 1 + tests/core/consensus/config.py | 1 + tests/core/custom_types/config.py | 1 + tests/core/full_node/config.py | 1 + tests/core/full_node/dos/config.py | 1 + tests/core/full_node/stores/config.py | 1 + tests/generator/config.py | 1 + tests/plotting/config.py | 2 ++ tests/pytest.ini | 2 +- tests/tools/config.py | 1 + tests/util/config.py | 1 + tests/wallet/config.py | 1 + tests/weight_proof/config.py | 1 + 71 files changed, 72 insertions(+), 59 deletions(-) create mode 100644 tests/core/cmds/config.py create mode 100644 tests/core/consensus/config.py create mode 100644 tests/core/custom_types/config.py create mode 100644 tests/generator/config.py create mode 100644 tests/plotting/config.py create mode 100644 tests/tools/config.py create mode 100644 tests/wallet/config.py create mode 100644 tests/weight_proof/config.py diff --git a/.github/workflows/build-test-macos-blockchain.yml b/.github/workflows/build-test-macos-blockchain.yml index a4bed0c5c345..eb5c49115643 100644 --- a/.github/workflows/build-test-macos-blockchain.yml +++ b/.github/workflows/build-test-macos-blockchain.yml @@ -89,7 +89,7 @@ jobs: - name: Test blockchain code with pytest run: | . ./activate - ./venv/bin/py.test tests/blockchain/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/blockchain/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-clvm.yml b/.github/workflows/build-test-macos-clvm.yml index e71ade151b30..4b50f9858988 100644 --- a/.github/workflows/build-test-macos-clvm.yml +++ b/.github/workflows/build-test-macos-clvm.yml @@ -76,7 +76,7 @@ jobs: - name: Test clvm code with pytest run: | . ./activate - ./venv/bin/py.test tests/clvm/test_*.py -s -v --durations 0 -n auto -m "not benchmark" + ./venv/bin/py.test tests/clvm/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-core-cmds.yml b/.github/workflows/build-test-macos-core-cmds.yml index a29a1dcceff5..1b39507bb3e7 100644 --- a/.github/workflows/build-test-macos-core-cmds.yml +++ b/.github/workflows/build-test-macos-core-cmds.yml @@ -89,7 +89,7 @@ jobs: - name: Test core-cmds code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/cmds/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/core/cmds/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-core-consensus.yml b/.github/workflows/build-test-macos-core-consensus.yml index 6c3ebbc27be2..de123a845a0f 100644 --- a/.github/workflows/build-test-macos-core-consensus.yml +++ b/.github/workflows/build-test-macos-core-consensus.yml @@ -89,7 +89,7 @@ jobs: - name: Test core-consensus code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/consensus/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/core/consensus/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-core-custom_types.yml b/.github/workflows/build-test-macos-core-custom_types.yml index 0222c2ce00d3..da1af33f771d 100644 --- a/.github/workflows/build-test-macos-core-custom_types.yml +++ b/.github/workflows/build-test-macos-core-custom_types.yml @@ -89,7 +89,7 @@ jobs: - name: Test core-custom_types code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/custom_types/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/core/custom_types/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-core-daemon.yml b/.github/workflows/build-test-macos-core-daemon.yml index 84fbfb637598..bb949be6bde8 100644 --- a/.github/workflows/build-test-macos-core-daemon.yml +++ b/.github/workflows/build-test-macos-core-daemon.yml @@ -101,7 +101,7 @@ jobs: - name: Test core-daemon code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/daemon/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/core/daemon/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-core-full_node-full_sync.yml b/.github/workflows/build-test-macos-core-full_node-full_sync.yml index a978c428490d..070827af0845 100644 --- a/.github/workflows/build-test-macos-core-full_node-full_sync.yml +++ b/.github/workflows/build-test-macos-core-full_node-full_sync.yml @@ -89,7 +89,7 @@ jobs: - name: Test core-full_node-full_sync code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/full_node/full_sync/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/core/full_node/full_sync/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-core-full_node-stores.yml b/.github/workflows/build-test-macos-core-full_node-stores.yml index 81114297b741..ae890bc0f991 100644 --- a/.github/workflows/build-test-macos-core-full_node-stores.yml +++ b/.github/workflows/build-test-macos-core-full_node-stores.yml @@ -89,7 +89,7 @@ jobs: - name: Test core-full_node-stores code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/full_node/stores/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/core/full_node/stores/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-core-full_node.yml b/.github/workflows/build-test-macos-core-full_node.yml index cba830984ad3..417aec6810ce 100644 --- a/.github/workflows/build-test-macos-core-full_node.yml +++ b/.github/workflows/build-test-macos-core-full_node.yml @@ -89,7 +89,7 @@ jobs: - name: Test core-full_node code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/full_node/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/core/full_node/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-core-server.yml b/.github/workflows/build-test-macos-core-server.yml index f6fb4537b17b..5b214cac40ef 100644 --- a/.github/workflows/build-test-macos-core-server.yml +++ b/.github/workflows/build-test-macos-core-server.yml @@ -89,7 +89,7 @@ jobs: - name: Test core-server code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/server/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/core/server/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-core-ssl.yml b/.github/workflows/build-test-macos-core-ssl.yml index 55f54619d184..5ff17da64fc6 100644 --- a/.github/workflows/build-test-macos-core-ssl.yml +++ b/.github/workflows/build-test-macos-core-ssl.yml @@ -89,7 +89,7 @@ jobs: - name: Test core-ssl code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/ssl/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/core/ssl/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-core-util.yml b/.github/workflows/build-test-macos-core-util.yml index d3c35befdc77..7775a0d12166 100644 --- a/.github/workflows/build-test-macos-core-util.yml +++ b/.github/workflows/build-test-macos-core-util.yml @@ -89,7 +89,7 @@ jobs: - name: Test core-util code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/util/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/core/util/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-core.yml b/.github/workflows/build-test-macos-core.yml index 5ad96190f25d..3b6535909f7d 100644 --- a/.github/workflows/build-test-macos-core.yml +++ b/.github/workflows/build-test-macos-core.yml @@ -89,7 +89,7 @@ jobs: - name: Test core code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/core/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-farmer_harvester.yml b/.github/workflows/build-test-macos-farmer_harvester.yml index d976167d6e64..883e33ef291c 100644 --- a/.github/workflows/build-test-macos-farmer_harvester.yml +++ b/.github/workflows/build-test-macos-farmer_harvester.yml @@ -89,7 +89,7 @@ jobs: - name: Test farmer_harvester code with pytest run: | . ./activate - ./venv/bin/py.test tests/farmer_harvester/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/farmer_harvester/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-generator.yml b/.github/workflows/build-test-macos-generator.yml index af9677d0c1e9..2b4edb974fc9 100644 --- a/.github/workflows/build-test-macos-generator.yml +++ b/.github/workflows/build-test-macos-generator.yml @@ -89,7 +89,7 @@ jobs: - name: Test generator code with pytest run: | . ./activate - ./venv/bin/py.test tests/generator/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/generator/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-plotting.yml b/.github/workflows/build-test-macos-plotting.yml index e7af72bbe566..ec5b9cf89586 100644 --- a/.github/workflows/build-test-macos-plotting.yml +++ b/.github/workflows/build-test-macos-plotting.yml @@ -89,7 +89,7 @@ jobs: - name: Test plotting code with pytest run: | . ./activate - ./venv/bin/py.test tests/plotting/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/plotting/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-pools.yml b/.github/workflows/build-test-macos-pools.yml index 15623c2c41db..4f7901bebef1 100644 --- a/.github/workflows/build-test-macos-pools.yml +++ b/.github/workflows/build-test-macos-pools.yml @@ -89,7 +89,7 @@ jobs: - name: Test pools code with pytest run: | . ./activate - ./venv/bin/py.test tests/pools/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/pools/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-simulation.yml b/.github/workflows/build-test-macos-simulation.yml index 64e59534b9e0..e5c74f561369 100644 --- a/.github/workflows/build-test-macos-simulation.yml +++ b/.github/workflows/build-test-macos-simulation.yml @@ -101,7 +101,7 @@ jobs: - name: Test simulation code with pytest run: | . ./activate - ./venv/bin/py.test tests/simulation/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/simulation/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-tools.yml b/.github/workflows/build-test-macos-tools.yml index 3190020bd959..aa43cf015dfc 100644 --- a/.github/workflows/build-test-macos-tools.yml +++ b/.github/workflows/build-test-macos-tools.yml @@ -89,7 +89,7 @@ jobs: - name: Test tools code with pytest run: | . ./activate - ./venv/bin/py.test tests/tools/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/tools/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-util.yml b/.github/workflows/build-test-macos-util.yml index efaf870eb5b7..3e5b5c191def 100644 --- a/.github/workflows/build-test-macos-util.yml +++ b/.github/workflows/build-test-macos-util.yml @@ -89,7 +89,7 @@ jobs: - name: Test util code with pytest run: | . ./activate - ./venv/bin/py.test tests/util/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/util/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-wallet-cat_wallet.yml b/.github/workflows/build-test-macos-wallet-cat_wallet.yml index a7e05deaf8a3..bdbe48290fca 100644 --- a/.github/workflows/build-test-macos-wallet-cat_wallet.yml +++ b/.github/workflows/build-test-macos-wallet-cat_wallet.yml @@ -89,7 +89,7 @@ jobs: - name: Test wallet-cat_wallet code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/cat_wallet/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/wallet/cat_wallet/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-wallet-did_wallet.yml b/.github/workflows/build-test-macos-wallet-did_wallet.yml index bb453c2c7b1e..1bea77b1f4a5 100644 --- a/.github/workflows/build-test-macos-wallet-did_wallet.yml +++ b/.github/workflows/build-test-macos-wallet-did_wallet.yml @@ -89,7 +89,7 @@ jobs: - name: Test wallet-did_wallet code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/did_wallet/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/wallet/did_wallet/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-wallet-rl_wallet.yml b/.github/workflows/build-test-macos-wallet-rl_wallet.yml index 090d3ad9365e..2ebbe237c422 100644 --- a/.github/workflows/build-test-macos-wallet-rl_wallet.yml +++ b/.github/workflows/build-test-macos-wallet-rl_wallet.yml @@ -89,7 +89,7 @@ jobs: - name: Test wallet-rl_wallet code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/rl_wallet/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/wallet/rl_wallet/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-wallet-rpc.yml b/.github/workflows/build-test-macos-wallet-rpc.yml index 11d2e3b6e7ab..b7be9d327471 100644 --- a/.github/workflows/build-test-macos-wallet-rpc.yml +++ b/.github/workflows/build-test-macos-wallet-rpc.yml @@ -89,7 +89,7 @@ jobs: - name: Test wallet-rpc code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/rpc/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/wallet/rpc/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-wallet-simple_sync.yml b/.github/workflows/build-test-macos-wallet-simple_sync.yml index 6688f3224ef8..c943ac4c6793 100644 --- a/.github/workflows/build-test-macos-wallet-simple_sync.yml +++ b/.github/workflows/build-test-macos-wallet-simple_sync.yml @@ -89,7 +89,7 @@ jobs: - name: Test wallet-simple_sync code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/simple_sync/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/wallet/simple_sync/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-wallet-sync.yml b/.github/workflows/build-test-macos-wallet-sync.yml index e24edb51cecc..ac0c0ce180d3 100644 --- a/.github/workflows/build-test-macos-wallet-sync.yml +++ b/.github/workflows/build-test-macos-wallet-sync.yml @@ -89,7 +89,7 @@ jobs: - name: Test wallet-sync code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/sync/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/wallet/sync/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-wallet.yml b/.github/workflows/build-test-macos-wallet.yml index fee9f5c09935..9f5f2376a2f9 100644 --- a/.github/workflows/build-test-macos-wallet.yml +++ b/.github/workflows/build-test-macos-wallet.yml @@ -89,7 +89,7 @@ jobs: - name: Test wallet code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/wallet/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-weight_proof.yml b/.github/workflows/build-test-macos-weight_proof.yml index 5c443caee4bf..a20984374c1b 100644 --- a/.github/workflows/build-test-macos-weight_proof.yml +++ b/.github/workflows/build-test-macos-weight_proof.yml @@ -89,7 +89,7 @@ jobs: - name: Test weight_proof code with pytest run: | . ./activate - ./venv/bin/py.test tests/weight_proof/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/weight_proof/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-ubuntu-blockchain.yml b/.github/workflows/build-test-ubuntu-blockchain.yml index a929b884e68e..e022781bdc0a 100644 --- a/.github/workflows/build-test-ubuntu-blockchain.yml +++ b/.github/workflows/build-test-ubuntu-blockchain.yml @@ -88,7 +88,7 @@ jobs: - name: Test blockchain code with pytest run: | . ./activate - ./venv/bin/py.test tests/blockchain/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/blockchain/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-clvm.yml b/.github/workflows/build-test-ubuntu-clvm.yml index 2de335682de4..350224056312 100644 --- a/.github/workflows/build-test-ubuntu-clvm.yml +++ b/.github/workflows/build-test-ubuntu-clvm.yml @@ -75,7 +75,7 @@ jobs: - name: Test clvm code with pytest run: | . ./activate - ./venv/bin/py.test tests/clvm/test_*.py -s -v --durations 0 -n auto -m "not benchmark" + ./venv/bin/py.test tests/clvm/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-core-cmds.yml b/.github/workflows/build-test-ubuntu-core-cmds.yml index 474044698ce7..a8a76f925081 100644 --- a/.github/workflows/build-test-ubuntu-core-cmds.yml +++ b/.github/workflows/build-test-ubuntu-core-cmds.yml @@ -88,7 +88,7 @@ jobs: - name: Test core-cmds code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/cmds/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/core/cmds/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-core-consensus.yml b/.github/workflows/build-test-ubuntu-core-consensus.yml index adcade20830f..c5d4c99816e2 100644 --- a/.github/workflows/build-test-ubuntu-core-consensus.yml +++ b/.github/workflows/build-test-ubuntu-core-consensus.yml @@ -88,7 +88,7 @@ jobs: - name: Test core-consensus code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/consensus/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/core/consensus/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-core-custom_types.yml b/.github/workflows/build-test-ubuntu-core-custom_types.yml index 023ad90bb082..1dbb92a55ab8 100644 --- a/.github/workflows/build-test-ubuntu-core-custom_types.yml +++ b/.github/workflows/build-test-ubuntu-core-custom_types.yml @@ -88,7 +88,7 @@ jobs: - name: Test core-custom_types code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/custom_types/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/core/custom_types/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-core-daemon.yml b/.github/workflows/build-test-ubuntu-core-daemon.yml index 476b681575b6..7dfbf69c77c9 100644 --- a/.github/workflows/build-test-ubuntu-core-daemon.yml +++ b/.github/workflows/build-test-ubuntu-core-daemon.yml @@ -100,7 +100,7 @@ jobs: - name: Test core-daemon code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/daemon/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/core/daemon/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml b/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml index 31080127a9d4..29d352090421 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml @@ -88,7 +88,7 @@ jobs: - name: Test core-full_node-full_sync code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/full_node/full_sync/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/core/full_node/full_sync/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-core-full_node-stores.yml b/.github/workflows/build-test-ubuntu-core-full_node-stores.yml index 2c8b50c68aba..ee7872baa14d 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node-stores.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node-stores.yml @@ -88,7 +88,7 @@ jobs: - name: Test core-full_node-stores code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/full_node/stores/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/core/full_node/stores/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" - name: Check resource usage diff --git a/.github/workflows/build-test-ubuntu-core-full_node.yml b/.github/workflows/build-test-ubuntu-core-full_node.yml index fa58214bd3ff..145edd820ac8 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node.yml @@ -88,7 +88,7 @@ jobs: - name: Test core-full_node code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/full_node/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/core/full_node/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" - name: Check resource usage diff --git a/.github/workflows/build-test-ubuntu-core-server.yml b/.github/workflows/build-test-ubuntu-core-server.yml index d73d1ba4beed..bb8d60e75acd 100644 --- a/.github/workflows/build-test-ubuntu-core-server.yml +++ b/.github/workflows/build-test-ubuntu-core-server.yml @@ -88,7 +88,7 @@ jobs: - name: Test core-server code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/server/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/core/server/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-core-ssl.yml b/.github/workflows/build-test-ubuntu-core-ssl.yml index fc1ac4c8289f..a4000b081dc3 100644 --- a/.github/workflows/build-test-ubuntu-core-ssl.yml +++ b/.github/workflows/build-test-ubuntu-core-ssl.yml @@ -88,7 +88,7 @@ jobs: - name: Test core-ssl code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/ssl/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/core/ssl/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-core-util.yml b/.github/workflows/build-test-ubuntu-core-util.yml index 93aee80915b6..076ee0943dde 100644 --- a/.github/workflows/build-test-ubuntu-core-util.yml +++ b/.github/workflows/build-test-ubuntu-core-util.yml @@ -88,7 +88,7 @@ jobs: - name: Test core-util code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/util/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/core/util/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-core.yml b/.github/workflows/build-test-ubuntu-core.yml index 57a6a3b2d177..428f755292e6 100644 --- a/.github/workflows/build-test-ubuntu-core.yml +++ b/.github/workflows/build-test-ubuntu-core.yml @@ -88,7 +88,7 @@ jobs: - name: Test core code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/core/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-farmer_harvester.yml b/.github/workflows/build-test-ubuntu-farmer_harvester.yml index 157dbd140273..882efb7cd47b 100644 --- a/.github/workflows/build-test-ubuntu-farmer_harvester.yml +++ b/.github/workflows/build-test-ubuntu-farmer_harvester.yml @@ -88,7 +88,7 @@ jobs: - name: Test farmer_harvester code with pytest run: | . ./activate - ./venv/bin/py.test tests/farmer_harvester/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/farmer_harvester/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-generator.yml b/.github/workflows/build-test-ubuntu-generator.yml index 54ada47e1cea..1195dbd98d18 100644 --- a/.github/workflows/build-test-ubuntu-generator.yml +++ b/.github/workflows/build-test-ubuntu-generator.yml @@ -88,7 +88,7 @@ jobs: - name: Test generator code with pytest run: | . ./activate - ./venv/bin/py.test tests/generator/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/generator/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-plotting.yml b/.github/workflows/build-test-ubuntu-plotting.yml index 8430fa8aaeec..db621908e873 100644 --- a/.github/workflows/build-test-ubuntu-plotting.yml +++ b/.github/workflows/build-test-ubuntu-plotting.yml @@ -88,7 +88,7 @@ jobs: - name: Test plotting code with pytest run: | . ./activate - ./venv/bin/py.test tests/plotting/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/plotting/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-pools.yml b/.github/workflows/build-test-ubuntu-pools.yml index d10655de209b..dec387db50a0 100644 --- a/.github/workflows/build-test-ubuntu-pools.yml +++ b/.github/workflows/build-test-ubuntu-pools.yml @@ -88,7 +88,7 @@ jobs: - name: Test pools code with pytest run: | . ./activate - ./venv/bin/py.test tests/pools/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/pools/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-simulation.yml b/.github/workflows/build-test-ubuntu-simulation.yml index 81c91603f81f..645a121e08e8 100644 --- a/.github/workflows/build-test-ubuntu-simulation.yml +++ b/.github/workflows/build-test-ubuntu-simulation.yml @@ -100,7 +100,7 @@ jobs: - name: Test simulation code with pytest run: | . ./activate - ./venv/bin/py.test tests/simulation/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/simulation/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-tools.yml b/.github/workflows/build-test-ubuntu-tools.yml index 8f689c66c6a1..94b566cfe90e 100644 --- a/.github/workflows/build-test-ubuntu-tools.yml +++ b/.github/workflows/build-test-ubuntu-tools.yml @@ -88,7 +88,7 @@ jobs: - name: Test tools code with pytest run: | . ./activate - ./venv/bin/py.test tests/tools/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/tools/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-util.yml b/.github/workflows/build-test-ubuntu-util.yml index 74991c734bba..b25e6145ab8d 100644 --- a/.github/workflows/build-test-ubuntu-util.yml +++ b/.github/workflows/build-test-ubuntu-util.yml @@ -88,7 +88,7 @@ jobs: - name: Test util code with pytest run: | . ./activate - ./venv/bin/py.test tests/util/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/util/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml index 785234c66c05..ae92a9c7ee09 100644 --- a/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml @@ -88,7 +88,7 @@ jobs: - name: Test wallet-cat_wallet code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/cat_wallet/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/wallet/cat_wallet/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml index ee606bc54df5..8a30c642df06 100644 --- a/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml @@ -88,7 +88,7 @@ jobs: - name: Test wallet-did_wallet code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/did_wallet/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/wallet/did_wallet/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml index 176bc2bf73a2..1354f84ed34c 100644 --- a/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml @@ -88,7 +88,7 @@ jobs: - name: Test wallet-rl_wallet code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/rl_wallet/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/wallet/rl_wallet/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-wallet-rpc.yml b/.github/workflows/build-test-ubuntu-wallet-rpc.yml index 51177944a934..1aeb970a4ea1 100644 --- a/.github/workflows/build-test-ubuntu-wallet-rpc.yml +++ b/.github/workflows/build-test-ubuntu-wallet-rpc.yml @@ -88,7 +88,7 @@ jobs: - name: Test wallet-rpc code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/rpc/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/wallet/rpc/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml b/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml index 03849eb8da14..29ac1de6a4b2 100644 --- a/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml +++ b/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml @@ -88,7 +88,7 @@ jobs: - name: Test wallet-simple_sync code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/simple_sync/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/wallet/simple_sync/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-wallet-sync.yml b/.github/workflows/build-test-ubuntu-wallet-sync.yml index 7eae2814f67c..06b8337b007c 100644 --- a/.github/workflows/build-test-ubuntu-wallet-sync.yml +++ b/.github/workflows/build-test-ubuntu-wallet-sync.yml @@ -88,7 +88,7 @@ jobs: - name: Test wallet-sync code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/sync/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/wallet/sync/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-wallet.yml b/.github/workflows/build-test-ubuntu-wallet.yml index d55940c9fed0..1c0b5073a110 100644 --- a/.github/workflows/build-test-ubuntu-wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet.yml @@ -88,7 +88,7 @@ jobs: - name: Test wallet code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/wallet/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" # diff --git a/.github/workflows/build-test-ubuntu-weight_proof.yml b/.github/workflows/build-test-ubuntu-weight_proof.yml index a9c4a762b411..ad4469649edf 100644 --- a/.github/workflows/build-test-ubuntu-weight_proof.yml +++ b/.github/workflows/build-test-ubuntu-weight_proof.yml @@ -88,7 +88,7 @@ jobs: - name: Test weight_proof code with pytest run: | . ./activate - ./venv/bin/py.test tests/weight_proof/test_*.py -s -v --durations 0 -m "not benchmark" + ./venv/bin/py.test tests/weight_proof/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" # diff --git a/tests/blockchain/config.py b/tests/blockchain/config.py index d9b815b24cb2..93f77cd99ac9 100644 --- a/tests/blockchain/config.py +++ b/tests/blockchain/config.py @@ -1 +1,2 @@ +parallel = True job_timeout = 60 diff --git a/tests/build-workflows.py b/tests/build-workflows.py index 5a5965cb0513..29f10a49bd85 100755 --- a/tests/build-workflows.py +++ b/tests/build-workflows.py @@ -79,8 +79,7 @@ def generate_replacements(conf, dir): ] = "# Omitted checking out blocks and plots repo Chia-Network/test-cache" if not conf["install_timelord"]: replacements["INSTALL_TIMELORD"] = "# Omitted installing Timelord" - if conf["parallel"]: - replacements["PYTEST_PARALLEL_ARGS"] = " -n auto" + replacements["PYTEST_PARALLEL_ARGS"] = " -n 4" if conf["parallel"] else " -n 0" if conf["job_timeout"]: replacements["JOB_TIMEOUT"] = str(conf["job_timeout"]) replacements["TEST_DIR"] = "/".join([*dir.relative_to(root_path.parent).parts, "test_*.py"]) diff --git a/tests/core/cmds/config.py b/tests/core/cmds/config.py new file mode 100644 index 000000000000..7f9e1e1a76e4 --- /dev/null +++ b/tests/core/cmds/config.py @@ -0,0 +1 @@ +parallel = True diff --git a/tests/core/consensus/config.py b/tests/core/consensus/config.py new file mode 100644 index 000000000000..7f9e1e1a76e4 --- /dev/null +++ b/tests/core/consensus/config.py @@ -0,0 +1 @@ +parallel = True diff --git a/tests/core/custom_types/config.py b/tests/core/custom_types/config.py new file mode 100644 index 000000000000..7f9e1e1a76e4 --- /dev/null +++ b/tests/core/custom_types/config.py @@ -0,0 +1 @@ +parallel = True diff --git a/tests/core/full_node/config.py b/tests/core/full_node/config.py index 510676ac744e..54c7a57c3606 100644 --- a/tests/core/full_node/config.py +++ b/tests/core/full_node/config.py @@ -1,4 +1,5 @@ # flake8: noqa: E501 +parallel = True job_timeout = 50 CHECK_RESOURCE_USAGE = """ - name: Check resource usage diff --git a/tests/core/full_node/dos/config.py b/tests/core/full_node/dos/config.py index d9b815b24cb2..93f77cd99ac9 100644 --- a/tests/core/full_node/dos/config.py +++ b/tests/core/full_node/dos/config.py @@ -1 +1,2 @@ +parallel = True job_timeout = 60 diff --git a/tests/core/full_node/stores/config.py b/tests/core/full_node/stores/config.py index 24f501b9f693..08d539dc1b71 100644 --- a/tests/core/full_node/stores/config.py +++ b/tests/core/full_node/stores/config.py @@ -1,4 +1,5 @@ # flake8: noqa: E501 +parallel = True job_timeout = 40 CHECK_RESOURCE_USAGE = """ - name: Check resource usage diff --git a/tests/generator/config.py b/tests/generator/config.py new file mode 100644 index 000000000000..7f9e1e1a76e4 --- /dev/null +++ b/tests/generator/config.py @@ -0,0 +1 @@ +parallel = True diff --git a/tests/plotting/config.py b/tests/plotting/config.py new file mode 100644 index 000000000000..c5495db27705 --- /dev/null +++ b/tests/plotting/config.py @@ -0,0 +1,2 @@ +parallel = True +install_timelord = False diff --git a/tests/pytest.ini b/tests/pytest.ini index cbf633bd24b2..c5834bc821fa 100644 --- a/tests/pytest.ini +++ b/tests/pytest.ini @@ -1,7 +1,7 @@ [pytest] ; logging options log_cli = False -addopts = --verbose --tb=short +addopts = --verbose --tb=short -n auto log_level = WARNING console_output_style = count log_format = %(asctime)s %(name)s: %(levelname)s %(message)s diff --git a/tests/tools/config.py b/tests/tools/config.py new file mode 100644 index 000000000000..7f9e1e1a76e4 --- /dev/null +++ b/tests/tools/config.py @@ -0,0 +1 @@ +parallel = True diff --git a/tests/util/config.py b/tests/util/config.py index d9b815b24cb2..93f77cd99ac9 100644 --- a/tests/util/config.py +++ b/tests/util/config.py @@ -1 +1,2 @@ +parallel = True job_timeout = 60 diff --git a/tests/wallet/config.py b/tests/wallet/config.py new file mode 100644 index 000000000000..7f9e1e1a76e4 --- /dev/null +++ b/tests/wallet/config.py @@ -0,0 +1 @@ +parallel = True diff --git a/tests/weight_proof/config.py b/tests/weight_proof/config.py new file mode 100644 index 000000000000..7f9e1e1a76e4 --- /dev/null +++ b/tests/weight_proof/config.py @@ -0,0 +1 @@ +parallel = True From 4fe5737da040fb68fd73b7f1aea13a731da31221 Mon Sep 17 00:00:00 2001 From: Amine Khaldi Date: Wed, 23 Mar 2022 00:19:32 +0100 Subject: [PATCH 246/378] Fix timelord installation for Debian. (#10841) --- install-timelord.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/install-timelord.sh b/install-timelord.sh index 24fa76004092..3c99263768a6 100644 --- a/install-timelord.sh +++ b/install-timelord.sh @@ -20,11 +20,12 @@ CHIAVDF_VERSION=$(python -c 'from setup import dependencies; t = [_ for _ in dep ubuntu_cmake_install() { UBUNTU_PRE_2004=$(python -c 'import subprocess; process = subprocess.run(["lsb_release", "-rs"], stdout=subprocess.PIPE); print(float(process.stdout) < float(20.04))') if [ "$UBUNTU_PRE_2004" = "True" ]; then - echo "Ubuntu version is pre 20.04LTS - installing CMake with snap." - sudo apt-get install snap -y + echo "Installing CMake with snap." + sudo apt-get install snapd -y sudo apt-get remove --purge cmake -y hash -r sudo snap install cmake --classic + . /etc/profile else echo "Ubuntu 20.04LTS and newer support CMake 3.16+" sudo apt-get install cmake -y From 620a1b3f2385b9f5fe66e0917257c6f891595045 Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Tue, 22 Mar 2022 16:20:32 -0700 Subject: [PATCH 247/378] add optional persistence to SpendSim (#10780) * add optional persistence to SpendSim * Accidental rename --- chia/clvm/spend_sim.py | 90 +++++++++++++++++++++++++++++++----------- 1 file changed, 68 insertions(+), 22 deletions(-) diff --git a/chia/clvm/spend_sim.py b/chia/clvm/spend_sim.py index 7dc9de37e5df..5c22f365b01a 100644 --- a/chia/clvm/spend_sim.py +++ b/chia/clvm/spend_sim.py @@ -1,5 +1,6 @@ import aiosqlite +from dataclasses import dataclass from typing import Optional, List, Dict, Tuple, Any from chia.types.blockchain_format.sized_bytes import bytes32 @@ -9,6 +10,7 @@ from chia.util.hash import std_hash from chia.util.errors import Err, ValidationError from chia.util.db_wrapper import DBWrapper +from chia.util.streamable import Streamable, streamable from chia.types.coin_record import CoinRecord from chia.types.spend_bundle import SpendBundle from chia.types.generator_types import BlockGenerator @@ -35,21 +37,44 @@ """ -class SimFullBlock: - def __init__(self, generator: Optional[BlockGenerator], height: uint32): - self.height = height # Note that height is not on a regular FullBlock - self.transactions_generator = generator +@dataclass(frozen=True) +@streamable +class SimFullBlock(Streamable): + transactions_generator: Optional[BlockGenerator] + height: uint32 # Note that height is not on a regular FullBlock -class SimBlockRecord: - def __init__(self, rci: List[Coin], height: uint32, timestamp: uint64): - self.reward_claims_incorporated = rci - self.height = height - self.prev_transaction_block_height = uint32(height - 1) if height > 0 else 0 - self.timestamp = timestamp - self.is_transaction_block = True - self.header_hash = std_hash(bytes(height)) - self.prev_transaction_block_hash = std_hash(std_hash(height)) +@dataclass(frozen=True) +@streamable +class SimBlockRecord(Streamable): + reward_claims_incorporated: List[Coin] + height: uint32 + prev_transaction_block_height: uint32 + timestamp: uint64 + is_transaction_block: bool + header_hash: bytes32 + prev_transaction_block_hash: bytes32 + + @classmethod + def create(cls, rci: List[Coin], height: uint32, timestamp: uint64): + return cls( + rci, + height, + uint32(height - 1 if height > 0 else 0), + timestamp, + True, + std_hash(bytes(height)), + std_hash(std_hash(height)), + ) + + +@dataclass(frozen=True) +@streamable +class SimStore(Streamable): + timestamp: uint64 + block_height: uint32 + block_records: List[SimBlockRecord] + blocks: List[SimFullBlock] class SpendSim: @@ -63,20 +88,41 @@ class SpendSim: defaults: ConsensusConstants @classmethod - async def create(cls, defaults=DEFAULT_CONSTANTS): + async def create(cls, db_path=":memory:", defaults=DEFAULT_CONSTANTS): self = cls() - self.connection = await aiosqlite.connect(":memory:") - coin_store = await CoinStore.create(DBWrapper(self.connection)) + self.connection = DBWrapper(await aiosqlite.connect(db_path)) + coin_store = await CoinStore.create(self.connection) self.mempool_manager = MempoolManager(coin_store, defaults) - self.block_records = [] - self.blocks = [] - self.timestamp = 1 - self.block_height = 0 self.defaults = defaults + + # Load the next data if there is any + await self.connection.db.execute("CREATE TABLE IF NOT EXISTS block_data(data blob PRIMARY_KEY)") + cursor = await self.connection.db.execute("SELECT * from block_data") + row = await cursor.fetchone() + await cursor.close() + if row is not None: + store_data = SimStore.from_bytes(row[0]) + self.timestamp = store_data.timestamp + self.block_height = store_data.block_height + self.block_records = store_data.block_records + self.blocks = store_data.blocks + else: + self.timestamp = 1 + self.block_height = 0 + self.block_records = [] + self.blocks = [] return self async def close(self): - await self.connection.close() + c = await self.connection.db.execute("DELETE FROM block_data") + await c.close() + c = await self.connection.db.execute( + "INSERT INTO block_data VALUES(?)", + (bytes(SimStore(self.timestamp, self.block_height, self.block_records, self.blocks)),), + ) + await c.close() + await self.connection.db.commit() + await self.connection.db.close() async def new_peak(self): await self.mempool_manager.new_peak(self.block_records[-1], []) @@ -158,7 +204,7 @@ async def farm_block(self, puzzle_hash: bytes32 = bytes32(b"0" * 32)): # SimBlockRecord is created generator: Optional[BlockGenerator] = await self.generate_transaction_generator(generator_bundle) self.block_records.append( - SimBlockRecord( + SimBlockRecord.create( [pool_coin, farmer_coin], next_block_height, self.timestamp, From 063333ee92989d5666e293aa9179d3de651ed13f Mon Sep 17 00:00:00 2001 From: Adam Kelly <338792+aqk@users.noreply.github.com> Date: Tue, 22 Mar 2022 16:21:06 -0700 Subject: [PATCH 248/378] remove duplicate event_loop (#10768) --- tests/core/test_db_validation.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/tests/core/test_db_validation.py b/tests/core/test_db_validation.py index 64d5e1e79e6d..39e28d42fe13 100644 --- a/tests/core/test_db_validation.py +++ b/tests/core/test_db_validation.py @@ -1,14 +1,11 @@ -import asyncio import random import sqlite3 -from asyncio.events import AbstractEventLoop from contextlib import closing from pathlib import Path -from typing import Iterator, List +from typing import List import aiosqlite import pytest -import pytest_asyncio from chia.cmds.db_validate_func import validate_v2 from chia.consensus.blockchain import Blockchain @@ -25,12 +22,6 @@ from tests.util.temp_file import TempFile -@pytest_asyncio.fixture(scope="session") -def event_loop() -> Iterator[AbstractEventLoop]: - loop = asyncio.get_event_loop() - yield loop - - def rand_hash() -> bytes32: ret = bytearray(32) for i in range(32): From b1acb5597ba242649d1dc97de7fd605148e33816 Mon Sep 17 00:00:00 2001 From: William Allen Date: Tue, 22 Mar 2022 18:22:17 -0500 Subject: [PATCH 249/378] Adding check for python3.9 alongside python3.10 on Arch (#10363) * Adding check for python3.9 alongside python3.10 on Arch * Adjusting install.sh instructions for Arch * Disabling prescribed python install for Arch * Setting Arch install script to exit 0 to pass tests * Adding workflow step for functional Arch install testing * Adding noconfirm to pacman install command * Relocating Arch support message for install.sh --- .github/workflows/test-install-scripts.yml | 4 ++++ install.sh | 18 +++++++++--------- 2 files changed, 13 insertions(+), 9 deletions(-) mode change 100644 => 100755 install.sh diff --git a/.github/workflows/test-install-scripts.yml b/.github/workflows/test-install-scripts.yml index 83c8696b7246..e8e27dd6156a 100644 --- a/.github/workflows/test-install-scripts.yml +++ b/.github/workflows/test-install-scripts.yml @@ -125,6 +125,10 @@ jobs: if: ${{ matrix.distribution.type == 'arch' }} run: | pacman --noconfirm --refresh base --sync git sudo + # The behavior we follow in install.sh is unique with Arch in that + # we leave it to the user to install the appropriate version of python, + # so we need to install python here in order for the test to succeed. + pacman --noconfirm -U --needed https://archive.archlinux.org/packages/p/python/python-3.9.9-1-x86_64.pkg.tar.zst - name: Prepare CentOS if: ${{ matrix.distribution.type == 'centos' }} diff --git a/install.sh b/install.sh old mode 100644 new mode 100755 index 2f76b859943c..3dc27ae13f5f --- a/install.sh +++ b/install.sh @@ -134,21 +134,17 @@ if [ "$(uname)" = "Linux" ]; then sudo apt-get install -y python3-venv elif type pacman >/dev/null 2>&1 && [ -f "/etc/arch-release" ]; then # Arch Linux + # Arch provides latest python version. User will need to manually install python 3.9 if it is not present echo "Installing on Arch Linux." - echo "Python <= 3.9.9 is required. Installing python-3.9.9-1" case $(uname -m) in - x86_64) - sudo pacman ${PACMAN_AUTOMATED} -U --needed https://archive.archlinux.org/packages/p/python/python-3.9.9-1-x86_64.pkg.tar.zst - ;; - aarch64) - sudo pacman ${PACMAN_AUTOMATED} -U --needed http://tardis.tiny-vps.com/aarm/packages/p/python/python-3.9.9-1-aarch64.pkg.tar.xz + x86_64|aarch64) + sudo pacman ${PACMAN_AUTOMATED} -S --needed git ;; *) echo "Incompatible CPU architecture. Must be x86_64 or aarch64." exit 1 ;; - esac - sudo pacman ${PACMAN_AUTOMATED} -S --needed git + esac elif type yum >/dev/null 2>&1 && [ ! -f "/etc/redhat-release" ] && [ ! -f "/etc/centos-release" ] && [ ! -f "/etc/fedora-release" ]; then # AMZN 2 echo "Installing on Amazon Linux 2." @@ -192,8 +188,12 @@ find_python() { if [ "$BEST_VERSION" = "3" ]; then PY3_VERSION=$(python$BEST_VERSION --version | cut -d ' ' -f2) if [[ "$PY3_VERSION" =~ 3.10.* ]]; then - echo "Chia requires Python version <= 3.9.9" + echo "Chia requires Python version <= 3.9.10" echo "Current Python version = $PY3_VERSION" + # If Arch, direct to Arch Wiki + if type pacman >/dev/null 2>&1 && [ -f "/etc/arch-release" ]; then + echo "Please see https://wiki.archlinux.org/title/python#Old_versions for support." + fi exit 1 fi fi From 4ea82fdb09c97ab5c6651e4640c09ff3c62bd271 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 24 Mar 2022 09:29:05 -0700 Subject: [PATCH 250/378] use DEFAULT_ROOT_PATH in tests (#10801) --- tests/block_tools.py | 3 ++- tests/util/blockchain.py | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/block_tools.py b/tests/block_tools.py index 6906710501a8..63b80b8fb9ae 100644 --- a/tests/block_tools.py +++ b/tests/block_tools.py @@ -75,6 +75,7 @@ from chia.util.bech32m import encode_puzzle_hash from chia.util.block_cache import BlockCache from chia.util.config import get_config_lock, load_config, save_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 from chia.util.keychain import Keychain, bytes_to_mnemonic @@ -1375,7 +1376,7 @@ def get_challenges( def get_plot_dir() -> Path: - cache_path = Path(os.path.expanduser(os.getenv("CHIA_ROOT", "~/.chia/"))) / "test-plots" + cache_path = DEFAULT_ROOT_PATH.parent.joinpath("test-plots") ci = os.environ.get("CI") if ci is not None and not cache_path.exists(): diff --git a/tests/util/blockchain.py b/tests/util/blockchain.py index 3cf4283aba83..4b38e0238a9a 100644 --- a/tests/util/blockchain.py +++ b/tests/util/blockchain.py @@ -13,6 +13,7 @@ from chia.full_node.hint_store import HintStore from chia.types.full_block import FullBlock from chia.util.db_wrapper import DBWrapper +from chia.util.default_root import DEFAULT_ROOT_PATH from chia.util.path import mkdir from tests.block_tools import BlockTools @@ -45,8 +46,8 @@ def persistent_blocks( ): # try loading from disc, if not create new blocks.db file # TODO hash fixtures.py and blocktool.py, add to path, delete if the files changed - block_path_dir = Path("~/.chia/blocks").expanduser() - file_path = Path(f"~/.chia/blocks/{db_name}").expanduser() + block_path_dir = DEFAULT_ROOT_PATH.parent.joinpath("blocks") + file_path = block_path_dir.joinpath(db_name) ci = os.environ.get("CI") if ci is not None and not file_path.exists(): From 2eb4fdeaee564760363bf508cbeaf5bff5a684a9 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 24 Mar 2022 09:30:42 -0700 Subject: [PATCH 251/378] Disable the pytest-monitor plugin in CI if not checking results (#10837) * disable the pytest-monitor plugin if not reporting results pytest-monitor uses multiprocessing and has caused multiple confusing issues. Perhaps it can be adjusted to not use multiprocessing, but for now lets just isolate the oddities to where we actually use it. * use a template for resource usage check, similar to timelord install * hint testconfig.custom_vars --- .github/workflows/build-test-ubuntu-blockchain.yml | 3 ++- .github/workflows/build-test-ubuntu-clvm.yml | 3 ++- .github/workflows/build-test-ubuntu-core-cmds.yml | 3 ++- .github/workflows/build-test-ubuntu-core-consensus.yml | 3 ++- .../workflows/build-test-ubuntu-core-custom_types.yml | 3 ++- .github/workflows/build-test-ubuntu-core-daemon.yml | 3 ++- .../build-test-ubuntu-core-full_node-full_sync.yml | 3 ++- .../build-test-ubuntu-core-full_node-stores.yml | 1 - .github/workflows/build-test-ubuntu-core-full_node.yml | 1 - .github/workflows/build-test-ubuntu-core-server.yml | 3 ++- .github/workflows/build-test-ubuntu-core-ssl.yml | 3 ++- .github/workflows/build-test-ubuntu-core-util.yml | 3 ++- .github/workflows/build-test-ubuntu-core.yml | 3 ++- .github/workflows/build-test-ubuntu-farmer_harvester.yml | 3 ++- .github/workflows/build-test-ubuntu-generator.yml | 3 ++- .github/workflows/build-test-ubuntu-plotting.yml | 3 ++- .github/workflows/build-test-ubuntu-pools.yml | 3 ++- .github/workflows/build-test-ubuntu-simulation.yml | 3 ++- .github/workflows/build-test-ubuntu-tools.yml | 3 ++- .github/workflows/build-test-ubuntu-util.yml | 3 ++- .../workflows/build-test-ubuntu-wallet-cat_wallet.yml | 3 ++- .../workflows/build-test-ubuntu-wallet-did_wallet.yml | 3 ++- .github/workflows/build-test-ubuntu-wallet-rl_wallet.yml | 3 ++- .github/workflows/build-test-ubuntu-wallet-rpc.yml | 3 ++- .../workflows/build-test-ubuntu-wallet-simple_sync.yml | 3 ++- .github/workflows/build-test-ubuntu-wallet-sync.yml | 3 ++- .github/workflows/build-test-ubuntu-wallet.yml | 3 ++- .github/workflows/build-test-ubuntu-weight_proof.yml | 3 ++- tests/build-workflows.py | 9 +++++++++ tests/core/full_node/config.py | 7 +------ tests/core/full_node/stores/config.py | 7 +------ tests/runner_templates/build-test-ubuntu | 3 ++- tests/runner_templates/check-resource-usage.include.yml | 4 ++++ tests/testconfig.py | 5 ++++- 34 files changed, 73 insertions(+), 42 deletions(-) create mode 100644 tests/runner_templates/check-resource-usage.include.yml diff --git a/.github/workflows/build-test-ubuntu-blockchain.yml b/.github/workflows/build-test-ubuntu-blockchain.yml index e022781bdc0a..7c82decc4bc6 100644 --- a/.github/workflows/build-test-ubuntu-blockchain.yml +++ b/.github/workflows/build-test-ubuntu-blockchain.yml @@ -88,8 +88,9 @@ jobs: - name: Test blockchain code with pytest run: | . ./activate - ./venv/bin/py.test tests/blockchain/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + ./venv/bin/py.test tests/blockchain/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor +# Omitted resource usage check # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme diff --git a/.github/workflows/build-test-ubuntu-clvm.yml b/.github/workflows/build-test-ubuntu-clvm.yml index 350224056312..8f636f0c5fac 100644 --- a/.github/workflows/build-test-ubuntu-clvm.yml +++ b/.github/workflows/build-test-ubuntu-clvm.yml @@ -75,8 +75,9 @@ jobs: - name: Test clvm code with pytest run: | . ./activate - ./venv/bin/py.test tests/clvm/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + ./venv/bin/py.test tests/clvm/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor +# Omitted resource usage check # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme diff --git a/.github/workflows/build-test-ubuntu-core-cmds.yml b/.github/workflows/build-test-ubuntu-core-cmds.yml index a8a76f925081..6a470e9443ec 100644 --- a/.github/workflows/build-test-ubuntu-core-cmds.yml +++ b/.github/workflows/build-test-ubuntu-core-cmds.yml @@ -88,8 +88,9 @@ jobs: - name: Test core-cmds code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/cmds/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + ./venv/bin/py.test tests/core/cmds/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor +# Omitted resource usage check # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme diff --git a/.github/workflows/build-test-ubuntu-core-consensus.yml b/.github/workflows/build-test-ubuntu-core-consensus.yml index c5d4c99816e2..1e5e31b97ecd 100644 --- a/.github/workflows/build-test-ubuntu-core-consensus.yml +++ b/.github/workflows/build-test-ubuntu-core-consensus.yml @@ -88,8 +88,9 @@ jobs: - name: Test core-consensus code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/consensus/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + ./venv/bin/py.test tests/core/consensus/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor +# Omitted resource usage check # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme diff --git a/.github/workflows/build-test-ubuntu-core-custom_types.yml b/.github/workflows/build-test-ubuntu-core-custom_types.yml index 1dbb92a55ab8..6e7d4ce1603b 100644 --- a/.github/workflows/build-test-ubuntu-core-custom_types.yml +++ b/.github/workflows/build-test-ubuntu-core-custom_types.yml @@ -88,8 +88,9 @@ jobs: - name: Test core-custom_types code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/custom_types/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + ./venv/bin/py.test tests/core/custom_types/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor +# Omitted resource usage check # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme diff --git a/.github/workflows/build-test-ubuntu-core-daemon.yml b/.github/workflows/build-test-ubuntu-core-daemon.yml index 7dfbf69c77c9..caa4c4c4d4e3 100644 --- a/.github/workflows/build-test-ubuntu-core-daemon.yml +++ b/.github/workflows/build-test-ubuntu-core-daemon.yml @@ -100,8 +100,9 @@ jobs: - name: Test core-daemon code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/daemon/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + ./venv/bin/py.test tests/core/daemon/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor +# Omitted resource usage check # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme diff --git a/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml b/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml index 29d352090421..e7253f90235a 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml @@ -88,8 +88,9 @@ jobs: - name: Test core-full_node-full_sync code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/full_node/full_sync/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + ./venv/bin/py.test tests/core/full_node/full_sync/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor +# Omitted resource usage check # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme diff --git a/.github/workflows/build-test-ubuntu-core-full_node-stores.yml b/.github/workflows/build-test-ubuntu-core-full_node-stores.yml index ee7872baa14d..18a91428d3b0 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node-stores.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node-stores.yml @@ -90,7 +90,6 @@ jobs: . ./activate ./venv/bin/py.test tests/core/full_node/stores/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" - - name: Check resource usage run: | sqlite3 -readonly -separator " " .pymon "select item,cpu_usage,total_time,mem_usage from TEST_METRICS order by mem_usage desc;" >metrics.out diff --git a/.github/workflows/build-test-ubuntu-core-full_node.yml b/.github/workflows/build-test-ubuntu-core-full_node.yml index 145edd820ac8..42c82d0c353a 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node.yml @@ -90,7 +90,6 @@ jobs: . ./activate ./venv/bin/py.test tests/core/full_node/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" - - name: Check resource usage run: | sqlite3 -readonly -separator " " .pymon "select item,cpu_usage,total_time,mem_usage from TEST_METRICS order by mem_usage desc;" >metrics.out diff --git a/.github/workflows/build-test-ubuntu-core-server.yml b/.github/workflows/build-test-ubuntu-core-server.yml index bb8d60e75acd..4f821ea031ca 100644 --- a/.github/workflows/build-test-ubuntu-core-server.yml +++ b/.github/workflows/build-test-ubuntu-core-server.yml @@ -88,8 +88,9 @@ jobs: - name: Test core-server code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/server/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + ./venv/bin/py.test tests/core/server/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor +# Omitted resource usage check # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme diff --git a/.github/workflows/build-test-ubuntu-core-ssl.yml b/.github/workflows/build-test-ubuntu-core-ssl.yml index a4000b081dc3..ca99844a326f 100644 --- a/.github/workflows/build-test-ubuntu-core-ssl.yml +++ b/.github/workflows/build-test-ubuntu-core-ssl.yml @@ -88,8 +88,9 @@ jobs: - name: Test core-ssl code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/ssl/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + ./venv/bin/py.test tests/core/ssl/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor +# Omitted resource usage check # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme diff --git a/.github/workflows/build-test-ubuntu-core-util.yml b/.github/workflows/build-test-ubuntu-core-util.yml index 076ee0943dde..ce765305e1e0 100644 --- a/.github/workflows/build-test-ubuntu-core-util.yml +++ b/.github/workflows/build-test-ubuntu-core-util.yml @@ -88,8 +88,9 @@ jobs: - name: Test core-util code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/util/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + ./venv/bin/py.test tests/core/util/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor +# Omitted resource usage check # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme diff --git a/.github/workflows/build-test-ubuntu-core.yml b/.github/workflows/build-test-ubuntu-core.yml index 428f755292e6..82436e9c386e 100644 --- a/.github/workflows/build-test-ubuntu-core.yml +++ b/.github/workflows/build-test-ubuntu-core.yml @@ -88,8 +88,9 @@ jobs: - name: Test core code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + ./venv/bin/py.test tests/core/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor +# Omitted resource usage check # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme diff --git a/.github/workflows/build-test-ubuntu-farmer_harvester.yml b/.github/workflows/build-test-ubuntu-farmer_harvester.yml index 882efb7cd47b..d7c4a87a5333 100644 --- a/.github/workflows/build-test-ubuntu-farmer_harvester.yml +++ b/.github/workflows/build-test-ubuntu-farmer_harvester.yml @@ -88,8 +88,9 @@ jobs: - name: Test farmer_harvester code with pytest run: | . ./activate - ./venv/bin/py.test tests/farmer_harvester/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + ./venv/bin/py.test tests/farmer_harvester/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor +# Omitted resource usage check # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme diff --git a/.github/workflows/build-test-ubuntu-generator.yml b/.github/workflows/build-test-ubuntu-generator.yml index 1195dbd98d18..80b3cf9d0308 100644 --- a/.github/workflows/build-test-ubuntu-generator.yml +++ b/.github/workflows/build-test-ubuntu-generator.yml @@ -88,8 +88,9 @@ jobs: - name: Test generator code with pytest run: | . ./activate - ./venv/bin/py.test tests/generator/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + ./venv/bin/py.test tests/generator/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor +# Omitted resource usage check # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme diff --git a/.github/workflows/build-test-ubuntu-plotting.yml b/.github/workflows/build-test-ubuntu-plotting.yml index db621908e873..d2b58a509fd9 100644 --- a/.github/workflows/build-test-ubuntu-plotting.yml +++ b/.github/workflows/build-test-ubuntu-plotting.yml @@ -88,8 +88,9 @@ jobs: - name: Test plotting code with pytest run: | . ./activate - ./venv/bin/py.test tests/plotting/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + ./venv/bin/py.test tests/plotting/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor +# Omitted resource usage check # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme diff --git a/.github/workflows/build-test-ubuntu-pools.yml b/.github/workflows/build-test-ubuntu-pools.yml index dec387db50a0..da00076e8ef4 100644 --- a/.github/workflows/build-test-ubuntu-pools.yml +++ b/.github/workflows/build-test-ubuntu-pools.yml @@ -88,8 +88,9 @@ jobs: - name: Test pools code with pytest run: | . ./activate - ./venv/bin/py.test tests/pools/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + ./venv/bin/py.test tests/pools/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor +# Omitted resource usage check # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme diff --git a/.github/workflows/build-test-ubuntu-simulation.yml b/.github/workflows/build-test-ubuntu-simulation.yml index 645a121e08e8..5338a00c34b2 100644 --- a/.github/workflows/build-test-ubuntu-simulation.yml +++ b/.github/workflows/build-test-ubuntu-simulation.yml @@ -100,8 +100,9 @@ jobs: - name: Test simulation code with pytest run: | . ./activate - ./venv/bin/py.test tests/simulation/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + ./venv/bin/py.test tests/simulation/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor +# Omitted resource usage check # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme diff --git a/.github/workflows/build-test-ubuntu-tools.yml b/.github/workflows/build-test-ubuntu-tools.yml index 94b566cfe90e..22d7d2133aa8 100644 --- a/.github/workflows/build-test-ubuntu-tools.yml +++ b/.github/workflows/build-test-ubuntu-tools.yml @@ -88,8 +88,9 @@ jobs: - name: Test tools code with pytest run: | . ./activate - ./venv/bin/py.test tests/tools/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + ./venv/bin/py.test tests/tools/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor +# Omitted resource usage check # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme diff --git a/.github/workflows/build-test-ubuntu-util.yml b/.github/workflows/build-test-ubuntu-util.yml index b25e6145ab8d..62e097fcbedf 100644 --- a/.github/workflows/build-test-ubuntu-util.yml +++ b/.github/workflows/build-test-ubuntu-util.yml @@ -88,8 +88,9 @@ jobs: - name: Test util code with pytest run: | . ./activate - ./venv/bin/py.test tests/util/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + ./venv/bin/py.test tests/util/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor +# Omitted resource usage check # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme diff --git a/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml index ae92a9c7ee09..31fef9b66831 100644 --- a/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml @@ -88,8 +88,9 @@ jobs: - name: Test wallet-cat_wallet code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/cat_wallet/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + ./venv/bin/py.test tests/wallet/cat_wallet/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor +# Omitted resource usage check # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme diff --git a/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml index 8a30c642df06..52ac48a37a32 100644 --- a/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml @@ -88,8 +88,9 @@ jobs: - name: Test wallet-did_wallet code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/did_wallet/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + ./venv/bin/py.test tests/wallet/did_wallet/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor +# Omitted resource usage check # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme diff --git a/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml index 1354f84ed34c..edb05105a71b 100644 --- a/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml @@ -88,8 +88,9 @@ jobs: - name: Test wallet-rl_wallet code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/rl_wallet/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + ./venv/bin/py.test tests/wallet/rl_wallet/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor +# Omitted resource usage check # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme diff --git a/.github/workflows/build-test-ubuntu-wallet-rpc.yml b/.github/workflows/build-test-ubuntu-wallet-rpc.yml index 1aeb970a4ea1..45b8003d424d 100644 --- a/.github/workflows/build-test-ubuntu-wallet-rpc.yml +++ b/.github/workflows/build-test-ubuntu-wallet-rpc.yml @@ -88,8 +88,9 @@ jobs: - name: Test wallet-rpc code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/rpc/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + ./venv/bin/py.test tests/wallet/rpc/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor +# Omitted resource usage check # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme diff --git a/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml b/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml index 29ac1de6a4b2..b580bdd7e6a7 100644 --- a/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml +++ b/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml @@ -88,8 +88,9 @@ jobs: - name: Test wallet-simple_sync code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/simple_sync/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + ./venv/bin/py.test tests/wallet/simple_sync/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor +# Omitted resource usage check # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme diff --git a/.github/workflows/build-test-ubuntu-wallet-sync.yml b/.github/workflows/build-test-ubuntu-wallet-sync.yml index 06b8337b007c..e304d92947c0 100644 --- a/.github/workflows/build-test-ubuntu-wallet-sync.yml +++ b/.github/workflows/build-test-ubuntu-wallet-sync.yml @@ -88,8 +88,9 @@ jobs: - name: Test wallet-sync code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/sync/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + ./venv/bin/py.test tests/wallet/sync/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor +# Omitted resource usage check # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme diff --git a/.github/workflows/build-test-ubuntu-wallet.yml b/.github/workflows/build-test-ubuntu-wallet.yml index 1c0b5073a110..2d8258c8828e 100644 --- a/.github/workflows/build-test-ubuntu-wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet.yml @@ -88,8 +88,9 @@ jobs: - name: Test wallet code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + ./venv/bin/py.test tests/wallet/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor +# Omitted resource usage check # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme diff --git a/.github/workflows/build-test-ubuntu-weight_proof.yml b/.github/workflows/build-test-ubuntu-weight_proof.yml index ad4469649edf..0ae325bb7ad9 100644 --- a/.github/workflows/build-test-ubuntu-weight_proof.yml +++ b/.github/workflows/build-test-ubuntu-weight_proof.yml @@ -88,8 +88,9 @@ jobs: - name: Test weight_proof code with pytest run: | . ./activate - ./venv/bin/py.test tests/weight_proof/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + ./venv/bin/py.test tests/weight_proof/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor +# Omitted resource usage check # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme diff --git a/tests/build-workflows.py b/tests/build-workflows.py index 29f10a49bd85..b9c8e2c0d428 100755 --- a/tests/build-workflows.py +++ b/tests/build-workflows.py @@ -68,6 +68,10 @@ def generate_replacements(conf, dir): "CHECKOUT_TEST_BLOCKS_AND_PLOTS": read_file( Path(root_path / "runner_templates/checkout-test-plots.include.yml") ).rstrip(), + "CHECK_RESOURCE_USAGE": read_file( + Path(root_path / "runner_templates/check-resource-usage.include.yml") + ).rstrip(), + "DISABLE_PYTEST_MONITOR": "", "TEST_DIR": "", "TEST_NAME": "", "PYTEST_PARALLEL_ARGS": "", @@ -86,6 +90,9 @@ def generate_replacements(conf, dir): replacements["TEST_NAME"] = test_name(dir) if "test_name" in conf: replacements["TEST_NAME"] = conf["test_name"] + if not conf["check_resource_usage"]: + replacements["CHECK_RESOURCE_USAGE"] = "# Omitted resource usage check" + replacements["DISABLE_PYTEST_MONITOR"] = "-p no:monitor" for var in conf["custom_vars"]: replacements[var] = conf[var] if var in conf else "" return replacements @@ -134,6 +141,8 @@ def dir_path(string): conf = update_config(module_dict(testconfig), dir_config(dir)) replacements = generate_replacements(conf, dir) txt = transform_template(template_text, replacements) + # remove trailing whitespace from lines and assure a single EOF at EOL + txt = "\n".join(line.rstrip() for line in txt.rstrip().splitlines()) + "\n" logging.info(f"Writing {os}-{test_name(dir)}") workflow_yaml_path: Path = workflow_yaml_file(args.output_dir, os, test_name(dir)) if workflow_yaml_path not in current_workflows or current_workflows[workflow_yaml_path] != txt: diff --git a/tests/core/full_node/config.py b/tests/core/full_node/config.py index 54c7a57c3606..2cbb1c088328 100644 --- a/tests/core/full_node/config.py +++ b/tests/core/full_node/config.py @@ -1,9 +1,4 @@ # flake8: noqa: E501 parallel = True job_timeout = 50 -CHECK_RESOURCE_USAGE = """ - - name: Check resource usage - run: | - sqlite3 -readonly -separator " " .pymon "select item,cpu_usage,total_time,mem_usage from TEST_METRICS order by mem_usage desc;" >metrics.out - ./tests/check_pytest_monitor_output.py metrics.out - ./tests/check_pytest_monitor_output.py metrics.out + ./tests/check_pytest_monitor_output.py Date: Thu, 24 Mar 2022 09:45:32 -0700 Subject: [PATCH 252/378] Check for requesting items when creating an offer (#10864) --- chia/wallet/trading/offer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/chia/wallet/trading/offer.py b/chia/wallet/trading/offer.py index 2f8a31ecb44d..05d742f25c2a 100644 --- a/chia/wallet/trading/offer.py +++ b/chia/wallet/trading/offer.py @@ -94,6 +94,8 @@ def __post_init__(self): offered_coins: Dict[bytes32, List[Coin]] = self.get_offered_coins() if offered_coins == {}: raise ValueError("Bundle is not offering anything") + if self.get_requested_payments() == {}: + raise ValueError("Bundle is not requesting anything") # Verify that there are no duplicate payments for payments in self.requested_payments.values(): From 9c443624daad5967c81463128f68323840a0645c Mon Sep 17 00:00:00 2001 From: William Blanke Date: Thu, 24 Mar 2022 12:33:38 -0700 Subject: [PATCH 253/378] updated gui to 054d7b342e7c8284c9b58a775f87d393a1008bfe --- chia-blockchain-gui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia-blockchain-gui b/chia-blockchain-gui index 80e8bcb83c8d..054d7b342e7c 160000 --- a/chia-blockchain-gui +++ b/chia-blockchain-gui @@ -1 +1 @@ -Subproject commit 80e8bcb83c8dac3c2e37e40a7f701ad9842bb120 +Subproject commit 054d7b342e7c8284c9b58a775f87d393a1008bfe From 5c43950f933837d0f3d914c4632f9bfa5a04f43b Mon Sep 17 00:00:00 2001 From: Jeff Date: Thu, 24 Mar 2022 14:57:08 -0700 Subject: [PATCH 254/378] Added `-n`/`--new-address` option to `chia wallet get_address` (#10861) * Added `-n`/`--new-address` option to `chia wallet get_address` * Formatting fix * Complemented --new-address with --latest-address per feedback --- chia/cmds/wallet.py | 14 ++++++++++++-- chia/cmds/wallet_funcs.py | 3 ++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/chia/cmds/wallet.py b/chia/cmds/wallet.py index 2d0f85b7ef88..b2e93954051c 100644 --- a/chia/cmds/wallet.py +++ b/chia/cmds/wallet.py @@ -167,8 +167,18 @@ def show_cmd(wallet_rpc_port: Optional[int], fingerprint: int, wallet_type: Opti ) @click.option("-i", "--id", help="Id of the wallet to use", type=int, default=1, show_default=True, required=True) @click.option("-f", "--fingerprint", help="Set the fingerprint to specify which wallet to use", type=int) -def get_address_cmd(wallet_rpc_port: Optional[int], id, fingerprint: int) -> None: - extra_params = {"id": id} +@click.option( + "-n/-l", + "--new-address/--latest-address", + help=( + "Create a new wallet receive address, or show the most recently created wallet receive address" + " [default: show most recent address]" + ), + is_flag=True, + default=False, +) +def get_address_cmd(wallet_rpc_port: Optional[int], id, fingerprint: int, new_address: bool) -> None: + extra_params = {"id": id, "new_address": new_address} import asyncio from .wallet_funcs import execute_with_wallet, get_address diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index 2b957ccd9016..a50378a4d6f2 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -216,7 +216,8 @@ async def send(args: dict, wallet_client: WalletRpcClient, fingerprint: int) -> async def get_address(args: dict, wallet_client: WalletRpcClient, fingerprint: int) -> None: wallet_id = args["id"] - res = await wallet_client.get_next_address(wallet_id, False) + new_address: bool = args.get("new_address", False) + res = await wallet_client.get_next_address(wallet_id, new_address) print(res) From 58f8a4d2edc662e0e006a35ff2d5ae4607dab980 Mon Sep 17 00:00:00 2001 From: Jeff Date: Thu, 24 Mar 2022 17:40:33 -0700 Subject: [PATCH 255/378] Minor output formatting/enhancements for `chia wallet show` (#10863) * Minor output formatting/enhancements for `chia wallet show` * Updated format based on internal poll results * Linter fix and row rearrangement. --- chia/cmds/wallet_funcs.py | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index a50378a4d6f2..3f51051b50a0 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -483,17 +483,26 @@ async def print_balances(args: dict, wallet_client: WalletRpcClient, fingerprint else: print(f"Balances, fingerprint: {fingerprint}") for summary in summaries_response: + indent: str = " " + asset_id = summary["data"] wallet_id = summary["id"] balances = await wallet_client.get_wallet_balance(wallet_id) typ = WalletType(int(summary["type"])) address_prefix, scale = wallet_coin_unit(typ, address_prefix) - print(f"Wallet ID {wallet_id} type {typ.name} {summary['name']}") - print(f" -Total Balance: {print_balance(balances['confirmed_wallet_balance'], scale, address_prefix)}") - print( - f" -Pending Total Balance: " - f"{print_balance(balances['unconfirmed_wallet_balance'], scale, address_prefix)}" + total_balance: str = print_balance(balances["confirmed_wallet_balance"], scale, address_prefix) + unconfirmed_wallet_balance: str = print_balance( + balances["unconfirmed_wallet_balance"], scale, address_prefix ) - print(f" -Spendable: {print_balance(balances['spendable_balance'], scale, address_prefix)}") + spendable_balance: str = print_balance(balances["spendable_balance"], scale, address_prefix) + print() + print(f"{summary['name']}:") + print(f"{indent}{'-Total Balance:'.ljust(23)} {total_balance}") + print(f"{indent}{'-Pending Total Balance:'.ljust(23)} " f"{unconfirmed_wallet_balance}") + print(f"{indent}{'-Spendable:'.ljust(23)} {spendable_balance}") + print(f"{indent}{'-Type:'.ljust(23)} {typ.name}") + if len(asset_id) > 0: + print(f"{indent}{'-Asset ID:'.ljust(23)} {asset_id}") + print(f"{indent}{'-Wallet ID:'.ljust(23)} {wallet_id}") print(" ") trusted_peers: Dict = config["wallet"].get("trusted_peers", {}) @@ -513,9 +522,24 @@ async def get_wallet(wallet_client: WalletRpcClient, fingerprint: int = None) -> if fingerprint is not None: log_in_response = await wallet_client.log_in(fingerprint) else: + logged_in_fingerprint: Optional[int] = await wallet_client.get_logged_in_fingerprint() + spacing: str = " " if logged_in_fingerprint is not None else "" + current_sync_status: str = "" + if logged_in_fingerprint is not None: + if await wallet_client.get_synced(): + current_sync_status = "Synced" + elif await wallet_client.get_sync_status(): + current_sync_status = "Syncing" + else: + current_sync_status = "Not Synced" print("Choose wallet key:") for i, fp in enumerate(fingerprints): - print(f"{i+1}) {fp}") + row: str = f"{i+1}) " + row += "* " if fp == logged_in_fingerprint else spacing + row += f"{fp}" + if fp == logged_in_fingerprint and len(current_sync_status) > 0: + row += f" ({current_sync_status})" + print(row) val = None while val is None: val = input("Enter a number to pick or q to quit: ") From 292c26d3d86c46ce9ce3362adb9efd3189827451 Mon Sep 17 00:00:00 2001 From: Jeff Date: Fri, 25 Mar 2022 09:24:59 -0700 Subject: [PATCH 256/378] Hardcoded SSL test certs/keys (#10828) * Hardcoded SSL test certs/keys * Added a second set of certs/keys. Cert/key sets are infinitely cycled-through using get_next_private_ca_cert_and_key() and get_next_nodes_certs_and_keys() * More cert/key sets and a tool to generate them * Updated SSL generator to sign with the appropriate root CA. Fixed linter issues. * Linter fixes * Updated generate_ssl_for_nodes() based on feedback --- chia/cmds/init_funcs.py | 68 +++- tests/block_tools.py | 7 +- tests/util/gen_ssl_certs.py | 113 ++++++ tests/util/ssl_certs.py | 57 +++ tests/util/ssl_certs_1.py | 684 ++++++++++++++++++++++++++++++++++++ tests/util/ssl_certs_2.py | 684 ++++++++++++++++++++++++++++++++++++ tests/util/ssl_certs_3.py | 684 ++++++++++++++++++++++++++++++++++++ tests/util/ssl_certs_4.py | 684 ++++++++++++++++++++++++++++++++++++ tests/util/ssl_certs_5.py | 684 ++++++++++++++++++++++++++++++++++++ tests/util/ssl_certs_6.py | 684 ++++++++++++++++++++++++++++++++++++ tests/util/ssl_certs_7.py | 684 ++++++++++++++++++++++++++++++++++++ 11 files changed, 5013 insertions(+), 20 deletions(-) create mode 100644 tests/util/gen_ssl_certs.py create mode 100644 tests/util/ssl_certs.py create mode 100644 tests/util/ssl_certs_1.py create mode 100644 tests/util/ssl_certs_2.py create mode 100644 tests/util/ssl_certs_3.py create mode 100644 tests/util/ssl_certs_4.py create mode 100644 tests/util/ssl_certs_5.py create mode 100644 tests/util/ssl_certs_6.py create mode 100644 tests/util/ssl_certs_7.py diff --git a/chia/cmds/init_funcs.py b/chia/cmds/init_funcs.py index 4182c5be3a4b..f64eb4c2200f 100644 --- a/chia/cmds/init_funcs.py +++ b/chia/cmds/init_funcs.py @@ -44,8 +44,8 @@ ) from chia.cmds.configure import configure -private_node_names = {"full_node", "wallet", "farmer", "harvester", "timelord", "crawler", "daemon"} -public_node_names = {"full_node", "wallet", "farmer", "introducer", "timelord"} +private_node_names: List[str] = ["full_node", "wallet", "farmer", "harvester", "timelord", "crawler", "daemon"] +public_node_names: List[str] = ["full_node", "wallet", "farmer", "introducer", "timelord"] def dict_add_new_default(updated: Dict, default: Dict, do_not_migrate_keys: Dict[str, Any]): @@ -213,7 +213,12 @@ def migrate_from( return 1 -def create_all_ssl(root_path: Path): +def create_all_ssl( + root_path: Path, + *, + private_ca_crt_and_key: Optional[Tuple[bytes, bytes]] = None, + node_certs_and_keys: Optional[Dict[str, Dict]] = None, +): # remove old key and crt config_dir = root_path / "config" old_key_path = config_dir / "trusted.key" @@ -236,6 +241,11 @@ def create_all_ssl(root_path: Path): chia_ca_key_path = ca_dir / "chia_ca.key" write_ssl_cert_and_key(chia_ca_crt_path, chia_ca_crt, chia_ca_key_path, chia_ca_key) + # If Private CA crt/key are passed-in, write them out + if private_ca_crt_and_key is not None: + private_ca_crt, private_ca_key = private_ca_crt_and_key + write_ssl_cert_and_key(private_ca_crt_path, private_ca_crt, private_ca_key_path, private_ca_key) + if not private_ca_key_path.exists() or not private_ca_crt_path.exists(): # Create private CA print(f"Can't find private CA, creating a new one in {root_path} to generate TLS certificates") @@ -243,33 +253,53 @@ def create_all_ssl(root_path: Path): # Create private certs for each node ca_key = private_ca_key_path.read_bytes() ca_crt = private_ca_crt_path.read_bytes() - generate_ssl_for_nodes(ssl_dir, ca_crt, ca_key, True) + generate_ssl_for_nodes( + ssl_dir, ca_crt, ca_key, prefix="private", nodes=private_node_names, node_certs_and_keys=node_certs_and_keys + ) else: # This is entered when user copied over private CA print(f"Found private CA in {root_path}, using it to generate TLS certificates") ca_key = private_ca_key_path.read_bytes() ca_crt = private_ca_crt_path.read_bytes() - generate_ssl_for_nodes(ssl_dir, ca_crt, ca_key, True) + generate_ssl_for_nodes( + ssl_dir, ca_crt, ca_key, prefix="private", nodes=private_node_names, node_certs_and_keys=node_certs_and_keys + ) chia_ca_crt, chia_ca_key = get_chia_ca_crt_key() - generate_ssl_for_nodes(ssl_dir, chia_ca_crt, chia_ca_key, False, overwrite=False) - - -def generate_ssl_for_nodes(ssl_dir: Path, ca_crt: bytes, ca_key: bytes, private: bool, overwrite=True): - if private: - names = private_node_names - else: - names = public_node_names - - for node_name in names: + generate_ssl_for_nodes( + ssl_dir, + chia_ca_crt, + chia_ca_key, + prefix="public", + nodes=public_node_names, + overwrite=False, + node_certs_and_keys=node_certs_and_keys, + ) + + +def generate_ssl_for_nodes( + ssl_dir: Path, + ca_crt: bytes, + ca_key: bytes, + *, + prefix: str, + nodes: List[str], + overwrite: bool = True, + node_certs_and_keys: Optional[Dict[str, Dict]] = None, +): + for node_name in nodes: node_dir = ssl_dir / node_name ensure_ssl_dirs([node_dir]) - if private: - prefix = "private" - else: - prefix = "public" key_path = node_dir / f"{prefix}_{node_name}.key" crt_path = node_dir / f"{prefix}_{node_name}.crt" + if node_certs_and_keys is not None: + certs_and_keys = node_certs_and_keys.get(node_name, {}).get(prefix, {}) + crt = certs_and_keys.get("crt", None) + key = certs_and_keys.get("key", None) + if crt is not None and key is not None: + write_ssl_cert_and_key(crt_path, crt, key_path, key) + continue + if key_path.exists() and crt_path.exists() and overwrite is False: continue generate_ca_signed_cert(ca_crt, ca_key, crt_path, key_path) diff --git a/tests/block_tools.py b/tests/block_tools.py index 63b80b8fb9ae..b3f415a4f902 100644 --- a/tests/block_tools.py +++ b/tests/block_tools.py @@ -86,6 +86,7 @@ from tests.time_out_assert import time_out_assert from tests.wallet_tools import WalletTool from tests.util.socket import find_available_listen_port +from tests.util.ssl_certs import get_next_nodes_certs_and_keys, get_next_private_ca_cert_and_key from chia.wallet.derive_keys import ( master_sk_to_farmer_sk, master_sk_to_local_sk, @@ -144,7 +145,11 @@ def __init__( self.local_keychain = keychain create_default_chia_config(root_path) - create_all_ssl(root_path) + create_all_ssl( + root_path, + private_ca_crt_and_key=get_next_private_ca_cert_and_key(), + node_certs_and_keys=get_next_nodes_certs_and_keys(), + ) self.local_sk_cache: Dict[bytes32, Tuple[PrivateKey, Any]] = {} self._config = load_config(self.root_path, "config.yaml") diff --git a/tests/util/gen_ssl_certs.py b/tests/util/gen_ssl_certs.py new file mode 100644 index 000000000000..753114414c33 --- /dev/null +++ b/tests/util/gen_ssl_certs.py @@ -0,0 +1,113 @@ +from pathlib import Path +from typing import Optional + +import click +from pytest import MonkeyPatch + +from chia.ssl.create_ssl import generate_ca_signed_cert, get_chia_ca_crt_key, make_ca_cert + +# NOTE: This is a standalone tool that can be used to generate a CA cert/key as well as node certs/keys. + + +@click.command() +@click.option( + "--suffix", + type=str, + default="", + help="Suffix to append to the generated cert/key symbols.", + required=True, +) +def gen_ssl(suffix: str = "") -> None: + captured_crt: Optional[bytes] = None + captured_key: Optional[bytes] = None + capture_cert_and_key = False + + def patched_write_ssl_cert_and_key(cert_path: Path, cert_data: bytes, key_path: Path, key_data: bytes) -> None: + nonlocal capture_cert_and_key, captured_crt, captured_key + + if capture_cert_and_key: + captured_crt = cert_data + captured_key = key_data + + print(f"{cert_path} = b\"\"\"{cert_data.decode(encoding='utf8')}\"\"\"") + print() + print(f"{key_path} = b\"\"\"{key_data.decode(encoding='utf8')}\"\"\"") + print() + + patch = MonkeyPatch() + patch.setattr("chia.ssl.create_ssl.write_ssl_cert_and_key", patched_write_ssl_cert_and_key) + + private_ca_crt: Optional[bytes] = None + private_ca_key: Optional[bytes] = None + capture_cert_and_key = True + + print("from typing import Dict, Tuple") + print() + + make_ca_cert(Path("SSL_TEST_PRIVATE_CA_CRT"), Path("SSL_TEST_PRIVATE_CA_KEY")) + + capture_cert_and_key = False + private_ca_crt = captured_crt + private_ca_key = captured_key + + node_certs_and_keys = { + "full_node": { + "private": {"crt": "SSL_TEST_FULLNODE_PRIVATE_CRT", "key": "SSL_TEST_FULLNODE_PRIVATE_KEY"}, + "public": {"crt": "SSL_TEST_FULLNODE_PUBLIC_CRT", "key": "SSL_TEST_FULLNODE_PUBLIC_KEY"}, + }, + "wallet": { + "private": {"crt": "SSL_TEST_WALLET_PRIVATE_CRT", "key": "SSL_TEST_WALLET_PRIVATE_KEY"}, + "public": {"crt": "SSL_TEST_WALLET_PUBLIC_CRT", "key": "SSL_TEST_WALLET_PUBLIC_KEY"}, + }, + "farmer": { + "private": {"crt": "SSL_TEST_FARMER_PRIVATE_CRT", "key": "SSL_TEST_FARMER_PRIVATE_KEY"}, + "public": {"crt": "SSL_TEST_FARMER_PUBLIC_CRT", "key": "SSL_TEST_FARMER_PUBLIC_KEY"}, + }, + "harvester": {"private": {"crt": "SSL_TEST_HARVESTER_PRIVATE_CRT", "key": "SSL_TEST_HARVESTER_PRIVATE_KEY"}}, + "timelord": { + "private": {"crt": "SSL_TEST_TIMELORD_PRIVATE_CRT", "key": "SSL_TEST_TIMELORD_PRIVATE_KEY"}, + "public": {"crt": "SSL_TEST_TIMELORD_PUBLIC_CRT", "key": "SSL_TEST_TIMELORD_PUBLIC_KEY"}, + }, + "crawler": {"private": {"crt": "SSL_TEST_CRAWLER_PRIVATE_CRT", "key": "SSL_TEST_CRAWLER_PRIVATE_KEY"}}, + "daemon": {"private": {"crt": "SSL_TEST_DAEMON_PRIVATE_CRT", "key": "SSL_TEST_DAEMON_PRIVATE_KEY"}}, + "introducer": { + "public": {"crt": "SSL_TEST_INTRODUCER_PUBLIC_CRT", "key": "SSL_TEST_INTRODUCER_PUBLIC_KEY"}, + }, + } + + chia_ca_crt, chia_ca_key = get_chia_ca_crt_key() + + for node_name, cert_type_dict in node_certs_and_keys.items(): + for cert_type, cert_dict in cert_type_dict.items(): + crt = cert_dict["crt"] + key = cert_dict["key"] + ca_crt = chia_ca_crt if cert_type == "public" else private_ca_crt + ca_key = chia_ca_key if cert_type == "public" else private_ca_key + + generate_ca_signed_cert(ca_crt, ca_key, Path(crt), Path(key)) + + patch.undo() + + append_str = "" if suffix == "" else f"_{suffix}" + print( + f"SSL_TEST_PRIVATE_CA_CERT_AND_KEY{append_str}: Tuple[bytes, bytes] = " + "(SSL_TEST_PRIVATE_CA_CRT, SSL_TEST_PRIVATE_CA_KEY)" + ) + print() + print(f"SSL_TEST_NODE_CERTS_AND_KEYS{append_str}: Dict[str, Dict[str, Dict[str, bytes]]] = {{") + for node_name, cert_type_dict in node_certs_and_keys.items(): + print(f' "{node_name}": {{') + for cert_type, cert_dict in cert_type_dict.items(): + crt = cert_dict["crt"] + key = cert_dict["key"] + print(f' "{cert_type}": {{"crt": {crt}, "key": {key}}},') + print(" },") + print("}") + + +def main() -> None: + gen_ssl() + + +if __name__ == "__main__": + main() diff --git a/tests/util/ssl_certs.py b/tests/util/ssl_certs.py new file mode 100644 index 000000000000..3f3637272964 --- /dev/null +++ b/tests/util/ssl_certs.py @@ -0,0 +1,57 @@ +import itertools +from typing import Dict, List, Tuple + +from tests.util.ssl_certs_1 import SSL_TEST_NODE_CERTS_AND_KEYS_1, SSL_TEST_PRIVATE_CA_CERT_AND_KEY_1 +from tests.util.ssl_certs_2 import SSL_TEST_NODE_CERTS_AND_KEYS_2, SSL_TEST_PRIVATE_CA_CERT_AND_KEY_2 +from tests.util.ssl_certs_3 import SSL_TEST_NODE_CERTS_AND_KEYS_3, SSL_TEST_PRIVATE_CA_CERT_AND_KEY_3 +from tests.util.ssl_certs_4 import SSL_TEST_NODE_CERTS_AND_KEYS_4, SSL_TEST_PRIVATE_CA_CERT_AND_KEY_4 +from tests.util.ssl_certs_5 import SSL_TEST_NODE_CERTS_AND_KEYS_5, SSL_TEST_PRIVATE_CA_CERT_AND_KEY_5 +from tests.util.ssl_certs_6 import SSL_TEST_NODE_CERTS_AND_KEYS_6, SSL_TEST_PRIVATE_CA_CERT_AND_KEY_6 +from tests.util.ssl_certs_7 import SSL_TEST_NODE_CERTS_AND_KEYS_7, SSL_TEST_PRIVATE_CA_CERT_AND_KEY_7 + +# --------------------------------------------------------------------------- +# Private CA certs/keys +# --------------------------------------------------------------------------- + +SSL_TEST_PRIVATE_CA_CERTS_AND_KEYS: List[Tuple[bytes, bytes]] = [ + SSL_TEST_PRIVATE_CA_CERT_AND_KEY_1, + SSL_TEST_PRIVATE_CA_CERT_AND_KEY_2, + SSL_TEST_PRIVATE_CA_CERT_AND_KEY_3, + SSL_TEST_PRIVATE_CA_CERT_AND_KEY_4, + SSL_TEST_PRIVATE_CA_CERT_AND_KEY_5, + SSL_TEST_PRIVATE_CA_CERT_AND_KEY_6, + SSL_TEST_PRIVATE_CA_CERT_AND_KEY_7, +] + +# --------------------------------------------------------------------------- +# Node -> cert/key mappings +# --------------------------------------------------------------------------- + +SSL_TEST_NODE_CERTS_AND_KEYS: List[Dict[str, Dict[str, Dict[str, bytes]]]] = [ + SSL_TEST_NODE_CERTS_AND_KEYS_1, + SSL_TEST_NODE_CERTS_AND_KEYS_2, + SSL_TEST_NODE_CERTS_AND_KEYS_3, + SSL_TEST_NODE_CERTS_AND_KEYS_4, + SSL_TEST_NODE_CERTS_AND_KEYS_5, + SSL_TEST_NODE_CERTS_AND_KEYS_6, + SSL_TEST_NODE_CERTS_AND_KEYS_7, +] + + +ssl_test_private_ca_certs_and_keys_gen = ( + SSL_TEST_PRIVATE_CA_CERTS_AND_KEYS[idx] + for idx in itertools.cycle([*range(len(SSL_TEST_PRIVATE_CA_CERTS_AND_KEYS))]) +) + + +def get_next_private_ca_cert_and_key() -> Tuple[bytes, bytes]: + return next(ssl_test_private_ca_certs_and_keys_gen) # type: ignore[no-any-return] + + +ssl_test_certs_and_keys_gen = ( + SSL_TEST_NODE_CERTS_AND_KEYS[idx] for idx in itertools.cycle([*range(len(SSL_TEST_NODE_CERTS_AND_KEYS))]) +) + + +def get_next_nodes_certs_and_keys() -> Dict[str, Dict[str, Dict[str, bytes]]]: + return next(ssl_test_certs_and_keys_gen) diff --git a/tests/util/ssl_certs_1.py b/tests/util/ssl_certs_1.py new file mode 100644 index 000000000000..0c2c899fdf40 --- /dev/null +++ b/tests/util/ssl_certs_1.py @@ -0,0 +1,684 @@ +from typing import Dict, Tuple + +SSL_TEST_PRIVATE_CA_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDKTCCAhGgAwIBAgIUZnoqyaLGQMl08azdwpafGGfEGR0wDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMB4XDTIyMDMyMzE3MjkyMFoXDTMyMDMy +MDE3MjkyMFowRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8G +A1UECwwYT3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAmal1SzZpSVae9RShUcN1MjM/B8rJD47K0c49Uxj/YUD7 +oGuSKWpYw/nM7aylODTqfJt660KN+Q5AeiZemKZ+YfbiQJ+1YUZazTcjoqOghXFl +E70KM6xyOiTr1SBSS1zf161BHPvTmbBQpkqsUStSZISBKaqU4lhJnBsllei8eFFN +mHEO+bgGQbTnjVWwHS+xgdajMiemK+Cql+pIpaoduTTtNyfwp9m2wF45EUT4DyUu +cU8En3NOOrarkPPG7x+FEANo0/UrqJwozY91Qsv6EPkhFUbBvF8l/p7uNCK0CIBK +yTOoPkeHvEeWX+LZlbICpdJQXIdiZJAqIu56yOPqAQIDAQABoxMwETAPBgNVHRMB +Af8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBiRATAkJaW55BPdZ6TKMYfSFlJ +9lO3K0eS6miuX6HG7OI6DHCBf18fjDLxg7O/HeVxSgI5txB4iEulRBcv35veDti+ +R6PuebE1+g0QTMlw0WXlRMg+6bVaV2XwXoWQhhSnDexdQtcDPKd4xC6VGfK3sPjQ +ZSriMmrerVhGL9d1pYBtxolDbk9e/AwrnWKLPUDrR8W5DA+yJ/lL+k5pPrACNR82 +cljDU3YA+bE+9LpNtfatPZwceaM7GupXJy9lngwlNAM42XFvWsTjH8Uy7ZJFOi0B ++3rLUOIXxCdpb88qeUGBN6BAUMhItoflSktN813vgKkiV7I0gNoU3XnUqG7w +-----END CERTIFICATE----- +""" + +SSL_TEST_PRIVATE_CA_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAmal1SzZpSVae9RShUcN1MjM/B8rJD47K0c49Uxj/YUD7oGuS +KWpYw/nM7aylODTqfJt660KN+Q5AeiZemKZ+YfbiQJ+1YUZazTcjoqOghXFlE70K +M6xyOiTr1SBSS1zf161BHPvTmbBQpkqsUStSZISBKaqU4lhJnBsllei8eFFNmHEO ++bgGQbTnjVWwHS+xgdajMiemK+Cql+pIpaoduTTtNyfwp9m2wF45EUT4DyUucU8E +n3NOOrarkPPG7x+FEANo0/UrqJwozY91Qsv6EPkhFUbBvF8l/p7uNCK0CIBKyTOo +PkeHvEeWX+LZlbICpdJQXIdiZJAqIu56yOPqAQIDAQABAoIBABWbvuLUw/mMNM5C +GG1nDxQAINz3p06Ixfy7A+Srnz4N5VSpy+QHEHR+rFK/9Hvy9QaQ1rg+o7hiSK7k +tmjBAQTFswtjah5DxoEVP+2fFPOu/ofIDac2mNmUV5Wg9fGjHdc2hfGNeDQklzLL +TXAcp3l7KK6zTjyGLdPF/YMXN2mzzWa5Z/D66q9xXAIZnY0ggj4wJ2oOJvbq5Vec +1/NaXZcKZ6uS8a7AJ94133ItmxeBxJASgy+yF2T1YPOq0lK7OpU4qrG66iHAtB+z +LKao6dXiYZG5uuJ1j30Gk2V/QkT+NJSDnNcliK5pmmjUKHrDi8ow9m+dl7AjQTlj +CBw7AAECgYEAzH17mJR2/eO5MHGhz53QI+R9fcb7DiDT7o3M/rj8VK+sk5o5hqQn +D+5Ryy9H3UVTeXwDIayM23cpEmJFCBZ4cRnz+T11ZZ6P9RMIbfPxO6ooLFciHyHj +MGvOGdH14Mj9asBa8pSthlPNF9zHQ2OuoS4VBclPrmiQFGRfQBcq/FECgYEAwF5T +Rv7z0dttLMo2jaTCroorqD8FApGqK/hGol0PF6ozDXJqC77CBfocfoxK9VAg7Vn9 +IRwWQFiIGdHpPuoN0bn4U75X43cUe7eodpck88Sy+KDwurWVPQ6ltO/hDcvxRqAs +rAfCAqG/lRx7Gx08Fmtim3KpRyiV0yvGTH4UlrECgYEAqgouHFJYIAacl4vl4Z54 +1V/Keixb1wO1N0jyjV2FdWYfOx2jeDJHyReDLFHEkFp1by+P6xBwkI4luQO+I1uM +C4BpP3e7hySy0DdjawrOLa7weO57kSe8oycB2racnq6DC6Gn/s9i+6/ze0Q67e6V +57FKCeW8PGr2Y/6StdiOBgECgYEAoNo+qE7nccMZNyqfEzGB2JCQkM6hUdSbhsjD +x2ApDpCyv7u8ELYhZv4MdYS56QZnghCNKPJjaMqeg3iSoJj1lTj7/Ipc00bvScP4 +ibE4pN0FCgEJShYsBDILPZCXjwHJblThBbg28hxuJjD6f2GirNx+R51JDsdRAJLJ +7Yw/iwECgYEAqNgShZvjTVsFz1hRKTPfJbhJZseqquN+xypJMZyoYJHNF5Urk5YE +ByHQAzAIcMYzsSvqGQbDK22UVP/I3aqcz9JAf1sl6pr+KGRPeIumVOPssKMIhP+x +ivPcyK9bCjrJiirx+TSiRy1698pdp+dDwFhcaHRBFf6FJgxwFHgM9Gw= +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_FULLNODE_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUT5GDbvyrLH9Vkb7sFWPi1uFfhnEwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyMFoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQC8vFVDrrbPlVphmwXld9chzAuCt6cUwFYBTxaIW01Ex5Ft +sJvGti6ve5BFx+B+wwA19Y1t6QOOnOn56x0Phm5/NqYidqYCBcrrHK28OH3kBhiO +NZiJi+tybGhxg2kjZldPBmoVA2r9MU1qA0PrRpozYi1mH8W2f/7yCstAHaYxZcjn +T70KkjWc7jMpV2g/6wLUaKJEgryQWqMr1Ru6BR2Q7IVIe688ptO53JRutPj/Fh6t +J9jWyF7b0EwoYc0Un2lRrzYgGQ0gGXGJCax3pWjZAokvVG5PQEPqK9TXqWVj/J+c +aA9jPEQcfivNW0nU8LmKHZyOaG/q39Y1wd9EhfOtAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQAvyyCobTP0LbxqcCcNhY+3 +/GPpUfYgjZdRy4uRtSLjcY3v604Azyuv07rMlT8qkvyYTuTzGXqpqVN1XJnllklH +psdkdA4uhlzx2ewhZw6usGV8QZEcOl6kMUeZK7TDMRkE10EnH2Qzg7R24coy9cEr +anGHgti6QrTK62aNT7aBGIDmteI3BRXc8Pb3BClDKLPpOC8DwRPD7nmQ/0awF3nN +B7caCwX4NCUuL1/Kbpz5Nt8wZepCcnXwF8BrSsiBFoUI01tGTm4BQVc1mufCmAlX +G8mCVW2JuymC6yia/bAUbvFhowbUrX55/z8SVEKQTLRQK2CglORjOTko0UbUcgN+ +-----END CERTIFICATE----- +""" + +SSL_TEST_FULLNODE_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAvLxVQ662z5VaYZsF5XfXIcwLgrenFMBWAU8WiFtNRMeRbbCb +xrYur3uQRcfgfsMANfWNbekDjpzp+esdD4ZufzamInamAgXK6xytvDh95AYYjjWY +iYvrcmxocYNpI2ZXTwZqFQNq/TFNagND60aaM2ItZh/Ftn/+8grLQB2mMWXI50+9 +CpI1nO4zKVdoP+sC1GiiRIK8kFqjK9UbugUdkOyFSHuvPKbTudyUbrT4/xYerSfY +1she29BMKGHNFJ9pUa82IBkNIBlxiQmsd6Vo2QKJL1RuT0BD6ivU16llY/yfnGgP +YzxEHH4rzVtJ1PC5ih2cjmhv6t/WNcHfRIXzrQIDAQABAoIBAFnEbf2CJPs4u0M/ +W5+Xz2AIz9S9ix+Il5+JwVrbqjWzgg0c+gqabjwS1j0KY1GHaBtCDqGfOYzkPzka +RbkzpGynTn+H1U+S97+55Txn1iDVcWp6PXH2deb3fvm2mhQ3QgGZOG2EMaf5giuR +IAXQj9kusg8nv38dA+KVlbSKJZjKUXVYrlpixfpzz9Z/CuhouRundF/yNBVbSnzR +v70ywjVGykf0YD6y5ZDW3BRAg7lRJ/fF1bK3L/sbLj4BugMs9gjt+cMRcusJdoP6 +nWk5Z6Mo5VdZspb351EcpNs1AbTb40MaqNQuNg9jIgs9rZd5ZOMR0a77QufSMzML +qNFBQqECgYEA9AmL1/goSocUEUYRiioMrMkm0OA5/xwrpp8lcOYUVAhL2VD+lFNa +V4htxyUOuE1czQPXSmfioxQrbfHm0uxPMYUDWygdc/zyOlRbvMqmgimNFqRBiGh/ +85NuYMSf6AOe7i+hwqZu/ZLDlzA7mEQG0csVLkPphe5HEBu0d5Wu1LUCgYEAxfzM +0xXeyocyuJblLOQxSfnwonG9d8F4hEO42lCEEDh8zVIk23vJ5E72YPwuiRU49Zrp +PoKiM9TjzsmnQIRany7BmNjtuhlvMirwIte0Helc0ks4IpzjymWSl12msOE4Jssl +6cy3xcX+E+JE8rVcrxDu2aAOiBC1fmLLQJlhNhkCgYAzbWuWCMrc4dh9x2lc132y +T/WpIQe31kTwqSsnvqTcDJ+HXYU41tP8DFkuFYYjmtIKtluBZ6EgQtjgI5FEM96A +jgpmBG8oiU62sh5fC8nJNl5wPg89YuBMAW7KX8VfDJxKj6kkLxTGxU4Ip3Z3oSZa +wdRl2pP00IETSPNgHCAq3QKBgFWw1cHirFvB6k0EWkp0tXSMLf9Q9S042n18hixP +Pul6WWHQVM1+JWKgXniZjVadjdqXYq5Agg2m7bZZhv8gicxtwzLxaOrsCTmQZgDe +lUGA+EC4d6JbfyfhkHHdAcF6qP/5Wv53MW7zA8X9X9QgdO38iTQ91yxC9xqtjcT2 +3aE5AoGBAJ7clDveQugUC+OoXgru0emb3sXRk8kXDqUTjUlsjU1m4f3D8ZIx7+E6 +4xVBExXHddMx9QYiJ5uVkcNB6UxhppqlsJPoKyj8dvDTYeC98Zm/TFl0B/P4iY8D +3w/REN/c8t2oYOpy8/0kgp2szliDeQDxqiijo9LeRRmxvTWYAJMI +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_FULLNODE_PUBLIC_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUBV+nxo/NCVdtZFyXlpmq8HplFQQwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyMFoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDXf+htQ1YfVlp2jqHJ+EUmB6bhxE1tYl5i2VaZVanzjYgA +9LDKeRhvUnaOWvLkGQbyzqvnAylXHXSNe9KAZrphoxDP40Ogu4IEGatxkAKafJ0s +KKAB3QwJlnOm2NJNuH9vOirw66ihkpz6Ydg7hmdWCF7bJgG82qwzQvEtIABUxZeq +EMJHLuTlVC5fyuIyggzzU1h+ss3bleM6oqT54yxJPoZYmvy067jeLGiyPg95EP+s +nuxLFhzeW1OZHMGmniDvO0zKGr9chiXKUSgugtT3MHUndociVCmc2OQaqjmWKwGr +O0mU5+lMgP2SojC7wpItyHSfn62YTLQ5VduDd5+fAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQBeYWzDc79lXdOi2iopi50n +Hoc+91TiUr9Ubbd8lHWhidHcRYYWo1vFwM3of3eJ4rhAKPgwx9aclkryUEcZ05U9 +tzs5OrmYsgnzdoX+zJenx/7NwuMO7Oe+XBAd16/4vGgSU9vPge1Ny4zN+zA1vn2V +zVlSBuGzmJI1ugPOVK9e0XAzJNsG5/pPRB29mbWWrrmFBmHvf33+PJms19aSfKrV +lJXYQV4Q9/xt5ehTPo20HF2r2CDiEjhV+P9QVRnwymuE/Zap+e1v+N+iWJVUB9hR +il4RskorFB1oY+FcNvDlsfO2bl29hGX3nNBQ0o8k1fjvuLz55MUNyGTeeqBGKkU2 +-----END CERTIFICATE----- +""" + +SSL_TEST_FULLNODE_PUBLIC_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA13/obUNWH1Zado6hyfhFJgem4cRNbWJeYtlWmVWp842IAPSw +ynkYb1J2jlry5BkG8s6r5wMpVx10jXvSgGa6YaMQz+NDoLuCBBmrcZACmnydLCig +Ad0MCZZzptjSTbh/bzoq8OuooZKc+mHYO4ZnVghe2yYBvNqsM0LxLSAAVMWXqhDC +Ry7k5VQuX8riMoIM81NYfrLN25XjOqKk+eMsST6GWJr8tOu43ixosj4PeRD/rJ7s +SxYc3ltTmRzBpp4g7ztMyhq/XIYlylEoLoLU9zB1J3aHIlQpnNjkGqo5lisBqztJ +lOfpTID9kqIwu8KSLch0n5+tmEy0OVXbg3efnwIDAQABAoIBADq0WF+zcTmWL4yO +bFp1rHigqwBjlmgO3QF2jVW19Vconf0Mq0Bs3pAs2akL85DZlH/+duu2e8OEfaSx +L3XVBj7kygantVuK2O8/AuorvdnRyosmAfif+9B80MKJ3DhZ4zUsllgNCmIBa4v1 +rY8BnRLdsuFmKCEHPNO2D7coOY7dyIc+MHF249nKyPYMjfeyKyALqF5EcQPL/2Kp +lSaR6dJoQyMo/f+IvF3r2UCfLMWWpK2MDuLpSvAISy+GF4OtL4wBZ+R3BG8p2dw2 +tesFIVLfKYafpZiVQz6Pa1CIQvp150EFtilZwSbJByK5qyubKTOT1u4V+ANF4gii +Sfh9FxECgYEA/gjYjm28swTPlh+7s4BUSbWZMJ5FgYjFuGHaRKQbqD8kyfEIMie9 +Pu2iOaS5Ym+0Z/cI7mqmepQdfkL5NfkosHZVA0KR4P1QhrfcnXbIinfYDlPaG4Ee +syWkDYIJDawTWDMQvBnGlyAi3eO7nUJoWke5ozHg9VxRWe1NWDNqFtcCgYEA2Sq8 +3DK0H16YMztn7aKIWMS/17E11F8hZaSWiRXTNgdnTcye64Y+wLzfQaw/nLFw+rSO +qHjcTK333l90l0gKflJBePORhEZ7/5p/o3xOCdsRKH8itH+21aq/K+p4XmvodgVd +tnY93QcRid6U7e/ZDW3od6pjMLllJNUkKjJtTHkCgYA3p4x6N8R9m/I2u6ENxHGy +7FwxcJtds69No/KD00hT5fGTZIEdK+OkI7/EzTfoPvzRQifsw+TROh30CYw7rOij +MGmTm0QGfLjlquZkgR+SZospKGnCWNl2+iok43ZEToy2aAmkjCkb9uhsoHX8EA54 +qPocrNLSLnWMNBcb2bfGZQKBgGuUwm6LJ7QKlnd6zGdqhwUCos7lWPdWESNbP7+1 +ciZn0IM4BNpEbL3qUucjv3eOZ5uq6qkHBott+0bMHuP6qkgd05VphAL1L+RV0zlY +EQjM31kicjzcr+R1a7MDupF7/3LIAb6sIMVoBQY5n6mnke0XL3xoii7PCQ4QEJRe +2spxAoGBAIWBbVLqYSzFZzzmxhPdGi8tNLgj3idkVAO+Ovuwf4RLiulatJcebI5A +HAuJZY7XMzGKsrTX0ZY/j5uYuParAy0kzyBpXlh4FvxswFC+R2GiAMXXJCqXL2N9 +tbM0dgomIxudh/4gfrCUid4mNEtYALqorXhMwJLPDaGZ1KbyytSw +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_WALLET_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUXxPjAJgu/kDoq+EOobX/wSYZjW4wDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyMFoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQCzYrIdaNeAReJVfaMXpHtY1JgjpAAP3XipZfrmRj0SY4pS +zHfEDSZByX7LLwde8B2ivQHDDmmIgDhL9TZ3pZlYmk2Vq0BQHJMciaI/fzF1+FNQ +imkccmvsC+LXft3SMG61ldFszMkVWGOLdflBzBudGa7z034czq3/Ucj2mE4jEsPI +OxnFH06DBadMDtGfaiyFPvEc1Ywr+LdJQ24LVrih1IZ2dmQtS40n2K77UUsvUs3s +jNx27qISfUI/tspa8DnKujuY/9x9AIzo5ldq8N20Ccns6kXxSewsxX1vH2uNH9RP +ReReTFsZU3z8xicFx8SBE4VcRXXTTsqm974QMYJlAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQA52jQ4+CoZILaTRP3mFYpg +1OmD/bwzBsHbDZhB1co6d6lWel07fj3uNekBigyHgnciMGvXm2wSJIxEvsMoXIPh +yyBjdLb8IwaZRldnLPewUuTuA6WQx6eNwA+qEn6vT9tBVLMO0pRNXy1201c7oeCj +QCJQzn+tZweNdGuX06xgPX5q196gFS+BZDw/sn3ctQ/xCXC6gno21NRqyM2VdSCM +UIzCBQLoL9DVvpmC0atV9BO7LTvOYlEuCF58IXpBBi7P+3i1Q2z4SLamsKKcU1Pp +dJkK0lA+FfIYEGsUV09ddxGXRghU7YV0kU4iPMRs/ckin95kPiUOQx4lLo3N6u8+ +-----END CERTIFICATE----- +""" + +SSL_TEST_WALLET_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAs2KyHWjXgEXiVX2jF6R7WNSYI6QAD914qWX65kY9EmOKUsx3 +xA0mQcl+yy8HXvAdor0Bww5piIA4S/U2d6WZWJpNlatAUByTHImiP38xdfhTUIpp +HHJr7Avi137d0jButZXRbMzJFVhji3X5QcwbnRmu89N+HM6t/1HI9phOIxLDyDsZ +xR9OgwWnTA7Rn2oshT7xHNWMK/i3SUNuC1a4odSGdnZkLUuNJ9iu+1FLL1LN7Izc +du6iEn1CP7bKWvA5yro7mP/cfQCM6OZXavDdtAnJ7OpF8UnsLMV9bx9rjR/UT0Xk +XkxbGVN8/MYnBcfEgROFXEV1007Kpve+EDGCZQIDAQABAoIBAE9OfQx/g3bUbqH5 +L5eOQnIdWz22jch105ig23He77UniMneV7y3S4ieOo49tnaElbWS9ip0Prf4Z+s8 +992hus/vOAnJcl94opllR/PmmclcBgl/h8Tp2Ui8YIeBMTRx8SAaokIFr4jeUPQh +Lhem0zZ7Wlu1zvWRcl+EmuJap4DdVu3jpbddccJ00nfuw+GDgynhbJjRBmrS+2f+ +a49qfH2HSoxDEHeAJuLap4b7svvyGl1G685aDGgx/mqsHxPJXiavFYQaVY1MBUiq +NMx8NUZtxFahZIsj4ScuzOx6mVhDudMlqtfBZ0c4ae0OImWqWDRyx6kJOA++5RNE +0gLxuukCgYEA5qoghL0ZLJk8UIyBI1/XECK1DP0cbOVTPSOuYBu+9ur36BhS9XPG +Ut4M3YAYLccfvV9wqolZz3811kT1E0vqWHNgyZ7I2VWP68rW1+k+F7vZld0ThXrm +T8QbH8IFGjGwPEJ/BE2OYp6r6wAaAQ2Lr8r08b0XegZLbYX+eoX9c9MCgYEAxxax +0G6uqy67z/b5/OrP6S3/XSIGEYwCrAqHikDBX22QeTop5Medzf3MFHmxS7VOYZ0X +Tuo5oieSE5DHY8Ih6DJ1hc+8LyP2aVd144A0f5SIQFJxfIxMJCp2w/9rKgyYlwRs +RqsdJ75a3jBpo7NhwbQhr4+xKdxTUWBKNBRtpecCgYEA2tIyHzq1ExYbnd5s8/4L +rAcA8t17heLX4HxlBE/ODbhCji/lI209i4eTdN38EhGBDsnnvCCozqvDiw3H4RJ3 +soliHGNB7su4yNuYjSN8AE/4zq73wf0hWDKV+L660Gqq4b/Dd3WLygr01O83vB/5 +kD9dt6bHCr/F9pTTIbDTDHkCgYEAqbXE2gKfzrjFzN+h9mFL3mAhgQiR179cP7+y +Dr5omKpTL6PPNoCbGo+wufuKkRj6uecpiVHM01ecBCW0cmt4b/EjkW+l4SFs2ht+ +GPKezeqVww8EQsFt3p58I1PFzFB0ereAfTU8Yza3SxRF2Q/+0xp0ZK2+vgpc08+b +MY4Ach0CgYBM1qpAPFDx1fkIUc6uCr5+CJmN52djuNh5PQkBuoN4S9c98lKWXi+q +eVrsNKfDS5+92E2O4iKgWd9aCRzqxxq/0mIgT+3QLUj84SyXxlrkrV0QiU3xMrnQ +L/CjF5cbKXcixbuIlTfKwoqpRj/igThAKNtBGLKZqw0eV6uISbbDNw== +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_WALLET_PUBLIC_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUIlcBc0KB9zljlWlercBlXSmz7i8wDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyMVoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDFXFmZ+JAM+10oeWht8WutI9nkQRV2oMtCwmdJYerOQXEx +NGNAsUZAYQi7pUi3WFfKIOXcr+hwCFdCaoXG8A07kpAXnKlFYO8agYCwcyiYGx1Z +raB2UtUik/6j1/N6bluG4uMljojWRIAK5umAVALAmEHj4Qu0QSsv2Bi9cjqlPn80 +/mM0iMqXhuQ3u+ae3t8yxbI8pQ2Zt6WJV1q9idiu6JB1ZBkm1VSO2m80rVIia4JS +nK5y/psDK5xzhi2NMp6WrCAc9ll0uNEysW2c+1ZZ5K5Sx6aEuOJadDGmZwxaHBdg +EUxrKtCTFRppHmgsr/pBwwXtI1g6rHY/Zm/8SRsDAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQAn8BB5RX1ZvKsKEY+UMEEZ +7u/yoXiocrxFsRyHGgjaB3Lh4ArMsFM6I3xIbeKjojidci2d5IxWACM4V4A2MQoG +FTiX192lg0a0VgutjcylGfXQuQuC4vQux+WaShksDTYn+CKl4vSDaZIkGi6j/MNb +lqgULuMUuilLJyNPqGobz/b1jE2N6uqMD6YUJHakD4ph/oVKOcxyvlcV9jGIdOgd +UlEk/zn3PPC6CadBQ3diEm9LYoLi3QPcsLYIFRSt2YcmdvMSl/9+uLAlP2Ja7RQr +8KPvq4WWGh7S/9tblPBrPeab+Zw405qBc3RfAYuLvk4+DXA0iXIBO5yNSDpXzN3U +-----END CERTIFICATE----- +""" + +SSL_TEST_WALLET_PUBLIC_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAxVxZmfiQDPtdKHlobfFrrSPZ5EEVdqDLQsJnSWHqzkFxMTRj +QLFGQGEIu6VIt1hXyiDl3K/ocAhXQmqFxvANO5KQF5ypRWDvGoGAsHMomBsdWa2g +dlLVIpP+o9fzem5bhuLjJY6I1kSACubpgFQCwJhB4+ELtEErL9gYvXI6pT5/NP5j +NIjKl4bkN7vmnt7fMsWyPKUNmbeliVdavYnYruiQdWQZJtVUjtpvNK1SImuCUpyu +cv6bAyucc4YtjTKelqwgHPZZdLjRMrFtnPtWWeSuUsemhLjiWnQxpmcMWhwXYBFM +ayrQkxUaaR5oLK/6QcMF7SNYOqx2P2Zv/EkbAwIDAQABAoIBAQCNG2cwz+fmcD39 +9zf0C31qEEz0hpga9gH93FLOw8SG7YFJpeQk19qbowUEbLtd4zr5WKYgMGEm4L/K +y7CUOQOtCTAewbLA2Pp7YAYrolpuzkwg1yP4FWcvztJsQLVjXs3v1f6D1rH9SIKn +yMtAJlDFdNb/X+LnYQTIs1+U67wEcjONZqCEjVedUvQr1ciraOKIwPRuMZK4rSjC +dJbIeD4LOyafv3SKVptaWfP24tGBLHCYcTbMDnyDNfdRvLtUK9d/9u/Wgmrjm6mP +bemtc7gnhvZ6nWySVSvzpo8V+VtVJL5ypsUcs8csOfZJpzuRput2BOYC2ffSv96q +1aHdEWfpAoGBAP/sbXcDOa9G8DmiE27FTTuOB5//ZPooJI1xTa7dnt5xNGzisGWN +ZX4U5JxgaVXR3uE8PlSdTQP5wgJV8b5bFRY3cnpAt4EgH7FcktvxeQUzDjzn/IsX +B1xO+facsl7Ssk84QWciS1w+oHteVEmZGYbHrUyk5HBbBSvnmE+LS3TPAoGBAMVr +cZRRW7cjnxPLiIEb/YJwUtmz7rFx2TFp3f1tT7Q4F3cFpYtS9phTG62NlLSmnttE +okmRkZI4PKmSvinnUe8sRrvgIlF7Xn1p3q891BiFmw+bj305qUAbwXV0mdFkxk6r +IqmWsGo6Xe8tKDnP9h7gQShqTKXnfV4B6xyj9SuNAoGBAJPvG7uOzrpl3BjEek0u +mZ1SVVAENl9v8ukb1Ja/HsVgVLiYNPUOzdsawqcuB9WG0joKM9F/d/RTW/UzruCl +D8Re2rqWTDzEz+0bIP0oURdTUuicBNx1vFh8gnsuSuELE+09DHlMVpmEzgliFoDQ +kfPZ2nASZMYZpxyg6+cXEs/7AoGAfNfpR4X7neDk5Du94we0VRx7CDkFJSl91AXC +4FSUJr+h9x6XBXg9gS5tPl9ePq9vnfHVPvjTOchedE3b+9vQsJMrV/vxN93wbxbY +P7G1wpwa5s/U+bfRFD15JbHK4+P6lB0dGmm0vjiS1oGUAptEZVojWk9+kRvG6AAG +kmIM1LECgYB7Gfaz9h0a7mbyiiMd7gXjYV760/7sgoJhmeg353FrKGxfRyowndXu +O8I4aDwSbCqtLu/Q2XOZoW+jchmvHqs/Kg9PWait7lb8bWcAxhcuC0d76OpCyp5y +S/qbLTKwRiskQR68jBd7K4TM0vYLiyc3GUEhaCUXGHPNoKtvZQyVXg== +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_FARMER_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUH4ybLZJDdYBBZEXa7rRC0r1WLYUwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyMVoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQCr67MRDiKgPiH15yQ6ZJeEcghZEoYXb98sCwHKTk7cDMp+ +9Q92IP761jOmqXn1BTXNZQc62a1kZ0yIBUdqCxH5R3+BGF31fDwK0VPQhSh7Np85 +iv3SHlrJBmNF+JX6A+aiKf0f6Zaq0pK1NiTaY7mfXMynSVdxJwwTgqQR0qfvqXEm +XWBeARhXpUelIyUexXHrGpXumAWY4wn1YEW+SpcGNXElidd/HLbJ/OHkCExd0nge +yKDQBabCAiIb6fm0wmL6HcD++mqsEt9II8U1qoUPfpUWxFpUSWBoZWcATqrPIPYu +LeHPvl3ZctQCuTthWHvum1zRK3G8YUXcTWwWsNBHAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQAb3eNfxvPPv3QjmDEIcOhv +fyGS8orMzygz0eCtX0ODKsIWEBZw7bSym6fhEYvkltSf41KkeBInjDJnqiCJ2Yef +VvXcGLyHw/klmkafcvcVCoa95TNTuKb9ElJbdIuoQ3EWk4uQEK0ARH4K+mXA5lzh +RVbG9EWvmdYXaG9BrnX9Hr/lCEPXynGtbgKvglpFn8mAWz2ru44BsrkiZgMFBpPF +R6VJV3SRCTib0nsYXkLXyRgG5JcH/VPpFfyS+sLi/98AIG2mTWXTpj4dT5f9epTr +Cn00kytB4+9ZUvCMUrg0BciQkCz6lr46gcOwbMpzoxXWnK+cwPcr9zu+LLEs1NiZ +-----END CERTIFICATE----- +""" + +SSL_TEST_FARMER_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAq+uzEQ4ioD4h9eckOmSXhHIIWRKGF2/fLAsByk5O3AzKfvUP +diD++tYzpql59QU1zWUHOtmtZGdMiAVHagsR+Ud/gRhd9Xw8CtFT0IUoezafOYr9 +0h5ayQZjRfiV+gPmoin9H+mWqtKStTYk2mO5n1zMp0lXcScME4KkEdKn76lxJl1g +XgEYV6VHpSMlHsVx6xqV7pgFmOMJ9WBFvkqXBjVxJYnXfxy2yfzh5AhMXdJ4Hsig +0AWmwgIiG+n5tMJi+h3A/vpqrBLfSCPFNaqFD36VFsRaVElgaGVnAE6qzyD2Li3h +z75d2XLUArk7YVh77ptc0StxvGFF3E1sFrDQRwIDAQABAoIBAHmFHQmNKESEJpUe +UKlFuSPRRr1PLqEaXnFPRnCtcWhxUiDzL36cTB8ZkWDYom/iwujv5HBgtQMnUR1E +Pfpi4M2HEEU76A5BRl+PHuNhe/+72EhgSpFfC2TUsw8ea0RRxZAShe0su2b7eN/F +6b7EhsxyV+ZXHQvKQer4iOhgMnxf68BuYpL4eyYfPlKFlSN1YcBCpuh+BPBuVdqV +JSHBumr4SBHFrG/Gn+IpzAJ3EyB7idTYn3jzr/iXqtwjhSI+9Hmh8rs4ctBElrxO +of57t3uPO2Z12cQtMN29kJTplXud6AJBacM1AWlY3jbUcAYlqQtyFGNlWwqjUdP+ +zXyAtoECgYEA3EXfLDNnulR4so5Ab6rYhxRmb2c+5He7BNc1fCri5U/bE7GPh0Ew +uHDLa3mc3XrHEwd6B1DpnzpiV8a7Lb9U32xPYoX73753a5RaLxr/yrZZZyuirtTE +vKdyOVufoOzD53XDvlUtkDwYLywFKpiIVp++wsf7BeKuuJ7oHpPuwfcCgYEAx84n +7sr6MsKizGMELYtjIQ24pHdzDQW6Uk3U2BU2glvzNJ0KIOgdYN3nQ/4LBhENgRL1 ++mtDgQ/+NF9Q4DgpriAQCp8Kth4hosgHOZZ9mbPwMvL40XxXOtk1POHWN8SXWdU1 +tHw69yMk4vS1IgjGsG6QvNfoNlscNjzW7/GD0DECgYAjmadYHXbGRqC7OwJvCc21 +BzcHCki/5Bn1zlJ2nvfM1/swU6I+2epl5NT7qcwQf6dtC+hNBma7tVPvm87kteeh +tH+gDMeIgeALIw7wTgzJVm9cnVDNsNWbJ/TuEEDcYWfIIOBiAqI6jXvbI+Ix4DUo +yuip0mhfqyNR81zQlFgiQQKBgEs3KUM0d/Fp4d0tHSKECWIlBzAqo03wrQ5UrF5X +xfhW4vwYbfqrRnvzrR6kYMP84WeImr6VaIkKWzid9RUjL1WUTlWhP2gFecYMpOOh +6lBVM4QKgW5i73eA0xDDN2AxCoTPxXLXHV9xhG5HjnRsd1dtl+DvKkRkEf+88XDM +K0HRAoGAIOufId2qbyFGxWld5DpWTb2RPt7ghbxYK6SNXk5mzKOdKA6c5La9evDF +btn0mfIQf/4qftgACHTYcx/IYtOHzl83rLvuP0YE7+8ErU8TCUta1lAOIBqm/WO2 +RjFbl5GxJxthIxT0CK3BEj8rSuwk9pXsnawEPN9/Ca0BuseIy5o= +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_FARMER_PUBLIC_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUQv7oQqnVOgMJxwGj1o9mVlDjSV8wDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyMVoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDWc6RPgLnigv9641Ig8ro/KxVnUM3ORHs2hgp9Cj4lljnn +3Pl1IOlZu4pI8nQ0MGNVaiRTH5iqi1PmxfngFSsCAKjLFjty/Av2HpFiuYXQPxrC +suMPbF+KTpxi0QyxM/04inX9sZZpAPXbRtJ7XuFq71jB1mHvvyq/7/0mzTMwdUnX +LRfTMSN8TKVc+x0nGaQynIQbFxW2UcWLShaeNpfk0++2MeHOqrJo1LRta3uN8MBT +tdd7ISMv85gbwBajJ833BeE5/hxbDuohWnN6NOko+lKf+iaiVZG6LIqg4YLO8nEC +3eCtPSqZDeMcojF2TpfAs7RYy/uPTnrurq1I3M1VAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQBQPItY6FjeAmX7cpd0BPdo +lWYb1yYOUVdVkHV5S160tGhLCcipZF511IOvjH+UUh/YVRUPyAhb+tCURTxWMAmz +xKiMESk5uJWoxWKZYOTkEE1SzTHdOLIO4gVz+z5ewovgSHOQ6R95AtCCeFNf+G8K +JZux2BbxkHZTiUK21UiarheTIOvdwn4W3NaGnDaxgIQncptLlLUSHKcc5FbLNhU9 +4dlPe/VMglwI6R7euYFBdcJCpAnyhkeGGiEGZMPgu7oG8AY/PnbiAq4EFQa5rwB9 +KtZGtuEvHhChxuCJrdhS3arkhq7qNPQc2iYRUeAnzK+CqVMMP+j9SqJA47YEoLKN +-----END CERTIFICATE----- +""" + +SSL_TEST_FARMER_PUBLIC_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA1nOkT4C54oL/euNSIPK6PysVZ1DNzkR7NoYKfQo+JZY559z5 +dSDpWbuKSPJ0NDBjVWokUx+YqotT5sX54BUrAgCoyxY7cvwL9h6RYrmF0D8awrLj +D2xfik6cYtEMsTP9OIp1/bGWaQD120bSe17hau9YwdZh778qv+/9Js0zMHVJ1y0X +0zEjfEylXPsdJxmkMpyEGxcVtlHFi0oWnjaX5NPvtjHhzqqyaNS0bWt7jfDAU7XX +eyEjL/OYG8AWoyfN9wXhOf4cWw7qIVpzejTpKPpSn/omolWRuiyKoOGCzvJxAt3g +rT0qmQ3jHKIxdk6XwLO0WMv7j0567q6tSNzNVQIDAQABAoIBACsYXiKj6bb9QD3/ +xJdeb9MV4105vcH/vQr98Mmj7006XTSdEXxaOsqPh4CVSIjcWHnntJkHtnQ/P4MW +sdo4JsZmP4VgWF4JDJZPGkROp/drVwNdU4fb7W8r0P2CqRxLKE3edUugDmvXh5Cj +MNUeAgqtQpbhcBjvv7WGksbjYbAQ43UH8/zDFpSDmMbPjUHOnKoCZSFC3IfCoUPg +8U55AWBQVQnqP5E0R38PpAeTmHr80HFBPppYteCH7/OATr8/7ffv7Vd10/bVL1DP +Lqa8YZdNP2UOtKCDnbxq4CLFm9gQ2jIwhjL5c6YDVvXeOWiN5PKybhgU0yX3mQZa +UMFL/RkCgYEA+9l7oRtXt4Me1AkSLSstQlyGaMEEnYAwf6iU/HBu9dH7ZAadte2g +an9sciJUlODvcj3fK8rIewgJFY/wzdZPuCQv3fQGghrM9gXMgz8NiU1hK0AK/XI2 +zUwVJpiFXjemRly41Ss5UG1H3tHQObl6QyEeAzpJZrNENALFL6exwQ8CgYEA2fxi +BRGr3yHq9t/RsClpPlSHuE1IM1l8G1hXxrkROo83dILEsvIcHInntoJ03SPJI9NT +0Oa/XQ/Q/21xPOmjYl+b3jKBn9Yaad0l/mwoG1M+NXEniZ2/Fok3EGqu0hoaPpJ/ +ipCtnNWurA8BaSnQJD/2Fu0UnZdexlHlsr32A1sCgYBZpHSjyJa1RB+R+1ST7T1j +1Ikm+iUJZppcpgW5wM9OAhrH5K74FYe2wHo6Ocv/Xfz8ndc0wC4R1K9fFGfy7Chd +88tx5iz23FE99JxxztyjlX5Tpa0Dv0aQVldk8H7wJUCy5MgJYCQ7Y1pkjivekA1b +nYsQPQvpWT+af63uI3NaswKBgQCKMry98/cf7nP1ce6RnZ9weczVpoFItMm+2GJF +xZzLoHKK9kDYJjB2U2PIzKpkbMSfZuIzhab6zAU3et4YvRLtUioSU7jkaauzRBZL +V6yRrlO5M/TaBV1ZX0K+sLQG1a+fzeb4JUM8NCiaQqUlU/H2mWpeHI4+XvIiD1ft +2wr2EwKBgQDMM22ppSf22Cv9aw7VQW2OJHI3+22LyL7SCqQDqKGPnnDlyKtLz/sK +iQozbZbSmVHFbz0DJMNMpIWm7irD44cdpnDxxO5KDd1FJjH2Ha/RiPA6QAiJkiOT +oAFJ/tFPZF+faEhscTBdGU4yS6frDVDGJpyFgqAvTxi7H7PRrfRP6Q== +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_HARVESTER_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUCg9lMTGr9/8DyLNEaafobyIYAS0wDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyMVoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQC7v3qmcCjmcREbWhZbtKpeZEwUPVkFpHg9yd0YQW0ZACZf +BZsJn5S9fkpfpJ9E758wRqvntAoKUwh0yntqjSNVEF+IwPs8eVV85yJLjZWzRlYN +FyZnr4ihtKAleDkXnSwDKKuWI3tm68GCmGlvzRvv8gR9/8Wut01JB4LjUslzaL5B +dM36gzjWGSxUD1A0gdG6leCMbJkRV6VQDLKMzMqfkrkleNZMfarrh4y2odaxGSlt +YGvIkeJg4R/vhUdWWnSCSZk8EtPZTC2ic/dMh0TZTw6zsaq0kkSf17a9HQ7exjgj +AMzTl4/O2mPQ1/j6t3nLqxXAMKRmQwe0RhW47fCzAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQBSd5/DP0a3Pi7gQeYounMW +9qa/ZCn2JyIlCSKPiQ2lRJeFLzXzG/e1hEiNtSBWS0oMi5HtyhsBSbmPeEp4Enl4 +Ri7UR4OhyEOddOAI98IEPtditV7YVczQi0h8a3/518yxvqVcUUOmIsABIvNWWtB+ +jov5sHygh2gQ9LK/9SXWxguvI4U0AiFteJvLKSFqqcUWbNXQn09fJNyM8Cx6mnTG +5pba3FCnr39CdRgzn21Zp+plodI6xu2T/VmvkBcU4jFVY/i36LJkJ0EBjcaUiE3h +/1+POQXMivBuERizoRvtIx/xmsfKaFSp26iBW0I/AkQjCAJiVS+RF4DfaHLzg8jT +-----END CERTIFICATE----- +""" + +SSL_TEST_HARVESTER_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAu796pnAo5nERG1oWW7SqXmRMFD1ZBaR4PcndGEFtGQAmXwWb +CZ+UvX5KX6SfRO+fMEar57QKClMIdMp7ao0jVRBfiMD7PHlVfOciS42Vs0ZWDRcm +Z6+IobSgJXg5F50sAyirliN7ZuvBgphpb80b7/IEff/FrrdNSQeC41LJc2i+QXTN ++oM41hksVA9QNIHRupXgjGyZEVelUAyyjMzKn5K5JXjWTH2q64eMtqHWsRkpbWBr +yJHiYOEf74VHVlp0gkmZPBLT2UwtonP3TIdE2U8Os7GqtJJEn9e2vR0O3sY4IwDM +05ePztpj0Nf4+rd5y6sVwDCkZkMHtEYVuO3wswIDAQABAoIBAEZ5IXjIMRIO7vTt +Y+cYcbrsuwH95SSRD/FhjHRGWsU/oSeZ2xBJrnNSrGgqSv59U6uzW2Ol2P73G/16 +48ijIdgURUf36FZS1RwFRoJFqyOYC0Tuo6PX59mLC3IFJqkOfi7RXVcGCpQfeoui +2jD1NL9kgPsqvvFOLNx0zVS3Bpci3L+HgQHRgSBJVjCuLnB+CF5nlb8abzh4gv5n +zE7vyx+sZNJW+qk3TN8yM7D144YxKXZpzzK0tQCjmf2m8P2L4vuJLhuoi43kFgfm +fgWUNFSfNrtSkVcYh9VEiaN0ZtpZbDgfNOixXIDq5N0whK3HoJSM0osST/C2Cz9V +uyXAwUECgYEA8FUVMek74XOXKWDrvC6wenyKXQ0xnHJy3qAvH5amO6LvnXWLr0Ex +ZbDz+BSzHBbjyY26m1z48BUS3GRY52mROVTlWJT+RMzdjEDWZdxjbCqrlCvx6mfI +ZmnAJJ2oAw2ndw8a/2vtoVAzayxz62LUOY2m1HwotXh8/RS9/0UcoXUCgYEAx/zQ +KSp6WB2MrwK+r5fLlmJtNVZEvqajjcD8g/1EggFN5QD5a/mLeoaYYREOu/mmWeJH +GSK2FnSCeiTYzcVbao3BD7u9pt/6s+ev/QpiBAxCnyKAQ7zTiVEvzvruEhLi6TBq +3rjvjzsOz0PYgNTzK01x/ZU8BIr5czcKRJ78HIcCgYEAvkiPRHpHCAUOLRvo6ZEJ +96D9qBkXK6hOHMg1J0yPB27FSyVTWIpEgyBsugIhod7Zsa5+jh45l1UIHulwnqCt +8/essssc2vpde3umhPXO3kiWmvWET7SmLbgTAqq06N35tsGF/a/FcNBgNb33depE +3+Cws1IupSflxjeTCzb8KTUCgYAqFvI0h8UiAG4Yc0pwqFDnwKVdYV+shGPNtL0w +hkBB4EZlmRPlfqq1SdiOLUndlAhHyJDQIHcUOMcxL8oVXKEFxvnH9upOUbtw26U1 +a1b/pRjsZxV5rCcVMmoOdM9gLGtnSpJjd6arjXFre8r4KllXVsBT73GnPxyK/B2E +HbXPxwKBgAkjgd/D6v8LLneuqLOYvuEwcecCuOrkkVl6S24y/Gn7hS4awojpraoj +lZk1ZlWJwEj3KYp8toy7Wpz5bvVmZT6nVDudP2uMy3BrifIK5zZ/mKZZ4JmU7WSV +R5+9lwCKYXUDvK7dVBY6M8oh8WD5F6tHAX1EV/b/PEnqRpaqsjGi +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_TIMELORD_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUZZmj3NMn41MJf/JZnA19S7GWBEcwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyMVoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDiAHh3GaeWWIKsNlwDE6s8EfKs8AKvq7ThxFGtTAbZs1MT +ckyg/7/phaihOVXmpdl9Lhul9nUxr3pFkW3pDuCXWeF/QB4pJqbbDUdOl1fu1g6z +8H3hLIKK8+yKm8SliLKGlYlm0lrCh+bR1gn46ACImMxs2PiLJydaG6TH/rnQZo8k +8S8unwpjrI09dwnwdhTGG6F3789bEHyZbwzmitHRrzYUKDviazaWvyspDNg6CJ9x ++ufdVyY3vTAVsQZaGyTZMqxE/3cLtT3CHCGZaMddlVmicEIkgKh4vCYV//6suVbW +cQnzBXYCbAC0jp+TaHFoLztoB++ODJbQ02kI6xylAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQCMUxFYqgEUN06caqVcTTZ5 +rW3V8oRETxkCZ5okxRnKKJk6L9YhdkiL4Mjsbn+9ZwmWwEZJjlqOGUC8uZZgrRjr +fqyftmYjvRVMZNZoh1wqp3kcEDXPLOqM7ixpK/D/ScOZc8G0EpiTykj4lmgF8/7t +11fCyv6vHYLqsDFNC3BJ8g8E09Zej2bEBwTXp6g52J/YCuJ2xpu9g8mOF2xs3ECv +M74JEGlF0eNjuAQ+R5J5DaNwojHDoVYMYT5X/KaPxmLLl6b6n7ZvKuCZHs1CoLFn +1Jg1JGLOczrHvHnRPr4zLqffdnq/SFQP1MLFs+QUORLpHved+PPZJogSbRn7HzGQ +-----END CERTIFICATE----- +""" + +SSL_TEST_TIMELORD_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA4gB4dxmnlliCrDZcAxOrPBHyrPACr6u04cRRrUwG2bNTE3JM +oP+/6YWooTlV5qXZfS4bpfZ1Ma96RZFt6Q7gl1nhf0AeKSam2w1HTpdX7tYOs/B9 +4SyCivPsipvEpYiyhpWJZtJawofm0dYJ+OgAiJjMbNj4iycnWhukx/650GaPJPEv +Lp8KY6yNPXcJ8HYUxhuhd+/PWxB8mW8M5orR0a82FCg74ms2lr8rKQzYOgifcfrn +3VcmN70wFbEGWhsk2TKsRP93C7U9whwhmWjHXZVZonBCJICoeLwmFf/+rLlW1nEJ +8wV2AmwAtI6fk2hxaC87aAfvjgyW0NNpCOscpQIDAQABAoIBADgCidhqgUAfe3MW +ncMdcsiWYiA980x6L3/sWZmrR99YM/ST1S3pdDR5rYsXXJSm8bm2XZ/J7s17gcAQ +BL9Hsp2P5vTUfSURsTSEm/F8TIyifE5YAbp2f3vUbAEGDbxAno4ALWdQJrIjYC8M +7rfDN66iv7cSJrbF41jPlQ0DsiVVbkmUVQmb40o5iymROxj1as8ovDFqPC2mPIux +94gpKNf0FcwN/WPhz6uqHdkD26E5lIKSHy4G0Ko0uBWyITYPDQnp+57IRsm5goz9 +8FzzKKlQOOh/MP4jondCjNbQXUMND2E/MUZoa/WH5JOthkrGnrupayQ9WSPWjmDd +Ru+DMxkCgYEA98ESR2Qaxu+LvdjLTl4c/G+I6/6pKmKpAhomWS1s5a4oz5llJZeM +3bGxQJYM5Zk9f5lhBgHtM30N4W5sX7aKgixeK+rITqneX0jE9eyH/WisI4WHOwxw +EXWuWhOK5MkctZPuruzCkaLAvhxtsmOQKhrSO5oCsBes0VK/dlglRJcCgYEA6YYQ +Suam/OckqmYlbu+N1km4AtRO6O+qbkRsDT88vH1Z/PAIIbS0XYlzhfc/jStZtUOE +k5UmEVqMeDHHHHg/ejczbNPVaTm0oWfWYH6D1UQdaQsI+AFDfYbuVc69ik9idnpb ++8yUHyI7ikjowKvf0f/4rgHmZX4B7Ga5gtVupCMCgYEA6fp/zcZfd4MhYSDOGGvP +SiP3lpDBqFLWtDKIBu5ciqkox65hlNgEZBZ9hLZw5aEMMGZk9+x33Il8w2qqlNXr +BzbplOY9V/UbGre5s1s3rv3cnAtuBDkh2YtfJpiQMrMwFtfnsXHN6wZequxkOPXI +X8tGwp0XbsBdKK7SPOzP/W8CgYEAxUSNCtjhg462+IMlaBtRVA4eNbWnmzqqXE/M +fzxGUGwL0pHqLJ78Jm/weOCufNB8DZWlrw41hD9bnkVej/w8kz+CX5JrG2K15gtT +m1wEfen2dj/uKaLXoniTaUUl9GqvIKqofYMKNWqzpVOF2wzWHA4Bwuyz9lSFx2/X +kmA+AMUCgYB4x8hb035umUb+lh3EyS06edJPZdrHV3teJAZ25RAU76OQzGzfndCS +VhgssI9LPWYXaO1vS7u9jSg56f6zNPHtpngSOZc9uySdSIIKM7pbucYmJB0Jrdhb +3HmdkTpb6wlZD/69AF6++09b3iFk67xsQCSSST1jW/66ZqLBkp57fw== +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_TIMELORD_PUBLIC_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUCo4bPbI2+6ucwAKVWOVvza/5Ve0wDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyMVoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDTrCQBeTfXl3xKstyCxQpGelFNEagDRZLaJe/dPLN9FiBq +N25JmL0mMiJfSSI1HQQoMMM0ayilEU8epeqWkWLi+Pw2aR7nuayPzlqfY411pOVy +I0lZWi/M+w+ytyHA+/ORZHqSUf3Zccs8PouexC7bU4ScLHo6X7bOSiLfezNsqPAc +RngI9C1rPLrzQQHUd1m2aQOhvMmbXfawaofZzSRZT/Ipdhm6TsTy4325XM8BtoXm +CQtEUyM8xgmOkQF/4m/N51/+7Jnchcb3pBP1RujmibOwH4fAlDwu6A5kwe1oFMaq +ql15KdElfkRvzbVDdyAVjUnW2j0WZ3WnumM0uTSbAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQBGVJ1SnI7UT7M9BSt6Sfnu +noJa96HgjVJTwMBkGIP7phGY8sm2B9OSVlaVUC6tZkJaXSPDqejdHRFleFrMn+yS +eY3apM70u+GHRIjozf6FtQQ8D9AWY3sL1eDVfoNh/w6UPaLtt0GtvLXhR9gCvPgH +WKNJ7/5hSsJHRVKBRZtuLwrU4nIwrdvcFHh9btAeNU+ZQ9BPx+jD7G0b3MC4aePg +M/mUQkCBShhQPFIuPO5B5hYmyqvSfKfRCCXz3yObo0/DmEk1XlWu5fUStUXMoBRl +JQ8AxL5aH9GLLkdhMqzInZiiEgFEEEF9OjeA0FpwG7qmRMwL0d6Mp/VfyMV3CwqT +-----END CERTIFICATE----- +""" + +SSL_TEST_TIMELORD_PUBLIC_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpgIBAAKCAQEA06wkAXk315d8SrLcgsUKRnpRTRGoA0WS2iXv3TyzfRYgajdu +SZi9JjIiX0kiNR0EKDDDNGsopRFPHqXqlpFi4vj8Nmke57msj85an2ONdaTlciNJ +WVovzPsPsrchwPvzkWR6klH92XHLPD6LnsQu21OEnCx6Ol+2zkoi33szbKjwHEZ4 +CPQtazy680EB1HdZtmkDobzJm132sGqH2c0kWU/yKXYZuk7E8uN9uVzPAbaF5gkL +RFMjPMYJjpEBf+Jvzedf/uyZ3IXG96QT9Ubo5omzsB+HwJQ8LugOZMHtaBTGqqpd +eSnRJX5Eb821Q3cgFY1J1to9Fmd1p7pjNLk0mwIDAQABAoIBAQClwrZnmP/MC0tA +TBU3KwrC6mLkkaEa/s7jmrXecPy2Ri+YPlRVuhDV6ojUSbdKFLD+sEENuaUYrxdg +jtnIk4325LjN+0BCgzrJWvXIv/M1X6521X9JQ8EPKsS+VX9PW38AKsl58E90ixJ2 +2RwJduSiySKeEo6dS/siTRhGHnrE6ZFG9QsMTKhsLU0WfQFY3h9NzmAv5hN5eOta +vVofMnOLRPeAEpvw0SzmnHf/oBs0cV/JTez/G1LB/xtG6QX2ko/DbjjtlKPr8kT+ +Jz8i2pJvh3IA4ZxJYa7PiBxLszKvDgiOvbI/P6yKAwbeB0O1YWeiEdzQwujdEpzy +0+tNlE1RAoGBAP1qNZvUBJDGCVFKLKN+MU31K63nzGETV4kDsurvxMSxhSeOJILb +aSx62dCWKyU/mrqE+uUrpAoV37Tp2rzyDeqyOSBoW/d7shsYk0Y+/oYQp3t4DB7J +g8CQdL3ihgUvONwBUcG7MW7/RyXP29Na2eASlkkRdLPlP+jG9Wlzt1qHAoGBANXU +69QRx/4g/LyYCinkgeHqNqu4VORbyd5bnfCXkPwDppV317rJuMOJjye8IQngX6G0 +fOdFXaxGWHE+w26bGI5gRvuPrG44NnD6xLfp2fFa+iUaW8qtY9z9qLK5w6RK7rGf +oMV/cxofWpSrEVQu4ZojswpgBQH+hxo+c6280bZNAoGBAOLrgVepcfEltGA7WF+K +d6IEQXm3UFc5F7BURJmF3J/5CnASI3Wd/b7bv0G9xqLTIr2UqIogGwMA9VIt+jYp +VfbsYqU3berds+35gp9rd0Ubkq3IIKpM7pK3iCIkvrfTwkmXUNt2wVxQcShVydWn +y+OPIU4KuIMCbMkHp+gmu2/vAoGBAMWw2m7wGYQbn04FCrB8cJAa53yPKP0O121a +KoT6u7Ii9eoOKEaqpMUy8kT8K1dkN0XbBfcTLG1PhAj+y9QAVA2deUKuK+6izcJa +NXELJNF9GPMgkWiqMT9ozISgNf44wME7IXo2QIYQIsB7/8NirHTDOI4JA9g6J1He +Fziy7vepAoGBAOqr2nikLpb8vBozfHmXcTx72cRHOm53L9u3UwUwnyQML0JwDNe1 +xrmKS/sKZgVk+TcjEIakfLn04wmk2ZmEPA/n2z9Xc17DQlstlC10Mg6eIga/Aw/s +ocV3tE/6LBAtsPRjqR4U7JK4so6w64zHc6vbG25YBZunSnuegy73OM/3 +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_CRAWLER_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUZXUt4iAbkAXAUd0mDEZSMXmcl3YwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyMVoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDxVh9zMP+GF0XYDAqC3vehKpK2gTXSZTdgQWn0YMOkKQFZ +NNHHkWAtePO5p7wSbysSUnurh9GlYtsK4kDnkj1eVBCWfVyPM/0cfkbhIcv5Jpkr +qKABv+6b3v1mikl+erJ2SE3GEWY7wThZuPvNh8kDAYs7MuN766jQ59wSIxvCPlpp +cObzbz21dHRrxDl8zCzNOk2z41sY12sGvtR1fWTIByqxnNy7mfa15/uEtY5qVaVz +uQBm+gYV+lRoW3sHBJz9TEH92s+VnSpZj/hHbL8kpN8y0zichH9vG17E78LcCfCR +B6fd/Civ8mZZNhH1cCGlm26w8y7qM2PnEwvsVJ/xAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQAyQKtGaSz4s/U85m3zSqTm +fykoZVo9+BhqPEK3bqiA/wQ4RnQushPEuaLqdD/qI9l6h6jKgnx3hyJ0Ht8CYo4r +KAEhRVSoImdqW8TpFIOG2vNgK09HuI0ahwT3NfmXz8uI87IkzHco+QS/i7AMvU7V +vjyFXSjWWQYmnzb7gWw/LEcpqScMB6/4UOam3G/xznMiFCmUDn9CEh/VNFaE6YK5 +9VcfE2mcOftm8/jstoOgz5a/XduxJGObE/esMf2XeLLvfhIJV5yL+AkyVUrgESeB +/SVfSH0gcu98o+jp3fT8zKF6Q53D4Uyf28HUY261pvvvZFnshprKR3Tzfjo+LcpW +-----END CERTIFICATE----- +""" + +SSL_TEST_CRAWLER_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA8VYfczD/hhdF2AwKgt73oSqStoE10mU3YEFp9GDDpCkBWTTR +x5FgLXjzuae8Em8rElJ7q4fRpWLbCuJA55I9XlQQln1cjzP9HH5G4SHL+SaZK6ig +Ab/um979ZopJfnqydkhNxhFmO8E4Wbj7zYfJAwGLOzLje+uo0OfcEiMbwj5aaXDm +8289tXR0a8Q5fMwszTpNs+NbGNdrBr7UdX1kyAcqsZzcu5n2tef7hLWOalWlc7kA +ZvoGFfpUaFt7BwSc/UxB/drPlZ0qWY/4R2y/JKTfMtM4nIR/bxtexO/C3AnwkQen +3fwor/JmWTYR9XAhpZtusPMu6jNj5xML7FSf8QIDAQABAoIBAGjyLZz2+rcB2dJ4 +cf21HfQMwl3w1EnYz/rgl0W46nqxhi+Xo33oPu2nQj1CrqtJgm5mRfcyib0kvuH9 +v4Gz+1HQtqHqg9yWHARO+V2fR8bhvQvaOTJpl0Za8tCrZAhHLOH40TFHkbB8dpe1 +tHINESFog3ZLy9awhOnLWczdTY3qnVLl/SuK7hK8WTjthl5ZBIXiv+gvXs77eahl +Daa9q3KQRwlaDpjrkHiM8H3T645TREhatMBvpExO89bnMHPNTiLh5UF9SBrjF7RR +xJwfYrgKmf/GPSeWvb4+RBGUwCHCmuFHibyqJ/VLT/pKeTzQWLKPxavrgmEIJyqN +9R47Xu0CgYEA+Jx6iKhdJM3Egp6k1mLOLQ2m+yjTU5D39/DjIhdEu/fI8zBGSwwO +JtAGj9qUpNG0m74wqd3y8++LIxJmTjSqG9Z1jxT/sri4UvT3bqZy6Ry4/iwk+KZr +8IOYLzNmdxDhazSTa3UeHouvNmeLwmzCCNCQkHAN2jKsdjJiIe3ZkL8CgYEA+IJL +diWOyBcMtcIH+bCaU80OYRiMOatexktBHu07r1eCETlXJPn3ifmcrM9JXhCKqlVc +9ZLunPq0NwqHdNGVTBtU2gau0DhomrXr7y4WGOdiB27cV1FsuU9PmpvL18/8AWxZ +jEqmd3LuXphYdHWhn9N79Yxwdl7YKHAUXYB3S08CgYEA1VIfaiddVPkixxmtQy+g +zdPLFfOf8TKRMzSFEHl6xvcEfHdNuZNsiS0ylDjwFsTB/mkhhIAnudwvPTbKhgx2 +clCAqvdPuGD7+GKt9Unpi9DTg3UJfDoAoG2qJcYrA7t+UOjaHfhukbM18q7Co1+C +1uFvSiB8ImAcz4bH4WkfqC0CgYBz3WwJneFAcV6/r8PAKxMJV2YI50UZ7ki184hd +PwbA7e/6z91NpC5B6lueRtdSQCwm1r4M0YDnOAymTQZy9PTDE0swjEUdV++Nkpx6 +W+Z5nggapxmcrJ4gmFXpJBKagKJil3345yVONAvnluhHBEFjH1uAVQZqajYmRHt6 +TNdSCQKBgQDB8Q+Vmqt34P/5gipoYFHdBaipLGupamJ1CBX1KoqtmMEWDgSxhGI4 +Xhtm4AC2f6xwlo3S9R74QipeDyU0gN1LfGMdX8nsWUP2gcy6b1m1kSHI/e4AzJ4l +aDkjJwkopaytAEq/SVAhqNPHdtH0ihLkBOoi1xkl9u1tNpMwZHv0NA== +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_DAEMON_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUQBjubkcEz8Ame+evhVR/P+08N7IwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyMVoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQClp3iVfH6FLpZut6UdSrfkIfqED7h0aj8rkUi934BDFQiP +I87MuuAl7N3Nj3U3my+vppp3se0kt4cro79KStORGGuKm6EftkWJj9PIx/UKU0jv +PmtU2T7jzmohwM0RFwXSUUpAnInGUptSNlngbDJSQo92KYqCnCCw2Kllh8tSnaqn +/cVeBeOQ6ippM6YwJGs4rVMzS0v93aUvTXsKHaqq+wLe7Jcbiy2iAZT2HWZcuIt/ +uhWDTRiDMxFsSl53O2dtG5tlvxS+kjZ0YOZbhJMcddHBNg0fX6oPeEzMB0DMs7Nj +3Q4CDrPXYIaLryMccGDljrF8xZ1+rlOLar1A8pufAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQCKnK69Hada53vK4LIQS8mk +PqMiGLkrNsdqo+8/zm31wWQaL+XfWEzdEWfpSy4nPAKN8PriBafoRJynxUzNKNfP +YwzSpdwh49I/hQXgTg43Ps/Ud8eMAOwZ8hQFjtpxwMN5KMCNUZ2WkN7gyHN4eCFL +Vi52fq0zSvKIgCiB/G5oauC+4oesD3KhNFNdGDM+u4cZowfJP0ytetW9YRppUDyL +Di8vN8lRfLQmt/yvEF0Qgxuz1w/4D090mSHNwkiQFvu1+A21PEkk/FGsOqaEXyjN +a/hVuQii73GQQJgDCbeBECbmNz32rXYhzm0/Gg2QQ3B592qNnyDfU0Jxsq/IJtcU +-----END CERTIFICATE----- +""" + +SSL_TEST_DAEMON_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEApad4lXx+hS6WbrelHUq35CH6hA+4dGo/K5FIvd+AQxUIjyPO +zLrgJezdzY91N5svr6aad7HtJLeHK6O/SkrTkRhripuhH7ZFiY/TyMf1ClNI7z5r +VNk+485qIcDNERcF0lFKQJyJxlKbUjZZ4GwyUkKPdimKgpwgsNipZYfLUp2qp/3F +XgXjkOoqaTOmMCRrOK1TM0tL/d2lL017Ch2qqvsC3uyXG4stogGU9h1mXLiLf7oV +g00YgzMRbEpedztnbRubZb8UvpI2dGDmW4STHHXRwTYNH1+qD3hMzAdAzLOzY90O +Ag6z12CGi68jHHBg5Y6xfMWdfq5Ti2q9QPKbnwIDAQABAoIBAQCYEW7uu4Rhumy6 +YreedjLtqAuTI+NOayJmWk6OjxftfOeIj3SOGJcf+Zt70s/mJf3Wn3h5nMp3xEq/ +0ugNyTcCoYpHiaqVs/uN1oyyam0V93KivYhGMdA7zAc9yQH3SE09zwX83jbT6o1P +ITnMfUaMoTGVZXkTgUO5VIvc+pW51iyZ3mD8eH4MmkV/GoeOBkD7T1TMrTiMpeDD +K18lhmmEeMMqp7lhZ781pnfu9WzHeY6b37kap24EGBgDUDNtTdhu2bN2ejW4g+VH +0CzZcYJ02jwY1UE9fxfN/GXkfdQ5vPDy8xdo7mE3N0oMqfD+I5MZdZqG8xDKnofg +qSNv2LWxAoGBANcmVHsmAMvt0FXQ1j3GD/wJk5y/ty+UEqfVq7bmdpFYSxS4CKO2 +mTlRgc+7NEYz5x/2y/7Pji4l7wICBymbgvx+Gdr60HEF/RO6wdYgZNmRcR5doHc0 +RBt+8F3YCryN2AonA0abpa/mRu3QNNZNr8Vobw5v3khrhB7eSZAtkRL9AoGBAMUb +Vk1DDTElgPMzeXjjWD2UOBpLgNJBK9Ku6eGu870eOD3/oN0MwYt6WroEEpkC3EU5 +Q2ATqei5Tg0I4tldlT7bA/RY3bhY17rkN83v2Sp09xAn5TvVRMaIV9m1GsSRkuCq +hgKMAoZJwjuAaEsoAa5IoyFmu2/gFICTOEF4iNHLAoGAN1lpCnVwZwY7PpiHRUUa +/6AHFaBMpDTXx820a01G24V3a1EdB+EF8jUBzEtA236myxZWzKrgzQZ9Qmr7JL0Z +KZPXWvqDfVApF1ZIX6ndyAseqs0zZvdPPjOd6saVnIRxO8tlkFiie2omfS+/KBK8 +UXDYgUJOURs31ikhi7HtTJECgYEAneKX7quXFZcFA8pnsv3o4OqpRebU+ZZalBio +H68UbpiWVJM/N9HP9vm7UuWQQCCacJi972fQ4ioM60QC8jqUIhUtxbypXdFMfNfy +G3PUcL3gaYCLjrH2tVDhjfITzwEMtgnh7ohYPVk3zJG++PTC4+grQ8YWvjawNY23 +sjnq3qECgYEAsrKp9xPdTpG6Y4LhhBF1O7fP2IT/8aIWMNCPoTTbhkhULlYoRXf4 +hKpYRnpWkntdGqej0bq1IBt89j3LF8KLAG4d0zhyEQlFucWifd76srOPw60j86AO +Ix29DNjD9RATOzpDEvgp7R7H4/bazcqcpo/Pz0kE3/UwuCi+ebFyt+E= +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_INTRODUCER_PUBLIC_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUbJaDX38rnF2BylBTXCWNjn1E4kkwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyMVoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDfTXQTHHDgCbBC8m1oybg5od+wH7Pfh4duI9m/I1JZLZoz +74GaQZlDG2bTHwwa3ccXDF5N3BbVG5BWGhFKquy37Od5mB88pDLngazumte0DaRC +MvM542TUwBVwLruT2jNDIPtXFZ5b7XMZh+dyLXHbvP9wyEUUBKcSi5kRgp02OYJC +g053dKazEAFtmOHoPNlCWlqqjfpMAXsbiFUBCoXmqYcyRBnNpzV1ET0nZC9W6Don +racpKDIvCGQPvZ6PL1iP/SLGx2n8QQ6y+MwRO+f4A+5dabTJHgLBQ6s3nL8ijPUU +V8jLUAkP4Yzrg127RqfHRolf644d3RqMyxGabOlPAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQBKXvslWP38FdHCrH4uZq6C +r38iUHhM83Bd5PWOWpJLSepoDuAuV6FaLsDVqjQxI8M1sv6uLfBbfsCBZY6e61QM +vWI8vzDiCaUicntxozI02gSedAOyecUksADYkoL0xTPWlWHoD3EwASbwTC9Giip6 +0A3Aja5mgDxB6vwRjhXs2hX0ERTJ0kmXG4As2gPhuqoXYKo5E2y+z8PkvEkjEeUx +laMTDrnaYZjfk1Vq5qED49CwAZljaIghe+Bad84JtEOckAEUteoCtsG7Th2/nQ9t +Qs81jLixhO1sYJmhoYxM8kwtv8/hC18u9qvKxZfqZfa178G3eePvbYNeS3LRlhg6 +-----END CERTIFICATE----- +""" + +SSL_TEST_INTRODUCER_PUBLIC_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEA3010Exxw4AmwQvJtaMm4OaHfsB+z34eHbiPZvyNSWS2aM++B +mkGZQxtm0x8MGt3HFwxeTdwW1RuQVhoRSqrst+zneZgfPKQy54Gs7prXtA2kQjLz +OeNk1MAVcC67k9ozQyD7VxWeW+1zGYfnci1x27z/cMhFFASnEouZEYKdNjmCQoNO +d3SmsxABbZjh6DzZQlpaqo36TAF7G4hVAQqF5qmHMkQZzac1dRE9J2QvVug6J62n +KSgyLwhkD72ejy9Yj/0ixsdp/EEOsvjMETvn+APuXWm0yR4CwUOrN5y/Ioz1FFfI +y1AJD+GM64Ndu0anx0aJX+uOHd0ajMsRmmzpTwIDAQABAoIBAD13nJQGKCwDfrlu +8h7+J+/1VxWsJF9Ld0QiKjYrCufxXZkePJaxp/aI/GzxBuv+UGdPyEW2Z7KRu9F3 +q3raQf7+/1jBPxf6OujvESM9DFNLzgNK8xjkN0U4+q2+r3OrhKDd21HFaan0WtKU +TmlniQfrpoTtG9a+0R6RvrjLM2tRNW/562bPR+tUmo9CHGXIwPWEb3r7sD+8UTf1 +yS/xwq5HWLancjiOfEbNk5hweXI+d2x8Jb8vPM4Dm4frdfFWIt5Mek37aSE/Dn7f +9DxKfYp3wrKX378P9S60gV8qqOTEMQlBQoDOkPJxcokGYtba4Mln6ApASqV6Ocna +yQoREEECgYEA/FMVleoa1Yt/YX4UDco4Bwyh3CNflnJvTMRadKBb7ql1OvK/dSWb +WnTIc/Nmi5yAvfhUCQtzw2jdnOH1be7JQ+MkfYVgpeKanAtXGxMldgv80FtQFXQY +qipGnibAHjOWniomutV1WW6bGBiA8RUdcxh+cVCd7oRiflp6NjnopssCgYEA4o4l +eINvoccBC4DD5ZoHczaqda5qgz9zvh3c6qCOFdHHyCH/zlH75LBzGNAH1IFqacB/ +nfSJsMV657Xp0uZi16YoVPVCHCFA9bFNo2w5ubaYeFdSPQi8ZwbJgmYtAPvyDoWT +UHT81RQ+GSyRaWVey39RcHcsoay1ilCs/6LmMw0CgYA0Bc1Fg2bU8FXq+9uWnELA +8VHN2V0z35Qi97jOouFRa47IAJSIyqAlHj7V6TETR8kjYbexxbKwb0aBufSoHbtR +S9uSJZWvnfDSi2QCKQhoNkCBlNIGGlGbg+vbX5HsqCY9peMmUixHrA4+AY9UJU5V +FI+9PSnSq2jDNFROKdJV0QKBgH9KSndZseD9hPLHmElqr4DmWAPiyWmQvyE0eilB +qFNOGKezopxzp8mn8iMgzyVwyS89vvYqrSoq6pFBvmyGkUaEzuhdHJXdgTgKNIr7 +hbt4glYrCcPNIr3oLFQdwG9rH2dVWZ28/UljJDjUt6a2E/rWQBWmf+ceuKlMBsdi +6WAJAoGANzWzy3eDpkIWgDZJ2+kCiwSAYYmcyh0YhZENiomHdv5SCxAZzpomHI7V +kZlHSzGYnaHaUqRW1TVXN1PgYxI4HKdGlY5kKQlw0aVhUEPwJNF2HhxAltLsFiqe +RrjJMJLNHQwsrOGfhn+JCUlprbuThjUekSqNtOpNh3qqwEAtC9A= +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_PRIVATE_CA_CERT_AND_KEY_1: Tuple[bytes, bytes] = (SSL_TEST_PRIVATE_CA_CRT, SSL_TEST_PRIVATE_CA_KEY) + +SSL_TEST_NODE_CERTS_AND_KEYS_1: Dict[str, Dict[str, Dict[str, bytes]]] = { + "full_node": { + "private": {"crt": SSL_TEST_FULLNODE_PRIVATE_CRT, "key": SSL_TEST_FULLNODE_PRIVATE_KEY}, + "public": {"crt": SSL_TEST_FULLNODE_PUBLIC_CRT, "key": SSL_TEST_FULLNODE_PUBLIC_KEY}, + }, + "wallet": { + "private": {"crt": SSL_TEST_WALLET_PRIVATE_CRT, "key": SSL_TEST_WALLET_PRIVATE_KEY}, + "public": {"crt": SSL_TEST_WALLET_PUBLIC_CRT, "key": SSL_TEST_WALLET_PUBLIC_KEY}, + }, + "farmer": { + "private": {"crt": SSL_TEST_FARMER_PRIVATE_CRT, "key": SSL_TEST_FARMER_PRIVATE_KEY}, + "public": {"crt": SSL_TEST_FARMER_PUBLIC_CRT, "key": SSL_TEST_FARMER_PUBLIC_KEY}, + }, + "harvester": { + "private": {"crt": SSL_TEST_HARVESTER_PRIVATE_CRT, "key": SSL_TEST_HARVESTER_PRIVATE_KEY}, + }, + "timelord": { + "private": {"crt": SSL_TEST_TIMELORD_PRIVATE_CRT, "key": SSL_TEST_TIMELORD_PRIVATE_KEY}, + "public": {"crt": SSL_TEST_TIMELORD_PUBLIC_CRT, "key": SSL_TEST_TIMELORD_PUBLIC_KEY}, + }, + "crawler": { + "private": {"crt": SSL_TEST_CRAWLER_PRIVATE_CRT, "key": SSL_TEST_CRAWLER_PRIVATE_KEY}, + }, + "daemon": { + "private": {"crt": SSL_TEST_DAEMON_PRIVATE_CRT, "key": SSL_TEST_DAEMON_PRIVATE_KEY}, + }, + "introducer": { + "public": {"crt": SSL_TEST_INTRODUCER_PUBLIC_CRT, "key": SSL_TEST_INTRODUCER_PUBLIC_KEY}, + }, +} diff --git a/tests/util/ssl_certs_2.py b/tests/util/ssl_certs_2.py new file mode 100644 index 000000000000..ffacbd4594ec --- /dev/null +++ b/tests/util/ssl_certs_2.py @@ -0,0 +1,684 @@ +from typing import Dict, Tuple + +SSL_TEST_PRIVATE_CA_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDKTCCAhGgAwIBAgIUbFhRlgpIM3M+ZYuTigQ1Vbmi6P4wDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMB4XDTIyMDMyMzE3MjkyMloXDTMyMDMy +MDE3MjkyMlowRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8G +A1UECwwYT3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAsc4R76dCSzj3MR7adSLI+Mk9cX42bTHiu00c964rUXr2 +eGpYy1fmtdiq71kkVFRCtX+m8tc0jA5RCOCJsVKPZ7a2zQVcUkC1HJDCg8lufI2p +hS33zpjP+BePGmxQDhBU744cmFA/TVN+VTEhTlbmhFo62eX2Repbl2coF3MhoKTz +cilUgiCww+DUa6IQk06Zkh8TFJ8iPp67QRM1wEhMEcKrnRNxPGHDxa9ZxhwrmKft +UOFrat+ijftQDexMkVZLUXPoksM/7afjqvP9fkFpQEJZ3R1p3uwSX+oIr4yE/0il +nnLnunvUcWYTmvVcwrV5Zu+IOtV+yBbJxmFsobiecQIDAQABoxMwETAPBgNVHRMB +Af8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAgxl6fGre+yFpKxEVcJ554klsg +Hefei1tv94BDMqdzfdeocTBiYmN70j9KEZr5CoMUhpRh0sESpgZ+6JD506Crpa6y +CwkhfQXwOH58E6pbqLLpG3F96BeIBQ/IS0iGvRaerK/9hybt4NJAyrXX1idJikiJ +h4qck731LOSiCw7T138yjlVEXm4DdCj/MYJDJJznMYDXJrJEE7tDHHvPaPU3Snrd +CeAYwXaZlvoCbU0f0DCyAXS5+/HgzY5qVrvDXCHTAEnMSEQyIhESSL2xmumTGNCA +BBlbMJ6oLn3fOHrB7qQortCwcAQkbk/MiZhAkL/+NDz6gdIZqgXLDDVZxext +-----END CERTIFICATE----- +""" + +SSL_TEST_PRIVATE_CA_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAsc4R76dCSzj3MR7adSLI+Mk9cX42bTHiu00c964rUXr2eGpY +y1fmtdiq71kkVFRCtX+m8tc0jA5RCOCJsVKPZ7a2zQVcUkC1HJDCg8lufI2phS33 +zpjP+BePGmxQDhBU744cmFA/TVN+VTEhTlbmhFo62eX2Repbl2coF3MhoKTzcilU +giCww+DUa6IQk06Zkh8TFJ8iPp67QRM1wEhMEcKrnRNxPGHDxa9ZxhwrmKftUOFr +at+ijftQDexMkVZLUXPoksM/7afjqvP9fkFpQEJZ3R1p3uwSX+oIr4yE/0ilnnLn +unvUcWYTmvVcwrV5Zu+IOtV+yBbJxmFsobiecQIDAQABAoIBABilM59AU9QRZVIN +sMIjvC+f1UBx+iFQlNjZa3Z5Uc/Nd3RhaUVmPbhe+/KJLJvzwfteYkmuwr3XDixo +0y0dAHuju9rXL2DHT1NSTWPu+72P9Ttcj8i3Lbx5p4BGGyKX6O37iMMj/GI/fHda +g/9T1EfsKdQiJ+yw+1kVF12Iu9SETXVSl+xtEv0D0nj3vvSCzHf3wPELNMDigb1k +R43zsbzT/QF03rlMy22xkNWDyxki1EdmkuU93+8DQCso5r5OwI3hGDQzWW2c5nPm +49+m7lHe3apYUxjhbdLAWfyviEI5ysIhf9qwRXZJTHj7qncrkYZMm4wI49TWb318 +Sw/EwnECgYEA3021Gg6lqwfL3++mUKYyGEKVwRFPK9cZU6+xMT+yjPtahnbRlueL +pZ/rRYKUe5iIGYNdHoQgLqsE9hh0iZbjiNLQ0HLvMBt0QdMey+F6vKlzkSiBJsmf +mZXgQMI36uHbuJcm9Dm/nSoxQT0rmHk1rdW0hpJZOtUKMlm1SdjUH+8CgYEAy9bl +tTyq8oMwXJ7dp3UfLtSL4y5ekPLe8o+/cMTOHjaZBDFdPXgFN3DQLqrhvPAQHl1G +PI+zsjLoZYBtrOgIJ0Dt/4N9jijSdfhfZtwpXcVxWPXSjh/hTmY29pD3IDV9sbyq +PyLxpMaCL8BrUXPTpLyuuUUpanPxhzG5FXs6x58CgYEAtruiXcZqr8Dbh09XbFv0 +VoH2hl2hyiBla2Q0vjZ/6HqwI6UL8k9cqZZqMyGeXF3/0dD73MMGiuoMT07H3ugJ +HqhVlJ7ZOSbeRhd79h94DvcjyT+6IIGSB17selH07FMTOMUIbYbsVLJ4SAjEqitW +UAC3kAAm4MwBYh6jCeFUBSUCgYAkuSeTOkXWARqSZTCrvacLidFV8YGp/Yd7DbjA +uUQlH1L79WTF8TyHW3S2I6Udo+ZTghxoRr9qCE/kEXow33CwmbsHAIp7NRGNnVya +rGlrcmnUTB1N66JsvG+EhmNvUxO9FK9bRpFgTT8hGTp8ZzCnCjM0P6iRfCf0Ylnk +69mQIwKBgQC0lu/wkUsJtbMaUDW5v05XoVw3c+aHd3fFX7WW10d4aDqzFXYMjdxT +tKpkYxpBuoco9E1PZMF248wGMsIltykwPvy7QI3gRbsZIW6gTwUBsI3xTYOybu2x +trfVO6ksP0sIoYOJH2JbJfIyzy49hA90x1cIA7i/HdJWRGitJ/xKkw== +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_FULLNODE_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUJBzmO7OHvvlimGi0mXLrh/8TPeMwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyMloYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQC792uVfc1mFbJluKxzWxZ0r5yaKGjR8azECVrNTcegAjLF +Qd7+iZS+7DZwEA6Xy/h8T9DZd3Q1m0Rjlgg73ngO50leCvLqWEvrhueuEIWLdbef +4NaX4qkusMI/orYE/ItKgL86HWkGUELnmf3TkMccohR63vS+Fr6dZDvIw3g+gBPL +P9MlK7sdXq1I8CIcV+2du4jhmkA4mJ278TdQZOfXnX1hhe1wPuyhJyBN1sV2ntLG +TLcRl8WQSIh3j4vhMilEU85rA+q4TuqrOlZtKPTPNGeACwrD7qQkWihsEAKWMdQJ +e++9+CdigL/S3IhMmdTwHHNht31oYl1vAqbQbCEzAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQAttszf/gjwRvkxZs8Iq474 +aoBzfsF/NKCKBNRoTdVwQc6DycJylA9JL45BFhc39tka00ttcKGbXmbnOzltnFsH +QuD/1B4pQOESZzUEcsHHCn7tKxIrCn4wk9OiiqcIwHwavFy+guHFSw71jVFhv6im +ILe/K+wzpgc+pUeYiQlWRSuoR7YABZekL8bjpCpuq1kXzP4bV9pnq0sMayCHdH+q +enRnvo4u6PtMUGdX+bye3sp609uFgH53v06ufl3ylkoK5TRuaOj84yYuJDBrAAG6 +ZYZaosM/CKXoumetH90Sc2rAvWp2HniHe99STfaw1aNK9YV1T9SjIqhzyzoMzGRP +-----END CERTIFICATE----- +""" + +SSL_TEST_FULLNODE_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAu/drlX3NZhWyZbisc1sWdK+cmiho0fGsxAlazU3HoAIyxUHe +/omUvuw2cBAOl8v4fE/Q2Xd0NZtEY5YIO954DudJXgry6lhL64bnrhCFi3W3n+DW +l+KpLrDCP6K2BPyLSoC/Oh1pBlBC55n905DHHKIUet70vha+nWQ7yMN4PoATyz/T +JSu7HV6tSPAiHFftnbuI4ZpAOJidu/E3UGTn1519YYXtcD7soScgTdbFdp7Sxky3 +EZfFkEiId4+L4TIpRFPOawPquE7qqzpWbSj0zzRngAsKw+6kJFoobBACljHUCXvv +vfgnYoC/0tyITJnU8BxzYbd9aGJdbwKm0GwhMwIDAQABAoIBABntdxmX0M1UENeX +MbJ3zhEqaB+bk1niTEJ+R9gp8m9P3lD3VRsnPy1Wx+uNS3YE3LHJELXulEkQsc5K +07fuaAEmRiiCuh85Lr++TBbmkIU8J6gWC4PH8C5Qk3rTpufpLg2I1NffVq1YROJN +i4WzYsPAV7LbLkdKO+DaAqUe0WdNZph8Fhrg/WhjGC5VAajRgfVM7cbnW4SjrQzz +lqoVB3GQPWLgUhHoBj6yRMBKP7D96h+oqUZprn19ilEeaoDVvDMx1Uqj57PGCZ8k +c166dctWvSVxLKwbau20HLAZCZWQylhOugk3sEIceEhkiP95LYNfAZAHiU1Aqfpe +PgKSAYECgYEA8uvqkXwejuFEMGJWo/JUH/pETc420gatwVD222LYYFeNhk3C6k/E +iHa/iE8v9jKHTv7xmlG6ybBkluDGsaK2vngzKpc97nCYfg3mFjS3CN/leZDMwUDX +Bj1XgFhYM9yLVojwVJl3PkA4B/WYmnxgSo9u9oCy9K8HdSMUcCcPQgcCgYEAxhYU +9OPA0VmKYwipWfpZlir5LrvLkwJdcmAV5UhKVXkMXr+M9ByRRLYrr++6SxipjOjO +Zi1t1hB79pxtc2mFBCT9gtnbNvcgEdkxSo08U/uj3xcp1oCZAN9Vu3Dk19Oi3jUT +BAlE1V+Le3b0mPh9IytPjpO7xjWstpi5ar6obHUCgYAH0x+QJh5Z3dmzcUd3KnQZ +P5d5ph3P8BUit/froyhzGf7eB50yrPUYrDKJMnvKA2CmY8HmhaFto7fpwD3GNQaL +5hVH1u1Qw+G1lb8GkhYfPA8JNmfSBcOnWMx7vtzAducqF2keYH8dyzXC099hgoYb +gLlDSWv6Q7UKyeF7p1ZvewKBgCnWbCK1H/qXoOT95Vb9y4IuHWdDfuHT9Ay+QqGk +vR6EbJpudsTwa7ZJi7yjM4O898KtQTrPiY1W+ffCXlOsC84uSeUjQmu/kmRyrTiD +0CQk2B28dLe7fZVzllX6qDr1lka8iwGlO4adoYY4P703bqbI9Qq2JUjd+VavtynW +jxgpAoGALXc4mrAJl4ckfso2OOXo0UHYuO7XCVO3Ll5bzGZbaiRuCYG6ilm2wKRf +FnrYRd9KtvrA100LxTTepdo0tTVcdAWY7STJpBIycUN72OMQOCI3VWSTkSpJHGjp +WBqJDcueUhUe8x5//qsMtXJFgXc2ZerODemObr+bRT1nBYMiZQI= +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_FULLNODE_PUBLIC_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUF2mq+KA3KYyp6bso3F79MnD6AacwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyMloYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQC5wiF41CixaxHM2oHf3nYkuGmLeG6xBdSogQuaSMAw6KCy +AvCfLDBRgfbvWjXV5teUv61+xgyLctNLmJQBg0owYQhhZnwIZK2jK9h5CvquI9UC +xAdgtvKc6yFKlWS435xSt+ja2w5eHsE0QE6U9SL9qm7OnehvF0baDA7CjQNKQ8SR +lkrl0+mQZCEc5lfroYlpOJ1XKrmLhfX4KV+RcNEuLF4roY1TdkUfCOk9Jq8/DrCD +rAOuFaU/GShqmG9REt3HKIGBO254/xoTcSkGIOHOGI/2uZI0MVUDK8tNDwAaDLcg +RAclaoGQ1++jcjnmjvopsqySEJwJrDJMkpqKd0SzAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQCImPY2TwUGre7AM6nzBqrh +WCXTEct84E1jfJtgWZ5xYdVMNd0Ujg6A89LZnc//KW1xVDImmTrsxwHj/vR7lRdr +L7ADGbKOepqKMgUdT8Im67jpMen9Bnrx/q5dtW8qmUMc+EpGwvMOASYEa5jxQ1Uy +Bxpyk3lpeZs5HZJPG0WVFQnNXtlG8LMlmlj1JAEB79hbK7PqPvat6rnS6NTCHGtC +vjAd8mHjVQJkla6u4dV4oI+niEJk12d1yJ9dMSZ8hO3CiBJBieZJ2v2S04N8UUMh +PQEX/yYT94hLaYWYextZRDxNWnjS68Ju1xv8j8GKdMLE693Z6K5kJkspBy0sEtxk +-----END CERTIFICATE----- +""" + +SSL_TEST_FULLNODE_PUBLIC_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAucIheNQosWsRzNqB3952JLhpi3husQXUqIELmkjAMOigsgLw +nywwUYH271o11ebXlL+tfsYMi3LTS5iUAYNKMGEIYWZ8CGStoyvYeQr6riPVAsQH +YLbynOshSpVkuN+cUrfo2tsOXh7BNEBOlPUi/apuzp3obxdG2gwOwo0DSkPEkZZK +5dPpkGQhHOZX66GJaTidVyq5i4X1+ClfkXDRLixeK6GNU3ZFHwjpPSavPw6wg6wD +rhWlPxkoaphvURLdxyiBgTtueP8aE3EpBiDhzhiP9rmSNDFVAyvLTQ8AGgy3IEQH +JWqBkNfvo3I55o76KbKskhCcCawyTJKaindEswIDAQABAoIBAAWr8LuwtqFcJIn9 +rfN45mCOpOJWRgLvq6ONdR471Gpp3+YvgstJXRxP/IsoVPZ3+uMWyyRQxbdIRT1M +plA5gv1hKRFYQLc847RUtWJUvHyuqWVROOxyCYxS/Yw6bX3bjflUli6Ae7rR85I1 +2HBh37ShDIsQdTVXH5muvpCgH5aX18gONUYsQt+xDbnuDW1Cdl6MFYCNCOzL1W5k +6kccejG2aDvNN4f9FZdtvft4sQFp61LGxClZGml9zlFuOav/dJx3eXU47Z8DVADw +OJmtXJend7A29uPFyyoQ44uE0F1SG0dzNmmrxrD39c90w3BTvyGsilmjkr1diRkv +jRHl4nECgYEA4qHmHJlA0Liu6z9Wg+/n6Z7+OnZAfTqvCUayPhD5lKu8WWC6LsI3 +7m2AhkkREaVHWu+9Slte4c5A3stH4eEkSKSWSef5axdlYZc2ST3UNOFAzMkPle2A +kZAYWZ7XkBzptJhztgBiQUBJLrQ1SuXayvnrpgqIIYkeuKuRYc89zWsCgYEA0dRP +rMdz3+rHlGmNWgU038G57qGlWFr6NLEqSITYz/7Xp19hP/bfuD44PvniH9N8x/3a +IlgNqaZLU5IyFzIfYE6yuPsUXC0nNzxcc9FCiLedH7d+247jDOcbbQXOi3pTa8Ar +rkwHTzAbQXT8VuAOcs+cjDMa6rW9IsvHvuD4r9kCgYAqDOAlbkoYcCwEejwTPvBI +6LdDIa3Vjo3rqrJn0b59V2AbNVdWVbMLCkZOpEAGhiQ3O0RkB5ATVbGzpQQxZRTW +ZbN9Aw3EURL+iJAKBu7Y4PKlDKlXqDmyyIm8FzzoAHVcjOzrWCbi9Iqfn9BDlWKD +RtvycwHPNyH8IdlXzJwrAQKBgHirKVrKpSe7hCUkgoangd3AMiY6zbS/NS7CR/fG +dk0/WFPHtUyss8Hn/j6xQ8pbvGHi6eRgURMkMCOSar4ONB8VgxCATBgqW2gXjW+J +g0LOnUyVIR4w0QAllA8hjMOHiJvpuvUUgguXNcVx+oAXgBekr3mtFiiudWOiX2+1 +Py8ZAoGBAKPAc1n0UbvdNjQHllxm9Ehok3hcVvJZRRBtcXytKWwl3FI6/I50/XAy +8PaI4gbysiPgMl+zTU9mIZGIxyMHNy6vr5/3g+ZceNadAsIQnJQLTMM95WP/nKcQ +XsAuSwTFviI2LBxC7Scnpc8JHugzP6a4oiRs8yqSfKXYGck8jaAf +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_WALLET_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIURlqEdkPWX2ld1ZkYRM10UTM191wwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyMloYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDKHNdtHZzYGVJlTaAmiJDMsNdVpyo3sHewUF23K2zMTUZh +2DBX1heO09RpOIoGIdsgm2P2T/+731O6Hi1+pJyLBq/7LtoPX5fZL/AT1uQN5yDF +xBUvmaFmnapEyRC2tg6H/YNBCTrHO3OKWzF5Zn5dZDmS8D5FcnhPups13k6YYklZ +ZewRTTUxUlnorKdDh2Ur5UPHnpxfG+yRYow/prs6sfQNj1Hdv4nsjEwTPwJQVIC7 +xViZMiSWUaYMx0giJe1wggkuVYTBQ6tYCOtbO/FKErYS+NrZ5lUfUzmpzlXIM1bn +Ux+4SwMYddcbedVywMap7mzupD8ZQNCuio9Z7nc1AgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQBghZflsoqx/js1oQC2JfNc +gD5HWNqlS6JbupJr2zt7KaKFVQfXmMEUNcVsEr9PXIGA3vqDnpT3L7Ybk0R9knYP +yGb1c5uRFn5A7moAcJjgwUrDE9GQBx59KRXmK4DofcwinUPEe6BU8sn9VfqjTprQ +WFFLyt941YCxaCCGPgZTyGbHWK2pTQY0aBSQAyNppk+dsca6l2HkelkmUrS6I30p +r4nhJq+eXmehrtn8+zXT6qxmr/vw+F7cwnYyxLswBMc8ijcmvBbkcSEcFbdP8TMN +A4FFYyh0rIijlKTsoEyvUP7LGBBJv0d3RU30jHPBm4faJ1wWXLm6p3hQEJmLzM0i +-----END CERTIFICATE----- +""" + +SSL_TEST_WALLET_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAyhzXbR2c2BlSZU2gJoiQzLDXVacqN7B3sFBdtytszE1GYdgw +V9YXjtPUaTiKBiHbIJtj9k//u99Tuh4tfqSciwav+y7aD1+X2S/wE9bkDecgxcQV +L5mhZp2qRMkQtrYOh/2DQQk6xztzilsxeWZ+XWQ5kvA+RXJ4T7qbNd5OmGJJWWXs +EU01MVJZ6KynQ4dlK+VDx56cXxvskWKMP6a7OrH0DY9R3b+J7IxMEz8CUFSAu8VY +mTIkllGmDMdIIiXtcIIJLlWEwUOrWAjrWzvxShK2Evja2eZVH1M5qc5VyDNW51Mf +uEsDGHXXG3nVcsDGqe5s7qQ/GUDQroqPWe53NQIDAQABAoIBAHiLmzFJaDK7Z5lk +IxDYgwSStNwxR8zPQ5O1Wy/Uhp+Tt1bESpEY8BQP47CeODRQHRHlnElcjXrQSG/J +b/kI1RVWd7+owgZJTZgML3Slxn9ESxepS7mIN+usPdGo2n8fNquFWLOBfb02iEMN +AQUXTGcHUA+DmqBxFbD363rFjLr1VM0+N1vjw20auYGpvWOFWbhXjgWGBHuJhRNn +tgkXSsrOsPzbGIWw3GEt/Tlw50ehVNTSGA1jdxUlNHF7sGfFLVYjcO5AjCx7661v +w+w1eBwWI+SrYxXp1YgHlaJS1/u0vCvQ0kLcc2Ry+zpmesvi4aKVodKXWn7ZEsKb +qv+pYgECgYEA5pchaX88w6FiH80fzF+mcCcDnliw83kRtIWRE0XmhIL5ocnWuOcb +pgSFr5kEw80pPDt+IYrKHxyh/fVZV33n8JJ2fjNsHFwpZykIDhO2H2CmkXm+fTxu +gD5HIGzPx9qxvni8xjjXfSFCGvKuwGa975XGL45P0LsT90iAumkNnAkCgYEA4GJd +oRpSdxxDYuTvDHkvdlgTYSFAy/4njk7MsfnEsbn2l/p/xDx5H2nKrql/7J26pHKc +ODZcQXaRGaOi83K+vYAmHPC8zdmuEgh4hlSqJ04IEcU+gadxeCDh/RcBUPMvtBe8 +nDl15HrGi/k1MxsdrrL4Un1mrvtwvfjbvX+tZM0CgYEAhS9TeBiqox/qig2zSRsS +CgMuvt3hTq9l/4uKEMS18WGpB76JzACIYqqIALV0IBe2snh2UK9WMQQbuJBmivdI +6RXfZOMUlYjRzSjQ4ziVX6g2bR4RXUpzVJUkBeFzXa6+LRKVjjQ0mqyD/wae0rhF +CkXK05ryFNCJJrH00DZvSukCgYEA2n0N3Jxr5k2gFEEFwGiUXbEflbmyhbBCRiDW +0wp5i/Gfe5dRJ/0WmA8EbBTiWr2viweKtHXCWYAhhAzB1DpMHuwUsKN9xRgMlsSm +z27LjKA/3UXqOoeYRrgGNdJb1r4mGj/uyVRuRn+Cq7OLKOtjeMQOZwxymzp9Ko6T +Ma+MYJUCgYBeY597VeXh9vwMlfgjIz0vbGjrIgGSS0YqM//SgfziHDv9Bh/sQY/J +zy18xbkjdbCulGGyro7zNDXmOcf5CUsgei2cc/vgESQEZpoMSnVvG1MWQ30xq32M +g4FlmGzdEiQiRxA8+9lzOPJRcQdUgS+Le7fOWH85q2aXrv7wp6lT0w== +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_WALLET_PUBLIC_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIULNQ5Bq+zPWOGPtKDkjFf9aHtCBYwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyMloYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDmIHrbtqy5/pMJq5NZmVRFbX8wHSciOLyUwWY6K6BgOtZH +8WaKoOS03X8KVnWKqXGcUY9DKWar8iQ+VbGP7YLw9wJjr4hNsW1DqN7uzxla3XIF +upT/tKAhZpiqOHKstGEoKwjVD3Dym5xmSP6ERQTfMolo7WaakTpANCGKW0tKM2Rs +6pSaHV4GIvbHSer/apt1zr2IBFQI7Sj9KSXZ1OhOZERSE4FigTAJvW5PEcuVsjwR +LB11vpPKiF7YkLmsj0MQRHlFqZukrSdlvR9ZLaAREJLgk7SwCwUSYGQEGhjiEkH6 +vWXT/35IYCkiYNI/vcdCQlN4xBOOAXgO3c1HtikFAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQByHJaF2w67743OsMwKbQ5j +oWAt+rqivYmqJ6tVKEpCAZu3aMPdchcoZdfWRfh/tRP0SwkNE7PwIyRxo0qynrPO +gXhGR17ezrgz3a5AzQ42EXg0xb75QzpkXdrOZK9ynjer4jlMx+a+IJniYlSpyCxc +AnEKvnH22S4j1xCYC9zMDUF6Jqm51h+Pnrz2sHr7NyRu7Lq9cJ62rfyT9kFW/aZX +1/xAwzw2GmyYRpw7ZEiRN9Ipkjs8Nn6yCWOvs/wa6el06mXB/i/IvOdMI7/B3PJS +sOvOjmtIS04kyR2ZB4Qhkty5WSHrF0Se/9WH28L6a6bjFKLRMKLTRLdeZ3ybEOFN +-----END CERTIFICATE----- +""" + +SSL_TEST_WALLET_PUBLIC_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA5iB627asuf6TCauTWZlURW1/MB0nIji8lMFmOiugYDrWR/Fm +iqDktN1/ClZ1iqlxnFGPQylmq/IkPlWxj+2C8PcCY6+ITbFtQ6je7s8ZWt1yBbqU +/7SgIWaYqjhyrLRhKCsI1Q9w8pucZkj+hEUE3zKJaO1mmpE6QDQhiltLSjNkbOqU +mh1eBiL2x0nq/2qbdc69iARUCO0o/Skl2dToTmREUhOBYoEwCb1uTxHLlbI8ESwd +db6Tyohe2JC5rI9DEER5RambpK0nZb0fWS2gERCS4JO0sAsFEmBkBBoY4hJB+r1l +0/9+SGApImDSP73HQkJTeMQTjgF4Dt3NR7YpBQIDAQABAoIBAQCTXdlPOfwCX3Pp +jWYeyoGctDHurbyRvaOF3xOHzMg213bBO2VfAQl0iSMBi7xZv4hxggksCScmlTmA +cX/zmzVu+b8d9xpiJmzCFzIr25NxDL4nzQP9e73PpdO9rchBsIFHJ8fQKMM7mUre +dYAHU+t6wvIbr2s1MCsNUlToNO5R5MBRrJjxqvDteLMrcH9Q6ae6f4sWscqs/oyy +yXa7Qve74dGh84PfNCDeXljQjXRE0080OMWBfY2F5Oy86Vf42q31M563ufKIcCBM +lMLKBZjc5L2AqFZKOERITBfax4GAoV3xfiRfClgAOA6cuQ385tp18LxG0IYf8Qcj +xQuhvyoBAoGBAPbmLyianEJV3j7gojm6dl5rcYblzqU6rDrCOGNSRh0N1bGBP/T7 +OEe9bi7apK/09JN4sm0c4LhhyJDanCfaRQ9DRn0OKU0plELSWGvJ3XIAiCoeZSg2 +I40A0CSg5SDnsFYIBdBqFi6E3oLwM3VsntQz3WH9O81k8KWraFdgMgipAoGBAO6c +Bv7k1hmvbc2j4CLdcf1hkpDGuv5bCBG/cPIg7EZUmpfeKnkP+7TyOzJJJufKE4U4 +GOjkN9k9sYoNb7VubkWpdO74ylIszqo7bTI/6iwY8N+ZpJmi53poU3Fp2PTOJy0g +UcDDtaOiOaD+gEF1+5LxC+uUonL0g6bUmS9lpQr9AoGALGL5e21ARlS1ncw4nfQ9 +r3/VaxEJc37206FzDbgOzs5b6ot3+gzn803E7ztzfAanqZN7UE5uv/ckXZZPmIKP +A81ucLEJD8w30UOLjeU+oG4kDJ5mRTJmdcT9pngeeSnt86mBkhRgZICSmCuitKuQ +akngtOsXwzcwZDhKi9rJY/ECgYAoC6mk59UC9I1SIPnCADZcVx7ZC3FgtPhyuhWD +nYDqANL9P/0S2lrdMHY850gPSLvj9NlBZOP2osMEL2MbKRB6wojsfna+OeTpbxXR +hCaSBhGPBWM5obyFr7KpayFNXLf4e98cofv/HX/chDoUQm/ZZnkgrY5iCqV5v16C +NOXxJQKBgEHFazeFqKKFnIc7oWhZUKvU+UQ04KKGFIuRg0IKhIjxySQGqiJQIvJA +DfNrpsARfWpfCo+dSVxBIcSUATKvA3LMVPaaYAZSj8n7YFA8M/2oCegtM68QM5DZ +Rc8M9EbKkv9/LE48h4gc9ywIw1PD14SGq30il8wldWRSYyry7/9t +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_FARMER_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUTnXTzqEOILFs1Y6LuvX7UkxgI1EwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyMloYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDWr7824CNwPS2WHAlYs2QpHVtj+nMn/nl02e3JIX9utKfz +2sVXy9dT0sW7VcDaeUYBi6pDZz8DecwGjvnBMAfh+bkEx9gCs1tY8Iqitgmekoy7 +e7+zHJ1zuo0J+p/zeW+Lwg6D0FztarME2gm2tuw1c5FTIfWWnwotYetXxUWCeKNc ++TgeK8WnfUjRcULzyFRAA16n6AmqihfcfVmT/Jyr2eubKeyCJFGUx6jIClb+OFUU +0h4VCZzhHOx/R/X6B9HWJTWgdoKUxQcMt1sdlNuntOkgg1JsJPVPM/+NnijCdHEC +Wsk6MGCiVVH/vtR6ek3sDitsJfk36m5NjiDzeK9dAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQCj7q/5jVqInWNHENfekLWU +u9N+cSMhDMyq/ANc/wm3OcOAYUODAmfgZDuGAmHW73BSeoEuXQdsnxQTCM0bVF5v +9hKcQkLY9v0tez7xcDwicFQfg/SzjCehbeKtKZGnrANLg1tEm2QFUuqmepbhatF1 +NREBmX1EtUrZ8l9e0i1oDk13eOJvdkZ5pfQC7UOK2N7dAHQmsWJtJgHpGu9KgdDV +eKRf55N0tiDKFjumdFucvMQVaOtuZX+4bKrLuDE3y6lG/Bzt+ZHZkbCsYAqCtubp +PfGozr3jW+plZvnRz4X2GxQRWFw2jVBcBzu8f8Ahp7q62nT9bEfJ6fsKqwhX/KSx +-----END CERTIFICATE----- +""" + +SSL_TEST_FARMER_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA1q+/NuAjcD0tlhwJWLNkKR1bY/pzJ/55dNntySF/brSn89rF +V8vXU9LFu1XA2nlGAYuqQ2c/A3nMBo75wTAH4fm5BMfYArNbWPCKorYJnpKMu3u/ +sxydc7qNCfqf83lvi8IOg9Bc7WqzBNoJtrbsNXORUyH1lp8KLWHrV8VFgnijXPk4 +HivFp31I0XFC88hUQANep+gJqooX3H1Zk/ycq9nrmynsgiRRlMeoyApW/jhVFNIe +FQmc4Rzsf0f1+gfR1iU1oHaClMUHDLdbHZTbp7TpIINSbCT1TzP/jZ4ownRxAlrJ +OjBgolVR/77UenpN7A4rbCX5N+puTY4g83ivXQIDAQABAoIBAEOfWgiL8z2wV4KX +1C3XW65Dq/zC77DiTBmNZ4PaBEy3pMt/1ndAItQpaNUIPtXSK0XjWz8d71BF9gj+ +0haS7Xi1cxzZYeX/3r8WDWURF9iV6rRHV2uwkLvaQACrq+RCFOudtXq5j/vMhxT5 +JOQjnCV+AIGCCdxmvgrrc/jSj4F3sFiIpMh5z7oufTmT3vmcx8dk4dSCujpMywHm ++xFj7OQm3DGVY5sqF/tevkD2OTal0pWHj+EfvDaNWVWa9buCsjy8h7urPplLX+pd +ie+BIS1sID5YTRmFQrxzzbrDuE7lXB7yV47f0sj/+23S9yVqIpUIEqLhIFnrP2rG +iVFbISECgYEA+3AoJOcKXvWZYOHP4OeF63Qu0yUq780sk1I8DUSto0idoJPJZG33 +RB1/11is+DvfQDt8/C5SpfNONvRLfDs3uKyJ8aCps4dyf4Mp97nTpibRkXfUSc6T +1cR6HWw9d6Om1bqDAgp1HceWM0pGRg57tPmLzFxO91Qh65Ttp0p2w1kCgYEA2pTk +QYUzoOgbJhQAkYnzGSP4caYst0lUq/rfFIYvL6eejoLcqowst0iX0tPfD429wJcV +P45/JGN2yQz8QrPbctflsUCPVQ3E521oem5FLNcsKr97oxzy1HF1PWjHX6xhZ6/7 +tfVXaP2cCAeAKKCVvph3vDI7SzfU8ek0DK3NH6UCgYEA5KUdHFGtMKUOEPfHXbGs +KmzAl+lYnjBptJ43Val6bN1/2aIKpXUKQbrBokZVJHbtkS+HfJtzNM2H9pk4e4Qu +K5Va64s6RrOI/0N7SnaFbLYoJKxfM67S6LV+hnsDemQrNngg4h44WhhBEesc9F// +RpcW9YOLm4W6QsxvQI2KaiECgYA33Kd9Kzqnm8occC45A8VyHmRHP03cRcxy63mJ +uEVk63S1PTKCD7L54H6Urfsq8XGWP8Z5aMSLmzPna/8oWOjzr8OPCk3XUd6Jusdh +yr65GAC8qBVD+YkBzEFHQXj6tYZrRmmQ9jOxrGbtEmWpfGjovfSTz06iCZHNhWj8 ++Ioc1QKBgQDTA8/Hw9zpG80MY5e51AHF9amNOYGa7/OkhDBAs+U0zjQcF1O0IlMK +eooQuhSf6RX0/DI2JgSs4Yf9Gx3jvCGW4w4LGTN3veiyQeLocqbNlGijaz+R6bcP +zOfGG0s1vdq/thQbnBerx9sjoyE9RIeOmf2c7Sv3vn+uQDTR7QIpVA== +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_FARMER_PUBLIC_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUR24x4LgpE1t6Nmpk9Lbcm2p9bZ8wDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyMloYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQC2ye4u+jn4ldMnKFJTrpndM/mgaOI9irWCQ9/kES44RvYz +SCWgNIqU83XC7siCkDTAfmQ5dPDgwExGp9mqbBjLa3QuWLiBSmstk/RdvWyYTyv7 +Tk3BXjmYU6nbBr0p310+D5Jw8RKB7O88wM603E5Cb48GKmh71DY5+JxwaIJbFATs +DmPXQ5tIO+z2WZmzB/Zba7pYOILk1XDR4YPLgVdvOGlaMCkfMALYKtdW9ek6hIFS +oPlm/mbzbIld80PYP9E3Sf9iX55GVytuIt5B0u6PbUYFcrKyNld9K1UEA9fyNECa +KUVe6UK1Kc+LYGdkRv/qAmMG85zMM4CHs1dP2jRFAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQAREhwgP+CXusazRom0cMeb +KfKZIY2TMHWTYceAB21vUDSor8dfDNZNaJGqNDWJiwxMdqCa6k5u4ClBts2silfs +qYWGQvUpygD9dq9Nazsf8GATZJ/X5OfVKSb/j9OsmikirngsB3KfoOVUz77aALec +yuH9tji46NtYUXEMYsCvQ+8SAmLQNra/jqzM/SHAL1Rz2JNxXY2ldBpsWuwaau8n +j4FqvQtT97zC/RV32B/Io4/LJnD2bvPd41Wf5YutTOJXUFuVDQPD9rI0VkZk87a3 +o7oGLlusWLZ1ZIqTylCNVtv/Lz0JXwImhYIsDcNswVhNIWiKHSJkR4aS3gqVt4Sr +-----END CERTIFICATE----- +""" + +SSL_TEST_FARMER_PUBLIC_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAtsnuLvo5+JXTJyhSU66Z3TP5oGjiPYq1gkPf5BEuOEb2M0gl +oDSKlPN1wu7IgpA0wH5kOXTw4MBMRqfZqmwYy2t0Lli4gUprLZP0Xb1smE8r+05N +wV45mFOp2wa9Kd9dPg+ScPESgezvPMDOtNxOQm+PBipoe9Q2OficcGiCWxQE7A5j +10ObSDvs9lmZswf2W2u6WDiC5NVw0eGDy4FXbzhpWjApHzAC2CrXVvXpOoSBUqD5 +Zv5m82yJXfND2D/RN0n/Yl+eRlcrbiLeQdLuj21GBXKysjZXfStVBAPX8jRAmilF +XulCtSnPi2BnZEb/6gJjBvOczDOAh7NXT9o0RQIDAQABAoIBAHu/0BpL3A142aBs +AviWf6KrenfoKisCCopri1trakA9gpcLZDXG/H+FKBfV8ze5i406xH3FtwJiDgHT +x2BCmLK3R8vM0vTtPgx4W3tPMAMZrqwPwkKEmTeQhLE9Kbn8d7L5deoyu5Xso7cc +zZTShcUio9DHyz7yV9f9gWP0zTP8/qYIlWTS6laSsBKlcemsMmSklopj+aR0Sbkl +8qF2rlywc4994+ZmD/TEQMSJ/BhPfUkQqCQdoOj2tPIOPtXFqckvZkfvYC4pWJLl +61GXxv18+y+QB5uMOUcTQ7vvLVPwbfXd7TS1bhUiOFSJw/fxmMtOOoGDOirxnfLD +8vBeVUECgYEA7BVUmT35dGp4tEvvvl/RUJtmBdj0prgMOjadO5DX0ZbqNYWFakuw +p9X456G1T3lgRqRp5YddJpCFSYH8YnKoOSCzZ80jIUbjDW/0mnESZCb5e8TXyj2d +TxqZpOrVkuE+Dadn2O/iEROz/oBXN/QpG58GqptsDuSJ+P3OjVW1TnUCgYEAxjWa +V/NcMWqY61TJabAy5wlGlF78YjdhNjS0a1vq4NgQcO7k0+Jd0ErLkMpTN1B1t+Yw +MW2BMz1E58AFKUzHd/f6eeJGzFjEV4cirwViNPkf0X7/mIGAAR/6DZLay/IxW3fC +8kWe0cqKFsXAa2TAtgr0q7ji/XaBmmRihqo1NJECgYEA2d3VdJX837JiMgDh/o3u +XLUMMdlF7ZVrs00zTQUeJg1floTh2nZ/UGPmj9xrtiwM7SRNlcSV8kDswCl1AGVP +WWkUJ3boNI12e7AVy3ENx98v3UiK29iAk7+4irks35995JvY+hpmRdwo/3TxCHIK +fbMEM+26iwPdUCrpswSljQECgYBZMf4G3ij6w/GU1U7eFMCTX4U3d5czy6AfeJBI +T9De5dxgNeEbyJ5XAZGh0qqoFbkjzJ6bX5/oowXDFJI+B6MrxZgeCUm646dXjCR7 +hnyvQfyjlfcHdh9YYj9wpEl5xb+YXciIpfPWjMKfhNYWffyK71sze7wKO64PAGWm +HCpdQQKBgQCftgQ1tpeLhjiA3PCkYUpj0XnErP0+FWhLq0pEkwsVKfprxoMO/foY +Ku3/XVtRat0w6/CdQpmN+pk9PDFle/rnD9emxqkeo8pkDxIo7bYSr5wF4oV9c36j +TTp/SFMfM0rMkuoMeNM2uVXroCXOExSbMmBcWfthFwoCJv8tOCGLhw== +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_HARVESTER_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUQsRmXdBM+Yt81O+qtGzJpULDwhcwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyMloYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDc2huslrMBGoEROWCsfrd4LwHPSXrehqP8uthUzpsxhxJP +HU2bDi4Q56IiAaXl33txbDnKE7zOE205rp0h/k3mvnbo9zBFHZakIZuySP5L/+ry ++Jyld9QuggpD5mWWHW6lCmUtGfY0UJJJbr6KPyMqzzQ0xrjLAPWqVvNR19Iapd5D +GsWgBSpKyUIwPUf8JpkFvSv4udZlrONoMA+/PAVEeCgOKbWdwi8vrpR43VcUPSe0 +TbxE/bWHgOSDW1FtE46logzJLOzvx/k3Lhvumfrr+znNsZyjk14peqkdAUtOA0dm +VEhs/wHthVkw/47MO7G96DzB0NI0++anRhNXLiAZAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQAVNEaquHBV5LaHJDGrr66p +xPuM9sjaahXr01VfAFBqFGzr+8ZH2T7VL0bg0vO3aimM+Kp45eRGM+gffZ70ab0q +94CVT++3rhoro6nXhx1U1lHRNRqVOyGImwMbz/MtmIAeme7My+kZDcNlFg+MC5DY +0pjN/EAoBslrhGp7yWzEh6oo6cKsYajfqcrewwMRrY7shqrpH/fs9gAB4uzryCVV +54tBj69C9JVi0d06YijpLcUT5czScd6yPlX61XKR9lpKUDyEGbRU+4f4DP2oJzzq +6tsME2HXXcZr2fsxkBeCjZpDEsKPMxsHBnebwSTNESCLQgdQc7wJ8QVzgOkAGoNg +-----END CERTIFICATE----- +""" + +SSL_TEST_HARVESTER_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEA3NobrJazARqBETlgrH63eC8Bz0l63oaj/LrYVM6bMYcSTx1N +mw4uEOeiIgGl5d97cWw5yhO8zhNtOa6dIf5N5r526PcwRR2WpCGbskj+S//q8vic +pXfULoIKQ+Zllh1upQplLRn2NFCSSW6+ij8jKs80NMa4ywD1qlbzUdfSGqXeQxrF +oAUqSslCMD1H/CaZBb0r+LnWZazjaDAPvzwFRHgoDim1ncIvL66UeN1XFD0ntE28 +RP21h4Dkg1tRbROOpaIMySzs78f5Ny4b7pn66/s5zbGco5NeKXqpHQFLTgNHZlRI +bP8B7YVZMP+OzDuxveg8wdDSNPvmp0YTVy4gGQIDAQABAoIBAEYIa+oox468Bt2d +YkiQzkEwNtSyqmHSNEI6Rctu0Mu911J7KlbXAkieC03ZU/A3E//9n72y0JZQlrxJ +4M4cxDs9fpuVdxsTrOdTOPoqJ2mWN7zglVvrSb+NwQ+TCfe2UZXIXBkXOHmSazEa +CxXvu4kht55cvdCx9zUS6Ym0dI26IGBybmr7zwOnCg7Kz4WTfO2JB265mLu/B6wV +V+W5cLPeh8bDKl0Za4+v4XXdzMmUliBmyGyyhT+VcuqQWwFwp2OrsPKAth73S4rN +pxMd1DrOyrZtAMgkdfzkNTJy7oFW54ns6OxG9p/p/t5N19HCL/kpUGcBoGUQFhXb +YxrLy/kCgYEA+xQRNVJTQIqoK0tCEv5dIyziD0ZfzsDDhc5+qFV+7uKsfdyRhLGR +GBP9JfQ7Yc0U1W2omreUGRSwVxU6YpqMQQS7tEQQhiQp3yHuP45mVKG20qqFzMDG +zeACkTOSIyh0BMkaN+nxkS4PnqlRaqUsyuBRsomof6y87g86cELreYMCgYEA4S5c +uf76WFOg6IFwM+1d0P2EDwa+jeXOvZI9fQ44rzyUNkMZfsvFw7+h8hsZHihrKhTR +hCemvEcgDIfiCmsCYMRpoI4oJUQGPCeD4mCu8I/ackIX/UCsm7OmoYyNX9KEPJ4C +eUCocYaETc31wjOyXqHzjIGynZd9fCFHRUjyeTMCgYEAoveRe9SlqNzW7tL4Xi+v +8GbvjA6XnTFNN4qid0yXCSMj5sXFxiUjEkfXJk1yRbbBcQ6uQ04o2GavVra2oM3D +f2g9FXKgbxEGBIiXhbU+Amy8p9x64D24TGS8Bdc9YYieVYTaebRlUWBeTxODfv8b +JcuX+5SSQ9yw/KvxuHAXO1UCgYEAjhG2JxMb14ZLyuV/aQOlYSnRm7lhmB0ZZrlR +WgkS8lyCFgoXl8vwpiPNhPZbzo8prY8c8QgjRj6Jld5VWsVQ5sSE7+tAoOJeHK3B +o53kQZpA9D2G7R1UyZd61gnbWE11aNk4LlAA9j8sUfpTx4beTp5XDpr1mj/tx9cn +JCt178kCgYEAttYPBKitz4UVAVfFNdJk68N/Tuw3KoZH06l0TCy1Ygof0S2GZpE9 +iWNVNino9Jw+AjQWK0FgKUVurK6fiBTlvtZHZk72nJRTGpeIGwa2kZbeL3qn9nQm +bsUE07d0gn+0Ztdo5iI8hB+y3xTN1tq4hIa6xCXKHz5Dnytqkon2h/Q= +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_TIMELORD_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUXa72z76zyiyFs6cTa7GIU/m41T0wDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyMloYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDTSy0YIggCwTF96eLR2AxXmpXg30F1eG4SZzr6xWd5sXiL +crUjSc6V9YZRPF+g6ODEryNYjSxWlYf0EtFPD1MGLW+7ssUJrWK1BX+FFCLvjQX6 +ShfRFoOqmkAhWp813xYaWihdLLv2UglphfekREFhRw5h8ikfk5JA3AgTFVGFjB/q +1pZ7gWVamhjjzvrbhiXdqcHDqMuNUMQoCuPLlMOq1D59nzYhUUYZOTP8aAOD2dj8 +a8plM6OQwAuQarqt9DW69Tm7AxKbFrXnzXqTT4Ccp4rEJxscV8OIP2DVioFLV37B +EWq3pVcarIJku+Lxcpf4C7jXvhBUYAFVeQLMmjzXAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQCSc1CA9sp1CEbEV+7+8EOc +5v3DU1rTEW4dG94MiWDXH/s8B+nWb2lsXLd9Gmau8S+/c3c8rW3eejS7YZ5w7lnb +NOdc8SUwPDM+/f89UCiEeuk9fUIe9EK3pER60Fn4H/0ZXQhVob+1lGScnIyx5vr0 +Hq5rPO99qnZ3kgLeYRfHQygSpFQ28XHOkQGVSklgCbCBdE0lNWKpaw8oxw+Ki2zb +tSGF5Uoel75Y3e2ZqtonZCuvE9Q02LGmDmmfSrRuS9jW2hvV/CHpRnv02GMPQT4+ +JTKtqOeY6+hI/eg4UKBYbGLY2Sn1irKMRFwAzftgB2eeU+Bhxb4ioksFlhDvzkMf +-----END CERTIFICATE----- +""" + +SSL_TEST_TIMELORD_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEA00stGCIIAsExfeni0dgMV5qV4N9BdXhuEmc6+sVnebF4i3K1 +I0nOlfWGUTxfoOjgxK8jWI0sVpWH9BLRTw9TBi1vu7LFCa1itQV/hRQi740F+koX +0RaDqppAIVqfNd8WGlooXSy79lIJaYX3pERBYUcOYfIpH5OSQNwIExVRhYwf6taW +e4FlWpoY487624Yl3anBw6jLjVDEKArjy5TDqtQ+fZ82IVFGGTkz/GgDg9nY/GvK +ZTOjkMALkGq6rfQ1uvU5uwMSmxa15816k0+AnKeKxCcbHFfDiD9g1YqBS1d+wRFq +t6VXGqyCZLvi8XKX+Au4174QVGABVXkCzJo81wIDAQABAoIBAQCK0TwqO8mJjcXn +VK9JcKkDMTPBDsyh0eJ3Qs00DleiUx/AdjddnNMWIL4DRygIvpdVgT82oWF87Tbp ++yb6yzWfvGBJL/VWG0zbY2ZZV1ZwjfVccCtfmmwcvMTfZPtu2EdcPtmABbDj7xfx +9SszCAjwUU+4t/GepkSTSjwf1YS+uJRm8JmEVJdysRo2p34hps0UQEQSQNpSEPsA +lG3aYtR0spLQUXXoDGBxJ/mHFjV8mlmYm9bVhfAdjLSaaY4z+e60LgiTUVApyLuO +Mm8OP86aIxK7Vy4U+okg0ZlzL3xgsXiReCfLDR4BaTbXYS80wsIg/tqB1soLzzSO +qGsMXMIxAoGBAOxnudUSXgv+bboPdA8cxfvZfJNc23/uBXLCtnEMHQCYok0jGro8 +o/Lsl9HkPRyX0DQXCaOwO3CkEXx2i3J8NKjTJuyLvfVefqclpGSxewq8+N09Ins9 +wxOow/3nJcDPJrbdgZP48cIqFleNbmF/CmsPFf5HWb6j5tTFhFWcwR8/AoGBAOTO +nBDqb8HjWXBOGufitEG1kUwjbsZQ9wnfbqdlugni1QurY1X7JlbAG9QyOQmLXngK +6NQVqIkvu0aWdiFavg3UUVaTnVmgKmmmO+npCvJmHUvwkKQaDpbScF4d1wLfaWe8 +pkaoaXUjuTQMF/lPh7xBDNMPz3cvSqjO+TDjrJRpAoGBAN/rr/2+lA5QRKbEFG5s +Fqvi6Ti7771ROx/khuK1UpNKABGIeryEy9ZBe9x3j9sRgUiVua+uMd8TyOxXbOZX +95khx/CuvJM5mkXAReKKqb0WGbOVQJ/zdF+er2ZEF2J11HnJff8nAfej3d24PSFk +L/4QIAjmlH8ayH4pSJu/Cr4JAoGAB7ZTGlrck3t3S0rGq2Q/0Ssuj5NuK28VNJb8 +YtYR9D1aEv+e5IoHm8rz5S4gMAsrjv5HJMbqHF2ogVOW/b7SQyMR7sog9qFobJE2 +2caKIOuCN0oPhgh+z2Sedv2ofqdLJTz9mcoZa+JdXry9niCpIZZLuV2CD7FfYdtA +GP7DlWkCgYEAllfMeW5fWwJsio/N8PDQRISlN8yh5HA9DPfl343c8cGCx5ENl9n7 +QOqa6xCTtTWPdDwnTQ8oCf5v/97bGPwpWrYxFxqzld+BIc8UHdtBEMwqzptvw2TD +i90QCyFVv/7y+izXLsrTUimCAM++muTYyXhfq6Z+trMr0PzAz5yxNmM= +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_TIMELORD_PUBLIC_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUfuygNa26jw3yA1o8IQkLYCW31D8wDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyMloYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDlsfb6//PmK5sQ+v3l3+IIjiTdRcoIxHB3e+fMu6kZPD0r +xLiIbQtw06CZsX4yVF3DaiIPobGKfI1fv7pxyHM5OldOAn52SFIWjnU5PNGfrw7H ++6Z1yCnB6lC89UO39ArbfiNrYGpf0Lq99ECHd/EjRP/mMd717fOQ//2zkhzeenXC +9jRffLZhkhblPCyFepVBAmBQPKB0rNmnhblwtueUM86LWPq0FrDp0MfP/HbgPQ/v +VxY1Pd0mBQ23xXnKCQDg4buXAqv9IVd/kNxzG7H8+5k2Ji1QSoSi301XWWa66kXu +svWc4125avodKiC5t3Czbce7kLAq8mvU32GacJ+fAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQA5M3qJYfjlVzdXzu/JTmG0 +4Mp2G8cjAXcSWEPwz+FI6gImLrA4tZz5MER5WxMQF9ALKKPt58qi3hk1b8Ko8OUa +wq802DBkg+LpurAmPNht3IZ8s4oTkSWOP3b0FYA+/rYqM4+EGBcso6cGSl6msayc +0eyUO+AlWYg+2Bw9AiDEiAiLMaODgmMh6ztmycNluMhP8TNOzQRqFlrEs6YvL/z6 +tryToIqZVjIZogcU5Bko8KMcLOEWbLprSzSd4CIJEtuao3TdbU40TZmhUgcorX0h +GKaAgtGOxcYjApuavtzr3BA1X/HKLOUNOMDo/27lFPSL2PhSBBRZGGU56FQm2LB4 +-----END CERTIFICATE----- +""" + +SSL_TEST_TIMELORD_PUBLIC_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA5bH2+v/z5iubEPr95d/iCI4k3UXKCMRwd3vnzLupGTw9K8S4 +iG0LcNOgmbF+MlRdw2oiD6GxinyNX7+6cchzOTpXTgJ+dkhSFo51OTzRn68Ox/um +dcgpwepQvPVDt/QK234ja2BqX9C6vfRAh3fxI0T/5jHe9e3zkP/9s5Ic3np1wvY0 +X3y2YZIW5TwshXqVQQJgUDygdKzZp4W5cLbnlDPOi1j6tBaw6dDHz/x24D0P71cW +NT3dJgUNt8V5ygkA4OG7lwKr/SFXf5Dccxux/PuZNiYtUEqEot9NV1lmuupF7rL1 +nONduWr6HSogubdws23Hu5CwKvJr1N9hmnCfnwIDAQABAoIBAGKeWDZIMoW+byOJ +P/20dC3MKdO/JRFCli8Wyd1DLUUicfkay0f32ZOlqSyT4mTliCRgqyMe/0VAWMql +XP2BXMdTXyylMXrDbks0+uuKS05OMQB38W5cdGFHo0ad32dZple6/qYZjVJ6IdNw +zUvcmXHVLDG/c8UPVyYIYvr4XuC9zwc8QyY6HS5FLL2YeuDWXMFqhzNHRPuLUebP +cFHxM/doBHuL/hOvRJiwuaG//od+f/j/2tAjpg9Aw0+H9Swyzjbf8SCh40B3oNHH +C5FfU+F1C5vY09oq1HCJnuNuYyKz1TxUHcm6FZQbbaFcqMYK9u7Ys++ZCm0N6RJo +JMSP7uECgYEA+UO5pmJGai89V2/OgghxCjJCHy2eZ88Cn3iz2G6DU/ispiSOig+r +i9LRmoI2s9ip0mNL8oVjXABHpbohyWTn6PNFZq74VNvfGFyH179nXPJZwNszLueg +Aky47Sjmfbr/EZwPsaSXWNJVx+c0o7AfJuvDOC3PDvRIvCooB+RCwckCgYEA6+be +kZG8Hd2ys/hRzbQ27YS0Rp0uqLWHda7HxRVUcIB4eYFVn/kb3Ll+WJslO9/FZL7w +e+IWrU0N09qsXLXGN/4Tn/W27kf40F1kIcHaUGmgEgdQ9fmcBQIMGxmHr1OX8J0o +CnjMaqvdHkwQgiBAVoAtZry/ci0IuRJCP465SicCgYBgMygaM8FzR6oH7cmoW7Os +uLrWJ5gD/lvHyiC2vegHZ2jScjdkxylwvDtSw0BzZoIcBWCRR7OSFTWRm2VgwYXT +XNgDCjIjJfxS/Zsbw+4TbCEBkleNma0iVhPky60xBxNb63wPFjOm/v5GOVASgG11 +avYb32oTHmpX3Hk4mnq9KQKBgFrBNi0wsPuYeBCu3uHRjDQykpx8CiBTvipzNF3J +B/REKJUuQb/KuYJgRpBWF6wCOdG5d5FheLHxa/luLlN4hyqxb+FhSaBARiP7WfN2 +vcOj7zYgZHBNOE3g7MFcQAwej9y8yVPGB3aeafIm907ok3fz1gOpZ8nIM4vnz17e +TMCPAoGBALrKGVHHstz8NNtCwgthyRrOsh6shgSQrQ545wi6FVyXT0AEM9mI0x6D +hoErSrwA3WAikT4homKcF8skuk8IEEureOBLN3mj8YsrG8Ga5fUmGqxSMzf/IBK8 +pVPqryaTVINRh3Yk9EEZixme+u/YZ4D0owyCdph9yG6ojNqMr6oI +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_CRAWLER_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUWOfPmS17sBQ2KK+z+yx4bCQgBiswDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyMloYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQD9vVqZL4co41Wj3v05i+zWg7J+9ODBraJDHeEaw3T7fXDD +I+ZmzNDER6uWmEGqFOvXHdwi2b0mDa6+LLOB2ksOiLknMME7rAF1BusxhYLypF7t +8uQXq1aGQ4fb8LN0llRR3EagfB9mQEJtLHksK50Q39tL+00L/Te56zvjwJR4IwME +MifXGQF0TbN5ofVPOxVczpo5cULvVyLst8S7C4qCs9iHxWFe+o7CXlJRtedNL00+ +SdqkTllHXeEYM3nmSoj15telQDTW9AJyHQUTB34qEt8o2Oa6nrrEBRNHZGlBmwcF +EJt0EPpp6A4lHd8Iq/aQAhrweud/kWzde9oMBx11AgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQB0VYSPGkkpREqo5NXcXn3h +vhVmZwV+M1ZZac0/P5g/ILxhRSIWn1JTLd0y6eFznEPOYrQHZws33jsfZlU4OyT2 +8Ypxp6v2TRZjY4RpEPy85VAqu5JU0+FZpJkHt4PXaLSnOX3Nd1XZi0AXtQHLvrTK +qEe0A4v0hkwAQK1Ds02udRn2eyHSZVfMVchMPJSD2wrlZ5Mz7uIWNu1jdTrNzHsY +sSBZlwrOzUYSgYP9YucbzEquj/tmn2rhZXNDaFZZ4/Rw1kcKH96vBTIxKnf/ocla +bYmP7j1r3Auw6AxLR5zUtybAj2M9tdE1c+qSP5mvN/ckCBhwOP1XuGjHs40bAiea +-----END CERTIFICATE----- +""" + +SSL_TEST_CRAWLER_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA/b1amS+HKONVo979OYvs1oOyfvTgwa2iQx3hGsN0+31wwyPm +ZszQxEerlphBqhTr1x3cItm9Jg2uviyzgdpLDoi5JzDBO6wBdQbrMYWC8qRe7fLk +F6tWhkOH2/CzdJZUUdxGoHwfZkBCbSx5LCudEN/bS/tNC/03ues748CUeCMDBDIn +1xkBdE2zeaH1TzsVXM6aOXFC71ci7LfEuwuKgrPYh8VhXvqOwl5SUbXnTS9NPkna +pE5ZR13hGDN55kqI9ebXpUA01vQCch0FEwd+KhLfKNjmup66xAUTR2RpQZsHBRCb +dBD6aegOJR3fCKv2kAIa8Hrnf5Fs3XvaDAcddQIDAQABAoIBAHmcMTnN76eHHVqG +2zKjf4VoZTo3vyjUQTqNrX+YQg7ulgbQw/JtI3mPKAvrOT18/XOCWilR1jfQwvle +j0IjD2oN2T+RPGuPCru97ycy6AnHAcBlczBs6/E5mHmvKTMtgYgiXK86DgqBrdSJ +RiIHXSXjREVsUgb2+6hdt7x2ZjIVEWTbepfJ+gA1y/4GHgrntC5CtDkm+uh3W9rM +HnI8fmnpTB7nT32IynVbQy4/+ZHflDYMoKLCIiamvyk4L/mSghYD2YFduoLlfAqG +r/G/lA5gzpnrzuPmudIil//yrEeEcir8xkeuCC6dYQU/T8AKJh/o9jEcwHA2SBVd +XK/UPgECgYEA/12P3hhGEST5j61iNQQtSr5p4vX12o3Ov2FSJYi/qzqDyz5GU1r5 +EGYej4lBQvR+C98hR4bVjJp+nciOcAejtaa/Oz+UU8nyfS6Z2NXqV0REZyAEeGLo +dVvUIFM24j/mNI2B20kJvAvBAz8Jy9efbBphTA/38JxBGSLKj8VTGbUCgYEA/l7B ++xXACCPGeF3UCWBghjB4vsut+LXiyf39b1n7BCaHiWGoiVKF86yUlaJu8DGxMIDx +U5jP0hjSww8I3qBY2K8iAegyMlH5O3RspEGNL+mIRVFMditUKRHT2wUsf6MkF5os +GxKPAw+nFheUmLsdin3Z4B9TgDp7s01ejFUjTMECgYAd8NZhb+8nK3KnSejt1mOJ +E/JOThBZY68N+VcsV3BBn0a8mbydIVl6dr62jZ09QHVW5v576G85YRPfZBhvQjYL +olVhmP7HJDJuQvx+0/X57WMnxDVB+DbRK8cfUyJoPJ29I6pKD7I4fhhPTSDU6Z8j +iaRGysiDSY7IZ6/gU35+0QKBgBt12VUk1PVL/2oRHwngGKuD4hUe4eoeoJwTyl9S +BHI/QpSHMW2ZthJHSEcbIQTXKHzG/sZ6kbzppx8dqWR3RQAnb/FqwriB9vj/KZdV +6EsK2AY7r5h2NwC0Bv33AgXJc+UEZMyLZVr8Ppp48zbFxHul+HZki1wldCM4MAPQ +UR5BAoGBAO+FGYfEeYmTVo7deE2GstA990IbuJxYUbXapSDGZehlu6calWGSg+dd +l/hugFQ3HiUxps8veJIHP5W+2LSgsN5zBT53I0YRvfK7/juAqG8Y8bUcDU2nG8BQ +/v8mMVAlIqNeGoD3szW1/6BBnvHBi7bKrAEoZGcIiWzb89av7upM +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_DAEMON_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIURUSWnNQeBOShv/Sz9MuDq2t6VVwwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyM1oYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDEDI/6MSomdp0AjdEuQgP6og1bMKOb/X0G9mEpvKjyIl+8 +SSijACXVRhSG7Nx0tU61pebjXF2ax6jgqpzSrrlv/w6WxSmRcqbrdZFSUWMG5FFc +dYfSKyrMmF/p0o0kXQRaBSeIhkLnGS43Zna1w5b7ak76EttzQd5uiJ6bG7vCpGn0 +hXNgdXtRIaruK4TWt740F2vyhIRtsEIUURaLe8MPT8ey4lm4Fz2URnUvfdMma7GS +rZIsT17kw7SQBlaqoGXslYNB2yMdnAWRJzuh6cvnxQax2Mfciwit5fkFQlnVPdZx +W/rLffyCCef9Z0m6WArb76OTwcCq4pmwh3dfOi/ZAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQBu0wa4sLoqzpBt71pELonc +siZZ+dCLuFCXvyt8nQcN8arR7Dd5/eb+kn1AFNRFggbnf/z/jTc6ujISUrHIzAkz +mVbuwzyA3gKzZfyTgv6H8N6qvqLPBTWMgs82UXdKE4gXuheUT9+lAp6z81FbuFK9 +PSX+gebb/WwxdEP0w7A4mws6udBnOpPW9i0ArZKNKgWl8RiaVxoJWt74AG/t6HIx +TV2iONXfZJO1k4rJb/jsmOavfXDYeEVrqXJ40f20Ex1wWPGd5Hm/yOrFutF7nvye +mHxa/DuK2vpqPuwcJLYr2FHatClZNiCza6JLeMq1CWgEJ0Io9fte87JuP4ZWi4Q+ +-----END CERTIFICATE----- +""" + +SSL_TEST_DAEMON_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAxAyP+jEqJnadAI3RLkID+qINWzCjm/19BvZhKbyo8iJfvEko +owAl1UYUhuzcdLVOtaXm41xdmseo4Kqc0q65b/8OlsUpkXKm63WRUlFjBuRRXHWH +0isqzJhf6dKNJF0EWgUniIZC5xkuN2Z2tcOW+2pO+hLbc0Heboiemxu7wqRp9IVz +YHV7USGq7iuE1re+NBdr8oSEbbBCFFEWi3vDD0/HsuJZuBc9lEZ1L33TJmuxkq2S +LE9e5MO0kAZWqqBl7JWDQdsjHZwFkSc7oenL58UGsdjH3IsIreX5BUJZ1T3WcVv6 +y338ggnn/WdJulgK2++jk8HAquKZsId3Xzov2QIDAQABAoIBAFHGz7PUGDntFjZp +8YxzGwfL2vhjxItH5IQn94WaXXqK3hZDCgFcCWv6DIvvd1HzYv6gUjwahi0PBaGZ +aUr0eQN/h81aNqmmAnyLEPAeZMk2Kb4AhIQlX3fyQ2fcXOWDK1KFfeUT5ApT1ZRk +WDYffPYodQMZJJLiMe83a7lViy93+1/6fT/Rr1taSnIZOp0qsYlwKoi5Yzi95mrI +X4sppl3QCRKUQ0sIOHb7L4jI25yLxP94yLM6f0F2cV41flBIz4y/oEi2MFVRqF1f +hI3//VpZEEqyqNsSKzUriJf99EdcFa/SpkI1Gn4SJwep1kYBcbJJ2/L5m1metMA4 +IaKbYtUCgYEA+yYWnIkesYbWRreNp3WxcSm5y3kVXrf1ijCwXeKPNpEYLR+uCyaG +eoVihfAO7r39R+q4DVEC19uPO9AuQYhiRWgSkOTBxj2oDKQCo8DQDhzJLLC7drLR +a4ubYbpX/JwaTf3cnghGwTgGpxur2HMH1eGDm/zxSfTg5C3k4iljsRcCgYEAx9YC +nEWnkzG60pvjckghcTvB5NPcBUdw/UZB+gUNFfhpfXbOxvKf7Y2FkAwtU7PQD8gj +SqCgvSBR0mjIX4wDv+qj4bg4tw0XKHq/Ab3A4wOWwTfvca3swrhjuk+yFvn2OgYF +r/flx/jJmqJANt+AAbM+79eQt0JOef1XK84KXI8CgYEA9OAiU+ZugvHRo4n9GDZt +GMVyXq5k48HCK0dl72Sj9rRqOjUVYdHidTvw4F88cBPXDXQSmQlQGF//vHYzY7oy +9zGOSLCDL2OWoxyXZkrtkZUHogeAATBBePbaPeOPPsKF811+6qdvNZ4G/pDAcX9X +OHd193YdXdriHBmTfeCVT2kCgYEAnt6vOJnPvZ+9jQ5N5l8d5y3A2jDsLG49j725 +WhGF87+H7izz+wqMxojKdFiZ+H9tm/5awEuvPmxhYEvZcMyZdowOObcBr9WCYbYk +ADhhDpmd0oKro87H+Y4qCsatKMMUU9DR2LHWRgKuFHeFYZqR48gi8v6HmInoZedY +1AR8DFUCgYEA1OGjovhcCK3UsqlN3tGVqS3fZieFBJM7JrQ7NJnsHw3NeUc3qmQo +trUJEr862rWLazcnbRvy/Z8WgwwVOEvbSFpmigTLRQMxhvw67NcCqwgh/A077r/+ +9JL8ek8G8zw0L4PSjoChSwE4hM0QxhFkkwPxjUuBuLmd9XvwrHKJg+s= +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_INTRODUCER_PUBLIC_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUPwUT6RFj3c1lZddwvYGkWtbwHzIwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyM1oYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDPobMhkfO6/94LcS9llcyUkzxSeRcWOnmtEXgJxwe8biHW +5CWrNu5GMiCFSs6Z9os8M4Xl1s3DvYCfxiG5XaA7SqLX1183QMS/a4zOPbozuVL7 +tyLpGx511o6gQrHMPFIqc50SVPeW2gMd/PW9WcYXI2rmMOGMfUhI5kGSZFXIWZzT +DRdIEyFYsMh0vNPuMGhcuWglXD1Wv6LGEoDsvFnmvqzbplxgZeSilvUyEMhxx/gJ +XwhsdXnxwVcdg7DWA1adELbiwx4TrA8ws+r5EOK3uvmemxMhwcjMtKPU3LvafBzK +R5MkMQpxY4RehqO5TiCAsJXMrkLoKkMGCtfW2TBNAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQA466vxCP9fhomcXQolSuIj +speXwl0h7xPZvT+0dk+HVJbr/peTKMlg7szvwbNmjJHijwlHD5oim+GB6+WBiSXS +G9ceIe7bBLoPRizsw6b963u8jvKHPRXux8HNspYw18MV8E4SxMB6Sqtk+i8HWgcU +kSu6wjQurRjBMuZtu7abRn8gGhaRWMbu0TRrJlYRDpI6zQJfo+HWc4Wn8knJ0PGo +BqLKD7kCrTqp9eES6WUKMBJ/xLRdIK516I8gw4mQJxO//Hej0o98bkFihcSZMT0o +y4QKE9FN6TjHYPTllouTn4G8e43PnqYT4K8pium+tIuO61W+4OsgvRnk7ykFJpgj +-----END CERTIFICATE----- +""" + +SSL_TEST_INTRODUCER_PUBLIC_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAz6GzIZHzuv/eC3EvZZXMlJM8UnkXFjp5rRF4CccHvG4h1uQl +qzbuRjIghUrOmfaLPDOF5dbNw72An8YhuV2gO0qi19dfN0DEv2uMzj26M7lS+7ci +6RseddaOoEKxzDxSKnOdElT3ltoDHfz1vVnGFyNq5jDhjH1ISOZBkmRVyFmc0w0X +SBMhWLDIdLzT7jBoXLloJVw9Vr+ixhKA7LxZ5r6s26ZcYGXkopb1MhDIccf4CV8I +bHV58cFXHYOw1gNWnRC24sMeE6wPMLPq+RDit7r5npsTIcHIzLSj1Ny72nwcykeT +JDEKcWOEXoajuU4ggLCVzK5C6CpDBgrX1tkwTQIDAQABAoIBAC/N6gFTqksmuAfo +YmJAMB8RGzVd3dvnULZxLDMMGRLgRHhGhQm7lvagee5Wf+Tg58PPlQeLAksL6X5f +zsnv7YFZOM77Llf5WJM9/uhJPALGq31699W1wbid0q7BTFBanwxZHBPpbivUPB1+ +bVHQRpRzOhyPqo5/FdJ6+SPsZ/e9h809qKLgqDP9qX8H1lcvyvWev8+63JmTWxJx +d5+J4fOZzfdOqH7D7gqSKlS1f9QX/9mfsiy4PnXZ3lQm1C4l8n1t9iHcM7Cv1hVA +FELsLWTUVUPhn9BgWaIqbq+oSHzaJfWexys7v9dtO0ndO7uFKm8T2hHMFh5mkwkz +jd/dD10CgYEA9J7Q+dc1haY3+DQXHDThhEh2iFyOFWVj3V2a7PCLBqy71CxHr9XR +qVakwiG6Q1Bnwz/XmjKUWq3veXkVErmZOAABU+HIFA9QAO56cTRyyfNO5NTilE30 +VE20rf+zGOupyidMXfKS2rAjfK9RPY/48eKlT8mWj4oEIRIatTUQ9dMCgYEA2Upi +ddMcXTNMDIVe9pDGLSljixlU3EizQPmRczlC/tAC/VAPqqr6FPHrxWF45qhpQAIV +m+zqHG4Tlsq9GMI0QjgAwKRoj/LTJNsejxPKWw2m70XC0N/7OqrOENdNBmtTA1vD +MIuBU3t3u/apGNoAXq2Lzi/qAzxF5wz8nvf8zV8CgYB8ft+IZ/j7Xg8aKRih6F/l +/SMmX0SIqyNaJCuW7w0yhnLJlwec+8tKzafojVXIsIE+o51NAvTx+ZfpULBi6UaL +c6U5Va2IWAq1jqmON8077rJ2T8pJBCuXRDzyWTKDOl0dX0bEgZIv7nkBGhDUrhNa +t6i+pLAVuX5EfnxNQQaytQKBgB0MJZdsrlkDN4Jg/e3mMqfs5YK90ytTiKsB8eC+ +YedgdVXZsw50ptPP912+hwQGtXM6FBtxE2bTjEjXN2os5AGKLQeTsuqzYmiF5lLo +KV/8tjk8USvNFW+lT8DOl1xpUoKbbL97lsFLOxlkgvxwgCYn+w2IODdQgmO9x7OU +oTXvAoGAa1tJJy74pmt1INKATcDst9nJzZ0KqV6p9nFXwWwdjpv+qvc3JQ90LtJN +dYFLGEzMhp1zi5fSez4SoA6m574UgLOJCZSwWMXyC02a5THmRpCzLQ8ZZfHFEyie +QD+oEewAG5ucttPsaj2iK9YI3RonLcuQGaFuvRqsAuNWMwQyDtM= +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_PRIVATE_CA_CERT_AND_KEY_2: Tuple[bytes, bytes] = (SSL_TEST_PRIVATE_CA_CRT, SSL_TEST_PRIVATE_CA_KEY) + +SSL_TEST_NODE_CERTS_AND_KEYS_2: Dict[str, Dict[str, Dict[str, bytes]]] = { + "full_node": { + "private": {"crt": SSL_TEST_FULLNODE_PRIVATE_CRT, "key": SSL_TEST_FULLNODE_PRIVATE_KEY}, + "public": {"crt": SSL_TEST_FULLNODE_PUBLIC_CRT, "key": SSL_TEST_FULLNODE_PUBLIC_KEY}, + }, + "wallet": { + "private": {"crt": SSL_TEST_WALLET_PRIVATE_CRT, "key": SSL_TEST_WALLET_PRIVATE_KEY}, + "public": {"crt": SSL_TEST_WALLET_PUBLIC_CRT, "key": SSL_TEST_WALLET_PUBLIC_KEY}, + }, + "farmer": { + "private": {"crt": SSL_TEST_FARMER_PRIVATE_CRT, "key": SSL_TEST_FARMER_PRIVATE_KEY}, + "public": {"crt": SSL_TEST_FARMER_PUBLIC_CRT, "key": SSL_TEST_FARMER_PUBLIC_KEY}, + }, + "harvester": { + "private": {"crt": SSL_TEST_HARVESTER_PRIVATE_CRT, "key": SSL_TEST_HARVESTER_PRIVATE_KEY}, + }, + "timelord": { + "private": {"crt": SSL_TEST_TIMELORD_PRIVATE_CRT, "key": SSL_TEST_TIMELORD_PRIVATE_KEY}, + "public": {"crt": SSL_TEST_TIMELORD_PUBLIC_CRT, "key": SSL_TEST_TIMELORD_PUBLIC_KEY}, + }, + "crawler": { + "private": {"crt": SSL_TEST_CRAWLER_PRIVATE_CRT, "key": SSL_TEST_CRAWLER_PRIVATE_KEY}, + }, + "daemon": { + "private": {"crt": SSL_TEST_DAEMON_PRIVATE_CRT, "key": SSL_TEST_DAEMON_PRIVATE_KEY}, + }, + "introducer": { + "public": {"crt": SSL_TEST_INTRODUCER_PUBLIC_CRT, "key": SSL_TEST_INTRODUCER_PUBLIC_KEY}, + }, +} diff --git a/tests/util/ssl_certs_3.py b/tests/util/ssl_certs_3.py new file mode 100644 index 000000000000..cb3bf0677fba --- /dev/null +++ b/tests/util/ssl_certs_3.py @@ -0,0 +1,684 @@ +from typing import Dict, Tuple + +SSL_TEST_PRIVATE_CA_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDKTCCAhGgAwIBAgIUeLxDdxR+RmiMMvvCxRfsQJAL7vUwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMB4XDTIyMDMyMzE3MjkyM1oXDTMyMDMy +MDE3MjkyM1owRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8G +A1UECwwYT3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA0Bvshi5y4VEw+k32NOOMFZJaraLoJx5N1WOJDYGqSDPe +7ICPjz4IWDQN+yPrFv+ms+es3Ys8WOKEOMWR8U9mAxG21t+n8LH7NAMlCRNscBZ/ +Si04bqfrBc3adznorQLtShOpYxEfRdu7kpHo6+D+4Av96qMpMLsPKiZWjjy9O3ai +oNzymj0mwlZ/1gaxpvfmvuqNNR9Kog8o7crLbp/x0ZPiU+Q0y6Kj4saZwu5Nq2cO +JqLn6ex8w576/UOAN4u28S3m2pVgoYkqGqJSXORUqZj38Ro0zAYywIHjiIfhLp5a +dhqDyf/IUCawltEzdXoOp3irYaV0SmAB8+e2ILZJawIDAQABoxMwETAPBgNVHRMB +Af8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAchOBNfaAIoP97Lmyk/S4Hu94I +bGqj/BT6/q+UnWYcIt3UO6/d9a6WO7xtSPKiuObip8M6UQYVKQC8YecnAvXFwI6Q +r4z2TdGx2MjsxfQSNX0BYASlv5EWd3LYJs+Ayi2lxh+3XLSH+BK/7R1nL1MaN161 +vAN2rGOo5J1niWyixyGK/jrKTtsRTxDcpFeIYdk1r/qGiY5uxwg6ny0JYHXYhSNm +AyADKnZoXw/kb6+mEHJHZaTR1JpNHkQ7azhNYyVMUseCi+5vYuzA2OoxSgBegSj+ +1fRUZI+j46hkr5Qspw9P++dDLzEGgarbxruRf6VsosU3sSYxq6LC+a9UHXNG +-----END CERTIFICATE----- +""" + +SSL_TEST_PRIVATE_CA_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA0Bvshi5y4VEw+k32NOOMFZJaraLoJx5N1WOJDYGqSDPe7ICP +jz4IWDQN+yPrFv+ms+es3Ys8WOKEOMWR8U9mAxG21t+n8LH7NAMlCRNscBZ/Si04 +bqfrBc3adznorQLtShOpYxEfRdu7kpHo6+D+4Av96qMpMLsPKiZWjjy9O3aioNzy +mj0mwlZ/1gaxpvfmvuqNNR9Kog8o7crLbp/x0ZPiU+Q0y6Kj4saZwu5Nq2cOJqLn +6ex8w576/UOAN4u28S3m2pVgoYkqGqJSXORUqZj38Ro0zAYywIHjiIfhLp5adhqD +yf/IUCawltEzdXoOp3irYaV0SmAB8+e2ILZJawIDAQABAoIBAQCDVHvpGbrpsiEk +dLqhGdA3dMrAtQOoXBlmRpAg8+kP85wEyATQsqb1crQ3/qzHMMJ02glfLhUBSsGC +SjwVerO30CAAbdg/rzIF2s4uchGGksv1daAdRN6uJQBvKR5KwIQasVm96PpBTa+L +iYTiBnUR0r+EqT6/P+0L/nG1BWOt98bITaxO5Cn2mXGP1ZZ8iA9RBx1pxMAUDG4d +5Y4+Xt8YMP8OfEeVIFO/djRTh0OSTugSGwjnJReF+clX19+dJ/aBXOUHrLdNCV8c +9GjqHDEy6rXIb9N8UY6nVVvzbC/ypY6k2VtDhnWUYaLcpuHekmrONJRKXc0lKR6w +8FDACpvhAoGBAOi72c+PH0FjT2pyVQLc/Ht5UDt0jvsGPEa+EvYYC+ZFEViXj98+ +KGxtIrc6Jab3Si/eCMS+qPAO3F8wC1J+qHq34g6B2v2SH3d5KOWXZwMFkEcM3psO +xBawVtH5suhTK86Lq/qi2EE5yT0NNlM+NM8zyhYtm/w8kjnly/5J53DTAoGBAOTp +3+mvSyah3pBTDHLlLUw92baOAQRMR+7DHOmtsyVwzbZXzES1IVMt02ZH5FNN2nk7 +8XUldTNUkvnHPrGVogI3d8DlWctfh7ri0T9SgydudIOP9ETmdr379uepAHQavE8q +fyxi5o1Ypp69rPWZWmicTbiNyhAoTqFhauA7XCYJAoGAayDlLuSLl5a2HKKKPSop +0lBSPTv3ANeq8UlXAw6ok5NhW61QXYuIIfjOjRbn9AZKkOQclyvIKdA9YleELrH8 +rZhtJw5hFm2nrGAKEjzx/vMVqY7j/O38FxGOtVLCJqz6MjYasOE6uDN4TXECe6jb +uDD3qePOtHnROXNsxh2Qul8CgYAq87tA9NRMDmldeUfHszrZqG1WdLS6IroIkfG1 +4xLPPqhKw5Sfe8EiA2I8Odcczmnk/5th2MJx/DeLyJf56FK6yb+doStFHsqwBWkv +0YKsfmw8V2GFIB09rq54b1yXbIDS9e1g3bnW4cB53qs6dijhohpvO6OjfnyqiUXt +hxXWSQKBgA//yaex8PP/QQNzTYVgFGLmy+TuI7rbrVtVRs1tUzpitzL66Cd4qdL5 +qKeMxbohv6aeXDYQweemm5QFvrP63syIyphe/ZPwF5q7V5ZtFwUVwXYW96hwRdVk +lA+tw7ba6RnXdDI+i2PoHfmdP89WJWeucidP8VcmhY0RiAYpG/vd +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_FULLNODE_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUFhEfo/xtdcjm/XhkbuouJaZUBEAwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyM1oYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDQEygAVJgviM/8f0wuEsmCJoiNJ3gvrX8EckcllVXeyg23 +iXiIadXqtCdPOXQlJHw2fZ838o/WuJkI9TsrtUkG3e+VEJKku3GtDez7gDufVZd9 +e7jmqbO1mMwvpbFwEUzR9IumsxiOqDVAzSJIJGnWKhiOLYkXdKI4D+dyniEMKVFk +Y1TcI1huQngH+dBYujHlJZ4USOB+xVv5LfgrKYiZyHdKmRchIdhcjft5b7gBp38z +helydqA+mEU8zFqL5iwpNOALmije5IQKDrVHThIVmlg+G96DakoVBcpEqttfHb0N +E/oX3v0yAeV6IJ94VwCsSzndHt/KEG5PjsqQ60KHAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQB63tKOcuBjL8tjnIlDozpm +t+LRcNiubKRc/rhTBYIoO6cTMN61ITe2FvogzNAPR/wmsaDO/sqG1oEHrrnJ7hXi +zNVj3GC7XYlqXvoRsCTdqoZE4ATPVZkJqhDx+/5IruWo1Q7F3nPzjLcS3mpwu2xc +Tl2HBEwCX2gTuG6lB5DUztrgGLNh27pqRLoGW4fzmVtHD/z54SFsQvPw0flYMRtc +qA/4eoWLvQNGoqsfA2F+eFTlXiPr/jp3jFVJvqixlwEN1DJkTi7hfMTEty7xrlZq +dZUmYxygqt4sy0AMbwCbemYJmvONlW8lN5GYo5NjB0BaMYsGMrehY2XHZg2npy+U +-----END CERTIFICATE----- +""" + +SSL_TEST_FULLNODE_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA0BMoAFSYL4jP/H9MLhLJgiaIjSd4L61/BHJHJZVV3soNt4l4 +iGnV6rQnTzl0JSR8Nn2fN/KP1riZCPU7K7VJBt3vlRCSpLtxrQ3s+4A7n1WXfXu4 +5qmztZjML6WxcBFM0fSLprMYjqg1QM0iSCRp1ioYji2JF3SiOA/ncp4hDClRZGNU +3CNYbkJ4B/nQWLox5SWeFEjgfsVb+S34KymImch3SpkXISHYXI37eW+4Aad/M4Xp +cnagPphFPMxai+YsKTTgC5oo3uSECg61R04SFZpYPhveg2pKFQXKRKrbXx29DRP6 +F979MgHleiCfeFcArEs53R7fyhBuT47KkOtChwIDAQABAoIBAE6dPjrJu2oSPcq/ +ac/qhznmRydVp0IUQe4zIxBfGL+BBae3h9O6cPkpMcTsBybVXxzTX6mquo+81Q0e +YknER6ARkCh4x3731x40KbpoG566nu7pJNX5fg15eoPyDVUzJBwbUfwcpIWZpe0i +0/X/1AD3jKmDKM204mleEOssNX47G7z5GlLY73hRN93+LAFmiVzIRsNAmkgEOVKV +NI7kj4Eax+1i6LfU5EeSi5XbqaEopWyxuUg/WtNFJzaTtVSnZMqUvMS7R0b3hWqL +KeJCe+H5JXn/GUUiHUcKaFW4yTXLEGMOSXrxevEUt/+PwEKByVIa3Y3ljVh4eHMg +eSykGAkCgYEA6WiLDxSPmSYzQRHkD968bztk0x/XSEbCoJdS8msGjwApJ7eZbLBz +hW+4k/HhIPez0atU2TBm6XR3YauWxtu+p93KjvqieMHgudZ95+4hK3jkUTpnN4Lx +MNfZsEsUpiZ34kv8sjJxr56gWPBdMUdyAorRqeQGYua3Ddg6nLFXbZUCgYEA5Dbk +PewvLZLX7lbN7Y7r+uSLIOq2CuQLMirLuDKooaQFLdjhbxZqpQ1UlLIb7V66PVTW +TJCWr9M3btWMwUSvWquyCJi0ppwCRB3JOiO66eUqUwjT6gv8jXy9HDJLRc8jHjbF +Xvr+g2IxPr7tpVClBIq+NJnP0vAhPN0DGVGM0KsCgYAEo/53y0nbmqXUOl3VbvFC +KOUlSXHHTxjZhoiwpy4XM8KdtonHXm69jW4XCu0V8bbSiVyDgPHa3GTvPTEfPQk6 +Xy+CzjriucAVEc2pCdQBAENR1h5tPR48gV4joiqD7yndBvO8O0KFYlr/ya+gpjH/ +GPF4Nj9mQf4LuWvY57G8TQKBgQCAfZNmdeteKnZfIAqTvUuKGFFpOB1E5n6TQVsw +G32sfK/Zz2ml5SYoReggTGPC8vnC/FgoBaSB3FcylRPJ4UUltNPpWSklQWNZPLgG +fwWHGVsKI0dFWHhapSfIj1yoMmbgZRAdWQ4hpRB69n7Q/CXc98z9yrgjWMYuAXX0 +NGEnPwKBgDpMnmFVf9YqtIrjb2dfUFXi/xRM/0C5/fKOxLdMAqelQrHUCzkfjnq5 +sRf+TU0gV63g0vmDkrHPDew7NAr8m/cehPgELvtBvouEbGNQOuvNDunVwLU6vaIh +OqtmlgNu99SfZUJUg8pIOD2oAJKSSg+iw11bnPuKJxPyWd9HtMZl +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_FULLNODE_PUBLIC_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUO/5znSza9Dx7FlysyCyStw2FDwQwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyM1oYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDFUvDaklLh3JqASKA1I6H/cULbcPCiXDy5CTtXJUzSRc8Q +ckjI4pepWm2/qPKkmiW+6V3irdc1sGF5ICB0mh41UAu5338+0nuQ2n6LWChL4OG7 +JNU4uBIbf5PXmlZhj8BHAfpAi0rvXIdlEUyVhDnjfnOnYK/2Eivh5gOvnmoHA9EA +BI1KCvhIdv9H4Vdrl+iFTL42hPe1X4TBRcg7HuFVZ14kzxWQ4x9Ep/tzzq8eaLSM +rlrRmBlrXMgN66ib65deEbuGPxVdHwY4OkR7hxal41ON6sduZ7d5ypHWGUwrT0eY +Pga4/b10/trBHGlk7cFmLmqTDVEtPMgrHFrtM4YJAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQAk17NlkhSre6JbKzlUdYB4 +GOddsueSKsnathkvevXLM5Q2zhY1GKdeT6chlflcgLs1q810LwyAvVD/A5k3jOZM +YxR9mQMAARuk1qZsxeYfu1ARZWEU77HJj2foWw0r5+1FmT5vCPZoqnINElXHMbuy +DtgYUjAS/31pqHtFKrqFAV3IruQyosYoVOFmKb419s+7ST6u+FQZpiouKKnSpqfc +0K9+O4pcCvPgcrj8rTK5cxLeIYd7LwL9grxE9DrIJhsaoKUkwM4+tiimr+7OSYov +byhFnXiLH9MEZgJXtV9jwQJideTpcI7rdVFVc4+tEP9A5Prru6asNC2UsXbg4kZy +-----END CERTIFICATE----- +""" + +SSL_TEST_FULLNODE_PUBLIC_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAxVLw2pJS4dyagEigNSOh/3FC23Dwolw8uQk7VyVM0kXPEHJI +yOKXqVptv6jypJolvuld4q3XNbBheSAgdJoeNVALud9/PtJ7kNp+i1goS+DhuyTV +OLgSG3+T15pWYY/ARwH6QItK71yHZRFMlYQ5435zp2Cv9hIr4eYDr55qBwPRAASN +Sgr4SHb/R+FXa5fohUy+NoT3tV+EwUXIOx7hVWdeJM8VkOMfRKf7c86vHmi0jK5a +0ZgZa1zIDeuom+uXXhG7hj8VXR8GODpEe4cWpeNTjerHbme3ecqR1hlMK09HmD4G +uP29dP7awRxpZO3BZi5qkw1RLTzIKxxa7TOGCQIDAQABAoIBADJ/m2wUbmmnD1Mt +QvLWf6rjzXxjVvH2MQQZvLn8rcBSZT+MP9xJQQ7yOYwHLLG7UVWeW4ybeKwgy5E4 +C5ZLqtdx+M5EyEfHjh/wCtqWYRmqH5rJPlgZo2iuKaPPt7OYGlkRxH2oKDFBuNTA +rJzHDhmOTwLS17VdySUyvFbBb9kDSQwaH04yRhsGoMuy+qmBc75eOCl6FtOFHtJ4 +7EjdnpRp6L6lKU/pMflVx3kwepAjGul+xZXhkXdfyLfGsAVsZQwePiq8aMZ/S/Gj +yqrCMQ+EqeTb57SSXWK9XYrOIsQZSJjuqiOOYgqKofv7HqJlmC1cLhgoMG5o0jP2 +bTKvOxECgYEA/0ythY6A5KxT06gdYltsMlIcuamVw5EjoDpf8oyBwD6cbUm8Mb2t +VOtPbbHxx4NqGSn/uxT8UgmOGZ4C04CwmgWmMmODCT/c9LBHNbWOTiy4OLIJdcUv +01x1tYZvdbe1FYKxga+hR+UnKJT1t0m5UjKjhXnIYGR+weXpm2hco2sCgYEAxd2K +ggTm/xNtDHNxjygKik4FfEhGcgafnpCrm6dCGyLAe5+T9YOt6owGTS2AiJiG5PJR +GmJxyJa44je/r1+LLVN8SmsLXTX74UizjoVNaCoy9Ze3Xb4RNsPYXe0wIgCi+hBP +PEmFZvSxlxXpHP0ppknCxI08wBWXH6goSkcrDVsCgYAx/69gH7eIkWNdaWhP0f3P +oRs1FUxaX7ttbuFJnFDw+JIkKTOtPiuLHQaSQi5K16bYxMvrtMFxw6NLyxFcoLB1 +ibOx3KFWF2bLmRZpI2R6VPHDUMLfiL3EFDCNW4XKtS7GxrDC6EWa0fsoTHwO6GZd +/cLeqiofDq9wg/mLURYLfwKBgQCcywLAJ1xZUy59yFl7pLI9iLgmFSvlncQNJf0m ++jKTSEWZcQoRash9bXps3BrXo255rF+CdfJOjslxUdYPBbpHL7n1SsAVm/q+Ohs3 +XADMA2ygWxpOKZiAek9RluwmdbSwTg5L6sLQpCS6Yf15IFBJ2neBw54ZZMJ9roZG +3gCKswKBgGRoPW6/tJRuf/OfVM9szhruhNa1tUJre2RrKnXzp4P27nfLHKUuHFoc +A0NXSDu8b1WDFHi2d/GIQm/ldiShGqx8HLY4fgBKTMLZ6nJXZ4uhv98xjOYy4jdI +X3HwYl1ukULFfhMVlS4qF6dQS2FF2ZxbkVwKYCjFrtyEJOjYcce5 +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_WALLET_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUBN4qin5jD3jO3rKq1yvO8YTCOM8wDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyM1oYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQCkLFKcf8jK2dB0dVNFO8ULxmS+4939oXH/TSF7JiNmUxGI +WkPyUSgHQOatbhAK7Z91ZnmWyVrnGzXjccvW64uLLrLFA5mL4q0iiwroxPnaP0qv +w5ZnZmDgH/SPrzt5yw8ndagkfpxLekG0Pwbh4Coffg1VgEw/gqjv//moUTSQB66i +daNHd4bN0KVDXmycUdqI2ER5iqnDnb6Fi4DTQXSkwd29t/mRaHcQLrwiV58YqsBF +YcpE70UWgV7j+e5qYUlzJ0P6Te8QU1pjqigWoAuJNptq7E9wenSJ+7RZB1c+R+48 +bBuQpoX+UVQfTHIeJ2NSKiMN8deLUhPj+fQrCp+1AgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQAccb5ZaUOWhp4FAwQ4iTfo +5RHaPGD770BgpIvl5yKAmfAN04iF6GgcCvZP69m5lFlNuQNaz0kOTIY8cNvrksHF +OYNbm7aHHdCGKs8sjdXxrJrL07MwRsN+XdWIDECGQBIHmvcOeY3HLdxcGr1lSpuP +xdT79hYrYeMBaHF3RVItts6fNVBGFmA9hf6/cDEIRt6TDbfBy+PCIRQO3OxKa+ch +sIemO+3tJsyB6xJz+iG7lO8QCI3qtn02XoGM82WMjgJVorWMzTSoMIP9UWMECoDw +Q9zy6Fqy4cOWT2qduWD/7r+5e2jmI22wEU9LW3QlNt9XziVBGqnZEiKfOdnf3XdB +-----END CERTIFICATE----- +""" + +SSL_TEST_WALLET_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEApCxSnH/IytnQdHVTRTvFC8ZkvuPd/aFx/00heyYjZlMRiFpD +8lEoB0DmrW4QCu2fdWZ5lsla5xs143HL1uuLiy6yxQOZi+KtIosK6MT52j9Kr8OW +Z2Zg4B/0j687ecsPJ3WoJH6cS3pBtD8G4eAqH34NVYBMP4Ko7//5qFE0kAeuonWj +R3eGzdClQ15snFHaiNhEeYqpw52+hYuA00F0pMHdvbf5kWh3EC68IlefGKrARWHK +RO9FFoFe4/nuamFJcydD+k3vEFNaY6ooFqALiTabauxPcHp0ifu0WQdXPkfuPGwb +kKaF/lFUH0xyHidjUiojDfHXi1IT4/n0KwqftQIDAQABAoIBADLetP5fLgWE9m2P +iSzTt1vNrpvjmX6kjuEvsicpiyCCrtUUOyeTdBbDSncEup3YQWesSBKr86nWqZz/ +Ps0qkUOgRa58ThClPUaN1OSJXG3+3JKXxTvm4i+wVyRKhOBZRinQ8DfWr3FHwaIr +QWOuBP9bHKCYr4eiYdxz8ZTxDJtv9trGCajRnTv/zENoAHC3Jb9g4SPZ+bCNP54n +gBEwY9c9tP9DKtGDMS0wfLO3La/VDO4Ft0KGMMur5hyNzi6xA0v+HJmaQS3h5nkO +H3MbDQBDbuvjpAk4IlYKExFsiXbT3v+M1Fq3G1z/32Gucx9AraMngdOOyoxe2NvZ +KgtD4wUCgYEA0lwkaXW64p+NE2hEorqBadCDyKmHnYhKjzG5/PuoOxlBsAwtx7Ce +fJ40C1yu4OTANufR7VXWOgH1eaO5lCEe+VhX1NyVb5otuJrnAzKQ8da3lEIOLPcb +fDkGS9LKdEWSIeA7gIRdi0S+EFHbBqgg2hsWO4LVAc/6N9OlPNnfmQsCgYEAx8rd +AEw0mqE/UWvofI60b3pceDkcWtOZef2WqkVLYRvWjh72S2KSmhbJxB7aAQrSgvka +9aCHS616yo6tai07evd7VCRNmPzozRnG1NlwIZW/eFL0JYOXuuH0C6Iqi6q/VLtY +iBfz7114IYGA8Nqj9GKRfZXFGuBZ0sbSetNhoj8CgYAf1NuZrbv64QPbBPMl4K0G +kwvuCGFCIEaQBolLU9VwI/FBr4YZ6osA9nuPoJXB6DuB03B7xnplSriXkIPbe2uR +daHMzxg5zA3RGneMj1FJlyEuaRR2D2p0ULi4Lox+LazgPWsjlmQFWACevZQ0HKrj +9idWGAUdghgt7HPbkmh4YwKBgDvzGkdplmtDsS0sVPFzHJ9Ktw54DJMQZUAeoKPs +8QZthP7WOY87P8QuzFIl88JWTf5w4u8LQS2rG2pGT7DJa8ylEAOadRJP6UcJ0giy +Shw5w99F/O54wwGXpVQMT/nivVCeep0zmsWbZV2gb2FWKdY98WwekatT4IAHmsay +QNyfAoGAVetKQkO9H3VvM8dloVF/Bh5ui6uKlYO/zNop01ClUH3KBc3WwdVXCasq +BEsJ0HiuHTU5N3UPicvLddCdxkAJKTQkwqOFqcuHAv6HR6RFBWaWN/iz0a78Q1Bh +gwCYEHjiPevWb3rLufd26SWtG/tIsrIEjx3BttKDRqqngLKSUvE= +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_WALLET_PUBLIC_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUU/+7JgmNQiwRivfB+ypu3OtSTJEwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyM1oYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDT/HGxRGnq6IATEXgdHTrT88mmY+JRNzeBnD9Uh3uwBkai +kJam31eY3oxEsQWVZRJ+Qsynr+VBgxFquNBnHmQ27JOqAH800oM1CC0bq4Z8ac6X +O1SjCdUQdaqpjScUNfxWgk6gG3e9KiuHcvyOezdIvPb5Z8Lgz1Wk53IiUpjOBRrL +jvBYVebvpAog12q8Flbf3Myu27ZsL3lUhVWpfl91sEQ7rEz3ollxl52Mz2JA1B2+ +TzaU/iDctTQnRCk75RqDmVshnKXKXGNnClwZXtIea4mfVVJB4mJhwGvgsLRRj7eg +BM6E+jzBfz181Cr0PrhpzGJnfuOtEepby5F71GbPAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQA5+/o35BCJRvrfMIp311Xv +ybio+Yq7Jpp4wDOT+6XlpGMMIHGtDw3fYXhZeXDLJQ144reDOnLWBEN7of1M0NHu +Y5ccTVeYaepZnvncyXtgokDtPZjegXj96zs1IDr5JU4F3x7xw3+zppTAZCgGQ4A9 +rMwwFjsm+fbjITR9T5fLppUSW1aTCSii3i0VBzGyXZlLDuCqdOLYCOX6WFtx7j3y +jQYZlqXqv5idx1BiEJT4vqC2N8elpmPsZ0TIR7xIFft5HvlU/U+aCO4NRetJaRj9 +JK9D6Jt3shpKLbyJOfa7foY6LBf+qS5MpC3kTnfoeVNpI6DHKWKE6d5+au48ARy5 +-----END CERTIFICATE----- +""" + +SSL_TEST_WALLET_PUBLIC_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA0/xxsURp6uiAExF4HR060/PJpmPiUTc3gZw/VId7sAZGopCW +pt9XmN6MRLEFlWUSfkLMp6/lQYMRarjQZx5kNuyTqgB/NNKDNQgtG6uGfGnOlztU +ownVEHWqqY0nFDX8VoJOoBt3vSorh3L8jns3SLz2+WfC4M9VpOdyIlKYzgUay47w +WFXm76QKINdqvBZW39zMrtu2bC95VIVVqX5fdbBEO6xM96JZcZedjM9iQNQdvk82 +lP4g3LU0J0QpO+Uag5lbIZylylxjZwpcGV7SHmuJn1VSQeJiYcBr4LC0UY+3oATO +hPo8wX89fNQq9D64acxiZ37jrRHqW8uRe9RmzwIDAQABAoIBAQCbj8xq1paXcQjT +dWVckB+kfGlFNlVVXhzYey2qPUYSFXjuQQac7JbesqnimrlLOYGJsEF46MZm/eTh +GUCt+4p1F8UA4x52R+lLGHXpsUSethKJvltMzaFSU9bqV5AO79L+NN+39JA4++I3 +orUdZeRa93iR64gB64Sg38tMzqodvB/9vD1CgD/UFT/dy/FjMkg3UMU0oBZ0Zgjd +WRZUePFIiMQ+/VtEq2PVuFfwPqyeKEy6DUgd1wVliQNomgqoUhxc4lPFr1KhWvAE +q1Z8+H0Pd+uYzNTOUJWhhH1pLpinOu7V+AhtXWEhe0QXpoBELlCiqB5lMamTfg6j +hY6wJV+pAoGBAP6WFITgdZHTrOtOMiflKW14kyuEXcPmUo3V6L+esH6r85JRAspb +ceJulRsf/KlTZzh3wRRUb7xTNE/S8/w2WSoVuMbsmY3hs96oXByAhHS9SSMaQ8ms +2+SjxpSerPr6qGOxg9bzm0s2z3b+ZcgJrZ+bKtrKPvacpZItKO/BbW7jAoGBANUp +zbhCcOzbKvNvRWc2BIMFy+x1PzW2ebq2XtyxETnqBVxW+iGbzBdiE/cpPn+P6Tch +kiFnuOaul5Q0N+KzIa1q8MtwTKPPZcQQc3Rv4xw96tldja3ZCnOo5WlW2UuJgacK +XXtQ2KQegTj15dytPhR+41v4GksjFnI5gPvb7SAlAoGBAPjHW+HFHd6UxQNj9Gs7 +6tHI47fAr4gBiGaFw92Mitgd2/T9KQbpeU5V1WseRN6KW/G4RHtDT7Tucc4XTMkH +qvYPJ/NrvVoCVqycRPatN8KEPfYJcnifbHnu+Ny+ejb/vpE9JKJmhzhmpTGYw4lI +u9ud27DVtdVzmfBQJRK1J+UVAoGAFw+Is/BsKxOi1+cnyPytDEeqQhCFIBh3nt1v +8cwuIufQYKcANHaYY8c2hbyuiDpXbqFxH7AK7tngiCYGDb53XD1/g2LIx8f+KHHn +K6eXGE0ShSV72FzspoqVFwpQQ73CiwGyD081wLuUG0du8KrFVo5Lpn12yr4nBYB3 +Fcg7JQkCgYBLRw82I1NXVnqODF47PHuAuyNKY/ZT76zRY7UuVpraXrqY3Vc7j6kz +0ZrV5NxAxbZMpJVGEfc6B8YIQz3np3P5s5ME4+bbIzOBQcmIJXLWeMq+oZhTBcTO +u300qymEVtlVqhWGtHlFbxSPtV+d1BFCptUShuE5BpvqUiyRIZHokA== +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_FARMER_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUbgDpFoeaeAbCjZcmf9YI/M3IB1IwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyM1oYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQC4vhXYgLWi/5xCrTP3efRW65MPn/vzynsCul7dya5vmTiO +BZV2BkzUBRfZygARIUy7z9VsX22c4I/xRtJUkjAtDaSw02bnlL0BZSjZg8DYWjZO +woO++FNweJ2SxvaPQypDCyRQuiIMGBcsqMX89owC2y0cQtYbB8kA90WXNeaSXXzB +WTpPd0pRDx+VOs60hGIfH5y6S/MkgDPHLkXYCGMqRK0vBbKr9yWq7YNkFmgGd6oV +qAspX0ykx3KOiubBO9WV9pw6Tn6Bzm+5yD2E4p+wmR6r25MEHExB8mzULoD3wQWE +1xRQDZ2dF9dr0CXSnDlOpCf3LcHX0z1NJzG93/M/AgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQC7rOkhmH/14hXD8dFxw7fv +MBE07KvCbbYxoupD3pUIHO27Nuwy1ZtcppHB1yLztFVy1gk4u5HfRPkliVrsUlCy +IQ6JiWl11TU9vPQHU6ZxAVYrmS4UhxKjOVQWJ9ZXHcQWamNZS31uCfEpG1AvLcQp +rP441aWGVl9pn4DvYCcrGA9iOIh4t4+IP9KdcehXwf5aIip6j7FR7lRgoS65xx18 +aekW99yqzJtyqtx+UCn64a9lGFlCvzdUvh1N8qtv5O1FGC0u5erCkTpdR4mwbljh +Wl7fa/81yeoV1PM6sHrp3mEK1qY5yElHIEKlsoJvnoy0GRJXY+bhfEcZVEDvl9rW +-----END CERTIFICATE----- +""" + +SSL_TEST_FARMER_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAuL4V2IC1ov+cQq0z93n0VuuTD5/788p7Arpe3cmub5k4jgWV +dgZM1AUX2coAESFMu8/VbF9tnOCP8UbSVJIwLQ2ksNNm55S9AWUo2YPA2Fo2TsKD +vvhTcHidksb2j0MqQwskULoiDBgXLKjF/PaMAtstHELWGwfJAPdFlzXmkl18wVk6 +T3dKUQ8flTrOtIRiHx+cukvzJIAzxy5F2AhjKkStLwWyq/clqu2DZBZoBneqFagL +KV9MpMdyjormwTvVlfacOk5+gc5vucg9hOKfsJkeq9uTBBxMQfJs1C6A98EFhNcU +UA2dnRfXa9Al0pw5TqQn9y3B19M9TScxvd/zPwIDAQABAoIBAQCzrr4Uq2r+tFpJ +R6jppMYP3GRWqCYoAeIOzzIByW1NwdsN10R9XLdQ5/tAqoXSI288pHJSS6aTFJ8r +7tQXyW/uAf0Styq5RyNlvfwzQ0BHrcZwaCQ3k9Oj6sxnu/iHcq4iMy4JDmCbHrs7 +hpO67BlVldOUTzVraPEJbXdUEOrHo1fMtWwb78sIYYTItX8a/rP5wuJs1EaQiVHR +Db/NoGpJX9fdriLweRgi7pvdRo2So1ZoC/95fQNFstx8KdMXTj8QKHnXV5LOndMq +5X8EmtACBU/Bjcrjij0YAh1g44NRQN4jmGWEJZjLdTsyoE5tI7rsCt1QGqPs7gWT +7JX0rNghAoGBAPWfjVQkn0ZTer7672frYhATzHypX9wW+SvUMS3id4hqKjch9Q2w +Z+t0lrpKSOtKghNBzrFms8CPhuAy417BmmuA6HRFaqp3Ac0uAyA1NN8QaPNeHf1/ ++Uub1uNdaN8pA4W8qKvRMb7dVJQ2gjbcr/ALpwQx9T3M9Xxy9hCyoGkpAoGBAMCM +GaIYczaeMV+jTkUSYdISC1rttJjFxEKuMWvNwajapl05kHw/fTt/UXcEsy+SBMQk +ArQHx1w7NGbkBNaffE+zyPcUjlKwHoix45Tnqwj4VIha9rNVxPDZIeas+pzhcmgo +KHWy1wflDIgBkeIWQr8Y/QWWvt+Qq3oD1MPlOD4nAoGAScL1bTxmPHdbWDkBZkLQ +uyVG9nTi3bRkdZ4OesoUvXmsXcwrzEWan6HuldkzFr3UXDYZ/TprZrtzdKazk0Qu +vHQE2s3x32lHuDdGJwjzbL1/1v3/oZ4p3mPZX4QwtzuY3DOwr5BuEPRkrvHDnvgd +Ocg2CtN542pGmm3nqVILTCkCgYB0X7jWaaSo7C+3OAKEaLnKt7E5QdYXR+B41MN1 +/qP/pDdMvRAAqHbOUQMxxhtusvhCe+lOWi06J2ZikYoDFd2SZn0eKMRkYaHyyGFe +jC6pez3MM/5LIZmoX/PHceD+lJwLK8pYaMDiOqO6SAid9wpcaYPzrsqqYMvjMRGV +XKMDnQKBgQDdmgTf1Ga4rBBXlc01bhQCz+TVD5eoKG2jy04eMxX94CagK4r2YFeh +DnF5ARyYEZBE3GZfXRVR9qyW4YxWzW3k+nloAE3WqKbWYbmLqjZdKd+ppAhXpgfG +7FPIfDJX2VcARnCR0CkxXF6f/6eNKg0lKfkuEA5q83Cp9HQCVXj+3Q== +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_FARMER_PUBLIC_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUcSw+1Ww+zDNNx/vNquiiebp0nRAwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyM1oYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQCj34tYd0hFq2vNzARK5oT3ZU9MX0GNLidDDBPpDkXEYZnR +yVqdLe61OWuU1dY7Imdoc72HCDQLEd3bV2dux2jE3ktHP0zELRIucyhBJvx5zl68 +ZO004prWYH84O1GlTHsz/OQZeTyN8detpOgEmjZYJnPOWoMW063r5YAxfZ5BRdlx +MTmnJjLIxX2q60RjoVZXNtl5DOOxnYdGX6w5L6KXMr/A/65AePP1RYW6ugkQwcKA ++PsAbDz9T7qDmgu9gHcDtGUg30ua6kLgHPx3ZrL5RI9KxfP9l/r3TEXg1XhlVyv9 +ogb12SQHitHR9HFcs2y1CraEw7HGTTfn1wqbqtzFAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQB4Z7WQTY5irFfCZzVsLjpa +ntAiAe/EdaGbx/12Hy07efLQ29D7eMWOvjjlDUjAZFDefH6SPB3bZXY5rhY1qHnC +7l3Fut2SqToPy4/8m895GNOtvPJQJgw3nUijKA4k+tSdN9XZz39sDYGKzHPMuAk4 +D0S6MWIrT00zEUMJMUoFfCF7W1II5phrlAZTczdEiXo3fn6nSgsDXGgZS6Y4QXh/ +5HCPCuI/F1qZWK3eVdrrQaB7u2LLo3+qfku7unZ7+/EgomRL27pMukrL/N2kGU0U +7P8mpcs7j+Sn9POI9h2fSbB4IElbHP1AM/mTfLc38bKQNt1bnXzMKfhnOWHshBrm +-----END CERTIFICATE----- +""" + +SSL_TEST_FARMER_PUBLIC_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAo9+LWHdIRatrzcwESuaE92VPTF9BjS4nQwwT6Q5FxGGZ0cla +nS3utTlrlNXWOyJnaHO9hwg0CxHd21dnbsdoxN5LRz9MxC0SLnMoQSb8ec5evGTt +NOKa1mB/ODtRpUx7M/zkGXk8jfHXraToBJo2WCZzzlqDFtOt6+WAMX2eQUXZcTE5 +pyYyyMV9qutEY6FWVzbZeQzjsZ2HRl+sOS+ilzK/wP+uQHjz9UWFuroJEMHCgPj7 +AGw8/U+6g5oLvYB3A7RlIN9LmupC4Bz8d2ay+USPSsXz/Zf690xF4NV4ZVcr/aIG +9dkkB4rR0fRxXLNstQq2hMOxxk0359cKm6rcxQIDAQABAoIBAQCFaZo9hKcPKVcT +7bPU8sVv0Ef16lsowFlwiWWwSFFeZwNeuuoNqvZ7Deta+Zh2/jRn4kp7o58TIBGZ +4BeyJaBTHpL0x0ENOZBixpgQKthDplKUWCqR8qaSP29zbT+0LobjNVDSuFQnT6wC +j43hKVdy/qMrbZ7pt54Rvf9Wy2lKw3CSLuN9nijyzRdI3Y7Qgp7j5mbYTodagBEw +rZUNxZgZ1Drtl1cGOpFzDhYfIgOBgBTMcfDJh7QHgrGNBEqdOTngBN8TcrK0jGT6 +4Tzxra7yhmyJmCa5nzk/I3ZZ3DMdVG70I9earH/MukI1rH4BmLLKFjy7XGWRs13c +EKe46T3hAoGBAM+RSuQbNZBY6QTQ5b0ppG8C1b+LT1+UOFlWnywNNgB3yc51zTcn +TNF+YkNRb0q3VDcdYWxzMrU0F/30UEPqx3QguXZ4YYmAAlE4G9j3OlbX59CAEmnp ++aWRq9sAxdzzQ+vGn7iTWkDPdgf8jsSm0BYP/NdKk57wS8j1+PPc2GG5AoGBAMoc +Phx8XMUwfkCZeWNnaLHZGMbVJz4lC82GlaaLfdqR2fitxZSB9YAyNe3SE3FlThi8 +sW1D7CxuGqQRKtql8XlpZhik9gGYtd+vricF2UiO3c2OKTg5FPiHanh8WtYCER6A +QwdATKjIUyE5wS9Cnv96xg0DISlcwliOFbV4x8ltAoGBALMqDYPRvK1pIVGn4vYh +0K6FuuzIGe49aVor+96xVJCY1VfhtQ5LXPJjbfv/edn7XrToJmTPFtD0M1VgojvN +lvY1HQEWrdJa7SUgEmF4HSJC4PTG554GeReiIr4575BlZpZIbyuJ/Vh9+rqwFKfH ++Uth53QKClwhvLitIIOWeCv5AoGAedR2eFNa1J67rBMXgh0mlfZIoiqA4kwQhk53 +TRJOxf8fRnDxZejE5QbfTUFR6Qvo/K5ZwYStC8c/FeEnVO+s1MP9ACQICsRT9cd/ +khRLexprh2oHXNXD12BBhOlpFBHg4eLtBKT00NUYjzGyStu83kHSuqtFRRqeKCVV +3rM0sy0CgYB5QvJ4/TSqWhVodM6DVxSnYzoF77E3t2umW440Hu8grLsDJrA8SBMZ +0WKME6fXcbNm3p4eYZ+2sPiLpVf6w2JlykY+sDa+S1MtBAkgHwJqTIpQDd0egSvs +kgCFy+t4irJ+X3Oou+pERFHP/cMiQGAMvstgpz76/4CQdTsAejQsMQ== +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_HARVESTER_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUcr5lcnpo87wW1R4tGOd5W3U3m7MwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyM1oYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDFYItZq3rmZTSdcJK7m0sPbVSyexV4mIVkR1JLSYDkRUfC +xWqPB2ZY1sc6a+Tns/0qVOEddjfD8j5GrRU1g9VluSnKp2bg9NgQkqTEuyLDPF9C +iP1JOQudPSPQTeSwyOk2bQQ4LP/nRZXzZ+NnAEEHTkTD7D6LvFTGrIEShLIqxHdY +5EeFXF3zTpp6LOQYii91joqN6i4BUbbMr6MoRIgVUZeEdmKPHkyaIg2LJ+HiqrNw +xc8Yg1ksRBYAKBbPptLsIPXp0T5ja1SUEUkyDaFclzZnAfQhB57+0VuA4/pIJcLV +9p1avjw1PyrHqV48AGB/NqWA/+4kXEkZ+DrrY1D7AgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQBIvNub05nXZoxHcysNf39o +DudZ8wmn1urJpYQQUhFfZ7eGyTTgNiEI9Mz+kVK9f+BR6ylw5yDGpIC2BY2uDdR3 +r+y6bs2loQzfIMWAOEsx86zMG1XmYj8zQ7pjZ/AOInu/Cy/w6AwNIt7U3B8+nZEA +qPatVOMPh0yWJ3CGjY3lP9mjhREyUBwBsUOq813JYXLsq/euFm5HPVptYf70Ms2Q +3qkSmyZFozJbW2/wqsb083kJ3djkVliqSDVQ2TnQCi8PT/9uE8LfMTrnWbVZlTO3 +hxByAQAco0fiTQ7BN74FuhzqBLBadcnQ6HnWe87E/0OI9co3+vJR1H6xHSebJ9pu +-----END CERTIFICATE----- +""" + +SSL_TEST_HARVESTER_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAxWCLWat65mU0nXCSu5tLD21UsnsVeJiFZEdSS0mA5EVHwsVq +jwdmWNbHOmvk57P9KlThHXY3w/I+Rq0VNYPVZbkpyqdm4PTYEJKkxLsiwzxfQoj9 +STkLnT0j0E3ksMjpNm0EOCz/50WV82fjZwBBB05Ew+w+i7xUxqyBEoSyKsR3WORH +hVxd806aeizkGIovdY6KjeouAVG2zK+jKESIFVGXhHZijx5MmiINiyfh4qqzcMXP +GINZLEQWACgWz6bS7CD16dE+Y2tUlBFJMg2hXJc2ZwH0IQee/tFbgOP6SCXC1fad +Wr48NT8qx6lePABgfzalgP/uJFxJGfg662NQ+wIDAQABAoIBAE67ShrEukt5B1nd +88n+EhzfQa/IRTJLtLnhcUQy18U2lX9t/Cl9jCfX1LTLIQ4Dz/K41OtJosj4sjrD +A/jK0662A6Ogzvrg7+b8d9k5xI4YYO9Lca1poeZg4w5OY/DY054wMkSuPo3kRgJH +6H/HrCjb4bY9YF2hDDgLy5i1cdR4wsWlVCV2NkLQSYifPQxGHY4jcDGbyUNhY0wZ +Nene3YvzbYLaJJtISX29K/k6Pfatteu8gu0AqzxNLJa51gPyrNFjmlj4hLhtR/Ty +75T6lIAciLkW0nswCb3ZgRh9xGgASyK9rqtrEVqW+mG4TCVd45aK1s83EBxs3RFO +RcZzmnECgYEA4SaQpH0F3WU7aQMBw6T+jQrSwJnNlES2ugyDSrLX7Exu1tVgrz4y +HzArBg1xHyKnHQmaLrlMPowcDhCtBO+YC+j+gGak9UbaeJbR4vcVuv2gHDQ1Wqd1 +wvxoV1FnCNNta/TwKKoQfEZasTEq294i9krbpWh8sqR61dEsKDAV/OcCgYEA4GvK +Az9jh3VkXv4Vlgvoc3fnnhx/NEXzd51TcBFp81zD3zH1GJ37bUMK2jm2JFW1PYK0 +AyVEJUA+MWEISt7lOrjI1s6HZk3Xsh0Qqdv/WPA9OrG3eJxPH4Bo/M1HWVG72oWD +tbjoOViXYKYd212hKTdUCMqGaW6i0kWIajFUVM0CgYBw2TOGPmPCJAszBx7LQNeV +OeeIQY6Y0hgECGGF/z3aYjTr2Ocs7q+QkkP+NJ6OTIAWPcnZwWZFs1QceJ8/6hb7 +YTyufsQPbAP0jSOF7vIlVxn5CPH1DhooMPrbSSGres1NXudAenzozRqH1Wz15tD/ +QWX18fkOUQKASOco/XEH4QKBgByDtZQ6DqRcdxdWw1lgQ/W/6278gfEbXjb5h2t6 +2vJv+/c0+sZY9GRKm2tk3864ESIypDquFn2BLyXJBWu17HxMlEAu16dZBqn75W0f +pc8gHzeA8yXg/nCrOSu9zW3845h9VGHXj7IRnpJKKQsBV4PMIuJHEVL+GrQK361W +fTeVAoGASnt6kmfgDED1kdDZGRMgb0GvyAVObjLRBu9eRX8nQEKKpFydBmdDM1+Q +coUvrLhjqLb81znytV8Erb2Luq1xzlnZrWK3uXuPJW7T/m+DL+Oa2+e8pgRq2/Xi +QJbqzpOmiuAeJzdT9us1UALoXHxdzAw/oSC9Xq3j66O1vArAm9A= +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_TIMELORD_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUPDgBpxOAo73yMiUGMFNbK0SvFvwwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyM1oYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDDT7pUEKAS2A96fkLb1IfDbHZCpOTUgKk7+uNQXTKarZv0 +yys2/K/StPGJ3+mFlN1+dqubVHUILhPUpfFNHH3zintDsCjwu1GPKRuqgPla9ZAb +zBft+KKm2CewqvVotYOcfIAUqZMmfWuyxZFL/ITJqnJKHqYU3yO8pNO3k9c9tKVe +AVLlarcHJitjSkKz+s+3XRJYC/x6BsguRiNGSPVPAM/jV9PGhaQqlhWtQT0/kcEs +MRICpIPiPd01bX9NWbSoEbCVN2mBBxPi7fV4V2Y+xkdjj0QtkLmJJTnatNNK9UjK +5npRqHyw1ogyguno10Jx6e5vd+3hWiJlBOzpqiT1AgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQB5xd/q0Z5SfPECusnXmAwF +X48KLYhjc1/483R1gz2Vy1XNPWzDDs9I8MWD02BWr0dMMuhV6iPFmmQcJ76ejJD0 +NMtBpjDFfCknFuMavbog9ikPEXhL3EtzXP3jC+xsdvYwi35hkBgGs5hnAkheKklp +cjRurmsBAHJBlEsUc2C6kxq8FXcLM+WzJJYOitzE+fpyxrmeOIfpUd8Vwiu90Ta0 +/w1mGD5vlXBTiFCwLEc2fVR7OPm3QxKvJ+eUdLTh+X4VgJ5vvdiqCodgAeqDZw/Z +x4hOnAEktgEqadUz5t7fJzxuWWpNTL8OQLJgW1cE/Kt/P8F9IQ5rC7PS+XU+Ds7U +-----END CERTIFICATE----- +""" + +SSL_TEST_TIMELORD_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAw0+6VBCgEtgPen5C29SHw2x2QqTk1ICpO/rjUF0ymq2b9Msr +Nvyv0rTxid/phZTdfnarm1R1CC4T1KXxTRx984p7Q7Ao8LtRjykbqoD5WvWQG8wX +7fiiptgnsKr1aLWDnHyAFKmTJn1rssWRS/yEyapySh6mFN8jvKTTt5PXPbSlXgFS +5Wq3ByYrY0pCs/rPt10SWAv8egbILkYjRkj1TwDP41fTxoWkKpYVrUE9P5HBLDES +AqSD4j3dNW1/TVm0qBGwlTdpgQcT4u31eFdmPsZHY49ELZC5iSU52rTTSvVIyuZ6 +Uah8sNaIMoLp6NdCcenub3ft4VoiZQTs6aok9QIDAQABAoIBAQCO9vlJN7I0mPsb +ividuYB3SBl4xwLJmjRGt2tVFCNKnfIeyekkIusArXpwlfp10XYgb/VFihjwl+nk +KmPrMgPwFVoNPhF5xWP6Cvk5YZclQcLu1gJeKzXEM006QKnKr7NbBLcsaMRR1FTV +U1D78iexBpcKMk0X0g5ys6RWKF3Rx/PqdcDnH71hlZFKUVaKVeXvPdAF+Jo2D7Rv +a3PktRMjB1c3JtVI5wGvvkIs50n2pZsIkh88qhwZRurvJiY/mUSyi2CgnIkLJLQO +kziMr8gOPOmRzy1sDY3fUYaoI3cnQ2va0zqLg8GkQDfLg3UbirFYXchaRPkoYRSw +fPGpnXsBAoGBAPvFlKjzh4Anob4CgYNgKnZHTeDHEY5iGQECbcFWnv9TJ/PapRUC +MOPjW8c/SxZdz8neBt+pyl57nVrwvOWO4Q6ccJr1a+1a6Lc/BxL+wUw6fCxjfQN6 +s3VLKkrALxgmbWjhAOACDOs9Uj4O1lhLxbhPStaSAMh7nc7OAOubi20FAoGBAMaX +aYz+zF8lQnQUzO0zg+4VbS7bR5KfjLoMXF4B+TJ/n21oJUjKSPRTcBZmvSSxu2Hv +8Dj4guxTVezxis+h/6NFf0xrPD7zxqKNMZ71bszR0qfNzEkMix4inj10gUCPhgA9 +k8buLFm3pIAJ9NeiEIKgZT4H9TItItLCfQC8i9sxAoGAX69Ah1E+XwMw4jC7nf1P +RfJlc5bUYkN/8zVEFyVfefPVjES9VpWllQZUXA3+8HoovTSHcjtqMKxUKjqx36CE +gfQMi6fYI7XYGcR3YM23EsxrYsdQvKDGUT6GzS/q9gesrx5MIdZFqKV+ex1Scu4h +L6Ha9F86svbgC7eY+/H6dC0CgYAODRaFyF1zefJqvjIFsnhqNw/jmdZFlI5jd45t +hFw6a3c/SXgh31YsG1855okJeJ3WfyCTF1pEGF1jB3AX4tFwnvEz2f6IQb1TMQRK +x/jP+ySZhOEoZf/N4QsrM/wVMlJ739992so+itTTzmCJhUj/xROEwRFjPHhTSzmG +/NA0cQKBgEE19gpbOu7f+6/mBWrS6AR7eGjZ7M2wRap7Eoh9M2VV/ou3tV8Z4bHW +qcLBFa2HW3E/Qgh2jz2q+aWrRKlbJfzH/EtoQpwkUeKaDnONK5O6V/kR1VNBnUrB +0+C+ngvuEff4uTk3zmaCc+a04HXWXhuzSuG9LA4V7twmgNo9AlSs +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_TIMELORD_PUBLIC_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUIDNyJc/WIgjFdIWtYp9/dZyhDFwwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyNFoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQC/em2gGo4JEdff7eRAMG3two7JlNvEzuTn95dcxWo3Pydz +OJ83gPrGCM93zBvxsCOppyER+c34D9toxAvu5N7S/68eGOPlvbqS1aUJtMRXIoGg +ghIlz1w97VOmQ2DS4PZX1Yw+563ah0T/kmKvrJ6Ti8MOqvJ28GNnDObBP1hSfezu +/iRbiaGJUF0wRULFaPX8uNnOT8oxDXzsyBRKmZLN/O3bwbpX3hzIK2D7BBpqUza0 +AuO4/zemr3J21QFu4rD/Wbd5/zX4uxSOAIXbT2haDT5KZEUYQv2jXbHY6+tzpMRC +v+Bb3yAeFGt6K44fzqi0fj1pzoHZQ7pgqKwRKA0tAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQBuL8MMR6mTzcL/0OOWVnQ1 +phT2S9L1l6VmsZYK0/+kUNRphtglqiUg8TuHDUvXY6Bq5r+NY9De6ZhJMhsVYt5A +XRkBjEwIPLC5+JZi8iv9uip5qyqsfrt706zGz1NfVIk6pS97Nssj4d2WkdB8BrD9 +fnngf6tnQJX4K7fhE/NWcE6YMSBA4GG/JvpH9ouJx4aN77zF6PKyX+iO3GDHXqNH +BAdEbHgr4pUPZuUg6c2x4T20NKnMNkx7C3Tx7tp2w49VamUwnwvm7vuzjajd2st0 +q2O+l3iNwRuvzL+KaDhqexf3shDSCxuYSaInne2XmCiJjVW7QRfQmyA1LiUk/vfP +-----END CERTIFICATE----- +""" + +SSL_TEST_TIMELORD_PUBLIC_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAv3ptoBqOCRHX3+3kQDBt7cKOyZTbxM7k5/eXXMVqNz8nczif +N4D6xgjPd8wb8bAjqachEfnN+A/baMQL7uTe0v+vHhjj5b26ktWlCbTEVyKBoIIS +Jc9cPe1TpkNg0uD2V9WMPuet2odE/5Jir6yek4vDDqrydvBjZwzmwT9YUn3s7v4k +W4mhiVBdMEVCxWj1/LjZzk/KMQ187MgUSpmSzfzt28G6V94cyCtg+wQaalM2tALj +uP83pq9ydtUBbuKw/1m3ef81+LsUjgCF209oWg0+SmRFGEL9o12x2Ovrc6TEQr/g +W98gHhRreiuOH86otH49ac6B2UO6YKisESgNLQIDAQABAoIBAB8r4h7XU9ocIoWc +57SfbbXwH2inqCNg/xjYULbUmJcH2/dA94KEp86Hbqb0/nOZFiUvRQ31GdfRVQm4 +KK0qay/0WeDPcoJbIb13tFdhKzl4L5wesK+hE0YtlZmSjHeoEdJ4vE0dUEssDEqC +3Tf2JRamAQopQDGmrrf+/K3nDwzWlRr+UcQF6Muw75vBvYVkfkCiImiV8oGlXRJ0 +CP8+0XPOmgcSmDcGM/qWxjDNZNoKt5SBx83xMticdUTvUKBIvDmSMj2VxTsMPbcy +elEncnbiG//Smz/tU8sggtrwhIv7zCZP02TJBFSNmE+orZFmHXAIBsDfW+27vN8w +75B/ES0CgYEA4d+9mpNDtxcphunjnoegwLDB7OApGY5SACCvt2HXQiWXl9DvPSU5 +tAreJdMV6OOTkTwFTGQhOEP0uSd1iDrm0DUvtUjgrPd5JyVAlMyMpc4z8fEoB+jk +lDoXOmqnP1t9w/GJB7YX2KI8qWsT/qvNGCa6XNBEFlp0fVzOzLJARr8CgYEA2QRG +vBEtTTGi4fe+A8Lhb2izc4YHhMJQBZJceSTxO2hN+BxSzltyf/7QMyC98sUGfOmG +bPzISZT9uFdA0ZXZjaoe+boa5HQJf1WxfAUlAOy50eetd+90+TmQ/qFePS23Im9m +dXyR0O+eiKhMfMJnkf1s8xQB2Lec9G7AloMlcxMCgYEAsoW6P+/g91bRNZaqluOv +hFywCV5qXY6E9SDggNpN3jQECrPSQsunPcvRJKgfiwBD4+hCb8w8DVJ4m9a6KEAV +qb4/xNKi8VJvaSciUfkRuQKvP6xQ7V9/OkBnl34wTf0r+7Btk7CyTEB+HZFKLmDv +KwWBClZ5WgmIRIUT1emUr9UCgYAJMhYxN+UchULqok3J6QMWbnBUL9S3umgbpFUI +yRjztHrBTanwlo1mgQyfbf6+f7zDpD2O9rMh8w9BNWlIuDnMt+2yFUG4dnZEkAQC +RlGIFX/WNiPylhH10YukToAoxXwiGGhWCB5BpTWpgsAi9TXgSMXKEwn0/erHrL26 +Yvo+QwKBgEI2e6WELLT2ebBK/x9pxj8w0IevV0oHg3Msod20iw1omrkCyge/Tt1+ +ABTK7xhmMPLJHD8ZiMl8sjEKTx8PkAZoKdfZV0Qr+BIhMneO3QTLcdZiuIbjb9Mo +3QvOzzxKKksub2JjN/VORKTqMuuSDQLeEBtv+sRGtkTHJrFtnqwo +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_CRAWLER_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUMqlg7Aqc99ifdKynHggljrPnQjIwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyNFoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDr7NWQ0b8tQSlXzaa41vexl0Zc3WrfF8aw4mEaMcXM6lkO +hX5QLZpuTCs0RGwLXVAuqEcbQqA7GIbhq/ch4c9PSkh+yBUOB8SHx3UanMe9sH7S +80rZCX+8HFMVMce5mMEPIRA7WmGRPf7+Fh0yHMAaIsaqDbI5f9DqLdWE+FsrFrdC +kp0RYTYQsYEl3YEPJfcYbfrX3I9GxKLSUX1iKsMlyohrFSlw/t2LlG1jeuxd5oLR +L79QY35LkCAsofDK/lThovM0qqnescGx4feZsLMCL4rv0td2GWNhA+4UofhTdul0 +zRcaDZVrNEhCJk0RNf4wXtidqJN90Sf3dvIjx1qDAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQCWj0tol4kMluTkz+sGjfpU +xTnbVkoJhaOVI3tdK5+Z8WkCS/IlFLFoHX1I5CoAtFDUTCrp1tU/5e0BzA2VfLME +B3+qgLQF1+Z2Qy+OBTRToUuOSg0n7qSQdG5zIsspKwRl+iobFKaxqw8j218/NKaa ++RJTdhRRrmrokiBxpz5AJK1WnLjYe65N/ZdfxHYoBMDtzrkCrPsoZQpHaUf/9K0p +v0tv8s4nr2NcAZrQeALoTvR0oJ5QlmRxfHY6xC170dLV9KHJTbV/FXXFmFQL1WkI +L74vcnYPvpD8tdY3kD8AXUFoYNb3sjjjmM5QS1pYva9PQUiuHqr8MAqAF4sHvnLF +-----END CERTIFICATE----- +""" + +SSL_TEST_CRAWLER_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEA6+zVkNG/LUEpV82muNb3sZdGXN1q3xfGsOJhGjHFzOpZDoV+ +UC2abkwrNERsC11QLqhHG0KgOxiG4av3IeHPT0pIfsgVDgfEh8d1GpzHvbB+0vNK +2Ql/vBxTFTHHuZjBDyEQO1phkT3+/hYdMhzAGiLGqg2yOX/Q6i3VhPhbKxa3QpKd +EWE2ELGBJd2BDyX3GG3619yPRsSi0lF9YirDJcqIaxUpcP7di5RtY3rsXeaC0S+/ +UGN+S5AgLKHwyv5U4aLzNKqp3rHBseH3mbCzAi+K79LXdhljYQPuFKH4U3bpdM0X +Gg2VazRIQiZNETX+MF7YnaiTfdEn93byI8dagwIDAQABAoIBADXUNrIxOSZxLKas +9HJOEfvCITrFBkJaoWnwbOlWG+RyP9mRWc2fahHqbR0i02gQZWAP4xF0NSzmnrfU +zbE8XVmhAEN1EWC/Ivc76hslVGmSYI1vF0/H3A4mhEpcrk5JbRsvlw2DxKkn9Qsy +Ln0llCsibiOUtmpqIFeeF46cP+jneMayLw4CCWoOSDPHcVLcdcD47qfkTJKlG83s +AVhALyiTbLSnKLj0tkAeIMiCecQbFQDdp5scemRgaRscQ1dGi1qaX7Zj1LqIxZBL +wQ/cqEKOZSx4apzv7U6U8WSZ248jUXTxBM6RxToAnYMITv8T/pDaB6QepvOB1E80 +Re7yNuECgYEA+RUVa4So6gYBYoXLFNvgmX/YUDOCM0D2lbNmS2WVZjr6VHu3mGP/ +TI6dFnJ94O8Fxssj2Ei4F48isXXT8vag3M3GHSbWzo5cREdWmXUUM2vQisWdjQHM +kRBLHOAAxJ0M/3h8uu5jbQeaKiKvB9OU92W7FAPFIUOzJ3UrMFXZ/zMCgYEA8no0 +sXyKJovzlhtchIGDtQ5pd+IfsoPz6edd2G0Jp3r00YJPuhpyivb3s2oTc08oEquf +dE+OaHNYIGWYwbpg4eDq0Wsfu1xt75/nKSUlmILKltwyq4U5Cxye3P3+ly5/aCzK +XRokBgyzuIl6WgSnHD72R4jiCobNhcXcHPdfd3ECgYEAm1rXHVSEtlJAkt64J65s +i9D9bihyUN137y8R4nzdjgHDGOaBQH8+QNXCjLmkYaMziyYwmTnh+G/CR8UiCSxi +cNW3d38+A18vlTaZgOVRUDEyxRs0hTpWCTSMZNoiIH+EF+NiiIUfZmWTdixj1xHU +m+nLoVQoo/LBzx55bZBeg5kCgYEAz6w+6SxjHjSLQarZiFtstGtNhXHT+A7vnwub +4rswo5K5j57uLdRs+fwfljhpxD6tcaAwB2wD6g2wEr8xH+tDAvKh6w62tL+rIKpx +T1oTYxXR8XdSpniJrKysm1Wm8VDPqieCgk7bP5stagXFFsgZYCXExOvFvYJLECGw +LdbomuECgYEAkiL1w+snhDZaGUfmA41JbMiAp+fWf6dgBfTieY2PLeB2PHOIQR6D +M3cX0bxsuFEqvTSer6/8Ocu+q5zAqEh5YxxRPiQ3zdqeYEMPGUWq+CGvtKY6IEN5 +0ITdHQVmzbREwX9F5wiYXy0VzjhzMbXdTK8XpYfNm34B1tfTJRg9ZyA= +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_DAEMON_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUO9NYo6D7Fn6nsq5uHYKUMvoi2dwwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyNFoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQClBJSKiguayx4ifSydXnEMoDWK+HeUkK/38DBFn/psx0fp +joXEUFXWcDba1WH9RtExs7PYD2n4w4BPSQjz6yIh1oNWc2HOpyHl0Kk5pL68yxWp +3jFzAJQHJGgkeRdMH36lUQwEnQ3U8a6YNqd0j5qp24p2qyv9AGARvcrCgp8GeyS5 +t6/tedqzzyujUNiOWzwdfCsdKo7Bv6VbPtx18b1EDQylQefSg3yxLXDgZEPlFPkM +91/B/p2Om5p2Xi1ybITT7dgexHqqVo6bPzL7mYUZKSZ+dYZIb36mTxY7GWDf8ifE +kMO6+/8Wetwn+hw5ap4mzsAJGZU1a0lUOsQa+T1pAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQCqtpCsF7eS40VB/wdjpn2o +jxGRfet1uP4QIAA3cNwGG4eWu4xW5lBk4duE6DXZYkD30qZYjHEsIPkefEc4sni0 +O3IDSeomhw8Ek/GCgPdk47JF0tJEKRIe4hjMGKI3H0Gmczqp6F78QJNUvn5n/nBH +9WWefV6sMsBLJ56CKRoakwYJzti0aSbAaaT1f/iwctTTxg7Jg4begI7XJyxTvs36 +hwnNCv0+XSol4rerpLVaMm2IKbzfpCHygqxUQ/erch3NJ7mRLQgT/biYM1atPLAF +b1BApc+IIrwDbDMeZd9DG/Ov1Kbns+3Lg5H1eNXhCf1DW9HQNu0Z3XzbARmxxQM6 +-----END CERTIFICATE----- +""" + +SSL_TEST_DAEMON_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEApQSUiooLmsseIn0snV5xDKA1ivh3lJCv9/AwRZ/6bMdH6Y6F +xFBV1nA22tVh/UbRMbOz2A9p+MOAT0kI8+siIdaDVnNhzqch5dCpOaS+vMsVqd4x +cwCUByRoJHkXTB9+pVEMBJ0N1PGumDandI+aqduKdqsr/QBgEb3KwoKfBnskubev +7Xnas88ro1DYjls8HXwrHSqOwb+lWz7cdfG9RA0MpUHn0oN8sS1w4GRD5RT5DPdf +wf6djpuadl4tcmyE0+3YHsR6qlaOmz8y+5mFGSkmfnWGSG9+pk8WOxlg3/InxJDD +uvv/FnrcJ/ocOWqeJs7ACRmVNWtJVDrEGvk9aQIDAQABAoIBAB80a4Z7LlCaQluR +QiOMHWKe1SEvdSVx6uS+1dIEu41gbdfbrK3/5wuC8syU90+22Y5FhifAWnDBP30+ +uWOuviiZ8QIjFYbHkiBsQeP1pF/9I16Y9s7heByVpN/oyiAKAJ/wYI5qyJfREAwW +obnoAf5G1rs0CUBxlrkkI7h+jOXji6E9EetW7kHI9owvZRgNMEEW2d/46Fh7zeph +00h8S6/AcsaeG0d5Wr7Dtpur28RSNUfiXJ1jccaQTgSQlE/CtTuD6T3CEbdPjIuL +xInR+ITVusVQmaouh1VZ72ciHFckgHMlzBJnUJmCN6iLEEKHtEwJ4Op5UjR77kxa +63dGc4ECgYEA2k2L0FEsRJnXmvYSh7LLuT6AH/FsCQ6sS7SMw3ATZBRuVFFEoLlk +2FHOXSrSsAZDumSFCK8NK09aGZOmZfJXi8C8BEvGTlbYQA4T2kAqkB3kc3jpq7D6 +r2WuVv0JDSveJHxj13ljfEpaPkpqG/u2pr5QF2XfV8e8oguIwzpW7VECgYEAwYN6 +fXYnLEhLttOqko7LQ9yrNtfR7uA/u8Rn0p44qA2DRuEjzgmCvZJH7wPJUkK22oMG +G9XcFW+SDZT5U3F6JnQSebSPF3R0yXptV+oWMAXRoNe+Ei8C6vn0k9dOXsp5Msd6 +dswiEIxSf9smwiz4wdmwxev0jOuACRaOhMm06JkCgYEAmNzCMX4VtHfRjPYQZasi +krWcPEHud60pot1r0BKz0VmpJCvAFZeccQlfqseovo+0b1mh+kGxxAkNu2kzlKGN +AhRU0+FHnGWdicURy7sw0rfL17vsTEhiUd2upcULyIhWRlBADYs3ybm61XGAOdYZ +wUr0hF3Wdf4sLYBMJQC+C/ECgYBtEjlZO95qhDlAzF8pChkhn+BVIiIuvPDPWa2X +Nh0DA9LJgZ6XxB6P0UKAQqcXmYjM8urfh5Pr9R3iT+SUFrLdt7CuLoo0kyw8X6f/ +1G47FRyJxvBX+W1wmgAz0DhZD1BuwaaSDQ7qOt1JOXHIImb3hEax2APX5ekdtjwQ +NkykiQKBgQCvrWTglqks5qiSAprEJduXZDuE8RUpyikfRgyTht9/yJ1ZLroj3g0w +ichdRSbSmj6F3dfUDYKZWEfFA8idT9nGDvvr4YVmuVmalc2AHDun1+SiY61ktGYE +jWBtNKr00MGo1LojGaaNp9Ru6w5o8Ae7vsbW1lknomm05kcELEPjFQ== +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_INTRODUCER_PUBLIC_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUKrZllgClUfhW5LVCxMSaWdpORVUwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyNFoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQCxiqJHT5HiXBr1CxICYNvxLMEOaxEV+k98RIt1zrjrcfCk +GlQX8Y4nqF2gn/7s0wgW2lkPAjTnFH9r29lFzTx6sVqGuTal5efJiUqDxMYmWkuw +hVXcqSc0ocxKtL5hRikp+UifMW6YUQr0XDBrszSvVZRKiRtcwGBX7CUJ3pR40HxH +hdzF3K0XnqtymKa/Dz7egwpAms8EH16ndvNgt0Oy74iLLqbLSYIMqtRe99V7kClM +QpDpzQX47iQyxL047xpS0nArTgOfE2wCL+4lWa220O9TZf7Mon3XtX7SJb71lEkj +iNsQ++9S0vzM0BcWGeV+Y7H0MqBmbI0sYLltHG4dAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQBWXFmtsc6tZu5VmqMEjOZ4 +sPaf2yJgEDaPv70R8SiopJSXHNbn14+ksehjdLa4Z3R2QhzCicVMnhvovtXeXUi3 +HuOnF6X5Xrk92wfw+1WW8DUsCLFFAjXggRxU5PDbXNafMSoP8005Jt7ai9S02JD2 +yrbE1tgvCm4HYoliURCXh9//w1fen6D3WddcHuCHLYad5bHnplOgvtFobJaG3FEA +Px3wWmlv2hH22gQf+zGHkwVOd8Z+G9eSCl+wABRGUm4ef3n94DoqE1GqWJ02fHBT +hv3YRFiVH8A7AtBWyY3sdH5tgaO9uCwNiQ0gL64UyKS49F8jT5ChVTxidKyoQbWB +-----END CERTIFICATE----- +""" + +SSL_TEST_INTRODUCER_PUBLIC_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAsYqiR0+R4lwa9QsSAmDb8SzBDmsRFfpPfESLdc6463HwpBpU +F/GOJ6hdoJ/+7NMIFtpZDwI05xR/a9vZRc08erFahrk2peXnyYlKg8TGJlpLsIVV +3KknNKHMSrS+YUYpKflInzFumFEK9Fwwa7M0r1WUSokbXMBgV+wlCd6UeNB8R4Xc +xdytF56rcpimvw8+3oMKQJrPBB9ep3bzYLdDsu+Iiy6my0mCDKrUXvfVe5ApTEKQ +6c0F+O4kMsS9OO8aUtJwK04DnxNsAi/uJVmtttDvU2X+zKJ917V+0iW+9ZRJI4jb +EPvvUtL8zNAXFhnlfmOx9DKgZmyNLGC5bRxuHQIDAQABAoIBAAvvnBVbPh2Pv6g1 +xFIwnNjL/3ausAlgOLPMD+wtp7T8hgciVgD+FmaIJTNFTmgxj9updk9SAKiAckiY +ETVmJOjCv6lLDmd822ZrOn09X2z4qRoG/MzG+oHJVui22g3EH8RYpA8/zYWj/S6M +fBzhgWtAP6X7LcHAlTmUALF3K1gr309YpV+X1YiwyREU6mzc72ELXC6ZUWcn3jnr +AH4J3UEeWS4eQZs612WJC8TMs502jO5eoDS+qoev/lt/a0WCHrBz/DEwTE4/zIRN +FyN3FrYu+73ROsnUCxzMM76y6ff6N0Mg0d0z/PrC9QvjzDqJGQOSxMerWqTrwNvc +SmSVnMECgYEA5hAMxfHodOKUKfkxYfCcb1qx4Pjj7Uo/g34PJDuFWzJ002mb6Znj +xEwdWh7FDANwywojSSvXSryFyK54RR8yLjrUmtBzxiiRi8uUREseX6MxiBO/ZeQC +t+Gd/VJ9PpTVsrn/X4xmsozXBBKxzcqHKB7GwDPSZjzA4HFgAQkQqCkCgYEAxY6+ +4vyOPfypuJAuPZ1Uswu78M6Km4K4JlviJj0czMm2L+7TBDljurV8yNB/dVd1lYS7 +xnE2u3KlIVPaqNpTR78OLkzDYIZr3lkk0XI2/sA0f7iuHY20DfWqtM8asT+dg2SJ +E4NQM27zRHGOplsvD9cVqTbH+u5iA3ElT2gc5NUCgYEAqQQzjhzJjU2EUk3LdMuC +8d/sfH39XS/F94Fd+F1t/HDWGRcmPvkatvTAP5wJYWkJrXDWOYRm13Ymxyc+HnHr +uMDpvo7T70mQm+ZFF+Mj/ljzI6h2XZGkWZj8K8Y1Uwue733r2jNYo2YK9OgTDj/z +CYuKslugABI0FO/V+wzA2vkCgYBj/sC3+8WzsnPlq3T8UupQRhw24xRdamjzYYay +fDRbo63HzUaZ2MKV+s7ShlY9eqtVpv20kBF4B6t1lxASb4+/vQDchpZOATwQK2br +bLhRcdAg5cWbx+HfPv3MzxdfqCd+HiET819g6UPQ3PmrUnQbvG6GW+gVJxwNSfPs +oXIekQKBgGbtSfX6kZFV3T7DzbR/5WGw/48KoJQelQsyxIxxygTTbuGJ6UmTf8DD +VO5VhdMozGozRn91daoIT7usaqlY+tOcSAtwvj1EL2MoshcSk9JAzC6eAW8ZY6+d +GqmLP29tn8eIkavwWpCe0sinVGfXoqdMHgzDenUHeXShPqVGWGKT +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_PRIVATE_CA_CERT_AND_KEY_3: Tuple[bytes, bytes] = (SSL_TEST_PRIVATE_CA_CRT, SSL_TEST_PRIVATE_CA_KEY) + +SSL_TEST_NODE_CERTS_AND_KEYS_3: Dict[str, Dict[str, Dict[str, bytes]]] = { + "full_node": { + "private": {"crt": SSL_TEST_FULLNODE_PRIVATE_CRT, "key": SSL_TEST_FULLNODE_PRIVATE_KEY}, + "public": {"crt": SSL_TEST_FULLNODE_PUBLIC_CRT, "key": SSL_TEST_FULLNODE_PUBLIC_KEY}, + }, + "wallet": { + "private": {"crt": SSL_TEST_WALLET_PRIVATE_CRT, "key": SSL_TEST_WALLET_PRIVATE_KEY}, + "public": {"crt": SSL_TEST_WALLET_PUBLIC_CRT, "key": SSL_TEST_WALLET_PUBLIC_KEY}, + }, + "farmer": { + "private": {"crt": SSL_TEST_FARMER_PRIVATE_CRT, "key": SSL_TEST_FARMER_PRIVATE_KEY}, + "public": {"crt": SSL_TEST_FARMER_PUBLIC_CRT, "key": SSL_TEST_FARMER_PUBLIC_KEY}, + }, + "harvester": { + "private": {"crt": SSL_TEST_HARVESTER_PRIVATE_CRT, "key": SSL_TEST_HARVESTER_PRIVATE_KEY}, + }, + "timelord": { + "private": {"crt": SSL_TEST_TIMELORD_PRIVATE_CRT, "key": SSL_TEST_TIMELORD_PRIVATE_KEY}, + "public": {"crt": SSL_TEST_TIMELORD_PUBLIC_CRT, "key": SSL_TEST_TIMELORD_PUBLIC_KEY}, + }, + "crawler": { + "private": {"crt": SSL_TEST_CRAWLER_PRIVATE_CRT, "key": SSL_TEST_CRAWLER_PRIVATE_KEY}, + }, + "daemon": { + "private": {"crt": SSL_TEST_DAEMON_PRIVATE_CRT, "key": SSL_TEST_DAEMON_PRIVATE_KEY}, + }, + "introducer": { + "public": {"crt": SSL_TEST_INTRODUCER_PUBLIC_CRT, "key": SSL_TEST_INTRODUCER_PUBLIC_KEY}, + }, +} diff --git a/tests/util/ssl_certs_4.py b/tests/util/ssl_certs_4.py new file mode 100644 index 000000000000..ca3c576bd21d --- /dev/null +++ b/tests/util/ssl_certs_4.py @@ -0,0 +1,684 @@ +from typing import Dict, Tuple + +SSL_TEST_PRIVATE_CA_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDKTCCAhGgAwIBAgIUS+xy2kGNomsBGfU0DEELWiHoSa4wDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMB4XDTIyMDMyMzE3MjkyNFoXDTMyMDMy +MDE3MjkyNFowRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8G +A1UECwwYT3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA9wgpBPIdclPot8ydp7C8EfKfZXve31sim/LBU9nViOTB +2vZGWcnwmb8bBg9gAaCCeSRHF0FqEmU0hUMDD7AY9npBpIzA4SUjUL3++/kKlZa7 +Yw6kWJoogcVXpCYvubeslpReXq7EhrrCwE83uqJ+gmz+pGkEaEoED42mw4xvNl+K +8B+nfYJ9aYlErX3JjQGtby+RWUw9aH3xBDDZ1Fss3+289OQ4+4KV6jPXXKZgniD4 +exnpss6LqdVhuRt6Wuhol7eoWl/kpcQfQ77fHr/8xTW1TsuFrHjsgJUXqGi73OuF +esOaijmYCLkgklocSND0LDeF9uzNjKEOXl86iHpOKwIDAQABoxMwETAPBgNVHRMB +Af8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQDeZ8UXHc6TqZGkvrFIY6U6ycHP +YSH42ihMkqJApBYWEpb8wFM3vAJIOZV0BBYguat8/7gApRGiy+xnq1TFDJiMLRPu +Q1yDWbYXM1fsObgyh83K8ZNQ/nFr0w5o+HcShkjia6pWaNsX5OErrJ57iJefaADU +xnLvxHXPDvwvs9f1mLZIZgzYtv3go9oQVOCgeIKOyV7cFUAAgIeyOidcoQEN6Gvy +DzlkZdmRRkvvIddDG0kcURmhMJzqg3Tn3hmG29wzSbsw6LK9kwr7S9BKNv/bgxDi +DOLVn9LP8qDeRA7zQtbneTtcEU6saUQT8Q9JLyvGUjk+Qgwdj24M79L9s0MR +-----END CERTIFICATE----- +""" + +SSL_TEST_PRIVATE_CA_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpgIBAAKCAQEA9wgpBPIdclPot8ydp7C8EfKfZXve31sim/LBU9nViOTB2vZG +Wcnwmb8bBg9gAaCCeSRHF0FqEmU0hUMDD7AY9npBpIzA4SUjUL3++/kKlZa7Yw6k +WJoogcVXpCYvubeslpReXq7EhrrCwE83uqJ+gmz+pGkEaEoED42mw4xvNl+K8B+n +fYJ9aYlErX3JjQGtby+RWUw9aH3xBDDZ1Fss3+289OQ4+4KV6jPXXKZgniD4exnp +ss6LqdVhuRt6Wuhol7eoWl/kpcQfQ77fHr/8xTW1TsuFrHjsgJUXqGi73OuFesOa +ijmYCLkgklocSND0LDeF9uzNjKEOXl86iHpOKwIDAQABAoIBAQCHx450T3fr/T7u +t6L4JuZYnxkJuNo5vmf5e6bTpen+wm3jInZsp9h9SVNbM1w7yLOuTkhc+fGJhuMl +VD38g3hVEcG+5jamCbmtiaD7cllk+2KoAcZHhQQ6v/N6IBsfY1uTsJ1mQX136HNf +LKufA+2xVqNoTujDQduViPBej3QZV7obRrcOuycF0SPbXQadbE0cEEKo2htfTgcH +R0LhPOFQQUvlV7fnTgUtCXZAnxRFvciLABUZ3SHwSswBhhWcbWmzaZQxzfCFOdpF +OvBk+xqlpSsa+pmgJ/MnL7O0b4y9JAdLlTf/w95w5gyJMOvn7B3LGeuwIZFHcxvd +CQMjlk3xAoGBAP19pcjZ5Kn2ChQXoeOx44mOd4ahAZXOgDgs1mR2tRPtdjH23Mqf +C3LaOtn9aBpW1A5UyJRHNOIlBQnqbvQcXHFbi1KJIs/L1yv638dUzmZyUZihunOV +hpLQCY5KFqej544BaLJSfBO+xK4mV0hxDiDky9WqzAP7kvmReRGCKqu9AoGBAPl6 +JUEOvhBkVHAS3FpVKbEkv9cIFLoY9wLI8/aprcEvSEgn3LmLJKcDauNKJBKXnukX +oXRkWYSU+CrOE4k5vDvos8O8KLJqDFZfrPNCuDz8hEdt52DI3Eu88oknfXbuSht+ +cJ04WjgyyIf5nDKvCgI7WvtuBLhGlmh9GRmvGcwHAoGBANc4xfhpH986GcaDZh0n +nPPetRbmPq1NncmUMBcuPoID3JWBbmbOcG78YHlS0P+D9xmP3JkgeAMFwWhw1fGG +3uoT+o+CBb9951vc7gCUvYV3zFWWAvM94ftmjKZ1uxRsch48jgLRS62MC/t8bCEC +dCdzeqkYEY3UHC6u16cI6GfVAoGBAJogvW1hF0l3Qrdu35YrcTOQ2biWtH94tvqQ +fjDRCZkFhimV/wbekQlh0iKUBo85/yJQyB3pdWi0xFFluaoY8lMs5Aq0b4wyembO +e0Ja4QpEk9CxdLZVwcxE8q6LqDbW5w/vYNGxJAP+U+e17ateeteAJiiaAu56Jahu +SRiWFmD3AoGBAPv5pzfb6L51/pey6kBL+Kry9gTEHN1hW9mfv4KyQbjBIDfBLSNj +GjrtGQCN3g0o/ElLP+IFXxhJ9k+KlFUZkQAFYMR3X9p1you8BL8eOCjJW+mk2H7P +rkUFuht8pmfgMrCAVcKneG+ZZfc+bvghcCmHVeNiioKfOw/7/fqDQ048 +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_FULLNODE_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUcVyMMiaYY4UmD0lc+D0TSzOgx6AwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyNFoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQCq6EP4SllvUyXjblM/2V88nxkpBwgC9ta7Ndd8pxIZR4Oy +lnNYiaes3gTVurf7HMAIZ0wHnlzRZghVvpZQ3eKGR1ETKYXaoRzlgwa1uZKGn8/a +DBU2bhioG31zYIOZ/QDf3ZcUR5JkcybdpY+Yj7YGowRkLT/GGN+5GgTA7tXJRWJQ +lpHNaIjFgjj5fKB+xXck6OhVHftl+rB2ZPrVgWaXPDBWjgpF1SJnxsbmOK9o4kB6 +bw6zm2wuByZrOYg8ckuIqbJhDjINKesfcyb9qSk77h+8ILj9b11OzZUfX7PhvgO1 ++W9se/RCYtfGgT4nSh9No7l2vA/IkwOnZOK7nBgDAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQAHcwPIii2n0DabvcPo4Kfz +iiHQfxOxlnTovNmI3VIWNYoe+ElYkuzKUlwbYEve0dEJwu6mWazIEo+Qexgv1Cs3 +Yn7djsmsC7q8pOvSmw+3t+6aOXGzeANcFUPJTgNIlxA0sucHoWwpROLsndBdqVWQ +LsV5IwgLJbcUt3tjaIsqfUXabqJMm/KleCd6uDzOuFlW7E1jdUyPomvbOKgc/jAt +/AxQimzkMt2uBRH/2IyOW1IS+035AxyyCfZgyhJIhf0l5oA6IiIvS47utLJ/DtWb +93+WPiBYIgNofNr2qGrDtK8Pm6BjjJtBlFvu9gNcHn0E2mC6TKNg2ppFNxXdX70Q +-----END CERTIFICATE----- +""" + +SSL_TEST_FULLNODE_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAquhD+EpZb1Ml425TP9lfPJ8ZKQcIAvbWuzXXfKcSGUeDspZz +WImnrN4E1bq3+xzACGdMB55c0WYIVb6WUN3ihkdREymF2qEc5YMGtbmShp/P2gwV +Nm4YqBt9c2CDmf0A392XFEeSZHMm3aWPmI+2BqMEZC0/xhjfuRoEwO7VyUViUJaR +zWiIxYI4+XygfsV3JOjoVR37ZfqwdmT61YFmlzwwVo4KRdUiZ8bG5jivaOJAem8O +s5tsLgcmazmIPHJLiKmyYQ4yDSnrH3Mm/akpO+4fvCC4/W9dTs2VH1+z4b4Dtflv +bHv0QmLXxoE+J0ofTaO5drwPyJMDp2Tiu5wYAwIDAQABAoIBAGjkEAMsrmMSpuhE +Z7eCE19DTc/OTu5yzCstykjoyMTXDU7n43btVQlVYaZC6HOnm3wM2a67VL/3XRoy +1FJhO4up7WpTS6F4zCFYHyAc+n7BSnHKhKJZQ6y44m+TRnGVw5mhh/2cR4561dmm +qNC7Mr40Apfw5XkQ/w29mDlI29CgUZOCg9e/DNfNcHULXjaBvIT6Ecr3IOXl/d9B +zdtyAZohHCv5m3Psv7ciFjDKb1nY1+c6c8WUFUYgPQz6SUbm7mvtLiH1gVBuEjgw +xI6RwhcNqEVY11+NO8hVcHTvwffEIDMbn7W2L4nvJsU6PlHvsxZx5OHyJIPLmMi3 +IrnbKRECgYEA3jrZ+zMaP+eeuV2syjc3moYK+dbKzk+ewk6EBQsUTyvq6CPvIZXL +5MnckGjw3CECIKwZG0gKph2+Y6M9h4sfg2Wz7Kyfygk+TolMU21Aw8mRhkWRCs01 +9hiZbCrNypiQa4BmeFWo+uyjVDafSRJqcTIcVSHaoeyRn9uRIYIvOfUCgYEAxODe +//ZBx9lufRXMYR6NoEqok/fsK8uORr5+l+QdZimAkdSDmGtY4/o9/TBg6fm0C0kr +9o0owbi43FwEq3ZLyPIevCaKHClZO5MQm+PfRYDs+uZhrAWaPVW9Q0b6eiYe8Xwv +Vw9aR23DlTPXzV1kiuWwsLIq3+Oq9C7pt2qZdxcCgYAfMyIBa3ZG/IzDN4yXw1LS +JfmKhAZrGrCOVRmh36FVUDQlrU3YaEB8+X425BTUwumajq7jrqSYF9rwAC4WRokB +GJk/JCk24z9VJV+K4u7Rzg3ZTREE9DScPW3kysmjpPG5tggs4tHbkLeJjjWku6oo +BEIIDb21OBJl4ByrGKzqtQKBgE0Ph7nAdfb3kFu9kIXjI6Q+FMX2IKbzwfjGz148 +l5VJYV2zRN8ABYcWh/T2Xri2WFaiiWaz0eQhnZoDGoDSiM9aldUncJ+dP6Ql6DZc +dyQJVrjOPCTM/JZNXQtcWOY+zZXP+eelxrx2pjtcU3e6uoPza7l9w3Jm9p8lTa3R +N8h5AoGAUAtG7NJ3Q3CCUsz85OUaDGlQVPNrKi1BBG5KEZJla2n82qjnUdOYRWx1 +c9gr9O94Yukur3VWtrNKpxnjwJx+F3akW9rHwEEy5xtlAJbi1mR7/1thgObUS73P +uapcJeqezHfl4ir03YUHSAJebwefo54B5t5pOdWEFQLS2AkggSM= +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_FULLNODE_PUBLIC_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUIpT5MM70VeIvnygCEHRVsL6Aq80wDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyNFoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDsxtOxdud/1pyMnhIdMYyqWmH80gO0Afp/ojKApcqzV3aB +KBFfQWLp4hc+CwykpAItx3vuTNXn/dQFxzC2NXrmq1OlPiPUcxorzctxqDNdoXlj +WTtxjkC+kxvvA2d7hgl+Huz3JxTWWMt+fM8eLTAOdVpPea7unW5GfQ4TwhdEMsGQ +4AmUylw/vjJDUaiOHlKwwD2c4EakZNd35pCtRZfNjM3CF9BxdK2xysWp+xoOIY2C +G61UFfaXiVK+XkbZB1g0+nhNEsur03gFXkFEVmetXZvpfTvQ3J8a3f/sguzLqmYq +spKleR4utiP3llVrgnKPWpDiMvsx+xRbMlbSUMvHAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQAV6jnHn5x8uYtbM9jN3cqQ +FK0OrZPSDsk+QJt1I0U0+HF8j2jMZ1RTxRhUCnjCoytqbh/fT/Xk+8s0N614lvja +84fmW8dKq+asD2mWrPZeEgpipk+fzthUOrovNobAWtpdEqjKWEvQxMOICijEXFbv +95V4kZS7JN+s3jAf5nXOK9EhGVI9214UyGNZZU/nzFKlOLcZFhGBibUAT7oA9bKi +BLbCPI4/CIdUnwaK7y8xF9z9KL+A24ZZzhLNVC35gl0zESgVqa0DN9XQ+GP0qC1S +1Z6PF4qBDeta+3R28PtfPIiNpYJ6uHAOT/iDmIWBKTGC2Y6NVs2diNZSOlZXSzN6 +-----END CERTIFICATE----- +""" + +SSL_TEST_FULLNODE_PUBLIC_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA7MbTsXbnf9acjJ4SHTGMqlph/NIDtAH6f6IygKXKs1d2gSgR +X0Fi6eIXPgsMpKQCLcd77kzV5/3UBccwtjV65qtTpT4j1HMaK83LcagzXaF5Y1k7 +cY5AvpMb7wNne4YJfh7s9ycU1ljLfnzPHi0wDnVaT3mu7p1uRn0OE8IXRDLBkOAJ +lMpcP74yQ1Gojh5SsMA9nOBGpGTXd+aQrUWXzYzNwhfQcXStscrFqfsaDiGNghut +VBX2l4lSvl5G2QdYNPp4TRLLq9N4BV5BRFZnrV2b6X070NyfGt3/7ILsy6pmKrKS +pXkeLrYj95ZVa4Jyj1qQ4jL7MfsUWzJW0lDLxwIDAQABAoIBAGOH7OMTpZrCf0pJ +BDpLYuMVXU1mhvH7Ru6yIuKbTsr2wDTov+y30rmYNcb55BWtb9EIoxr4J47+z0qi +geKGNqSSbnXu2ibrP5wcRSIA357DSwCtOOSyNJsnwb1GRBDTtlfS7i+yuPqllt0T +4AjCXAon4I+6CgP6H6n31ZwOobMjh+SOsgfcaCiSspyD1Dwnkxvz1tVRrLgv4RGa +gyzCEUN0zJaQTvTnlBhSOvZ7E+AgNa0gt2lYUKuSd3m8Hr/xtKKAyotFZZjkkI+A +GjK3eL7S2zL2yaYyJvQmGpwGwceti2oq5sytWXXREraFBxjusF1Yxw4n/pguvrAS ++5CDlFECgYEA+O7KoYx5g1JEOFCFpyfDVyR3s1SOVBe2pQIe7qcNbtrDFQu624Mv +YSvYNsvplSJsbFUCPIcqzEf+BIzgQQ17yDacczZnk5rN3VurU9xRaGjZTKeF73nE +bDhRXmIJlxCtkcXi/vzDUJdhAtzN1yTHhwgz7SvHHeqqZkKwPjEGo7kCgYEA83+v +vZfMOWJjHXGlKj2cAmHkLGrKal5l6HpMxOMqaoVbx/oDguQLP5uH8TQnckAJVyKA +ks0gJrJsu4lk5CCt2xgMQSLl+heGcN+usVmxiV7bBv3HC7E0wNp9/13NJWL5mrL6 +COEDIl/qjF9DI8ISam+L7KQYk+TPp4lxgyxxq38CgYEAk8pJuUHKPsIPyJNd1lDQ +M6NuAaUY3yo5AJxSuwOpAQCWQ590L7Eh5wH92wKTSjxmvKZ3rnHdYez4DcGJvnV/ +4O0zU1+gfMyynlI3VJGAL4nYQR9QcE4N5OZGwM9ZvDtloR8oVpTAbM+DBA0NlEa8 +wxmGoq+UBtn0ksPTGQlIVNkCgYA+XCUV2RpkV94qXECAYObjdU6KUY8lTqeqsieX +BNaIET9AJ7defiijUaGvFDxF9kBuIFftQLtLLcbLAJTmF7hus+nvhJCBTCUSIzcK +FH5zP+e4EqY3SFrKSSqbi9pOCNsD03JVc8rpssbOzFbVgY5V09tx71ScC61iqsB3 +Z5p04QKBgCDlV7wh15Q+C0KSR26EE+6Aj+CaH8uRSoDO707vf+ZS7oqw5Xivl32Y +QHFVDim4TF3UyYJvCRKmvSMNNsAKc9hFBU5b3TlBMwa5Cx8l+fOmYhe9eNsP8BNm +aOrHGu+nvZ/oke47p1HXEEqUfPw5hPdwo11jwa5lUtSJZhLQhhT/ +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_WALLET_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUBOIDJICHJA4JiPMdTj1KC19GQOgwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyNVoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDUjguITn4XjS5HDyHEP5Gmq+dgP1dxpYLMDWnJtHZatsYJ +0x/rnEmDTReeQgiCbISO1oyK/TZ/L1IfzoRTm///uNns8uBnku2VmUDaTUd5K6ws +WqxN3YU+h3P5de6otb0DGkayANr3bMy3KQqGm/XxQB2VvXuo4nzbB4Tk5jDO6vtk +lG4184IbY+08nrj41L3YNCOynSnRDi+1ipfc/hrIKuCA5Giuf56S02hHapjEw8+W +Tvokx1drAlcJkFBLDVuPd146L7klCAhENaIYxJmoqLme9ZNOta2jmSEpeIHbsw1x +HPJNi+6n/Kh7k8Lsp0fE+jgQfPDEaqOXsnLCZOcxAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQBXHURcU2ox80IFiBq8xEDO +cmHDZVOrQqpmOdygP+q/JRFoX9jF6X0nR17DBfDedgBz1ghax993nXwGJswvs1K+ +1DsTIKxO2HJefd/5iur6mxLFF8RXdku9as1tfNx7CZMm9WcML3XJu/u1/E+JRUFT ++QdoAX3+GnZGY6g+2MUSo11+3Ide9aaHvRUVDIXxqxDOZgLvy+EpIoQU4lkL7cuJ +CuFB0LYM9FMih1BX1uEu3wnCRhJq2nJuIObMEU8wQj8MmgPZTcJh5tyVR8OeDZ73 +CACnvjXydwYvFmcB/NCMLpolQxxgrpYKrz8dx1NN8mZRNb+HUrM2tiLsWKAdFEid +-----END CERTIFICATE----- +""" + +SSL_TEST_WALLET_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEA1I4LiE5+F40uRw8hxD+RpqvnYD9XcaWCzA1pybR2WrbGCdMf +65xJg00XnkIIgmyEjtaMiv02fy9SH86EU5v//7jZ7PLgZ5LtlZlA2k1HeSusLFqs +Td2FPodz+XXuqLW9AxpGsgDa92zMtykKhpv18UAdlb17qOJ82weE5OYwzur7ZJRu +NfOCG2PtPJ64+NS92DQjsp0p0Q4vtYqX3P4ayCrggORorn+ektNoR2qYxMPPlk76 +JMdXawJXCZBQSw1bj3deOi+5JQgIRDWiGMSZqKi5nvWTTrWto5khKXiB27MNcRzy +TYvup/yoe5PC7KdHxPo4EHzwxGqjl7JywmTnMQIDAQABAoIBAQDQJN1UOJEvnPgx +a7cER7/ouDQWw1BtIMgJ7CSo+ghgYtVhr5Z1khFG+8piFoXgukXA3oa4YKfqOjuw +m/pnKb+x+qGlcF2h73aq5W1lmQGhvcuXj59ljMS7a8d9BSiVm4qhLC0IiN/kJW3+ ++ritArL/8WpHRUuAIXJkxmM0B9rJ8/bPYhcK6cdgwV09KOKRL5FVrmVQIzNlM3F7 +EakCLVTaHeAlOHTt24XU5EuhV92ZtHorQOUvdx/wjWTGosOZxs92DS43gS/ZszUq +uH5mfbRTyTbde/BEKSDE27dgYDV2uJ9FuUeTJcJl2kjuKRUPrhZf+0FoCrhcAhzg +JIjVt3ABAoGBAPJ/AHWpEPlp5v4RxhVrwZGHbwac7kzzaaUqS3r4tgw8iAb7iEZF +VDyVrM4gXsfaKedUobxaVMUvH3ajlIHF6ncnND0I7u4jff6lc+V1HqZbOs4xaLwu +zlrfecfU6JOrQ9UnhYk18jpShzMsoBl2mdPFe8QZClvn6UMfXaOdot0BAoGBAOBk +NEchBh9Tms2826G2PZHclp9JVpCYWdxqW1/X3MoTt4T8gQUSGbeEx3ieY95oqAE4 +eE1RhEniIQ8sQtdgnZud/LD+9mrSCtaAdkDOSCulCXNt04B84odid3LHkd+qZ5/5 +rqctxAmECxLFaYHU6eIeAMhyoFx8gUcPLjIvRpoxAoGAPbMox8c7pWpXtr+I+fxP +5RpjmnglffjVIYwwZlqP328rYTNwyghr1Xpz3IKJ+ym8TbrP6B/Hv+AsjkAy0g4s +iSK1fO4f9QGc1kT8xx5UrRk7TiluL6ttH9wKnVjD0IbcHIkQxgeu2T4UXmX1WmU9 +4I833X5Nj3LjfAuoBLBgNwECgYEArzUOrMtYYHWDVkm0pJwLjyzSXyWAdf6/i59w +IXIHb7HdhfUzOKZddjIzHjdue2b0Z5+UL6sKxDXQ4mwI9Or3pV7Cw+EQv2+qDrrX +mtp2970xl/OVRao9psB3zCOP/zirGPp4KQlPHK8BhnKmYz3AMVKEAf+evxhoSPfN +dln7osECgYEAy/HevTnTfcCn6TZyYBz3rHfZ8mZBO1I6gyVytcNQNgmz2ZPI/Cxs +iDg78PSjypKSZ1PyYFc+5dIIw+02guglrTnIrrxUbGp7cy26VQfEwGLjaqFHZplc +PFS4W+Fz2xVGnkv2sQwh3tSihThxd3hQK9isI3iHBeZB3HDWpGQzRMg= +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_WALLET_PUBLIC_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUN7Hj/9zSlPgvmKSmTRU0Kd3ok3QwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyNVoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDgmG/ekJTb+4NZslwT7MXUZGuOOId95dTgXWrev88flfsE ++lEl1MvVKQLVi3F1I/mxZujYbCW4bRqmwEM/jwpLNxd6lzQjGLoePQWTURCBdnAY +RtslK1eetPhKRMJTTzaH21prkEEEwjqNb4he91O95VMApYSkEX1vOPd6LhxW2X5E +Z7rwZzgL+qwKtQy0VdnxYIcBnEXIKanNodC3vR6BRTU8vBvMM1wtxjKAAcs+fQVY +6eyBu3Z450Fo2Ge1+orDgLdN1qSFEDw/DU98V7Xo8CzEoBV0vRxja7VU3kRqIMKk +EyVwjD6QBfm6CadVC0+vj3B8sbFF4C+w/gkZUkodAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQB0UQbCG+es9N/hKKbzG6+P +nXbd/c8LB0iJIPZ6xyPWoZsyzPmItMt3KFnYko7EbZ083Uuvfy+IyHSPcxihxQl4 +qLGvjxHG+relpKexwnhYgRWuW7d8xI2WMjDT7Xs6q6npbCQjwpZXbKxe2Sr/WMu/ +HitB3RwZ/CCeQL5oh8rm7TRWi6boqJXG6Yx6jjPRrs+f+V7deXNxHs2eV3RUKdBp +Lc6IiNoTtdkZm9MS+xsucm0HJa/Mf0/Gbos5A9oqB++q8FR18Rg7cEt6oTyPjlBF +xq/9SRUCLAeaIVufV+Be/bx/tX117/MUQth3cqSqlMkPfvyIYmZFbJbr2wlyQ4wS +-----END CERTIFICATE----- +""" + +SSL_TEST_WALLET_PUBLIC_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA4Jhv3pCU2/uDWbJcE+zF1GRrjjiHfeXU4F1q3r/PH5X7BPpR +JdTL1SkC1YtxdSP5sWbo2GwluG0apsBDP48KSzcXepc0Ixi6Hj0Fk1EQgXZwGEbb +JStXnrT4SkTCU082h9taa5BBBMI6jW+IXvdTveVTAKWEpBF9bzj3ei4cVtl+RGe6 +8Gc4C/qsCrUMtFXZ8WCHAZxFyCmpzaHQt70egUU1PLwbzDNcLcYygAHLPn0FWOns +gbt2eOdBaNhntfqKw4C3TdakhRA8Pw1PfFe16PAsxKAVdL0cY2u1VN5EaiDCpBMl +cIw+kAX5ugmnVQtPr49wfLGxReAvsP4JGVJKHQIDAQABAoIBADWQZW3BMZ9dVrA3 +t3oRCAVlhbk/hiDihWiVHv3M5Qr1bA593IiXPZ2y0Dg5r29uiwhiMLoc4MohSy/l +vqQT6zKRCwpzsT2Fki3QA9pkhPk7U/SWQYGV2qnBI04jI+1WgPzZtbDdkIQgBnLg +3Lc5aUFqxebrkrzGZxH1liAPizay6ds8PsGGN7AAeGHdcyL4oGIlQux/jbiGbBzU +NBJmD3qvmbjyrc0J2wYyCJtlCyMYUW6Z428iRI8Qzv1wBzAmnEoKHlFnsIwvzPRU +SlG0CdvWyckXPy9MfWpjmMhQos7U5MZwECWl0aF9MPSkLSxuyfeVWw4/p9tOWS/f +ebyJVwkCgYEA9xU9GAgFzpTHE/1uFNqHSfQxqtEULo/FsxBcOsRJyVvO55+mQN+z +S0UtdiQJ7s4Vzz5UXrnhG3qgxtQvgt6grg2m7pPN5UekIDNsQoDMwMLA8Bt248vw +3TLbSe5Rn2H54F1MuxfoxG/sTMpzNAgNuc9TuPkRok6X+nsfbfiPQXsCgYEA6LNw +kuShVbWRu5aZd8t2hVSFOWN+lmI4MHhpDCFXdTyJ2kxU3k1vdPth0i9kFU3JktNI +Ptnf3R32Uf13lWa75e/0tHUlHaYs1gpdKqDU1T7OvU0xk0zzeogshc35cl1b7Lyw +Op+PJX3F+QjK3Y5gDEnKAUpi3Z4MFvi7sV7BE0cCgYEAuLa1/5svzCpJUdZqT0i3 +T0AxSUQY1F5DLASVDpHjn5b07Q/bGDCkNyc2P1Xd3xtODqrIJDDN6t8YBsxl4G5S +rQwjucPhbwDJ4BDRZD4p1AlEd7vwe4fhP1xft7tkZcS3K1ZUoKHVL9WEUDwhN8q1 +iC1Ip8X7ut0KnQUij/H8FrUCgYEApUF5uvgfE01E16GWD5sw6nMhwaXE6muKV/HL +OFAdWibKB3uZ8d2wP7WevLPnMbHyaxEdB3WXI+L5YTTOTg2Ndzg34kgnOa0fvknR +7EoXm/FkxM8jW+aUKvq2E7g5ZFykwbUmuhtCf2+YvsjduQY5c8Cbctsu5xAsqcuv +D8GpAFUCgYAWVdHC37arXcZiwLO/YYPE94PbIqd5LrMaDXcWNcwFSbB7kOnINt9G +stvnxoD999PrSJLRbnvWyQhcGhK8qXZtDyh5sw/VuUsufcz+xSwI1+K3NSBEJc09 +e9XstIYrR5rdizf1g6Ai45W+wAcs2ysUBfnY3fUv4+n8ZoRiLXCepA== +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_FARMER_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUH2fAuEi168jppWUawNNToTni7CQwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyNVoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDRJQfoJxErXVdwTU4sAQ+05EX8tC1REoYFJyB3hhB4LfOz +meCPe85X6FTYbcPdaL3NVevqEL2hzbqESD5Gf5Fvx9ctIdzwYv674P/tqPrIkTd+ +cLshBKaR6CeSjPt5cFbWhg1hnmWy9Ta7SsYqOXh2aBQJKTRIK80U9b+8iVs407E+ +NCMljerodAdePW5X13i0ca9ifwbTZkS2/c9nQEi7qldj18/MVax6qezY0aKqu/96 +8DSSTc6uUiNLKmRYP5xRJw06ggVooviX7snMzG6SgsbLK2tZJXeJj/EECOY4UrPc +FOkVmjxGBP3QD0oLoXnZpxvutS72ovUrcPxAMImxAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQBSOIrT/3mFkZmuoQ/7UcPR +TOssC9o0xAwR0iRdziNQBZne2zLCxk6p32di+T6GIJahqYFqVU2cfDyyw9YUCr3P +nbMjuwsSrvor4LwJ9jlWKRRyBm/adCttg6OQIO3rYk/rX7XE+fBY1wYhqK+KcSCb +iRLqHIPEo9On/kpaSOWKFeSrGLMkj8nQWMNNtbzTNcdhQlxEGXcxPsVb3PGs2b0p +n63n55HTUbiL0UsHQvxR8gr0YiSpmlgJLi5pipDbyMiVHyCxj5j5vEovXckRliVd +iEbH235NTXPtJ9aBVEDsHW7W1AhzK5gh0Ev0xOC9vP98qTYR+4Oq/3Mz9r2jP5/d +-----END CERTIFICATE----- +""" + +SSL_TEST_FARMER_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEA0SUH6CcRK11XcE1OLAEPtORF/LQtURKGBScgd4YQeC3zs5ng +j3vOV+hU2G3D3Wi9zVXr6hC9oc26hEg+Rn+Rb8fXLSHc8GL+u+D/7aj6yJE3fnC7 +IQSmkegnkoz7eXBW1oYNYZ5lsvU2u0rGKjl4dmgUCSk0SCvNFPW/vIlbONOxPjQj +JY3q6HQHXj1uV9d4tHGvYn8G02ZEtv3PZ0BIu6pXY9fPzFWseqns2NGiqrv/evA0 +kk3OrlIjSypkWD+cUScNOoIFaKL4l+7JzMxukoLGyytrWSV3iY/xBAjmOFKz3BTp +FZo8RgT90A9KC6F52acb7rUu9qL1K3D8QDCJsQIDAQABAoIBAQDBn1fXnDtv+yVV +KayCXqUs4dzNW/1MPirnIFcVcH9U0633izDzhTn99nB+Qfh/xVVagP48ny0AXBce +GkfVOorpgoh6FwyVXADa7S3i13r9LjvDChikM8sF73ibW3wA7HjoeAhxZJRgflYi +RNJ7CuO0MxzBcm0dl7dwfSb6I+vZCa5b6McimTnKhXSBwQy5m85ldAVKOZveU+aH +gLU1qSmW+2x9jErdzJadhfqrPDcy8UBla0uqamXfPBCeSdcSRHtlWvc9RxBcdtQF +jkzhCyGK4K2cBKEMa1rWCcr+kAd2uasEoSTfe4LMPgQGbS2e7sl9fufbKRDkZWrH +NuGr/aVlAoGBAOvjN8lAgOQJyI1PmKcZnbUXRFhyYvjZ9kWmsN73RpZ3yhWPL1MV +0qyVHD1VcqnRWW/Dw3Jw42cLw+LZNJMSNxPnl5e2MtxSnmYn0PcrkUcpOafxy6dO +MV6op8PeA6njtMMEFnnPBoVwM9km2OiSW2/L3+rndtS3eApRzkm7jjjfAoGBAOL6 +FolDf9EJS/njwHFGhMRfUvTC5C+YJh/vUjkY22rUw4v5GfxTlDVigbbqzerzMnA6 +XSU11S/I/n7HuqGbc5dyUGGyk8S7Iv7hvfDPLfy1EXlKvQElpcbXG2Ln1wZ9E124 +9+jRzzOUv0GcuxMXMGi+OIRsWyyzhNOZUlLYWT9vAoGBAMZhFO1//eXKBIwzQKJn +fKZrpuLrcCjwxZjWEzGwrACnRaDUBmzNkZvq2xEJ56DBm4HPFXQNVHG5B0ikev6k +9wUaY/cHF8cLBIPNQIRec5NxLDf9tdRCgmqlVFH5SQN7qO3JZk2Sul1Ge5RIg2s0 +iwl+YBZiCyHiGmYzXlXMElPpAoGBAJrLQglUzluqQnVmvLzEAhHGjNW+AE7xLbcD +yQiFikZ+Weog9XbfLSmHR72OvuZn+1MMiq+w2fZf1ihyYDaMxLVZUbZ/SkWV9pTr +MVWEhfFdL1CQBvw8R6Wm19eJE10qecWmOvQ9+lhMLm85y1+Gpg4ZBIRTPY7r0z9X +xgwX3i3jAoGASf/FiOO2qtKDxn61LLVedxDtdmxd8FHLP35i+4S2ZyeIe8l9K/Cf +lbXe42v2NEKSjYq+z0HKdtjgZLkL2ucF+rLbkBfg7G2hmWob0hfYCE8xcnNPWIKM +yoKW82uijH86yJEWHAFAv3IZhU1CQP0BkYteAxE8CExQU/CSuHsCDao= +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_FARMER_PUBLIC_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUFPTg1QrNlWLRFn/hC78RXi0IOD8wDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyNVoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDB7fCDqqnna6BfaJsv4xztP80jBCygHHhHdliSWbGX/tm0 +lK716u0lBRiI3FmzPk9MqKMuV2Vvjsqm7gwLzG3oMHKaUPJUOiVf5v5/rXauNiAA +m/sfRAROef+ZVoVmU6rqwnGvTf6POI/MPdEbCPGIXu3bWjPXPq/BKzJ0KCJh7ekR +DaFWCkfQEs3Zilr2ehrdObzwHOxJh408t2qGeCQDlQeDXZYa5cTHo0tNoSWdfJIi +1jNlEnvpo6ZV4fyKY1MiN42J/2swaxypdS3KxvPZzKWebgaloFaZoY3HziotiIva +EnJrUXNgQI1lW4/a7jooQ4c7A0t2XBidRS5Sohx7AgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQCAQP7xtLehAfWTp1k3E7IM +eMT0Qm2iBWQqB4RLxIJWgHyVHYeY7Lp7lS+BZFKHOkbVoc2J8cA3qGGDw3WrmQuF +uRxfL+bLC+HMSjEvPbXmnMWAqMKmVBzOvZf91VVKsFR0h4Gyt78EFteZVEHZI5bQ +jC2RLte2uCtht/Wum74z7jGzplx3fRV+zcig7Gka8IwTcHaRpfhhZa0wbbR3Kfe0 +uXcCgDF18rwr2uIDNPBmJfy9C3nyQugWzNV/j/FDmrqHD94tagfDx5wYPb/ZFVpM +TRg94mZSL6OkgITfba9MkOBcVGbkCO15NckC8rm0p3ngbEgu62eW9FlwsE8b1bVK +-----END CERTIFICATE----- +""" + +SSL_TEST_FARMER_PUBLIC_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAwe3wg6qp52ugX2ibL+Mc7T/NIwQsoBx4R3ZYklmxl/7ZtJSu +9ertJQUYiNxZsz5PTKijLldlb47Kpu4MC8xt6DBymlDyVDolX+b+f612rjYgAJv7 +H0QETnn/mVaFZlOq6sJxr03+jziPzD3RGwjxiF7t21oz1z6vwSsydCgiYe3pEQ2h +VgpH0BLN2Ypa9noa3Tm88BzsSYeNPLdqhngkA5UHg12WGuXEx6NLTaElnXySItYz +ZRJ76aOmVeH8imNTIjeNif9rMGscqXUtysbz2cylnm4GpaBWmaGNx84qLYiL2hJy +a1FzYECNZVuP2u46KEOHOwNLdlwYnUUuUqIcewIDAQABAoIBAQCwX4qi9RBZXNUa +cLTTNKcWTzRuaFl9tObfd47Oa6zNJAcz6RXGqsbLKHtL3bvm/QB6I9VlTC8A6sj8 +UPu7r002IvnXx07ds5RSSG+mB0ks4CTy6OnXYbDY/rOr7bide/KyV+21FiYyc6q0 +gnQvNk8VS+Df4oXLeUO3V2Ynpmi+zl4B9dDT5nZ6bb26fYI8ei4HDPexPdQbJQ3k +mCimfkMbodEsrJfhM1V/NPwHxJvfXa8mTsEuJnn+efB7bsS3GjTtQ7nuIiliSqHm +SONfIUWYdgEo6chSnKA7MDWFsoPabV3zYrPU/aJqMeoCH2jNEu52NiOhTkv79Dg3 +AAqci1CBAoGBAPbdSxAWGaRv2KEa3Zry7snhhSVth3/wY+imVO7U7Z3dO23b+KNE +iEobXbuK6eLKvskvcHel04DfUbOhNws1qytDpG4rLklXZvX8eWpTECkt4BPfefVi +SeN8FCxpx2JSmQgiCzDuAGTg530OaHHTbymhEr+Sl8rQWBZYjo99A5lLAoGBAMkb +KLDai93wWYt96MYn4NPxBhUNbSW75wKSi3PHgHvnAM9qsInXo82BFmw6oUFCit6h +zRc+gAp2wsqIaka9/Hao0MSeCgR3xuuohZeOKmSmItnw70k75Qgrnu9wKyCEdpC4 +juDnkaf8G6lT7+Re9g48/MA0Qd6l6WVvXs5nTDuRAoGAaEDqe1+p8pzdcqQi5FYl +7BIWpcjMyYYe21irU7WOp/WPLIUSSvkdSZanDhXLUmDnE5W6PH1Ghg1Jtr+lvFEs ++Xd2kKQhxw1nSQkXyYRMtedO03W0TqH0rGJxLpR5hJd3U0z1RvOsLO4iNNkJ2NA/ +COiiP09MVXWJTd6WThiwlWMCgYEAlbwfA+71DP6T7YSF+GRgxe1DdhFVHy9UxVmP +c1krlRVeSRFK+JcSY0SmCVduEUUWWMVoCtKCS0g5qMsBNkLm4wK2zm5NTx5Pgc8s +CLfVYLNCZ7s4rvJliTvRTr3ZnpCCJycDWvmQPd88SUsx5nu+AMPv/Lvr/3bQ3LGb +iVroK8ECgYEAoSKTrvPcy+/ViHCr8ctBK7oIWiE07l0OeXNsH94hTp25OtBScGIy +alsJJrq3P8NjRJvxvbV0FXhMN2Mq08w3sZdCbZW3t7hu9mk1chxREcmmmfcJIqxx +x3zd2+szabHIFg5ozqybYbjmJfDZxNIG8h4HvHRdmBZocbRQHBM5VN0= +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_HARVESTER_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUC/PhYZPSyT/cH2+54Hfayu9ZXIQwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyNVoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQCog5fCeSavpTI1/3mA/Jv9XJF6ZGRzmNJq8JdRPUd/V6dQ +eU5cOE7QGXbqBV1gQUOXIf8Ke9wgwcMPGRLBbhMQcd9CqWK/rZZOuczRBoqyEvQP +YhkyPWmIbRsNTLg6KYxghdCD9Ps75YWBpxb7uGwKgYadixPGGTDlbGw4m4T+xoSX +kkXcoCiYf+AnWdT/xYloEG+ovy0oBfOfv3q3ps7ECZRX/hZ+4RCRgA1IYi4z7VwL +JepZz87PH6zv9D1CmtraQVZUD3UJRfMGVsimLlH09r+mbZm3Ndu9cO7fLLkLzLP6 +w9hhibZtnNAOyLDYrgOjl70bGjePzeodmqzPMY2zAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQB2/xNHBk4oXI5iOyhh4Kob +7ZVFfnjnOFFVhoWYAHSL4rJy6Zk12VwqsvUh4viwkSgmpKkZxyjQR7rKGTyopzHJ +aUEV7mhHc6lpM1biTZyXHz9PIJAgiHe1J+CTm5qVx9STi1b1cJ1FlTYK+7TT/noZ +Pscg1KOw5rmUbTcJ6UnBF1EtSSl+JzPZDxE0/NKBKJey/vRZXj8l4WLiN7sZTr9v +n3AnByDua5653q+AOTjoWP6/U/w1JWa4QNCzk05qBnFLczWGmU/oAXuvIRkTZ+wq +L/VQNnOjmBDeN+LAVAjUxDuGxVJiuCQisUiV5KZVl2kAayCgJccEI5N+OYKO2oI+ +-----END CERTIFICATE----- +""" + +SSL_TEST_HARVESTER_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAqIOXwnkmr6UyNf95gPyb/VyRemRkc5jSavCXUT1Hf1enUHlO +XDhO0Bl26gVdYEFDlyH/CnvcIMHDDxkSwW4TEHHfQqliv62WTrnM0QaKshL0D2IZ +Mj1piG0bDUy4OimMYIXQg/T7O+WFgacW+7hsCoGGnYsTxhkw5WxsOJuE/saEl5JF +3KAomH/gJ1nU/8WJaBBvqL8tKAXzn796t6bOxAmUV/4WfuEQkYANSGIuM+1cCyXq +Wc/Ozx+s7/Q9Qpra2kFWVA91CUXzBlbIpi5R9Pa/pm2ZtzXbvXDu3yy5C8yz+sPY +YYm2bZzQDsiw2K4Do5e9Gxo3j83qHZqszzGNswIDAQABAoIBAG0x0E4ZOUNJ9Y5d +/HrjtaTorfA0S49IcNkRC8x9u+29e9K+uFMzvYZFafPdBBPSVp0BT4WYmxyy0dXf +tnKXBE18rGJC5pU0Q5jB9wFfjtIzS+kH9THD77WSlZv5ocs2jxsguuw2+/FlGizY +fCEi8QehxPwjWe3c9v1DU6EezYBVT9ANny0k/tjIRWgl8XpCuErIOh5LbzdnVINx +smAPD5xbYA85HDSGxWL7ar/nV2cMyMPrXjq13cfMyJSgMgGQ7imMje3c4aUDNZdK +hhuNIAYqGqfGRvT7Sg18o9upN0pm8NX5gXDOifgqLgI0Pt07pomm3LdOlz1mJlqo +sWYiuYECgYEA1xuwXlNl29BiKKAxjpUPJrxwVMX2J1Ijrx76YfLWRkWRzCotYqKD +vvy3NyVUNJW2mDunAq1q/kjlf6c0M+bAQdF895hiYgQWmz40NwtlBG0NIuUvtUCa +EAj1RXi0mKvZ9da8p2eRL4GZW9CSgpK0d9Yq9NVJDxohL7OHuE/KXkECgYEAyIxi +k0BC4My6GX7zaPBt05mRWsaWhXpP4pN67sox9QLm19rP0eO2ivDrvc4d1ue9JyOe +W/mmmPI8wTIm2AgJvXKhUz8x1idwJ5I30NicOflBEhg9xgNlXFv6ja/QZE2PjyUz +l7UTE525Zwn2r2mtQSQoCljPYF2HhgfFkIbglvMCgYEAzCOP3gSBbvk0nl9giHK3 +XUiJxjnUX/6YtNHORnRBm5DcS4hfZ/LY2sBUU7ZOUlUeYxyBY44WMtoVSm7woKzF +GfFoCkUIYQKGPa/rt61NocSoKcyc2QNE8iC+O77QjO7SO3cdtDUaWJ5CXxryX45A +TFXokE91NSrUAcP78hNu9sECgYBWB/MZnA2Uhf6nhVBCCjHy/gPe3yYfKHMwjXfF +DDQWGSKSIqnYLklWnTdj+xHN6Se5rIv4hMY1AmWRs0P6lKgo/w25unhUmCKCtzT+ +gI20SPrjGkcVtMs8rbB0K6HIBYW6MIlLYUBHv/eS/jE22qyaLzqGBccgXb8PfjIA +Z/vchQKBgAv1/ufFNbrJqWWHsop8lz73D8jG0SEEFXsyOvsAT3E0MzfKK00J+o/q ++1Sw3QAXWmtn9/6QeRFk2J5ikSKCpz20oWsVWIoCHVZDb6/FkrLJn1HbFGIi2BD9 +eIb2MjCrudZLGJKlrL1aMSHJ1QZZNczDF/RtaUyD3e+15qLrXczQ +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_TIMELORD_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUKXI5XpFZ30y1ouQq11Ke6CJYc80wDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyNVoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQC5cLVLbbFTsBDiYGEvK2t3V2I1o/OjEbX/xqAcfYasvNTo +E8q61KL3tjIB5kdKYp2Ywl0ibUSnM8M9Zm2uMz+Wrpc0YB1H6IEpoN796q36m6Xh +/O/lDpatgwZkRiBnKVAuvifGeVb868keNBKAZ2DNHdpQcRtbkG6jMKSNonmOh3Jx +nurvv01mvf7VR7iTc9o4kBIgO2lOnxB2dpFcPxdi/kIIGjrUgU7tzAh4SfQgKKVg +WwRDk4BrlLWLRGkPsIvvoUNkVuC0adqXOaX+8WRfalw1mNed3OpYQ+W0SjHWuw7S +CxrpcfvXMniy3CpEdBJmbtKyC8Somj41QxmRRZblAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQBVYAjTaWQpR8Ke7zD5ahJg +G/UDUUh9ZhUGCc4E0e2+qVjDgkJMqNjXx+OUUvj+iKo0A0UI/QJYf5n7kxs6C0GP +JLqt/OHbxdqRji3GQvftRAMjaIQ0KmRUIgN0XwbDUvFJ7QHBdV33AeWvDh1wWvtD +DYmMDausznQQBlYApt5LDq/Y+96rmMiaPGOga1Lz1GWON5v8RU3qPLyc8GgCeoaV +zoVLKiGDVtrhUzaOBTsZ464ZH74hIzjEb/QDqyoQYgk4o1RSdewNhnzmwWmS6hXD +E11IwGGiy0ix9OWyL5WyXvjHqjtJ219KIYQuLsEV/lOdHsybBcAQygjVqgMkz5CL +-----END CERTIFICATE----- +""" + +SSL_TEST_TIMELORD_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAuXC1S22xU7AQ4mBhLytrd1diNaPzoxG1/8agHH2GrLzU6BPK +utSi97YyAeZHSmKdmMJdIm1EpzPDPWZtrjM/lq6XNGAdR+iBKaDe/eqt+pul4fzv +5Q6WrYMGZEYgZylQLr4nxnlW/OvJHjQSgGdgzR3aUHEbW5BuozCkjaJ5jodycZ7q +779NZr3+1Ue4k3PaOJASIDtpTp8QdnaRXD8XYv5CCBo61IFO7cwIeEn0ICilYFsE +Q5OAa5S1i0RpD7CL76FDZFbgtGnalzml/vFkX2pcNZjXndzqWEPltEox1rsO0gsa +6XH71zJ4stwqRHQSZm7SsgvEqJo+NUMZkUWW5QIDAQABAoIBAQCTMXd2K9e5ieOd +DMuXWWuwCtiVo1Hcek5wfATTGIAx1KFapXsh2W6SGTiQcWzdCnH1szGiBgGchmXO +8uLUhzFONb8nf7M+RLthg6P9AK6gYbPGMbNpqa7Ig1wrc858lDplH+MKk34MvEuj +gcm9ylD3/14uw9jnUTnApve2xOSf0GDUdIAT0AkYfHmn0/NSev40uMi7q0KmH2Tf +3VCHjWTLKvEp5/z6a2vBw2pxQj4KpC9TobfZZCTBbnzt/VnW4zG3mZHcAYV+EZqU +/UZzmOHQC/0F4FkOzS8THlWb5rXUAv80NpOTzf/LNQXy6QhBpQN9a3atPvzvN4Eb +dt0vD+PxAoGBAOh12gPNpXolJJYIHGhhKqYzFIBh9pCJPlmQ3ok+lS4Frhfgzjzu +5m1daB8pUO0du4n+KLmQMb+cXpZTJ86KqRE2MXtuP71wt1RTxx+7KQCuuz/KfY8z +8A7NzGk9swXx8EqdfgYcm4gqrRjOYhvwWZ3R1m9b0x9A35QjBAZbLcVDAoGBAMw3 +8FguXSmIKUtU916S8kCZ8y+w+1Uh4tDYUMprbbvPrsmdiZI1pCRULcH0k7uw+r+n +UviUk0T3CZBTSP5W+0WZ57uSY+aeJzZeuyn1cNguDBiwVL9zmV2JkvFcIfXEWPkL +Ae5OBVfGUtucg6+Xp7UdXrYWg2XkMY79eqhVWNy3AoGBAKCfbjlai5DOwWz5xcdJ +/JJCkVP0XM6aRn7U3y+uEp5uRlExgNARsx62gA+oGMb+2GsNN06hF/7yKVlts/+/ +R/sgmyhSkbBwhfy6tshyJm7WTYRSglfE54cTJL9DZsQg3IxyLnZCpiV8d4bAdIIh +nYqzR5xCsqrRxKszVsCdmA7JAoGAbwaihm5+e8vpF2mUKzict/56thz0J26Kz5wr +IEGToR3iGv6pAnJjUNTrI52Ci/JGANhJRZgRENd1vZ9p+cz0QvzPfayy33hwPSD3 +hHJJ7V3reai0CnogkTfwSYQbenBLJuqTHCoSwYuzFG5dMaOzq8XR7hEDUuvi/ahV +fRsZerUCgYAXEJOAnffMvFJZrfflJ62HbPxQDuKtUqZ2aWFRh6/d6yLC+xCTv+AV +Ygl8U9GHqiafLb0jqzzHU+NzpCFsI9IG/6r5O3k+CoQkw2u1mN6diys8IIip5PRv +DsER2F0w6ZrwEkUpFxxIpQgTCXarTFugMbWkjhjcpgK/lDGWFsWW/w== +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_TIMELORD_PUBLIC_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIURVoAXVmDtr+4fUDOOVA0ZXW+RTEwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyNVoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDZsq3BSwg/a/I3AlY+Uj3d4S+W8jIhWHI8XHX5Ud7k+auJ +gfKvA/D6PrYI/TunHFzTGdVaGOd2y2Ylw7KUFZ9YIjtfQfNI0UnuIEyovcmLBiw2 +tCtQE7ItEgLkrEjgBX8DNWlQxGKZ8+HAeY5Ux3eRsLJJn3fBFAxOtC+cxF5hFFvt +EcGTMo1lYr/A/XKt5Klh8eoOLkE8JNbwuO7AQC5NQN1AP3ZP+3rOC6AU+xdECF33 +9H3gyfXeGk5ixLpV/d+Oz3VzRgmBCBt7+QLT/MSgm+xD3hO21MB2cBBIHJ6bzeTy +4nwvidsnSPPn6RoHg5vTR6HFmLvALirbiA0arr3/AgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQBl4c1dN+9iucNMtM0bLiAq +qeGRLnjGhizrhXuI0PtO5zrgmWgPnuJ/gZUwoPrGczO5g8ZO2zQhDuz8QWXLCNby +UL5C0aQrMafIqawKd3oumnHFOotY5eW4hf1rKyYfCGJ15xisWDIXuVSHzf6YlNlk +avHfchtw3I6xPIt6ui08qnRjuxVeoHbKKgM3DOx3vfvWiOyjXI0pZt2uYdMHdX8s +kOqJVoEF7XvSheDLXjhF7iWeGDmqjuBHv6eRfnA20MOXbE2l26Oa7M9z4xzVZBvU +GxtQwVXfxwmo54EuxVSS23gTD2q85JRRfPWuZnOs+NUr7e8P9MkejuC7iuvA2P7G +-----END CERTIFICATE----- +""" + +SSL_TEST_TIMELORD_PUBLIC_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA2bKtwUsIP2vyNwJWPlI93eEvlvIyIVhyPFx1+VHe5PmriYHy +rwPw+j62CP07pxxc0xnVWhjndstmJcOylBWfWCI7X0HzSNFJ7iBMqL3JiwYsNrQr +UBOyLRIC5KxI4AV/AzVpUMRimfPhwHmOVMd3kbCySZ93wRQMTrQvnMReYRRb7RHB +kzKNZWK/wP1yreSpYfHqDi5BPCTW8LjuwEAuTUDdQD92T/t6zgugFPsXRAhd9/R9 +4Mn13hpOYsS6Vf3fjs91c0YJgQgbe/kC0/zEoJvsQ94TttTAdnAQSByem83k8uJ8 +L4nbJ0jz5+kaB4Ob00ehxZi7wC4q24gNGq69/wIDAQABAoIBABDpO2wvivV6SjeR +u+dddibdTlgYemJyv3UG7bcvb/QznOqyqIqF8NtPsc5i9ZZWsrNHZ3Z3RsvIoye9 +2wp734P2LMyKj/6RG4AfDDVzgMuG8DpTpqWy0f2ET6s54vNcGfDC3mqCxvIUqu3L +w428bQJpSoBDngbmqsoWXzh7XKWHzaeRNzhmEd58yNZH8Fb0OhPWESNA0U6t+oJx +eZ7mtSzIY21mF0Qo2JIRMC2BMwpK2MMHsvTKs4VcARBAA2X7K9ZAxAoASTZU3xCc +nOglUUOZzPa0mGses2E+b1pbI2M2YLhwov6kRm7MihtjXRgMHCVcOJwuNK6QR0HC +K29CP6ECgYEA8mZ1mvY5Xgt85fgQepVib23vfbdXHqRRKkkGoqky+o6Pa55bgSxS +wzI6dj3LJD/Qk/5I7gS0Ft8FCYeFQTDBf2a/wZ9+RH4k3NOR9WYVxEQtAnT9DeSq +DfZQqopSQ+29eosBS8uqfI+8C1C+k3aS4qJqpSpyaj58ZrOPCcZyA48CgYEA5elt +GE0oIzRaPmbH7aRSgSc+D/MtmMmlDQekA1v5vPKPCdrwCvNHh73sGpHGeYDuEZZI +wMMccVJ6rBQQ4Hsk2fmrHj3dbn2HNB++n14zxAX9iv7sVKGvrOvxTnhSidRXuQ92 +Ap3wHFAY2x4oPRRTBT+HJuz46wmqooOFGMd0ppECgYEA21meaNkRdqn8nvoIp3UQ ++3bHNsM24fKdxB8LExz7lcJ1xFQrx8t9JUgJoUAv2KCqtZFxG3pEIUI1g7cP/bsK +DqjDM4qJr59a6j3GIgP8BHwRIt0MtYrL3BCeVIURBolXYlHxnU4y+77x0meB3V16 +c/23dbjgioX6+tDXyme6er8CgYB1puMV+X3drg+0OSJ8QIeb4foHbXja4+1bYpqS +wYFmKHX8JBaMc/wZwZ3N5uU7DjhFtbMbOX0XnI57+nS8eyfbh8ECY8Qpo4EJsmj4 +4tr4p9wcQeGsWWUtxzuf3UDXmVser4PDSREzW+WsU51hzEHDwMOnrsKefD7elREK +Ih9WkQKBgDfRy6VKMVBnyvPW+mby5sS3aAS1Mq6CJXjWTWkLoolkhATd3YrrC2b+ +/IuSbjsgagN3u+Ih1uIKb3FDHjKv7o2o+bRot5415P4z+FQeKFrjU/pPF/0B80ug +PdlK3MF+/2UgKAwawIimV8HSgtSSx6VgDk8q0HUPVJHxdziyo4dR +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_CRAWLER_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUGfJCme460FNV8uIRcAb2+fNOKnowDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyNVoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQC5pSuCco5DLrrC806I0t4XFEp/2HgHVve+xI3mIyl4A+mL +XXasrNMeVEo3MU1ADZBntZJcDl16PvHx01/sY5vo+0BwjzXIZyl96B5yH0ibxWeo +tZbkaU2FOg1/aR1sDL9guxQZ3JfrOA7AhFX+W5o36X9ySUsVcj9IFSuBd4WreAz+ +lo/DCcsdNq6PvfpwR3Lz9VURBsYmRseO7cHdcASzr/UbfaIYFaJxYQzUkC127qXt +6KEqaUKPwt6/pNSCx6PChTS9isqz7oTg8CH9Ku8M9lQz+w9J9T1/AmuFnKUhLAg6 +MVvl7bGDG657ndawiUYLsFnAtCQBxjnhdhngv0LPAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQCr3TlMTjaxIWlpeaHo0rcR +CmZNB01iJQKHhu6HEKJunILBf1eH8Gm/CLftytMtbxPfEUB/OG04CvDfCYAk+yVK +lVhlLXKU1MHNbjCEYYhL9Nai862dxvAMLI4PMbrhIl9qPYfQ2tZ/yRKKZIy0Jnsx +EQECv1dFXTDPFZOd8/ViNFmHNLR9cHAnEuTFKnM2NoTOYlRF+f01yTF3AAAgdNw5 +ViGPO3v4Xv5XeLVmpxOqG4ZXdCE4ZfSkqmR3MuDpe5GsYYOeEClLSZJxX4kNwd7e +EK8qZ/rAt2Yk+/O/FKGa5C59qj3dKiVVTN5UPfFD+cPYFFjCNjy+GP+Mw8sKit+r +-----END CERTIFICATE----- +""" + +SSL_TEST_CRAWLER_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAuaUrgnKOQy66wvNOiNLeFxRKf9h4B1b3vsSN5iMpeAPpi112 +rKzTHlRKNzFNQA2QZ7WSXA5dej7x8dNf7GOb6PtAcI81yGcpfegech9Im8VnqLWW +5GlNhToNf2kdbAy/YLsUGdyX6zgOwIRV/luaN+l/cklLFXI/SBUrgXeFq3gM/paP +wwnLHTauj736cEdy8/VVEQbGJkbHju3B3XAEs6/1G32iGBWicWEM1JAtdu6l7eih +KmlCj8Lev6TUgsejwoU0vYrKs+6E4PAh/SrvDPZUM/sPSfU9fwJrhZylISwIOjFb +5e2xgxuue53WsIlGC7BZwLQkAcY54XYZ4L9CzwIDAQABAoIBAQCbQCT1z4Vna7Hm +DQF1bRssI9z1s3sVcEZ5c/jTKD6qzmLGGOCBIXrg107Ff2aCFZXZFUCT2bOU4wUE +3mdO0jJ1kYDfYPRyZsuNLswfVkgrdNfugAXzeJjKvLTDA44GaVa2t1zlD9TAcj3s +A//CWqrK7WuWkPLIuaVwS7v5ZpITxT/fV3rEm/aNI0bKIQvg/QpXqX19q91WfFct +1zhzim0JRU6FmBqhpYG69Z2da9ZoNvj6ZENC0nNhaI243ubkvzGSz/vICREnGvlu +mTq70i77G6UK7Li2+7jTKtxSWhcwpUFjdnzxwNd7aDlvoTS1F0gbXQ4QlaRm9cv2 +0viZO7QZAoGBAPRepRdW6DRnYVbtd6788i/qKiTK+odtbQj9C8N5jBbLD/NWvTXL +syxSbxJUpIDRUgZOqLepNoetZZwDJOWnavMaLKsnxYGMUstH+cMrAeYbhyxlf+ol +oK00XzqQjW0kUL20usHQLr5GcWuAonsYIfLSucEZDI/bF+413hIlqDQzAoGBAMJ7 +CVo12fy36HcpKtWR55r9G7SYh71moSvt/LNlDwQaCrvt3ySKOid1y4Ufob5eQvDO +UBk6LFpuGH1pIBKQKz8okw++W2w7jqmcY0RdUTS2jKk9fwwIR4EtSpv8cTjE3S/1 +g9P9SjVVkAL/o/VVSEWTYLj/ho9Xhh3WDsI9h3r1AoGAMwcUaDRAlrjDrbg2lrbB +B9pY5IfyGpdx/j+A1leqNhQ/B2wkZHhduLKZ+PTtyOxsuV5xgrB174z4u8Q4TzBP +d+YOT8slRfD8VPB5qhRv+BHlfxLOzCEBVUmrXPpUXecIaSS1HsWPDTJ+eplI1HVs +mV0BZt4JLnzsmVRsQ9PTNNsCgYA8QgcJznm2Vf1PPpApEEYkvZvh/wi/5Ja3l8ue +ggd/C9qbk/55weJ264advslMxMQU/LfQuTeY5VftM69eURE1Rosaa67EAEgZwXz4 +Z7mLjaxTm9xLjB0rpy7g2fzyy/yEqZupCWf+0n4Gj9LrZvs3o4xqhbHZpBLIF9UG +1i2uKQKBgHEEncIbSz/BYXH3u2jh6CacXaoe5eh4S6ihEup1EZ/ajavYLtD/cfT4 +9QuJjBSD9me7I4Rdas5OtvRVcUbo8BLISU3Y9bMkTW8vf1wof7oIAlB6Q9jZFaXA +p05qDfXHJ4/DcMWQTkfC0PNVslc5lPHJ7jylPNhMq6oR9Wn3OH8/ +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_DAEMON_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUEfp1B/UPB0Hf5bcKFsJZ/Jg5ZUMwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyNVoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDAIc6vaGVenMWjN8uyIetlwtTXCVRK0aTwLj6HHXpcJCSF +bIhvsZRlZ7Fvu4VelVNzXH/kmZhpUXY59QQObspZ2z7TfVLgS0r4mAU8WqmCGi+F +7cLVX7Du2E06jGhsBgKd9l6Ybud0SlW2/y8IzdIq75bW9qD7viAkbXg6sSR1zqwg +2lFpoSKaf9TFvdbgtiCIeCbTQEwTZktYEEd3S+yY/HierG9RTcpXSJvC1Orcv/J8 +iH89U+C8QfNjULCChEOWhaw+qol2fKOK3394JsujORUn/9eHeJt1otc3JWnyR0DG +ok4y62xxl/y529npRJEkwXO7zoZXQVm+PoWZvYHDAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQCEugh4aiI6L9PbBZDEnc7o +tUU2boCKwOsI6igtPaBhZZcQhyTi/qlmPMKZm58TFS8s//oiKhD3q31+OhQIe/Ak +pvuwJwcJLe0r4iUzbtGJsn3aIpXP+GHqEDM+38tZhXMpxq5Kv8Zp8Nm7R7XidGHg +OOBXQ64UDTUAHYvs+n4ltYLgTsqCPPWsHxieABjC3YDBJAdGa/H4cRB/qU7vKNkN +uAyfhVLBzaAcyYtQBef9cuulTUaE+KA8mN1kJNbA/b3E2MCGYCqf4eNN6gRdmtxG +16aP/7qxvVlgWh1mYfUvIlszazjI8TI2N1y1JZOWpmO4RoyWhvqhWdPXKC/HyVub +-----END CERTIFICATE----- +""" + +SSL_TEST_DAEMON_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAwCHOr2hlXpzFozfLsiHrZcLU1wlUStGk8C4+hx16XCQkhWyI +b7GUZWexb7uFXpVTc1x/5JmYaVF2OfUEDm7KWds+031S4EtK+JgFPFqpghovhe3C +1V+w7thNOoxobAYCnfZemG7ndEpVtv8vCM3SKu+W1vag+74gJG14OrEkdc6sINpR +aaEimn/Uxb3W4LYgiHgm00BME2ZLWBBHd0vsmPx4nqxvUU3KV0ibwtTq3L/yfIh/ +PVPgvEHzY1CwgoRDloWsPqqJdnyjit9/eCbLozkVJ//Xh3ibdaLXNyVp8kdAxqJO +MutscZf8udvZ6USRJMFzu86GV0FZvj6Fmb2BwwIDAQABAoIBAQC9oCBHyvdRe9Us +FCN8ejHES5iZa2HAPk1Vp66a2CMt0ZYiAU5fPprBwqfDKQampSap0v9+9YERYQ8Y +gJQyUnJwYQ0O2r/zExy5YgC44pouB/4jZthGk50i/mSqhm2BQCVRFhmixMK3aa5T +YGRhghINwk3Td7LHA4zhpxFki/T6Nvp5R49zhWST2a00JIfKEUF7X/3ILVA1ShwT +Gl3lWZzSL3qWkluNMtHJVuA+9HRDsjbWID63lF4K5HHl0c+0yW2OTOgMH7zuxmLT +IhEdc5Vr5wCvgFOr1MwsXpO/OIT0KrorobSSn+x/HWMbvUoTVhfjiE7qxRsfqh5w +ZPru0OBBAoGBAPU+4ZSAJKKtBEjs5ej7LJbmGedQCBYka/u+FUWw4fp052bkT72M +4Y6cfQlxXs59i0E/JPPqytuXx9bRCNG7929yvCzcrZBNij5NLR/EYrRqtnDf7O49 +kCu8owgv6/zglt47kjyDuNv0JhX4D9iIcz/4TuFUG5zLn1SwWqUCOv9RAoGBAMiO +rNT2o4gJSg2wKn9oWuR6ysBQuq7Lj9kVZkilHVp2SwTtnLc3CjfFbmPivAhodRN8 +y/zlln539Bi3JyolYg0YDeU0oeI79HyeKUgflxY3rXB2tXhnK5W4l7Od/BZFc2Cw +hSYMzlcGte13DD/gr7XWohqEr5aQCXgM/3tZ+3LTAoGBAMmb6+4cegGRolghB9BD +zCAxAVJ7JGqvfmXxmaM1ClDPEfwv7K2yxypp0xCUNpAh/PyiYEp01lc3q30ZUtq4 +X20rMS7gK37Zf7A/2bynwUz3/QtFyoz/5ylNZekxHBtCtkPzTQCaeLm5OCYPS1eC +tNv90TrD3f9EFbOVVq8X6lBBAoGAGvwngRgWdM1bK3BSp4XxBOEIusuh8rbtCfZ5 +Jrkgs/VKrsUR2w0K0Oo9qi7twevcJN0bzVFO6IFXVKQAHwmcocpkxDxKs9gBU2ss +fsnRWGnxajpuvF6VXLXTo5VUP+LkXVQi9jWu5cK/Y84q1cVznvHcKdlEjuueeoq1 +LXG0BYUCgYBK4SYd4U/DGiSE45D/ikQa766J59cd4hOHAKTdoTh7ZWmn9Oh5ms8R +ggcxJQt8ZgGnOJFqjF2xN3tp8oluk8gTx932Myh90iYUPbxXGjB5mYJ0g0fDXK1X +i9wjKsZocYKO4ZzhtdXzbBqQcYMhoBicoEWY8NpzyA+0gVIx+bxKuA== +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_INTRODUCER_PUBLIC_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUQGJa9B0MRFmHuvjXjyCBbXypCBEwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyNVoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDGNB8T3gN1kLSJ4QhzMcoS7UPLvoWxvxVqYUSEap+4EI// +LDKVlFKOl5icf808nVxGwb2XD7Io5wkX1kppBiqudri+WwHn9ayyhdpSi6eUsi3G +suPObKUTzNH9LOli6DA++GBaYoQUVXoOKWz3DVk8QMSCXgej5+09owdwIMHl8ytV +G36/LpPjH8Chr4y2MAUtz2Dc/Evyq8XfBIeD835Azhr2RXs3c/AOXVWXPt9A2BW8 +sA7WvztNhYnqtL4EXpgG6H018aZTnCMEHEbil05fkfs+zRxgncqQ5BDSqARbkSDu +SeczaJtmW7E4yqRZTFZF2AnssyDuPQcl+qiYNNU1AgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQCFCSfOPoAbrIMBPLkiHGOv +a3OJmi/Kw9qfOp6iCVb4A0CE9ShzBZJqvSfZ/CnPeLsXy9zaRytrVHgkOXQyzbbv +uKiaBgwnLT1VFQlPorGBVJZisLl6uc+PIPK+9syqnOvaeilbI/UH1WrcESwrV2C1 +7CcV8ibj7a9WnWC0LvzNMDHFKqPyOUTmBZWHvS21fYHUXV/5abX19TmwOzJTOWuc +cms/M6RfeldLIgGhxGnqn1un2KLXJ9wGHqcTVkbp3Fvk3Sk/T2zxgmJCLLLzI7Ap +cvVw1It2vL4bmGfF/1CTjfawhEbmtvTl1NkY/e0c2AORZ1eCvz/ZQYuwZxCseP0u +-----END CERTIFICATE----- +""" + +SSL_TEST_INTRODUCER_PUBLIC_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAxjQfE94DdZC0ieEIczHKEu1Dy76Fsb8VamFEhGqfuBCP/ywy +lZRSjpeYnH/NPJ1cRsG9lw+yKOcJF9ZKaQYqrna4vlsB5/WssoXaUounlLItxrLj +zmylE8zR/SzpYugwPvhgWmKEFFV6Dils9w1ZPEDEgl4Ho+ftPaMHcCDB5fMrVRt+ +vy6T4x/Aoa+MtjAFLc9g3PxL8qvF3wSHg/N+QM4a9kV7N3PwDl1Vlz7fQNgVvLAO +1r87TYWJ6rS+BF6YBuh9NfGmU5wjBBxG4pdOX5H7Ps0cYJ3KkOQQ0qgEW5Eg7knn +M2ibZluxOMqkWUxWRdgJ7LMg7j0HJfqomDTVNQIDAQABAoIBAFKU4OX8OODBHBfe +pRCqDBH6vaakiTvX6+pZAJ1Td5zPec/N8H2WQRecXj/GmBLLVek9S+sm5QpZyNYf +uP0tTpdGbA8UCCVHnV78mkyOV5KC8sO5QWV+qwEm889S6SMGryNthWfjaDi4rJQ9 ++mKtMyMBsV7IItLODXEC/lRfsapG/PVnO8J1dCx+47BJNw4lA1XZQrwwg1TrrKTo ++t9N9R9GmUe+zqXKs0d4al/02JDDrPdReuKMGOAMnWvc14Ppyb8eYfC8lk6nluHt +gUccQlkXH963kPxHhmysC1Y470DcqUooiJADOh5GfD+W860GDeWqib5I5IOJ4HmK +xgTq64ECgYEA8egiv9nFuJNV+yN2FgkrKo2kqdsyMnlOsC1ppdfsUk/Clx6LB2WA +K+82boWmnzIkqhI96wlZ1Y8LXFS5fqMQeeO65VNwQyhk0KQnlJ/dTYT3FzSuZ61d +8CcD3p/nMrcXT3H9HOfYJooF1eATtcbCP+Sc3SS9JczYDcut59aA88UCgYEA0cAv +NHIVOUVL9EOyIQLSBsGTF9u23bPzCUaGUHc82acruRwJEcSzpfNaj8NbOzk0tR7r +ZawkbrlhxRqVC28rE4jvhPSAO9swq3Gr4IMjhGnJ2gc81zPV1NyRY0jR586z4xWk +kra01MYhEcJ5gya6D6t1y11glq4ht0zhrFcdwrECgYAn8/cJSKZnPa5NtCWkrg77 +EDnJ8/HudCqS3m08ftUBIzs4SkscBZ+NogyTZG+Ii3eSv0CKuRilNOLjdPrN95CZ +EQulJIq+DMXZz8LZwS2DyBonMwQ7C18gctEoy7AbqDGpZWIwi/ofI1yjXkbjFtiu +RMvDmnXC8HoejS1DxSG3IQKBgQDM+b/nw7kD77lrKqCv696tpYwGm7uX6xwNq3Lk +vbGkjd6HlmMyjwR0n12X8nR8asocWev2vwQXhGiMQw72TpxNCdvwFTQfynNEh+BM +ljsmUm9k9v+42roTu70ExowCuZhHycW7bntHF5wHjAJNbZIUcB28MDOM7Pyb8bD0 +R2oY8QKBgAJUUkmpgpNz4oNNv+mztAElg5LKEX6fNRqrKHF6H/rHmuI5zTrTsjPc +UwBCIG6KBlcmpAsTUpxJ03VrazZMcuyaw+sXXpT8PAcacSTHf5CYY/mEEA4lTcZt +iPfa9TABxtW6v3r2l5tO9Yz+Yb0v3nfGtaxDgp3U4+0/QzUwX2Jd +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_PRIVATE_CA_CERT_AND_KEY_4: Tuple[bytes, bytes] = (SSL_TEST_PRIVATE_CA_CRT, SSL_TEST_PRIVATE_CA_KEY) + +SSL_TEST_NODE_CERTS_AND_KEYS_4: Dict[str, Dict[str, Dict[str, bytes]]] = { + "full_node": { + "private": {"crt": SSL_TEST_FULLNODE_PRIVATE_CRT, "key": SSL_TEST_FULLNODE_PRIVATE_KEY}, + "public": {"crt": SSL_TEST_FULLNODE_PUBLIC_CRT, "key": SSL_TEST_FULLNODE_PUBLIC_KEY}, + }, + "wallet": { + "private": {"crt": SSL_TEST_WALLET_PRIVATE_CRT, "key": SSL_TEST_WALLET_PRIVATE_KEY}, + "public": {"crt": SSL_TEST_WALLET_PUBLIC_CRT, "key": SSL_TEST_WALLET_PUBLIC_KEY}, + }, + "farmer": { + "private": {"crt": SSL_TEST_FARMER_PRIVATE_CRT, "key": SSL_TEST_FARMER_PRIVATE_KEY}, + "public": {"crt": SSL_TEST_FARMER_PUBLIC_CRT, "key": SSL_TEST_FARMER_PUBLIC_KEY}, + }, + "harvester": { + "private": {"crt": SSL_TEST_HARVESTER_PRIVATE_CRT, "key": SSL_TEST_HARVESTER_PRIVATE_KEY}, + }, + "timelord": { + "private": {"crt": SSL_TEST_TIMELORD_PRIVATE_CRT, "key": SSL_TEST_TIMELORD_PRIVATE_KEY}, + "public": {"crt": SSL_TEST_TIMELORD_PUBLIC_CRT, "key": SSL_TEST_TIMELORD_PUBLIC_KEY}, + }, + "crawler": { + "private": {"crt": SSL_TEST_CRAWLER_PRIVATE_CRT, "key": SSL_TEST_CRAWLER_PRIVATE_KEY}, + }, + "daemon": { + "private": {"crt": SSL_TEST_DAEMON_PRIVATE_CRT, "key": SSL_TEST_DAEMON_PRIVATE_KEY}, + }, + "introducer": { + "public": {"crt": SSL_TEST_INTRODUCER_PUBLIC_CRT, "key": SSL_TEST_INTRODUCER_PUBLIC_KEY}, + }, +} diff --git a/tests/util/ssl_certs_5.py b/tests/util/ssl_certs_5.py new file mode 100644 index 000000000000..a0ddac66cef1 --- /dev/null +++ b/tests/util/ssl_certs_5.py @@ -0,0 +1,684 @@ +from typing import Dict, Tuple + +SSL_TEST_PRIVATE_CA_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDKTCCAhGgAwIBAgIUXU/nGxb+rZck2qIMztmDWKDZCBcwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMB4XDTIyMDMyMzE3MjkyNloXDTMyMDMy +MDE3MjkyNlowRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8G +A1UECwwYT3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAm8r7ngBPSkz0U2XxFwI0gT3xt/yqKTI0AZiSicyyMNo0 +oSHZHRVzIfzu/c+cI2SPFHA0n9ZaswiztWje38uzRjEqD30EmF1By54A6c5pDJgV +MVd6LXafbv7tWxSLdyLPJkoa8gcqAtR1tOFXRHRtKNa6g2thyU87/V/UXJ9+C4eQ +mmpq3goVzkA7ZRx0FbdXwijAGLcL5ZWStUPTaWjR+V3ApxUZYy8JV3tWybEm5FDK +JJOvdd0bJQgT5WTCYRKNYsXyjcRP2ypi/Ry2M1oQLBbqCIldrvvIyoUodbkV3Yc7 +AFhg4gUKc/O6zcIO/3PXKgFOAMLangjIBwWc9yyNXwIDAQABoxMwETAPBgNVHRMB +Af8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCILSP1KclF/iLlNb7w2bE1hZ5/ +IJcWsZJSec7vlZkF3AGxrUc2XzdT53gooZqpg5YIdMYqDZqCfPphvUbqGELbImfH +D7sWhD8jU0FsKc5ho6+Uwmj2I5H+xnSVSF8qEbSBk8nasAac+bXQ6cakqkG/cbO0 +9HBBHTd6V25KCeyvYN0kyuYMyT7GBfzOBmhyx5zf2L3oqoqVKAokbmC/9cvBXMUX ++1BWyowMjBVH5C5frOymcTF7b3ZlMuibFdl01lVa76QjVno/QMZ2bqnLaqDJA306 +f7vTyuGSYJSoXnEh0UJ4IR2ct0F+6JvuTCL4p/b97C4Au+Lq9jt+2sGV9CAs +-----END CERTIFICATE----- +""" + +SSL_TEST_PRIVATE_CA_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAm8r7ngBPSkz0U2XxFwI0gT3xt/yqKTI0AZiSicyyMNo0oSHZ +HRVzIfzu/c+cI2SPFHA0n9ZaswiztWje38uzRjEqD30EmF1By54A6c5pDJgVMVd6 +LXafbv7tWxSLdyLPJkoa8gcqAtR1tOFXRHRtKNa6g2thyU87/V/UXJ9+C4eQmmpq +3goVzkA7ZRx0FbdXwijAGLcL5ZWStUPTaWjR+V3ApxUZYy8JV3tWybEm5FDKJJOv +dd0bJQgT5WTCYRKNYsXyjcRP2ypi/Ry2M1oQLBbqCIldrvvIyoUodbkV3Yc7AFhg +4gUKc/O6zcIO/3PXKgFOAMLangjIBwWc9yyNXwIDAQABAoIBAHLTHbbrhYU+yMl7 +FkGeF3K2ZCT2LbhlTx1qBX9ZBnCpMycb2njsKUqAsOkTDoKriCVJOhAgngLcxA9N +9w69hSmT7OszeqKOAYOAti2dO6HTqbMPRXaiuonFjM2Xi99IIaOX9Noz24vwabzi +ZT6IDTiPYzKff5gvNQjfi5ak2vLFVowAQH1Mf3dh86jvhD9PpLvYiUL2tb3w514c +LhUQtrk9YMhOMmCMKH8HxvU/8IsGhLNOwuOiQ2O6tXLfld+udt93n0JyshzRNcyl +I231KlThxK3BiXku2plE/qw7K3sW01amaVa7trfWxrOv7XQF8u4OOcLO7Wx1O1LY +gfbrAhkCgYEAzmZsCeTMTGJxNbkj/Zy5rU/+wyn5sFl6xfzXj64X0Ri6C8AKNBI7 +o1T4zSp8aUGHAg1SAW5OGWC5vVgACrIfIWoFpg5Rx9l1EkyZVczX0tbjLabQeA7j +4rHGrKLK38HFtf7lyHXoGVj55nvlxri9t9X02clBqz2WzJREiaYbFzUCgYEAwTs+ +th+yhJIwLIqqSeK6tPgK9Ofp4M3Qf8LD7yOZD8qmVdRlz8H2aJhbchyLGnaj0JJS +lH93sU81wZ4Lo3Uowb1uq+qdz/nuf2My6zFwG28+ww6KHnkss7XGvGQjLLBtLiol +j83Pi5lkKcsypf0psnKV8cKYpXooIod2MyU+YMMCgYBxWh6LcHQinw29i2gQqDnw +zLYFSNAv4XRjt3BLIDlERGgoe9cescS+9rONOYAJ7krO/bHDx2hs14oqSmH7fcdK ++ocPo12We/6nhhnP3SfKSumI8Mwco1DT9v49YUo5iJmkUdCwPtCw2wSjZ/fRIzRN ++dr2oGjIOpLO176sOeU24QKBgE2k3rgT2InIrC7ZsT9rKZbaLJzoK2Q3j1YnDtAi +v7hGt7u5Uwe+aqLwxZ3+ti52CbEfeqtM5O2MZI9eUFLoGu5uje/qoGsXhKwPUkCL +Zv6/HrsGNp20FzBHFIpSuoeUhOqN6PX1vzXa9xKMIdfs+DpKLNIuXWPwx/vH7sjy +aDQ9AoGAUad/5kDcK/FwSklNenqITWn9c/OFecb6Z6u+F7TPirleXPbhNQ0oXUW8 +pVVnLpzWWWpu9amxlS/4a4t7/lTEyYaT6rsjJr5ZBvDrIBwFc3zvXWYxbY9hnCRP +LQfRJRhurOJYEByEDjyiXRA9WPHcIhpKVGN8DGyjx/gvJjfTQR8= +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_FULLNODE_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUPIoys/kxRUAxhIW+huwsgqplVZ0wDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyNloYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQCpDfVovOj9fLjpheYdKVwV5V3hN/bUxqlYAW3s+zFU9Bpg ++SnI+5XuTW6SLpiPjx/5kZJsztqxI/Nr7BuTpHUOfbaCHkoJGBAcAkPdnOma8lH7 +bpZ8CpVjONeHqmTvsDP2dgCg8QW6nqVksEHMtkOFadTifIODxdWJtsB4KjzKlV1U +aiF0hUIJmbvX08bArrzrsX5EgM3pQV6vgo1wYWM/X9zRjAd0xJDbhVqOsQpK4AJS +zAAfqCwxNf04EHhRFD35Uam4NiBOzR3T8rB4XGcpMBYLPC6reLHtOmxneJ081NWh +ZgVDhxPg8/30Bs3OhNhT9ZlfBJZvPW8ReT/JzHXtAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQA4hi9UEfJtPMsTjpI08Pdp +AMNa1ybci7kDVaMfcvKvMDcOtoCEt5K1t3fGWrYojfgnJnSRJLTSZIa7IdBbyZG7 +e5ClNLzw6bCqiQ55mgyAFMFM0VUaYu39zRK5X6fA2qWXFYVbOGAbEgU8sFuOmBid +MjkEQKL561tiibAkVJucp0hLf1xzoH4dJZqFWyFiThH8RTUq4Gd4atH0pzk01Ts3 +VbQyinIqEU/gwLAawnOGtdMYdFPNtll0F+lP1+h5AZYtsTcfilm18D9Th4/rfQay +Ob1SSSdL7MXqy0pR9sF/BiXTeXauOK6Y5DooJ32y68yrNKL+TfbEzcPSfqiaK3Tm +-----END CERTIFICATE----- +""" + +SSL_TEST_FULLNODE_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAqQ31aLzo/Xy46YXmHSlcFeVd4Tf21MapWAFt7PsxVPQaYPkp +yPuV7k1uki6Yj48f+ZGSbM7asSPza+wbk6R1Dn22gh5KCRgQHAJD3ZzpmvJR+26W +fAqVYzjXh6pk77Az9nYAoPEFup6lZLBBzLZDhWnU4nyDg8XVibbAeCo8ypVdVGoh +dIVCCZm719PGwK6867F+RIDN6UFer4KNcGFjP1/c0YwHdMSQ24VajrEKSuACUswA +H6gsMTX9OBB4URQ9+VGpuDYgTs0d0/KweFxnKTAWCzwuq3ix7TpsZ3idPNTVoWYF +Q4cT4PP99AbNzoTYU/WZXwSWbz1vEXk/ycx17QIDAQABAoIBACP1aCHjLNveT6a8 +aHoDdibiJtnlAYe4ygSCKVOjCpc7ZPEDjrPFb9rEdaR6bND8bJy1LiQey72qG/j0 +u9jnvk5axxtePfk5ORP8F1toKPhgWrfUigXQan40dQPSZq3lGOhvqSqSmdlcLWoB +Y72bdzlFjZavTXoV9pnYWZA1y8B7NFnaGfaBHfc1iNJ89cwLV2QhADaYb7tKIooZ +nlPyI4n/uF08+UmKzKEpwtDsxJxssa/J71YrQS274r13yEyYKGhsoVEhWrfD+d4t +BlBWHwFfXQLPVgRQ3du3pvMqAMAMNZEb0k20WeHcaAybRGaEIe6ltqOGH2m6j5uT +k7fYr7ECgYEA1fExK3+VrKKCVSkpRpky0bbLgwS+YJU89kPBurdpVBMxYh9XuGNx +xPYL6trKE5rfSXFP+sz8Vqi7WOEXCFzFx6ZHiBokaVdSWG1Avk16lB30Uu5y/r2W +BmfezbribDM9oQLYLlUd7tAxGU+/SIciREHpfelq+ejLmeWKHdWdVpMCgYEAyknC +2B8w4PwSoDMBB0xD9FM6q/7iaXChpa4n8yl/YLVEZlSrCyWFz0awPHQTOvszE0xf +UJ16HYDktMT3tyC9cDd5UHN89zi75edEMPLmPnLD6K7QOVaVR3X6PUqA0SM1spM3 +UTXWFxFhYTUTtudK6rMSseDCxGpY+r4hPmR+UX8CgYEAlNevyL6DyE5rdIolgEt3 +MrYFEosLVDCf8Akl0BxoeCi+M7Dwm4T8EvbHRcafzlHyRKtD5I4WhMfxR52aI6Q/ +qW4C2Cqv6GXrEUA5SeynekL4x3XDpX0K0jwTo3gArRxdJRbQhjOLlqlbb2uu/eue +KHTe2E27slCGzfQHSkhipWcCgYEAiUS0a2P/Dyz+lqcFs6YVFt7DmaNEkLhVeNBN +W7x1K3LWD3q09sNnodgeD2fVBNkhN59Drrit/QdSKzjdv+7/nf6G3AkCa+Cb4M6m +f3DUvNu0BVlbAw22DuAIBz9fWovCDIPJrdoShWTN5+DUl/Er7UfHD92tTQu9hakv +dd9LuJECgYEA1L3P0JKDoqkgSBEwsHbyScv2qo23xKZsTqdY09Cz0iG8wMkajRdb +QkWj15kmqYjhl/gpgmWfnB4xRmzIhKiI+k2AvVmiS9ttYgdqPwYJdauq4ON/H5hA +dXPyLob4mYRF76KcYeJeVyjRPXwtbojJQ/FFGZSKsuO2toathK+vBM4= +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_FULLNODE_PUBLIC_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUJ/ehUeJz6rFUOj2Sq6rL4SW0Wf0wDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyNloYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQCyjDKG7d7fU8ZdpnD0Ba5S3/F6FXpTvUBqxSRWOO9yI4tQ +hSqOJIP9x8u0aFhgmgMN8Rig3JZ6KfvCeQ1qO1xkcuZ1o3FoOOYkhZoj5OKjFQDk +6g+cOSrvUe4QtIaPxlfNhtV0peVQuKQGSOakEyC7Z5amZL7Ypwjbt6Wr/N5gZ2Be +2+UnoCOxYTVvUmOWl11Bre9eQZXYIM1D2oYht62HvGEMhoZD0NSoRsEiHPiJ5Hhi +ci+U/oO2XP9NrKK3LKr9A8NzWB6oHXqw7qidCQqGumY0zhE4MarACVzqJG6kCB4/ +cL1I3eSjDT6iEUWwn1OLFc/sdTaB50LNfEI0DeL/AgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQCfYRU1NbezmtNTU1hvOHLn +Dcw1qrUujyl6XSE4kNPooKreJLQwtlV5EV1h9APIk11jrieiEkxo9IYVRzadyrrf +3Dh4x2KFn+R/m+ybHWTICcBL2FvvHuvVx/ilFraM3e+Kv/s+pQRs3YvQuCYduBTq +SXz12aZO5ttTmG7LK2WcX5OgwC1kmSw9Km2DFb8zu/cNv/VQkRujsG0doVVrqxHa +4/CkSlTBNCBJO2Brhtbpx4F6kKfK6u26i9pW6HHgvctpC5PORTeGofPVM26Hpsap +6mHfYpFu1jIr1MhQcWZss369DEwsSy2nr5/3R4zIDWMlBXEKd8p3ey/wjUk6dD+p +-----END CERTIFICATE----- +""" + +SSL_TEST_FULLNODE_PUBLIC_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAsowyhu3e31PGXaZw9AWuUt/xehV6U71AasUkVjjvciOLUIUq +jiSD/cfLtGhYYJoDDfEYoNyWein7wnkNajtcZHLmdaNxaDjmJIWaI+TioxUA5OoP +nDkq71HuELSGj8ZXzYbVdKXlULikBkjmpBMgu2eWpmS+2KcI27elq/zeYGdgXtvl +J6AjsWE1b1JjlpddQa3vXkGV2CDNQ9qGIbeth7xhDIaGQ9DUqEbBIhz4ieR4YnIv +lP6Dtlz/Tayityyq/QPDc1geqB16sO6onQkKhrpmNM4RODGqwAlc6iRupAgeP3C9 +SN3kow0+ohFFsJ9TixXP7HU2gedCzXxCNA3i/wIDAQABAoIBAAxh3qsu2fMA+1PJ +VDIIJtPGhheiX65pBIujCmcUYb63qlS2N4JOE/1Imt7zEZX5eFbCLqQRSDpGqRgO +jxib70dkFIl6AUZqE8PapBrzJ6iJr1swxE5gRJL+SpVL/7z+014EiM8jJikX9QAe +lCgyz2VPxMnbWMTrqJicPtgTnFRxGR9KQncVKOyrfWNm6e8r10iMfhvx1HIUJGEI +V0AGsTpfjRc1ATDXvuKJ9iGQuAvhCOrfn75Q7kK1yxKgYoC8DXieIRRirIhoOSdG +nv1FWD7r9D2z7DWq7AVQgPLAuh3ulyE3y+gvDMZcEpYlHtr5h9qbvhSkB3UCfJav +KmU8AukCgYEA6bpcB7FZq9jRhCVcDOK2MiBUfs83IQ8z3/jiYTsrXfqrXDZcvrCG +JzMXbqB/BaE6bA6NQMLgwIiXkTLj4pyrtARlaU5XBUb3KQ2y+xkWvA2n1IVSJopE +dGClmIvDT3usUItcDzSr3uazfQSmjVHkpLW5gTtUK8dwJ34EaXZcnrUCgYEAw4/A +CKT9hqtO+JpvQDlT4NCXTfM/YVuQeIUc9xZQwp7guMEXkGKEbTsCVjGzCVleBR+J +ibg5jgcSjVd6OaXECXbsUufnpQGULbrnD89KmJUch5DyQwVklX5IbzCa3OGvuamN +Uo0ELPi9gxTJkpH2vFgAoHSEOdB942z/IuY5V2MCgYEAhXh/r4DulTz2wIDZJR6e +LtfZiKTqdX2KAR/Onvm8FSndi4YbxmVl5qK9gdYzU1Kz2xsgPNhMooYeD7PBARq4 +zs8n3k/3T7Mr14zUJaI5ImCl863CsPGKj+7VAdzmRtB4IXLDuoc4ksypuP3b4p3e +dNS1v3/S3EFC4bqL6HHICHUCgYB7w4oA7ooUpG4CH5qwxpcy/FAFYSCHeO9hlrzS +EylhQjNuOaW0FuVAS8wayLFKBWjfTSo6IoEqRYeUM/yCZ0o9wymk/mc3olwo5NQ+ +yS2oixXXJgBsMgmKIrWsyNH5YEtZ8NgjmmM+It2tC4bWX9ILOJaM9bCI9k31lJGT +gKhhiQKBgQCJOyZqyxQXD00jxxAzF8b8EPnculpWG0jvT40jYxoRzrx/+oI1KO2Q +6QFtyBjBibf+wDuXfNDjsahoX8EB3yQX+ofG4fCJ9WG3aNcgR8enPfejKoCzwRNf +o/6JFcAVoVo7l+lwcFJ4r8d1KXbP9Qt7doZSgmGrUSTvBTEWz+j3Ig== +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_WALLET_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUfnFEiDBphxztIz9efqOLOpYjA6YwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyNloYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDkLNTI9jghjYaXRMvt/3oFVDZ8FUraVUU718V8vW5NF4Or +I7HYi7EgS4BnRh9CfU8r+mkPxPTYJ7kgjKOcdNGw1ARCiIIl95gw6ymjcXZ7isLC +F+79kJNQSKq6lQvrPoKoQpfy8dgjRaGY7gH3DeX/Oi/QFd3N0C5ZGpyzE7+VmONg +gzoGk5MyLbBCcKcOsBQUqOR7p2bEYXDDP/xl7+u3R2rhWVsp/E9W0eJJmtiVwYrJ +y6sbzcWBxjCM3Rv4NZs9tTH19EHWDJM4whHgnEiB7eXz03+0tELhnwkrrm+a0qC2 +uVqzG9HZ2/hVY0VmPyPVprAihsAPLyJlnpArCs4ZAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQBUBe8jhAQgvnojPHIVbZTy +HyzRwfN8bx6sRGMzRWR1nFuSgeYz9ngzanf6adVCh7K7O8O1dZwdaPPZB9RKPKUH +0oPYPwhSyvmT6so2Xr/YB5Yx/KbrSK7dMlbxQ+9ct9saKkaioVfo7OvgOY3fFg9k +Qs0RwtRpE1TjaSJw7ScwlaUR7GYUrcIBuCKROcZJPTQaVSM537SOFQXqUn0M7Wfu +QM4545j1aULGnDzbP72fpk/icndS8ArmvAW3JpIe+HFk9IxBuzUh4HKHzO3Dny0l +rKOiVmeztN+a8mipfJRTveuZs/QykCugYkafg+nB9GOTjyBwZzTD6IfW1LO3UZLt +-----END CERTIFICATE----- +""" + +SSL_TEST_WALLET_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA5CzUyPY4IY2Gl0TL7f96BVQ2fBVK2lVFO9fFfL1uTReDqyOx +2IuxIEuAZ0YfQn1PK/ppD8T02Ce5IIyjnHTRsNQEQoiCJfeYMOspo3F2e4rCwhfu +/ZCTUEiqupUL6z6CqEKX8vHYI0WhmO4B9w3l/zov0BXdzdAuWRqcsxO/lZjjYIM6 +BpOTMi2wQnCnDrAUFKjke6dmxGFwwz/8Ze/rt0dq4VlbKfxPVtHiSZrYlcGKycur +G83FgcYwjN0b+DWbPbUx9fRB1gyTOMIR4JxIge3l89N/tLRC4Z8JK65vmtKgtrla +sxvR2dv4VWNFZj8j1aawIobADy8iZZ6QKwrOGQIDAQABAoIBAAK9cxSyuDvW6j3Y +yqYiAkIcH8dfrhVvHrS4Q5va3n84gBFHDXSvQMJFhdY3plpzDMdXa3mQAOyzlWqJ +pdFUKcx4z1BJOV7hWUeFG2vmCekz4mDYTrtmyA4XwU0aSxlZF9KTciWtt828oVMn +0Ig594AYH8jc6lv1Wwkg467W1t8iABEthulRcdZq/xXVqT8q9uq2BYqsXaSN4oFG +Zg7LnCXFt/XfuIFs++mI7NZQuF5v2EnKWaDCWhpVMWvUiA+17r2mr+Qrbol+TdGX +WHZqRld9Fj1XzPLgxlqfn6AMLlgivTp3NrgLAkvNZ/aipq63KIfWys2p9DbavhBg +jJHYWl0CgYEA/fo2zXk7IAOvjKxenrDiqY4KWduD+Ww9EpStKuoauOcrbO27p1CY +nRDK4SRAUO+AI7OQY0TZHwLuv2/pyHZ8bvvJSHrecS8PUhV3mJhOVupl5JxaHtYv +EEf0+L+irdNTXUAvOCKi7KIB5mCaXGhdSw+raxj++Pt2QaAqOclKH3cCgYEA5f4D +h+0wY5zRkuOSKzpHC6Xd49plCDTW7jtKfx+PR9UIPIoAd0xhon20QsaDnl5MOf2g +ojvNAqdn8uUERr3nN3PgWcBEGd2wYq0ooGUD24yoTASDXgvTMNggYybStFbrymPa +vvF+9yuWfKLwKpSKlxEEfnbnxtBm7rg+W3VLgu8CgYB39fZqqQdXQMZrUINEu1Hk +OlYDSV8VsZ1LKHR+n6LNkUr+oW+QQM5E6ciZ/RBv6iABPPBHIx7WugDg5VBsQiLW +HRFercJhfZPj9oXNyqq9/OrxxzP9+rayHvrDf2isZ/OpSQbEof+Ie6EgGqLuYNEo +Ahe6d0z/d27M4oTvVHcxUwKBgF/Q1lm/iARH5cujQVb+/XAt1uZBKwwjL1Oqodua +I1ASwU6vU4hf6uEOK5YSK+1DbdBPCKft7/fmFFlN7d0m6nfgr5vUjMqV4BEMALvp +uZSy4b5htvTsSjy0HbIRD4EQIUV9GjmoVHPW1efw3ctvfNl4vn2NPfxHAEr9uQTT +NfVDAoGBALk5KL5Q6pAbxqaJw2e33I2Y7sVwKYcRynFdgi4oAcspQdt/22h2cBwO +l23240xCSSbf39fhrDJCZ3v2lWOGQMvdEq1i+9btj0FwhJPmcLGMOIIls0ycWYIw +/mrdq5dl5C17E1WfbzE1SMBsCeLFkaWa4l37KxN1o7Ck3Sl5W2tv +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_WALLET_PUBLIC_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUIYvyPYsDrigeLVIkFXsvpHrP/F4wDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyNloYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQCpwUvZ+07XGeclurl24s5mocAP/pmlUQU50unzsPRWarrt +4DIjFX4CUErlCnwGKff6sXkSNzJOCNQ4NQXIJ5imbEvx7d5V80z/5Phus2JsY4uf +IZ/LidOk4tSlUpLDuYURCziqy4fljEY4M58zfgCBHz2aBsLE6LJ7/WZSI6LcXcs6 +p9UkVY74uSfnFjVN7W7ivNutdv/vo2qsWPHojn35yYV2JEBje5LMtw+7SDJ8ljMc +kkVh+67msxiLlmxhZadA1eTf31kyceW3PcXA2rGqTMIcdb7REjOhA4kPgKV9R3lA +C0wqWEvufnkh2FuiOjI0mcYguRdvRJQooqMDr8KhAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQC3nE/WNyDiL0+UqIamE2ER +Xii4bQkuOfIFQKn13ciD3hxBI25sDS1T7ssAk/XvKxgMij7jx7vtRV2vJg905c53 +2+QxDkkCOO1wrYsvJfCNJ5yz2JkO0eXG1RLmIViixLoyipjEDwSwPM7SpK2LngsL +teV/CKkQAmdOIXB6e3KMv7DvBHboGmm/cv3JrKdLxcQd80HigqNR29nPAJhBx4T2 +VZneCNnrErYn+OsaM1TdlyIoTF15Aq5fJY5hfK3v5xLv7JP6X5XqXTVh8Bua/A9B +7pWnFtxNcMr2NbC5Jfmu2Zgc7xTcB1M0bRVSLO4ytMFnBvm451K3Awhz25CRF5zg +-----END CERTIFICATE----- +""" + +SSL_TEST_WALLET_PUBLIC_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAqcFL2ftO1xnnJbq5duLOZqHAD/6ZpVEFOdLp87D0Vmq67eAy +IxV+AlBK5Qp8Bin3+rF5EjcyTgjUODUFyCeYpmxL8e3eVfNM/+T4brNibGOLnyGf +y4nTpOLUpVKSw7mFEQs4qsuH5YxGODOfM34AgR89mgbCxOiye/1mUiOi3F3LOqfV +JFWO+Lkn5xY1Te1u4rzbrXb/76NqrFjx6I59+cmFdiRAY3uSzLcPu0gyfJYzHJJF +Yfuu5rMYi5ZsYWWnQNXk399ZMnHltz3FwNqxqkzCHHW+0RIzoQOJD4ClfUd5QAtM +KlhL7n55IdhbojoyNJnGILkXb0SUKKKjA6/CoQIDAQABAoIBAGhbdWb3UJt5yBjw +to14lwyPCYSLrybrLPxERiDSuxLZIDuWZRweXU3M0I4HqQEdEd6i9dwV5K4GTXiU +WA6ZEQXWc8Wxxsot/TsfJv7e9nXNqIrWX+b/vwWRkMplfeYnCb/VlyugXdXnK0/n +pEpCfsriSruCxn/I0djZieqbD8bKPaCNkHUqcQsgibxC22qYF5U8ShGfuqc8X/Vs +df245Gcrh0QhCf/pTau4qPi+E473LtcM6bKjSR7EJGCCQCQXf63+TP6G8b/PI7oI +ZoCVEImrtEFI0rdC6u7F6KWCwc1PNhE549TkzkC7L+NpnP6EGroRcPzXgAzz/5iB ++Yju3AUCgYEA3SWRPsO3E4Tzvld5YvIFG9Fte6oIrkpwJJ2NfCVtoe0a7YOQJ5hj +N2qNGjMszhQF7ieG5IsBwvU/cAfTp1SNh8C86ScRwZsQ/IK4iKOVY84u+qCLxZG9 +4m1ZJi0Zwuo0i2Gs+T/wbF88SQAErVPdDf46hAQVu4mUs/019/RMTPcCgYEAxIJE +9aIhYa0p+RuIQw1kpCMsFr6zM6384aOV+zjvF/FcHvsFajcJWev+qG22OHAYRGjF +6RzJ7KRMel+dYMyrkdHwPA1DFxtXTnge7zg6P0xFVxysBw+GBIXJb6DGNqhkT1tP +KaQOOPOwQypJjzVXPknaSStiA6EwiULxltRK/ycCgYAdH9B0GqRmvrC3FaAX5tXD +Zx5rFeaUxZrlR5aVjfxVQfu04gm/HTOb3b19gNXawgpR5gS+3ou52ECliXJXbCxD +f5+heRK+k6R2DOUuoZSQE1xeh3xA5cPDKTF/dJsa72tCG/gCz2fjbdtrpcP3676G +FEAymLMgAquB1Mwhvpu52wKBgBc7X3O6yz+E/WVZ/+4Nc0yEa/30ZbNCapcyg9TD +kmC+RCnVe3pnL0/WOrEm51gcyIGt8Vfx811qvy/ohe6fw9jlfQVcfAYLUXMReHbH +qvs4xSnbVesvxqRaPMpZs5VaqyFGpkFCB/xrsvb91Nx9becLTCdCXcAYGmjf5Tfz +uToZAoGAMYalPv6XvJBf3CvlvEeBGiXDfaV4O+3t1kevtbSgrrV6pW5sy/WwUco8 +V7Q7wLdRDi+sghPqhxXqLnIyp8chU5LMOwEU3m2kw3pH9ApZKLmaUbcid5pS+4rd +G5puMkmYGwaZRBY2HGOXMA0kdYXiPw2y/jdH+Oz9DvWd2tVL8YI= +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_FARMER_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUSORAd4kBPC4fSPqHEQskE72Yt1kwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyNloYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDLIIgg1ILQGtE9Rkza0ieGUNn6wDssvm0fkcpIZXhrYbow +ClydjwDMQrLHF295ZgfkqDEmD5MUeJTxBNznDlZtkvBIRjPN+M7ZnlGJCUTc9jex +U0a/KxrK6ygsaWNzQ8eNaTNOAR/l6j/kuAUC4vKoYKGzLCW45LlKVB9xQDWinpov +nXpA6S5UUI6YxqeriHr2IiLtXSZmqb1lqftTGXenvdnHrJEmer4iYjXHAfMhq+5/ +yHCZmpy4IAX39zwAWA9FnYieGSE8KyEqZW4HfF1Fe3V9C6nsZ2gVL1k0sgAgJ8oW +3CtM8tljHCmyxxD9EHSjJAkSnY3OqU5P2+XMbNcJAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQAijxQXc6ndCfyUhP7DmFCr +OR3jfPd7Bvc5M306CepT4ZGxXrjgy5idkKS8PAdSTMFJ/h3ShkGatgnb/OoOqiMh +YV78dx0wr4/sdEauPHODkQXmtPj+u2Al9ZyYflTLeiMV7JOn8H4oEE+r0Ra3o89P +F9+GOd/jnF8BpO5GUDN0nN22tNid5kIepMNVG0L3iDMGSDMzcXbGE49a8FPc4CfK +24Fd6BZ2YsUM4NWQ97lfGKtCsnZkChwNVkSkVXP38Zvz5H+sHaMyWQ+hiNBlB7Up +OrNw0uq/jvNv5IGwx/JEuYuXXfPd5CvPesk43ycsBvbjyYFSOsdzU55jEcivPQNW +-----END CERTIFICATE----- +""" + +SSL_TEST_FARMER_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAyyCIINSC0BrRPUZM2tInhlDZ+sA7LL5tH5HKSGV4a2G6MApc +nY8AzEKyxxdveWYH5KgxJg+TFHiU8QTc5w5WbZLwSEYzzfjO2Z5RiQlE3PY3sVNG +vysayusoLGljc0PHjWkzTgEf5eo/5LgFAuLyqGChsywluOS5SlQfcUA1op6aL516 +QOkuVFCOmManq4h69iIi7V0mZqm9Zan7Uxl3p73Zx6yRJnq+ImI1xwHzIavuf8hw +mZqcuCAF9/c8AFgPRZ2InhkhPCshKmVuB3xdRXt1fQup7GdoFS9ZNLIAICfKFtwr +TPLZYxwpsscQ/RB0oyQJEp2NzqlOT9vlzGzXCQIDAQABAoIBAHEcRnFxpP5ZUJa8 +ZOOdDuFeeGOHU+xQhdeEiY3S40F4hANoYbZjAWC862yuAicpx89uUSAOoCpQEzA7 +Mv9/HmWZ4y972DEkEZtg66pRfQVGHjEiXEzrpdnFJPPGI9j1r1Nxd15Chg6zaKzm +Q/QdiF52oNRzCvZwdzWKro+T38oTZLaYF/GvHZ29ikO2v/txEhY0E6Ox8PjUEuaE +CH+SoCqAkrpyT5ue+Zq76M1mB5krnLQEfoyHWSKykeQ9svSSrlpa8sJvAufSG5xS +xgqh56i3pGsGrwP5QLC6j6sK1hnknvs7U07HFZ00WNON5bYQTqd11n0denPC1xHn +l/O4AxkCgYEA9i61oB6TbS2KK7RAKRXiF7uIKZgWBv4yv/ynsf7d65gkfG0XZyuP +UB2yCn5fn5dLvweHbK7NnzmfqqiwNQYM5JbPld/eqzVs8s3B1M2rWTLugfZZ8NeE +W4P/mYwdpSpdQiSgBwdlolAtIZcbQwfujEXTUK6ZjDaYsHsIuW5CdLcCgYEA0zpE +ePAZ6KAC89a6IDvErllaegPhY30jIuEf7JFBAan9o+4cmzp4WXsaLkbjeSjnI1Yu +czUorTraK7OCLp+tphSxbpoE8T5OtH8xB4gTFGmm4lz2UFv5ObNpA9+rrb6Jf0p6 +x9CyClTKF1vsDTtWQvEg+kDisE+tNdLtHYBM0j8CgYAw6XTinFCUR5EFP+njf9qM +9pCGGxZ9SzIQHQXAgq/a6D6PjikxMWFm/I9sMFGVZr0A9mD8wfpOoWdMw/lGf64+ +GIyj7XfTMmk0EJdrTXW24jyrC6QxCtDcUeyNuF4He9RNmPNGkjyqNB3TZ69d8Qx/ +SDxE8nvFdO9/WOKR3QtNHwKBgGQhO/JEh1Oh/qROhv7etlab3ur2SfLakDxpkbOY +C8PZLHZ4WrEvH3vzgi6rxgtaW2+B3BUa/wRXYLLUroKhiTSwnIe8lVky2yZvIPPc +CodjqgumW9EuOE+k/8QpVH3RU+a2jMuJ38xL81ztY1HGbhbfrW1UMuG3c3mPWn3g +owoLAoGBAOjm/N882BzVt1wK50hYf6ywdD8RA1UJg0T8XN4meoruG0nxHo0LJG+q +ot0B638GSG9ESBbAeYxQZ8zqgH5y1DS/O7xmLnHNck7Eu7DblJeQc496/yKW7Uci +cGosIckRRVM0Dekivt3fxdIvct9UonKUJsAxoZsf+fnmMj1QTMai +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_FARMER_PUBLIC_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUPNbZirLECcLXWjdq7AF5vdOX4UowDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyNloYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQCVE9/KTj19nPrT0DaIRmLQBhJCvvMsJnY8Fa8R0hfM+g41 +jHKzvriwD0yArxbdj+eKr00ZWM9PCG/TpnUkW33PSya+VQ4iADUTcD36F/5p0sfe +psu1+tUf7MdMGERrRnNwG/xTAo47whrJ7QoIYWjhtiOREjImBjWXRgatT4uLuNsj +wguPTFQklEH53mxunBago7qJLwJTFVJ+hrxMuHAwYHxhdNKA18oVKVcg1zE3JTwH +wcY/n8kvUXK6L8GJ3Cororn53ej4KBUbvrsAs6AXtpwUUQy/H0s1ZkuF7r1nmUH/ +zGvzrPTMnFvtRA3yyXxpjG2mJvx0svwqVAVV0HpDAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQCPL//viWfMURAJWiGJZSNH +uy1UWUkt/Ga5zIdr+22nHPEXTE6H4+TbDKomVa7xJebZ/Pr0zG4Y41wk/l65Qq4C +5FBNiGoLTO3T+6aSCF7iwUoWRW3leCL29TZrX3AG+R3CnYJnJtGpH+vmqb4lp10c +duCxdd/1Foe3V2Hc1QhFRwd3uG9wYJ1VL4ifjghT03Kp8UcPDYY3w4A3/QEjVMsl +A0pgj34t7oiT/K54bWtkWphbDu5jX8JP8+A01CXS9njXU1OXEnRKq/x2OhIFEAKS +eKywXcUoqfNpX0qmTKSahTslCZu3kUtOgyHQMOJIceot1AZ2hgzDmmEL3+DD2gZ9 +-----END CERTIFICATE----- +""" + +SSL_TEST_FARMER_PUBLIC_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAlRPfyk49fZz609A2iEZi0AYSQr7zLCZ2PBWvEdIXzPoONYxy +s764sA9MgK8W3Y/niq9NGVjPTwhv06Z1JFt9z0smvlUOIgA1E3A9+hf+adLH3qbL +tfrVH+zHTBhEa0ZzcBv8UwKOO8Iaye0KCGFo4bYjkRIyJgY1l0YGrU+Li7jbI8IL +j0xUJJRB+d5sbpwWoKO6iS8CUxVSfoa8TLhwMGB8YXTSgNfKFSlXINcxNyU8B8HG +P5/JL1Fyui/BidwqK6K5+d3o+CgVG767ALOgF7acFFEMvx9LNWZLhe69Z5lB/8xr +86z0zJxb7UQN8sl8aYxtpib8dLL8KlQFVdB6QwIDAQABAoIBABW+c1rXtKJYvkEc +0odn9MuwxwMTRPbAmWhEJWftA+my41WuKaDMBbYwVRFD+IrSjYwt64nx6TL24RC2 +68kkyyHsLTd/wnL1Isi2C2QqEcKvqtVv8LCXaHSinaMcuwYGnZnRiyk0aziOJEgl +mdwFET4yyddEFypyp2hsH1cyDgGP+O+8P4JKAVMSrtCCM/+5a0fj/Z2TUIEUDkzT +OCIY+GsKDwE88VPErSfzx6gZPWpPkQMI9evhGDKhwvUuBNgKal7oeSRbzxQ5Ihj4 +crACs07FTRrrEB3TG22cPk+gdMvM6nEbrXybIjLM8XzOpmw6M5RQdwYeO+O8+4en +YrH0KhECgYEAxniu226jcNCbl9D2UHEm31fIs6esyEz3IJKB1ZC2FAjQJdZ0Y2oy +KRAIxDBFHY96yL82vKXloT1b0LsBtsTFJLrqLGtiuoxLgUxNtU7s4r0aX0GjpStW +Dk0ncrInwxlh3uYcpM2Hj4qjNam0UrttDr1/177tOwaPdER11TkiRDsCgYEAwEn9 +HSb3XecbMD9Sap8ER7hIwxU5im/VopDzHqvpOb2a8TIQMV2rk48htZIkt0yVf7hv +X7Sx/FpaZOJPfa+ND6KeQKw1noajEeSHxLz13aOnFlUtSHzNTUcqk2oNSx+0lP4n +C3W5BJR0fsgtcFAd5lxEajwIxdmPnKm4T2j06ZkCgYEApE7k8+T0ikEpjtYAFTiX +5e7WyWTXNjwBm4Wu1w+mrY9eQvT4BhW00SnlGAaeMYrHK8qhliwBnysdCADJunXM +gEv98ig05Buhprl029UrZ8sGOjYtNGBcLhrRvbKgGHS7Ab2fmRBOWhd8ZsDH+HYS +I1HetM3ruCIGQUssAgn6xGECgYEAke2UsbMIt7LT52Gm4lObo/IvBh3tdSo6Lw1h +9Dzy2mcSV0lvEIfN1kYhhvJJ+vGb4znNDAzNpn3LbBRzzyaTHvKCtwH6DzyONN4C +S0Q8MuAnxcMOgpx0EdmYbhdlz0VYfloCt6e3qcogPrccBMhIaLJNGXJGFiBt5K7I +uTsl4sECgYAmsWXL22XeKAwv2JutLK5Hz/xn5BmzFDGBLvu0+FVml0aocYVVyQS2 +mzs2kRoJdLEDfcxibawQimTuCs53PkoKVNHB2JzFS8HNHdcFHaCb19ZO0NoSDbst +sSu4qj9TPFACKOKsfRpSIWoeHN/ps24ost1vE183ARVTsm8KTHgEJw== +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_HARVESTER_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUa0HzW3fH6O0kNO3yGndcTG3+kX0wDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyNloYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQCtlD5CM6r5C3//rZwWLmMHcIO3lDFfdSwrvONmMhwlkIDU +/FgLr20ZS6ny7DPSRVuU3jijL00Q6XZqpmmzsoN+RU6VITNuam/gB30E/WfF2yds +ifWtVpE9wM51V1SigEtAIviooNR/CrfXxaljw1wmReWjCAUP4MWJBBFGUymWpBlF +vEo+7VWZ5B4bfstRTZaFWRln/otMh8v3SOaJSmNafsDnulq1JXJz/i2hFfPjgfbo +rGp8bfCWpNmEzJSn+CY7aAyW9eKKR+unWzM7+PEnI2l+5rrq54BfxpMdfAFHlMK4 +NpIrJNmnjMLm1VQpm2/RFxI3LP+IDedCJmWpXbRpAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQBc3mZ5yN9pXLT/0Io6Ql2k +SxPSZc68538f/gjpnlXDxApiP0sk2RvAL3C3E3RCH1KeP0wjcRnOwE3J+BTaisGW +BbSQjQMnGm/zKtkwBaSIIjkKXSRCACEzneTMxPwEqCUgAWJMJ2/vzgmbZcQ7TxHQ +UZpOnDhjknCLmxxEk3cGk6+1SIAO9NQF4z4fL5grfQup6sBeyN+srl0WnUFWfBIi +d/pZHcUCKL+FmUrp6eCKGAFGQiM9TyJ62H4Cs/J0bR9e1asLOwSAunTB8+JnQa18 +ug0LdcWQLjcoIMaZk5XIn2wlmFIsTqVXS5i2Os7w8tb/XtdxL+2Qi+Hk0oBmGffx +-----END CERTIFICATE----- +""" + +SSL_TEST_HARVESTER_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEArZQ+QjOq+Qt//62cFi5jB3CDt5QxX3UsK7zjZjIcJZCA1PxY +C69tGUup8uwz0kVblN44oy9NEOl2aqZps7KDfkVOlSEzbmpv4Ad9BP1nxdsnbIn1 +rVaRPcDOdVdUooBLQCL4qKDUfwq318WpY8NcJkXlowgFD+DFiQQRRlMplqQZRbxK +Pu1VmeQeG37LUU2WhVkZZ/6LTIfL90jmiUpjWn7A57patSVyc/4toRXz44H26Kxq +fG3wlqTZhMyUp/gmO2gMlvXiikfrp1szO/jxJyNpfua66ueAX8aTHXwBR5TCuDaS +KyTZp4zC5tVUKZtv0RcSNyz/iA3nQiZlqV20aQIDAQABAoIBAAhgnjydt8O7TVsu +qtjbNkZWpNTIXzWnNxGJVURKaNdbSQx+fVVbCx3sa/BgfAPK+yeNLhiaINMPIXr2 +OyXEGNqQR8Gkz5Glq26ZjeweutJuyFFRuzy5b9sWIiDBrUEGhhs7VNr2oCrdfo/4 +Zzt8Y0cpmnKq4Wupwn7hZmAJhXlSr9deWrWUfzEbgzuqtMgIIVI9VWqW+wxCs82H +WxzD5crc6g9U4eo541wOx3eSn3Ni75Hotnkx1Vgs1Od/ivloYHAISDcbZB89C+c6 +FQtEiOphj0xU0V377e5RNNnjj6HXwdRti95YpQf21cGol2JRngR/w+o2AQuSO8E5 +TqiC2wECgYEA10jKJkWfGBcEd5s3SpP4j9mFrvVKAOeP1XDqwvs6uD52CkF9k2O9 ++Ja7O9QVxKI7Jt6fHISkrMY1srms/VGw/9R2lxBMHRgrsyZZUIjPSU09qwU5IORw +4Zv52zWqG6MQZu1dUsFRBsu5ibVbdzTic8G9v4V/U+fa2uz6cgLiTEkCgYEAzmhA +PL7acIhmtGhTxNPrnnoUB8sizfTP0E3xTytmTjbtr+lrNz5RvOIv9owp8hEWqMxG +ag9lBdERQnJhrW++ivjkprQZmyVzS9NR/XYXG0W/FSqYLZZX3xa/bmh5eRM0qcyu +AQVDfAvUs+7Wc0LTV6t931zIhTcz/jZ98wR85yECgYEAzKDGbKxedVJjj6B8ZKnT +aD/U3qEOD2ALClEDBAQyIzBTmJn5V6BF0MTNASgs7LNbUC3oxP2bXRIltlTghgQh +Hnp/okT+Y+U2nFlGKdNwW/dMN4OGcqpQVVGho2gV4aEUFRFnVCKl9rSsDaXRY7Rj +zq2Hw0SL62AFWXRI9Reiq+kCgYAFqnjw8fA/HI9tLlv2UDbsj79TA3F+I9U8i5cv +LCrPxNQ7evXVe2F1BOR6KRjRq0Rq98iLCsckJLwLjeY+g43AdNqZ9OGrD5kdoLxk +b8RsnDqFkzjAL5tT5WT12+pRt4Q/kP3Jy2Ix2oVJNyot2czBYFTBN+PNPFmyBb7p +V9sx4QKBgQDWEIRplbm9f0NnTBaZ68qMtEt7nXMqEv3vjalTeriB+X9GJKDKz4Ym +slr2WOPJSgmZApbz3B8EJmbKYys6gE1z0dN3Pl763YU5EhM6kezEX2C5HfL4iSFW +Obkbg7hMBYQk0faEyd6dReIZwVHD7WAf+zCvh9ZG6l4ic1wQs5kYfg== +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_TIMELORD_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUfogbG84aJo97wy3P0ZHAkXShqO0wDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyN1oYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQCxBstxYtPbm7eb1iRbL4SZCF0xIueUH/gZ5nfj3Tbo9DIn +QcHVbNkr5XxH5MBtQcT8bgF/tgBkj7uuFTymT2EpBrtTbdLCTsZ2eMQIABOQ4APZ +NKDiYb6g95JC7vdmuLfuB6VyvlMCZ/Ffyvow+PCgpT7ElNjMiLf7y6mcvzgNKSLN +JYqrJCf7vT0a4W+isck6/fD9J1RUhE0Xkts5wmpwJQGsRYme2hNy/PmCcEY9rqpi +YOxWWeyLbdZ4nI4OH652JwXKUTijqCZYc6+BoXfjwZGq05WK5NhBZVnlYMd5ZFab +DnXGR1WC9ysv1xkFqpSfPlwSZdZcJzBnXXwhJ4qrAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQCYInnIgvKBvmX65bVLmhNt +cEUickmBTKOEG4i1yovVcDPIPLGH+3p4Oxng+N9zJh6n4SyCeOou30tRuNEunHOC +PmixRUxM04iNm5lLLdS8dd+kErpX+EYT20amgbnN0HNDRi7+EANXTm2ld9HD7skb +M6lABrsQQdmepdNz6609G0HO/I9rdUy1GaXlwd2th21VyzKmmq28nIT7KidBoQNG +MhTjsnNrZekjW/k3sJA9nEhyERmdsApb/TlUV6A4ttQZOqPV3ClkumqQn04jLEhg +dBNTzK0UEqdVXrPRpJBa/gSkSsCWXjMhwY60pHDsDmnOQH0kLQ5KzPU7IF3x+l3M +-----END CERTIFICATE----- +""" + +SSL_TEST_TIMELORD_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAsQbLcWLT25u3m9YkWy+EmQhdMSLnlB/4GeZ349026PQyJ0HB +1WzZK+V8R+TAbUHE/G4Bf7YAZI+7rhU8pk9hKQa7U23Swk7GdnjECAATkOAD2TSg +4mG+oPeSQu73Zri37gelcr5TAmfxX8r6MPjwoKU+xJTYzIi3+8upnL84DSkizSWK +qyQn+709GuFvorHJOv3w/SdUVIRNF5LbOcJqcCUBrEWJntoTcvz5gnBGPa6qYmDs +Vlnsi23WeJyODh+udicFylE4o6gmWHOvgaF348GRqtOViuTYQWVZ5WDHeWRWmw51 +xkdVgvcrL9cZBaqUnz5cEmXWXCcwZ118ISeKqwIDAQABAoIBAQCXksvK7+WaWYAi +rH5AnTUZmvHASrSiPaU+9/ibYCPN3pi6yDDhPuvMDBgXrqOcaP3zbXVXFkzLzc3S +xlhBxiHY8OygCJ62xKBlfA3NE9Os7kIdTlSawTpptNDFArtOdsb1xhJBZvjITJt9 +e9ww5lWSFyrhQtlGd6GgtMcrcQbbLHqP1bKKSwPIYI4V6OjxBRK2QPahgrudPdNp +C9C9GWCO5bVary0uKfbXvgHhUOrqC2ogtmnOY41KilofbxCRX7Mc7uLzR2SALKEz +FpTt/CThsWt4IeUUvxSOWtpUa+qIZ2U9zRZZUxIwXx9I84/K3unbk7REHG4RHspG +itgQTD/xAoGBAOCHw1v7K+xNRrDbdce5X6+jJFdVb4NNXkkWPzqD7PuGEdL6LUuG +lm75CAXx0YZtXATog93Fz1ZDBH96tLPzI3qxIEdoU2i+iPWFv9q+SMA7IHNqc8V1 +AL1XUFWnJlfTUGihvlHGEODLIZ0k9JpMPwDIdrxp/kaqJGkJ46Ue55BvAoGBAMnW +k7m7fI2aIfRaiuAUsDfaNAk73N/M3XmA85h+h+o3+hI8OYv7kCnK6ahD6vHygpD7 +eLJQgD3XPEpKswgP+uUt5p4cBAkt4gGR1sSVSEMrc7OJMF7CTYC+c9U/SRqG34V1 +eYvCorVpbFp7GbY3WbDJwJbtPnukaWlVVPvwTQ+FAoGBAMS7UBp1DnxDDXCDKkTw +kP2k9X+sNUQX80gYvRf5ZhjQ1SdF25A0gfUEMNp8kni1s439aSVVYCEWIYfNLS4L +GQg00LKgn4zEfd6a7YqtdbMxW3KlUIEvzpEYQyR5i6giWG8FYWvnHvzIH1DAg636 +pq15+EeIm6qxA6whZRxV4tHzAoGAJnp/4zK5Bg3SV1FDlICdL6irru74prnZpyZM +SlAk/SP6yqsslWE6FJ2YefmousNu3ND0K5ppOGDmH4uqIelZ/YMIqi/RHSbgJUh2 +VzfWdOe9wQZwcEA+okKstoTAHQyFZF4G8/wBJPCaNY2uUyyuLqPn0V4dQVkQt4IL +F5SyPDkCgYEApt5A/pc42+/bxjxw85ENMO26eJJ9YObUgZUgi+BwR7BdzpmTfxfm +Ubbpg0q5RA4hFofkdqQkPxx9A3Q40e8BNGto2NHKf//jhBCvbZs0uv4akAv7fERy +0SMTECjuLplUvSyzUQ/evpXDRkN5CuH7eo+GuIJPPulPCs5+FUo5TOg= +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_TIMELORD_PUBLIC_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUT43GvhVmx9CzguV2nwdektqBaZwwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyN1oYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDRiTszw1IkIr9n6KjMvgS/9I+p+La/NwfFPsIWkTE6cffr +LIi9H+yTMTM3tbNXM4JhvqdeN+c2mDAVjhoi+AAQH3gkpXitxD4ZjYzCCTtwd+q0 +gpCKLPOmKxYjqCcNkmEc70g/QOs95HyDdvLL1G1sgMnhldGuc0a1HX0Te9nE7KkW +40Etk6lhEKEa9o9rEjAL84Z6kizKELOtjgd5wM3FaAMB/UIhf+kDxB5vtAhsKewb +2Uy8wMpXJW79cqfLxgjDhd7LEZwSba2Z+XRDD+B+NuC58cv8H8y6b69Rwjmya50X +K1gW5/aZA2+mo9NlSAMdlOM5ntgd2Cbh0zg4jDPTAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQC//83StmG2IlGISJqGVwSj +B+Jl/eodYO1iYuWtCfobI/WZEz7Noqtuth9U2Pjc/VDImy+w/x6XNPTTa0MgUnqi +/GHG1qRIefWrWp00C7fFbrJW1llYv/AFZfgYZB92Vr2X7RupnQOY2a/XwT8dzstU +ZPTNK5wV47MmUp+u7p2Q24ywS+GuQTK7IZnhQjP7ttKKBgdBp9evHuT4B3yl7qUK +JWb3nAreUSESWkumSXlted0sDQQ7ahilzHPkemRgJZotbQID9sV7WqHYMCRtZENX +53jFAQFtxj7mjjacvwWs45XgEGsr37LjCBHUHujVEccfGXN9+LRrpS6pKiyJeMg1 +-----END CERTIFICATE----- +""" + +SSL_TEST_TIMELORD_PUBLIC_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEA0Yk7M8NSJCK/Z+iozL4Ev/SPqfi2vzcHxT7CFpExOnH36yyI +vR/skzEzN7WzVzOCYb6nXjfnNpgwFY4aIvgAEB94JKV4rcQ+GY2Mwgk7cHfqtIKQ +iizzpisWI6gnDZJhHO9IP0DrPeR8g3byy9RtbIDJ4ZXRrnNGtR19E3vZxOypFuNB +LZOpYRChGvaPaxIwC/OGepIsyhCzrY4HecDNxWgDAf1CIX/pA8Qeb7QIbCnsG9lM +vMDKVyVu/XKny8YIw4XeyxGcEm2tmfl0Qw/gfjbgufHL/B/Mum+vUcI5smudFytY +Fuf2mQNvpqPTZUgDHZTjOZ7YHdgm4dM4OIwz0wIDAQABAoIBAB+9ILmTgXK1zLZp +mIAC3GdTHRvK76uBI20uN2oBripDLyFxSnkTR9t33WE35aV0yPATV/i+kQhE/yuU +rcLUO/Y1PhaW9fOkQR/PwB14FofPsj6LdGdprbJi3mSiSOAWZx1h5VindbqXTIEB +WH+lermvvGSuM+ev0GsIv3RfEzpvtHYynKTQ92PDmrbJS7/x7IQGl20UUyA6T9lg +PidOesZrMJQuJvqCWfqp/bTYuwQPneawUsV4vNUxuLcd4ZVNPDJG3/Q/YtwpoKu0 +UDGH8DnMkqn7JVKvBonDhlARdtdJ6h7g2NmwPTSyKZmeNjwKy0Vl06bcY71PNRZB +8ET8PtECgYEA6dUyY/CkB5B46eD5Qpm2Kd0gsnCxaSvzvE1LC/2xIqeszKDjsjj6 +rS1zz8Fr5qsyYN/b4Liq61AR+kUEHcPfAgRggq8Jy8965xBFDuqTCdtW6jADQ7VG +TrwDZMg/byBqQUbE7dScCl549xItUKh3WgyjZVbv6CogaJyrkq1S5ysCgYEA5WZi +utnrUfSedb11LQPCFQnJ8gnvnOUsnigeRaMxArIXj2qmADmQfZjIoDOfHoKEbRR5 +/j2bHHuZTJ+2FWLI8cSCEAfuxTXCCj/SXU3BnzlawTheUQrvOddGWstUCh50VoHM +XPA8bT+9mfs75dXUNguuFHidfS0fr3tCCkzukfkCgYB378mj51dLJfhPBfz0A0Gj +YW+W9ySYbFndOMwIf3xu6RBB+TgxPvadAxZG9s/whdkWRVxTfIT2o6BE/UdqOQBW +2YXjIgLlTiuc/wRc7Ua0JJQFFNFn1kAUvG0FMY0P49F8X988mfPbga+MEv+5Ql/N +iXP508jEDW+IGOwMFOjT9wKBgHyePDApRe7FpndrroX/rqVjJfN4dlSTIsPgI2HN +H0jJmobsdrVUkCvKneJ5aI1YdbwUDZmRufulIUhA0teXTHYaPFWdGZbEd16+APdy +0CZBMA4bIxF/kSmoyq10G3lLxgNgi7ZJQ2pN4CAQHR/kI14gxjaUt2lS5A2eNegG +lutxAoGALG1JxRXI1G2QVqNXjveLI4BA9KKZlXymNPRYxHZwr9wkgr1OZUR6hzGf +GFzfciXBeiM5DgwCGzOsRmhEVWQY0AWtwNpkvdTo2+FG+wiehzSkMviiwi4wXDZs +ZPIOBDSJCDbdO4GS9MJR1HtSQ09uMVK2/DMEOUhmfY2fkd9+rcQ= +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_CRAWLER_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUaD0m6kjEe/kX8KV86Tm3JMOXw5gwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyN1oYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQC6kaAaFDrrIxHCuID22AfMrV7fVrB9F5h0uQP5KSawigxJ +BHqs9gIjPi7ur9nFGxGFjh+cyFF7kkWbLOJpABEu6J1DC6+MPe67rnQjq3IEds8d +c4ToidqfP1/Pgl4uOfV0MJpYF77Vpc/P2SHApjRIl7wCPxBS50BZkPaXbobb5e9r +3QSFpyLsvs0jeu1xp4w7DBdJsiHY/Qh3ekvUAH3A/tC/kQ97K9ZndgIQflW+OO89 +FwWS9pKLXkYbKejhbaQ5D6wP2xcvY4C093B8kpaeMA9DCW8tUATGGTYaN6MUhNt9 +d0LQZ0R58uolJW2o2Qv45TokYoHlsuGbDcc11ijDAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQCaugeA7XZuF/JeC6T/cDGq +c5Y5pEBuhvNHj6aESm2FvuRIgXS6djo1YZF0McTiLVOtnUqDBSl1h/VH1e4n8zj3 +MAVMPfXbAByexDGjbEIo0/aLmcUAAy3h/HQYmkX+Ge5Bm0MCszSbM/YqMPV30rSz +Gq/KfB8s8QQb7T2sS10VTlIBL54AjEgnyunR3vPjx0rqfnFRHdQoD7MdUwOEq3qE +6FFzpmp/fUaValfF9FS8w4vDq13LUY7OhphmW8mJHJ6e7GcUxFPLKs1oNpsMMPJz +wd4te+SB/dQ8CH1o3xvvFirUiPNz1wXRziJSO6AqjNXBMe86qnELwfXohI5oxUl9 +-----END CERTIFICATE----- +""" + +SSL_TEST_CRAWLER_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAupGgGhQ66yMRwriA9tgHzK1e31awfReYdLkD+SkmsIoMSQR6 +rPYCIz4u7q/ZxRsRhY4fnMhRe5JFmyziaQARLuidQwuvjD3uu650I6tyBHbPHXOE +6Inanz9fz4JeLjn1dDCaWBe+1aXPz9khwKY0SJe8Aj8QUudAWZD2l26G2+Xva90E +haci7L7NI3rtcaeMOwwXSbIh2P0Id3pL1AB9wP7Qv5EPeyvWZ3YCEH5VvjjvPRcF +kvaSi15GGyno4W2kOQ+sD9sXL2OAtPdwfJKWnjAPQwlvLVAExhk2GjejFITbfXdC +0GdEefLqJSVtqNkL+OU6JGKB5bLhmw3HNdYowwIDAQABAoIBAQCWTQ07FUMl/Rmo +0jTrJ7yY0q7UpCUIkcK7ffXKe7F0lbIx/M7LmmC8fbMXjUmWNilWe9nR17t1HrC+ +w1kfF/O/45wV0Es7YwV546AiwFLZb9GJO3A+WhhrJIYOSUuQWBb65NDi2TZfLfaN +zrIXXo5OURcghCelcjFwNo3CD0PL0ECB2vfBJ27QYHlv3mBGCxKTLyCPb+Oyn1fT +bDGLiwHv5tPxx8zkcaNXDztjKDJYNMSS5hE3/69dyyIdkjA+PM8Qwghr5V14m2lJ +7RUIWntNzsbxRZ6t24rK1ytQVO2o9kOx+sEtNvu7hnvmkFSLhUhtXzEAabQXyXOn +ftCDVhqZAoGBANsha+ofkczMozKrALzEeugh5D57tDmGvcG2puIiiJiYCighovkr +cvQ0h5gr18lraEP3768rZlQ0814tA17yoXaHNeNY6IyE7EiLkTtKx4tRfi1FmgK1 +YTc0LWQhELN6lg1YqbuRgAn9wPxJY1MEFutuGXJCzcsGhCNCvqmRhl01AoGBANn1 +rabHjCBZRzWOeh8fNfAcbNdVLsrD/nmyCDxEN96q/ascb3fcyNx+USqKz1acoJNC +vGILzRa9gm/86KA5r7l41+bf0FYz0muTiqpzMNN9LCBG5ChosmsSB8v7i9Q4i7yu +pCcv5ATSSeAGUcvrenK5uWM7ht+fjfTdW1RsIsUXAoGAYSYVEMwEOLa0157GienV +z5pO9YCkayiYcgxHOlQzGOu2/QnElhE0Op4bS1SMq2ip5hBCu/dSu5xqFOOB7hNF +kCXrtypQlxPLKXJu5cmGY/ayKOIFoJPHUNEaGp0qKVf8tFgNj/G2wTc12uOyXDig +7Kl4MJb7Y7o01OkfXE//MM0CgYEAmJ87GNUUXzaE8ZCyHQba2ybcZDCG0n9Ju2eZ +8rGGOcqcVGxV9aXJlPRy24pVw31rx5JsnW9MDkdnhgmfz0p1rTdcX5OLrEEfcCrh +Z5e/segttPBPJaiifu0iPokHEfUCjH3x/mq/jUy/ZDqonlVm6dz5Xu449HAilDtI +j6Yk9sMCgYEAjE0g9OFzhHJtSwQtTy8S2GYJtFAvy8KneXsR/YAz/C0/CgvT+OsK +ACI948k1VXpUK/u2A+bmqxThN7OPDC9sLgThrdtAL+7UKH68fkqFYu0usiTif2z7 +/1VI9PPkm9ZKtHx6O97fdBjv7DHhGdYITB+pVXxI6VKXmS4+YgfonJo= +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_DAEMON_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUTYcm+E1jiGL7UEiopTFGmSwk3eQwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyN1oYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDD4xmVdRUqCRCDRebD9nds1h1mtyutoeHfruSn4PiEx30Q +YSzGRszSTvM+PwzL6jug6CVmlfWzLBC25unegeZhjlhoMkMkVavA1bZQ0sx2t2ll +wqFMhk0BvfQ0ftQ0BEgHpbll361gZGb7sLB0kuh1LhRE6uWpkg3WJT36DwzkCJ+R +YgAA63J11uLhMVk22bQ9UDIlmlL2Kd0M1sLrdPtF7h2wB77RirXpH9RxPilPWTwd +t63eHKetGMwmeCHP6VHWc3mtmnYCcEf851HXr/VYYdwn/Egbn5NWU9MkyG4U6Jw/ +7iVRvf8XIv6IGcwY+qB922OjrCxsHYCWUbfwiaFXAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQBFYpf2hnFIOfEEKFkNni9T +QK//s9W2Bf2hwn59TC6tvpN0pIenfqZ5A/Evc8PnjaO8UY+rWsxRL2YZoDEABnQb +3x0VfnZKQLKGju6JiVJWSn5F5Ilj2ntglHsAgQp4QBEMbIwfStW9AeaCwVoeD34B +/NFUoD33QM6E2yKuRetceeauBA+giBkSFaUA1jeSHfRGeWtuJmnNHvd9iA8cfrSg +gETkXKUyTYkS7Afi47oCMblmuy1pKOnQsirih8Vnic0Wn46bObLn8lt3k1d0+G0F +Nx37aAAP/ArHTyRh0ctfw99aTDgOm5v46NZNLPH9z7NPTtN0tz3r3C2nT4SUB+qq +-----END CERTIFICATE----- +""" + +SSL_TEST_DAEMON_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpgIBAAKCAQEAw+MZlXUVKgkQg0Xmw/Z3bNYdZrcrraHh367kp+D4hMd9EGEs +xkbM0k7zPj8My+o7oOglZpX1sywQtubp3oHmYY5YaDJDJFWrwNW2UNLMdrdpZcKh +TIZNAb30NH7UNARIB6W5Zd+tYGRm+7CwdJLodS4UROrlqZIN1iU9+g8M5AifkWIA +AOtyddbi4TFZNtm0PVAyJZpS9indDNbC63T7Re4dsAe+0Yq16R/UcT4pT1k8Hbet +3hynrRjMJnghz+lR1nN5rZp2AnBH/OdR16/1WGHcJ/xIG5+TVlPTJMhuFOicP+4l +Ub3/FyL+iBnMGPqgfdtjo6wsbB2AllG38ImhVwIDAQABAoIBAQCv0n98LwM4H7q6 +mVtwOSEoh2cMcwy5ZLwg0hJavQtT4trWgOJ3dcUSX9rk3CLYRP4Qh05Krf9DOyIl +iU4RcfcfSW0A2Vx6mIr5Itnp4cu0IxxvQisVTNaB4cX3+H7v0Yf1lUK7tfEgu/3T +m9xGRjZwN1PqKAzMD1RsCjF+6VNguOqHzsz+8aqppbAQPqGfVCwqjdfG0nn6h89M +KxpRBQXhsqLvrxVy452Danoz64vPEwRePXhF7ZOMStno9Gzc6ArjEhIFzkNvPp+M +OpwjOTeSzzFmkGP9bHPL9hbDWpFy/wVJYuJoHGdmtFZPdGyfTzOvWqxuBCnTkRAl +Na8oFz1BAoGBAOd9Pl09+zXzi0ILdnseLvGZ3aVeOr2fxf4v7/zHDCgpro/+9OPu +oYacMnnp+nbURrjCQBeE5lu5AKtq056JxLngXIAFrnVaEu/4BjuBTa/nauXJM+r6 +q5CLtNQSWsq46wpcto/btPvv5kl5AaGx/AtkmGqD6O7e0W6Xm2+r5AiLAoGBANig +0uCRtv4I/vTjl6A82aRRy7zWyXRYXei7ti2G5/EOs+MwYs3tJF6ncrVMyKjcNpof +DcnuO5UO8bL6Lcz5G/XPmMfnoicsuZxZk5X7NofRPEwG1JKvUA7wvHBJU630M+p9 +5xLI3fKIW1LB9zZT7wG64iJXx1CHFl5IoqCSDJflAoGBANH4m3nN/6/nMbh9V0HD +lgcVbqNR/mwDoX63krIxBgjkDe+U7iJVUHQd9/b3UXU5hNCPeb0bkis+eqoBouPZ +yPRk2uJQxPay9hxuV5Df70yP1zmIsCwCpV3eKu51m57n7mIeyIViXx5qcvLP7Lfz +DlBzNYDgF4eb2lG6+IVpX9STAoGBAMgpGZBss2PY1hNatABYGVWOSq1q3OvGtsbT +owpAC9IdnrN+Qt05kBBxsji5APOdvkn8BZaerKkXDNct+OHbDy26qtWTPq3p0nsX +/ZlobENkXs10xjffCx8y6zrpVgt2h/3UZY1i0klGGvPFy3GEbmPv1QCckMrkdxOZ +E8NAD6jNAoGBAJ9h+zA+zuJZZPddUltNeg1ZxvLbjg9rd/7YSGMVNmCWBpwv33HR +i881B4UXr3m/cEbA1PxBwHkSXGkfFvsmdqFN5UITd1oF3EAHVK0oUmewgl9clj8B +exwCTm7kDpCVHXsNu/ncODBQM8+r4Z3vjXAyw0DT94iaASYypEDpXVgw +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_INTRODUCER_PUBLIC_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUbtU04ozLF4XldtM40YPkF1DIMKEwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyN1oYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDnZYSeBOBGn94eiPAorxg9GdUoIGr4UUNxrtXTBmX8zZZp +LIu3m+2T+BO0ig6a4SZ7IxD4Ocbj2YP6sYl44F3fNXjvekvNzTE6LaeCvOh5myUj +3h/cCZTMCV9Ja4CMg+xOCBLu+CjyWSkp5Z8kdYz86o7gKNh1eJT50a8pARqyNdWw +Q/YoYTTpfG80GQp31Zb3hzX9fl6dT6+gYuV1xkiPrSuxX4oZZ7ZH+ktIwiFoXGOh +sQu1cLaLHP0iImF1QCHh56RwV7j/RcgqMx/hdzz80rShbUost85ngl+0ss3jYm1+ +Z5Ps8u8FRSdgN4H+HQL16q8OGrMZXdnRKUYEiCY3AgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQDG/ltB2n5vOIPi90d+GikR +nGu1SR7ALScsbF4w4uIH8UvtMTu4nctlLJWPNuor3s7ylnwv0eMwumtuHYuIBSm3 +9umrUIGlwMedCdMNKpvQF/WkXevEQj1azfGmltta+ZrQBxwhHg069y8Ykb84SM8D +5vEy0rJ+zmrvFYeKaxzAjA1sG4bjCiMMiwJ2rHXFjIFdQHMwwYcFQ1FeAPxEe/8T +PGYY561vOKVP6P86swKPOsQn+3MYR0Ehi8vdw5E4f3TcOkyxx5sPmiC/3pq0h4U4 +kmLI+Ng3D1A3NzSel2J0mp7RiPmUhu//WZOE3G38+27jGp9GPEM/zlc1gL2EHN9w +-----END CERTIFICATE----- +""" + +SSL_TEST_INTRODUCER_PUBLIC_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpgIBAAKCAQEA52WEngTgRp/eHojwKK8YPRnVKCBq+FFDca7V0wZl/M2WaSyL +t5vtk/gTtIoOmuEmeyMQ+DnG49mD+rGJeOBd3zV473pLzc0xOi2ngrzoeZslI94f +3AmUzAlfSWuAjIPsTggS7vgo8lkpKeWfJHWM/OqO4CjYdXiU+dGvKQEasjXVsEP2 +KGE06XxvNBkKd9WW94c1/X5enU+voGLldcZIj60rsV+KGWe2R/pLSMIhaFxjobEL +tXC2ixz9IiJhdUAh4eekcFe4/0XIKjMf4Xc8/NK0oW1KLLfOZ4JftLLN42JtfmeT +7PLvBUUnYDeB/h0C9eqvDhqzGV3Z0SlGBIgmNwIDAQABAoIBAQCPSwZ89HgORCHA +tvxBtWxFKiId3zVe4LPrSmGPdH7jtkxWhQdghGbzIsTRIE07DAJQbr6reNv5bVGV +hSukdwyqlOp3IjyfDVpWtL7u7xzncXPmaj9Ae45xa7xeMvxAB9Hl4IoZAgQZT612 +DIQoh8LvPDGODr08wZc/vOHDerOVdyNdVIu27pJziTHyhHBZMpJar31yBsG2J4j1 +FhKZrmZr+01fV/hGCQ1NkKy26gOA7OnOCqlco12iHX5PmWwAU1S6UJf6oSPxXAUS +4Za5T9pxvpFgYKlmp5665lhdRYNA+Kf3Z+dF6n956AOFZzBE1KuxTCl4ea1LPJqY +cmHGbeABAoGBAPy7PmMp/C+WbdK3JXactlV/XvMB5ldqbfh7q5CqwTG74q9Y70hJ +VFhfB/a/ZcMEb6vELgpnSzOMZFg6AA89PWBEip/2U1Yf6ugqjsU9xlKkXVnzoS/p +99QDtZ2vEw6Cm1dA0zIt615wz2A9/wDsBktya0/ijiBuxmc9CZcyUYoBAoGBAOpj +o0PlWbVTsKoj5N/rQH0VcIYwtIQqS4rbpsgAjhihA5oAS8ZKl484pAwf2adXdSdR +5vn+FP7y17oRt6ttQPITApKpZSIcxNuQlLNI95c49oU53tAvXZdddb0arjoqJtw1 +GkTlHj08whgxpTd4S80GGOn3GROekcJHVnjSA4A3AoGBAOSCiI4w0AxW/0WewwjT ++Sik2bzu4s33NSeO6jkLq1LEhtn0l6XMZ67ffdvkgqYpxK6R2u8dJimdrrz29EbT +IEOCtbScjA07HrJ8iEpe6IqggqdqWTtxWNsh33yLZ7ee78Wcn1innEDvzxE9/Otg +fPCKq+y287rvbgS6c4l5vbABAoGBAJ0EOHggaZM2SFACEa4Li7z/oszSTeuH5elU +sgqjjI1lN+NvtVNV3uf7+rGAmK8owHuhu0jXdDtCdU/Z1J/LZcmFAKE9R1mtyhaI +aYUdKXetmj+vf9sZD+p5mokfGX4vhK7aCAoFLte5HxFUGKjrNmRXZFM/zBW/kUeD +wKLZlazLAoGBAJXC3C/NiCxyxPuSAQ3/0CA3cN6rqul7Z+buPi5VRyXJTljB+4pp +4FUSFNtXSssU61cBvxXu2J7VBp3PkFOHrAzRZWIdq2loLE2t6Ma20Mj2quT/0M26 +fEK1BVt3jMFt2veQo0EZ7HMTvtSDeoKgkBnvcFvmXhjKOaqZYbSVEY7r +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_PRIVATE_CA_CERT_AND_KEY_5: Tuple[bytes, bytes] = (SSL_TEST_PRIVATE_CA_CRT, SSL_TEST_PRIVATE_CA_KEY) + +SSL_TEST_NODE_CERTS_AND_KEYS_5: Dict[str, Dict[str, Dict[str, bytes]]] = { + "full_node": { + "private": {"crt": SSL_TEST_FULLNODE_PRIVATE_CRT, "key": SSL_TEST_FULLNODE_PRIVATE_KEY}, + "public": {"crt": SSL_TEST_FULLNODE_PUBLIC_CRT, "key": SSL_TEST_FULLNODE_PUBLIC_KEY}, + }, + "wallet": { + "private": {"crt": SSL_TEST_WALLET_PRIVATE_CRT, "key": SSL_TEST_WALLET_PRIVATE_KEY}, + "public": {"crt": SSL_TEST_WALLET_PUBLIC_CRT, "key": SSL_TEST_WALLET_PUBLIC_KEY}, + }, + "farmer": { + "private": {"crt": SSL_TEST_FARMER_PRIVATE_CRT, "key": SSL_TEST_FARMER_PRIVATE_KEY}, + "public": {"crt": SSL_TEST_FARMER_PUBLIC_CRT, "key": SSL_TEST_FARMER_PUBLIC_KEY}, + }, + "harvester": { + "private": {"crt": SSL_TEST_HARVESTER_PRIVATE_CRT, "key": SSL_TEST_HARVESTER_PRIVATE_KEY}, + }, + "timelord": { + "private": {"crt": SSL_TEST_TIMELORD_PRIVATE_CRT, "key": SSL_TEST_TIMELORD_PRIVATE_KEY}, + "public": {"crt": SSL_TEST_TIMELORD_PUBLIC_CRT, "key": SSL_TEST_TIMELORD_PUBLIC_KEY}, + }, + "crawler": { + "private": {"crt": SSL_TEST_CRAWLER_PRIVATE_CRT, "key": SSL_TEST_CRAWLER_PRIVATE_KEY}, + }, + "daemon": { + "private": {"crt": SSL_TEST_DAEMON_PRIVATE_CRT, "key": SSL_TEST_DAEMON_PRIVATE_KEY}, + }, + "introducer": { + "public": {"crt": SSL_TEST_INTRODUCER_PUBLIC_CRT, "key": SSL_TEST_INTRODUCER_PUBLIC_KEY}, + }, +} diff --git a/tests/util/ssl_certs_6.py b/tests/util/ssl_certs_6.py new file mode 100644 index 000000000000..3cb5205c1400 --- /dev/null +++ b/tests/util/ssl_certs_6.py @@ -0,0 +1,684 @@ +from typing import Dict, Tuple + +SSL_TEST_PRIVATE_CA_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDKTCCAhGgAwIBAgIUD5VUdvJQlRhGJg2WJ+/K8I2sYZcwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMB4XDTIyMDMyMzE3MjkyN1oXDTMyMDMy +MDE3MjkyN1owRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8G +A1UECwwYT3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA47sS3nsWACyhnX2pvqdSMSfDWyeF1t+kmVvap2hk22yA +oAM56XCtHifhP8Zu8nI8uumP7Bnh8sW4yqE1Tl/Y5gkYD57r4ZH9o5lj1FvnZnoJ +lERpCpgeR9Lpw6xqfoPt+9a/KR6mQ93AwqH6tyqSicl+7TTNp8UAdAN/oFnymQ3T +VcBXV30YgiX3GIYNvcV0f+hglgA3BsprZLJRuaCv9fLT8PlhCqrFyJ67XflT98nE +0pyaGIAEcvPCPQstAkcc9nnhmARkoAq7/3dsOWRr31paoWAnrSzXJtb+NWomlNEQ +ZADbNd68TIV40H6bcN45ff1nOfC78NJkXIjwdnFmuwIDAQABoxMwETAPBgNVHRMB +Af8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAnl8bxBpQL41lIPB/o1Fe629IK +pFpJDjLC8xyUZrcoZigEYZ+O06Kbc8W52n3awRlLgSMjPUjwb3JExhPW1JcL2DAJ +ARoiu5XDCoTVAADeUAyf60YfARlu9y+hRDUSgYrUknuvJ07fTobJ4JbpMvP/mI9T +ILNZNJZVcaSD4ectURKYkveGtm5R9F3LIO0gscEFgWkhrPeLjiU6Hm1uuZgjRW8C +o57vHwW4yfmiWaXb3dwIjh8SBMgv/g9iDZskIkTCfRBEp8jM4ZlCAII0sNejSLkG +NjUDOtD3AYD46mLU9uOUkv/ecDn13usCaJSnQ7RfO7S/yGZQEcNw+DHHQXbS +-----END CERTIFICATE----- +""" + +SSL_TEST_PRIVATE_CA_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpgIBAAKCAQEA47sS3nsWACyhnX2pvqdSMSfDWyeF1t+kmVvap2hk22yAoAM5 +6XCtHifhP8Zu8nI8uumP7Bnh8sW4yqE1Tl/Y5gkYD57r4ZH9o5lj1FvnZnoJlERp +CpgeR9Lpw6xqfoPt+9a/KR6mQ93AwqH6tyqSicl+7TTNp8UAdAN/oFnymQ3TVcBX +V30YgiX3GIYNvcV0f+hglgA3BsprZLJRuaCv9fLT8PlhCqrFyJ67XflT98nE0pya +GIAEcvPCPQstAkcc9nnhmARkoAq7/3dsOWRr31paoWAnrSzXJtb+NWomlNEQZADb +Nd68TIV40H6bcN45ff1nOfC78NJkXIjwdnFmuwIDAQABAoIBAQDbXQOTDaumpVdU +loyhGxngQbY7ozmYPruWAlYANl4Yw06vVAw/4orP5ohiX2nV3tbDEWLSgigVLAKS +JXFsuoT7KT1lJqd/FJrnMUVpdNHU868whClOzDM26mLFWtsGXV0r3rMrD9wGuaAJ +m/Ae6kJXuaGrKtcDSY/jMM+KjbpTVJ0iWVS2yQtOQffgOc2l9SXi7EwxIccBGrbF +H+a+JOZIbLsPMXwCzo1i0PMeAkG/FneqkCG7BEc2RWeQl6nzkoxANydlLLwnJmzG +Xd+UPAJo2TTRlkXpK06Jb/+wsPSpEq3Sim9IQQMHp8kxtFSjgr2sDG28g0SRfV/K +3RMX8A3xAoGBAP8qXGX8QNl4IbGJl786lk5cCzVlEzmCSgPcKVv7V6hPWknRUInO +rB4UbI66hFAK3SGJrs6+gyj/1GB4mM88TSdGJaifZnt4dekA3NZdZbrMINoIcS64 +KFGpFyFlj81zxwXyEDitchwYrkMEQ8Y+wO20lepCPoEuMnhbJ+kH22hTAoGBAOR5 +viyu7m1DU9qv5ZWhFoS1+tDT3g3zHsKSrrKz5sE07tPz5t9FQztKfflmv9jCurDq +C+Dro00v+WnxUQSuDYXNk9JXU0Ly+Z/Tt7S3AEZQkarjya/jkDECsEQJuuHtOo/C +Lc/req4PeG3IHqoAIwJR2lLoSmA1/bFkBqzwvZr5AoGBAJ75Rm3n1oNm/B7/aYKj +vsd9QyJ2IZ8f1HtJLJ95HajoH4mEFlh1Yfivot7dx9eSnSjq/fUi7taZZTjhidr7 +Z9pDMu02uoPMjjGn7u7Da8EQspsEzXddFwmhfp65Bvnq2yGSKZcWQNTOb6ujOZKG +wG6Ypxo3QXf8T0d7C4d89K6vAoGBAKbwxaJEl3ZCsayixbAVKi0CAyg/BotOz3UA +VKHCjaMymCdKmbEeHMff4DgxeZBaiKrn+JH52zJ1EVr1tmsi+kKAObiBkhbjDr5X +pm1nIc4+5JDnK+FW9264Ni8gpOqbuiR8j5jSyKsSJr6gcLdqrintwQa/MEkoahwJ +qE8JAKGRAoGBALTTp3skePMl/wCpdzvOqNMvJzoDV9xX1ttPCKF0pNOKMmFdSRib +8JN6dpRuQ5CugLYqNQgMExxgpytduGVRsALp/hKNo7MYCmLRsRAcSIe6YpgvnVYw +y+ufj7QhG3hCPBMCAC2YHLTAAAkzdiookyhTmtfWtSZJuZlUYgLsMQ9g +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_FULLNODE_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUI/qIR41vNfZrImHDASkbd5CdhdkwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyN1oYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQClBLYsg7P6WBNexxAiF04sCZN6saN+dI0Q/KAN1Rp2jfTX +nApvuNImoc2fozCViausJmEWKtx7RHQheDJmx+0LSpQ0fgdJJSNK5UVRme/pgVyE +/FACdD+fBuiasX1LoaBUqk7mpfeyQpfwKW5D1q3HgDK5ROmiaMe5VnMD0BvW/QYc +1T3TDuBLXYZpAg7c+fdqmBKx4wnE1v9PqChdUd2AQF2INdQuz3QOHGAQ+cJsXqvy +cwep0LvlK9/j9dy08qKFvc40EZwsYqOh0HT6KzUupVh80qhZxOz3QSok6gNhGpnI +JHOFvJ+i2P48Wb05dCiqBqYkEnNQ252b1l7o+grjAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQBYYc7lM9pM40R/SUMzhI4C +D59d0VMwKjTqXRy2ifrcZYVxmc9CAldj+nk04piXC+YxUihsIRSOxppd2I72lbo7 +bCRPxj0rYK/iNBlkzFwvVCQ2e/chcKcFOEcXzEpKGzVpL2GyLv/NpxH87hLLJwSG +FPGsMm5MznnMDuCX1ALedLFKyvMoy8SSg/+z7TNiblJxEabxgEADpC/pbkLnJvlv +qQBTPZk9LD02ZXivI624ltlQt92tqIUFZBfQXKe6WDjz2IzeeaYSlhTBKvqzXXXg +aUXnGR1lA3Q7dBaCPSNkUimOm0qT6t81DLkvoSyQu3P0OvKFCmZNLxDbxEO5LBAK +-----END CERTIFICATE----- +""" + +SSL_TEST_FULLNODE_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEApQS2LIOz+lgTXscQIhdOLAmTerGjfnSNEPygDdUado3015wK +b7jSJqHNn6MwlYmrrCZhFirce0R0IXgyZsftC0qUNH4HSSUjSuVFUZnv6YFchPxQ +AnQ/nwbomrF9S6GgVKpO5qX3skKX8CluQ9atx4AyuUTpomjHuVZzA9Ab1v0GHNU9 +0w7gS12GaQIO3Pn3apgSseMJxNb/T6goXVHdgEBdiDXULs90DhxgEPnCbF6r8nMH +qdC75Svf4/XctPKihb3ONBGcLGKjodB0+is1LqVYfNKoWcTs90EqJOoDYRqZyCRz +hbyfotj+PFm9OXQoqgamJBJzUNudm9Ze6PoK4wIDAQABAoIBAGKIOa7CxUiKJqbE +/eEdQVQSM7ljMFhlh4XJEliwEikQAk5rod+r3++pVXoomu3vUk5sbDQsS5JfAWiq +uI2eeu/vDCd7ySXnwvcJhyZ+YD5xO6k8bxnIB+UQ3vrfWA086NslBg+6sfgCw25n +jqt7GDCzX5Y4i7iqD/eeovZ34OexH+J6dE0rtlO6hblbnrk8jgJJU/CsK+fPybky +U/Dpls6AMEI/s/6sC7Ajieq4HN8lJpnOS03LD7Xo5XQRzHDHzKfryFP6y/Ng0lGn +H9WD1kRWtvQ2JAct9V2VTgNUFcdVGICqn01uiob+3jI/LyfWylMqhoPOJv/K7i3B +pmi+FIECgYEA0WIeWEAREuLvs2sFFdxkUbfzJpEVrPiVy57csc7+q/5db6fFd4nR +Arcr8VoFQcVbEzMLXcJQzV8lXJUacPetjwNh+91NUdM6dFRotG+AYc9Gb0wd/YMg +5PCyqwWQfRXHj7OLB2iQ7jI2oZSi+MSNLBuNZiPoP5D1qSvyJlY43JMCgYEAycH/ +/dWP8/ukyxcZuwf+qeseZ7StCprTXY1+eEm6ov8okegvUfPWXUSFp3vu7nUf+jQb +j3KyemkUcDmxNXNKsWGyxpdUG/2kmg0Mktky+ndVhXIKJ/bkiBFfZagxnIO5MyBU +BoD9Kr55dCw4BwIDQoQ8h5SjBuKcQNe6Atx3WnECgYEAprJHFltrPG4QB/ecBNuB +ws2Ad6Pqbds65vGDIsx5VNUd7lbTj2fZTyAli/DvXn2RFKf+1JZvXhHx3NOYSXfm +ZYV77NkzRYG1eAf7lCtxOm2a1eDzv3E2Lw6yMBYuAmfkiTOX7D4DpJ+1fs6nMQbK +neMRn8/Wh0URT639Cyh5/KECgYEAl1DorXd+f348GhU5NwzH8qhkJWNcZJe57nAw +agbh811rEAMTemNmYo0BsJtCl5VAZmauROfA9A3bRk/+mBff7SlY65iTfGq/CMrA +o2JC2ZamjW76Lv78ZxCge52pl7kzLSUR387eETDJ5ldDrf3UdA65VvYjTwamKfuh +hoPTAAECgYBp0yhXkzMrGtsTfiiKSWAGe0HSakvz8H3bQDuQnUw67iEH6FbMhRtZ +Pu/QTafLpAMKrtbDMuls+2pxf2BN294naZyB+fI0zAmefXtTjx23hxGoFnh3FEVx +eMX8OdBvkvk/Ifbfa8PqD9/bTQTcez+RkCnzANn04+h/tUCzJExfhA== +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_FULLNODE_PUBLIC_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUBJH2JfwoCndkFqwVE6FeUMjDVx4wDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyOFoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQClPnyntGjxicj7xn5m1Ujkgid5yz+/d/g6kdRTJt0Faeiz +oqm+9o0YUsLeKWSjOhjgcKGPh542JPzYqTaV531R8gIYJEDs9hGR3dbjmeu5RzVd +Ycv3Ru9BInMudjH5GeelUos0nT3OUJlb75QcNB2Q3miP2SfPcHNtGHa72VXCP7mN +zmiadi6gEJyPz32eBAh64CPoV177/sJqqlrFERmo+LyUgNNiTpFqwbKYIW2dCozG +3V/8dz25xQVvOMbVG7GnmRHXr6M6wTOefqsciOiGpPyfsTGfx++W+poGHrT1xqCL +IQNUVJZ+jip0BkWheR//bwe3RqCR/fCpbsd8CwTFAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQAP3lZ7n12+Qq96lT5Q28/m +rldsTbJguDNPxOQYx8rvZO21Bf3YjJZmqAFM0H6wBgQf7d0CBru+3NxlwZs9RiLf +bpThwzl+oWWVw9dxyYmcBl6RXf9DvisbIxKvQloUWjPAGfRS64qyOqjjFwBc5dlc +r9AOzgsN1pM7f52o5Ks5frq0vfO20lVbkvFb9TfzcuzRh0xNROlrAVK2q3XEaEab +nBTNTYt5kq8KYU+s0jstlPFF9gGx+NOXKfZmdKtOfribuPJIQ5ADvxGDaWEmQHhF +PlgF7Y0kmP/vUknuY8rwMrGi+cncaafO+GIiaBXDGxa92SNWe2J7RtRWDC5NF1en +-----END CERTIFICATE----- +""" + +SSL_TEST_FULLNODE_PUBLIC_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEApT58p7Ro8YnI+8Z+ZtVI5IInecs/v3f4OpHUUybdBWnos6Kp +vvaNGFLC3ilkozoY4HChj4eeNiT82Kk2led9UfICGCRA7PYRkd3W45nruUc1XWHL +90bvQSJzLnYx+RnnpVKLNJ09zlCZW++UHDQdkN5oj9knz3BzbRh2u9lVwj+5jc5o +mnYuoBCcj899ngQIeuAj6Fde+/7CaqpaxREZqPi8lIDTYk6RasGymCFtnQqMxt1f +/Hc9ucUFbzjG1Ruxp5kR16+jOsEznn6rHIjohqT8n7Exn8fvlvqaBh609cagiyED +VFSWfo4qdAZFoXkf/28Ht0agkf3wqW7HfAsExQIDAQABAoIBAQCEiL3KdLTHJdAB +wI6ZQ/AW9x1aXl5e34ZE07oMh1s2gF+X1Jt5Ap7gJ2EMdv60vGA/g5bRW+DVZI03 +6WBSkzy8gPKVEl7Qr/wflmJIYqfiDu8KWuoRBt/Wyh6/4STruo9E1hO4u3VbCOI4 +DswoYtRj4T7UQUPg6Txq//gbapGSHefwrZ5hH9A/YB01ouVafnSxxh33JqodIjqe +1uL1LInxF+ghmH8hlifP2xmD6O0Y9Y7kq+6715gLmz+UToGvLwrEiJoN5JLfbCnI +lw5z+1LDzlBfS2jlyZTg7nUQrJtTnlcg5oQdaaM5nDo+R2Cl/kU7/2Rir0sKIefF +Y32lvkfxAoGBANlyjRYTLuvsi5yQo+yrMspDiWHrIkJZgl8Zp7DAWPz6BwsOqLFw +EuBBE+OtTYokLDKsprrWPLNKafXAQrRdV3WfQuEvM5WMUK3qMGb3UX1L6S7iMa3u +46DPbk+TYkljLH82mvTmg595g9UyTJIEodfWzkNT4R69TtgtHb5kzR+bAoGBAMKK +iughFvrb9dnEOFnFgWTxfFt+SRzZmJQDhu50dvjKmSB1uvxHfswdX0sJINVqylOB +x74bCQVERGOrEwfcp7lqGHEvxHXsoKxo0A2pYvIPz8pSvkMHFU8ZMjhpukb8eSeq +SmGOB4I3FvoXxl6XQVIfNzKaCtWIpRyyaz3+xiMfAoGASdPNuR/8P1fJsvec4F7j +2K6Dd35o3FQdooJIYAd+by3ItGVeuxfNINyXjyao++z6IJ5j4W2ZoFn0jd6gBzLl +3eabw27OOckxH0sy8dKolTzvx94+dcp0+IVU86mbpkUIt4xnohydtciBDSKNwsnV +1T12PH+/IDuwCE+RGLKKvl8CgYAn7mAMvZ4F59hS1p/qbUvd+GNaNFW1gkR7PS4Z +C/bPomZD72ZuGg7AvtashDclF0JoWZ8yOkJjvlt7ScYiOvyCZnCV6wQcYHGqceki +pL4WiYBROyKqh22sSrOwOKNvpqsHpUCXPZv08nHY68Bf8hQKUzX1QQEwS1f4x/aT +RxRMaQKBgHfwK+DSGjtojrwo10CKYQ94ZDwPnviuIPcZfNQ+X/KxRY+wCl2ft3iC +H23dOdNZ52NDlRVXafCCI7FFhy50/jojJ4MupVX3pTQ3oDiHkii0eHt4NwgAzqm4 +Um7BAuOB20ogIzQd5Hozd2eQTI0AFs4Zd3yBxTgG2B6BHPSwvxjd +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_WALLET_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUT34idpoOlGXOYcUny2DMZVL5T4owDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyOFoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDCzGdbpwHSb2Cr1yhXKPZ2aCWtHEksIl7ao2UCDO+ljCb0 +n6PxHAbgVHKJ0vy7mC6Y1VXdstr4FS1Ak22V+4ceo1UW6Tsls4I9RunAjkfAl9J6 +CzKCVr31L6GvWhIjgQBkxswmYK9AFx3WbOQfz7kN46usBX3YgAWiUwBseOS8KBuh +EWfguDqxL8yyPHf6v5kWy2QyfwUBITuLQE6AanbLrkoT5bRrE+06X7+PeKidHzXx +b72fSvuJ7ERbNPiHmmVoila6kb3PegcKnKsm7d6Blg9MwCeOcvTXRrXsCFHlJnlZ +YWczAKAvtd/OJeTEC4de4UTuuyuDm2+00FUjVKITAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQAgc1DcbsLsxrKHZEoIENxp +2pB5LM2Kq284Pll7Ix5x7OH8JRenLLZCGMLK+AeW25UchHZ8iagHYR6tQrc+7CLI +piE6YKchJu340c3RONXcxDZUKqXBxalUyVcb3z8I/syiqsvDrE94hnGfp4X9Yg/M +7EIdv+I4y41qTxMD4HvVTcXVU5gapEC2mpOQ7mfN/rnIU1Gv6kpwG1FrnUyH+aP/ +2lpVSke8eSY59W4hThqZNf5cSGw/t95CBHigePoTqoD5MfmGhhrwGrjr9JHXVMpX +IUNgsbuPb6N5J7a63Ilgr+26X2ZC2zco7Ff5zKz1xKPMvLQ4tGBjm4rwQ1r7F4jH +-----END CERTIFICATE----- +""" + +SSL_TEST_WALLET_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAwsxnW6cB0m9gq9coVyj2dmglrRxJLCJe2qNlAgzvpYwm9J+j +8RwG4FRyidL8u5gumNVV3bLa+BUtQJNtlfuHHqNVFuk7JbOCPUbpwI5HwJfSegsy +gla99S+hr1oSI4EAZMbMJmCvQBcd1mzkH8+5DeOrrAV92IAFolMAbHjkvCgboRFn +4Lg6sS/Msjx3+r+ZFstkMn8FASE7i0BOgGp2y65KE+W0axPtOl+/j3ionR818W+9 +n0r7iexEWzT4h5plaIpWupG9z3oHCpyrJu3egZYPTMAnjnL010a17AhR5SZ5WWFn +MwCgL7XfziXkxAuHXuFE7rsrg5tvtNBVI1SiEwIDAQABAoIBAQCnQns85xlZZujK +o+OvcyysXqB7E16A1sI7WTDRoenja4PHZ4uM61O8KsZuMQtwyCq6b/NaeMgrW4OP +fwcJUP+j/vqAwaJXrNqXYtwyyfgyFipTQGoOIAzbChr6RYxtj8aWwVtpWHshvLeR +9c2qwuFSW7p76fs0ejhcOIiUmvlL1GTF8fWvZ8PibxcMEHbNaclx58rpWdBKOZpI +WXQ593AOGwufWX8EJFSdv+zjJs+JIIrVVOqLUMhww3B9SKRkYvMGABTRFY53U6Ru +ixR5jBRCrSdot5S7YIktZ+BoyZYDl4LNzcEsWv8KDqD35RbjRUXH6pPRD5xaTucZ +nKCGCGUBAoGBAOUyXq7KRqkrDlXpgvbkfI99ESS9rHCBa88SLjIKIdGszE1AIUVy +xHjXH/eYWQQ1u9ghqEgU0uyPcDbGwR2DiKnV6M9Y1RqOxgMVe7b02SjeIzqp+6zw +ZeqJsaaUPaRQFOG0gKSjpxA3FrQZO2HRnnUIZT7jmXXsCSMxcsYmuIVpAoGBANmU +Og7mag5iTgpsP38O+vHkCi9dXOif+20xI04Wuyt26EapPC3oNW3TwsDkCTX7ljpo +yI0yRQQgNUvPu/trsBaqDhTOXTaFCvH3+EA9SbXKKoFD7XiQ2iPJ5s/wv9aAsu/p +zpE8UI619RWx1NNp9qzPPYRaT5QQWbw0vG4ASBAbAoGAG/XQHSyqABszjYGGK45m +L68HN184OSVgvIswPYSE6apV5hqrtGgy1bcQvEjGmz/e4ZgBioGX1aoN+3C1wF76 +fhWNaDYjETFXsKqfRAuMrnKjA5YXENrm4/zWQkcVWgU29u3pP6yDRfQifegSGXXU +CdzzVP+5Cey4lKWEpe21VvkCgYBQscq+j/Ixl3/+GZuLEhE7+SlV27iDhiIHozvG +0GNmgVkwK7/n/sEiFHGcfHZ2EPLsgsQuqcAzAYcCsO61FoFeRn+mc8raV4lxqFfn +61MVGv+cpYbDOmXM9CqoYE+lzGGQ3RQBhW8THqThAO0/44LbIJleJuhwr4e4Z46i +9XPElQKBgQDZDFu6mSo2MdYpEQucOgJAaLrwyUPCD9Ja/Gd+Mdb+2AC1kovp6v6u +xg+COGdQaLddU1yWbxB39lKC42yi7AKDb24gCBRmyj5lsgaipnlQkGtNNK10MWg2 +ff90qGZpBO6tE4L/GMNsEcvQxxFGkbrTnU08Cp9mz8zJr47rlRoRig== +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_WALLET_PUBLIC_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUUFx52dcZRD27Ek1S8a4FJOg5WTwwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyOFoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQCvCRPBz1Wjq7GEdOtWDZUp0BTB4pTIuXP2F7bdAwPTJ7Pd +T6mjIPxM85E5sJL0pHvF49BZSFPm3OcrVv/4CHSZ9OV9aDGeioHZj5DdFZJDFv21 +0E02uZSDZBJ6b22zbA0dqq/wWm/s6J6dbypcW4895QeH/OqMU//A4dFPeDJzBf0b +1BVjt/1QsFoEymb2nPRd0Pi05ROhuEaM4QuNSmncaFucLnDnn2UE3OB/O5K8zfyG +hsXxJx3RwS+0SLAIbzde9GxQUbU3SS0CFHN9J+UbeOX+fIBOvPrQsWZwRdOveTFS +tcqiEe0InWYXIIsfBxfdI0JNrsSrKlwMAhITg7yfAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQAuBrB0tzL1s5Eaxv04bVeY +s2bktuk6zFw87kN9mJBEAQSkVAzx/5DfL/XT4uFhls3VyLOU2yMu7Vhxm2ZuMVXk +QdnhZ43T87lBnDBbZHd1NkrCY8UBqePB6cgA4JpmR1R8ypZmT6/V+POjqKGGwxJu +sxjF0f5VB78yJlPEoAnzYiqZUitZ1Lptq8K4ICORtN2yWG+dsq8Cv/dIvMOvEMpy +O6ozbk1qY0EaRuZAl7S/xghUq3QQOFZLQIkNGSJ5Q5xN1lqPJiDBinAjSb/r/zYn +edxjLvMjHDSLEfzyD9g1nmNq9z/8QsiYdc0mVMkkuvhXGDdCMOFmnZ0Zc3nPdvQ7 +-----END CERTIFICATE----- +""" + +SSL_TEST_WALLET_PUBLIC_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEArwkTwc9Vo6uxhHTrVg2VKdAUweKUyLlz9he23QMD0yez3U+p +oyD8TPORObCS9KR7xePQWUhT5tznK1b/+Ah0mfTlfWgxnoqB2Y+Q3RWSQxb9tdBN +NrmUg2QSem9ts2wNHaqv8Fpv7OienW8qXFuPPeUHh/zqjFP/wOHRT3gycwX9G9QV +Y7f9ULBaBMpm9pz0XdD4tOUTobhGjOELjUpp3GhbnC5w559lBNzgfzuSvM38hobF +8Scd0cEvtEiwCG83XvRsUFG1N0ktAhRzfSflG3jl/nyATrz60LFmcEXTr3kxUrXK +ohHtCJ1mFyCLHwcX3SNCTa7EqypcDAISE4O8nwIDAQABAoIBABWYkx14jWJyTqa/ +HkIxztOn3sfFQwI5D3uvlVpTgudMoL6Pk6iS6047RxHNHXSdzr+tygkaCXAUlVkd +lNtvf6N3OdFtR6H+CONk+USt4qvwbEFAgCZeY1qKwFTm5qwaisu8QZTwM5sRYHCg +RKO8kEHcvj8jD3Vc79NGiWK6u87BHAUp84GLLgyuju1aNaZ8n0NM+P6wEWxlduFU +l/yNoSJkQXAs8okIIA1yR1BGoCcgzXJnGY+WGglQpmSxWigay7mgg6odLoWGf2jh +eL7AsB389cgGm7xfxDj9cfh3lSnZJCbSDFcD2nl3DlgktrDq2JpndI3IL46hBp4i +rJnVPQECgYEA5EVznienuuGegGAod0Faaj9W+eEngZmikB0JaykRJpR9D9NsXXxM +P5iMAhbTl/HWh1ISXD5M8DOBdNYHHJS1sbxflWrHxR4yHqPboZw3sFUOEp6yyZ1K +5eGfXPEWm4s0w9hytC5YoLp+8DP8wqBdEtYLL/CNKRhukzwYmkYEtkECgYEAxEwm +0u/eYHOcBnnbJTDZwQTzjySAlyVPa2DaLlEtPfYhP93Uy74P7hh4e4yABwVxhO7p +rnES/NFDOMSOe2hsd8E4amknI76C2/xNaRP92we6jU/Rsw2HFbuqapFvPzApY+rm +mtGu/kEG2TKCOqvTUar2azmV8jtFZrGavR8Oet8CgYBQ8Yuw7jDc5Lo2nWJb/1GX +UUR/MOa4Xwk0/wbi99n8CzVZkkff0n6bInWrG/sF6xx+OY0HJpRTrH/gNTCqYtUW +5EwkiIu0CyQMl1//K4zXuyFVWu7c7Sis4VeINnkkyLT6KVtrJvAIdnkipdygCwtV +7Y5j/nGfu+khznKO6fsrwQKBgDcdbpUxVk3ciAVld3t0wN8qJbyeMiC+EBOFKR1y +G3LXRbSp81KVdeGTK1j9NQFDrQD3F9Or2fn0Q5yPy7WotzPUo6N1DNFDb3iW2aFU +RKWYAPAmZimQ9cnLXGj5lrO2MRjl3oO/thObbHHVda1Fi1rV1sR6cbiGndDcsybA +ro8fAoGBAMreL4+KxIzdIBQ36bN70pxqUYREoeT+d3AUFhfFyYaH0U0PFXOf/XtO +kC7k75CYwsLiarYne531tuo6AvY39aDePK6D0uHF4BqI/PZ/rsIKOVAkUhWSNDU2 +b4NkEZRl1ejdxk/PjGR8oH+9S1X4Y1+kzJKpqt/k96Uct2eSxiQM +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_FARMER_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUR15D8Dk0zvppH7mRgS1FK1xRwf8wDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyOFoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQC+v0wEABCQFqugGlVovy1kKU9YYWmPmK431Z2Gm8CECi6Q +GujhMJ4WYp9g/NhGcP7U6uQnuI1Ab0paDkPW4mMhqpfrrFE+dy0q+G/vlJZnD32U +BvAPHHd6z8QBJ3bM9t1mMZMPwi7iCadwQMDHaz+DOLC9EJO1FhsATAw8FLri8lJz +Acvs7LUwbRqoDn+7gR5lFg+kxSkEr/E0rVQZW27n+5nDE7lwU15O4czbczN+z1jA +ELSn/wJOFNyVsrDANALQ7ibHafWzk7T5wWDUyMsOSOd+E3uYz+myOJej9XpfwL2q +ANPkt8sa9XmJwiG2O/O45Lt/6nsjJzQbVuOW6jcHAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQAqb3wJxEyRg3Z4UFzufrmK +WeWqEOCndJj5l3ppvf3yVW2VMoiqVssnfhtQ6LXJUZrl2qrkAYE0LFlWLpr9DQPG +i0Atr3dFH6+6f41pMe4GX2uusR/MdmRoU1mF2TpjxgkS8B5Q2aB23h/q6SR4n4bR +SWO7i3ARFljj9iAMVTOqrpyWbb+wuGWav709BqAXM7ZUYga6ifKaxL7ahr7K7HTY +yXbmisTBUY4yT7RYfvau1GyT93fU7K8cdtXBDHUBpVOWUJFQBHPKVKRivO7DkDTR +sPcphGcNjvGSNqjwGmN4Hzp+WC5/uJahRlhL7Y3+v3pfIeGUnOSatau6UuJpXNl1 +-----END CERTIFICATE----- +""" + +SSL_TEST_FARMER_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAvr9MBAAQkBaroBpVaL8tZClPWGFpj5iuN9WdhpvAhAoukBro +4TCeFmKfYPzYRnD+1OrkJ7iNQG9KWg5D1uJjIaqX66xRPnctKvhv75SWZw99lAbw +Dxx3es/EASd2zPbdZjGTD8Iu4gmncEDAx2s/gziwvRCTtRYbAEwMPBS64vJScwHL +7Oy1MG0aqA5/u4EeZRYPpMUpBK/xNK1UGVtu5/uZwxO5cFNeTuHM23Mzfs9YwBC0 +p/8CThTclbKwwDQC0O4mx2n1s5O0+cFg1MjLDkjnfhN7mM/psjiXo/V6X8C9qgDT +5LfLGvV5icIhtjvzuOS7f+p7Iyc0G1bjluo3BwIDAQABAoIBAQCK62N/XlSxu/EO +z174xJNYaUWiRn/M1xO7ElaBnJmfEJNM3GxS5UedYgJVbpBM7SqLAu9bhmtJTERI +Ri9eJs6vzECMoZkh90XsD7fmMr8/G+cHke3v83mI9hv4AzfgmsIwFVbXmULv2Lwb +Yu2DzzYYaYc+iv4oWosbskcO0sIxWax7UffFAM6fw1SYboL4ubxQrCYJKMvSCPE7 +tDtuvndO1tr3EFrAo84E5KuPFRR1ncaI07FPC2H7tK8qMf8N/ks9CNxA7jmnPS5L +yaIoNtwk4i1rFTVzZY4hPQD5cN/b947XEMiWSZtcKF8W9La2vbjcJk2U6dTQUre2 +4ej1dx1BAoGBAOKrQ8jFjB/eeQ7OnrKsyh+Fk52F0qEOWdnh/OmZ0BVPrt1R4Rtc +hEbYk4Q614AlHApiSP/SnQ0vtBx/C0If46ATRmh76hsmqeH0dK2FHNgih2IYsXdM +T+AiZDzhTrNdSiZu6ZiegeQeNUhx2dbJfLIaEbP0l/A9xPmrrjxaZCRxAoGBANdu +EqG/VTxxnqLNEsIWfPT4SVTXAfOHOMyxFvMJe8Nrqe7yAuIfybw/JNJHE8J7grvB +0aZLYwDTo6kmQW7cviNIkcc7TJCUczJjqlu0XxEIcu70J+1cel2Ekh/2l6qyUG4y +WaputvX2pgWcFiyBhIG0SGfjPn7Izzmk/th6Cu73AoGAWDkYtPhOxcitjZnWIu0l +7obrIZCInq8bQba33aREXUcaZIIN/7xOr9Rz9i+A2RuYgmImz4uGHS/IBp9cHmyT +CCb4bIpuYAr4bD3de/wncehekUvTJv/IxxAbu6ZmH2HcDoF04jYvkD5BtuS5SOQ7 +aIbFBSnaSWOBWzv7IPp9p8ECgYEAmu/BWq8fAE+/BdtzqD9AM3b2Vpwj/R+9jAm5 +NaYWat/d4coQpMyz7D2Fn+4amf72cU7eFzmUxJ1x0U+AM9lyrjGdFqrhUAJUL2Na +vIWm8bsf3hefN6kRIH0eEvro7Y09J2Py9Q6Xa6KumM4/bFbiE96zTHQ1GD4YO58c +4lgz0NUCgYEA3vVVE9JMVlN/KXW0rrw5WHCX3PpYhs3ohvXinur1vHSNdYi+thHp +QoyG/jA2t1jVQlrsbe245QNXzoh+2mZKtzEukY2B7ivoszSF0k5P8CK52efof6Ss +nejt8xMdtQlTxcFx2EOnsvPjn3nmMMuye4bUsZPprPqNrDtzkzJVccQ= +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_FARMER_PUBLIC_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUbNfOeBL5Eg+Tt37NytSIPVDVDKowDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyOFoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDdwdMrIMRVxTYSO0s1krlXy/sAk/BYSKWEUIe6Htzc2GGn +gAVNdvOiVsXc+T213Euw7yvBb0pfRUmS5f1O+uWZespgDTsZUy0SJYOuHhGFmI9I +UelOX6Oy8fRDWVz8lovFKRcr2ytywXTAb1mTGad7VHwPW3DlnE/xeAfmLPjENQxy +qiUjxqgqK1U+mdhxIxMg5hScsFjWdZDvngKzH0bojgicnKwWAymG7eJDd31TFQ3d +Cs0SNPzXmNAOKCRkiAMgaln3hG3FXASQbl09JvEdYPpxcJXUePxBIXh6D8KhkAGS +LoFq4uXSw4cm1fMO/TGHtZgVOFS9+EprA8L2KSjpAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQC3ZKaUzSnUlaeNCdUvyRL4 +RYVbzr5RuUPL085/JEtSpXUF7cZPMbpvpw/EWIUp+EPVG8h4/89F4L67MmHwCxgU +VwL2N87lk0b+TpOkLO93mO8gIGy7jJuIvDGqhWS13ocKI0Mm5Gl7n3zPS7pbqj2Z +F82nU6532WK16UBiYBYa4k+ZG4puYXpJc33/M8eiUQK5eY3sUL//OTJVz6WO8vCd +QVUn3+FwKZSJIaMaosKgb1SdHSqeti9RDNdQ44k6bfklZoz/NLOXi3bGUOt7RUp+ +PEWLB7UnsI5qUY8aUU6g7PE3qt96dTy/9c+NTcpEnHj20wykofuPkaZOgDNgx51X +-----END CERTIFICATE----- +""" + +SSL_TEST_FARMER_PUBLIC_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA3cHTKyDEVcU2EjtLNZK5V8v7AJPwWEilhFCHuh7c3Nhhp4AF +TXbzolbF3Pk9tdxLsO8rwW9KX0VJkuX9TvrlmXrKYA07GVMtEiWDrh4RhZiPSFHp +Tl+jsvH0Q1lc/JaLxSkXK9srcsF0wG9Zkxmne1R8D1tw5ZxP8XgH5iz4xDUMcqol +I8aoKitVPpnYcSMTIOYUnLBY1nWQ754Csx9G6I4InJysFgMphu3iQ3d9UxUN3QrN +EjT815jQDigkZIgDIGpZ94RtxVwEkG5dPSbxHWD6cXCV1Hj8QSF4eg/CoZABki6B +auLl0sOHJtXzDv0xh7WYFThUvfhKawPC9iko6QIDAQABAoIBAFMzntRGpgN8S85J +Wu2N0GaFjPZpizrEfv2G7XXCkKF3uiQLX3HyGHUnU3OWPDYYonmPMv6Pj/rw1yr7 +ia/xaOXN8VJahHr6/yUY2DAi7fYPCGtc+ElEjvnb2AbQ55eJsIVX5m64+7NBrFlr +LdOIQ5N9XlKwv0oW/NOfcm9FHk0W23qsFG6+lsPnGuCCvzJrzz2OxFsGhRD66e0L +3Zv4xMjxIKV2nHyI8Ca/rWgQnYJ465TWMbz8qfIcH7FKK2T7/poajaOBqiVQ0FvQ +JxU5LPoCDfqN1IQxt354KhOEy33a8vOlt6NkbN+0BUdB6Mw5JPTg1I6EVMKwZMlr +lN98wbUCgYEA7u4p8+WM16VoUWmwu+4C/GNziQdg7sDFD5sALARgaAle8ql+/pU2 +v4PbgY0H7AQ4YdLQkMG9k/JyitL1uIcdt9I3NErWahFZsxu6uMa6loqERW1/1JZa ++yJbDQk0s3P4XxJ16XEB9RHVpjM4K+70I2Hj6wBZ/KCABPJ9+lMkGGsCgYEA7ZmT +2Pf7RPZZUBDx/BcRXvgFQp0UBzcUCW+gEfcmuXUFsfxpEML3p6jkYTG5exmiu74V +JSMfJQbxgjx/8jV6qjEWrOI19ONmrLwAn6YSi4kTSFOzy/nunbO90yZS1EPWHTgt +1KwIpVV3YQwx8unAj3w+ioNKW2pNdKqgruD9qPsCgYEAoRnrJa2oupqn7QUDiqOg +VcdE1qcn/7Pw62XKxuMokd2HrL0GiOfZY2an8gFCzyvFTIhV4tUPW5tFCj/WONa5 +/gD7AgJX8mrCfZb73qz4kJrTNvDvGUtMCzsOA4sqgrl2RbxvzrhI5pJg6VBt1omn +CdhuUj0Jb+JUj4Fl2gyHMQMCgYBbKzHS8BxnMsvEcCwaGdPdh/ekvtcfiZ/TmxRl +sFsYkHGWAQ9jkhwrJiAztzu/o2+Q8Hf2nwplT8u9uG8zxheCVIGp5sbjZ5pPL3hh +yfyVtAxFFXEJFKbdtklhSvQ/ZKQ05AiZ8UUEA85h26cS2WS86eSpi4UtnJWJ7aro +T4alnwKBgGQfkEQvkthUo1xwkYQEgbJzk5aunQ3aZ0467L/kkEJZ2SknAXKLURin +uErwhm3wgKHMCy3p5SLU7uv1BihG6nNvLuskrD6mAkUPBMJmXCX1NEIBLwcMM4YH +H7IipX9XJOjCnauv9AsChOkz9pgG4/0BaaxkQ/Cm/zKm+wUXIfUx +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_HARVESTER_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUZGh0kY4W3yL0nzJVIYYmCTQM0LwwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyOFoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDVy8kOTXSTWhLK3BILK95b5/tY1dq7ARqrv4tVDWcXuyGV +vPPNj2BibF3Iv+4Gr/6XeZEWmH2ybbEKrAws9aBIw7ETQhTNjoBlEvkoKvISBkkR +KaQ3lUQ0Km0BONsdSwxkgcgzn61TeSOlWgpOuW8EOXZNSEUJTxp82GbbdVH1TAXH +28oxUvrV/hPltxuQJ+3tcqN78VD2cGPwp4+RNYwklG04Jcj97wjv6v41jVrQ8IR2 +k5GYuI8f9V6y1zw45A4XasZDb1/UmKk5cROSI/nJgOqZLmAhjK6eQMx1XqPYm2B/ +e5J7V2nrOVhsasddwNH8fx+QhBz8cZwd7uK17oWdAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQCpby2yro0LmC4xuhhuqs8q +pn4r6E5i5QTDHa5uVZ/GtNgYDP7x5MsSaWcVFHsq5TONiQY8rFg3iTMfoQTqvVqg ++4Eyopx/b7a8SryImxJXNVCNnY2GZ9DQIojYxDqiqsqDpIlgeR7FRCrjdy0W+5h3 +Icbugg7sDeLYnerD9RAQciuClMC8jgAUc8LWXvI715kxTbrr2//8wu0YpfBLHln7 +zL3zdyKcaAnqvbBx1FHgj4YI6ozfdpqYX88Uj4aeirCzHzT3QQGoAHXoDLFdg2yX +s44rft3s9/+MKiFdcvaxVkh5UIajc2q2ZZJ/mqdRtW/paO9cv+aNSdgPoRcn9hzb +-----END CERTIFICATE----- +""" + +SSL_TEST_HARVESTER_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA1cvJDk10k1oSytwSCyveW+f7WNXauwEaq7+LVQ1nF7shlbzz +zY9gYmxdyL/uBq/+l3mRFph9sm2xCqwMLPWgSMOxE0IUzY6AZRL5KCryEgZJESmk +N5VENCptATjbHUsMZIHIM5+tU3kjpVoKTrlvBDl2TUhFCU8afNhm23VR9UwFx9vK +MVL61f4T5bcbkCft7XKje/FQ9nBj8KePkTWMJJRtOCXI/e8I7+r+NY1a0PCEdpOR +mLiPH/Vestc8OOQOF2rGQ29f1JipOXETkiP5yYDqmS5gIYyunkDMdV6j2Jtgf3uS +e1dp6zlYbGrHXcDR/H8fkIQc/HGcHe7ite6FnQIDAQABAoIBAGzlaJotX5RyS6Ii +J9qi1JaReZfNDaU0scYLQUfoFLukqD2hybjJL7nSIo7PG8kyOYYj+H++2j9Y2dVq +vqTj02lcq8P4Y6ozzDgvStNn1HbT1sG0KR3anKvJdE4sDIdGI6DJ7hRFX75ltH9T +dm6UyvQm5JgGmIteTEqM2w8rGbzLtsg6SakuNoyGSL17wlc6GmYiO1N383KZaaqR +yfKz0yQDRIzHiGv9bQP53uPNZv0YL0+LJQg8lNGyq8+grwslT89zMw81pnCcWEjJ +Bm3g+97tyIzxBW3CBh3KCfAtggrYNZ0BAvC+Hh0+mExQSEJOK5Xh06r/OnUYLcKK +JTeK3CECgYEA7W/aWfOXwLz18qt4AqYSBWV8/MzAj2UGPvbHbyM/y5FOpBNHWRge +vwrCgLXWAFLYnSrbLs+7CcLdlURGo5aAT0Xfxefa2MKUK9HVBLGjzQAhL+g1Z7KU +RibPgxcxf9rEE0bfS59AamD9y3psRm6yzqfatYvvAQeMy8aKx30hGHUCgYEA5oLG +eNuHxRICGZNbV/4Hgj1dn9Sc+28wVU50Km0aT82rTddH2K7xX3EqN3+bS0npdOwB +2c47MeRJWwa0/QII+5MOPNcbhz6pHy6WhPkfbphjBfURtTlInJ4QBbe5Zdm7uGTs +zVSXyQ5vA2bhkGHlLR5pTf8hbQ/UipAglRXQ04kCgYAbMZ5egwm0Z2/71Suvkfmq +aI9CHOlJAxcOCxGYZv8df4z0OckIKT0MQF06hJj3/IUWqxvjZqNTYOAkAmfcwJX9 +flE8x2tHPDHgOu4c1YOfLQ6gAAZt1AQrbLKDnxpqPFIMqydT7+9ev7ERpvnTr0YH +aS4hcN90zLqRdDHc2zX5KQKBgQCU9aeqTDtlp6iBvI8Hwto5WMyaz7uOZCD4z5UU +IaMiNQdhayA7r3m9spSUfrrtIVtjOmxLwxDPwfWBhEkLQvhpEnPd7ygT8vM5elK8 +pXyhbIp5PBBwaw84XpB+EWcWoWjE1TuNyP+5ZsFokmBrfEesK33jcF66asNvs4nK +relf+QKBgQDREMabtiYvEmJhG+oVnqcWCpQnYetgrUYeXKj0sOeRvC15XRSt0Jh5 +TjxpY3OV37lvXDi9QCst2akbKxahzP4CDX1//RQsT2fgzLK5yc6xmPm5hdLb0EtR +K3WDy96qj7+5xWNpqSGollLAFxXmdZ4NOrnU3mMHxsLDYe0+9diGQQ== +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_TIMELORD_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUBingUaQUNXQpmRUh3SWSFMUaSqkwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyOFoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDTqgBgTsNxYUYJrT8MLFe8Myww/wTuLUhZWTkCqGzQ7grt +BPV1d5BTN2gthV1sltE3pjVowQrakbhnYyKQtxkuuaL2j+99cd1ZAC7BopxnDgnw +wNp7lkwsuV9de5GcykCz5wLzkfj5webvaOWsYptahtFkAv2Q2EgeXp8ZK7xoiW9p +uTZ/74+7i+FqY4M3DcM7/RATcQTtKW8qvqF3oFUXlFxZb7j0UMjqPGfzjP0ccze2 +zKxes1lgcic8TzCHWztOC4sYpSEfK/djDv2vSVjBBsGOWK+hi8rDCmoOKJonO9Fc +iw431LSHlvVyhg73Xp3HmBz+92IY1/EtJDrMGooHAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQCpHVgrVBHGBx+s+CllkVJS +re1AACf94K1PUrnCe3nSPIjsjlauMO1vJEg39XnOdxz1GIj0ZxPVoanRWjJWRC6E +TGivunOZzfREEFrMmvxO/azlgLn2VjhPxXxgyvyhjVE1ZUTabuNCMQtvwvon79Ko +TX0Na8Rkkdx23TPR33YIkQXeGnFHY3Cikc8UiXPus7LMMP89PmVh7ACi4t5v+DRu +CImEvP+jr18Sjhul9aA+CtL5PFPzn8bWlROCAgGI7GZPMhJvugCusHHWSzG8UblK +hWV8Aa5CTndzTOya2regyeZyy/ftXEmjqKjKSH5kfL83MI5MWr7AJMJ2oeEoC1W0 +-----END CERTIFICATE----- +""" + +SSL_TEST_TIMELORD_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA06oAYE7DcWFGCa0/DCxXvDMsMP8E7i1IWVk5Aqhs0O4K7QT1 +dXeQUzdoLYVdbJbRN6Y1aMEK2pG4Z2MikLcZLrmi9o/vfXHdWQAuwaKcZw4J8MDa +e5ZMLLlfXXuRnMpAs+cC85H4+cHm72jlrGKbWobRZAL9kNhIHl6fGSu8aIlvabk2 +f++Pu4vhamODNw3DO/0QE3EE7SlvKr6hd6BVF5RcWW+49FDI6jxn84z9HHM3tsys +XrNZYHInPE8wh1s7TguLGKUhHyv3Yw79r0lYwQbBjlivoYvKwwpqDiiaJzvRXIsO +N9S0h5b1coYO916dx5gc/vdiGNfxLSQ6zBqKBwIDAQABAoIBAER9H74J/meziAwd +iOcReholnMkF27yN3nk6y522je9U9ygWkK4/z+19YjJIkyBN9MjC+Uk6HZ73w+mA +RHD7LiTWYIrTrDS1IWBpFLgHPACEpGbIoGmZCnD2UotXLpjDQQh1WZkixKNcQLN7 +B8Yt6gLy2NI5vX6e3UcmauikJETWjMm2CBa4O5s0sKl/ZtzCUrzy4osyDdaOwpHW +2vPTHNgTVkETlI9UZehqC6iXOsOGzZR2lpcY9KGoS9rXAnc31tfRCfolyHvKHm/I +YCEFLzLypHp48hI5qgikkBhJFC9GnvOWJtvz9XQlkUahBIGx+xRSvNXHZS/OVPqG +dYZJp2ECgYEA+AttR6dXwChtGNjEwOI/F2X3RzBKMxS7tLaqEGD1SYTM3y/Fz7FY +aqiE5052Q5CXJj7juVB1YMfPXhq6ae6wCFqjZC9mzc6DhDs9NZ8Vx4sCsIZQI6uO +DAR9P4FyXW8mMAijN8oRQb4dkyk9NgWJ+1lA83cYAXWn6hGDHvT8cH8CgYEA2nPf +Hi8AOu0aDYWrBRyobttWvvy8t8+JT7qvRcyLgUL05mblKkt7QzrJuv5WvRYdMhiQ +Fh3eaXZpmBrb0O1jAeTlzOlkJtKYJlq0l6My9gLVEJuDN2TjZpcqSYQswUQWImx+ +wc71MUI03rLQnsXbuTgpzjxSp2nZ5XDRCMHnonkCgYBUz1NmFImkYi4rOLLNYI5B +6dNKu/Ai1wv3Fh2/y+cSZB3IkgUIppKcpVIL5H/7SgzkPsyOKDx6AtecTXc/kSUP +Ni53qt1zsRC2vaqIEwTXXWhmAwaTq6y93ysT86au27oq4kksYiLUX7StBISxW+N5 +1WKn3CiRFzF6jtJOW2WU8QKBgQCPew2wAITxLywud34VlHYYod3/AXdeb2Srxl06 +xhdW5kh+UFTyi7NBPAql1EnaVf0FG1b//8xGjqzaZ8G7H0ApTVOWXDEYwreGcYsu +EdX1+XRCPTJZHt2xU1ZGBRd0NSkooQQ8fZdULD0XjHEz55bit73VpI9RQFgKuCfD +zcA7WQKBgQCTBD1+CnwFHqrgp4tSjz+4kgclI89Hf+K0wSzi9hL5I4uvaJRvfVVD +mNs+eID0NbRALpNc9M+P6+EBdYFsSMdcOEeV672DFEukdy7e2ldbiQ221yTU3mSs +SHfBOBR7KJ6cmreUG9Cj9QLdsBuF2FEPfrzpP/YX/d7z/OdmStbX8w== +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_TIMELORD_PUBLIC_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUcWOy/+2e2VlxzEedhpSZS3UdmKUwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyOFoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDHS48UM5B+l3SpuKg1ZncimLWwOSyHyCASgN1dTFhFMW7/ +WN1hGGrB/2lw8CAdEBb1y+i2+PHo/HQq+2tn6GmT3744GnwS3vZOqvhN0LBA4Eg8 +DtE6utFBb4TZKcU2GQqGOHS3BdewXcVdoMfzM4IslqmHaQOXiqVwMrCGjiVKzF2l +SFzZdIsUBFiwSv02ZvWKhb0IbTsm7YK4NTGYKZxP4fsTHHZeBICjL2GEKlkXx47Z +JwthnpxB1AgeIdD+6tp8oCGPZxzd4lJxxTV385WH9zFg7jSp6poAOETra8ZnnceT +YgfHVvY5PEiKB+uZzMiPf9VIoVE3KwhW7JLWktnZAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQDGEr3k2P9Z4lOIwghhuVRf +T0YYzq39Vd/cDpQ/v1Ohs5kMdAEP/sDt5JWamtNLkbUDhbdgrE4Z5uKd2zTmL8Nb +aiT4vJ8E9hYYEheatE0qNJl0v3qxhz2mDGDfuYxGPPnssuxr/6cGWA2u1FasInYo +obLx7tBB3HNgKAtqSJZSwAMAhebJataR3REtkmBkn8s9zaTczwGsavH4AFit9ipB +oXkddyfvr5Ogpo1BEydLIOCv1c94zh9yI4gn9/Gz1uEgQZq6xg+A1VV4p3Oy0LWf +4OjEohsV2dX8KMVbVJwht4HiTv58M+iL9kxvcYe2Ao/z6CnvObxiFgD0eVZjNsVq +-----END CERTIFICATE----- +""" + +SSL_TEST_TIMELORD_PUBLIC_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAx0uPFDOQfpd0qbioNWZ3Ipi1sDksh8ggEoDdXUxYRTFu/1jd +YRhqwf9pcPAgHRAW9cvotvjx6Px0KvtrZ+hpk9++OBp8Et72Tqr4TdCwQOBIPA7R +OrrRQW+E2SnFNhkKhjh0twXXsF3FXaDH8zOCLJaph2kDl4qlcDKwho4lSsxdpUhc +2XSLFARYsEr9Nmb1ioW9CG07Ju2CuDUxmCmcT+H7Exx2XgSAoy9hhCpZF8eO2ScL +YZ6cQdQIHiHQ/urafKAhj2cc3eJSccU1d/OVh/cxYO40qeqaADhE62vGZ53Hk2IH +x1b2OTxIigfrmczIj3/VSKFRNysIVuyS1pLZ2QIDAQABAoIBAHfJS4tp3tyn7gAh +NEE8gDj0LqhoBQLr35Mfj24HeDrGlp+16wH0Kh7GhLrUKYkuZY6BbDOl98kBDFXV +Xl5LDEJMRkn0aUTybHsmOi6TU4z7AY/PnCS/qwy4mdHpUFbOwEuiXUywn06rXfiD +rgrYl9JzEByKmxUXyY5heUa242VL1YxVgKhtU41cTxo8TaaZ9mhQdjnMY09piuKe +Nb1pK5TzjIl/ci9VXp/JWtOcES+EGNLlboQydZAM6RXC9/GQKcdA39bibq/nLg1b +u2ha4NQPkxYUI3Qlzjz0X+uPvIr6vUHsvzUo5e6GFhVM3tm26iGtojogrP83/c6u +p12TDgECgYEA++fbGx/5q1Qhf5bbEZykseTmbOk+Qm969IEYpcDS2ENGVntc/zpf +xbwonZJB3gk1P9YWfOF0wR3IueG13D4cifRbSCfW8gkT9DzeNJO2QXsOFM96lb7r ++oUXIYqecno5T0V0Rhs4miIoj+M5owJiJcQHEbolKblFEWoKLb7AAxkCgYEAyojM +SM1mO8bT8WU4AQ3kGq0Bhy3fpQtAmkoOKWkcYsWwzOLMQDl9RM11r3k3ha92zzQn +8z5Dq+CMreO/7k2AO+iPQUxbVxSWcDfeBgFzUgRDrhGfVfInn1BFeyqTu8achZV9 +FVwg/b+qhNbnoOOL9qjsw6LYyG6w2uDEbzeJJMECgYASEb6QwolpNb61ZT/Pkpnp +vM6ej1iM8Wwzb7Mx6JP3OVQpTe4MzMYj6+xJ6TzaTXTJYHT6fj2ewKEuXDdsVfQ8 +HRATK/BqKiaJRXh28wWGyunmqj9T0H8i2Dshh8z3zzafLg86HCfCPqMENztiKwCZ +Tudm6SBTTtvadO3H/D/PuQKBgHM4yB9S2yp+vuzCDM5svKyAaDIjYuxeLm6YWt4s +Mj3vAdpN3K1dB2sxCBa/+1pOrGTM4z162rzZjq8Oto44ARAUCgu4CO7Ng5hr8B+y +i4zJaEeLNv6KOQhlfmHHoyxcnYdRuEuCkjJJ7BSOvk/FpEa7a5cQ+gttDag33wj2 +q0sBAoGAchejiA7NRNoudW5Nj5AZz5Lao2mY5VABIQEe3fbZKmNN8whsDul0bpEi +AboLaJVCj71HEwBKEPGcqgK9eAWRJqgmkZXQm1nXjmJeFo4PF98L98TBsM7JxnBr +IDrNl1cpFWosD7XWnabJnVLZ3ZfRbXHE1egjnTjtJ9fQhlI9Du8= +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_CRAWLER_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUDNXuwE8yqviMvf47HApsgcMIG6kwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyOFoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQC4r+RhnufHjAar0Rj+PEnVQFi2DtJGOVh4u96xaEbiY2EJ +3Z+jZG+pV2ivKfKZJiVCWsMa/XZ9GoDckAM/7/BACMozRYdBXGXJQ8luOaBLh6oy +w+nCMQ7zs6J3meJKm8AXiEpq6y7AGJ2P0EcG5qY7jiKqkNckFvJayuJ7mt9DGxS8 +OCNu0ja4waEtcVsMerM11zl4hQAxpm/+zRbTeRdHe6jY7Oxg3/V5e7W9kj6klzAt +LEFq3GNqztXlxEvnNCf2ItJ4RZ6P6dzho7hKQjIICkmzBrApcEZAj2Rhr47q2Prx +Ztw9E3iBTc3qpRkadbhQAtXUHDxTX/wUUzXgkBfXAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQDH2bDcb+NYjee2vV48E2Bc +16MvrBDfgQLA9Nv6oczcE2yfD1qOQTnoLs40QtKhDq2PMivTLSS+x6pstg0R63bH +7cCbWtGOYRDsjbjqTHwAFczQCgxZA3uKUeTaOv5M1jhQ4oP7Oz/oQv4yH7Fv9T0j +QOyCHe+CnLDP5VY0+3UWDSudCDIBWD/BxvsCBva+dstPfz3heIDciGiCRqrSXCh8 +ROiBKUh1TaASOtcIKYIJfrLFs5zpHT0zZSppKQcxPeKimCR5q4ZC+CuPXMJzvzrX +xPlO7OvgQ6dJE1/nOnXdXvl085ycWrrefNb7IvJ2Oc2/yB0957jbm0+13tuikXgG +-----END CERTIFICATE----- +""" + +SSL_TEST_CRAWLER_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAuK/kYZ7nx4wGq9EY/jxJ1UBYtg7SRjlYeLvesWhG4mNhCd2f +o2RvqVdorynymSYlQlrDGv12fRqA3JADP+/wQAjKM0WHQVxlyUPJbjmgS4eqMsPp +wjEO87Oid5niSpvAF4hKausuwBidj9BHBuamO44iqpDXJBbyWsrie5rfQxsUvDgj +btI2uMGhLXFbDHqzNdc5eIUAMaZv/s0W03kXR3uo2OzsYN/1eXu1vZI+pJcwLSxB +atxjas7V5cRL5zQn9iLSeEWej+nc4aO4SkIyCApJswawKXBGQI9kYa+O6tj68Wbc +PRN4gU3N6qUZGnW4UALV1Bw8U1/8FFM14JAX1wIDAQABAoIBAQC1MsVETe6jpitp +aXY3AZmC42XmATam9V5q3hAISQG63bv4PPuCCGlRYNPxf5toTLejmX7wvRsCdP1W +6Oh3U4aInNcdZKrmIqwuwhYf0eQGFq2Srwpqn01HJshQxabygkr+C5bPvbyZdc0o +qzT0fVk89x/l7sNVwjm3gHSixvkjUQKBxDwVDVLbNJLdeN6LIkep2PJoZCi8R441 +ySR1GkMlJjpCCkrv35/syAQpevbdM+7wBnuZkgfnzE7ECNKPvSMtTcgSf7Gs+943 +ydMau+e7sL+uDME2EIq0cveNceMQaHn6mYr2PVULsD6Sp42YlJWWZCgO053jPQxd ++qKREFZBAoGBAPGt0E5nN+FCsRXgYI3dAD9HOqjp2NjVF+qUtnHIw/rRnfsxPhSg +dQxjDMrBc0bYk5vim7q3Kd5W59ElSLFBjeUIbE+41dlJot99D25eD54ipWZqWJ89 +410y9O8/BOlmAalrlWmFUPvrIo/afVt4KbfZShvtCYkVHU5Uu7+LKmYhAoGBAMOh +iAGl6eP5KJBLJMe1ANNt7imVxOCzsEDryP2SPUk2tBpapxRCoYtHv0NFORKXUSX3 +g86eJL2EzFIvTHta4ksWs5xb6RdTET+CZlcA2FZik0kb50URt5R24HhwNuvSP9ET +E31E9n2V+gX5OtaqHJUqRpf2FY9Y9LyF9PebWc73AoGAHoP24F9yE9RK7ds19dOo +21SQGJHmWoKVkX4Th0x1Sm87RmNSVmJoRRcgn11Lw+9GvBEh3rKP684HWUYe5prD +Yt7sZiiNf/EnSpbM4v+ncy0mu0bER98VnUf421iWsy25Gf5GkFtP/W8UHxvUZj9G +7TrXe60zXga3e5OBXU8iHGECgYANnibrhnMbjXKHaNOsmyrtjM9xUZ2czqVMP52E +GTPu7sHmu7y5qlG4ckwcEKuCYcoiTHJ+ZX/FOYNHjUdTpuXtuCzUi4lUOFIRC7C4 +CbUFfmMmSaz1n+AZk5TYjWclT0Nl+F/47l4CK/h1hf3Uh47py0GSvmyNx/FxVhnR +Sp0bzQKBgHPyCjMEW/f17QG+NOTth87uBn0qxQ/3N5u3TK8j697GYz6GrZk8FsxY +2dO/O5Bp87LxlrDmI17SpbhP4OIwT53eQoMESa1MrUuOSl5TNQDLfP/ZRS2A5FZR +azPRUP1FibFtIF9+Oil/V6rWU+oOnvSSYt+P0Npy+WXnyDdRi+qm +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_DAEMON_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUaVjP5toSZjz+kCPgpM9eyglT71IwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyOVoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDKT+/vp2gbgVymuRMyDDgpefM0auPvmIFcNGIS7MiCIc+N +qpYCfjaCdZc5gCo0pgS4Z1rqbl88puYH47Awav+b6YXz5jvU66ZssVlRHXdrnD1r +kXTdRGQxEMBy/iUfEdQgOgsk779QoEpAWAyxyHp2r3LSdphnRpHua+04cMdpWiRN +Jv90l/qHL5DEubwd9GIrnOa4BmfCfpyn3waJITN49ngSNumyQiCOoMKIcCRDdt1K +yxTw00cLLUVhhaaOlMGZ2Xvmlpl1UhIpXS6BPnztV7lbAQzgfAKFL7f+ae3hAsrq +jTKKU4BpzwNLsggE0/UaPqrvDP22rcQ0GBtYLUTbAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQACY+ZfFoP7D93ZJmwjFm36 +AYwDLDCWbrwk1+MuLGVa3ppnbhb1ZnMpG+SUUoGUZ6BB0/Hq/945Y1igMPsElSRX +qrwq+Rtbr3f3ZqZe/JIVSlpFpPYYdZkXFJYrMkLSGJP8DkJVpmlskbXGqtaqcIQR +CurKKhkBsYsY3Z/YTEf82EHwXifukX2OlCmNWIEJe87/Dx/01GC9jD6iaHgJA76a +RIBwzf9YZqpgBEgOoTH5Pvz88tRf63njMdupgJVsxC03AqVBLFacDQ6aRjYgLedS +8gJnwq0CY6nfO63UkPH1wHBLd8UFSw75FVIUxyeh9JNjiaPj/vlr7sOVseqTaKZK +-----END CERTIFICATE----- +""" + +SSL_TEST_DAEMON_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAyk/v76doG4FcprkTMgw4KXnzNGrj75iBXDRiEuzIgiHPjaqW +An42gnWXOYAqNKYEuGda6m5fPKbmB+OwMGr/m+mF8+Y71OumbLFZUR13a5w9a5F0 +3URkMRDAcv4lHxHUIDoLJO+/UKBKQFgMsch6dq9y0naYZ0aR7mvtOHDHaVokTSb/ +dJf6hy+QxLm8HfRiK5zmuAZnwn6cp98GiSEzePZ4EjbpskIgjqDCiHAkQ3bdSssU +8NNHCy1FYYWmjpTBmdl75paZdVISKV0ugT587Ve5WwEM4HwChS+3/mnt4QLK6o0y +ilOAac8DS7IIBNP1Gj6q7wz9tq3ENBgbWC1E2wIDAQABAoIBABWBz/kDK+8Ynnfe +3uqIIARQwYkF//s9lCrwKqjmYR5sZ7sgslfLgRH3XD8xn8r/BytvWVvopdk1yOh3 +zQgop8m0VpgqFpw4/PU7GUqlPkfDUUg5K1pGZfxNUpgm6l1WMN3ILd0cW5M/pwUW +FytjK5moZyV2lBcOin71HD/OxXQuf0wKp4xKswag0DzUAB0AVLHmaIIMf9Sp0hXc +NqsdL5hnxHaMJ78lA9a14dVQ3Kx2Hovo3EFShqcEOqQH/Ekqj1QNlivn5TiarkhS +FFcIwuJisFg3RKZiSETEL3LHYr/tx9Q8g44GzgQb3lwBznG/0WjtatEqXUNqJ/nz +s5fp2+kCgYEA/7yAkYMKyIEYO6c+31NYrONkOFvD1EkQe9boSb+CUZbjGNHw1NDK +ChUYTbKOSV9PLV2SQKghQ4KVN+ZReVeEfdGptax03tWfrz6woRMzLTyo9hxlZLU5 +8xsNBt58f9XqjPsPU25FWSY2Q5C8AEykwNRiCBgsuW1wOmv+5IP39I8CgYEAyoVV +pJ8fy36CX+T+oYWyGMc1B0QCIWBPRZwA+EPgHWNBrNgd30JfndC9BJwq519eEaqW +x313/rM43B+mXgsMkndszIdwpt+i2Poqvv6FO6iwIdQsSe6FiUIRMYxwxccrzIp3 +0PrTLThIl3Z2gnaDFh1pj6yKHxvO/vTUHTtoSPUCgYEAvb4tokkZChUGVtRLCMW9 +KGF6rXogfPRM/6U/KeCmn0SVcIfr6OqUzRBDvPo7KeNGSsF/Tuuv6ngLoLMJRj+R +9QxHn5y6Bc94MD0SLiChuvGj5x7AeA0iVuZ+LteKhgrAgSORhnRtjayVXGIZqxsy +rkOmDJ1rKZUyTJZuQzxLD68CgYBzpMWt1kuCmP/7oDiipUR4j7BBMN2Cj4tUtWt8 +WM3uGhKTj2knZrBQ3rRAoKiDLFr7/YxR59yQbWjnkY8fnnGUZWuUMzbpo0RLYDbu +j0onMgE5n/2WTgSkxzwoLgjrdvsUtnEKvV+/L6eMDdJIV0Ita6guqZjJgMinn8hL +1POMzQKBgQD5HFgGlb0YEpEu9aS2cQiBGW5s5Ww64Sjpqui7PRm8J25V7mGIcpAk +N2LxBYnCSt5vOcpx7MiWqsR50ExROkS97383VD7tb+gSLc3NiGHnL2Q0A4DXbNc9 +VB9enMPAbSaXQ8dSj3l+GkC45xm5mRcespWoD1Q8N+FHENlkXYRj4Q== +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_INTRODUCER_PUBLIC_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUOwoUduReWasvp/QDg0CoLyhjMnAwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyOVoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQC6ghTGOkfeWwqPLfK1hMgIbY5HaowuUHRXkCyxCF0E994w +CVGnU+dCSJIy+xAqRDvmiQc4DyG4SSskTSCq1tv2xCDmdCb8BFjFVoKwtTpDjIkF +mZBRAWJWnuDxE1zUH1kwR9p/d44Ln+SJQV9/tHdLrn1exmAl7TZ3fMHFKyyXsded +qJ7K/9mlNAHs4Mz2hMZ6h93363bShVRP0clOzdJ0g0/OpXgRkRDSntDwATlBpsYU +p0mWrpyAdPIvADHBWlFZik2Ts/DLpdzPaHG0jUmQ3OFAOUNWBGvGTbkPeWsv3grk +4+m+MYbmoCg30l0ReeWy66J6OdjJMAiKuxoLfuRBAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQCYEncQIRyINhibeDZaPwhF +exLV0IfnbLFMpICG36F/sK7b9RFAg1x3SrPk6SXe7odKfmEvjKivptKXnUJ1mojA +cufGQiMY9k+xiLzL/x9nTKMuT6hcGAwdpSZl9T1xQOalI67PVl9OH02Qdab1h1bR +Un5OISmD4D0vtFH4CYXjG//r/SpfbC7d5h7v9/h8rRhZPN/uGgezHE1s6IJdeRas +zRhieSXDW0aW3Y7agkgwdfrPbz4wpEYUpWHDM0dyRMpqww/aXgw32L7Iv2wYJroW +R8FESaGFlY+7aBORNlOqULtg6SEzCy+Bb2Z90iUA1A8UuuGQpIPvZPKfv68Hr/mI +-----END CERTIFICATE----- +""" + +SSL_TEST_INTRODUCER_PUBLIC_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpgIBAAKCAQEAuoIUxjpH3lsKjy3ytYTICG2OR2qMLlB0V5AssQhdBPfeMAlR +p1PnQkiSMvsQKkQ75okHOA8huEkrJE0gqtbb9sQg5nQm/ARYxVaCsLU6Q4yJBZmQ +UQFiVp7g8RNc1B9ZMEfaf3eOC5/kiUFff7R3S659XsZgJe02d3zBxSssl7HXnaie +yv/ZpTQB7ODM9oTGeofd9+t20oVUT9HJTs3SdINPzqV4EZEQ0p7Q8AE5QabGFKdJ +lq6cgHTyLwAxwVpRWYpNk7Pwy6Xcz2hxtI1JkNzhQDlDVgRrxk25D3lrL94K5OPp +vjGG5qAoN9JdEXnlsuuiejnYyTAIirsaC37kQQIDAQABAoIBAQCHRA6jEePLzYWk +ADQWGB4nlqVpAEp40JsAYq03HBSypL59YlTZIY4I0a9O0C2SKfizo1AcBUV0bYSf +iFyX6sqPdwFRgo4/mztyq+KPHHEMSqGZ8Rzs0y8qGYKU9XEO20mJLaO3bNMVdTp/ +nW0QpqcauYttsDxNZXRqRo4WXNCzluf5HEargy2A3/44MGc34vmwvbUYPk+2DpFL +glsVXrRyiB17pwUMw17/h40t0sSTsm2GlubfGzz6PWTtJ7tqSXLMeFDn3zLOd89N +GGWyi/d682xzXmBJ1KA+rEKFCoC3S+enIJEMedJU2LQ9TtyPVy//8ocPksijlcah +Rg/bInIdAoGBAN/dH/3bVaLg0C0lrEdHVa38QnjbBce6nUkzjPqTE5H6AlUDReEM +kKBWbWXT03MYrsoR5+SP6YAmzrdFnVQIVgeEOCtTn1MTP0O8vgSaS+koA1K3E12V +Hnn/Ca2Auqhmq9Lu+PMgTlrfHmHlvfljHTeM8XCsk1Z1RnfCTB0VAP47AoGBANVI +J/v0Y6LhFAsGSDuM6Bxrz64rFOXDpwGMyIi9Joxe7Lhlx957So/vLMb7UyJhnN/U +0Fo/xNtuP6X0YtuOtfW/hfM4F7Q4IX9RWWEf3o4M9ePgmy221RNBghiCW3o409y9 +C9q9Ic0LWjFccRJYviitnU2I+vRTMv29mUGmIFOzAoGBAMVumJNjiGAw04itrAXl +FNxwGRPLXoqqu6Lekxw+8EiS7NGbJFr5oOe3sZ3XtqnQf/740cJTZcS+9RrPi5Sc +EXtpkbwscNxsoiQUZqVai05jGqMtkSe8pvdDBX7+hCCXBDzww6BwwooEMyB15Rj3 +38GpTRppKTxcC7z9H/GaH5kXAoGBALyT8haKPVaUoD4nLdLJgakvgg9DNFT7Kzzp +l52fcOgIPlHEXHZQmNqu5O6C5TyvBh+6cQZ6/7nsvwYjEQ2EH0rMLsmgPQUx5BxI +dzizDvqWZws3Wr2OvNMrxrWY/P0SJfE0jbi4JZzAftzKGYfZQzmAWGdnyEwjDCDi +dRFgq5CVAoGBAKaxSX+aKLw+rGS2c+gJj3WpmAjiyrpd/T/rWr/Vb7WODIENTZNx +cV6qrEx/K3KLNWWt/RSGF1WcwMogfIQeFSCuYUFC1OTaFIIzHXHoJ5kfNDuc58dY +5KBvJhlub9s4tAwlsEFHtHiKoOwpqDQqrMVk5T74cZnSY/01aMJzya8w +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_PRIVATE_CA_CERT_AND_KEY_6: Tuple[bytes, bytes] = (SSL_TEST_PRIVATE_CA_CRT, SSL_TEST_PRIVATE_CA_KEY) + +SSL_TEST_NODE_CERTS_AND_KEYS_6: Dict[str, Dict[str, Dict[str, bytes]]] = { + "full_node": { + "private": {"crt": SSL_TEST_FULLNODE_PRIVATE_CRT, "key": SSL_TEST_FULLNODE_PRIVATE_KEY}, + "public": {"crt": SSL_TEST_FULLNODE_PUBLIC_CRT, "key": SSL_TEST_FULLNODE_PUBLIC_KEY}, + }, + "wallet": { + "private": {"crt": SSL_TEST_WALLET_PRIVATE_CRT, "key": SSL_TEST_WALLET_PRIVATE_KEY}, + "public": {"crt": SSL_TEST_WALLET_PUBLIC_CRT, "key": SSL_TEST_WALLET_PUBLIC_KEY}, + }, + "farmer": { + "private": {"crt": SSL_TEST_FARMER_PRIVATE_CRT, "key": SSL_TEST_FARMER_PRIVATE_KEY}, + "public": {"crt": SSL_TEST_FARMER_PUBLIC_CRT, "key": SSL_TEST_FARMER_PUBLIC_KEY}, + }, + "harvester": { + "private": {"crt": SSL_TEST_HARVESTER_PRIVATE_CRT, "key": SSL_TEST_HARVESTER_PRIVATE_KEY}, + }, + "timelord": { + "private": {"crt": SSL_TEST_TIMELORD_PRIVATE_CRT, "key": SSL_TEST_TIMELORD_PRIVATE_KEY}, + "public": {"crt": SSL_TEST_TIMELORD_PUBLIC_CRT, "key": SSL_TEST_TIMELORD_PUBLIC_KEY}, + }, + "crawler": { + "private": {"crt": SSL_TEST_CRAWLER_PRIVATE_CRT, "key": SSL_TEST_CRAWLER_PRIVATE_KEY}, + }, + "daemon": { + "private": {"crt": SSL_TEST_DAEMON_PRIVATE_CRT, "key": SSL_TEST_DAEMON_PRIVATE_KEY}, + }, + "introducer": { + "public": {"crt": SSL_TEST_INTRODUCER_PUBLIC_CRT, "key": SSL_TEST_INTRODUCER_PUBLIC_KEY}, + }, +} diff --git a/tests/util/ssl_certs_7.py b/tests/util/ssl_certs_7.py new file mode 100644 index 000000000000..d7f79f276380 --- /dev/null +++ b/tests/util/ssl_certs_7.py @@ -0,0 +1,684 @@ +from typing import Dict, Tuple + +SSL_TEST_PRIVATE_CA_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDKTCCAhGgAwIBAgIUHoeobLQu3yMmraIDXDF+F6M4j9IwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMB4XDTIyMDMyMzE3MjkyOVoXDTMyMDMy +MDE3MjkyOVowRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8G +A1UECwwYT3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAskK29xVfnH/4tpM+loZOOZX3d0u/NARRkgnPeoTs14M0 +dKP+dZ+FRuuqKmJQHuO8+MFfeTFIiI671rWobtdGCXVD+TZ3g4btdtGdvPjIBXJs +RFhYkP/fIjzq0MoJ9O6qlhXh3skispfHRY2ysVHpT4d63tixhycDsMn82Jh92OGm +BaO8UpjIk1twEnfxsKm5MtPbvNJGVzAkwYXWVFdV7GdEYCKsVm94Sd85TpDg0xFq +q5YWOg5mpFbGAExBjFerHSuVBuNsBap0mjL3aIb6MXvQeYp+lKrzeUfkUH06e/Pr +AY+TQPO9ktcUHHZ8+HrXhOODdeDlbC8mWzR9I59AfQIDAQABoxMwETAPBgNVHRMB +Af8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBQ/s/lX6Tbdg1g1FIShK+BH+5q +msfNkAQG+mXYGbn6SYQQspYFZdZp4kXLdSKSjok5eWoMnfYN+hy29A5HgeJfdbM4 +4Nmt5OUvWT0EOTD1VtdVnsH8tac5Q1XevE0ZK9DXYBSEXqO8sVSt6VWmNT98lmEZ +AVWtawc0zt/31QAZEa5Jjghq/HRbIi6KUX1t3Hx/yVa9Uxiz1dBVE8A+QBwVKOzv +egKu6T94xBCcOeOh0NgluRdFiTr9cXAYNQAUM+WdmyAvFZ4qwuUKam7C7zX+td6M +r80F1SD8qbQgxYS1bwqx6K06xbFRLXpWbyHXG18v6ia+sifWblg6JQnYhxZg +-----END CERTIFICATE----- +""" + +SSL_TEST_PRIVATE_CA_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAskK29xVfnH/4tpM+loZOOZX3d0u/NARRkgnPeoTs14M0dKP+ +dZ+FRuuqKmJQHuO8+MFfeTFIiI671rWobtdGCXVD+TZ3g4btdtGdvPjIBXJsRFhY +kP/fIjzq0MoJ9O6qlhXh3skispfHRY2ysVHpT4d63tixhycDsMn82Jh92OGmBaO8 +UpjIk1twEnfxsKm5MtPbvNJGVzAkwYXWVFdV7GdEYCKsVm94Sd85TpDg0xFqq5YW +Og5mpFbGAExBjFerHSuVBuNsBap0mjL3aIb6MXvQeYp+lKrzeUfkUH06e/PrAY+T +QPO9ktcUHHZ8+HrXhOODdeDlbC8mWzR9I59AfQIDAQABAoIBAFm7l5qdWbnP+YT+ +bf0bsnjuctnMeX1Xxy/6XETScN6zn04v10GigVaH/urC/o3uGgwmW0cIdfi30Ppu +C1FwcEMGkqb6sgK1gwfS0NJ1cUq8pJ9q0Xp8MvhrLdDYQ1bWZWyTq1WYbiz0lkz+ +3TrBfu6XxlQzRHpCO2tc4jit2nu3k92I8EHo8sRPcFssgEH7JdFpGOm/G6ATWGpU +mSJZTxBOzR5iprR8Hl2RXIBDGXqVKXApLJ3OlchSOodeFt2h7ffiNJmuGZ/cnxs0 +05eXzIg0zJ5CffoQdO5s63SPRg+x8O+L+hN0k6Kh5CmFpAcn1jzZatvGO47ndBUx +kSyrH0ECgYEA2a1n+84y6A35LOeUvTE0m3c+DXoI7jl6zUSffXJ5PVH7lKro2hq8 +P5TDG2NV9qFv4KFJVy8hvy6Ie0+HTNP+QSUB2LvJSp4GhlQNP4/J6oY4NGfocfbK ++rc8/jEVQaoySq1ODLchEaxMjwgKOVcUqh6tCQjiTZ3dqpZuwUGALoUCgYEA0aTR +UYcynBWPyt5OT98EkVX4Nu+NqONAyKlDYJqU2NHP4xHFxVPPwYR1C4vZJEmC1z3C +Rad+svwBhCXsh54hTBhZkF+BV8ll4/BqiuHiaqF95VRz8B4wjv+0QKN1fCQO4RT5 +dEqbAkJYPq+vYCJew3iPWODLkaK/B8yc1hQ3l5kCgYAkYhGBSwPDOaKuWL7JqJHM +cm/SvNUFTGI0MQYfZ6TQFQXh4XcuDU3tqqW5zC6wHGeguhSSF/SiCdsSEUbiFoTm +ypK2cRzB9gvNI/ta5mOvaWO3jq6Rbdibc0kki3usEBB73t+uzGUgmRXqykM7Nkzj +6mCto+h/ZKWKP76fWp1cKQKBgH2KEiKdMExRiRr1xrWDmkuhzJKxHwZsl6XR3lwi +FVJFShTy1piU2MtMk36Hj09wid50yDpH09JAoHPO9fY8VjooNrICzwSPwOkfVd22 +6IvsCuTijs7SdUecjgdLGxZszVAx7DOcXXib3BYlxIJv8olhT43sh1q9t2FnQN/d +mXC5AoGAN5klrbJVgUE8kRik5GvpcoasyDktJQ8KKDD2u/TDkRaCJf6H0ZP145aM +k0Mg5DMG5M8+Eb6l+iEOX1lrrJ3EPtcQraUiFAPXF8slboUs7k5HZ6rVckHqMvBC +ZTAxfIdR90eFK6q0PTRYH2ZfxKKpPDORQvzt59reHexvOVTuozk= +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_FULLNODE_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUFNReIWFnacmYlEf+oXKER30rJfAwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkyOVoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDdivRLL/onTCvG9JKzYXCxW6hrwYRgmiyG4ISrIWlainW3 +ZCduM13t4qlWp2lVYCIez8K3NYb+m9Jk4tYJujzU9FhusvAy2D7SeGJGkAvDuYMK +LCRM0m3/Q0fls0RMHyNd7Csg26LNyGukdvEyKmonqA8q5MZENtsRx1tQtHORLlio +kEXjyd67J1wm5m4rOk+DqdrIzSAuomj+M/D1lRh0icjiSE0Wm9PXJllzSp/JyLqN +AMIDIjkG5CZc7cD2yM7OZQ+haCxIbAmxzyWVhLeuGiTP2qZ2ZRML2HXGVkQfbE+c +vFkPKVoY6pOI0USzM6ipqmYO9Y6357SIeSWhNNG1AgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQBGWHpJNp8iDR5uy14qFYwY +1wwIvQUem0IYADuTXIP3Y+kSKyZpuZeYWFlKzrfTf6wTlmZchcqpidUfkqPS8sFL +4LLRlDFTCWhIiBSs25gLLwRiMTErStedojGS9+smMVpp+/LguAp+kR0zyov7+Qe/ +05DW/fgtZFZIuDa9DX2aLcr2LHLpQU2+nKFM5E4dOEEwaf2rDkpqITME00bZRipF +yytQq6MIG+W2dl48b32B+VhU3UtjeJ4bYDY2BUvAoI69XN/RWO2JVZqEcuAKXjIY +m/+snrJJ8RNATWZ0XgFUQ3UL2j98qHwY5XtGNvLfkot6EC48t0jmuPICg/mTxCPV +-----END CERTIFICATE----- +""" + +SSL_TEST_FULLNODE_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA3Yr0Sy/6J0wrxvSSs2FwsVuoa8GEYJoshuCEqyFpWop1t2Qn +bjNd7eKpVqdpVWAiHs/CtzWG/pvSZOLWCbo81PRYbrLwMtg+0nhiRpALw7mDCiwk +TNJt/0NH5bNETB8jXewrINuizchrpHbxMipqJ6gPKuTGRDbbEcdbULRzkS5YqJBF +48neuydcJuZuKzpPg6nayM0gLqJo/jPw9ZUYdInI4khNFpvT1yZZc0qfyci6jQDC +AyI5BuQmXO3A9sjOzmUPoWgsSGwJsc8llYS3rhokz9qmdmUTC9h1xlZEH2xPnLxZ +DylaGOqTiNFEszOoqapmDvWOt+e0iHkloTTRtQIDAQABAoIBACHjhJUPxLtIKpYn +iV1JNXzb4XqCQqaoTtFe/MxUsxH3hiREfMedseuWtYKc3z8BEpcV/toZpQnDej6W +eFlKlM2ahwB//MA6VfnKEnZqyHHrKcFfmTnrIopel1vqvTLLvJQ8cSh4kIHb+6NP +0ntzA4QHcDKGhlGe9onUrgI9aEQ3wrHZvChyyAV8P/sxY2IseOf5PkJ/fBqttGPZ +9aRdpzvMCyfbhygHAVklucefuQehpc2duv6BUwCd1HgAIwZ42gaTQ8h8Yh2SxDiV +9DKkkQEJoG8ZXYGHfKpXo7u+RgTtpMPmRnls5uIt34qECRJGmXr1pC9iBx1oRIi6 +Qdno4q0CgYEA/lcKRnpjrnWfGYgLZpIPEr4pV328BZ1I7FjzaCcfDEU70gxFpCXa +ViHP8TEWG3+yH+WPodHMfLQ3UzLzSBu/o/XYfU0Nk4A7QrGt4heDRmPGGY+WEKs/ +LoiXlLrF4rOKw3feuR2wagg6NpP5uDraVJFCDG5O0/SIyWSc5ZW7DEMCgYEA3v0d +jfm0n6dNpJDgvCz50qhI6ijUT/Wvnl1VVXHpGSVJjDMSffLsGe9G9bHJCz0s3E0B +NfGJlixmkpzmtNmb4qt4ahdEY/ltYUFlWTvY84JjvJTFrjV0if3LLlVWeHm0aEcB +rBsp0Z+/qpoY6AJhWmN0jX2pS8WbsjHO9OM8xqcCgYEA603rm7ivcEAxqZVLtuF6 +QITeCquwwCD7zm2dA8bt2pRS+8mOxIagsP8nOqWHJnnFee0QLU3EObshVD/XA+do +LXDNkV8wKD6ClPl9PaczNHQqWouU8mb8VTjZxCfn3AzvXFgSHoFxLSffc48DgYYx +Z/vbd1S2aTHbOzdyUJVuL7ECgYAUApC5YdQEk6XTA7E3Ea4lajaI1LsgpcJpqqRy +s3Mgb4knDJo3NSpctW0ftSF+YbH53ush5RfcowVdWLkXN4PWll6K3qWjdwmKtayb +klRIncXHcW4/0Moxa9XkxYGp8/ntdZm/0PwytGwlqghcIYKM8unNnJ4pj4UGO5P/ +w7h7dwKBgQDYqai2lM4mtud8Xn77maR1olSScon+S9eG6GGyRQPVom4NyvfaQxvb +qFQZnwoQuh5ugsbyCA0qkm2RhBIqNWo4j8rM7AHb27qkB9F5tHA6S+DYG9kEvgmn +HFxiZCY+Lu6wRP5Ssnz444i2Tr6kUnIcj4XRyOQjcwKqhMYNod5XGg== +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_FULLNODE_PUBLIC_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUfZLZhnra/z8yyJivw/7O2simIzowDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkzMFoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDAkSC0hl+/EeP4XUtJDAWCBRosaU7munJ8XX4fZDcmXzsg +p2zdv9UZp2zkfXTYP5zFAnjG9QEiiURMZ4rIKdr/cJXY/93H6dwfrjZTS1gCSlpc +gkuq/cdbOeU6xCX8X/dotSQupbYr6ccPVf22iON/FotilEpP2yOFZi0d54uKnBoq +x6GP22b9/Ij+tQNacPZavQjZD2i7ChtLigtMDmrrFMOrsQjSCQPpGKsXmyNqTo+/ +BKAKEs+WghuoVh6G5BeC2EY/PoUohUxBUWlAXThA73yrq/nGQRWfPeNZ2r0d1z9m +AlUakb4a54M8tDO+OOqw985KyhPYRGxOgPDFy4cdAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQAQRNledZvjT6+Rtb29W8pN +11WfUj5+QW7DX7/AFd59PXQkPbTf9EPiNtbriPaeOQ/B+jJ10/hGGGUhft4IEzpX +ffQ09CPNAFMiyvvnOQdykH9lvDMcoVYi4EjzFsCa7uVbKSZApeO07hCWRtlEeDrx +QU//I1F/BcGQGh9Evh4LiZZUzRxqlGE11jSBNIpSNkVE0DO90lBwEujvb1OC4YUh +x8Ef3hzI8EkR4VHG8SwX+VlasHAvM06ouKAQeMMNTWxeShaTI+mZkVUgkWJvTxf6 +LwmU1B8s9M9hNUailBda/7ztvcZHWxghBhsWICZIngUvt8M5/rrgph+e0rI2QMiP +-----END CERTIFICATE----- +""" + +SSL_TEST_FULLNODE_PUBLIC_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAwJEgtIZfvxHj+F1LSQwFggUaLGlO5rpyfF1+H2Q3Jl87IKds +3b/VGads5H102D+cxQJ4xvUBIolETGeKyCna/3CV2P/dx+ncH642U0tYAkpaXIJL +qv3HWznlOsQl/F/3aLUkLqW2K+nHD1X9tojjfxaLYpRKT9sjhWYtHeeLipwaKseh +j9tm/fyI/rUDWnD2Wr0I2Q9ouwobS4oLTA5q6xTDq7EI0gkD6RirF5sjak6PvwSg +ChLPloIbqFYehuQXgthGPz6FKIVMQVFpQF04QO98q6v5xkEVnz3jWdq9Hdc/ZgJV +GpG+GueDPLQzvjjqsPfOSsoT2ERsToDwxcuHHQIDAQABAoIBADweEsPJH6MbBrzH +A3Xultmclis/RS6rDorc9T7/nmgQWvk6y7X+6Zx0tH4w3IWWdm7a8rHKU2xgxj3E +JYOP7ZrJnz57wtVioSIS1UrzvqoYZFV1KAJd8Br+3B2YlvNPUoIR6xXVDiZveYHE +Ks0Nt1g5xZIlEX4Uv+Ypm/Q2EU5YGnxs238K8kwkPtNTLyAQ7v40YAyZ8uehUu+T +vaMKi4GwX/+TaXWW8dek2fsktWGf10drMckvv0fyo3CdYlSMs0PFZkIHqMQuQv9/ +E+XI8viMovrnhU53jLbs4Tt+f3kDtWVUrd+kqWw5MXzT2K5z6OGQtjK2enACwilj +kbv/ntECgYEA8hUfsihlCFKZdJHZW6mldFY5Ld8YEsAqTter4r9iid8+R6yrSr2k +ZcdAyNFcDBGWkX51nupteU/LFHojlRymxVD2AVGanknYrJ4DNvvc08bCgVZOx7vz +u9KQb8E1xsi9T7uzoGfDqOapQSqPrFvxIPkqKWpOeqY5Jq4UZD7onmcCgYEAy6NA +oqFjbz8/6ImpOaR7cQMFgPnw749qmiRoIMHSL0ULkKq/XF8pN3/mxzKxmLGXGYm/ +yXbpcpuS5oIE2vhwxg1XAgUjpEG7stWtv0tlzpy230/TzwTJO2OOEaVcND+9nawV +T0ufX7c9CRcH4csgG4eJR749rG7jdLiGoqdms9sCgYEAng8YwNQLE2IK+8d2qZic +hNb/QmoVZ7i8Zvn+KnBXQDnYiie9N3GW8zUjoXrApMifDKQK3BWoILrul5IfxW4N +nWt9E+NaFtuUczBAXRgZWNS/jn7xtQuM2idjUvRNzlqm8HZXk/XsFE12WSwW3qyx +RZwp4Ryd3QrG6fBjNAi3DSUCgYEAoTvHR30lL8YSodmtZXh4TIip6O7895DERPui +fp04ADlY6Nho34hxKAJbBUl8GHw0OQI6GhfOyvGnJF+53G5tTELvqyfKipmJNhW9 +lgLqvuaSXMnl2LnfYuh2aj5VfQEi7x57WOd1buG0r+fOU9byuxlbdrSIPGkoKxiX +cV2+EZUCgYEA3rMsb5/F1nCsuhPS1DNlurMwSmodFaYasg2+cg3hQPtHe4LO86U8 +doW1c97sdKLLkH09E7ZmZafXTj4uKwQlLa35UmaRvPM+mWdPLr0Nkkm1dEAi298g +zD1u3pg1jQE1Ta4kuE65u8lwu0JKvje8pvHBRRcXuLvr2oyJ1VWVfvA= +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_WALLET_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUZmWsnGgQgJO07P9Mrqx1hHBfQbkwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkzMFoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDjcVLjFq2N6GKxvke2xGntOo62h4ze0fNrdj/OifXvpzxf +qnPkhBcb7+6GL/gO9IT6hnTCDSjNQG/Coi1grjoPT9T+DkamwioBcFdIOYwEqPCd +Fk5ZnqwAWWg94+dkb/jlRcFlShNcgDriH/MrgfQ03A4VfqzcNT8GzZ4zwFqAyhiV +KQ56q0qrnIrHQDERII3QMH72EGCVX+32IIkz2CJvo56yngwF+tDzYJYHMJnNTCG8 +iVD71v6qBM52G/6bKFRJhyZkKkVqPwptIXInDPY5ETgfZYgD67ffQ5+W1kXlCmmH +PuK8w48Fw7LjRDzLTYRFyDSIT1H5sdRvHLIl9BEnAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQBvOTJdNGUD5Zc976Q5SJre +15KN8PaOsLePPISn7e/2DwMGuq7GXnGa4JCb35XTGjX+Wxgnv/0/8asMimay6gwE +YZBcRwakm3l3//4W1ELc6SREoFcUTV49rnccq/N3u7ZrDubf1U9uKvFEpwp9TM8a +nq7tVOe3j+Msyvq4/q/l0nnHX/uuWYrZrC+Cm6tjytmOr6JzAhURFoVx3QnwaD8z +qm+cisSE3/lLuyTzgNUiQkdIC6bCg5KVOrTXFo3T72F7+oeM7h+OiDO71y79x9UH +r6YE8Ab1nYz6Cw1fwy3/QzxtazuTrJ5gBotF0oqCoayEtiOMZr+aQuM/qRs0EeTy +-----END CERTIFICATE----- +""" + +SSL_TEST_WALLET_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA43FS4xatjehisb5HtsRp7TqOtoeM3tHza3Y/zon176c8X6pz +5IQXG+/uhi/4DvSE+oZ0wg0ozUBvwqItYK46D0/U/g5GpsIqAXBXSDmMBKjwnRZO +WZ6sAFloPePnZG/45UXBZUoTXIA64h/zK4H0NNwOFX6s3DU/Bs2eM8BagMoYlSkO +eqtKq5yKx0AxESCN0DB+9hBglV/t9iCJM9gib6Oesp4MBfrQ82CWBzCZzUwhvIlQ ++9b+qgTOdhv+myhUSYcmZCpFaj8KbSFyJwz2ORE4H2WIA+u330OfltZF5Qpphz7i +vMOPBcOy40Q8y02ERcg0iE9R+bHUbxyyJfQRJwIDAQABAoIBAHyCXDLPBmGqNuVA +2nd2XNqudNP9rqOIYe6RRGrn4Ye5kHZ6lIkjupbjqTsyZWSifW28T4yvsYdzX/s3 +1wmXN1eMh3gxDoJZxq8U9eMnBbzDUz1bqbasA1MJnuRKsDCuj53LqwytGZ5I4HNL +tE48DRkm4lroBu9iAsfRpmqEQcdAUtXGLvGGkX+V9j2exREyVXrxALC5N+4i4w7E +f5aidhH79IGuMbfp88QS7NbjrpEwd1uyP3J7KCiyXwWIJnJyF8pcd7ZUsP95msE9 +STeoKBaV+cIE03jvTT4fpuQ2Cn2WYSxkgVdWM+XdxGouIQvboOBNvPdWfdQCLSHa +acRhzGkCgYEA/nd/BXmm9pkGKHm9XDHy97pYm1PSOihsgjZq148hY33gdA/ibUtK +X7nSlmGAUzVTU01nfPmjkHp0YyoDn+NejAyPhBUqPoxfC7p2In1MiibOlMDxOpPl +199+gvadHk59g5Ap02iCu1lnlR9DhFY+42XcBSLba8MdplOL4lhnx9UCgYEA5NAk +45Dt0NXnaKKLbMW5EOZA1zLUcomO8AH1dEF+l3e5UOm9g+YnYtUVALKM1nBc0EJ3 +fu8fmKhDqieLxb42o7j8yQqeKUXZW3XeDGPdLhR8coxICZv8f2j59Qgx6gwQ0ipy +IUan/0rDUm7dinu+07Va4hqnvpuPbUco3VclDwsCgYEAvwScyVNkzkBYqxGX4Blu +th+gXBkz+oxVx/lpgp7jBXh8gSNbaYfXMLyhJFnUpqGlBydXxCzxZ4dEzxu+1Mst +MhxLr27j40gkIP27qHA+gIZZFLkxXDOhmccfhNfzYcix14zkmNofKNwYMYzidfj4 +BGN2IjTkWaSCIVUd8K9EWHECgYB6wKu5pivfaJIgEWvJK/4P8ecBTFSrKd8UJYjg +GK7oZaN2pB823sdsfzIoUKG7/UXduHrRD0odJNBAPbz/lf9MMFb1KAwXylBEf+Rj +M0Qaj4UAEwAmn5eDZvcKHJ5imJyBk6Hs9jH0hNBqre2OeLq0a0vZl0E8wcndb1qd +/D12ZwKBgQCDKNhzVlKwtG5hF9rG0FLxUtOduOEH9OI4N0BaptoJncGw/LEqZI9R +ZWim7eYzst6zGvsm6hiCZXR9pt1IUNVC0YxaP38YoolFKYxwcraPIfhteXx5CF2D +EfVRIx4ZL8Ec0nKeG7sfzl1AM30kOT1sZ74ZnoS6iYNXGbkXIeecUA== +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_WALLET_PUBLIC_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUJg7OmxTb+nSse9yCvMecvDQ995gwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkzMFoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQCoMxRs8WLspmYqmfcq6MqZI2h1bNRqXkdXdDpRTNkdfusp +8Xm5/Oncd2ewVbOIWNtaw0c5Uq6futXmo3cMAf4fTCpQUxEeWcMw1KWK5vudfXFY +w0VNxS9fAR4ByRbNGDcW7HXu0silleuD8onSKxBEyUpNHksqmuWLJ+Av9zvRfl4V +P5WUVr84dpaotLXYx4PxoPINRloIKIrzsEAVwrmV8OxsbsxdlvokUYy6kezTRNqN +7izl2CCi5PvklOHYr9Bf5NgfAE4VMMTcVrQKebTkGAdlJipJ3N7gf75Yi/mMTXnW +SjMSRjJWGub0iowNUEXLXLaSQurQRJ5iCoMCZr4tAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQAw9TzzcnbQvjEC4zF4nPr4 +w6cNUog8tfiUx3Oh2Hf4+//Urhu+9rO2ylV90apT2kR5qUT0yod3s/9WGMe56P1b +3qES6PTN2LipOT0js9SmmhHmcy+tiQxeKk37SWjFMHMmfpaZHLZROxIAsHUlisYo +Pb45Lm9zd/yofyh0xTHC36KaU7R3mVzE0cKtFKHZIiwpd5BUp638lY6tngQzOOe/ +hhxRyJ4XoafugAgW9nykm+zMG9LxhHpoMFIZuZQZED4RKG7gKA8l0VqKm9Cg9DTm +xqhRHtHY9hS47dsHL9Ggv8XsBy4nnkemVhB33DPK9kYRmz+J4Y8gjoqlL7ZL8bnr +-----END CERTIFICATE----- +""" + +SSL_TEST_WALLET_PUBLIC_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAqDMUbPFi7KZmKpn3KujKmSNodWzUal5HV3Q6UUzZHX7rKfF5 +ufzp3HdnsFWziFjbWsNHOVKun7rV5qN3DAH+H0wqUFMRHlnDMNSliub7nX1xWMNF +TcUvXwEeAckWzRg3Fux17tLIpZXrg/KJ0isQRMlKTR5LKprliyfgL/c70X5eFT+V +lFa/OHaWqLS12MeD8aDyDUZaCCiK87BAFcK5lfDsbG7MXZb6JFGMupHs00Taje4s +5dggouT75JTh2K/QX+TYHwBOFTDE3Fa0Cnm05BgHZSYqSdze4H++WIv5jE151koz +EkYyVhrm9IqMDVBFy1y2kkLq0ESeYgqDAma+LQIDAQABAoIBACodB/szARrRmvkF +rc4vlTJ8nBXylsi/LEuoTUW34RCyi3zn+htoSMGrn+mVu6ri3KFADaA7pH9Xz2C9 +Avydrxv0/Q85jSq1PNsIEx7RMKTBGNUppzuOqIq4A+RcjfnyGzEBKZIPcq+K9voF +ix51K9CdOZ5PfHCBcgHCjS5VT8Pm4QtUnH7Ak+unT8QZ2DaK1PY9lnsburr2o7ax +kk7DLSsb60pb7XBDLrv0c5i4/XIhSP/GBUJ0GLTEutWG9iXwa5Nzlxoz4UGjnljP +80KQB1TGiZnFYiolrKaj+rmcuXeApYuiAwFI8B58DYyol3KJXmUCzilw38ycArd0 +nIkhyJ0CgYEA0TXHfUKZnF9X7Vig3hCCfHZFJy8g3L8RklIjZVn9VUGFZhCIPiMN +zgSI5fK8sx1oLKz4vYvT4+4QoS3kb13pWGRoG8ooF76R/0soXckKG/0Nl2fq6YHR +9Bygxvn9XxxGHToIJfxfcmzfE8h+wmpdd4t1vuWzIzrAqyoBDf2nxIcCgYEAzdFD +Bb+J9CZviAtsOAEiouZby/rtZ9ayLUulRTDZuKpVnVHRnzckA6cLEqECC4dr5WsH +bDFasktvx5chl5SuE3NFR8cznMyFN5rpWOpYdl1QwwOQ15sBJ2LuT+mafXpu1/7x +RhZyw5oaDDrpaqOLTvQbxlrXjpXYlNsIeXALyKsCgYB1uTtuIuHpekUyC6NKEiQ7 +ARpcuEpXrTSoD4xXZdIF/X6hNkBUJsmH2klmi7bfW3bZXOQDVQGAyt+UswxOFpxN +3wIuUQ1KfzQMYjBuxcfog/b38cPgberE4K8gCGAo+vIBVDxtk5vp+ZV1vmaF4/CA +antxVjP9aPwt1M8PHmMfVQKBgQCNrFYuRsJ3RV3Qj8xWYLGu4FKf/oIc0DSl6URC +dHXqH5X/TKq2pgYsXXfJwvrdZMJokVvypaaAxFyVTvrYlIee6+HsnrpwXHf25rNp +eSabk3BcTMAPKauJqRfR+kNVzEkwdVUvoZQuAI2djY/Oz/S8zFuyFiX7CDqlfIBA +11fFMQKBgEBlJNb5pEhu9tdULlOEroG0XeCcAKDfS0WULrbk1VCeRNOOXYdoiG2v +YImM6jUSIrHmZaMu7aaLYKDbC3xwd4ZyRRq4KjBYKHM6o4h+bZbxl3Eagjb0UQnr +vIsFw4+VWa5DOrdVtpoKBANUY0MSAfUPlBDBMGUuQ/q3X8GgxZ5B +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_FARMER_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUBK/eOJ8iPyAfvjMt3GSSInfW8+owDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkzMFoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQC6Z/5RC7RCEceZktvq4CJeAaGlKGJFpF3z/THIWoVjJ03N +KpDMprhq8N2HM1pM5I0HPDze0hDCRGpGh3wby3FmVAxb1vJyDTT/DLw5KYDBQaCY +TXwschPu3MYfSJIUXlJVWRC7di3xV6si3L1ALdfmK2des/YWCYP0qIjc1E8j/OVc +wD2PdyHR+p129SsO6WB8KIH/ocTw5YJisQBebUo4m495FhKLcKPiczUpRL4CmZUa +dFkrTWz1Vcr33f6fFybYaWI3lxHHYxOSB3MilDd8Au55tLhkpwpkaqPKWDP6b7au +WFuc3q8DegbYRLyq7qEudsG/u4afrHfI+vovHLVBAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQB/MflzZFEWhLZ/ZcHfmVkO +nTs74yALWjdld24er665E/tbRUScuGwpGJDhxoLE3JrsM8wIyqw/dOLqGscNprl2 +u5MBRMwzbQncE1kZ9YNyT/1oC84Fee3IFni47hBUJK3o1zOuRlU9cMTj3dttBEG2 +hByAv/ZP56H/dQN1/LIGoOHNSRhdzuY33rFYg6ZjV5mnFh4BHQ8jElA6lTiz2uBL +98RePraQx1ONCXyrjBV5o0G1Ra1UmRo/DBvkQu03eZXIyqSrZJKX1xM8345p9SjI +Ztpr9N0YFHRkMrj8SCiqYeRX0LdUW/D5e/gBf5j+dJzFiB6NHgyD6mKORQgxmIPY +-----END CERTIFICATE----- +""" + +SSL_TEST_FARMER_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAumf+UQu0QhHHmZLb6uAiXgGhpShiRaRd8/0xyFqFYydNzSqQ +zKa4avDdhzNaTOSNBzw83tIQwkRqRod8G8txZlQMW9bycg00/wy8OSmAwUGgmE18 +LHIT7tzGH0iSFF5SVVkQu3Yt8VerIty9QC3X5itnXrP2FgmD9KiI3NRPI/zlXMA9 +j3ch0fqddvUrDulgfCiB/6HE8OWCYrEAXm1KOJuPeRYSi3Cj4nM1KUS+ApmVGnRZ +K01s9VXK993+nxcm2GliN5cRx2MTkgdzIpQ3fALuebS4ZKcKZGqjylgz+m+2rlhb +nN6vA3oG2ES8qu6hLnbBv7uGn6x3yPr6Lxy1QQIDAQABAoIBAGV238aLwWXJOcWN +W+mgcPSMnMlCjyNrUbzSkDuHkl2jckUAK2tKQM3tKBhEyp1aNq2+iz+aRocIKHUV +oGecuLBDhaqj+Lo+GB/QddADmFMZfuoIG1QyEEmPoMQ8g09U/Cn63hG8RUu+Nag3 +UUhS69ccvxqciJH8Qfh1mHHjmuRyAMMsYlbiq2qJxcwPQQO8S+ieywfz5KtYKkLD +2Gwzvmit29r1APhp2jFaTIsrMkzY5nYc5+gp6K8+XZ3Bm2au50P/YnzBuXFDB6Tc +gDSPfaho5HawsHHyIop15fajn8dvJzzMTux4kdxXcsJcpasTyc1bW8v1VrcGgHcc +7yhogLUCgYEA6A6Nk8gfcrN7oTW47ECy+TfwiP47lCm3Mi+XrW6TdKL6fQ0mhfvv +WsvuHfmDH6b+yN+TZXQI49VTxJoIUbvNdKr+3i9rp+2vahib4+zEkos28ODei5fO +FJcZrjsM204kVpVz6j0HQRHE8SjAfUYO1jHLI4h0FyEFedF22nMlwSMCgYEAzaOl +HxZwiI1eUto2SdmT2TZZWE75uTn3ihk0wfJar//LG1uAxrqHN0g4KVMqgQN+IFJd +NZ2FPtIQ2E0P42lID2fGtpwDnMCAn/drpvhxDU5i2gNEZQ7LIpeZOLKC6NDQqb92 +8ReuKdPQ3QmMJ9uWn9A4IPkS7c1WvkIrEkTgYEsCgYEA0gEhlOTduOK+9kR05rEi +hrFeJ8vTxSD/XhZ98IEKRtqbT4IQI968XPICuvOr+4AYQVc7v+uDhBPxrBEtiDIq +G/QHlLFbfux1+9DrexgxSOFdxh3qqG+oGzAnGGruFqWf8w9riEbUgsl+7jPQB07Y +bHVBfhWl4ayLlRO/uK/OMfsCgYEAvYLbkIvQh7eovrhFIcf3Xk2dByo5L/+A5m8W +VMqx0tLsbjjks8CBDmxq/YKcgCsk2EqvhdK2Uk+roHtcchq7gx8DXZToK1So9UNC +na0GGtordXlfVbbNdAK7/SleRYrzUgyWY9eL9RY0vQ+ob68J4Bw3LgP88tSy5UH7 +iamaEoMCgYEAmuf1w3Sa8cunGBrzljwhr/5CjG3zDI1+fdFUePivoU5NocsKrLPp +KpkaBa2jcLXxf23NQBNNDWBqfsi9w28/5kl/gURT7ohOcf4YR+Ypnywpb8HI5mb4 +XxOG7la3Y9Mhaw/iC/JWbDgb2JORQmfEGhTn89ggy7yu52brz6t/MNQ= +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_FARMER_PUBLIC_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUDuk4yYHfeEkTr0RtsKoTAPWmWRYwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkzMFoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDjKy/CjxjtUi82UKxLzsR39LAoE9BUZwLlkvPrugeudoLq +EHlKTolTuvlFJAi1e5PKjJiHdKW2vSrpt9yBj56qw2xt7eJOQKxIE1kdeCdLoK1M +WaVWVekONJSlSqcHkW3NuBVtbAKJO+KIdTMMolYJlheQKBZp3JsSjLakDUQEoGyb +GT/+/xo3OZciBk1acmOBh9mJ///p8LO/oFZXZCXSau3+9K9vzcwS5J3HkDjkNVxb +AJny8gEe6ux0rXzkZLfG8WA7F0SVu+493kDHB4TO6o7UxAWatGVcyG+M3ujJ9iEg +0oeyoCghgpBSX02gM0alwW2cWNrkgisf0tyuJBRdAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQAqtpdHxteYdjGDUaNuVzRN +1qmnmD8m+scvNpqYU5wQ4xawZVFyx6k9dl40BYeYvTcvSpBmNoy9fTpuTYVeeOae +glreJiKnulsbMRHwxDjCKvhoCge8ZTsX/0Atfkb+AHNMEN/QXS857ovSy8JoV71Y +S3ZGa01vG6AdHfpin+xHuDBh/B7bCHPBEwNc943PgAeQ09X0ZP/EFb31h2sK/eoR +jytU7Aw/NwCnSaA689xoTmtJp2p7INSTXJNO/rJeRKPxKNF3Jcg9Ngtci7CxkXzi +4+Hs2aSkIAYj05UcYdiLs4TNR2ZrPKA4/ZSZUCCDQAogOEUg+SOMGPkevqpojYvf +-----END CERTIFICATE----- +""" + +SSL_TEST_FARMER_PUBLIC_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA4ysvwo8Y7VIvNlCsS87Ed/SwKBPQVGcC5ZLz67oHrnaC6hB5 +Sk6JU7r5RSQItXuTyoyYh3Sltr0q6bfcgY+eqsNsbe3iTkCsSBNZHXgnS6CtTFml +VlXpDjSUpUqnB5FtzbgVbWwCiTviiHUzDKJWCZYXkCgWadybEoy2pA1EBKBsmxk/ +/v8aNzmXIgZNWnJjgYfZif//6fCzv6BWV2Ql0mrt/vSvb83MEuSdx5A45DVcWwCZ +8vIBHursdK185GS3xvFgOxdElbvuPd5AxweEzuqO1MQFmrRlXMhvjN7oyfYhINKH +sqAoIYKQUl9NoDNGpcFtnFja5IIrH9LcriQUXQIDAQABAoIBAQCXviRUAQQ0mp5A +2NiOdtqUClWVH88cYgb0VRosTwKMjktakJCEizt+O7oAblaG67pIJWxJpyh+jZPZ +tOBNhyMEjC+kqq9teBPcvVfcsIMHKJg6FPO1XQOlYogcdWZnTsSbEyj1A54aD299 +mVP1T4bLNoAc4jo+kobfeDEUGmxh7Yj3LICr+D9yPzqKsNJAzbWILhtYzR/hNqb2 +JGBdDXu0usls/oSjNUYTAmtATQjVs5TbL7Ver+bdfDYaScBR8GqfavCTOrjGUaU/ +26yPpPKpycxL4biULej6SwmQm8AXaftb68+TaZ767DpxY5/3goqBfjKlu20WQyj/ +pTfxaasdAoGBAPdhtDkFddD12+tsm4F17RHAp7ODZ5w0Wd7/DuyRPbpq74fXCYuA +5VSj1SPE4t5tAqUn9Vo7zgxBoZIdeHWBAIdunQw4bst11PD0uckzE5oMDxj+lT6B +7XFZXXJgcRtYI8c2na9/ISf9I4WtE8Y422xPZl/U/FV1+k2sKuOLm6r3AoGBAOsV +NiDuMqR1/IopAkdZIeMMiunaLCdwkfJ12TQzG4hGDxE5Nfog90cEoJO4F6d9IZcG +qtmlLiJjLYILX5vvthoQ2STVxLxSWVcWdbhYth8PTBm6waAfJH/00lmZtI0bHxlu +eK6jr3QK7Rm8PFKP3xbnKr6BkTeL5aOfs139EXJLAoGBAL0MMnD9BjspF+ZCulfl +6cSOSNo7iltp+mAa5KnOmLC0ddaGc6njV94l0YUjOgimn8Xc0nghieX95d5GnT6W +1fOpiWTEX48mvhNhwfTLDqjDnGoKa704B19+3pXAs88kvTrJNxndelYX8iR+zsTF +wJF14BNOLYOVxDHFZ4U6tDyjAoGAWGfjsUKi3OJaFIMTjk1gxwgCfatEi5hz6mCT +TGQj6H2gUPPY7rXTCGwfDy5eBuix5x/kxHCwBtKRXKR2Uig1rVvErWuOztuRKYUS +xD7oTonsLojjJBpSGaSyLmv1UFNwwJmg3NxFsTgirljxvoLIfM52bqo/OEpuE7iN +Nb1kAD0CgYBb60T8GmoWRS4rS20J8KMs2Cmk0XmJHHNlSweYwO1fNFXXUwYG+bWj +ZNdOc5eLtNBi8at5vbK+c5fq+IRfgYZbdbFMqZwq366kUc5+BnCphdKOb4M9M60t +PtpalMa4s+J0SM3snolKUz45Lnz2KXVu3vy+rXScdo5+RJCNsD/xxg== +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_HARVESTER_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUF+41lzP8r7pBiPCZFG7cMvOPcnowDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkzMFoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQC2wkXkBcUaOlajfjIsrbJPbKnSspyB9PbJ2nT/Nga/za94 +79C2tTntMhbh40hT6E00HHxmwDOpzEEH+C+vkmhR3zkTFmP9uV2jSyylwBwWnTru +9io58MA/G25FMYH7a/3lKYspeh/mSKWqhV2xN07Mov9JbNAUVT13oHvUMlmhxaoX +pTKehNH6u3dm8ovYlZyimVRbu8d88Ke5atjV613Ci1zV63b/Vt5YmWR3bAw6h10W +UMSrh5pDEXuKplS9dTYOwfkVLhIinFSbyw66k9X1qNZIF2M10rvW8FsZxwtHbdAI +ArD4kB7FrsoezBW2+Drap4aeHD1cMOYZPh9XnW05AgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQAs7LdVyhNjP4DiXaKCy9De +7skX66TzJiwBBQ+fXjvAcOZCmn6lrje5ZA1pEE2e26huygXcADPntZJWgaV8uQ0M +SXml0t5eSbDFlxOOOVTk7bRyt3+Wj3kz4N0P9ehLKtemKd7A0kYrgcFhylQrEHFq +Ijo8uEhv77AzhJfNHDbqw+qax3rvOG2b1H+7yyn6u6kdiOVbGUd1ChIh1zTFFYrT +WJbF+67bEFy/AEUYFdK53x6M94NWoBKqkIrgvbjf6G+++6IiL+PpaR0j3Dnu86ea +anXRdbkhy36143Kk8GUy/Vx0e0L+aAbLDwobngzvMYactqDo7pqcDaORLSLKiQT5 +-----END CERTIFICATE----- +""" + +SSL_TEST_HARVESTER_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAtsJF5AXFGjpWo34yLK2yT2yp0rKcgfT2ydp0/zYGv82veO/Q +trU57TIW4eNIU+hNNBx8ZsAzqcxBB/gvr5JoUd85ExZj/bldo0sspcAcFp067vYq +OfDAPxtuRTGB+2v95SmLKXof5kilqoVdsTdOzKL/SWzQFFU9d6B71DJZocWqF6Uy +noTR+rt3ZvKL2JWcoplUW7vHfPCnuWrY1etdwotc1et2/1beWJlkd2wMOoddFlDE +q4eaQxF7iqZUvXU2DsH5FS4SIpxUm8sOupPV9ajWSBdjNdK71vBbGccLR23QCAKw ++JAexa7KHswVtvg62qeGnhw9XDDmGT4fV51tOQIDAQABAoIBAGS74kAT+hdBzp1h +IpDD0MO8dkJ/Voq/FgQemFxPUBsKaUy0iosaiuo1sK0jVKuDIIK3rM4J5LATuEiH +QOl6PmvaKSBfOBASyw0Fk39sy06frWsnXhD/pUdjfD1BU47ccF6OrnjXKpwIsN+z +kPfsL0/WC/ZRtsNuVGoKmBZXBlaGqXqJyuK4ZUpY/qkhgmc16BZyUpYbe2dAjaEo +PoVri1pWGCnrOxOiBakwDAh4NQ340WKROIZi1/Z6q+oTEDBiPh17TVt6IBvL4IOl +fAXHocnksr4GatfiGEPnLkIeIo6OLNQ+bln11Qhpemmyr12eRbdpjBGcV/Pupp8q +fqrrWYECgYEA3e6LmnMqGHNg3HMUj3nUY1K5aRtTo2O/SsCH7hUXf43EXG6cSgo0 +W87cs3e5FgXmxId62CQsZF9jLYtvjN23TQMbqQnbSeUJH+gElRLowyD3x2jD9u2Q ++bG3QlJIFGjxvfVd24+Zj9OvWRNdk/pzbqHq/zFCwdTNB5P3E+qWCukCgYEA0tBQ +Ozw4HowUMJdwSLDgJFNAkeAsX6MnXUk9EIr+vP2fusKxKLpHBP/Ojk/X1MaBQyE3 +4q5EkL253iHOJ6+IoKQANgtBlADpScg08rfzBdY53bvZBN48wQTDCsfRtMdj7g3j +ALVLd2+zmx5KCYMab2nyLGSTgnU43KwXdRE9PdECgYAZptPeA0evUc61TFvpBYTm +Ag7KNk1hikr3Ae/0Nd3kcWdr46EO8cUBg5SA7eqnwADfYGVzjCLRazEUd5RqLMpe +DWjqDeiZzu5SEMhOzsO2oh3hn5te9DCYm9D5ynboXQTsFutFUIDIXghbfGCJlR44 +gGCgJHp52vXj1VdupuO22QKBgBqI3+Bk1wd5SD1JgneT09Keq/zwg4VSKu1B/66q +YB/3qHhAcz4WHERT0nL1N8xvY+vILZmZp8W0K8X66VDzdjYKDoC+7/UqHDMOQSPf +5XXxnfz73PgQ5QLCj642sn2Xr0tSciUvrJ0O8UNwD4+c8eHeKv5NdoewK4UTICy7 +C8NRAoGALmB7np+s+wCZCpA+cTkgZpzbqnwfRnqCrBx7Yj+ZVAa5CDZWqztwmSkY +u8Y5q/qsPB5Fbno7rVAWuyk/smrDLVi0w6trv6RaOS4dhhKGSOhsAK2jXka6OztX +y7mCud9RuNytEuuLjWQFt5syjyrmVtI55TxHrF9Q9ylQAdvL9DU= +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_TIMELORD_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUaTJzz6JzsH2ZddKgtyDaLvK0+fgwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkzMFoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQCcusqOeHXjSZtVYgT32PKxIjsW/TPBlv/ovjQpFIkBZTCC +Rp+O0BNUJ13Kv9MVyyprKlR3EMldjIxolACVS+UIieuIERjufyMQVBuqJg+qr80l +rbBwcKmzB496Lopi4DpX3W3kqaXd1c65RNCAhiQ50ZfEvFwLIhRzxC/ZofRW/iFX +0B2x+LE7TZ8uEXhtzVPc9GYVnEk/I2hN426pX59WmF/+xQ+HKguzCCBQ/7V6TiS8 +1dLw4z5aIKedBgvlX8ibDkCUp6jOOfUTh12Xx6nHd3TX9qhL5h9zDfWbdoBdr3PU +tYEgNXrYVq75HTlHoSRCuG4SccoVCwNzdKyjJ4mDAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQAKaLVA7Ogf7S4gIwv/3K88 +PGJxIhTcgMSyCVdKnPLWRUEofdZVMZEDcVI+l+4iB8sJ0XOs7fZp+bAnaQfgXzE2 +wyWAODuytYlYZYc42xlT0PhqNnRKLUKf6uLCtNZFgpmiVmqgvAkzk/SJXTPw2F7q +hFA8K70Z6Wh08rZCP3hXLk5A8h6ejcNBHAoOg7B8Z9AMPlHW474xoGUrLjl01H4S +6C2/7D/URCQFBTrLt4sIvtxe+cAtggB4DVpJk7eoxk7JL4CsRu8flqGLn1wFraqr +jIb+X70mOXY7cO1R0n0uJLgHTU1dDkMlV3xOVfqhE+pe7caknwI0Nfzvlar491Ct +-----END CERTIFICATE----- +""" + +SSL_TEST_TIMELORD_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAnLrKjnh140mbVWIE99jysSI7Fv0zwZb/6L40KRSJAWUwgkaf +jtATVCddyr/TFcsqaypUdxDJXYyMaJQAlUvlCInriBEY7n8jEFQbqiYPqq/NJa2w +cHCpswePei6KYuA6V91t5Kml3dXOuUTQgIYkOdGXxLxcCyIUc8Qv2aH0Vv4hV9Ad +sfixO02fLhF4bc1T3PRmFZxJPyNoTeNuqV+fVphf/sUPhyoLswggUP+1ek4kvNXS +8OM+WiCnnQYL5V/Imw5AlKeozjn1E4ddl8epx3d01/aoS+Yfcw31m3aAXa9z1LWB +IDV62Fau+R05R6EkQrhuEnHKFQsDc3SsoyeJgwIDAQABAoIBACKhf3plkZ7sN79x +Din5rP6I0sesoRAInnk99eaR3AgL5OEUW9NBlNPGcwoGwsyQ/Ml7K/i3I9dg4/GD +qnFSuMPfPcTuCjVAsG2+N/KrwFB10f2eWFsv+b9OT1yvBfL9GscpbUvWVIkk4i27 +z9pmSYDhGAjnmer3188QrYYILCZAXnYmTQrqvHAOJEpxf7/4z2rcSVbVdez/58He +xjloGfKSWfITpPLhpcM0Fh4cpx9fUJHgK0rV6CcL369XXHiJZ/OtEY6n57jqfEZd +SQMUCAbxyZnb5Trl73ucBdcAkELjyURstlEBXuMQ65/USAZd1ndObjt25hYRBVTy +SBf6VnECgYEAyVvg3xBZduR8ioxnflQoyQmrgWBN3BNVNIypmMicF1fsaHVAXXB2 +gk7RjSKLNCzEZGkqcIOfw7nmC/aRdb6K6xOxeA2jC76ZP65dRqczOzDR7sDtW7CH +3zrMGMtvvjxzhtA7JhWtmDBmVzC8M3JeXiaVKuAjtEfKqwdB34RXfBkCgYEAx0KU +1ZCsIHY0BScRg/udckakILTVI76uGkQ/lEnEcbnTt2SB1NE7ityEVhTlcqWoKLbL +6N7X6BLENAifVgi+wj/n/v/OURCmQZZIWSgJNpV7q2XEURnRgcLqznUU95A8S1hq +QVeCABmzzNFX9NUsxG2MXkybqmgPBAtmEVYjZfsCgYACh1PYmUT2WEI0HzVBgd8N +P0DXHBV+OQPt6AJNN9+171W5rhdD1SC33DOHeTKUUieZNzTgOtbrx07bQZpsBxuJ +fjLRViKBCEC2awMi2wCqsp9AR03zw9DA/eUIPq3Fjs7Il57WiJkoexsd5y/F2Z7T +wdpHso7gObKS2UF7hBbxKQKBgEFRPw6e5P2jIbxKqUA1e9AY/fZGsNONzu6HTrwi +TkXlX1RvmuuTRsxnKu443Vnumaf2+/KzEd1eQXi5FtoR9c4hOsBpRE1ogsdfJwoq +yJJe//IYYvke2IGLzoKs+JmKa2lba0FOGSxFQJ40RXvZYVpmeqvyuthqUfnGbsfi +D6p/AoGAVQyoyJ3bgy2xWInoq49Dc99uo4bLmn4blaENuIUrEDV1p2JTd41jRLGx +pcv80SfZZtMa5YclBoezEhv8mrtBtnxNnNJ1UkDPmGagG88+avl/8FiBNtB+rZru +9Kh/jDmFddfY831nOdwgCMEZB/x9qCmyRTtzE35sKNZoAY2AVfs= +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_TIMELORD_PUBLIC_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUaXeigR1NhYcViVNvWOSNgb1hguMwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkzMFoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDlm9CbdDwaD7gUkwY+Y2by3np+QtBFoJtk4hcbdAQ4Vd27 +7sJGve+iYg4vCCg3fdpxKZrM7p8VlRlQOMu2+Nkel726yhvs9hALWx0UVppk4iKV +rj/dzd/Oiwaf/Tk3DwMnfiv4RyUU1YnVusUnche1KKHfwHGCzLAnlu286wQXruug +Xl6onBQiF3rDqkLVrHcqMiasCJHLS2S9xktDiKwNCphATHFuZtzUIZeJK+yNiF+M +JKXN1xQFq8k9kb2q8y8qAgf1dY3wW9Xj4iP1Qo9Bzx1CxJTLpezIk3QTcCWKUkE+ +2/4Nly+M5GoJDv59gzjvPmyWPTr6TZ3lrls/Z/rjAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQBQBbv4tGvdNY+P2A62GfhR +cyNDjXUKk+YHY64hGeiXp61l8e2Hf58lV9lgBoMpniXUvMk9Nx0g+d1Gsoc9VWHs +LP1MhVaf0zodeK45jJxuzCC+eDHSNmvhkUJEVD8LDgmHTJlddpwYBwKocyr4m3XX +2QMcgiKcYaqIt5MLFwCWuarQ6bXKT88qRVTxKTA4FoqXXqJKCbAsEMql1h+dvXeV +yTwPK3vuRrZxyiIYlILG/tgNS6P+qUwLCnYjoPX2Ml3djMkUhRBdsTWR8wWTYkb0 +qmqKNsNL7+OHzUI+Z8WBoja8MHVZohl0iDAvFO7Y67ovldeYS8mlb0Tv3nVRUhiM +-----END CERTIFICATE----- +""" + +SSL_TEST_TIMELORD_PUBLIC_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA5ZvQm3Q8Gg+4FJMGPmNm8t56fkLQRaCbZOIXG3QEOFXdu+7C +Rr3vomIOLwgoN33acSmazO6fFZUZUDjLtvjZHpe9usob7PYQC1sdFFaaZOIila4/ +3c3fzosGn/05Nw8DJ34r+EclFNWJ1brFJ3IXtSih38BxgsywJ5btvOsEF67roF5e +qJwUIhd6w6pC1ax3KjImrAiRy0tkvcZLQ4isDQqYQExxbmbc1CGXiSvsjYhfjCSl +zdcUBavJPZG9qvMvKgIH9XWN8FvV4+Ij9UKPQc8dQsSUy6XsyJN0E3AlilJBPtv+ +DZcvjORqCQ7+fYM47z5slj06+k2d5a5bP2f64wIDAQABAoIBAQDd6juzk6LnGVwz +3mnBcLc2ctp3H8JGGVU3KuFkcjwF6s+U7M0uLDLogdbtk/eyslum1aw890AgTuuZ +Qlt564eFbuk8GEznOGcHYrd3ScCNUpZUjoZBrNHrwSjVBpv+3+6Pg/2hR7nKKhy+ +ynX0iuvo9m2FYW0UGxsCGHiMB6T78QoxeHczwLq2c8isG9cW4xsXvFOls/5du6a2 ++wHP0qMAk6BRJ2XNlEw5bHXcV9yTdLK+HhSyNDtzUGTQuj0BPAHKl31M72JsHpYn +ofiIem0+P1XWj/pvPAofNp545vmjfhlVTkkjEjwXZbSujBQRhZ5JRshR9T3nlB5a ++WKBHVJhAoGBAPVvqmMPbW2oOIg4drBwgTIXIgBvMtUvIMI2fDqyMm+aQXfKowty +17Q470D2RHZ4CKm4KtGxP7zoPnsJ39XHXTrr2gono3t+mghwkYuroFNXOs0y1P3U +Y2BH++6rGpLv8yxMPaJo+1mVjXSIzeJSlGgKGaoy/+9qbcpf3NcUv1BxAoGBAO99 +wPlQYHgrp9C0KDjh8OWXiI9HdCTBZ+qE7AuiM2HGoF4WSe+BvpeBWQTzPcEPB/sK +nHW8GQ6u1TPpJamuvdg/hnLOmx7sPwsD98FkocMmbeaf/vnlSwXNZEdbypRNeggy +5utbfYf2520dmn73dMGEZSXfgj7ldJgouptBHmqTAoGAGcT0rdvz0FymOt86zwGw +/vJg1ozWWH3PQbT4zCzjkMYwc4RqJAyVy01jCX4R6CJoPnGgxU2H9Kypyr9Zqhd5 +mXMj/Ib26kN7psEy9ug5OCbjfKIGrPP8zyIfuIpsitr4vEDxA7lkEp7aME8g1s92 +14mf8jfSmW+iQWpZRJfgEnECgYBIdu6LAY0PD3aJqdl5zLPNZJqHcAFula7RwUKD +CqMtdKJVlbztYX6/7P14h/kpj1jE1yMcZLvYO4J7YJJq01rSMfaGiolZQ/aXaK3w +sHhZyij63XKTPpQCv3EWPmn/kanZDT3d/SLwnv7Lf2ed/1Yur9bDLOwGB/vNhpVg +IMYJTQKBgHp287GYTmfEIYQt8TwpFdIiehOc51bV62iD5ittj8oh98s+1+TxVvED +rpkuO0w2IV/CCMPagmh+5zHVeEjr87w2qTEzukw/uhsyqDDlwgStxv5mqGTHPBDQ +EDnFBXhZeOJsVFQFZ89HQ8hR4EyvkDRIW7X5oX54VxizlNpfpb4H +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_CRAWLER_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUdCBu6ITWB8mIr3CFYUGSoidgl9IwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkzMFoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQC6CqdPjfQSDqwE+0QwTkW3Gq2z+SdyOkCbA1f65tpk9dzN +yLx2xtHRV433IUFRxY4slpeO/ff/yWI11e8aubv1TJt1Df2XJWsrTSpasHiB0cQu +sBIUPCw0KkvNsZ93UVRArKyiNIut3tFTvM54hzUsSpMnH3gUiF2osJP/qFi9Fm4w +nHhhvjnm98YZ73r9qmS7x1pRLkduOLauQrZeMGtfqPbrDn+zrXISJz/WlVs1RFtj +S//fsuWYX1mecsowyj2uPgCiWxd4uYcnYEmv8o1+3yMucR6dWQCQxxLFaN4vgksz +vxQRQ+GzLYBG4rGXaJhuTd7zNX4MAvFC7dZF4Ey/AgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQCWFqbiMrpwtfPP9S5YKnFw +kv6aDeUeq6GYfoBdKYnVpfaXQgPXzc8eiNXptAPuMIVoSn811Hg+p9SsEjKBjPlZ +jdbbl44S2owhrJ7N4H5qTSoryBvuTwlh+aQDsDh+x5uO7PQzb2F3yU2cM2MvtU+2 +4pYr0Pucs2A0XzqWp+cZLhWHcTOm391Ab+Vr+Q/ZguvOxbmsBCs+qwSlPaktd9Yw +AqNKFau9hAnb++jd3k08EN8GBVlQBP/njdEoWEGjHD2SnTo2z1sv5jF+t9E28Swm +QbkNYNrgryTYJzPDZMMrK6DkkEFnH0djbKjwAdkooHr0pa9COcuYMhsqNMZ8irgY +-----END CERTIFICATE----- +""" + +SSL_TEST_CRAWLER_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAugqnT430Eg6sBPtEME5Ftxqts/kncjpAmwNX+ubaZPXczci8 +dsbR0VeN9yFBUcWOLJaXjv33/8liNdXvGrm79UybdQ39lyVrK00qWrB4gdHELrAS +FDwsNCpLzbGfd1FUQKysojSLrd7RU7zOeIc1LEqTJx94FIhdqLCT/6hYvRZuMJx4 +Yb455vfGGe96/apku8daUS5Hbji2rkK2XjBrX6j26w5/s61yEic/1pVbNURbY0v/ +37LlmF9ZnnLKMMo9rj4AolsXeLmHJ2BJr/KNft8jLnEenVkAkMcSxWjeL4JLM78U +EUPhsy2ARuKxl2iYbk3e8zV+DALxQu3WReBMvwIDAQABAoIBAFXwie1EA9U7ldcP +QyaVYbr9xfP3SnOH2URCPSgX8BbnREKDUhwEJ/RuX5QjdosRmWWbgxN42lD8dDOu +Sa+s0Ni1tLJT7iseC+2Kzd/q2SAPCHMQvqk+SqUmTLIONT3nAeaGXZDoeQWugkAO +XcprmvKE2IkgDlZdz/YW66cT2zotEggMAubtRTRZy5k9UwhZkHCB98dbCrLKDS2T +TECySx4OnhTbvVheIjXLZSHAI4KDyOfnkb4yYiiOYNnXaGMMqXJ6ITc4L055W+Sg +aqPXxifgK04cuceyciQH74HxWypFQMJX1HyOJ9qz5dF8FsJO8vw+USEcsMQvkaSi ++/iT02ECgYEA7cLr2c2vtAzEz/KqFH1ll5LALSmFlDfIr/s7ljDEmrgMMuJc8gQ5 +2UPiZhd5DMUBD1yncAd+O8QjoC/GBecsoAfeRYwowCO1VFZ+EdbL4X5NwdL4HwLe +KNiR3EzfWpEZPO8FAKHJdyoZhogV2aj8Wd1JgcW8xrIQHZIImeRg3scCgYEAyFAT +h+h2zWk76JTAvSDLWSRXBDdYuMc3dINu0Nv3pY86Kpxjq8nFdsdsP4gIoHleG5M6 +3RWljKFxALckNzgRoRt18GqX9k0c3svXWY1UhVgI+TFTwjm5JgkzIvAzWqB0BeZ8 +scxNyAkn/sYBa+SQw4l66Cq7uS/4OS3sC90LCkkCgYEAtsm0KK5I9lMavAQDXd1J +zU21EQNq/pgkYab0GHNFsuzr8/KzIhy9nJrj4zkIhxitx/GjiC06jxgri2svAjrH +xABIkY8/hPfu3/fe1DgeZi2D+g8HUlASG7Tj7knrLOWAUagwYFwBVuu21AarRbr0 +xuGpMWujxd3/JbyvgCBjmOECgYA4LNS9WYDvrCJj4EuI/ohocFuC0C6uaxfvMejC +4904bclHJ+J/y67314dQ7cpVjpPIsephE/AAV0oEhFfAsJWpE7VofcwuA4QkKxAy +igL4/i5OC/pMTrnQo+XWV3xfXv4KpY+0oPHzNjYkKc0+P7QlUgnI0CsjDQPUrT80 +OIIfGQKBgGSf00zTS9KDIHKtWkZi4qdMmUDiIoxlZ27eZelfF7XMNxm+eP5gtGPZ +Hq0JKSswlMQZE+KWakyaVh6OnGRdGLM2yuU4tYjU5ZgJ/O5BuZykCFLb4OPq7Uos +Wx8tJU4lx2r0q+TFzVwMv92K/p0sIGvHVfTn9/xNwtZAI561vEcu +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_DAEMON_PRIVATE_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUJWB6PMvY6/sycKKe2rKlyk2Jhx0wDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkzMFoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDpNyGq9RLF09KE+uiFLgNAWUXiUWRDWS86+S+knZMa40Pi +iDdpxcRxNgxUVioleNfKIuXQp1eKCwCuAv8js9hRqOIkEfZHtiNaai4iJWYKD8v6 +/Mh1iTQ8evvsxR9Edlr2vpf86uWcIZ6NfD2R0QuDNi/rEQimilQr9O0zgpVcZ/rX +lu/ApUkBZP7nRXSgFDwyxub9eGO/XXYCFiaznFEewdEwMxJ7mekMejT+47QZj1PJ +JLII1sgGUMOnwauUqub4L3ibcFtWVI948ckq/vaYsX/MVYZ+Vr8sDpcuAZC+2FuZ +Wj8cjoFXGG2uHbXilw/+9uHiIpH9jE7sQLz7RUOjAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQCCivAYNk17t6dwt0l3GmiO +eEQp2nXF7hkt3Nws4+GefOrcCzyTAoinwGzoSJpuv6YdIp8txyylWeEVVrvJRXyg +3m648yIgnp3U5fSD6a2V7GnCSOjCoT1dlcIMFPuOk/XKN/0acfAEsJGscS0y/mVQ +MiJjjNv3K+ZzY8nDu+UxdC+cqLaY3KFF9QarWNjyiDjRi+7NrJN6mdGtZITSND1w +3RXLCSaaB5VTHqkSTGNrSMr8Y5CGgXyPq9sQMLS2Hfb8K/Y57xlnuJJEkHo09pUF +mz79hUFHqgjrtxAd4UoBUujTpwBN1on/KFrYa7fGwmisb1V35Qjjogs8AGD8tnh7 +-----END CERTIFICATE----- +""" + +SSL_TEST_DAEMON_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA6TchqvUSxdPShProhS4DQFlF4lFkQ1kvOvkvpJ2TGuND4og3 +acXEcTYMVFYqJXjXyiLl0KdXigsArgL/I7PYUajiJBH2R7YjWmouIiVmCg/L+vzI +dYk0PHr77MUfRHZa9r6X/OrlnCGejXw9kdELgzYv6xEIpopUK/TtM4KVXGf615bv +wKVJAWT+50V0oBQ8Msbm/Xhjv112AhYms5xRHsHRMDMSe5npDHo0/uO0GY9TySSy +CNbIBlDDp8GrlKrm+C94m3BbVlSPePHJKv72mLF/zFWGfla/LA6XLgGQvthbmVo/ +HI6BVxhtrh214pcP/vbh4iKR/YxO7EC8+0VDowIDAQABAoIBAAPMBP1OjiawYy4N +E7oPXLgwe+XKY3KBQjaYlgD9G8cFSjam9xf+w0sAkUTSPk9r3z+IP+ucvd0efr1W +iSbgp7X0pPHnZPYX7g1ryyJ9L2McjLqiYPUg7bvKWM2rC+5Gawa3ZawVevWGypkN +G8eC+sgBGtid52EmwWYRz8bV6m6drs+JJ0wY6TBMggL5z7aOBgVmMhJosdTxBlg2 +mUMYVGXFkwhoOtTvQP4C4jTJ7cgLAqXvfnKiT1LAWrt8uQLeplyHCvabAP2yyM45 +YpkTMwtlidK5dJ4qL1TFRCaHrQzRcgg41GDB/BR6MOLo4QNyh1RcKxISCM/5pheE +Hry6x1ECgYEA+ZoZ5eAuAwku/073VtL+nmiL5yslEynJVf3WoW7BZbwjBdE1cyPL +kiFT47Pw+RYB1Z5OWhPKzzeWrH11I2FP1IwuahPbCrM04Z4VvjYrXj6GsX4CXLyT +9zyyGq4WQh1eW+MwZk9zrTgL2H70Wkybl6cdmDssXKV3Sy5s1rc1g9kCgYEA7zGA +MpqZCL0FsvFcG1Bi+VcDtBmjocQokeyCWHn5/dXKgfVcVeLxJW4ts/sFTtB/prpH +QZ0Na/Tzy93kw8y/yPZYCCLSl7n4c0+oQv0IWZa6d96xXxx/9n27wWg1TghJFxBo +kDPyFIzeq3kdYUQJrd+G493hH4Iyv82cp+viodsCgYEAuObsinsg+sTB2QYBeoNB +dc3S3gP7KhAJgzdQ2TP39sqBU1zg8JOyyWUBBSyWtZ8U6s+kEVyaIBl49/zUWspK +3hSeiZx95pZM9Vorl0X/qIg/NZs4WsSkBEIlWlheSsoAzacmgpQXCFn9hHq/v2kC +1jxJUy16toMpNTuGCyWbcjkCgYByAdENzZwlixrdSKdTKYSTPcM5I4NXxkxkCSuz +iif6sdz9BnrFQQ8ZfSNxhrLn9v7w9BakknvkOfO99vxjywKagbhB4H8p7G0cYRpd +G4fQU8R6//zgzY+8Z/+G1umZUN+ti5ebK/c1jlNPvcGgK7LFWiZME+SKhR81RoZl +j0wNQwKBgQD5QzQkxZmprgcoMfpg9KQKnAnEjRT09v0JygBCSkc9lyjU16xL32hS +miOy+/MGYilWacKiqkky+AvjnjHRtE9lSmvxaLLkuObFuMwPyELDOsQkkKEofztG +HI0r3T+jYkXCDt33mW9uE84DlJf0n5QQbpEJNsfumB6ODTh/rLl0MA== +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_INTRODUCER_PUBLIC_CRT = b"""-----BEGIN CERTIFICATE----- +MIIDLDCCAhSgAwIBAgIUIOpxHZYryCoSWIfdQ8uCSoeS3KkwDQYJKoZIhvcNAQEL +BQAwRDENMAsGA1UECgwEQ2hpYTEQMA4GA1UEAwwHQ2hpYSBDQTEhMB8GA1UECwwY +T3JnYW5pYyBGYXJtaW5nIERpdmlzaW9uMCAXDTIyMDMyMjEwMjkzMFoYDzIxMDAw +ODAyMDAwMDAwWjBBMQ0wCwYDVQQDDARDaGlhMQ0wCwYDVQQKDARDaGlhMSEwHwYD +VQQLDBhPcmdhbmljIEZhcm1pbmcgRGl2aXNpb24wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQC/LMgHvqhwBTGYi8fLSDC5BAMvpZlHojkCtD2tAe8x4xES +ziFbEN0h3majAaFEbKWf38QwZb8+MTqtqC3hBXLlmQMOHJ98B+pcJiquWmDFadWm +mRv/aSsDcN8Nuil07mTYVl4KjqJ3BIuBgBF6VHU5EtALFPwM/xnFONPB5BC4Gm6X +/Bb9QQ4lBZVbXvuaHoiY/qXZeM/M0J4jGjgW3CvFKx6Cg3QrSPTZQRo7YoFOJzCe +WEU1NjaY32vstiK7qTFJCZ+Vfk3NNU0QealoqUdd7SeTNgPwEpYlTLalyKJHGPAU +yeq2LHmUX9F81MZIdk3Y2ugwonIjdAgo2d1Bft1nAgMBAAGjFzAVMBMGA1UdEQQM +MAqCCGNoaWEubmV0MA0GCSqGSIb3DQEBCwUAA4IBAQC1ACGsaR3WXsh2iMZEtbFU +JDbmDgviv1WCft7Lt0lpTWCNq24PXn5EOFU2UPBcsLCjoMfgvjj+b4dVcGt7mX91 +dtMIwEORe9stfr1Y1Frql9nMgk8PyRYDt4oUUUwwZ/MjqU3ZiP4znsiZuEGTAdKb +BqhVubSZdYOIrYV5Chbta7/OXxXpOv458CWLKqvfoDVsWldhQDWOkeh6nz8dkPO3 +Jt0IRUPqS7TV2fUcbHLpO3z1xUxL83xN3wgoBafSSbvVcLAN1+//+1JqLPxznSjV +rYf9ERqDwJpIGysbRvIyu+ahf0zJwBwob/O6+1vuryLQ2SdMFIlk57U6JOkePfEy +-----END CERTIFICATE----- +""" + +SSL_TEST_INTRODUCER_PUBLIC_KEY = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAvyzIB76ocAUxmIvHy0gwuQQDL6WZR6I5ArQ9rQHvMeMREs4h +WxDdId5mowGhRGyln9/EMGW/PjE6ragt4QVy5ZkDDhyffAfqXCYqrlpgxWnVppkb +/2krA3DfDbopdO5k2FZeCo6idwSLgYARelR1ORLQCxT8DP8ZxTjTweQQuBpul/wW +/UEOJQWVW177mh6ImP6l2XjPzNCeIxo4FtwrxSsegoN0K0j02UEaO2KBTicwnlhF +NTY2mN9r7LYiu6kxSQmflX5NzTVNEHmpaKlHXe0nkzYD8BKWJUy2pciiRxjwFMnq +tix5lF/RfNTGSHZN2NroMKJyI3QIKNndQX7dZwIDAQABAoIBAQCoMQPDHJAgDdG2 +fbPHOrny7H7JGo4iIay6nkxsu3jvkO/idYuPDOUf+QSfgL2a72M/pqR6V+nLE5Cm +W4IRqLOPH/E6JyCBBI3BiKqgPk9JH3WiXq3tJV98ZX84GoKCp4H9eu69pwN0ZoE9 +66h00X1YOx7hwRKHdJ/9jaNvv/CdpsyNHsqK86HyI80WbdN/y0ib2CpWvDD0hMqz +x2svsUE7FPSpqqMVyXR9OH1gZ7xPDf9fO4/wFlK6BaGcbcq9/nLG5gsWMGGrH7i2 +yPGn5K80yzNW4b23SHVVcwKQY2+Q6bOu4t68Vvft+m1kMe8IkyEGyNNcksRjb9FO +L3RBSVTxAoGBAPMJ6dqGMBPeqkfk6QgAPTwVtJQerPO3O3hdTha8VTRY5k70xQOI +hC6fusAnuy//ZtqYS/U2rScEa3rpJk3UjVZ/fRzkSUK898MwHNiDNsFZrspDS/Yl +wYk0vRMgz7+cG/VfG2N8DV8BngxIBNa0X3C8A3vT/MsHVxYlsbGuZZVjAoGBAMle +zBzczZe1W2vkrvhTsIK6kovsMXBU9bz6VKMusZ8lWATrXkjaDYSCla0OT9IATfOX +w43KmG2t3Z+TXcm3CU9UpAZRkpFErd7hxGGApAP0TWOHG3NK3KiI137G4RHRoxE1 +eHpImN1MJFl0SgMMyOrVIvbfvb1jgcYcH7t4imktAoGBAO82LbeJZh7Yha+XronS +enL+RiuX+dEz41P4OlkEa7THX4ANSTDOGJQvYUeqk4KNlrXHOtQTSeBiaEuk2a+3 +apndh859H2KRzieO3oV4uNccJ38rN8QBq3kZsJP4MqK8y4P6ZWHJAvwlAmPCKwkM +pfe3BpLFt0Y6ZkwFM93X8mJTAoGAfjklyJG/bXEItUDLTG1pHwjEA2EyPC+FOcfQ +ddk3DYLjAXJnz1KfVohkOe3WqtP2CNMAiUiM83MgkH5XM7G/7DIp/qvzK4vZUPRD +nLp+FNx2BgUSd9pdJmdgbN9NBVZa2NajhkMrTswDnO7/1ZmV911SZV0qGiTdm8jV +OzX7zKECgYA7XzxqMEiTeyWnKbewk+QomhddzPpQ8JKTBVTSCY+LVsBaRNSLqloX +0jSpEfr1Zd8WocUYdheV+DEPC0XbUtdZhC33QYviEbnZ+DKZ+h5fxNVPf8MRBY13 +xDGazUAN3pGUwMzKB9MhLCAQl/iiJUEid1nxIzgvV3uQkeqjxJkffQ== +-----END RSA PRIVATE KEY----- +""" + +SSL_TEST_PRIVATE_CA_CERT_AND_KEY_7: Tuple[bytes, bytes] = (SSL_TEST_PRIVATE_CA_CRT, SSL_TEST_PRIVATE_CA_KEY) + +SSL_TEST_NODE_CERTS_AND_KEYS_7: Dict[str, Dict[str, Dict[str, bytes]]] = { + "full_node": { + "private": {"crt": SSL_TEST_FULLNODE_PRIVATE_CRT, "key": SSL_TEST_FULLNODE_PRIVATE_KEY}, + "public": {"crt": SSL_TEST_FULLNODE_PUBLIC_CRT, "key": SSL_TEST_FULLNODE_PUBLIC_KEY}, + }, + "wallet": { + "private": {"crt": SSL_TEST_WALLET_PRIVATE_CRT, "key": SSL_TEST_WALLET_PRIVATE_KEY}, + "public": {"crt": SSL_TEST_WALLET_PUBLIC_CRT, "key": SSL_TEST_WALLET_PUBLIC_KEY}, + }, + "farmer": { + "private": {"crt": SSL_TEST_FARMER_PRIVATE_CRT, "key": SSL_TEST_FARMER_PRIVATE_KEY}, + "public": {"crt": SSL_TEST_FARMER_PUBLIC_CRT, "key": SSL_TEST_FARMER_PUBLIC_KEY}, + }, + "harvester": { + "private": {"crt": SSL_TEST_HARVESTER_PRIVATE_CRT, "key": SSL_TEST_HARVESTER_PRIVATE_KEY}, + }, + "timelord": { + "private": {"crt": SSL_TEST_TIMELORD_PRIVATE_CRT, "key": SSL_TEST_TIMELORD_PRIVATE_KEY}, + "public": {"crt": SSL_TEST_TIMELORD_PUBLIC_CRT, "key": SSL_TEST_TIMELORD_PUBLIC_KEY}, + }, + "crawler": { + "private": {"crt": SSL_TEST_CRAWLER_PRIVATE_CRT, "key": SSL_TEST_CRAWLER_PRIVATE_KEY}, + }, + "daemon": { + "private": {"crt": SSL_TEST_DAEMON_PRIVATE_CRT, "key": SSL_TEST_DAEMON_PRIVATE_KEY}, + }, + "introducer": { + "public": {"crt": SSL_TEST_INTRODUCER_PUBLIC_CRT, "key": SSL_TEST_INTRODUCER_PUBLIC_KEY}, + }, +} From 42f5ba07da7e8211eae9281f118cc422cb5a78cb Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Fri, 25 Mar 2022 09:26:19 -0700 Subject: [PATCH 257/378] reduce indentation in a few functions in blockchain.py by negating early-exit checks and loop continues (#10872) --- chia/consensus/blockchain.py | 215 ++++++++++++++++++----------------- 1 file changed, 108 insertions(+), 107 deletions(-) diff --git a/chia/consensus/blockchain.py b/chia/consensus/blockchain.py index 06e440c921e0..c7f400e13f60 100644 --- a/chia/consensus/blockchain.py +++ b/chia/consensus/blockchain.py @@ -359,121 +359,122 @@ async def _reconsider_peak( return None, None, [], ([], {}) assert peak is not None - if block_record.weight > peak.weight: - # Find the fork. if the block is just being appended, it will return the peak - # If no blocks in common, returns -1, and reverts all blocks - if block_record.prev_hash == peak.header_hash: - fork_height: int = peak.height - elif fork_point_with_peak is not None: - fork_height = fork_point_with_peak + if block_record.weight <= peak.weight: + # This is not a heavier block than the heaviest we have seen, so we don't change the coin set + return None, None, [], ([], {}) + + # Find the fork. if the block is just being appended, it will return the peak + # If no blocks in common, returns -1, and reverts all blocks + if block_record.prev_hash == peak.header_hash: + fork_height: int = peak.height + elif fork_point_with_peak is not None: + fork_height = fork_point_with_peak + else: + fork_height = find_fork_point_in_chain(self, block_record, peak) + + if block_record.prev_hash != peak.header_hash: + roll_changes: List[CoinRecord] = await self.coin_store.rollback_to_block(fork_height) + for coin_record in roll_changes: + latest_coin_state[coin_record.name] = coin_record + + # Rollback sub_epoch_summaries + self.__height_map.rollback(fork_height) + await self.block_store.rollback(fork_height) + + # Collect all blocks from fork point to new peak + blocks_to_add: List[Tuple[FullBlock, BlockRecord]] = [] + curr = block_record.header_hash + + while fork_height < 0 or curr != self.height_to_hash(uint32(fork_height)): + fetched_full_block: Optional[FullBlock] = await self.block_store.get_full_block(curr) + fetched_block_record: Optional[BlockRecord] = await self.block_store.get_block_record(curr) + assert fetched_full_block is not None + assert fetched_block_record is not None + blocks_to_add.append((fetched_full_block, fetched_block_record)) + if fetched_full_block.height == 0: + # Doing a full reorg, starting at height 0 + break + curr = fetched_block_record.prev_hash + + records_to_add = [] + for fetched_full_block, fetched_block_record in reversed(blocks_to_add): + records_to_add.append(fetched_block_record) + if not fetched_full_block.is_transaction_block(): + continue + + if fetched_block_record.header_hash == block_record.header_hash: + tx_removals, tx_additions, npc_res = await self.get_tx_removals_and_additions( + fetched_full_block, npc_result + ) else: - fork_height = find_fork_point_in_chain(self, block_record, peak) - - if block_record.prev_hash != peak.header_hash: - roll_changes: List[CoinRecord] = await self.coin_store.rollback_to_block(fork_height) - for coin_record in roll_changes: - latest_coin_state[coin_record.name] = coin_record - - # Rollback sub_epoch_summaries - self.__height_map.rollback(fork_height) - await self.block_store.rollback(fork_height) - - # Collect all blocks from fork point to new peak - blocks_to_add: List[Tuple[FullBlock, BlockRecord]] = [] - curr = block_record.header_hash - - while fork_height < 0 or curr != self.height_to_hash(uint32(fork_height)): - fetched_full_block: Optional[FullBlock] = await self.block_store.get_full_block(curr) - fetched_block_record: Optional[BlockRecord] = await self.block_store.get_block_record(curr) - assert fetched_full_block is not None - assert fetched_block_record is not None - blocks_to_add.append((fetched_full_block, fetched_block_record)) - if fetched_full_block.height == 0: - # Doing a full reorg, starting at height 0 - break - curr = fetched_block_record.prev_hash - - records_to_add = [] - for fetched_full_block, fetched_block_record in reversed(blocks_to_add): - records_to_add.append(fetched_block_record) - if fetched_full_block.is_transaction_block(): - if fetched_block_record.header_hash == block_record.header_hash: - tx_removals, tx_additions, npc_res = await self.get_tx_removals_and_additions( - fetched_full_block, npc_result - ) - else: - tx_removals, tx_additions, npc_res = await self.get_tx_removals_and_additions( - fetched_full_block, None - ) - - assert fetched_full_block.foliage_transaction_block is not None - added_rec = await self.coin_store.new_block( - fetched_full_block.height, - fetched_full_block.foliage_transaction_block.timestamp, - fetched_full_block.get_included_reward_coins(), - tx_additions, - tx_removals, - ) - removed_rec: List[Optional[CoinRecord]] = [ - await self.coin_store.get_coin_record(name) for name in tx_removals - ] - - # Set additions first, then removals in order to handle ephemeral coin state - # Add in height order is also required - record: Optional[CoinRecord] - for record in added_rec: - assert record - latest_coin_state[record.name] = record - for record in removed_rec: - assert record - latest_coin_state[record.name] = record - - if npc_res is not None: - hint_list: List[Tuple[bytes32, bytes]] = self.get_hint_list(npc_res) - await self.hint_store.add_hints(hint_list) - # There can be multiple coins for the same hint - for coin_id, hint in hint_list: - key = hint - if key not in hint_coin_state: - hint_coin_state[key] = {} - hint_coin_state[key][coin_id] = latest_coin_state[coin_id] - - await self.block_store.set_in_chain([(br.header_hash,) for br in records_to_add]) - - # Changes the peak to be the new peak - await self.block_store.set_peak(block_record.header_hash) - return ( - uint32(max(fork_height, 0)), - block_record.height, - records_to_add, - (list(latest_coin_state.values()), hint_coin_state), + tx_removals, tx_additions, npc_res = await self.get_tx_removals_and_additions(fetched_full_block, None) + + assert fetched_full_block.foliage_transaction_block is not None + added_rec = await self.coin_store.new_block( + fetched_full_block.height, + fetched_full_block.foliage_transaction_block.timestamp, + fetched_full_block.get_included_reward_coins(), + tx_additions, + tx_removals, ) - - # This is not a heavier block than the heaviest we have seen, so we don't change the coin set - return None, None, [], ([], {}) + removed_rec: List[Optional[CoinRecord]] = [ + await self.coin_store.get_coin_record(name) for name in tx_removals + ] + + # Set additions first, then removals in order to handle ephemeral coin state + # Add in height order is also required + record: Optional[CoinRecord] + for record in added_rec: + assert record + latest_coin_state[record.name] = record + for record in removed_rec: + assert record + latest_coin_state[record.name] = record + + if npc_res is not None: + hint_list: List[Tuple[bytes32, bytes]] = self.get_hint_list(npc_res) + await self.hint_store.add_hints(hint_list) + # There can be multiple coins for the same hint + for coin_id, hint in hint_list: + key = hint + if key not in hint_coin_state: + hint_coin_state[key] = {} + hint_coin_state[key][coin_id] = latest_coin_state[coin_id] + + await self.block_store.set_in_chain([(br.header_hash,) for br in records_to_add]) + + # Changes the peak to be the new peak + await self.block_store.set_peak(block_record.header_hash) + return ( + uint32(max(fork_height, 0)), + block_record.height, + records_to_add, + (list(latest_coin_state.values()), hint_coin_state), + ) async def get_tx_removals_and_additions( self, block: FullBlock, npc_result: Optional[NPCResult] = None ) -> Tuple[List[bytes32], List[Coin], Optional[NPCResult]]: - if block.is_transaction_block(): - if block.transactions_generator is not None: - if npc_result is None: - block_generator: Optional[BlockGenerator] = await self.get_block_generator(block) - assert block_generator is not None - npc_result = get_name_puzzle_conditions( - block_generator, - self.constants.MAX_BLOCK_COST_CLVM, - cost_per_byte=self.constants.COST_PER_BYTE, - mempool_mode=False, - height=block.height, - ) - tx_removals, tx_additions = tx_removals_and_additions(npc_result.npc_list) - return tx_removals, tx_additions, npc_result - else: - return [], [], None - else: + + if not block.is_transaction_block(): return [], [], None + if block.transactions_generator is None: + return [], [], None + + if npc_result is None: + block_generator: Optional[BlockGenerator] = await self.get_block_generator(block) + assert block_generator is not None + npc_result = get_name_puzzle_conditions( + block_generator, + self.constants.MAX_BLOCK_COST_CLVM, + cost_per_byte=self.constants.COST_PER_BYTE, + mempool_mode=False, + height=block.height, + ) + tx_removals, tx_additions = tx_removals_and_additions(npc_result.npc_list) + return tx_removals, tx_additions, npc_result + def get_next_difficulty(self, header_hash: bytes32, new_slot: bool) -> uint64: assert self.contains_block(header_hash) curr = self.block_record(header_hash) From d20cf06f59d6d2d2d9f220a531af6aeb0856e716 Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Fri, 25 Mar 2022 09:28:06 -0700 Subject: [PATCH 258/378] fix typo and index issues in wallet database (#10273) * fix typo in wallet_puzzle_store * check some SQL statements * deduplicate name SQL index * deduplicate wallet_type index * deduplicate wallet_id index --- .pre-commit-config.yaml | 5 ++ chia/wallet/key_val_store.py | 2 +- chia/wallet/wallet_action_store.py | 8 +-- chia/wallet/wallet_coin_store.py | 4 +- chia/wallet/wallet_puzzle_store.py | 6 ++- chia/wallet/wallet_transaction_store.py | 4 +- tests/check_sql_statements.py | 68 +++++++++++++++++++++++++ 7 files changed, 89 insertions(+), 8 deletions(-) create mode 100755 tests/check_sql_statements.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 86e5236837ba..c181b0b36e47 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,6 +6,11 @@ repos: entry: ./tests/build-workflows.py --fail-on-update language: python pass_filenames: false + - id: check-sql + name: Validate SQL statements + entry: ./tests/check_sql_statements.py + language: python + pass_filenames: false - repo: local hooks: - id: init_py_files diff --git a/chia/wallet/key_val_store.py b/chia/wallet/key_val_store.py index 631641a16482..0232988b8012 100644 --- a/chia/wallet/key_val_store.py +++ b/chia/wallet/key_val_store.py @@ -22,7 +22,7 @@ async def create(cls, db_wrapper: DBWrapper): "CREATE TABLE IF NOT EXISTS key_val_store(" " key text PRIMARY KEY," " value blob)" ) - await self.db_connection.execute("CREATE INDEX IF NOT EXISTS name on key_val_store(key)") + await self.db_connection.execute("CREATE INDEX IF NOT EXISTS key_val_name on key_val_store(key)") await self.db_connection.commit() return self diff --git a/chia/wallet/wallet_action_store.py b/chia/wallet/wallet_action_store.py index 1abecbeb87fb..a1b73f16c31b 100644 --- a/chia/wallet/wallet_action_store.py +++ b/chia/wallet/wallet_action_store.py @@ -37,11 +37,13 @@ async def create(cls, db_wrapper: DBWrapper): ) ) - await self.db_connection.execute("CREATE INDEX IF NOT EXISTS name on action_queue(name)") + await self.db_connection.execute("CREATE INDEX IF NOT EXISTS action_queue_name on action_queue(name)") - await self.db_connection.execute("CREATE INDEX IF NOT EXISTS wallet_id on action_queue(wallet_id)") + await self.db_connection.execute("CREATE INDEX IF NOT EXISTS action_queue_wallet_id on action_queue(wallet_id)") - await self.db_connection.execute("CREATE INDEX IF NOT EXISTS wallet_type on action_queue(wallet_type)") + await self.db_connection.execute( + "CREATE INDEX IF NOT EXISTS action_queue_wallet_type on action_queue(wallet_type)" + ) await self.db_connection.commit() return self diff --git a/chia/wallet/wallet_coin_store.py b/chia/wallet/wallet_coin_store.py index f217ed684d4d..8c8a7121ecc0 100644 --- a/chia/wallet/wallet_coin_store.py +++ b/chia/wallet/wallet_coin_store.py @@ -54,7 +54,9 @@ async def create(cls, wrapper: DBWrapper): await self.db_connection.execute("CREATE INDEX IF NOT EXISTS coin_puzzlehash on coin_record(puzzle_hash)") - await self.db_connection.execute("CREATE INDEX IF NOT EXISTS wallet_type on coin_record(wallet_type)") + await self.db_connection.execute( + "CREATE INDEX IF NOT EXISTS coin_record_wallet_type on coin_record(wallet_type)" + ) await self.db_connection.execute("CREATE INDEX IF NOT EXISTS wallet_id on coin_record(wallet_id)") diff --git a/chia/wallet/wallet_puzzle_store.py b/chia/wallet/wallet_puzzle_store.py index c68df2898740..f4113c3a7b59 100644 --- a/chia/wallet/wallet_puzzle_store.py +++ b/chia/wallet/wallet_puzzle_store.py @@ -40,7 +40,7 @@ async def create(cls, db_wrapper: DBWrapper, cache_size: uint32 = uint32(600000) "CREATE TABLE IF NOT EXISTS derivation_paths(" "derivation_index int," " pubkey text," - " puzzle_hash text PRIMARY_KEY," + " puzzle_hash text PRIMARY KEY," " wallet_type int," " wallet_id int," " used tinyint," @@ -57,7 +57,9 @@ async def create(cls, db_wrapper: DBWrapper, cache_size: uint32 = uint32(600000) await self.db_connection.execute("CREATE INDEX IF NOT EXISTS wallet_type on derivation_paths(wallet_type)") - await self.db_connection.execute("CREATE INDEX IF NOT EXISTS wallet_id on derivation_paths(wallet_id)") + await self.db_connection.execute( + "CREATE INDEX IF NOT EXISTS derivation_paths_wallet_id on derivation_paths(wallet_id)" + ) await self.db_connection.execute("CREATE INDEX IF NOT EXISTS used on derivation_paths(wallet_type)") diff --git a/chia/wallet/wallet_transaction_store.py b/chia/wallet/wallet_transaction_store.py index 74a6216139d5..6d9b91d2301b 100644 --- a/chia/wallet/wallet_transaction_store.py +++ b/chia/wallet/wallet_transaction_store.py @@ -71,7 +71,9 @@ async def create(cls, db_wrapper: DBWrapper): "CREATE INDEX IF NOT EXISTS tx_to_puzzle_hash on transaction_record(to_puzzle_hash)" ) - await self.db_connection.execute("CREATE INDEX IF NOT EXISTS wallet_id on transaction_record(wallet_id)") + await self.db_connection.execute( + "CREATE INDEX IF NOT EXISTS transaction_record_wallet_id on transaction_record(wallet_id)" + ) await self.db_connection.commit() self.tx_record_cache = {} diff --git a/tests/check_sql_statements.py b/tests/check_sql_statements.py new file mode 100755 index 000000000000..f84829200429 --- /dev/null +++ b/tests/check_sql_statements.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +import sys +from subprocess import check_output +from typing import Dict, Set, Tuple + +# check for duplicate index names + + +def check_create(sql_type: str, cwd: str, exemptions: Set[Tuple[str, str]] = set()) -> int: + + lines = check_output(["git", "grep", f"CREATE {sql_type}"], cwd=cwd).decode("ascii").split("\n") + + ret = 0 + + items: Dict[str, str] = {} + for line in lines: + if f"CREATE {sql_type}" not in line: + continue + if line.startswith("tests/"): + continue + if "db_upgrade_func.py" in line: + continue + + name = line.split(f"CREATE {sql_type}")[1] + if name.startswith(" IF NOT EXISTS"): + name = name[14:] + name = name.strip() + name = name.split()[0] + name = name.split("(")[0] + + if name in items: + # these appear as a duplicates, but one is for v1 and the other for v2 + if (line.split()[0][:-1], name) not in exemptions: + print(f'duplicate {sql_type} "{name}"\n {items[name]}\n {line}') + ret += 1 + + items[name] = line + + return ret + + +ret = 0 + +ret += check_create("INDEX", "chia/wallet") +ret += check_create( + "INDEX", + "chia/full_node", + set( + [ + ("block_store.py", "is_fully_compactified"), + ("block_store.py", "height"), + ] + ), +) +ret += check_create("TABLE", "chia/wallet") +ret += check_create( + "TABLE", + "chia/full_node", + set( + [ + ("block_store.py", "sub_epoch_segments_v3"), + ("block_store.py", "full_blocks"), + ("coin_store.py", "coin_record"), + ("hint_store.py", "hints"), + ] + ), +) +sys.exit(ret) From 536d7c4e039e5c6ee1ddbf63869096a7a62333d3 Mon Sep 17 00:00:00 2001 From: Chris Marslender Date: Fri, 25 Mar 2022 13:44:00 -0500 Subject: [PATCH 259/378] Update appdmg to 0.6.4 to work with macos 12.3 (#10886) --- build_scripts/npm_macos_m1/package-lock.json | 151 +++++-------------- build_scripts/npm_macos_m1/package.json | 1 + 2 files changed, 37 insertions(+), 115 deletions(-) diff --git a/build_scripts/npm_macos_m1/package-lock.json b/build_scripts/npm_macos_m1/package-lock.json index acfd2b6d24c7..66df6b7672a8 100644 --- a/build_scripts/npm_macos_m1/package-lock.json +++ b/build_scripts/npm_macos_m1/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "appdmg": "^0.6.4", "electron-installer-dmg": "^3.0.0", "electron-osx-sign": "^0.5.0", "electron-packager": "^15.4.0", @@ -2429,10 +2430,9 @@ } }, "node_modules/appdmg": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/appdmg/-/appdmg-0.6.2.tgz", - "integrity": "sha512-mbJyAxn2/JmDpckNpXDU4AQ/XF7fltOyrLcETZxGfYwESt0NdCjQB8L2vIxxAyZKT0R/c0aSzb9yE+P2yDh9TQ==", - "optional": true, + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/appdmg/-/appdmg-0.6.4.tgz", + "integrity": "sha512-YTilgNF0DF2DSRzGzzGDxaTMLXlhe3b3HB8RAaoJJ/VJXZbOlzIAcZ7gdPniHUVUuHjGwnS7fUMd4FvO2Rp94A==", "os": [ "darwin" ], @@ -2570,8 +2570,7 @@ "node_modules/async": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", - "optional": true + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" }, "node_modules/asynckit": { "version": "0.4.0", @@ -2616,7 +2615,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/base32-encode/-/base32-encode-1.2.0.tgz", "integrity": "sha512-cHFU8XeRyx0GgmoWi5qHMCVRiqU6J3MHWxVgun7jggCBUpVzm1Ir7M9dYr2whjSNc3tFeXfQ/oZjQu/4u55h9A==", - "optional": true, "dependencies": { "to-data-view": "^1.1.0" } @@ -2668,7 +2666,6 @@ "version": "0.0.8", "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.0.8.tgz", "integrity": "sha512-Za9JKzD6fjLC16oX2wsXfc+qBEhJBJB1YPInoAQpMLhDuj5aVOv1baGeIQSq1Fr3OCqzvsoQcSBSwGId/Ja2PA==", - "optional": true, "dependencies": { "stream-buffers": "~2.2.0" } @@ -3597,7 +3594,6 @@ "version": "0.1.6", "resolved": "https://registry.npmjs.org/ds-store/-/ds-store-0.1.6.tgz", "integrity": "sha1-0QJO90btDBPw9/7IXH6FjoxLfKc=", - "optional": true, "dependencies": { "bplist-creator": "~0.0.3", "macos-alias": "~0.2.5", @@ -3795,8 +3791,7 @@ "node_modules/encode-utf8": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz", - "integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==", - "optional": true + "integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==" }, "node_modules/encodeurl": { "version": "1.0.2", @@ -3948,7 +3943,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "optional": true, "dependencies": { "cross-spawn": "^6.0.0", "get-stream": "^4.0.0", @@ -3966,7 +3960,6 @@ "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "optional": true, "dependencies": { "nice-try": "^1.0.4", "path-key": "^2.0.1", @@ -3982,7 +3975,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "optional": true, "engines": { "node": ">=4" } @@ -3991,7 +3983,6 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "optional": true, "bin": { "semver": "bin/semver" } @@ -4000,7 +3991,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "optional": true, "dependencies": { "shebang-regex": "^1.0.0" }, @@ -4012,7 +4002,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "optional": true, "engines": { "node": ">=0.10.0" } @@ -4021,7 +4010,6 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "optional": true, "dependencies": { "isexe": "^2.0.0" }, @@ -4227,7 +4215,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/fmix/-/fmix-0.1.0.tgz", "integrity": "sha1-x7vxJN7ELJ0ZHPuUfQqXeN2YbAw=", - "optional": true, "dependencies": { "imul": "^1.0.0" } @@ -4281,7 +4268,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/fs-temp/-/fs-temp-1.2.1.tgz", "integrity": "sha512-okTwLB7/Qsq82G6iN5zZJFsOfZtx2/pqrA7Hk/9fvy+c+eJS9CvgGXT2uNxwnI14BDY9L/jQPkaBgSvlKfSW9w==", - "optional": true, "dependencies": { "random-path": "^0.1.0" } @@ -4291,7 +4277,6 @@ "resolved": "https://registry.npmjs.org/fs-xattr/-/fs-xattr-0.3.1.tgz", "integrity": "sha512-UVqkrEW0GfDabw4C3HOrFlxKfx0eeigfRne69FxSBdHIP8Qt5Sq6Pu3RM9KmMlkygtC4pPKkj5CiPO5USnj2GA==", "hasInstallScript": true, - "optional": true, "os": [ "!win32" ], @@ -4385,7 +4370,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", - "optional": true, "dependencies": { "is-property": "^1.0.2" } @@ -4394,7 +4378,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", - "optional": true, "dependencies": { "is-property": "^1.0.0" } @@ -4961,7 +4944,6 @@ "version": "0.7.5", "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.7.5.tgz", "integrity": "sha512-Hiyv+mXHfFEP7LzUL/llg9RwFxxY+o9N3JVLIeG5E7iFIFAalxvRU9UZthBdYDEVnzHMgjnKJPPpay5BWf1g9g==", - "optional": true, "bin": { "image-size": "bin/image-size.js" }, @@ -5014,7 +4996,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/imul/-/imul-1.0.1.tgz", "integrity": "sha1-nVhnFh6LPelsLDjV3HyxAvNeKsk=", - "optional": true, "engines": { "node": ">=0.10.0" } @@ -5277,14 +5258,12 @@ "node_modules/is-my-ip-valid": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz", - "integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==", - "optional": true + "integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==" }, "node_modules/is-my-json-valid": { "version": "2.20.6", "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.20.6.tgz", "integrity": "sha512-1JQwulVNjx8UqkPE/bqDaxtH4PXCe/2VRh/y3p99heOV87HG4Id5/VfDswd+YiAfHcRTfDlWgISycnHuhZq1aw==", - "optional": true, "dependencies": { "generate-function": "^2.0.0", "generate-object-property": "^1.1.0", @@ -5353,8 +5332,7 @@ "node_modules/is-property": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", - "optional": true + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=" }, "node_modules/is-regex": { "version": "1.1.4", @@ -5391,7 +5369,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "optional": true, "engines": { "node": ">=0.10.0" } @@ -5556,7 +5533,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.0.tgz", "integrity": "sha512-PNYZIdMjVIvVgDSYKTT63Y+KZ6IZvGRNNWcxwD+GNnUz1MKPfv30J8ueCjdwcN0nDx2SlshgyB7Oy0epAzVRRg==", - "optional": true, "engines": { "node": ">=0.10.0" } @@ -5883,7 +5859,6 @@ "resolved": "https://registry.npmjs.org/macos-alias/-/macos-alias-0.2.11.tgz", "integrity": "sha1-/u6mwTuhGYFKQ/xDxHCzHlnvcYo=", "hasInstallScript": true, - "optional": true, "os": [ "darwin" ], @@ -6334,7 +6309,6 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/murmur-32/-/murmur-32-0.2.0.tgz", "integrity": "sha512-ZkcWZudylwF+ir3Ld1n7gL6bI2mQAzXvSobPwVtu8aYi2sbXeipeSkdcanRLzIofLcM5F53lGaKm2dk7orBi7Q==", - "optional": true, "dependencies": { "encode-utf8": "^1.0.3", "fmix": "^0.1.0", @@ -6349,8 +6323,7 @@ "node_modules/nan": { "version": "2.15.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", - "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==", - "optional": true + "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==" }, "node_modules/negotiator": { "version": "0.6.2", @@ -6368,8 +6341,7 @@ "node_modules/nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "optional": true + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" }, "node_modules/node-fetch": { "version": "2.6.6", @@ -6778,7 +6750,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "optional": true, "dependencies": { "path-key": "^2.0.0" }, @@ -6790,7 +6761,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "optional": true, "engines": { "node": ">=4" } @@ -7169,7 +7139,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/parse-color/-/parse-color-1.0.0.tgz", "integrity": "sha1-e3SLlag/A/FqlPU15S1/PZRlhhk=", - "optional": true, "dependencies": { "color-convert": "~0.5.0" } @@ -7177,8 +7146,7 @@ "node_modules/parse-color/node_modules/color-convert": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz", - "integrity": "sha1-vbbGnOZg+t/+CwAHzER+G59ygr0=", - "optional": true + "integrity": "sha1-vbbGnOZg+t/+CwAHzER+G59ygr0=" }, "node_modules/parse-json": { "version": "5.2.0", @@ -7471,7 +7439,6 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/random-path/-/random-path-0.1.2.tgz", "integrity": "sha512-4jY0yoEaQ5v9StCl5kZbNIQlg1QheIDBrdkDn53EynpPb9FgO6//p3X/tgMnrC45XN6QZCzU1Xz/+pSSsJBpRw==", - "optional": true, "dependencies": { "base32-encode": "^0.1.0 || ^1.0.0", "murmur-32": "^0.1.0 || ^0.2.0" @@ -7867,7 +7834,6 @@ "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "optional": true, "engines": { "node": ">=0.10" } @@ -8373,7 +8339,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz", "integrity": "sha1-kdX1Ew0c75bc+n9yaUUYh0HQnuQ=", - "optional": true, "engines": { "node": ">= 0.10.0" } @@ -8473,7 +8438,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "optional": true, "engines": { "node": ">=0.10.0" } @@ -8643,7 +8607,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/tn1150/-/tn1150-0.1.0.tgz", "integrity": "sha1-ZzUD0k1WuH3ouMd/7j/AhT1ZoY0=", - "optional": true, "dependencies": { "unorm": "^1.4.1" }, @@ -8654,8 +8617,7 @@ "node_modules/to-data-view": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/to-data-view/-/to-data-view-1.1.0.tgz", - "integrity": "sha512-1eAdufMg6mwgmlojAx3QeMnzB/BTVp7Tbndi3U7ftcT2zCZadjxkkmLmd97zmaxWi+sgGcgWrokmpEoy0Dn0vQ==", - "optional": true + "integrity": "sha512-1eAdufMg6mwgmlojAx3QeMnzB/BTVp7Tbndi3U7ftcT2zCZadjxkkmLmd97zmaxWi+sgGcgWrokmpEoy0Dn0vQ==" }, "node_modules/to-readable-stream": { "version": "1.0.0", @@ -8844,7 +8806,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/unorm/-/unorm-1.6.0.tgz", "integrity": "sha512-b2/KCUlYZUeA7JFUuRJZPUtr4gZvBh7tavtv4fvk4+KV9pfGiR6CQAQAWl49ZpR3ts2dk4FYkP7EIgDJoiOLDA==", - "optional": true, "engines": { "node": ">= 0.4.0" } @@ -11175,10 +11136,9 @@ } }, "appdmg": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/appdmg/-/appdmg-0.6.2.tgz", - "integrity": "sha512-mbJyAxn2/JmDpckNpXDU4AQ/XF7fltOyrLcETZxGfYwESt0NdCjQB8L2vIxxAyZKT0R/c0aSzb9yE+P2yDh9TQ==", - "optional": true, + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/appdmg/-/appdmg-0.6.4.tgz", + "integrity": "sha512-YTilgNF0DF2DSRzGzzGDxaTMLXlhe3b3HB8RAaoJJ/VJXZbOlzIAcZ7gdPniHUVUuHjGwnS7fUMd4FvO2Rp94A==", "requires": { "async": "^1.4.2", "ds-store": "^0.1.5", @@ -11289,8 +11249,7 @@ "async": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", - "optional": true + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" }, "asynckit": { "version": "0.4.0", @@ -11326,7 +11285,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/base32-encode/-/base32-encode-1.2.0.tgz", "integrity": "sha512-cHFU8XeRyx0GgmoWi5qHMCVRiqU6J3MHWxVgun7jggCBUpVzm1Ir7M9dYr2whjSNc3tFeXfQ/oZjQu/4u55h9A==", - "optional": true, "requires": { "to-data-view": "^1.1.0" } @@ -11364,7 +11322,6 @@ "version": "0.0.8", "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.0.8.tgz", "integrity": "sha512-Za9JKzD6fjLC16oX2wsXfc+qBEhJBJB1YPInoAQpMLhDuj5aVOv1baGeIQSq1Fr3OCqzvsoQcSBSwGId/Ja2PA==", - "optional": true, "requires": { "stream-buffers": "~2.2.0" } @@ -12072,7 +12029,6 @@ "version": "0.1.6", "resolved": "https://registry.npmjs.org/ds-store/-/ds-store-0.1.6.tgz", "integrity": "sha1-0QJO90btDBPw9/7IXH6FjoxLfKc=", - "optional": true, "requires": { "bplist-creator": "~0.0.3", "macos-alias": "~0.2.5", @@ -12233,8 +12189,7 @@ "encode-utf8": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz", - "integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==", - "optional": true + "integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==" }, "encodeurl": { "version": "1.0.2", @@ -12355,7 +12310,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "optional": true, "requires": { "cross-spawn": "^6.0.0", "get-stream": "^4.0.0", @@ -12370,7 +12324,6 @@ "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "optional": true, "requires": { "nice-try": "^1.0.4", "path-key": "^2.0.1", @@ -12382,20 +12335,17 @@ "path-key": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "optional": true + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "optional": true + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" }, "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "optional": true, "requires": { "shebang-regex": "^1.0.0" } @@ -12403,14 +12353,12 @@ "shebang-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "optional": true + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "optional": true, "requires": { "isexe": "^2.0.0" } @@ -12566,7 +12514,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/fmix/-/fmix-0.1.0.tgz", "integrity": "sha1-x7vxJN7ELJ0ZHPuUfQqXeN2YbAw=", - "optional": true, "requires": { "imul": "^1.0.0" } @@ -12608,7 +12555,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/fs-temp/-/fs-temp-1.2.1.tgz", "integrity": "sha512-okTwLB7/Qsq82G6iN5zZJFsOfZtx2/pqrA7Hk/9fvy+c+eJS9CvgGXT2uNxwnI14BDY9L/jQPkaBgSvlKfSW9w==", - "optional": true, "requires": { "random-path": "^0.1.0" } @@ -12616,8 +12562,7 @@ "fs-xattr": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/fs-xattr/-/fs-xattr-0.3.1.tgz", - "integrity": "sha512-UVqkrEW0GfDabw4C3HOrFlxKfx0eeigfRne69FxSBdHIP8Qt5Sq6Pu3RM9KmMlkygtC4pPKkj5CiPO5USnj2GA==", - "optional": true + "integrity": "sha512-UVqkrEW0GfDabw4C3HOrFlxKfx0eeigfRne69FxSBdHIP8Qt5Sq6Pu3RM9KmMlkygtC4pPKkj5CiPO5USnj2GA==" }, "fs.realpath": { "version": "1.0.0", @@ -12703,7 +12648,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", - "optional": true, "requires": { "is-property": "^1.0.2" } @@ -12712,7 +12656,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", - "optional": true, "requires": { "is-property": "^1.0.0" } @@ -13144,8 +13087,7 @@ "image-size": { "version": "0.7.5", "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.7.5.tgz", - "integrity": "sha512-Hiyv+mXHfFEP7LzUL/llg9RwFxxY+o9N3JVLIeG5E7iFIFAalxvRU9UZthBdYDEVnzHMgjnKJPPpay5BWf1g9g==", - "optional": true + "integrity": "sha512-Hiyv+mXHfFEP7LzUL/llg9RwFxxY+o9N3JVLIeG5E7iFIFAalxvRU9UZthBdYDEVnzHMgjnKJPPpay5BWf1g9g==" }, "import-fresh": { "version": "3.3.0", @@ -13175,8 +13117,7 @@ "imul": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/imul/-/imul-1.0.1.tgz", - "integrity": "sha1-nVhnFh6LPelsLDjV3HyxAvNeKsk=", - "optional": true + "integrity": "sha1-nVhnFh6LPelsLDjV3HyxAvNeKsk=" }, "imurmurhash": { "version": "0.1.4", @@ -13371,14 +13312,12 @@ "is-my-ip-valid": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz", - "integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==", - "optional": true + "integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==" }, "is-my-json-valid": { "version": "2.20.6", "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.20.6.tgz", "integrity": "sha512-1JQwulVNjx8UqkPE/bqDaxtH4PXCe/2VRh/y3p99heOV87HG4Id5/VfDswd+YiAfHcRTfDlWgISycnHuhZq1aw==", - "optional": true, "requires": { "generate-function": "^2.0.0", "generate-object-property": "^1.1.0", @@ -13423,8 +13362,7 @@ "is-property": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", - "optional": true + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=" }, "is-regex": { "version": "1.1.4", @@ -13451,8 +13389,7 @@ "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "optional": true + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" }, "is-string": { "version": "1.0.7", @@ -13583,8 +13520,7 @@ "jsonpointer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.0.tgz", - "integrity": "sha512-PNYZIdMjVIvVgDSYKTT63Y+KZ6IZvGRNNWcxwD+GNnUz1MKPfv30J8ueCjdwcN0nDx2SlshgyB7Oy0epAzVRRg==", - "optional": true + "integrity": "sha512-PNYZIdMjVIvVgDSYKTT63Y+KZ6IZvGRNNWcxwD+GNnUz1MKPfv30J8ueCjdwcN0nDx2SlshgyB7Oy0epAzVRRg==" }, "JSONStream": { "version": "1.3.5", @@ -13853,7 +13789,6 @@ "version": "0.2.11", "resolved": "https://registry.npmjs.org/macos-alias/-/macos-alias-0.2.11.tgz", "integrity": "sha1-/u6mwTuhGYFKQ/xDxHCzHlnvcYo=", - "optional": true, "requires": { "nan": "^2.4.0" } @@ -14185,7 +14120,6 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/murmur-32/-/murmur-32-0.2.0.tgz", "integrity": "sha512-ZkcWZudylwF+ir3Ld1n7gL6bI2mQAzXvSobPwVtu8aYi2sbXeipeSkdcanRLzIofLcM5F53lGaKm2dk7orBi7Q==", - "optional": true, "requires": { "encode-utf8": "^1.0.3", "fmix": "^0.1.0", @@ -14200,8 +14134,7 @@ "nan": { "version": "2.15.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", - "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==", - "optional": true + "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==" }, "negotiator": { "version": "0.6.2", @@ -14216,8 +14149,7 @@ "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "optional": true + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" }, "node-fetch": { "version": "2.6.6", @@ -14546,7 +14478,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "optional": true, "requires": { "path-key": "^2.0.0" }, @@ -14554,8 +14485,7 @@ "path-key": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "optional": true + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" } } }, @@ -14824,7 +14754,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/parse-color/-/parse-color-1.0.0.tgz", "integrity": "sha1-e3SLlag/A/FqlPU15S1/PZRlhhk=", - "optional": true, "requires": { "color-convert": "~0.5.0" }, @@ -14832,8 +14761,7 @@ "color-convert": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz", - "integrity": "sha1-vbbGnOZg+t/+CwAHzER+G59ygr0=", - "optional": true + "integrity": "sha1-vbbGnOZg+t/+CwAHzER+G59ygr0=" } } }, @@ -15043,7 +14971,6 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/random-path/-/random-path-0.1.2.tgz", "integrity": "sha512-4jY0yoEaQ5v9StCl5kZbNIQlg1QheIDBrdkDn53EynpPb9FgO6//p3X/tgMnrC45XN6QZCzU1Xz/+pSSsJBpRw==", - "optional": true, "requires": { "base32-encode": "^0.1.0 || ^1.0.0", "murmur-32": "^0.1.0 || ^0.2.0" @@ -15360,8 +15287,7 @@ "repeat-string": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "optional": true + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" }, "request": { "version": "2.88.2", @@ -15720,8 +15646,7 @@ "stream-buffers": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz", - "integrity": "sha1-kdX1Ew0c75bc+n9yaUUYh0HQnuQ=", - "optional": true + "integrity": "sha1-kdX1Ew0c75bc+n9yaUUYh0HQnuQ=" }, "strict-uri-encode": { "version": "2.0.0", @@ -15795,8 +15720,7 @@ "strip-eof": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "optional": true + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" }, "strip-final-newline": { "version": "2.0.0", @@ -15917,7 +15841,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/tn1150/-/tn1150-0.1.0.tgz", "integrity": "sha1-ZzUD0k1WuH3ouMd/7j/AhT1ZoY0=", - "optional": true, "requires": { "unorm": "^1.4.1" } @@ -15925,8 +15848,7 @@ "to-data-view": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/to-data-view/-/to-data-view-1.1.0.tgz", - "integrity": "sha512-1eAdufMg6mwgmlojAx3QeMnzB/BTVp7Tbndi3U7ftcT2zCZadjxkkmLmd97zmaxWi+sgGcgWrokmpEoy0Dn0vQ==", - "optional": true + "integrity": "sha512-1eAdufMg6mwgmlojAx3QeMnzB/BTVp7Tbndi3U7ftcT2zCZadjxkkmLmd97zmaxWi+sgGcgWrokmpEoy0Dn0vQ==" }, "to-readable-stream": { "version": "1.0.0", @@ -16069,8 +15991,7 @@ "unorm": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/unorm/-/unorm-1.6.0.tgz", - "integrity": "sha512-b2/KCUlYZUeA7JFUuRJZPUtr4gZvBh7tavtv4fvk4+KV9pfGiR6CQAQAWl49ZpR3ts2dk4FYkP7EIgDJoiOLDA==", - "optional": true + "integrity": "sha512-b2/KCUlYZUeA7JFUuRJZPUtr4gZvBh7tavtv4fvk4+KV9pfGiR6CQAQAWl49ZpR3ts2dk4FYkP7EIgDJoiOLDA==" }, "upath": { "version": "2.0.1", diff --git a/build_scripts/npm_macos_m1/package.json b/build_scripts/npm_macos_m1/package.json index 9c814aebbdfc..7535aa2244ce 100644 --- a/build_scripts/npm_macos_m1/package.json +++ b/build_scripts/npm_macos_m1/package.json @@ -10,6 +10,7 @@ "author": "", "license": "ISC", "dependencies": { + "appdmg": "^0.6.4", "electron-installer-dmg": "^3.0.0", "electron-osx-sign": "^0.5.0", "electron-packager": "^15.4.0", From 7ea074c9e70a28297754257d206526da6073448f Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Fri, 25 Mar 2022 18:49:11 -0700 Subject: [PATCH 260/378] fixup and enable condition checking tests (#10888) * fixup and enable tests for the edge cases of absolute timestamp and absolute height conditions in mempool_manager * Update chia/full_node/full_node_api.py Co-authored-by: Adam Kelly <338792+aqk@users.noreply.github.com> Co-authored-by: Adam Kelly <338792+aqk@users.noreply.github.com> --- chia/full_node/full_node_api.py | 5 ++- tests/core/full_node/test_mempool.py | 48 ++++++++++++++++------------ 2 files changed, 29 insertions(+), 24 deletions(-) diff --git a/chia/full_node/full_node_api.py b/chia/full_node/full_node_api.py index c660210a3aaf..b61ee9a99b96 100644 --- a/chia/full_node/full_node_api.py +++ b/chia/full_node/full_node_api.py @@ -1256,11 +1256,10 @@ async def request_removals(self, request: wallet_protocol.RequestRemovals) -> Op return msg @api_request - async def send_transaction(self, request: wallet_protocol.SendTransaction) -> Optional[Message]: + async def send_transaction(self, request: wallet_protocol.SendTransaction, *, test=False) -> Optional[Message]: spend_name = request.transaction.name() - await self.full_node.transaction_queue.put( - (0, TransactionQueueEntry(request.transaction, None, spend_name, None, False)) + (0, TransactionQueueEntry(request.transaction, None, spend_name, None, test)) ) # Waits for the transaction to go into the mempool, times out after 45 seconds. status, error = None, None diff --git a/tests/core/full_node/test_mempool.py b/tests/core/full_node/test_mempool.py index c72e490ecabb..0961a6153240 100644 --- a/tests/core/full_node/test_mempool.py +++ b/tests/core/full_node/test_mempool.py @@ -79,7 +79,7 @@ def generate_test_spend_bundle( return transaction -@pytest_asyncio.fixture(scope="module") +@pytest_asyncio.fixture(scope="function") async def two_nodes_mempool(bt, wallet_a): async_gen = setup_simulators_and_wallets(2, 1, {}) nodes, _ = await async_gen.__anext__() @@ -94,7 +94,10 @@ async def two_nodes_mempool(bt, wallet_a): guarantee_transaction_block=True, farmer_reward_puzzle_hash=reward_ph, pool_reward_puzzle_hash=reward_ph, + genesis_timestamp=10000, + time_per_block=10, ) + assert blocks[0].height == 0 for block in blocks: await full_node_1.full_node.respond_block(full_node_protocol.RespondBlock(block)) @@ -235,6 +238,8 @@ async def next_block(full_node_1, wallet_a, bt) -> Coin: guarantee_transaction_block=True, farmer_reward_puzzle_hash=reward_ph, pool_reward_puzzle_hash=reward_ph, + genesis_timestamp=10000, + time_per_block=10, ) for block in blocks: @@ -255,7 +260,7 @@ async def test_basic_mempool_manager(self, bt, two_nodes_mempool, wallet_a, self spend_bundle = generate_test_spend_bundle(wallet_a, coin) assert spend_bundle is not None tx: full_node_protocol.RespondTransaction = full_node_protocol.RespondTransaction(spend_bundle) - await full_node_1.respond_transaction(tx, peer) + await full_node_1.respond_transaction(tx, peer, test=True) await time_out_assert( 10, @@ -277,17 +282,17 @@ async def test_basic_mempool_manager(self, bt, two_nodes_mempool, wallet_a, self (ConditionOpcode.ASSERT_HEIGHT_RELATIVE, 0, MempoolInclusionStatus.PENDING), (ConditionOpcode.ASSERT_HEIGHT_RELATIVE, 1, MempoolInclusionStatus.PENDING), # the absolute height and seconds tests require fresh full nodes to - # run the test on. Right now, we just launch two simulations and all - # tests use the same ones. See comment at the two_nodes_mempool fixture - # (ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, 2, MempoolInclusionStatus.SUCCESS), - # (ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, 3, MempoolInclusionStatus.SUCCESS), - # (ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, 4, MempoolInclusionStatus.PENDING), - # (ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, 5, MempoolInclusionStatus.PENDING), + # run the test on. The fixture (two_nodes_mempool) creates 3 blocks, + # then condition_tester2 creates another 3 blocks + (ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, 4, MempoolInclusionStatus.SUCCESS), + (ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, 5, MempoolInclusionStatus.SUCCESS), + (ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, 6, MempoolInclusionStatus.PENDING), + (ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, 7, MempoolInclusionStatus.PENDING), # genesis timestamp is 10000 and each block is 10 seconds - # (ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, 10029, MempoolInclusionStatus.SUCCESS), - # (ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, 10030, MempoolInclusionStatus.SUCCESS), - # (ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, 10031, MempoolInclusionStatus.FAILED), - # (ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, 10032, MempoolInclusionStatus.FAILED), + (ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, 10049, MempoolInclusionStatus.SUCCESS), + (ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, 10050, MempoolInclusionStatus.SUCCESS), + (ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, 10051, MempoolInclusionStatus.FAILED), + (ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, 10052, MempoolInclusionStatus.FAILED), ], ) async def test_ephemeral_timelock(self, bt, two_nodes_mempool, wallet_a, opcode, lock_value, expected): @@ -392,7 +397,7 @@ async def test_double_spend(self, bt, two_nodes_mempool, wallet_a, self_hostname assert spend_bundle1 is not None tx1: full_node_protocol.RespondTransaction = full_node_protocol.RespondTransaction(spend_bundle1) - status, err = await respond_transaction(full_node_1, tx1, peer) + status, err = await respond_transaction(full_node_1, tx1, peer, test=True) assert err is None assert status == MempoolInclusionStatus.SUCCESS @@ -403,7 +408,7 @@ async def test_double_spend(self, bt, two_nodes_mempool, wallet_a, self_hostname ) assert spend_bundle2 is not None tx2: full_node_protocol.RespondTransaction = full_node_protocol.RespondTransaction(spend_bundle2) - status, err = await respond_transaction(full_node_1, tx2, peer) + status, err = await respond_transaction(full_node_1, tx2, peer, test=True) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) sb2 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle2.name()) @@ -415,7 +420,7 @@ async def test_double_spend(self, bt, two_nodes_mempool, wallet_a, self_hostname async def send_sb(self, node: FullNodeAPI, sb: SpendBundle) -> Optional[Message]: tx = wallet_protocol.SendTransaction(sb) - return await node.send_transaction(tx) + return await node.send_transaction(tx, test=True) async def gen_and_send_sb(self, node, peer, *args, **kwargs): sb = generate_test_spend_bundle(*args, **kwargs) @@ -575,7 +580,7 @@ async def condition_tester( tx1: full_node_protocol.RespondTransaction = full_node_protocol.RespondTransaction(spend_bundle1) - status, err = await respond_transaction(full_node_1, tx1, peer) + status, err = await respond_transaction(full_node_1, tx1, peer, test=True) return blocks, spend_bundle1, peer, status, err @pytest.mark.asyncio @@ -590,6 +595,7 @@ async def condition_tester2(self, bt, two_nodes_mempool, wallet_a, test_fun: Cal guarantee_transaction_block=True, farmer_reward_puzzle_hash=reward_ph, pool_reward_puzzle_hash=reward_ph, + time_per_block=10, ) peer = await connect_and_get_peer(server_1, server_2, bt.config["self_hostname"]) @@ -919,7 +925,7 @@ async def test_assert_time_relative_exceeds(self, bt, two_nodes_mempool, wallet_ tx2: full_node_protocol.RespondTransaction = full_node_protocol.RespondTransaction(spend_bundle1) - status, err = await respond_transaction(full_node_1, tx2, peer) + status, err = await respond_transaction(full_node_1, tx2, peer, test=True) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err is None @@ -1459,7 +1465,7 @@ async def test_stealing_fee(self, bt, two_nodes_mempool, wallet_a): tx1: full_node_protocol.RespondTransaction = full_node_protocol.RespondTransaction(spend_bundle1) - status, err = await respond_transaction(full_node_1, tx1, peer) + status, err = await respond_transaction(full_node_1, tx1, peer, test=True) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1506,7 +1512,7 @@ async def test_double_spend_same_bundle(self, bt, two_nodes_mempool, wallet_a): tx: full_node_protocol.RespondTransaction = full_node_protocol.RespondTransaction(spend_bundle_combined) peer = await connect_and_get_peer(server_1, server_2, bt.config["self_hostname"]) - status, err = await respond_transaction(full_node_1, tx, peer) + status, err = await respond_transaction(full_node_1, tx, peer, test=True) sb = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle_combined.name()) assert err == Err.DOUBLE_SPEND @@ -1556,7 +1562,7 @@ async def test_agg_sig_condition(self, bt, two_nodes_mempool, wallet_a): # assert spend_bundle is not None # # tx: full_node_protocol.RespondTransaction = full_node_protocol.RespondTransaction(spend_bundle) - # await full_node_1.respond_transaction(tx, peer) + # await full_node_1.respond_transaction(tx, peer, test=True) # # sb = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle.name()) # assert sb is spend_bundle @@ -2449,7 +2455,7 @@ async def test_invalid_coin_spend_coin(self, bt, two_nodes_mempool, wallet_a): coin_spend_0 = recursive_replace(spend_bundle.coin_spends[0], "coin.puzzle_hash", bytes32([1] * 32)) new_bundle = recursive_replace(spend_bundle, "coin_spends", [coin_spend_0] + spend_bundle.coin_spends[1:]) assert spend_bundle is not None - res = await full_node_1.full_node.respond_transaction(new_bundle, new_bundle.name()) + res = await full_node_1.full_node.respond_transaction(new_bundle, new_bundle.name(), test=True) assert res == (MempoolInclusionStatus.FAILED, Err.INVALID_SPEND_BUNDLE) From aa296db3cbeab6f1f28fc7704ede2505f79314ab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 25 Mar 2022 22:23:08 -0700 Subject: [PATCH 261/378] Bump colorlog from 5.0.1 to 6.6.0 (#9207) Bumps [colorlog](https://github.com/borntyping/python-colorlog) from 5.0.1 to 6.6.0. - [Release notes](https://github.com/borntyping/python-colorlog/releases) - [Commits](https://github.com/borntyping/python-colorlog/compare/v5.0.1...v6.6.0) --- updated-dependencies: - dependency-name: colorlog dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7677b28ba62f..a4856bf5b125 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ "aiosqlite==0.17.0", # asyncio wrapper for sqlite, to store blocks "bitstring==3.1.9", # Binary data management library "colorama==0.4.4", # Colorizes terminal output - "colorlog==5.0.1", # Adds color to logs + "colorlog==6.6.0", # Adds color to logs "concurrent-log-handler==0.9.19", # Concurrently log and rotate logs "cryptography==3.4.7", # Python cryptography library for TLS - keyring conflict "fasteners==0.16.3", # For interprocess file locking, expected to be replaced by filelock From 8781569829910bfd0e4a90c720736df046cb0f5e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 25 Mar 2022 22:23:27 -0700 Subject: [PATCH 262/378] Bump actions/checkout from 2 to 3 (#10505) * Bump actions/checkout from 2 to 3 Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] * Update actions in templates too Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Gene Hoffman --- .github/workflows/benchmarks.yml | 2 +- .github/workflows/build-linux-arm64-installer.yml | 2 +- .github/workflows/build-linux-installer-deb.yml | 2 +- .github/workflows/build-linux-installer-rpm.yml | 2 +- .github/workflows/build-macos-installer.yml | 2 +- .github/workflows/build-macos-m1-installer.yml | 2 +- .github/workflows/build-test-macos-blockchain.yml | 4 ++-- .github/workflows/build-test-macos-clvm.yml | 2 +- .github/workflows/build-test-macos-core-cmds.yml | 4 ++-- .github/workflows/build-test-macos-core-consensus.yml | 4 ++-- .github/workflows/build-test-macos-core-custom_types.yml | 4 ++-- .github/workflows/build-test-macos-core-daemon.yml | 4 ++-- .../workflows/build-test-macos-core-full_node-full_sync.yml | 4 ++-- .github/workflows/build-test-macos-core-full_node-stores.yml | 4 ++-- .github/workflows/build-test-macos-core-full_node.yml | 4 ++-- .github/workflows/build-test-macos-core-server.yml | 4 ++-- .github/workflows/build-test-macos-core-ssl.yml | 4 ++-- .github/workflows/build-test-macos-core-util.yml | 4 ++-- .github/workflows/build-test-macos-core.yml | 4 ++-- .github/workflows/build-test-macos-farmer_harvester.yml | 4 ++-- .github/workflows/build-test-macos-generator.yml | 4 ++-- .github/workflows/build-test-macos-plotting.yml | 4 ++-- .github/workflows/build-test-macos-pools.yml | 4 ++-- .github/workflows/build-test-macos-simulation.yml | 4 ++-- .github/workflows/build-test-macos-tools.yml | 4 ++-- .github/workflows/build-test-macos-util.yml | 4 ++-- .github/workflows/build-test-macos-wallet-cat_wallet.yml | 4 ++-- .github/workflows/build-test-macos-wallet-did_wallet.yml | 4 ++-- .github/workflows/build-test-macos-wallet-rl_wallet.yml | 4 ++-- .github/workflows/build-test-macos-wallet-rpc.yml | 4 ++-- .github/workflows/build-test-macos-wallet-simple_sync.yml | 4 ++-- .github/workflows/build-test-macos-wallet-sync.yml | 4 ++-- .github/workflows/build-test-macos-wallet.yml | 4 ++-- .github/workflows/build-test-macos-weight_proof.yml | 4 ++-- .github/workflows/build-test-ubuntu-blockchain.yml | 4 ++-- .github/workflows/build-test-ubuntu-clvm.yml | 2 +- .github/workflows/build-test-ubuntu-core-cmds.yml | 4 ++-- .github/workflows/build-test-ubuntu-core-consensus.yml | 4 ++-- .github/workflows/build-test-ubuntu-core-custom_types.yml | 4 ++-- .github/workflows/build-test-ubuntu-core-daemon.yml | 4 ++-- .../workflows/build-test-ubuntu-core-full_node-full_sync.yml | 4 ++-- .github/workflows/build-test-ubuntu-core-full_node-stores.yml | 4 ++-- .github/workflows/build-test-ubuntu-core-full_node.yml | 4 ++-- .github/workflows/build-test-ubuntu-core-server.yml | 4 ++-- .github/workflows/build-test-ubuntu-core-ssl.yml | 4 ++-- .github/workflows/build-test-ubuntu-core-util.yml | 4 ++-- .github/workflows/build-test-ubuntu-core.yml | 4 ++-- .github/workflows/build-test-ubuntu-farmer_harvester.yml | 4 ++-- .github/workflows/build-test-ubuntu-generator.yml | 4 ++-- .github/workflows/build-test-ubuntu-plotting.yml | 4 ++-- .github/workflows/build-test-ubuntu-pools.yml | 4 ++-- .github/workflows/build-test-ubuntu-simulation.yml | 4 ++-- .github/workflows/build-test-ubuntu-tools.yml | 4 ++-- .github/workflows/build-test-ubuntu-util.yml | 4 ++-- .github/workflows/build-test-ubuntu-wallet-cat_wallet.yml | 4 ++-- .github/workflows/build-test-ubuntu-wallet-did_wallet.yml | 4 ++-- .github/workflows/build-test-ubuntu-wallet-rl_wallet.yml | 4 ++-- .github/workflows/build-test-ubuntu-wallet-rpc.yml | 4 ++-- .github/workflows/build-test-ubuntu-wallet-simple_sync.yml | 4 ++-- .github/workflows/build-test-ubuntu-wallet-sync.yml | 4 ++-- .github/workflows/build-test-ubuntu-wallet.yml | 4 ++-- .github/workflows/build-test-ubuntu-weight_proof.yml | 4 ++-- .github/workflows/build-windows-installer.yml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/mozilla-ca-cert.yml | 2 +- .github/workflows/pre-commit.yml | 2 +- .github/workflows/super-linter.yml | 2 +- .github/workflows/test-install-scripts.yml | 4 ++-- .github/workflows/upload-pypi-source.yml | 2 +- tests/runner_templates/build-test-macos | 2 +- tests/runner_templates/build-test-ubuntu | 2 +- tests/runner_templates/checkout-test-plots.include.yml | 2 +- 72 files changed, 127 insertions(+), 127 deletions(-) diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index bbcbf914566e..9759b1f5c576 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -28,7 +28,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 diff --git a/.github/workflows/build-linux-arm64-installer.yml b/.github/workflows/build-linux-arm64-installer.yml index 5254306a4a96..109d981c2c17 100644 --- a/.github/workflows/build-linux-arm64-installer.yml +++ b/.github/workflows/build-linux-arm64-installer.yml @@ -31,7 +31,7 @@ jobs: - uses: Chia-Network/actions/clean-workspace@main - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 submodules: recursive diff --git a/.github/workflows/build-linux-installer-deb.yml b/.github/workflows/build-linux-installer-deb.yml index 3cb64637a209..00f835ed45b5 100644 --- a/.github/workflows/build-linux-installer-deb.yml +++ b/.github/workflows/build-linux-installer-deb.yml @@ -30,7 +30,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 submodules: recursive diff --git a/.github/workflows/build-linux-installer-rpm.yml b/.github/workflows/build-linux-installer-rpm.yml index e2fabeff2892..a287600d9321 100644 --- a/.github/workflows/build-linux-installer-rpm.yml +++ b/.github/workflows/build-linux-installer-rpm.yml @@ -31,7 +31,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 submodules: recursive diff --git a/.github/workflows/build-macos-installer.yml b/.github/workflows/build-macos-installer.yml index 56ab55481224..545933c9004c 100644 --- a/.github/workflows/build-macos-installer.yml +++ b/.github/workflows/build-macos-installer.yml @@ -29,7 +29,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 submodules: recursive diff --git a/.github/workflows/build-macos-m1-installer.yml b/.github/workflows/build-macos-m1-installer.yml index 889e464e8ea3..aeb329a5e6c9 100644 --- a/.github/workflows/build-macos-m1-installer.yml +++ b/.github/workflows/build-macos-m1-installer.yml @@ -29,7 +29,7 @@ jobs: - uses: Chia-Network/actions/clean-workspace@main - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 submodules: recursive diff --git a/.github/workflows/build-test-macos-blockchain.yml b/.github/workflows/build-test-macos-blockchain.yml index eb5c49115643..7bbe162bf030 100644 --- a/.github/workflows/build-test-macos-blockchain.yml +++ b/.github/workflows/build-test-macos-blockchain.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-macos-clvm.yml b/.github/workflows/build-test-macos-clvm.yml index 4b50f9858988..301c14f4c593 100644 --- a/.github/workflows/build-test-macos-clvm.yml +++ b/.github/workflows/build-test-macos-clvm.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 diff --git a/.github/workflows/build-test-macos-core-cmds.yml b/.github/workflows/build-test-macos-core-cmds.yml index 1b39507bb3e7..56f21050a0a0 100644 --- a/.github/workflows/build-test-macos-core-cmds.yml +++ b/.github/workflows/build-test-macos-core-cmds.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-macos-core-consensus.yml b/.github/workflows/build-test-macos-core-consensus.yml index de123a845a0f..8a4270a491af 100644 --- a/.github/workflows/build-test-macos-core-consensus.yml +++ b/.github/workflows/build-test-macos-core-consensus.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-macos-core-custom_types.yml b/.github/workflows/build-test-macos-core-custom_types.yml index da1af33f771d..05f219790556 100644 --- a/.github/workflows/build-test-macos-core-custom_types.yml +++ b/.github/workflows/build-test-macos-core-custom_types.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-macos-core-daemon.yml b/.github/workflows/build-test-macos-core-daemon.yml index bb949be6bde8..f37805790d38 100644 --- a/.github/workflows/build-test-macos-core-daemon.yml +++ b/.github/workflows/build-test-macos-core-daemon.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-macos-core-full_node-full_sync.yml b/.github/workflows/build-test-macos-core-full_node-full_sync.yml index 070827af0845..b63cd18ef95d 100644 --- a/.github/workflows/build-test-macos-core-full_node-full_sync.yml +++ b/.github/workflows/build-test-macos-core-full_node-full_sync.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-macos-core-full_node-stores.yml b/.github/workflows/build-test-macos-core-full_node-stores.yml index ae890bc0f991..67987142564e 100644 --- a/.github/workflows/build-test-macos-core-full_node-stores.yml +++ b/.github/workflows/build-test-macos-core-full_node-stores.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-macos-core-full_node.yml b/.github/workflows/build-test-macos-core-full_node.yml index 417aec6810ce..1fcb01a02bca 100644 --- a/.github/workflows/build-test-macos-core-full_node.yml +++ b/.github/workflows/build-test-macos-core-full_node.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-macos-core-server.yml b/.github/workflows/build-test-macos-core-server.yml index 5b214cac40ef..009e3e45ac6b 100644 --- a/.github/workflows/build-test-macos-core-server.yml +++ b/.github/workflows/build-test-macos-core-server.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-macos-core-ssl.yml b/.github/workflows/build-test-macos-core-ssl.yml index 5ff17da64fc6..933578daf19f 100644 --- a/.github/workflows/build-test-macos-core-ssl.yml +++ b/.github/workflows/build-test-macos-core-ssl.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-macos-core-util.yml b/.github/workflows/build-test-macos-core-util.yml index 7775a0d12166..60f894125084 100644 --- a/.github/workflows/build-test-macos-core-util.yml +++ b/.github/workflows/build-test-macos-core-util.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-macos-core.yml b/.github/workflows/build-test-macos-core.yml index 3b6535909f7d..2bff2a8443fb 100644 --- a/.github/workflows/build-test-macos-core.yml +++ b/.github/workflows/build-test-macos-core.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-macos-farmer_harvester.yml b/.github/workflows/build-test-macos-farmer_harvester.yml index 883e33ef291c..0c47da04bcbe 100644 --- a/.github/workflows/build-test-macos-farmer_harvester.yml +++ b/.github/workflows/build-test-macos-farmer_harvester.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-macos-generator.yml b/.github/workflows/build-test-macos-generator.yml index 2b4edb974fc9..8de17022e362 100644 --- a/.github/workflows/build-test-macos-generator.yml +++ b/.github/workflows/build-test-macos-generator.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-macos-plotting.yml b/.github/workflows/build-test-macos-plotting.yml index ec5b9cf89586..2538b5b578a2 100644 --- a/.github/workflows/build-test-macos-plotting.yml +++ b/.github/workflows/build-test-macos-plotting.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-macos-pools.yml b/.github/workflows/build-test-macos-pools.yml index 4f7901bebef1..5be9e30f6df3 100644 --- a/.github/workflows/build-test-macos-pools.yml +++ b/.github/workflows/build-test-macos-pools.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-macos-simulation.yml b/.github/workflows/build-test-macos-simulation.yml index e5c74f561369..740787646f62 100644 --- a/.github/workflows/build-test-macos-simulation.yml +++ b/.github/workflows/build-test-macos-simulation.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-macos-tools.yml b/.github/workflows/build-test-macos-tools.yml index aa43cf015dfc..b540fec94654 100644 --- a/.github/workflows/build-test-macos-tools.yml +++ b/.github/workflows/build-test-macos-tools.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-macos-util.yml b/.github/workflows/build-test-macos-util.yml index 3e5b5c191def..a09e4abd9058 100644 --- a/.github/workflows/build-test-macos-util.yml +++ b/.github/workflows/build-test-macos-util.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-macos-wallet-cat_wallet.yml b/.github/workflows/build-test-macos-wallet-cat_wallet.yml index bdbe48290fca..5fd931b6127b 100644 --- a/.github/workflows/build-test-macos-wallet-cat_wallet.yml +++ b/.github/workflows/build-test-macos-wallet-cat_wallet.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-macos-wallet-did_wallet.yml b/.github/workflows/build-test-macos-wallet-did_wallet.yml index 1bea77b1f4a5..6198c5b90f99 100644 --- a/.github/workflows/build-test-macos-wallet-did_wallet.yml +++ b/.github/workflows/build-test-macos-wallet-did_wallet.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-macos-wallet-rl_wallet.yml b/.github/workflows/build-test-macos-wallet-rl_wallet.yml index 2ebbe237c422..77b8f8a32bde 100644 --- a/.github/workflows/build-test-macos-wallet-rl_wallet.yml +++ b/.github/workflows/build-test-macos-wallet-rl_wallet.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-macos-wallet-rpc.yml b/.github/workflows/build-test-macos-wallet-rpc.yml index b7be9d327471..a81cf224cd81 100644 --- a/.github/workflows/build-test-macos-wallet-rpc.yml +++ b/.github/workflows/build-test-macos-wallet-rpc.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-macos-wallet-simple_sync.yml b/.github/workflows/build-test-macos-wallet-simple_sync.yml index c943ac4c6793..772cad839a95 100644 --- a/.github/workflows/build-test-macos-wallet-simple_sync.yml +++ b/.github/workflows/build-test-macos-wallet-simple_sync.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-macos-wallet-sync.yml b/.github/workflows/build-test-macos-wallet-sync.yml index ac0c0ce180d3..93dd7ea98680 100644 --- a/.github/workflows/build-test-macos-wallet-sync.yml +++ b/.github/workflows/build-test-macos-wallet-sync.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-macos-wallet.yml b/.github/workflows/build-test-macos-wallet.yml index 9f5f2376a2f9..5f9a43ac6f33 100644 --- a/.github/workflows/build-test-macos-wallet.yml +++ b/.github/workflows/build-test-macos-wallet.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-macos-weight_proof.yml b/.github/workflows/build-test-macos-weight_proof.yml index a20984374c1b..eb960d9d2555 100644 --- a/.github/workflows/build-test-macos-weight_proof.yml +++ b/.github/workflows/build-test-macos-weight_proof.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-ubuntu-blockchain.yml b/.github/workflows/build-test-ubuntu-blockchain.yml index 7c82decc4bc6..b12ddf152b3a 100644 --- a/.github/workflows/build-test-ubuntu-blockchain.yml +++ b/.github/workflows/build-test-ubuntu-blockchain.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-ubuntu-clvm.yml b/.github/workflows/build-test-ubuntu-clvm.yml index 8f636f0c5fac..a248ee202315 100644 --- a/.github/workflows/build-test-ubuntu-clvm.yml +++ b/.github/workflows/build-test-ubuntu-clvm.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 diff --git a/.github/workflows/build-test-ubuntu-core-cmds.yml b/.github/workflows/build-test-ubuntu-core-cmds.yml index 6a470e9443ec..fbc0e5ce3a0a 100644 --- a/.github/workflows/build-test-ubuntu-core-cmds.yml +++ b/.github/workflows/build-test-ubuntu-core-cmds.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-ubuntu-core-consensus.yml b/.github/workflows/build-test-ubuntu-core-consensus.yml index 1e5e31b97ecd..f39dde36407c 100644 --- a/.github/workflows/build-test-ubuntu-core-consensus.yml +++ b/.github/workflows/build-test-ubuntu-core-consensus.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-ubuntu-core-custom_types.yml b/.github/workflows/build-test-ubuntu-core-custom_types.yml index 6e7d4ce1603b..64b298ac0bce 100644 --- a/.github/workflows/build-test-ubuntu-core-custom_types.yml +++ b/.github/workflows/build-test-ubuntu-core-custom_types.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-ubuntu-core-daemon.yml b/.github/workflows/build-test-ubuntu-core-daemon.yml index caa4c4c4d4e3..08b2070a9cf5 100644 --- a/.github/workflows/build-test-ubuntu-core-daemon.yml +++ b/.github/workflows/build-test-ubuntu-core-daemon.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml b/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml index e7253f90235a..359a4ea7c13d 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-ubuntu-core-full_node-stores.yml b/.github/workflows/build-test-ubuntu-core-full_node-stores.yml index 18a91428d3b0..4dad8b797cf9 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node-stores.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node-stores.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-ubuntu-core-full_node.yml b/.github/workflows/build-test-ubuntu-core-full_node.yml index 42c82d0c353a..858baa51f443 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-ubuntu-core-server.yml b/.github/workflows/build-test-ubuntu-core-server.yml index 4f821ea031ca..6988285a465e 100644 --- a/.github/workflows/build-test-ubuntu-core-server.yml +++ b/.github/workflows/build-test-ubuntu-core-server.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-ubuntu-core-ssl.yml b/.github/workflows/build-test-ubuntu-core-ssl.yml index ca99844a326f..e57213856df4 100644 --- a/.github/workflows/build-test-ubuntu-core-ssl.yml +++ b/.github/workflows/build-test-ubuntu-core-ssl.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-ubuntu-core-util.yml b/.github/workflows/build-test-ubuntu-core-util.yml index ce765305e1e0..78720a60fc14 100644 --- a/.github/workflows/build-test-ubuntu-core-util.yml +++ b/.github/workflows/build-test-ubuntu-core-util.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-ubuntu-core.yml b/.github/workflows/build-test-ubuntu-core.yml index 82436e9c386e..d5a16c31292e 100644 --- a/.github/workflows/build-test-ubuntu-core.yml +++ b/.github/workflows/build-test-ubuntu-core.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-ubuntu-farmer_harvester.yml b/.github/workflows/build-test-ubuntu-farmer_harvester.yml index d7c4a87a5333..0e334124a252 100644 --- a/.github/workflows/build-test-ubuntu-farmer_harvester.yml +++ b/.github/workflows/build-test-ubuntu-farmer_harvester.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-ubuntu-generator.yml b/.github/workflows/build-test-ubuntu-generator.yml index 80b3cf9d0308..5930963c7c7c 100644 --- a/.github/workflows/build-test-ubuntu-generator.yml +++ b/.github/workflows/build-test-ubuntu-generator.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-ubuntu-plotting.yml b/.github/workflows/build-test-ubuntu-plotting.yml index d2b58a509fd9..5dfb0eb749e1 100644 --- a/.github/workflows/build-test-ubuntu-plotting.yml +++ b/.github/workflows/build-test-ubuntu-plotting.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-ubuntu-pools.yml b/.github/workflows/build-test-ubuntu-pools.yml index da00076e8ef4..a4a3fa2cddbb 100644 --- a/.github/workflows/build-test-ubuntu-pools.yml +++ b/.github/workflows/build-test-ubuntu-pools.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-ubuntu-simulation.yml b/.github/workflows/build-test-ubuntu-simulation.yml index 5338a00c34b2..e978bc111882 100644 --- a/.github/workflows/build-test-ubuntu-simulation.yml +++ b/.github/workflows/build-test-ubuntu-simulation.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-ubuntu-tools.yml b/.github/workflows/build-test-ubuntu-tools.yml index 22d7d2133aa8..8cada89c8edf 100644 --- a/.github/workflows/build-test-ubuntu-tools.yml +++ b/.github/workflows/build-test-ubuntu-tools.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-ubuntu-util.yml b/.github/workflows/build-test-ubuntu-util.yml index 62e097fcbedf..e45442742a1f 100644 --- a/.github/workflows/build-test-ubuntu-util.yml +++ b/.github/workflows/build-test-ubuntu-util.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml index 31fef9b66831..0c7e4ebaa269 100644 --- a/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml index 52ac48a37a32..2372455c433f 100644 --- a/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml index edb05105a71b..b7c4a16d874b 100644 --- a/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-ubuntu-wallet-rpc.yml b/.github/workflows/build-test-ubuntu-wallet-rpc.yml index 45b8003d424d..51de656d5981 100644 --- a/.github/workflows/build-test-ubuntu-wallet-rpc.yml +++ b/.github/workflows/build-test-ubuntu-wallet-rpc.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml b/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml index b580bdd7e6a7..e0f54f9584b4 100644 --- a/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml +++ b/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-ubuntu-wallet-sync.yml b/.github/workflows/build-test-ubuntu-wallet-sync.yml index e304d92947c0..b87e69eb2ed8 100644 --- a/.github/workflows/build-test-ubuntu-wallet-sync.yml +++ b/.github/workflows/build-test-ubuntu-wallet-sync.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-ubuntu-wallet.yml b/.github/workflows/build-test-ubuntu-wallet.yml index 2d8258c8828e..6f04b4e5f2ea 100644 --- a/.github/workflows/build-test-ubuntu-wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-test-ubuntu-weight_proof.yml b/.github/workflows/build-test-ubuntu-weight_proof.yml index 0ae325bb7ad9..c20171149264 100644 --- a/.github/workflows/build-test-ubuntu-weight_proof.yml +++ b/.github/workflows/build-test-ubuntu-weight_proof.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: ${{ runner.os }}-pip- - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' diff --git a/.github/workflows/build-windows-installer.yml b/.github/workflows/build-windows-installer.yml index 65bb4eeeb266..d621ef0ff971 100644 --- a/.github/workflows/build-windows-installer.yml +++ b/.github/workflows/build-windows-installer.yml @@ -23,7 +23,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 submodules: recursive diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 9e77092852cf..78c4b61a218b 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -40,7 +40,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/mozilla-ca-cert.yml b/.github/workflows/mozilla-ca-cert.yml index 4bcd7333e18a..73edbaa78a86 100644 --- a/.github/workflows/mozilla-ca-cert.yml +++ b/.github/workflows/mozilla-ca-cert.yml @@ -8,7 +8,7 @@ jobs: update_ca_module: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 ref: "${{ github.event.inputs.chia_ref }}" diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index dfebc9cda183..7ccc0f79b55d 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -14,6 +14,6 @@ jobs: pre-commit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: actions/setup-python@v2 - uses: pre-commit/action@v2.0.3 diff --git a/.github/workflows/super-linter.yml b/.github/workflows/super-linter.yml index 255ae54c0f32..77d3daba8fb5 100644 --- a/.github/workflows/super-linter.yml +++ b/.github/workflows/super-linter.yml @@ -49,7 +49,7 @@ jobs: # Checkout the code base # ########################## - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 ################################ # Run Linter against code base # diff --git a/.github/workflows/test-install-scripts.yml b/.github/workflows/test-install-scripts.yml index e8e27dd6156a..90917ac0f4ef 100644 --- a/.github/workflows/test-install-scripts.yml +++ b/.github/workflows/test-install-scripts.yml @@ -28,7 +28,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 @@ -181,7 +181,7 @@ jobs: # after installing git so we use that copy - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 diff --git a/.github/workflows/upload-pypi-source.yml b/.github/workflows/upload-pypi-source.yml index c02d72f92c50..9114d3498665 100644 --- a/.github/workflows/upload-pypi-source.yml +++ b/.github/workflows/upload-pypi-source.yml @@ -23,7 +23,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 submodules: recursive diff --git a/tests/runner_templates/build-test-macos b/tests/runner_templates/build-test-macos index b8d42f4a5fa9..88d648bc5081 100644 --- a/tests/runner_templates/build-test-macos +++ b/tests/runner_templates/build-test-macos @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 diff --git a/tests/runner_templates/build-test-ubuntu b/tests/runner_templates/build-test-ubuntu index 4b62ce49f5fc..75e1b6c580c8 100644 --- a/tests/runner_templates/build-test-ubuntu +++ b/tests/runner_templates/build-test-ubuntu @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 diff --git a/tests/runner_templates/checkout-test-plots.include.yml b/tests/runner_templates/checkout-test-plots.include.yml index 09e9cd034dbf..59f47e577d43 100644 --- a/tests/runner_templates/checkout-test-plots.include.yml +++ b/tests/runner_templates/checkout-test-plots.include.yml @@ -1,5 +1,5 @@ - name: Checkout test blocks and plots - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: 'Chia-Network/test-cache' path: '.chia' From 7c49e29eaf9d830d0598d02fd8a07dbc94b592ec Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 25 Mar 2022 22:23:51 -0700 Subject: [PATCH 263/378] Bump github/super-linter from 4.8.1 to 4.9.1 (#10894) * Bump github/super-linter from 4.8.1 to 4.9.1 Bumps [github/super-linter](https://github.com/github/super-linter) from 4.8.1 to 4.9.1. - [Release notes](https://github.com/github/super-linter/releases) - [Changelog](https://github.com/github/super-linter/blob/main/docs/release-process.md) - [Commits](https://github.com/github/super-linter/compare/v4.8.1...v4.9.1) --- updated-dependencies: - dependency-name: github/super-linter dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Ignore too-many-function-args in test_type_checking.py * black Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Gene Hoffman --- .github/workflows/super-linter.yml | 2 +- tests/core/util/test_type_checking.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/super-linter.yml b/.github/workflows/super-linter.yml index 77d3daba8fb5..8059e9035244 100644 --- a/.github/workflows/super-linter.yml +++ b/.github/workflows/super-linter.yml @@ -55,7 +55,7 @@ jobs: # Run Linter against code base # ################################ - name: Lint Code Base - uses: github/super-linter@v4.8.1 + uses: github/super-linter@v4.9.1 # uses: docker://github/super-linter:v3.10.2 env: VALIDATE_ALL_CODEBASE: true diff --git a/tests/core/util/test_type_checking.py b/tests/core/util/test_type_checking.py index 775affbe66b4..8e90f8ad4c34 100644 --- a/tests/core/util/test_type_checking.py +++ b/tests/core/util/test_type_checking.py @@ -57,7 +57,7 @@ class TestClass2: assert TestClass2(25) with raises(TypeError): - TestClass2(1, 2) + TestClass2(1, 2) # pylint: disable=too-many-function-args def test_StrictDataClassLists(self): @dataclass(frozen=True) From b1e7b8d0a18f39fd0b930c9e6df53ef186c957e5 Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Sun, 27 Mar 2022 06:05:20 +0200 Subject: [PATCH 264/378] fix type annotations for get_block_generator() (#10907) --- chia/consensus/block_body_validation.py | 5 +++-- chia/consensus/multiprocess_validation.py | 13 +++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/chia/consensus/block_body_validation.py b/chia/consensus/block_body_validation.py index 0ed75c90b18a..29d0567cb2b2 100644 --- a/chia/consensus/block_body_validation.py +++ b/chia/consensus/block_body_validation.py @@ -1,6 +1,6 @@ import collections import logging -from typing import Callable, Dict, List, Optional, Set, Tuple, Union +from typing import Awaitable, Callable, Dict, List, Optional, Set, Tuple, Union from chiabip158 import PyBIP158 from clvm.casts import int_from_bytes @@ -16,6 +16,7 @@ from chia.full_node.block_store import BlockStore from chia.full_node.coin_store import CoinStore from chia.full_node.mempool_check_conditions import get_name_puzzle_conditions, mempool_check_conditions_dict +from chia.types.block_protocol import BlockInfo from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.coin_record import CoinRecord @@ -45,7 +46,7 @@ async def validate_block_body( height: uint32, npc_result: Optional[NPCResult], fork_point_with_peak: Optional[uint32], - get_block_generator: Callable, + get_block_generator: Callable[[BlockInfo], Awaitable[Optional[BlockGenerator]]], *, validate_signature=True, ) -> Tuple[Optional[Err], Optional[NPCResult]]: diff --git a/chia/consensus/multiprocess_validation.py b/chia/consensus/multiprocess_validation.py index ba19fd1e4c7c..2ac7740fd45a 100644 --- a/chia/consensus/multiprocess_validation.py +++ b/chia/consensus/multiprocess_validation.py @@ -3,7 +3,7 @@ import traceback from concurrent.futures.process import ProcessPoolExecutor from dataclasses import dataclass -from typing import Callable, Dict, List, Optional, Sequence, Tuple, Union +from typing import Awaitable, Callable, Dict, List, Optional, Sequence, Tuple from blspy import AugSchemeMPL, G1Element @@ -17,6 +17,7 @@ from chia.consensus.get_block_challenge import get_block_challenge from chia.consensus.pot_iterations import calculate_iterations_quality, is_overflow_block from chia.full_node.mempool_check_conditions import get_name_puzzle_conditions +from chia.types.block_protocol import BlockInfo from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.blockchain_format.sub_epoch_summary import SubEpochSummary @@ -169,11 +170,11 @@ async def pre_validate_blocks_multiprocessing( constants: ConsensusConstants, constants_json: Dict, block_records: BlockchainInterface, - blocks: Sequence[Union[FullBlock, HeaderBlock]], + blocks: Sequence[FullBlock], pool: ProcessPoolExecutor, check_filter: bool, npc_results: Dict[uint32, NPCResult], - get_block_generator: Optional[Callable], + get_block_generator: Callable[[BlockInfo, Optional[Dict[bytes32, FullBlock]]], Awaitable[Optional[BlockGenerator]]], batch_size: int, wp_summaries: Optional[List[SubEpochSummary]] = None, *, @@ -288,7 +289,7 @@ async def pre_validate_blocks_multiprocessing( prev_b = block_rec diff_ssis.append((difficulty, sub_slot_iters)) - block_dict: Dict[bytes32, Union[FullBlock, HeaderBlock]] = {} + block_dict: Dict[bytes32, FullBlock] = {} for i, block in enumerate(blocks): block_dict[block.header_hash] = block if not block_record_was_present[i]: @@ -314,8 +315,8 @@ async def pre_validate_blocks_multiprocessing( # We ONLY add blocks which are in the past, based on header hashes (which are validated later) to the # prev blocks dict. This is important since these blocks are assumed to be valid and are used as previous # generator references - prev_blocks_dict: Dict[uint32, Union[FullBlock, HeaderBlock]] = {} - curr_b: Union[FullBlock, HeaderBlock] = block + prev_blocks_dict: Dict[bytes32, FullBlock] = {} + curr_b: FullBlock = block while curr_b.prev_header_hash in block_dict: curr_b = block_dict[curr_b.prev_header_hash] From 656d7d94d8361fdc93b86d8e59f5b29ebfc58d14 Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Sun, 27 Mar 2022 21:20:30 +0200 Subject: [PATCH 265/378] fix type annotations for FullBlock.header_hash and FullBlock.prev_header_hash (#10909) --- benchmarks/block_store.py | 10 ++-- chia/consensus/block_body_validation.py | 2 +- chia/full_node/full_node.py | 2 +- chia/full_node/full_node_api.py | 3 +- chia/types/full_block.py | 7 +-- tests/block_tools.py | 66 ++++++------------------- tests/weight_proof/test_weight_proof.py | 4 +- 7 files changed, 31 insertions(+), 63 deletions(-) diff --git a/benchmarks/block_store.py b/benchmarks/block_store.py index 4169bf50ab06..c2faf06fb2e7 100644 --- a/benchmarks/block_store.py +++ b/benchmarks/block_store.py @@ -288,10 +288,10 @@ async def run_add_block_benchmark(version: int): print("profiling get_full_blocks_at") start = monotonic() - for h in range(1, block_height): - blocks = await block_store.get_full_blocks_at([h]) + for hi in range(1, block_height): + blocks = await block_store.get_full_blocks_at([hi]) assert len(blocks) == 1 - assert blocks[0].height == h + assert blocks[0].height == hi stop = monotonic() total_time += stop - start @@ -352,8 +352,8 @@ async def run_add_block_benchmark(version: int): start = monotonic() for i in range(100): - h = random.randint(1, block_height - 100) - blocks = await block_store.get_block_records_in_range(h, h + 99) + hi = random.randint(1, block_height - 100) + blocks = await block_store.get_block_records_in_range(hi, hi + 99) assert len(blocks) == 100 stop = monotonic() diff --git a/chia/consensus/block_body_validation.py b/chia/consensus/block_body_validation.py index 29d0567cb2b2..5ce7cad6f590 100644 --- a/chia/consensus/block_body_validation.py +++ b/chia/consensus/block_body_validation.py @@ -346,7 +346,7 @@ async def validate_block_body( ) if curr.height == 0: break - curr = reorg_blocks[curr.height - 1] + curr = reorg_blocks[uint32(curr.height - 1)] assert curr is not None removal_coin_records: Dict[bytes32, CoinRecord] = {} diff --git a/chia/full_node/full_node.py b/chia/full_node/full_node.py index 1eb2a454f31f..080aacbb58c4 100644 --- a/chia/full_node/full_node.py +++ b/chia/full_node/full_node.py @@ -1544,7 +1544,7 @@ async def respond_block( elif added == ReceiveBlockResult.INVALID_BLOCK: assert error_code is not None self.log.error(f"Block {header_hash} at height {block.height} is invalid with code {error_code}.") - raise ConsensusError(error_code, header_hash) + raise ConsensusError(error_code, [header_hash]) elif added == ReceiveBlockResult.DISCONNECTED_BLOCK: self.log.info(f"Disconnected block {header_hash} at height {block.height}") diff --git a/chia/full_node/full_node_api.py b/chia/full_node/full_node_api.py index b61ee9a99b96..38e2debdda43 100644 --- a/chia/full_node/full_node_api.py +++ b/chia/full_node/full_node_api.py @@ -1196,11 +1196,12 @@ async def request_removals(self, request: wallet_protocol.RequestRemovals) -> Op block: Optional[FullBlock] = await self.full_node.block_store.get_full_block(request.header_hash) # We lock so that the coin store does not get modified + peak_height = self.full_node.blockchain.get_peak_height() if ( block is None or block.is_transaction_block() is False or block.height != request.height - or block.height > self.full_node.blockchain.get_peak_height() + or (peak_height is not None and block.height > peak_height) or self.full_node.blockchain.height_to_hash(block.height) != request.header_hash ): reject = wallet_protocol.RejectRemovalsRequest(request.height, request.header_hash) diff --git a/chia/types/full_block.py b/chia/types/full_block.py index 0f4156cd68fe..25a0772d6290 100644 --- a/chia/types/full_block.py +++ b/chia/types/full_block.py @@ -4,6 +4,7 @@ from chia.types.blockchain_format.foliage import Foliage, FoliageTransactionBlock, TransactionsInfo from chia.types.blockchain_format.program import SerializedProgram from chia.types.blockchain_format.reward_chain_block import RewardChainBlock +from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.blockchain_format.vdf import VDFProof from chia.types.end_of_slot_bundle import EndOfSubSlotBundle from chia.util.ints import uint32 @@ -30,11 +31,11 @@ class FullBlock(Streamable): ] # List of block heights of previous generators referenced in this block @property - def prev_header_hash(self): + def prev_header_hash(self) -> bytes32: return self.foliage.prev_block_hash @property - def height(self): + def height(self) -> uint32: return self.reward_chain_block.height @property @@ -46,7 +47,7 @@ def total_iters(self): return self.reward_chain_block.total_iters @property - def header_hash(self): + def header_hash(self) -> bytes32: return self.foliage.get_hash() def is_transaction_block(self) -> bool: diff --git a/tests/block_tools.py b/tests/block_tools.py index b3f415a4f902..2174912e1ffe 100644 --- a/tests/block_tools.py +++ b/tests/block_tools.py @@ -479,9 +479,7 @@ def get_consecutive_blocks( latest_block: BlockRecord = blocks[block_list[-1].header_hash] curr = latest_block while not curr.is_transaction_block: - # TODO: address hint error and remove ignore - # error: Invalid index type "bytes32" for "Dict[uint32, BlockRecord]"; expected type "uint32" [index] - curr = blocks[curr.prev_hash] # type: ignore[index] + curr = blocks[curr.prev_hash] start_timestamp = curr.timestamp start_height = curr.height @@ -489,9 +487,7 @@ def get_consecutive_blocks( blocks_added_this_sub_slot = 1 while not curr.first_in_sub_slot: - # TODO: address hint error and remove ignore - # error: Invalid index type "bytes32" for "Dict[uint32, BlockRecord]"; expected type "uint32" [index] - curr = blocks[curr.prev_hash] # type: ignore[index] + curr = blocks[curr.prev_hash] blocks_added_this_sub_slot += 1 finished_sub_slots_at_sp: List[EndOfSubSlotBundle] = [] # Sub-slots since last block, up to signage point @@ -524,10 +520,7 @@ def get_consecutive_blocks( ): if curr.height == 0: break - # TODO: address hint error and remove ignore - # error: Invalid index type "bytes32" for "Dict[uint32, BlockRecord]"; expected type - # "uint32" [index] - curr = blocks[curr.prev_hash] # type: ignore[index] + curr = blocks[curr.prev_hash] if curr.total_iters > sub_slot_start_total_iters: finished_sub_slots_at_sp = [] @@ -536,12 +529,9 @@ def get_consecutive_blocks( # Ignore this signage_point because it's in the past continue - # TODO: address hint error and remove ignore - # error: Argument 1 to "BlockCache" has incompatible type "Dict[uint32, BlockRecord]"; - # expected "Dict[bytes32, BlockRecord]" [arg-type] signage_point: SignagePoint = get_signage_point( constants, - BlockCache(blocks), # type: ignore[arg-type] + BlockCache(blocks), latest_block, sub_slot_start_total_iters, uint8(signage_point_index), @@ -694,15 +684,12 @@ def get_consecutive_blocks( eos_deficit: uint8 = ( latest_block.deficit if latest_block.deficit > 0 else constants.MIN_BLOCKS_PER_CHALLENGE_BLOCK ) - # TODO: address hint error and remove ignore - # error: Argument 5 to "get_icc" has incompatible type "Dict[uint32, BlockRecord]"; expected - # "Dict[bytes32, BlockRecord]" [arg-type] icc_eos_vdf, icc_ip_proof = get_icc( constants, uint128(sub_slot_start_total_iters + sub_slot_iters), finished_sub_slots_at_ip, latest_block, - blocks, # type: ignore[arg-type] + blocks, sub_slot_start_total_iters, eos_deficit, ) @@ -752,10 +739,7 @@ def get_consecutive_blocks( # This means there are blocks in this sub-slot curr = latest_block while not curr.is_challenge_block(constants) and not curr.first_in_sub_slot: - # TODO: address hint error and remove ignore - # error: Invalid index type "bytes32" for "Dict[uint32, BlockRecord]"; expected type - # "uint32" [index] - curr = blocks[curr.prev_hash] # type: ignore[index] + curr = blocks[curr.prev_hash] if curr.is_challenge_block(constants): icc_eos_iters = uint64(sub_slot_start_total_iters + sub_slot_iters - curr.total_iters) else: @@ -830,12 +814,9 @@ def get_consecutive_blocks( constants.NUM_SPS_SUB_SLOT, ): # note that we are passing in the finished slots which include the last slot - # TODO: address hint error and remove ignore - # error: Argument 1 to "BlockCache" has incompatible type "Dict[uint32, BlockRecord]"; - # expected "Dict[bytes32, BlockRecord]" [arg-type] signage_point = get_signage_point( constants, - BlockCache(blocks), # type: ignore[arg-type] + BlockCache(blocks), latest_block_eos, sub_slot_start_total_iters, uint8(signage_point_index), @@ -1356,20 +1337,16 @@ def finish_block( def get_challenges( constants: ConsensusConstants, - blocks: Dict[uint32, BlockRecord], + blocks: Dict[bytes32, BlockRecord], finished_sub_slots: List[EndOfSubSlotBundle], prev_header_hash: Optional[bytes32], ) -> Tuple[bytes32, bytes32]: if len(finished_sub_slots) == 0: if prev_header_hash is None: return constants.GENESIS_CHALLENGE, constants.GENESIS_CHALLENGE - # TODO: address hint error and remove ignore - # error: Invalid index type "bytes32" for "Dict[uint32, BlockRecord]"; expected type "uint32" [index] - curr: BlockRecord = blocks[prev_header_hash] # type: ignore[index] + curr: BlockRecord = blocks[prev_header_hash] while not curr.first_in_sub_slot: - # TODO: address hint error and remove ignore - # error: Invalid index type "bytes32" for "Dict[uint32, BlockRecord]"; expected type "uint32" [index] - curr = blocks[curr.prev_hash] # type: ignore[index] + curr = blocks[curr.prev_hash] assert curr.finished_challenge_slot_hashes is not None assert curr.finished_reward_slot_hashes is not None cc_challenge = curr.finished_challenge_slot_hashes[-1] @@ -1397,10 +1374,10 @@ def get_plot_tmp_dir(): def load_block_list( block_list: List[FullBlock], constants: ConsensusConstants -) -> Tuple[Dict[uint32, bytes32], uint64, Dict[uint32, BlockRecord]]: +) -> Tuple[Dict[uint32, bytes32], uint64, Dict[bytes32, BlockRecord]]: difficulty = 0 height_to_hash: Dict[uint32, bytes32] = {} - blocks: Dict[uint32, BlockRecord] = {} + blocks: Dict[bytes32, BlockRecord] = {} for full_block in block_list: if full_block.height == 0: difficulty = uint64(constants.DIFFICULTY_STARTING) @@ -1427,12 +1404,9 @@ def load_block_list( sp_hash, ) - # TODO: address hint error and remove ignore - # error: Argument 1 to "BlockCache" has incompatible type "Dict[uint32, BlockRecord]"; expected - # "Dict[bytes32, BlockRecord]" [arg-type] blocks[full_block.header_hash] = block_to_block_record( constants, - BlockCache(blocks), # type: ignore[arg-type] + BlockCache(blocks), required_iters, full_block, None, @@ -1500,7 +1474,7 @@ def get_icc( def get_full_block_and_block_record( constants: ConsensusConstants, - blocks: Dict[uint32, BlockRecord], + blocks: Dict[bytes32, BlockRecord], sub_slot_start_total_iters: uint128, signage_point_index: uint8, proof_of_space: ProofOfSpace, @@ -1540,11 +1514,6 @@ def get_full_block_and_block_record( sp_iters = calculate_sp_iters(constants, sub_slot_iters, signage_point_index) ip_iters = calculate_ip_iters(constants, sub_slot_iters, signage_point_index, required_iters) - # TODO: address hint error and remove ignore - # error: Argument 1 to "BlockCache" has incompatible type "Dict[uint32, BlockRecord]"; expected - # "Dict[bytes32, BlockRecord]" [arg-type] - # error: Argument 16 to "create_test_unfinished_block" has incompatible type "bytes"; expected "bytes32" - # [arg-type] unfinished_block = create_test_unfinished_block( constants, sub_slot_start_total_iters, @@ -1560,7 +1529,7 @@ def get_full_block_and_block_record( get_pool_signature, signage_point, timestamp, - BlockCache(blocks), # type: ignore[arg-type] + BlockCache(blocks), seed, # type: ignore[arg-type] block_generator, aggregate_signature, @@ -1574,12 +1543,9 @@ def get_full_block_and_block_record( slot_cc_challenge = overflow_cc_challenge slot_rc_challenge = overflow_rc_challenge - # TODO: address hint error and remove ignore - # error: Argument 2 to "finish_block" has incompatible type "Dict[uint32, BlockRecord]"; expected - # "Dict[bytes32, BlockRecord]" [arg-type] full_block, block_record = finish_block( constants, - blocks, # type: ignore[arg-type] + blocks, height_to_hash, finished_sub_slots, sub_slot_start_total_iters, diff --git a/tests/weight_proof/test_weight_proof.py b/tests/weight_proof/test_weight_proof.py index 8bc04a001af1..e7e7b86e6c85 100644 --- a/tests/weight_proof/test_weight_proof.py +++ b/tests/weight_proof/test_weight_proof.py @@ -72,12 +72,12 @@ def get_prev_ses_block(sub_blocks, last_hash) -> Tuple[BlockRecord, int]: async def load_blocks_dont_validate( blocks, ) -> Tuple[ - Dict[bytes32, HeaderBlock], Dict[uint32, bytes32], Dict[bytes32, BlockRecord], Dict[bytes32, SubEpochSummary] + Dict[bytes32, HeaderBlock], Dict[uint32, bytes32], Dict[bytes32, BlockRecord], Dict[uint32, SubEpochSummary] ]: header_cache: Dict[bytes32, HeaderBlock] = {} height_to_hash: Dict[uint32, bytes32] = {} sub_blocks: Dict[bytes32, BlockRecord] = {} - sub_epoch_summaries: Dict[bytes32, SubEpochSummary] = {} + sub_epoch_summaries: Dict[uint32, SubEpochSummary] = {} prev_block = None difficulty = test_constants.DIFFICULTY_STARTING block: FullBlock From a100dda37e53e4cbba8956d18d57ca8d5eaa1139 Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Mon, 28 Mar 2022 20:58:00 +0200 Subject: [PATCH 266/378] new DBWrapper supporting concurrent readers (#10166) * new DBWrapper supporting concurrent readers * adress review comments * fixup default database version, when file doesn't exist * remove unused argument --- benchmarks/block_ref.py | 6 +- benchmarks/block_store.py | 7 +- benchmarks/coin_store.py | 10 +- benchmarks/utils.py | 8 +- chia/clvm/spend_sim.py | 74 +-- chia/consensus/blockchain.py | 5 +- chia/full_node/block_height_map.py | 66 +- chia/full_node/block_store.py | 568 +++++++++--------- chia/full_node/coin_store.py | 428 ++++++------- chia/full_node/full_node.py | 61 +- chia/full_node/hint_store.py | 59 +- chia/util/db_wrapper.py | 114 +++- chia/util/initial-config.yaml | 5 + tests/blockchain/blockchain_test_utils.py | 37 +- tests/blockchain/test_blockchain.py | 8 +- tests/conftest.py | 4 +- tests/core/full_node/ram_db.py | 13 +- .../core/full_node/stores/test_block_store.py | 35 +- .../full_node/stores/test_full_node_store.py | 8 +- .../core/full_node/stores/test_hint_store.py | 10 +- tests/core/full_node/test_block_height_map.py | 127 ++-- tests/core/full_node/test_conditions.py | 6 +- tests/core/test_db_conversion.py | 31 +- tests/core/test_db_validation.py | 22 +- tests/core/util/test_db_wrapper.py | 225 +++++++ tests/util/blockchain.py | 8 +- tests/util/db_connection.py | 30 +- 27 files changed, 1198 insertions(+), 777 deletions(-) create mode 100644 tests/core/util/test_db_wrapper.py diff --git a/benchmarks/block_ref.py b/benchmarks/block_ref.py index cc5833400c04..31571484a982 100644 --- a/benchmarks/block_ref.py +++ b/benchmarks/block_ref.py @@ -17,7 +17,7 @@ from chia.types.blockchain_format.program import SerializedProgram from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.db_version import lookup_db_version -from chia.util.db_wrapper import DBWrapper +from chia.util.db_wrapper import DBWrapper2 from chia.util.ints import uint32 # the first transaction block. Each byte in transaction_height_delta is the @@ -57,7 +57,9 @@ async def main(db_path: Path): await connection.execute("pragma query_only=ON") db_version: int = await lookup_db_version(connection) - db_wrapper = DBWrapper(connection, db_version=db_version) + db_wrapper = DBWrapper2(connection, db_version=db_version) + await db_wrapper.add_connection(await aiosqlite.connect(db_path)) + block_store = await BlockStore.create(db_wrapper) hint_store = await HintStore.create(db_wrapper) coin_store = await CoinStore.create(db_wrapper) diff --git a/benchmarks/block_store.py b/benchmarks/block_store.py index c2faf06fb2e7..de41f5bec1b1 100644 --- a/benchmarks/block_store.py +++ b/benchmarks/block_store.py @@ -7,7 +7,7 @@ import sys from benchmarks.utils import clvm_generator -from chia.util.db_wrapper import DBWrapper +from chia.util.db_wrapper import DBWrapper2 from chia.util.ints import uint128, uint64, uint32, uint8 from utils import ( rewards, @@ -40,7 +40,7 @@ async def run_add_block_benchmark(version: int): verbose: bool = "--verbose" in sys.argv - db_wrapper: DBWrapper = await setup_db("block-store-benchmark.db", version) + db_wrapper: DBWrapper2 = await setup_db("block-store-benchmark.db", version) # keep track of benchmark total time all_test_time = 0.0 @@ -218,7 +218,6 @@ async def run_add_block_benchmark(version: int): await block_store.set_in_chain([(header_hash,)]) header_hashes.append(header_hash) await block_store.set_peak(header_hash) - await db_wrapper.db.commit() stop = monotonic() total_time += stop - start @@ -411,7 +410,7 @@ async def run_add_block_benchmark(version: int): print(f"database size: {db_size/1000000:.3f} MB") finally: - await db_wrapper.db.close() + await db_wrapper.close() if __name__ == "__main__": diff --git a/benchmarks/coin_store.py b/benchmarks/coin_store.py index 068f704b8577..08f31c09f19d 100644 --- a/benchmarks/coin_store.py +++ b/benchmarks/coin_store.py @@ -7,7 +7,7 @@ import os import sys -from chia.util.db_wrapper import DBWrapper +from chia.util.db_wrapper import DBWrapper2 from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.blockchain_format.coin import Coin from chia.util.ints import uint64, uint32 @@ -37,7 +37,7 @@ def make_coins(num: int) -> Tuple[List[Coin], List[bytes32]]: async def run_new_block_benchmark(version: int): verbose: bool = "--verbose" in sys.argv - db_wrapper: DBWrapper = await setup_db("coin-store-benchmark.db", version) + db_wrapper: DBWrapper2 = await setup_db("coin-store-benchmark.db", version) # keep track of benchmark total time all_test_time = 0.0 @@ -75,7 +75,6 @@ async def run_new_block_benchmark(version: int): additions, removals, ) - await db_wrapper.db.commit() # 19 seconds per block timestamp += 19 @@ -117,7 +116,6 @@ async def run_new_block_benchmark(version: int): additions, removals, ) - await db_wrapper.db.commit() stop = monotonic() # 19 seconds per block @@ -168,7 +166,6 @@ async def run_new_block_benchmark(version: int): additions, removals, ) - await db_wrapper.db.commit() stop = monotonic() @@ -218,7 +215,6 @@ async def run_new_block_benchmark(version: int): additions, removals, ) - await db_wrapper.db.commit() stop = monotonic() # 19 seconds per block @@ -305,7 +301,7 @@ async def run_new_block_benchmark(version: int): print(f"all tests completed in {all_test_time:0.4f}s") finally: - await db_wrapper.db.close() + await db_wrapper.close() db_size = os.path.getsize(Path("coin-store-benchmark.db")) print(f"database size: {db_size/1000000:.3f} MB") diff --git a/benchmarks/utils.py b/benchmarks/utils.py index bda6216385c4..3646db14f381 100644 --- a/benchmarks/utils.py +++ b/benchmarks/utils.py @@ -12,7 +12,7 @@ from chia.types.blockchain_format.reward_chain_block import RewardChainBlock from chia.types.full_block import FullBlock from chia.util.ints import uint128 -from chia.util.db_wrapper import DBWrapper +from chia.util.db_wrapper import DBWrapper2 from typing import Tuple from pathlib import Path from datetime import datetime @@ -176,7 +176,7 @@ def rand_full_block() -> FullBlock: return full_block -async def setup_db(name: str, db_version: int) -> DBWrapper: +async def setup_db(name: str, db_version: int) -> DBWrapper2: db_filename = Path(name) try: os.unlink(db_filename) @@ -197,7 +197,9 @@ def sql_trace_callback(req: str): await connection.execute("pragma journal_mode=wal") await connection.execute("pragma synchronous=full") - return DBWrapper(connection, db_version) + ret = DBWrapper2(connection, db_version) + await ret.add_connection(await aiosqlite.connect(db_filename)) + return ret def get_commit_hash() -> str: diff --git a/chia/clvm/spend_sim.py b/chia/clvm/spend_sim.py index 5c22f365b01a..b448966e4ace 100644 --- a/chia/clvm/spend_sim.py +++ b/chia/clvm/spend_sim.py @@ -1,4 +1,5 @@ import aiosqlite +import random from dataclasses import dataclass from typing import Optional, List, Dict, Tuple, Any @@ -9,7 +10,7 @@ from chia.util.ints import uint64, uint32 from chia.util.hash import std_hash from chia.util.errors import Err, ValidationError -from chia.util.db_wrapper import DBWrapper +from chia.util.db_wrapper import DBWrapper2 from chia.util.streamable import Streamable, streamable from chia.types.coin_record import CoinRecord from chia.types.spend_bundle import SpendBundle @@ -79,7 +80,7 @@ class SimStore(Streamable): class SpendSim: - connection: aiosqlite.Connection + db_wrapper: DBWrapper2 mempool_manager: MempoolManager block_records: List[SimBlockRecord] blocks: List[SimFullBlock] @@ -90,39 +91,43 @@ class SpendSim: @classmethod async def create(cls, db_path=":memory:", defaults=DEFAULT_CONSTANTS): self = cls() - self.connection = DBWrapper(await aiosqlite.connect(db_path)) - coin_store = await CoinStore.create(self.connection) + uri = f"file:db_{random.randint(0, 99999999)}?mode=memory&cache=shared" + connection = await aiosqlite.connect(uri, uri=True) + self.db_wrapper = DBWrapper2(connection) + await self.db_wrapper.add_connection(await aiosqlite.connect(uri, uri=True)) + coin_store = await CoinStore.create(self.db_wrapper) self.mempool_manager = MempoolManager(coin_store, defaults) self.defaults = defaults # Load the next data if there is any - await self.connection.db.execute("CREATE TABLE IF NOT EXISTS block_data(data blob PRIMARY_KEY)") - cursor = await self.connection.db.execute("SELECT * from block_data") - row = await cursor.fetchone() - await cursor.close() - if row is not None: - store_data = SimStore.from_bytes(row[0]) - self.timestamp = store_data.timestamp - self.block_height = store_data.block_height - self.block_records = store_data.block_records - self.blocks = store_data.blocks - else: - self.timestamp = 1 - self.block_height = 0 - self.block_records = [] - self.blocks = [] - return self + async with self.db_wrapper.write_db() as conn: + await conn.execute("CREATE TABLE IF NOT EXISTS block_data(data blob PRIMARY_KEY)") + cursor = await conn.execute("SELECT * from block_data") + row = await cursor.fetchone() + await cursor.close() + if row is not None: + store_data = SimStore.from_bytes(row[0]) + self.timestamp = store_data.timestamp + self.block_height = store_data.block_height + self.block_records = store_data.block_records + self.blocks = store_data.blocks + else: + self.timestamp = 1 + self.block_height = 0 + self.block_records = [] + self.blocks = [] + return self async def close(self): - c = await self.connection.db.execute("DELETE FROM block_data") - await c.close() - c = await self.connection.db.execute( - "INSERT INTO block_data VALUES(?)", - (bytes(SimStore(self.timestamp, self.block_height, self.block_records, self.blocks)),), - ) - await c.close() - await self.connection.db.commit() - await self.connection.db.close() + async with self.db_wrapper.write_db() as conn: + c = await conn.execute("DELETE FROM block_data") + await c.close() + c = await conn.execute( + "INSERT INTO block_data VALUES(?)", + (bytes(SimStore(self.timestamp, self.block_height, self.block_records, self.blocks)),), + ) + await c.close() + await self.db_wrapper.close() async def new_peak(self): await self.mempool_manager.new_peak(self.block_records[-1], []) @@ -138,12 +143,13 @@ def new_coin_record(self, coin: Coin, coinbase=False) -> CoinRecord: async def all_non_reward_coins(self) -> List[Coin]: coins = set() - cursor = await self.mempool_manager.coin_store.coin_record_db.execute( - "SELECT * from coin_record WHERE coinbase=0 AND spent=0 ", - ) - rows = await cursor.fetchall() + async with self.mempool_manager.coin_store.db_wrapper.read_db() as conn: + cursor = await conn.execute( + "SELECT * from coin_record WHERE coinbase=0 AND spent=0 ", + ) + rows = await cursor.fetchall() - await cursor.close() + await cursor.close() for row in rows: coin = Coin(bytes32(bytes.fromhex(row[6])), bytes32(bytes.fromhex(row[5])), uint64.from_bytes(row[7])) coins.add(coin) diff --git a/chia/consensus/blockchain.py b/chia/consensus/blockchain.py index c7f400e13f60..e6ed0519b288 100644 --- a/chia/consensus/blockchain.py +++ b/chia/consensus/blockchain.py @@ -257,16 +257,14 @@ async def receive_block( None, ) # Always add the block to the database - async with self.block_store.db_wrapper.lock: + async with self.block_store.db_wrapper.write_db(): try: header_hash: bytes32 = block.header_hash # Perform the DB operations to update the state, and rollback if something goes wrong - await self.block_store.db_wrapper.begin_transaction() await self.block_store.add_full_block(header_hash, block, block_record) fork_height, peak_height, records, (coin_record_change, hint_changes) = await self._reconsider_peak( block_record, genesis, fork_point_with_peak, npc_result ) - await self.block_store.db_wrapper.commit_transaction() # Then update the memory cache. It is important that this task is not cancelled and does not throw self.add_block_record(block_record) @@ -281,7 +279,6 @@ async def receive_block( await self.__height_map.maybe_flush() except BaseException as e: self.block_store.rollback_cache_block(header_hash) - await self.block_store.db_wrapper.rollback_transaction() log.error( f"Error while adding block {block.header_hash} height {block.height}," f" rolling back: {traceback.format_exc()} {e}" diff --git a/chia/full_node/block_height_map.py b/chia/full_node/block_height_map.py index 216e4d4a32d2..98a4d2826df5 100644 --- a/chia/full_node/block_height_map.py +++ b/chia/full_node/block_height_map.py @@ -8,7 +8,7 @@ from dataclasses import dataclass from chia.util.streamable import Streamable, streamable from chia.util.files import write_file_async -from chia.util.db_wrapper import DBWrapper +from chia.util.db_wrapper import DBWrapper2 log = logging.getLogger(__name__) @@ -20,7 +20,7 @@ class SesCache(Streamable): class BlockHeightMap: - db: DBWrapper + db: DBWrapper2 # the below dictionaries are loaded from the database, from the peak # and back in time on startup. @@ -47,7 +47,7 @@ class BlockHeightMap: __ses_filename: Path @classmethod - async def create(cls, blockchain_dir: Path, db: DBWrapper) -> "BlockHeightMap": + async def create(cls, blockchain_dir: Path, db: DBWrapper2) -> "BlockHeightMap": self = BlockHeightMap() self.db = db @@ -57,26 +57,27 @@ async def create(cls, blockchain_dir: Path, db: DBWrapper) -> "BlockHeightMap": self.__height_to_hash_filename = blockchain_dir / "height-to-hash" self.__ses_filename = blockchain_dir / "sub-epoch-summaries" - if db.db_version == 2: - async with self.db.db.execute("SELECT hash FROM current_peak WHERE key = 0") as cursor: - peak_row = await cursor.fetchone() - if peak_row is None: - return self - - async with db.db.execute( - "SELECT header_hash,prev_hash,height,sub_epoch_summary FROM full_blocks WHERE header_hash=?", - (peak_row[0],), - ) as cursor: - row = await cursor.fetchone() - if row is None: - return self - else: - async with await db.db.execute( - "SELECT header_hash,prev_hash,height,sub_epoch_summary from block_records WHERE is_peak=1" - ) as cursor: - row = await cursor.fetchone() - if row is None: - return self + async with self.db.read_db() as conn: + if db.db_version == 2: + async with conn.execute("SELECT hash FROM current_peak WHERE key = 0") as cursor: + peak_row = await cursor.fetchone() + if peak_row is None: + return self + + async with conn.execute( + "SELECT header_hash,prev_hash,height,sub_epoch_summary FROM full_blocks WHERE header_hash=?", + (peak_row[0],), + ) as cursor: + row = await cursor.fetchone() + if row is None: + return self + else: + async with await conn.execute( + "SELECT header_hash,prev_hash,height,sub_epoch_summary from block_records WHERE is_peak=1" + ) as cursor: + row = await cursor.fetchone() + if row is None: + return self try: async with aiofiles.open(self.__height_to_hash_filename, "rb") as f: @@ -169,17 +170,18 @@ async def _load_blocks_from(self, height: uint32, prev_hash: bytes32): "INDEXED BY height WHERE height>=? AND height (height, prev-hash, sub-epoch-summary) - ordered: Dict[bytes32, Tuple[uint32, bytes32, Optional[bytes]]] = {} + # maps block-hash -> (height, prev-hash, sub-epoch-summary) + ordered: Dict[bytes32, Tuple[uint32, bytes32, Optional[bytes]]] = {} - if self.db.db_version == 2: - for r in await cursor.fetchall(): - ordered[r[0]] = (r[2], r[1], r[3]) - else: - for r in await cursor.fetchall(): - ordered[bytes32.fromhex(r[0])] = (r[2], bytes32.fromhex(r[1]), r[3]) + if self.db.db_version == 2: + for r in await cursor.fetchall(): + ordered[r[0]] = (r[2], r[1], r[3]) + else: + for r in await cursor.fetchall(): + ordered[bytes32.fromhex(r[0])] = (r[2], bytes32.fromhex(r[1]), r[3]) while height > window_end: entry = ordered[prev_hash] diff --git a/chia/full_node/block_store.py b/chia/full_node/block_store.py index 418e0b576036..3da53d27adaa 100644 --- a/chia/full_node/block_store.py +++ b/chia/full_node/block_store.py @@ -1,7 +1,6 @@ import logging from typing import Dict, List, Optional, Tuple, Any -import aiosqlite import zstd from chia.consensus.block_record import BlockRecord @@ -10,7 +9,7 @@ from chia.types.blockchain_format.program import SerializedProgram from chia.types.weight_proof import SubEpochChallengeSegment, SubEpochSegments from chia.util.errors import Err -from chia.util.db_wrapper import DBWrapper +from chia.util.db_wrapper import DBWrapper2 from chia.util.ints import uint32 from chia.util.lru_cache import LRUCache from chia.util.full_block_utils import generator_from_block @@ -19,94 +18,92 @@ class BlockStore: - db: aiosqlite.Connection block_cache: LRUCache - db_wrapper: DBWrapper + db_wrapper: DBWrapper2 ses_challenge_cache: LRUCache @classmethod - async def create(cls, db_wrapper: DBWrapper): + async def create(cls, db_wrapper: DBWrapper2): self = cls() - # All full blocks which have been added to the blockchain. Header_hash -> block self.db_wrapper = db_wrapper - self.db = db_wrapper.db - - if self.db_wrapper.db_version == 2: - - # TODO: most data in block is duplicated in block_record. The only - # reason for this is that our parsing of a FullBlock is so slow, - # it's faster to store duplicate data to parse less when we just - # need the BlockRecord. Once we fix the parsing (and data structure) - # of FullBlock, this can use less space - await self.db.execute( - "CREATE TABLE IF NOT EXISTS full_blocks(" - "header_hash blob PRIMARY KEY," - "prev_hash blob," - "height bigint," - "sub_epoch_summary blob," - "is_fully_compactified tinyint," - "in_main_chain tinyint," - "block blob," - "block_record blob)" - ) - - # This is a single-row table containing the hash of the current - # peak. The "key" field is there to make update statements simple - await self.db.execute("CREATE TABLE IF NOT EXISTS current_peak(key int PRIMARY KEY, hash blob)") - # If any of these indices are altered, they should also be altered - # in the chia/cmds/db_upgrade.py file - await self.db.execute("CREATE INDEX IF NOT EXISTS height on full_blocks(height)") + async with self.db_wrapper.write_db() as conn: - # Sub epoch segments for weight proofs - await self.db.execute( - "CREATE TABLE IF NOT EXISTS sub_epoch_segments_v3(" - "ses_block_hash blob PRIMARY KEY," - "challenge_segments blob)" - ) + if self.db_wrapper.db_version == 2: - # If any of these indices are altered, they should also be altered - # in the chia/cmds/db_upgrade.py file - await self.db.execute( - "CREATE INDEX IF NOT EXISTS is_fully_compactified ON" - " full_blocks(is_fully_compactified, in_main_chain) WHERE in_main_chain=1" - ) - await self.db.execute( - "CREATE INDEX IF NOT EXISTS main_chain ON full_blocks(height, in_main_chain) WHERE in_main_chain=1" - ) + # TODO: most data in block is duplicated in block_record. The only + # reason for this is that our parsing of a FullBlock is so slow, + # it's faster to store duplicate data to parse less when we just + # need the BlockRecord. Once we fix the parsing (and data structure) + # of FullBlock, this can use less space + await conn.execute( + "CREATE TABLE IF NOT EXISTS full_blocks(" + "header_hash blob PRIMARY KEY," + "prev_hash blob," + "height bigint," + "sub_epoch_summary blob," + "is_fully_compactified tinyint," + "in_main_chain tinyint," + "block blob," + "block_record blob)" + ) + + # This is a single-row table containing the hash of the current + # peak. The "key" field is there to make update statements simple + await conn.execute("CREATE TABLE IF NOT EXISTS current_peak(key int PRIMARY KEY, hash blob)") + + # If any of these indices are altered, they should also be altered + # in the chia/cmds/db_upgrade.py file + await conn.execute("CREATE INDEX IF NOT EXISTS height on full_blocks(height)") + + # Sub epoch segments for weight proofs + await conn.execute( + "CREATE TABLE IF NOT EXISTS sub_epoch_segments_v3(" + "ses_block_hash blob PRIMARY KEY," + "challenge_segments blob)" + ) + + # If any of these indices are altered, they should also be altered + # in the chia/cmds/db_upgrade.py file + await conn.execute( + "CREATE INDEX IF NOT EXISTS is_fully_compactified ON" + " full_blocks(is_fully_compactified, in_main_chain) WHERE in_main_chain=1" + ) + await conn.execute( + "CREATE INDEX IF NOT EXISTS main_chain ON full_blocks(height, in_main_chain) WHERE in_main_chain=1" + ) - else: + else: - await self.db.execute( - "CREATE TABLE IF NOT EXISTS full_blocks(header_hash text PRIMARY KEY, height bigint," - " is_block tinyint, is_fully_compactified tinyint, block blob)" - ) + await conn.execute( + "CREATE TABLE IF NOT EXISTS full_blocks(header_hash text PRIMARY KEY, height bigint," + " is_block tinyint, is_fully_compactified tinyint, block blob)" + ) - # Block records - await self.db.execute( - "CREATE TABLE IF NOT EXISTS block_records(header_hash " - "text PRIMARY KEY, prev_hash text, height bigint," - "block blob, sub_epoch_summary blob, is_peak tinyint, is_block tinyint)" - ) + # Block records + await conn.execute( + "CREATE TABLE IF NOT EXISTS block_records(header_hash " + "text PRIMARY KEY, prev_hash text, height bigint," + "block blob, sub_epoch_summary blob, is_peak tinyint, is_block tinyint)" + ) - # Sub epoch segments for weight proofs - await self.db.execute( - "CREATE TABLE IF NOT EXISTS sub_epoch_segments_v3(ses_block_hash text PRIMARY KEY," - "challenge_segments blob)" - ) + # Sub epoch segments for weight proofs + await conn.execute( + "CREATE TABLE IF NOT EXISTS sub_epoch_segments_v3(ses_block_hash text PRIMARY KEY," + "challenge_segments blob)" + ) - # Height index so we can look up in order of height for sync purposes - await self.db.execute("CREATE INDEX IF NOT EXISTS full_block_height on full_blocks(height)") - await self.db.execute( - "CREATE INDEX IF NOT EXISTS is_fully_compactified on full_blocks(is_fully_compactified)" - ) + # Height index so we can look up in order of height for sync purposes + await conn.execute("CREATE INDEX IF NOT EXISTS full_block_height on full_blocks(height)") + await conn.execute( + "CREATE INDEX IF NOT EXISTS is_fully_compactified on full_blocks(is_fully_compactified)" + ) - await self.db.execute("CREATE INDEX IF NOT EXISTS height on block_records(height)") + await conn.execute("CREATE INDEX IF NOT EXISTS height on block_records(height)") - await self.db.execute("CREATE INDEX IF NOT EXISTS peak on block_records(is_peak)") + await conn.execute("CREATE INDEX IF NOT EXISTS peak on block_records(is_peak)") - await self.db.commit() self.block_cache = LRUCache(1000) self.ses_challenge_cache = LRUCache(50) return self @@ -134,15 +131,17 @@ def maybe_decompress(self, block_bytes: bytes) -> FullBlock: async def rollback(self, height: int) -> None: if self.db_wrapper.db_version == 2: - await self.db.execute( - "UPDATE OR FAIL full_blocks SET in_main_chain=0 WHERE height>? AND in_main_chain=1", (height,) - ) + async with self.db_wrapper.write_db() as conn: + await conn.execute( + "UPDATE OR FAIL full_blocks SET in_main_chain=0 WHERE height>? AND in_main_chain=1", (height,) + ) async def set_in_chain(self, header_hashes: List[Tuple[bytes32]]) -> None: if self.db_wrapper.db_version == 2: - await self.db.executemany( - "UPDATE OR FAIL full_blocks SET in_main_chain=1 WHERE header_hash=?", header_hashes - ) + async with self.db_wrapper.write_db() as conn: + await conn.executemany( + "UPDATE OR FAIL full_blocks SET in_main_chain=1 WHERE header_hash=?", header_hashes + ) async def replace_proof(self, header_hash: bytes32, block: FullBlock) -> None: @@ -156,14 +155,15 @@ async def replace_proof(self, header_hash: bytes32, block: FullBlock) -> None: self.block_cache.put(header_hash, block) - await self.db.execute( - "UPDATE full_blocks SET block=?,is_fully_compactified=? WHERE header_hash=?", - ( - block_bytes, - int(block.is_fully_compactified()), - self.maybe_to_hex(header_hash), - ), - ) + async with self.db_wrapper.write_db() as conn: + await conn.execute( + "UPDATE full_blocks SET block=?,is_fully_compactified=? WHERE header_hash=?", + ( + block_bytes, + int(block.is_fully_compactified()), + self.maybe_to_hex(header_hash), + ), + ) async def add_full_block(self, header_hash: bytes32, block: FullBlock, block_record: BlockRecord) -> None: self.block_cache.put(header_hash, block) @@ -176,56 +176,57 @@ async def add_full_block(self, header_hash: bytes32, block: FullBlock, block_rec else bytes(block_record.sub_epoch_summary_included) ) - await self.db.execute( - "INSERT OR IGNORE INTO full_blocks VALUES(?, ?, ?, ?, ?, ?, ?, ?)", - ( - header_hash, - block.prev_header_hash, - block.height, - ses, - int(block.is_fully_compactified()), - False, # in_main_chain - self.compress(block), - bytes(block_record), - ), - ) + async with self.db_wrapper.write_db() as conn: + await conn.execute( + "INSERT OR IGNORE INTO full_blocks VALUES(?, ?, ?, ?, ?, ?, ?, ?)", + ( + header_hash, + block.prev_header_hash, + block.height, + ses, + int(block.is_fully_compactified()), + False, # in_main_chain + self.compress(block), + bytes(block_record), + ), + ) else: - await self.db.execute( - "INSERT OR IGNORE INTO full_blocks VALUES(?, ?, ?, ?, ?)", - ( - header_hash.hex(), - block.height, - int(block.is_transaction_block()), - int(block.is_fully_compactified()), - bytes(block), - ), - ) - - await self.db.execute( - "INSERT OR IGNORE INTO block_records VALUES(?, ?, ?, ?,?, ?, ?)", - ( - header_hash.hex(), - block.prev_header_hash.hex(), - block.height, - bytes(block_record), - None - if block_record.sub_epoch_summary_included is None - else bytes(block_record.sub_epoch_summary_included), - False, - block.is_transaction_block(), - ), - ) + async with self.db_wrapper.write_db() as conn: + await conn.execute( + "INSERT OR IGNORE INTO full_blocks VALUES(?, ?, ?, ?, ?)", + ( + header_hash.hex(), + block.height, + int(block.is_transaction_block()), + int(block.is_fully_compactified()), + bytes(block), + ), + ) + + await conn.execute( + "INSERT OR IGNORE INTO block_records VALUES(?, ?, ?, ?,?, ?, ?)", + ( + header_hash.hex(), + block.prev_header_hash.hex(), + block.height, + bytes(block_record), + None + if block_record.sub_epoch_summary_included is None + else bytes(block_record.sub_epoch_summary_included), + False, + block.is_transaction_block(), + ), + ) async def persist_sub_epoch_challenge_segments( self, ses_block_hash: bytes32, segments: List[SubEpochChallengeSegment] ) -> None: - async with self.db_wrapper.lock: - await self.db.execute( + async with self.db_wrapper.write_db() as conn: + await conn.execute( "INSERT OR REPLACE INTO sub_epoch_segments_v3 VALUES(?, ?)", (self.maybe_to_hex(ses_block_hash), bytes(SubEpochSegments(segments))), ) - await self.db.commit() async def get_sub_epoch_challenge_segments( self, @@ -235,11 +236,12 @@ async def get_sub_epoch_challenge_segments( if cached is not None: return cached - async with self.db.execute( - "SELECT challenge_segments from sub_epoch_segments_v3 WHERE ses_block_hash=?", - (self.maybe_to_hex(ses_block_hash),), - ) as cursor: - row = await cursor.fetchone() + async with self.db_wrapper.read_db() as conn: + async with conn.execute( + "SELECT challenge_segments from sub_epoch_segments_v3 WHERE ses_block_hash=?", + (self.maybe_to_hex(ses_block_hash),), + ) as cursor: + row = await cursor.fetchone() if row is not None: challenge_segments = SubEpochSegments.from_bytes(row[0]).challenge_segments @@ -261,10 +263,11 @@ async def get_full_block(self, header_hash: bytes32) -> Optional[FullBlock]: log.debug(f"cache hit for block {header_hash.hex()}") return cached log.debug(f"cache miss for block {header_hash.hex()}") - async with self.db.execute( - "SELECT block from full_blocks WHERE header_hash=?", (self.maybe_to_hex(header_hash),) - ) as cursor: - row = await cursor.fetchone() + async with self.db_wrapper.read_db() as conn: + async with conn.execute( + "SELECT block from full_blocks WHERE header_hash=?", (self.maybe_to_hex(header_hash),) + ) as cursor: + row = await cursor.fetchone() if row is not None: block = self.maybe_decompress(row[0]) self.block_cache.put(header_hash, block) @@ -277,10 +280,11 @@ async def get_full_block_bytes(self, header_hash: bytes32) -> Optional[bytes]: log.debug(f"cache hit for block {header_hash.hex()}") return bytes(cached) log.debug(f"cache miss for block {header_hash.hex()}") - async with self.db.execute( - "SELECT block from full_blocks WHERE header_hash=?", (self.maybe_to_hex(header_hash),) - ) as cursor: - row = await cursor.fetchone() + async with self.db_wrapper.read_db() as conn: + async with conn.execute( + "SELECT block from full_blocks WHERE header_hash=?", (self.maybe_to_hex(header_hash),) + ) as cursor: + row = await cursor.fetchone() if row is not None: if self.db_wrapper.db_version == 2: return zstd.decompress(row[0]) @@ -295,11 +299,12 @@ async def get_full_blocks_at(self, heights: List[uint32]) -> List[FullBlock]: heights_db = tuple(heights) formatted_str = f'SELECT block from full_blocks WHERE height in ({"?," * (len(heights_db) - 1)}?)' - async with self.db.execute(formatted_str, heights_db) as cursor: - ret: List[FullBlock] = [] - for row in await cursor.fetchall(): - ret.append(self.maybe_decompress(row[0])) - return ret + async with self.db_wrapper.read_db() as conn: + async with conn.execute(formatted_str, heights_db) as cursor: + ret: List[FullBlock] = [] + for row in await cursor.fetchall(): + ret.append(self.maybe_decompress(row[0])) + return ret async def get_generator(self, header_hash: bytes32) -> Optional[SerializedProgram]: @@ -309,24 +314,25 @@ async def get_generator(self, header_hash: bytes32) -> Optional[SerializedProgra return cached.transactions_generator formatted_str = "SELECT block, height from full_blocks WHERE header_hash=?" - async with self.db.execute(formatted_str, (self.maybe_to_hex(header_hash),)) as cursor: - row = await cursor.fetchone() - if row is None: - return None - if self.db_wrapper.db_version == 2: - block_bytes = zstd.decompress(row[0]) - else: - block_bytes = row[0] - - try: - return generator_from_block(block_bytes) - except Exception as e: - log.error(f"cheap parser failed for block at height {row[1]}: {e}") - # this is defensive, on the off-chance that - # generator_from_block() fails, fall back to the reliable - # definition of parsing a block - b = FullBlock.from_bytes(block_bytes) - return b.transactions_generator + async with self.db_wrapper.read_db() as conn: + async with conn.execute(formatted_str, (self.maybe_to_hex(header_hash),)) as cursor: + row = await cursor.fetchone() + if row is None: + return None + if self.db_wrapper.db_version == 2: + block_bytes = zstd.decompress(row[0]) + else: + block_bytes = row[0] + + try: + return generator_from_block(block_bytes) + except Exception as e: + log.error(f"cheap parser failed for block at height {row[1]}: {e}") + # this is defensive, on the off-chance that + # generator_from_block() fails, fall back to the reliable + # definition of parsing a block + b = FullBlock.from_bytes(block_bytes) + return b.transactions_generator async def get_generators_at(self, heights: List[uint32]) -> List[SerializedProgram]: assert self.db_wrapper.db_version == 2 @@ -340,22 +346,23 @@ async def get_generators_at(self, heights: List[uint32]) -> List[SerializedProgr f"SELECT block, height from full_blocks " f'WHERE in_main_chain=1 AND height in ({"?," * (len(heights_db) - 1)}?)' ) - async with self.db.execute(formatted_str, heights_db) as cursor: - async for row in cursor: - block_bytes = zstd.decompress(row[0]) - - try: - gen = generator_from_block(block_bytes) - except Exception as e: - log.error(f"cheap parser failed for block at height {row[1]}: {e}") - # this is defensive, on the off-chance that - # generator_from_block() fails, fall back to the reliable - # definition of parsing a block - b = FullBlock.from_bytes(block_bytes) - gen = b.transactions_generator - if gen is None: - raise ValueError(Err.GENERATOR_REF_HAS_NO_GENERATOR) - generators[uint32(row[1])] = gen + async with self.db_wrapper.read_db() as conn: + async with conn.execute(formatted_str, heights_db) as cursor: + async for row in cursor: + block_bytes = zstd.decompress(row[0]) + + try: + gen = generator_from_block(block_bytes) + except Exception as e: + log.error(f"cheap parser failed for block at height {row[1]}: {e}") + # this is defensive, on the off-chance that + # generator_from_block() fails, fall back to the reliable + # definition of parsing a block + b = FullBlock.from_bytes(block_bytes) + gen = b.transactions_generator + if gen is None: + raise ValueError(Err.GENERATOR_REF_HAS_NO_GENERATOR) + generators[uint32(row[1])] = gen return [generators[h] for h in heights] @@ -369,20 +376,22 @@ async def get_block_records_by_hash(self, header_hashes: List[bytes32]): all_blocks: Dict[bytes32, BlockRecord] = {} if self.db_wrapper.db_version == 2: - async with self.db.execute( - "SELECT header_hash,block_record FROM full_blocks " - f'WHERE header_hash in ({"?," * (len(header_hashes) - 1)}?)', - tuple(header_hashes), - ) as cursor: - for row in await cursor.fetchall(): - header_hash = bytes32(row[0]) - all_blocks[header_hash] = BlockRecord.from_bytes(row[1]) + async with self.db_wrapper.read_db() as conn: + async with conn.execute( + "SELECT header_hash,block_record FROM full_blocks " + f'WHERE header_hash in ({"?," * (len(header_hashes) - 1)}?)', + tuple(header_hashes), + ) as cursor: + for row in await cursor.fetchall(): + header_hash = bytes32(row[0]) + all_blocks[header_hash] = BlockRecord.from_bytes(row[1]) else: formatted_str = f'SELECT block from block_records WHERE header_hash in ({"?," * (len(header_hashes) - 1)}?)' - async with self.db.execute(formatted_str, tuple([hh.hex() for hh in header_hashes])) as cursor: - for row in await cursor.fetchall(): - block_rec: BlockRecord = BlockRecord.from_bytes(row[0]) - all_blocks[block_rec.header_hash] = block_rec + async with self.db_wrapper.read_db() as conn: + async with conn.execute(formatted_str, tuple([hh.hex() for hh in header_hashes])) as cursor: + for row in await cursor.fetchall(): + block_rec: BlockRecord = BlockRecord.from_bytes(row[0]) + all_blocks[block_rec.header_hash] = block_rec ret: List[BlockRecord] = [] for hh in header_hashes: @@ -409,15 +418,16 @@ async def get_blocks_by_hash(self, header_hashes: List[bytes32]) -> List[FullBlo f'SELECT header_hash, block from full_blocks WHERE header_hash in ({"?," * (len(header_hashes_db) - 1)}?)' ) all_blocks: Dict[bytes32, FullBlock] = {} - async with self.db.execute(formatted_str, header_hashes_db) as cursor: - for row in await cursor.fetchall(): - header_hash = self.maybe_from_hex(row[0]) - full_block: FullBlock = self.maybe_decompress(row[1]) - # TODO: address hint error and remove ignore - # error: Invalid index type "bytes" for "Dict[bytes32, FullBlock]"; - # expected type "bytes32" [index] - all_blocks[header_hash] = full_block # type: ignore[index] - self.block_cache.put(header_hash, full_block) + async with self.db_wrapper.read_db() as conn: + async with conn.execute(formatted_str, header_hashes_db) as cursor: + for row in await cursor.fetchall(): + header_hash = self.maybe_from_hex(row[0]) + full_block: FullBlock = self.maybe_decompress(row[1]) + # TODO: address hint error and remove ignore + # error: Invalid index type "bytes" for "Dict[bytes32, FullBlock]"; + # expected type "bytes32" [index] + all_blocks[header_hash] = full_block # type: ignore[index] + self.block_cache.put(header_hash, full_block) ret: List[FullBlock] = [] for hh in header_hashes: if hh not in all_blocks: @@ -429,20 +439,22 @@ async def get_block_record(self, header_hash: bytes32) -> Optional[BlockRecord]: if self.db_wrapper.db_version == 2: - async with self.db.execute( - "SELECT block_record FROM full_blocks WHERE header_hash=?", - (header_hash,), - ) as cursor: - row = await cursor.fetchone() + async with self.db_wrapper.read_db() as conn: + async with conn.execute( + "SELECT block_record FROM full_blocks WHERE header_hash=?", + (header_hash,), + ) as cursor: + row = await cursor.fetchone() if row is not None: return BlockRecord.from_bytes(row[0]) else: - async with self.db.execute( - "SELECT block from block_records WHERE header_hash=?", - (header_hash.hex(),), - ) as cursor: - row = await cursor.fetchone() + async with self.db_wrapper.read_db() as conn: + async with conn.execute( + "SELECT block from block_records WHERE header_hash=?", + (header_hash.hex(),), + ) as cursor: + row = await cursor.fetchone() if row is not None: return BlockRecord.from_bytes(row[0]) return None @@ -460,40 +472,45 @@ async def get_block_records_in_range( ret: Dict[bytes32, BlockRecord] = {} if self.db_wrapper.db_version == 2: - async with self.db.execute( - "SELECT header_hash, block_record FROM full_blocks WHERE height >= ? AND height <= ?", - (start, stop), - ) as cursor: - for row in await cursor.fetchall(): - header_hash = bytes32(row[0]) - ret[header_hash] = BlockRecord.from_bytes(row[1]) + async with self.db_wrapper.read_db() as conn: + async with conn.execute( + "SELECT header_hash, block_record FROM full_blocks WHERE height >= ? AND height <= ?", + (start, stop), + ) as cursor: + for row in await cursor.fetchall(): + header_hash = bytes32(row[0]) + ret[header_hash] = BlockRecord.from_bytes(row[1]) else: formatted_str = f"SELECT header_hash, block from block_records WHERE height >= {start} and height <= {stop}" - async with await self.db.execute(formatted_str) as cursor: - for row in await cursor.fetchall(): - header_hash = bytes32(self.maybe_from_hex(row[0])) - ret[header_hash] = BlockRecord.from_bytes(row[1]) + async with self.db_wrapper.read_db() as conn: + async with await conn.execute(formatted_str) as cursor: + for row in await cursor.fetchall(): + header_hash = bytes32(self.maybe_from_hex(row[0])) + ret[header_hash] = BlockRecord.from_bytes(row[1]) return ret async def get_peak(self) -> Optional[Tuple[bytes32, uint32]]: if self.db_wrapper.db_version == 2: - async with self.db.execute("SELECT hash FROM current_peak WHERE key = 0") as cursor: - peak_row = await cursor.fetchone() + async with self.db_wrapper.read_db() as conn: + async with conn.execute("SELECT hash FROM current_peak WHERE key = 0") as cursor: + peak_row = await cursor.fetchone() if peak_row is None: return None - async with self.db.execute("SELECT height FROM full_blocks WHERE header_hash=?", (peak_row[0],)) as cursor: - peak_height = await cursor.fetchone() + async with self.db_wrapper.read_db() as conn: + async with conn.execute("SELECT height FROM full_blocks WHERE header_hash=?", (peak_row[0],)) as cursor: + peak_height = await cursor.fetchone() if peak_height is None: return None return bytes32(peak_row[0]), uint32(peak_height[0]) else: - async with self.db.execute("SELECT header_hash, height from block_records WHERE is_peak = 1") as cursor: - peak_row = await cursor.fetchone() + async with self.db_wrapper.read_db() as conn: + async with conn.execute("SELECT header_hash, height from block_records WHERE is_peak = 1") as cursor: + peak_row = await cursor.fetchone() if peak_row is None: return None return bytes32(bytes.fromhex(peak_row[0])), uint32(peak_row[1]) @@ -513,20 +530,22 @@ async def get_block_records_close_to_peak( ret: Dict[bytes32, BlockRecord] = {} if self.db_wrapper.db_version == 2: - async with self.db.execute( - "SELECT header_hash, block_record FROM full_blocks WHERE height >= ?", - (peak[1] - blocks_n,), - ) as cursor: - for row in await cursor.fetchall(): - header_hash = bytes32(row[0]) - ret[header_hash] = BlockRecord.from_bytes(row[1]) + async with self.db_wrapper.read_db() as conn: + async with conn.execute( + "SELECT header_hash, block_record FROM full_blocks WHERE height >= ?", + (peak[1] - blocks_n,), + ) as cursor: + for row in await cursor.fetchall(): + header_hash = bytes32(row[0]) + ret[header_hash] = BlockRecord.from_bytes(row[1]) else: formatted_str = f"SELECT header_hash, block from block_records WHERE height >= {peak[1] - blocks_n}" - async with self.db.execute(formatted_str) as cursor: - for row in await cursor.fetchall(): - header_hash = bytes32(self.maybe_from_hex(row[0])) - ret[header_hash] = BlockRecord.from_bytes(row[1]) + async with self.db_wrapper.read_db() as conn: + async with conn.execute(formatted_str) as cursor: + for row in await cursor.fetchall(): + header_hash = bytes32(self.maybe_from_hex(row[0])) + ret[header_hash] = BlockRecord.from_bytes(row[1]) return ret, peak[0] @@ -536,19 +555,22 @@ async def set_peak(self, header_hash: bytes32) -> None: if self.db_wrapper.db_version == 2: # Note: we use the key field as 0 just to ensure all inserts replace the existing row - await self.db.execute("INSERT OR REPLACE INTO current_peak VALUES(?, ?)", (0, header_hash)) + async with self.db_wrapper.write_db() as conn: + await conn.execute("INSERT OR REPLACE INTO current_peak VALUES(?, ?)", (0, header_hash)) else: - await self.db.execute("UPDATE block_records SET is_peak=0 WHERE is_peak=1") - await self.db.execute( - "UPDATE block_records SET is_peak=1 WHERE header_hash=?", - (self.maybe_to_hex(header_hash),), - ) + async with self.db_wrapper.write_db() as conn: + await conn.execute("UPDATE block_records SET is_peak=0 WHERE is_peak=1") + await conn.execute( + "UPDATE block_records SET is_peak=1 WHERE header_hash=?", + (self.maybe_to_hex(header_hash),), + ) async def is_fully_compactified(self, header_hash: bytes32) -> Optional[bool]: - async with self.db.execute( - "SELECT is_fully_compactified from full_blocks WHERE header_hash=?", (self.maybe_to_hex(header_hash),) - ) as cursor: - row = await cursor.fetchone() + async with self.db_wrapper.write_db() as conn: + async with conn.execute( + "SELECT is_fully_compactified from full_blocks WHERE header_hash=?", (self.maybe_to_hex(header_hash),) + ) as cursor: + row = await cursor.fetchone() if row is None: return None return bool(row[0]) @@ -556,20 +578,22 @@ async def is_fully_compactified(self, header_hash: bytes32) -> Optional[bool]: async def get_random_not_compactified(self, number: int) -> List[int]: if self.db_wrapper.db_version == 2: - async with self.db.execute( - f"SELECT height FROM full_blocks WHERE in_main_chain=1 AND is_fully_compactified=0 " - f"ORDER BY RANDOM() LIMIT {number}" - ) as cursor: - rows = await cursor.fetchall() + async with self.db_wrapper.read_db() as conn: + async with conn.execute( + f"SELECT height FROM full_blocks WHERE in_main_chain=1 AND is_fully_compactified=0 " + f"ORDER BY RANDOM() LIMIT {number}" + ) as cursor: + rows = await cursor.fetchall() else: # Since orphan blocks do not get compactified, we need to check whether all blocks with a # certain height are not compact. And if we do have compact orphan blocks, then all that # happens is that the occasional chain block stays uncompact - not ideal, but harmless. - async with self.db.execute( - f"SELECT height FROM full_blocks GROUP BY height HAVING sum(is_fully_compactified)=0 " - f"ORDER BY RANDOM() LIMIT {number}" - ) as cursor: - rows = await cursor.fetchall() + async with self.db_wrapper.read_db() as conn: + async with conn.execute( + f"SELECT height FROM full_blocks GROUP BY height HAVING sum(is_fully_compactified)=0 " + f"ORDER BY RANDOM() LIMIT {number}" + ) as cursor: + rows = await cursor.fetchall() heights = [int(row[0]) for row in rows] @@ -578,13 +602,15 @@ async def get_random_not_compactified(self, number: int) -> List[int]: async def count_compactified_blocks(self) -> int: if self.db_wrapper.db_version == 2: # DB V2 has an index on is_fully_compactified only for blocks in the main chain - async with self.db.execute( - "select count(*) from full_blocks where is_fully_compactified=1 and in_main_chain=1" - ) as cursor: - row = await cursor.fetchone() + async with self.db_wrapper.read_db() as conn: + async with conn.execute( + "select count(*) from full_blocks where is_fully_compactified=1 and in_main_chain=1" + ) as cursor: + row = await cursor.fetchone() else: - async with self.db.execute("select count(*) from full_blocks where is_fully_compactified=1") as cursor: - row = await cursor.fetchone() + async with self.db_wrapper.read_db() as conn: + async with conn.execute("select count(*) from full_blocks where is_fully_compactified=1") as cursor: + row = await cursor.fetchone() assert row is not None @@ -594,13 +620,15 @@ async def count_compactified_blocks(self) -> int: async def count_uncompactified_blocks(self) -> int: if self.db_wrapper.db_version == 2: # DB V2 has an index on is_fully_compactified only for blocks in the main chain - async with self.db.execute( - "select count(*) from full_blocks where is_fully_compactified=0 and in_main_chain=1" - ) as cursor: - row = await cursor.fetchone() + async with self.db_wrapper.read_db() as conn: + async with conn.execute( + "select count(*) from full_blocks where is_fully_compactified=0 and in_main_chain=1" + ) as cursor: + row = await cursor.fetchone() else: - async with self.db.execute("select count(*) from full_blocks where is_fully_compactified=0") as cursor: - row = await cursor.fetchone() + async with self.db_wrapper.read_db() as conn: + async with conn.execute("select count(*) from full_blocks where is_fully_compactified=0") as cursor: + row = await cursor.fetchone() assert row is not None diff --git a/chia/full_node/coin_store.py b/chia/full_node/coin_store.py index 8e6420f4849a..4b91d80f730a 100644 --- a/chia/full_node/coin_store.py +++ b/chia/full_node/coin_store.py @@ -1,10 +1,9 @@ from typing import List, Optional, Set, Dict, Any, Tuple -import aiosqlite from chia.protocols.wallet_protocol import CoinState from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.coin_record import CoinRecord -from chia.util.db_wrapper import DBWrapper +from chia.util.db_wrapper import DBWrapper2 from chia.util.ints import uint32, uint64 from chia.util.lru_cache import LRUCache from chia.util.chunks import chunks @@ -22,74 +21,72 @@ class CoinStore: A cache is maintained for quicker access to recent coins. """ - coin_record_db: aiosqlite.Connection coin_record_cache: LRUCache cache_size: uint32 - db_wrapper: DBWrapper + db_wrapper: DBWrapper2 @classmethod - async def create(cls, db_wrapper: DBWrapper, cache_size: uint32 = uint32(60000)): + async def create(cls, db_wrapper: DBWrapper2, cache_size: uint32 = uint32(60000)): self = cls() self.cache_size = cache_size self.db_wrapper = db_wrapper - self.coin_record_db = db_wrapper.db - if self.db_wrapper.db_version == 2: - - # the coin_name is unique in this table because the CoinStore always - # only represent a single peak - await self.coin_record_db.execute( - "CREATE TABLE IF NOT EXISTS coin_record(" - "coin_name blob PRIMARY KEY," - " confirmed_index bigint," - " spent_index bigint," # if this is zero, it means the coin has not been spent - " coinbase int," - " puzzle_hash blob," - " coin_parent blob," - " amount blob," # we use a blob of 8 bytes to store uint64 - " timestamp bigint)" - ) + async with self.db_wrapper.write_db() as conn: - else: + if self.db_wrapper.db_version == 2: - # the coin_name is unique in this table because the CoinStore always - # only represent a single peak - await self.coin_record_db.execute( - ( + # the coin_name is unique in this table because the CoinStore always + # only represent a single peak + await conn.execute( "CREATE TABLE IF NOT EXISTS coin_record(" - "coin_name text PRIMARY KEY," + "coin_name blob PRIMARY KEY," " confirmed_index bigint," - " spent_index bigint," - " spent int," + " spent_index bigint," # if this is zero, it means the coin has not been spent " coinbase int," - " puzzle_hash text," - " coin_parent text," - " amount blob," + " puzzle_hash blob," + " coin_parent blob," + " amount blob," # we use a blob of 8 bytes to store uint64 " timestamp bigint)" ) - ) - # Useful for reorg lookups - await self.coin_record_db.execute( - "CREATE INDEX IF NOT EXISTS coin_confirmed_index on coin_record(confirmed_index)" - ) + else: + + # the coin_name is unique in this table because the CoinStore always + # only represent a single peak + await conn.execute( + ( + "CREATE TABLE IF NOT EXISTS coin_record(" + "coin_name text PRIMARY KEY," + " confirmed_index bigint," + " spent_index bigint," + " spent int," + " coinbase int," + " puzzle_hash text," + " coin_parent text," + " amount blob," + " timestamp bigint)" + ) + ) + + # Useful for reorg lookups + await conn.execute("CREATE INDEX IF NOT EXISTS coin_confirmed_index on coin_record(confirmed_index)") - await self.coin_record_db.execute("CREATE INDEX IF NOT EXISTS coin_spent_index on coin_record(spent_index)") + await conn.execute("CREATE INDEX IF NOT EXISTS coin_spent_index on coin_record(spent_index)") - await self.coin_record_db.execute("CREATE INDEX IF NOT EXISTS coin_puzzle_hash on coin_record(puzzle_hash)") + await conn.execute("CREATE INDEX IF NOT EXISTS coin_puzzle_hash on coin_record(puzzle_hash)") - await self.coin_record_db.execute("CREATE INDEX IF NOT EXISTS coin_parent_index on coin_record(coin_parent)") + await conn.execute("CREATE INDEX IF NOT EXISTS coin_parent_index on coin_record(coin_parent)") - await self.coin_record_db.commit() self.coin_record_cache = LRUCache(cache_size) return self async def num_unspent(self) -> int: - async with self.coin_record_db.execute("SELECT COUNT(*) FROM coin_record WHERE spent_index=0") as cursor: - row = await cursor.fetchone() - if row is not None: - return row[0] + async with self.db_wrapper.read_db() as conn: + async with conn.execute("SELECT COUNT(*) FROM coin_record WHERE spent_index=0") as cursor: + row = await cursor.fetchone() + if row is not None: + return row[0] return 0 def maybe_from_hex(self, field: Any) -> bytes: @@ -165,48 +162,51 @@ async def get_coin_record(self, coin_name: bytes32) -> Optional[CoinRecord]: if cached is not None: return cached - async with self.coin_record_db.execute( - "SELECT confirmed_index, spent_index, coinbase, puzzle_hash, " - "coin_parent, amount, timestamp FROM coin_record WHERE coin_name=?", - (self.maybe_to_hex(coin_name),), - ) as cursor: - row = await cursor.fetchone() - if row is not None: - coin = self.row_to_coin(row) - record = CoinRecord(coin, row[0], row[1], row[2], row[6]) - self.coin_record_cache.put(record.coin.name(), record) - return record + async with self.db_wrapper.read_db() as conn: + async with conn.execute( + "SELECT confirmed_index, spent_index, coinbase, puzzle_hash, " + "coin_parent, amount, timestamp FROM coin_record WHERE coin_name=?", + (self.maybe_to_hex(coin_name),), + ) as cursor: + row = await cursor.fetchone() + if row is not None: + coin = self.row_to_coin(row) + record = CoinRecord(coin, row[0], row[1], row[2], row[6]) + self.coin_record_cache.put(record.coin.name(), record) + return record return None async def get_coins_added_at_height(self, height: uint32) -> List[CoinRecord]: - async with self.coin_record_db.execute( - "SELECT confirmed_index, spent_index, coinbase, puzzle_hash, " - "coin_parent, amount, timestamp FROM coin_record WHERE confirmed_index=?", - (height,), - ) as cursor: - rows = await cursor.fetchall() - coins = [] - for row in rows: - coin = self.row_to_coin(row) - coins.append(CoinRecord(coin, row[0], row[1], row[2], row[6])) - return coins + async with self.db_wrapper.read_db() as conn: + async with conn.execute( + "SELECT confirmed_index, spent_index, coinbase, puzzle_hash, " + "coin_parent, amount, timestamp FROM coin_record WHERE confirmed_index=?", + (height,), + ) as cursor: + rows = await cursor.fetchall() + coins = [] + for row in rows: + coin = self.row_to_coin(row) + coins.append(CoinRecord(coin, row[0], row[1], row[2], row[6])) + return coins async def get_coins_removed_at_height(self, height: uint32) -> List[CoinRecord]: # Special case to avoid querying all unspent coins (spent_index=0) if height == 0: return [] - async with self.coin_record_db.execute( - "SELECT confirmed_index, spent_index, coinbase, puzzle_hash, " - "coin_parent, amount, timestamp FROM coin_record WHERE spent_index=?", - (height,), - ) as cursor: - coins = [] - for row in await cursor.fetchall(): - if row[1] != 0: - coin = self.row_to_coin(row) - coin_record = CoinRecord(coin, row[0], row[1], row[2], row[6]) - coins.append(coin_record) - return coins + async with self.db_wrapper.read_db() as conn: + async with conn.execute( + "SELECT confirmed_index, spent_index, coinbase, puzzle_hash, " + "coin_parent, amount, timestamp FROM coin_record WHERE spent_index=?", + (height,), + ) as cursor: + coins = [] + for row in await cursor.fetchall(): + if row[1] != 0: + coin = self.row_to_coin(row) + coin_record = CoinRecord(coin, row[0], row[1], row[2], row[6]) + coins.append(coin_record) + return coins # Checks DB and DiffStores for CoinRecords with puzzle_hash and returns them async def get_coin_records_by_puzzle_hash( @@ -219,18 +219,19 @@ async def get_coin_records_by_puzzle_hash( coins = set() - async with self.coin_record_db.execute( - f"SELECT confirmed_index, spent_index, coinbase, puzzle_hash, " - f"coin_parent, amount, timestamp FROM coin_record INDEXED BY coin_puzzle_hash WHERE puzzle_hash=? " - f"AND confirmed_index>=? AND confirmed_index=? AND confirmed_index=? AND confirmed_index=? AND confirmed_index=? AND confirmed_index=? AND confirmed_index=? OR spent_index>=?)" - f"{'' if include_spent_coins else 'AND spent_index=0'}", - puzzle_hashes_db + (min_height, min_height), - ) as cursor: - - async for row in cursor: - coins.add(self.row_to_coin_state(row)) + async with self.db_wrapper.read_db() as conn: + for puzzles in chunks(puzzle_hashes, MAX_SQLITE_PARAMETERS): + puzzle_hashes_db: Tuple[Any, ...] + if self.db_wrapper.db_version == 2: + puzzle_hashes_db = tuple(puzzles) + else: + puzzle_hashes_db = tuple([ph.hex() for ph in puzzles]) + async with conn.execute( + f"SELECT confirmed_index, spent_index, coinbase, puzzle_hash, " + f"coin_parent, amount, timestamp FROM coin_record INDEXED BY coin_puzzle_hash " + f'WHERE puzzle_hash in ({"?," * (len(puzzles) - 1)}?) ' + f"AND (confirmed_index>=? OR spent_index>=?)" + f"{'' if include_spent_coins else 'AND spent_index=0'}", + puzzle_hashes_db + (min_height, min_height), + ) as cursor: + + async for row in cursor: + coins.add(self.row_to_coin_state(row)) return list(coins) @@ -345,23 +351,25 @@ async def get_coin_records_by_parent_ids( return [] coins = set() - for ids in chunks(parent_ids, MAX_SQLITE_PARAMETERS): - parent_ids_db: Tuple[Any, ...] - if self.db_wrapper.db_version == 2: - parent_ids_db = tuple(ids) - else: - parent_ids_db = tuple([pid.hex() for pid in ids]) - async with self.coin_record_db.execute( - f"SELECT confirmed_index, spent_index, coinbase, puzzle_hash, " - f'coin_parent, amount, timestamp FROM coin_record WHERE coin_parent in ({"?," * (len(ids) - 1)}?) ' - f"AND confirmed_index>=? AND confirmed_index=? AND confirmed_index=? OR spent_index>=?)" - f"{'' if include_spent_coins else 'AND spent_index=0'}", - coin_ids_db + (min_height, min_height), - ) as cursor: - async for row in cursor: - coins.add(self.row_to_coin_state(row)) + async with self.db_wrapper.read_db() as conn: + for ids in chunks(coin_ids, MAX_SQLITE_PARAMETERS): + coin_ids_db: Tuple[Any, ...] + if self.db_wrapper.db_version == 2: + coin_ids_db = tuple(ids) + else: + coin_ids_db = tuple([pid.hex() for pid in ids]) + async with conn.execute( + f"SELECT confirmed_index, spent_index, coinbase, puzzle_hash, " + f'coin_parent, amount, timestamp FROM coin_record WHERE coin_name in ({"?," * (len(ids) - 1)}?) ' + f"AND (confirmed_index>=? OR spent_index>=?)" + f"{'' if include_spent_coins else 'AND spent_index=0'}", + coin_ids_db + (min_height, min_height), + ) as cursor: + async for row in cursor: + coins.add(self.row_to_coin_state(row)) return list(coins) async def rollback_to_block(self, block_index: int) -> List[CoinRecord]: @@ -415,38 +424,37 @@ async def rollback_to_block(self, block_index: int) -> List[CoinRecord]: self.coin_record_cache.remove(coin_name) coin_changes: Dict[bytes32, CoinRecord] = {} - async with self.coin_record_db.execute( - "SELECT confirmed_index, spent_index, coinbase, puzzle_hash, " - "coin_parent, amount, timestamp FROM coin_record WHERE confirmed_index>?", - (block_index,), - ) as cursor: - for row in await cursor.fetchall(): - coin = self.row_to_coin(row) - record = CoinRecord(coin, uint32(0), row[1], row[2], uint64(0)) - coin_changes[record.name] = record - - # Delete from storage - await self.coin_record_db.execute("DELETE FROM coin_record WHERE confirmed_index>?", (block_index,)) - - async with self.coin_record_db.execute( - "SELECT confirmed_index, spent_index, coinbase, puzzle_hash, " - "coin_parent, amount, timestamp FROM coin_record WHERE confirmed_index>?", - (block_index,), - ) as cursor: - for row in await cursor.fetchall(): - coin = self.row_to_coin(row) - record = CoinRecord(coin, row[0], uint32(0), row[2], row[6]) - if record.name not in coin_changes: + async with self.db_wrapper.write_db() as conn: + async with conn.execute( + "SELECT confirmed_index, spent_index, coinbase, puzzle_hash, " + "coin_parent, amount, timestamp FROM coin_record WHERE confirmed_index>?", + (block_index,), + ) as cursor: + for row in await cursor.fetchall(): + coin = self.row_to_coin(row) + record = CoinRecord(coin, uint32(0), row[1], row[2], uint64(0)) coin_changes[record.name] = record - if self.db_wrapper.db_version == 2: - await self.coin_record_db.execute( - "UPDATE coin_record SET spent_index=0 WHERE spent_index>?", (block_index,) - ) - else: - await self.coin_record_db.execute( - "UPDATE coin_record SET spent_index = 0, spent = 0 WHERE spent_index>?", (block_index,) - ) + # Delete from storage + await conn.execute("DELETE FROM coin_record WHERE confirmed_index>?", (block_index,)) + + async with conn.execute( + "SELECT confirmed_index, spent_index, coinbase, puzzle_hash, " + "coin_parent, amount, timestamp FROM coin_record WHERE confirmed_index>?", + (block_index,), + ) as cursor: + for row in await cursor.fetchall(): + coin = self.row_to_coin(row) + record = CoinRecord(coin, row[0], uint32(0), row[2], row[6]) + if record.name not in coin_changes: + coin_changes[record.name] = record + + if self.db_wrapper.db_version == 2: + await conn.execute("UPDATE coin_record SET spent_index=0 WHERE spent_index>?", (block_index,)) + else: + await conn.execute( + "UPDATE coin_record SET spent_index = 0, spent = 0 WHERE spent_index>?", (block_index,) + ) return list(coin_changes.values()) # Store CoinRecord in DB and ram cache @@ -468,10 +476,12 @@ async def _add_coin_records(self, records: List[CoinRecord]) -> None: record.timestamp, ) ) - await self.coin_record_db.executemany( - "INSERT INTO coin_record VALUES(?, ?, ?, ?, ?, ?, ?, ?)", - values2, - ) + if len(values2) > 0: + async with self.db_wrapper.write_db() as conn: + await conn.executemany( + "INSERT INTO coin_record VALUES(?, ?, ?, ?, ?, ?, ?, ?)", + values2, + ) else: values = [] for record in records: @@ -489,10 +499,12 @@ async def _add_coin_records(self, records: List[CoinRecord]) -> None: record.timestamp, ) ) - await self.coin_record_db.executemany( - "INSERT INTO coin_record VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)", - values, - ) + if len(values) > 0: + async with self.db_wrapper.write_db() as conn: + await conn.executemany( + "INSERT INTO coin_record VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)", + values, + ) # Update coin_record to be spent in DB async def _set_spent(self, coin_names: List[bytes32], index: uint32): @@ -508,11 +520,11 @@ async def _set_spent(self, coin_names: List[bytes32], index: uint32): ) updates.append((index, self.maybe_to_hex(coin_name))) - if self.db_wrapper.db_version == 2: - await self.coin_record_db.executemany( - "UPDATE OR FAIL coin_record SET spent_index=? WHERE coin_name=?", updates - ) - else: - await self.coin_record_db.executemany( - "UPDATE OR FAIL coin_record SET spent=1,spent_index=? WHERE coin_name=?", updates - ) + if updates != []: + async with self.db_wrapper.write_db() as conn: + if self.db_wrapper.db_version == 2: + await conn.executemany("UPDATE OR FAIL coin_record SET spent_index=? WHERE coin_name=?", updates) + else: + await conn.executemany( + "UPDATE OR FAIL coin_record SET spent=1,spent_index=? WHERE coin_name=?", updates + ) diff --git a/chia/full_node/full_node.py b/chia/full_node/full_node.py index 080aacbb58c4..74037b760069 100644 --- a/chia/full_node/full_node.py +++ b/chia/full_node/full_node.py @@ -67,7 +67,7 @@ from chia.util.check_fork_next_block import check_fork_next_block from chia.util.condition_tools import pkm_pairs from chia.util.config import PEER_DB_PATH_KEY_DEPRECATED, process_config_start_method -from chia.util.db_wrapper import DBWrapper +from chia.util.db_wrapper import DBWrapper2 from chia.util.errors import ConsensusError, Err, ValidationError from chia.util.ints import uint8, uint32, uint64, uint128 from chia.util.path import mkdir, path_from_root @@ -85,7 +85,6 @@ class FullNode: sync_store: Any coin_store: CoinStore mempool_manager: MempoolManager - connection: aiosqlite.Connection _sync_task: Optional[asyncio.Task] _init_weight_proof: Optional[asyncio.Task] = None blockchain: Blockchain @@ -164,23 +163,8 @@ async def _start(self): # These many respond_transaction tasks can be active at any point in time self.respond_transaction_semaphore = asyncio.Semaphore(200) # create the store (db) and full node instance - self.connection = await aiosqlite.connect(self.db_path) - await self.connection.execute("pragma journal_mode=wal") - db_sync = db_synchronous_on(self.config.get("db_sync", "auto"), self.db_path) - self.log.info(f"opening blockchain DB: synchronous={db_sync}") - await self.connection.execute("pragma synchronous={}".format(db_sync)) - - async with self.connection.execute( - "SELECT name FROM sqlite_master WHERE type='table' AND name='full_blocks'" - ) as conn: - if len(await conn.fetchall()) == 0: - try: - # this is a new DB file. Make it v2 - await set_db_version_async(self.connection, 2) - except sqlite3.OperationalError: - # it could be a database created with "chia init", which is - # empty except it has the database_version table - pass + db_connection = await aiosqlite.connect(self.db_path) + db_version: int = await lookup_db_version(db_connection) if self.config.get("log_sqlite_cmds", False): sql_log_path = path_from_root(self.root_path, "log/sql.log") @@ -192,11 +176,37 @@ def sql_trace_callback(req: str): log.write(timestamp + " " + req + "\n") log.close() - await self.connection.set_trace_callback(sql_trace_callback) + await db_connection.set_trace_callback(sql_trace_callback) + + self.db_wrapper = DBWrapper2(db_connection, db_version=db_version) + + # add reader threads for the DB + for i in range(self.config.get("db_readers", 4)): + c = await aiosqlite.connect(self.db_path) + if self.config.get("log_sqlite_cmds", False): + await c.set_trace_callback(sql_trace_callback) + await self.db_wrapper.add_connection(c) - db_version: int = await lookup_db_version(self.connection) + await (await db_connection.execute("pragma journal_mode=wal")).close() + db_sync = db_synchronous_on(self.config.get("db_sync", "auto"), self.db_path) + self.log.info(f"opening blockchain DB: synchronous={db_sync}") + await (await db_connection.execute("pragma synchronous={}".format(db_sync))).close() + + if db_version != 2: + async with self.db_wrapper.read_db() as conn: + async with conn.execute( + "SELECT name FROM sqlite_master WHERE type='table' AND name='full_blocks'" + ) as cur: + if len(await cur.fetchall()) == 0: + try: + # this is a new DB file. Make it v2 + async with self.db_wrapper.write_db() as w_conn: + await set_db_version_async(w_conn, 2) + except sqlite3.OperationalError: + # it could be a database created with "chia init", which is + # empty except it has the database_version table + pass - self.db_wrapper = DBWrapper(self.connection, db_version=db_version) self.block_store = await BlockStore.create(self.db_wrapper) self.sync_store = await SyncStore.create() self.hint_store = await HintStore.create(self.db_wrapper) @@ -770,7 +780,7 @@ def _close(self): async def _await_closed(self): for task_id, task in list(self.full_node_store.tx_fetch_tasks.items()): cancel_task_safe(task, self.log) - await self.connection.close() + await self.db_wrapper.close() if self._init_weight_proof is not None: await asyncio.wait([self._init_weight_proof]) if hasattr(self, "_blockchain_lock_queue"): @@ -2225,14 +2235,11 @@ async def _replace_proof( new_block = dataclasses.replace(block, challenge_chain_ip_proof=vdf_proof) if new_block is None: return False - async with self.db_wrapper.lock: + async with self.db_wrapper.write_db(): try: - await self.block_store.db_wrapper.begin_transaction() await self.block_store.replace_proof(header_hash, new_block) - await self.block_store.db_wrapper.commit_transaction() return True except BaseException as e: - await self.block_store.db_wrapper.rollback_transaction() self.log.error( f"_replace_proof error while adding block {block.header_hash} height {block.height}," f" rolling back: {e} {traceback.format_exc()}" diff --git a/chia/full_node/hint_store.py b/chia/full_node/hint_store.py index 422eb544e7cf..38ead43e2b68 100644 --- a/chia/full_node/hint_store.py +++ b/chia/full_node/hint_store.py @@ -1,56 +1,57 @@ from typing import List, Tuple from chia.types.blockchain_format.sized_bytes import bytes32 -from chia.util.db_wrapper import DBWrapper +from chia.util.db_wrapper import DBWrapper2 import logging log = logging.getLogger(__name__) class HintStore: - db_wrapper: DBWrapper + db_wrapper: DBWrapper2 @classmethod - async def create(cls, db_wrapper: DBWrapper): + async def create(cls, db_wrapper: DBWrapper2): self = cls() self.db_wrapper = db_wrapper - if self.db_wrapper.db_version == 2: - await self.db_wrapper.db.execute( - "CREATE TABLE IF NOT EXISTS hints(coin_id blob, hint blob, UNIQUE (coin_id, hint))" - ) - else: - await self.db_wrapper.db.execute( - "CREATE TABLE IF NOT EXISTS hints(id INTEGER PRIMARY KEY AUTOINCREMENT, coin_id blob, hint blob)" - ) - await self.db_wrapper.db.execute("CREATE INDEX IF NOT EXISTS hint_index on hints(hint)") - await self.db_wrapper.db.commit() + async with self.db_wrapper.write_db() as conn: + if self.db_wrapper.db_version == 2: + await conn.execute("CREATE TABLE IF NOT EXISTS hints(coin_id blob, hint blob, UNIQUE (coin_id, hint))") + else: + await conn.execute( + "CREATE TABLE IF NOT EXISTS hints(id INTEGER PRIMARY KEY AUTOINCREMENT, coin_id blob, hint blob)" + ) + await conn.execute("CREATE INDEX IF NOT EXISTS hint_index on hints(hint)") return self async def get_coin_ids(self, hint: bytes) -> List[bytes32]: - cursor = await self.db_wrapper.db.execute("SELECT coin_id from hints WHERE hint=?", (hint,)) - rows = await cursor.fetchall() - await cursor.close() + async with self.db_wrapper.read_db() as conn: + cursor = await conn.execute("SELECT coin_id from hints WHERE hint=?", (hint,)) + rows = await cursor.fetchall() + await cursor.close() coin_ids = [] for row in rows: coin_ids.append(row[0]) return coin_ids async def add_hints(self, coin_hint_list: List[Tuple[bytes32, bytes]]) -> None: - if self.db_wrapper.db_version == 2: - cursor = await self.db_wrapper.db.executemany( - "INSERT OR IGNORE INTO hints VALUES(?, ?)", - coin_hint_list, - ) - else: - cursor = await self.db_wrapper.db.executemany( - "INSERT INTO hints VALUES(?, ?, ?)", - [(None,) + record for record in coin_hint_list], - ) - await cursor.close() + async with self.db_wrapper.write_db() as conn: + if self.db_wrapper.db_version == 2: + cursor = await conn.executemany( + "INSERT OR IGNORE INTO hints VALUES(?, ?)", + coin_hint_list, + ) + else: + cursor = await conn.executemany( + "INSERT INTO hints VALUES(?, ?, ?)", + [(None,) + record for record in coin_hint_list], + ) + await cursor.close() async def count_hints(self) -> int: - async with self.db_wrapper.db.execute("select count(*) from hints") as cursor: - row = await cursor.fetchone() + async with self.db_wrapper.read_db() as conn: + async with conn.execute("select count(*) from hints") as cursor: + row = await cursor.fetchone() assert row is not None diff --git a/chia/util/db_wrapper.py b/chia/util/db_wrapper.py index 53af97f96022..6c1068a94b6b 100644 --- a/chia/util/db_wrapper.py +++ b/chia/util/db_wrapper.py @@ -1,4 +1,8 @@ +from __future__ import annotations + import asyncio +import contextlib +from typing import AsyncIterator, Dict, Optional import aiosqlite @@ -10,12 +14,10 @@ class DBWrapper: db: aiosqlite.Connection lock: asyncio.Lock - db_version: int - def __init__(self, connection: aiosqlite.Connection, db_version: int = 1): + def __init__(self, connection: aiosqlite.Connection): self.db = connection self.lock = asyncio.Lock() - self.db_version = db_version async def begin_transaction(self): cursor = await self.db.execute("BEGIN TRANSACTION") @@ -29,3 +31,109 @@ async def rollback_transaction(self): async def commit_transaction(self) -> None: await self.db.commit() + + +class DBWrapper2: + db_version: int + _lock: asyncio.Lock + _read_connections: asyncio.Queue[aiosqlite.Connection] + _write_connection: aiosqlite.Connection + _num_read_connections: int + _in_use: Dict[asyncio.Task, aiosqlite.Connection] + _current_writer: Optional[asyncio.Task] + _savepoint_name: int + + async def add_connection(self, c: aiosqlite.Connection) -> None: + # this guarantees that reader connections can only be used for reading + assert c != self._write_connection + await c.execute("pragma query_only") + self._read_connections.put_nowait(c) + self._num_read_connections += 1 + + def __init__(self, connection: aiosqlite.Connection, db_version: int = 1) -> None: + self._read_connections = asyncio.Queue() + self._write_connection = connection + self._lock = asyncio.Lock() + self.db_version = db_version + self._num_read_connections = 0 + self._in_use = {} + self._current_writer = None + self._savepoint_name = 0 + + async def close(self) -> None: + while self._num_read_connections > 0: + await (await self._read_connections.get()).close() + self._num_read_connections -= 1 + await self._write_connection.close() + + def _next_savepoint(self) -> str: + name = f"s{self._savepoint_name}" + self._savepoint_name += 1 + return name + + @contextlib.asynccontextmanager + async def write_db(self) -> AsyncIterator[aiosqlite.Connection]: + task = asyncio.current_task() + assert task is not None + if self._current_writer == task: + # we allow nesting writers within the same task + + name = self._next_savepoint() + await self._write_connection.execute(f"SAVEPOINT {name}") + try: + yield self._write_connection + except: # noqa E722 + await self._write_connection.execute(f"ROLLBACK TO {name}") + raise + finally: + # rollback to a savepoint doesn't cancel the transaction, it + # just rolls back the state. We need to cancel it regardless + await self._write_connection.execute(f"RELEASE {name}") + return + + async with self._lock: + + name = self._next_savepoint() + await self._write_connection.execute(f"SAVEPOINT {name}") + try: + self._current_writer = task + yield self._write_connection + except: # noqa E722 + await self._write_connection.execute(f"ROLLBACK TO {name}") + raise + finally: + self._current_writer = None + await self._write_connection.execute(f"RELEASE {name}") + + @contextlib.asynccontextmanager + async def read_db(self) -> AsyncIterator[aiosqlite.Connection]: + # there should have been read connections added + assert self._num_read_connections > 0 + + # we can have multiple concurrent readers, just pick a connection from + # the pool of readers. If they're all busy, we'll wait for one to free + # up. + task = asyncio.current_task() + assert task is not None + + # if this task currently holds the write lock, use the same connection, + # so it can read back updates it has made to its transaction, even + # though it hasn't been comitted yet + if self._current_writer == task: + # we allow nesting reading while also having a writer connection + # open, within the same task + yield self._write_connection + return + + if task in self._in_use: + yield self._in_use[task] + else: + c = await self._read_connections.get() + try: + # record our connection in this dict to allow nested calls in + # the same task to use the same connection + self._in_use[task] = c + yield c + finally: + del self._in_use[task] + self._read_connections.put_nowait(c) diff --git a/chia/util/initial-config.yaml b/chia/util/initial-config.yaml index 600222592edb..ce386c7a2e57 100644 --- a/chia/util/initial-config.yaml +++ b/chia/util/initial-config.yaml @@ -332,6 +332,11 @@ full_node: # the particular system we're running on. Defaults to "full". db_sync: "auto" + # the number of threads used to read from the blockchain database + # concurrently. There's always only 1 writer, but the number of readers is + # configurable + db_readers: 4 + # Run multiple nodes with different databases by changing the database_path database_path: db/blockchain_v2_CHALLENGE.sqlite # peer_db_path is deprecated and has been replaced by peers_file_path diff --git a/tests/blockchain/blockchain_test_utils.py b/tests/blockchain/blockchain_test_utils.py index 38872f7b61f8..93152782cf3c 100644 --- a/tests/blockchain/blockchain_test_utils.py +++ b/tests/blockchain/blockchain_test_utils.py @@ -15,24 +15,25 @@ async def check_block_store_invariant(bc: Blockchain): in_chain = set() max_height = -1 - async with db_wrapper.db.execute("SELECT height, in_main_chain FROM full_blocks") as cursor: - rows = await cursor.fetchall() - for row in rows: - height = row[0] - - # if this block is in-chain, ensure we haven't found another block - # at this height that's also in chain. That would be an invariant - # violation - if row[1]: - # make sure we don't have any duplicate heights. Each block - # height can only have a single block with in_main_chain set - assert height not in in_chain - in_chain.add(height) - if height > max_height: - max_height = height - - # make sure every height is represented in the set - assert len(in_chain) == max_height + 1 + async with db_wrapper.write_db() as conn: + async with conn.execute("SELECT height, in_main_chain FROM full_blocks") as cursor: + rows = await cursor.fetchall() + for row in rows: + height = row[0] + + # if this block is in-chain, ensure we haven't found another block + # at this height that's also in chain. That would be an invariant + # violation + if row[1]: + # make sure we don't have any duplicate heights. Each block + # height can only have a single block with in_main_chain set + assert height not in in_chain + in_chain.add(height) + if height > max_height: + max_height = height + + # make sure every height is represented in the set + assert len(in_chain) == max_height + 1 async def _validate_and_add_block( diff --git a/tests/blockchain/test_blockchain.py b/tests/blockchain/test_blockchain.py index b72d3a4607bf..99927fc0ff56 100644 --- a/tests/blockchain/test_blockchain.py +++ b/tests/blockchain/test_blockchain.py @@ -539,7 +539,7 @@ async def do_test_invalid_icc_sub_slot_vdf(self, keychain, db_version): constants=test_constants.replace(SUB_SLOT_ITERS_STARTING=(2 ** 12), DIFFICULTY_STARTING=(2 ** 14)), keychain=keychain, ) - bc1, connection, db_path = await create_blockchain(bt_high_iters.constants, db_version) + bc1, db_wrapper, db_path = await create_blockchain(bt_high_iters.constants, db_version) blocks = bt_high_iters.get_consecutive_blocks(10) for block in blocks: if len(block.finished_sub_slots) > 0 and block.finished_sub_slots[-1].infused_challenge_chain is not None: @@ -611,7 +611,7 @@ async def do_test_invalid_icc_sub_slot_vdf(self, keychain, db_version): await _validate_and_add_block(bc1, block) - await connection.close() + await db_wrapper.close() bc1.shut_down() db_path.unlink() @@ -2324,7 +2324,7 @@ async def test_max_coin_amount(self, db_version, bt): # new_test_constants = test_constants.replace( # **{"GENESIS_PRE_FARM_POOL_PUZZLE_HASH": bt.pool_ph, "GENESIS_PRE_FARM_FARMER_PUZZLE_HASH": bt.pool_ph} # ) - # b, connection, db_path = await create_blockchain(new_test_constants, db_version) + # b, db_wrapper, db_path = await create_blockchain(new_test_constants, db_version) # bt_2 = await create_block_tools_async(constants=new_test_constants, keychain=keychain) # bt_2.constants = bt_2.constants.replace( # **{"GENESIS_PRE_FARM_POOL_PUZZLE_HASH": bt.pool_ph, "GENESIS_PRE_FARM_FARMER_PUZZLE_HASH": bt.pool_ph} @@ -2358,7 +2358,7 @@ async def test_max_coin_amount(self, db_version, bt): # assert False # except Exception as e: # pass - # await connection.close() + # await db_wrapper.close() # b.shut_down() # db_path.unlink() diff --git a/tests/conftest.py b/tests/conftest.py index 6a0ee5f4d071..c59b5f458946 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -54,10 +54,10 @@ async def empty_blockchain(request): from tests.util.blockchain import create_blockchain from tests.setup_nodes import test_constants - bc1, connection, db_path = await create_blockchain(test_constants, request.param) + bc1, db_wrapper, db_path = await create_blockchain(test_constants, request.param) yield bc1 - await connection.close() + await db_wrapper.close() bc1.shut_down() db_path.unlink() diff --git a/tests/core/full_node/ram_db.py b/tests/core/full_node/ram_db.py index 69aca2745c03..f5245a0dc3b7 100644 --- a/tests/core/full_node/ram_db.py +++ b/tests/core/full_node/ram_db.py @@ -1,6 +1,7 @@ from typing import Tuple from pathlib import Path +import random import aiosqlite from chia.consensus.blockchain import Blockchain @@ -8,14 +9,16 @@ from chia.full_node.block_store import BlockStore from chia.full_node.coin_store import CoinStore from chia.full_node.hint_store import HintStore -from chia.util.db_wrapper import DBWrapper +from chia.util.db_wrapper import DBWrapper2 -async def create_ram_blockchain(consensus_constants: ConsensusConstants) -> Tuple[aiosqlite.Connection, Blockchain]: - connection = await aiosqlite.connect(":memory:") - db_wrapper = DBWrapper(connection) +async def create_ram_blockchain(consensus_constants: ConsensusConstants) -> Tuple[DBWrapper2, Blockchain]: + uri = f"file:db_{random.randint(0, 99999999)}?mode=memory&cache=shared" + connection = await aiosqlite.connect(uri, uri=True) + db_wrapper = DBWrapper2(connection) + await db_wrapper.add_connection(await aiosqlite.connect(uri, uri=True)) block_store = await BlockStore.create(db_wrapper) coin_store = await CoinStore.create(db_wrapper) hint_store = await HintStore.create(db_wrapper) blockchain = await Blockchain.create(coin_store, block_store, consensus_constants, hint_store, Path("."), 2) - return connection, blockchain + return db_wrapper, blockchain diff --git a/tests/core/full_node/stores/test_block_store.py b/tests/core/full_node/stores/test_block_store.py index 620f79889605..a8330e3aae7b 100644 --- a/tests/core/full_node/stores/test_block_store.py +++ b/tests/core/full_node/stores/test_block_store.py @@ -118,26 +118,29 @@ async def test_rollback(self, bt, tmp_dir): # make sure all block heights are unique assert len(set(ret)) == count - for block in blocks: - async with db_wrapper.db.execute( - "SELECT in_main_chain FROM full_blocks WHERE header_hash=?", (block.header_hash,) - ) as cursor: - rows = await cursor.fetchall() - assert len(rows) == 1 - assert rows[0][0] + async with db_wrapper.read_db() as conn: + for block in blocks: + async with conn.execute( + "SELECT in_main_chain FROM full_blocks WHERE header_hash=?", (block.header_hash,) + ) as cursor: + rows = await cursor.fetchall() + assert len(rows) == 1 + assert rows[0][0] await block_store.rollback(5) count = 0 - for block in blocks: - async with db_wrapper.db.execute( - "SELECT in_main_chain FROM full_blocks WHERE header_hash=? ORDER BY height", (block.header_hash,) - ) as cursor: - rows = await cursor.fetchall() - print(count, rows) - assert len(rows) == 1 - assert rows[0][0] == (count <= 5) - count += 1 + async with db_wrapper.read_db() as conn: + for block in blocks: + async with conn.execute( + "SELECT in_main_chain FROM full_blocks WHERE header_hash=? ORDER BY height", + (block.header_hash,), + ) as cursor: + rows = await cursor.fetchall() + print(count, rows) + assert len(rows) == 1 + assert rows[0][0] == (count <= 5) + count += 1 @pytest.mark.asyncio async def test_count_compactified_blocks(self, bt, tmp_dir, db_version): diff --git a/tests/core/full_node/stores/test_full_node_store.py b/tests/core/full_node/stores/test_full_node_store.py index f5ff33c36b86..c96307e28326 100644 --- a/tests/core/full_node/stores/test_full_node_store.py +++ b/tests/core/full_node/stores/test_full_node_store.py @@ -49,18 +49,18 @@ def cleanup_keyring(keyring: TempKeyring): @pytest_asyncio.fixture(scope="function", params=[1, 2]) async def empty_blockchain(request): - bc1, connection, db_path = await create_blockchain(test_constants, request.param) + bc1, db_wrapper, db_path = await create_blockchain(test_constants, request.param) yield bc1 - await connection.close() + await db_wrapper.close() bc1.shut_down() db_path.unlink() @pytest_asyncio.fixture(scope="function", params=[1, 2]) async def empty_blockchain_with_original_constants(request): - bc1, connection, db_path = await create_blockchain(test_constants_original, request.param) + bc1, db_wrapper, db_path = await create_blockchain(test_constants_original, request.param) yield bc1 - await connection.close() + await db_wrapper.close() bc1.shut_down() db_path.unlink() diff --git a/tests/core/full_node/stores/test_hint_store.py b/tests/core/full_node/stores/test_hint_store.py index edd097664fd9..54cdf2d46c82 100644 --- a/tests/core/full_node/stores/test_hint_store.py +++ b/tests/core/full_node/stores/test_hint_store.py @@ -31,7 +31,6 @@ async def test_basic_store(self, db_version): hints = [(coin_id_0, hint_0), (coin_id_1, hint_0), (coin_id_2, hint_1)] await hint_store.add_hints(hints) - await db_wrapper.commit_transaction() coins_for_hint_0 = await hint_store.get_coin_ids(hint_0) assert coin_id_0 in coins_for_hint_0 @@ -54,7 +53,6 @@ async def test_duplicate_coins(self, db_version): hints = [(coin_id_0, hint_0), (coin_id_0, hint_1)] await hint_store.add_hints(hints) - await db_wrapper.commit_transaction() coins_for_hint_0 = await hint_store.get_coin_ids(hint_0) assert coin_id_0 in coins_for_hint_0 @@ -73,7 +71,6 @@ async def test_duplicate_hints(self, db_version): hints = [(coin_id_0, hint_0), (coin_id_1, hint_0)] await hint_store.add_hints(hints) - await db_wrapper.commit_transaction() coins_for_hint_0 = await hint_store.get_coin_ids(hint_0) assert coin_id_0 in coins_for_hint_0 assert coin_id_1 in coins_for_hint_0 @@ -91,12 +88,12 @@ async def test_duplicates(self, db_version): for i in range(0, 2): hints = [(coin_id_0, hint_0), (coin_id_0, hint_0)] await hint_store.add_hints(hints) - await db_wrapper.commit_transaction() coins_for_hint_0 = await hint_store.get_coin_ids(hint_0) assert coin_id_0 in coins_for_hint_0 - cursor = await db_wrapper.db.execute("SELECT COUNT(*) FROM hints") - rows = await cursor.fetchall() + async with db_wrapper.read_db() as conn: + cursor = await conn.execute("SELECT COUNT(*) FROM hints") + rows = await cursor.fetchall() if db_wrapper.db_version == 2: # even though we inserted the pair multiple times, there's only one @@ -160,7 +157,6 @@ async def test_counts(self, db_version): coin_id_1 = 32 * b"\5" hints = [(coin_id_0, hint_0), (coin_id_1, hint_1)] await hint_store.add_hints(hints) - await db_wrapper.commit_transaction() count = await hint_store.count_hints() assert count == 2 diff --git a/tests/core/full_node/test_block_height_map.py b/tests/core/full_node/test_block_height_map.py index 38f2c21ede82..ac5bf5c5499a 100644 --- a/tests/core/full_node/test_block_height_map.py +++ b/tests/core/full_node/test_block_height_map.py @@ -2,7 +2,7 @@ import struct from chia.full_node.block_height_map import BlockHeightMap, SesCache from chia.types.blockchain_format.sub_epoch_summary import SubEpochSummary -from chia.util.db_wrapper import DBWrapper +from chia.util.db_wrapper import DBWrapper2 from tests.util.db_connection import DBConnection from chia.types.blockchain_format.sized_bytes import bytes32 @@ -24,76 +24,78 @@ def gen_ses(height: int) -> SubEpochSummary: async def new_block( - db: DBWrapper, + db: DBWrapper2, block_hash: bytes32, parent: bytes32, height: int, is_peak: bool, ses: Optional[SubEpochSummary], ): - if db.db_version == 2: - cursor = await db.db.execute( - "INSERT INTO full_blocks VALUES(?, ?, ?, ?)", - ( - block_hash, - parent, - height, - # sub epoch summary - None if ses is None else bytes(ses), - ), - ) - await cursor.close() - if is_peak: - cursor = await db.db.execute("INSERT OR REPLACE INTO current_peak VALUES(?, ?)", (0, block_hash)) + async with db.write_db() as conn: + if db.db_version == 2: + cursor = await conn.execute( + "INSERT INTO full_blocks VALUES(?, ?, ?, ?)", + ( + block_hash, + parent, + height, + # sub epoch summary + None if ses is None else bytes(ses), + ), + ) await cursor.close() - else: - cursor = await db.db.execute( - "INSERT INTO block_records VALUES(?, ?, ?, ?, ?)", - ( - block_hash.hex(), - parent.hex(), - height, - # sub epoch summary - None if ses is None else bytes(ses), - is_peak, - ), - ) - await cursor.close() - - -async def setup_db(db: DBWrapper): - - if db.db_version == 2: - await db.db.execute( - "CREATE TABLE IF NOT EXISTS full_blocks(" - "header_hash blob PRIMARY KEY," - "prev_hash blob," - "height bigint," - "sub_epoch_summary blob)" - ) - await db.db.execute("CREATE TABLE IF NOT EXISTS current_peak(key int PRIMARY KEY, hash blob)") - - await db.db.execute("CREATE INDEX IF NOT EXISTS height on full_blocks(height)") - await db.db.execute("CREATE INDEX IF NOT EXISTS hh on full_blocks(header_hash)") - else: - await db.db.execute( - "CREATE TABLE IF NOT EXISTS block_records(" - "header_hash text PRIMARY KEY," - "prev_hash text," - "height bigint," - "sub_epoch_summary blob," - "is_peak tinyint)" - ) - await db.db.execute("CREATE INDEX IF NOT EXISTS height on block_records(height)") - await db.db.execute("CREATE INDEX IF NOT EXISTS hh on block_records(header_hash)") - await db.db.execute("CREATE INDEX IF NOT EXISTS peak on block_records(is_peak)") + if is_peak: + cursor = await conn.execute("INSERT OR REPLACE INTO current_peak VALUES(?, ?)", (0, block_hash)) + await cursor.close() + else: + cursor = await conn.execute( + "INSERT INTO block_records VALUES(?, ?, ?, ?, ?)", + ( + block_hash.hex(), + parent.hex(), + height, + # sub epoch summary + None if ses is None else bytes(ses), + is_peak, + ), + ) + await cursor.close() + + +async def setup_db(db: DBWrapper2): + + async with db.write_db() as conn: + if db.db_version == 2: + await conn.execute( + "CREATE TABLE IF NOT EXISTS full_blocks(" + "header_hash blob PRIMARY KEY," + "prev_hash blob," + "height bigint," + "sub_epoch_summary blob)" + ) + await conn.execute("CREATE TABLE IF NOT EXISTS current_peak(key int PRIMARY KEY, hash blob)") + + await conn.execute("CREATE INDEX IF NOT EXISTS height on full_blocks(height)") + await conn.execute("CREATE INDEX IF NOT EXISTS hh on full_blocks(header_hash)") + else: + await conn.execute( + "CREATE TABLE IF NOT EXISTS block_records(" + "header_hash text PRIMARY KEY," + "prev_hash text," + "height bigint," + "sub_epoch_summary blob," + "is_peak tinyint)" + ) + await conn.execute("CREATE INDEX IF NOT EXISTS height on block_records(height)") + await conn.execute("CREATE INDEX IF NOT EXISTS hh on block_records(header_hash)") + await conn.execute("CREATE INDEX IF NOT EXISTS peak on block_records(is_peak)") # if chain_id != 0, the last block in the chain won't be considered the peak, # and the chain_id will be mixed in to the hashes, to form a separate chain at # the same heights as the main chain async def setup_chain( - db: DBWrapper, length: int, *, chain_id: int = 0, ses_every: Optional[int] = None, start_height=0 + db: DBWrapper2, length: int, *, chain_id: int = 0, ses_every: Optional[int] = None, start_height=0 ): height = start_height peak_hash = gen_block_hash(height + chain_id * 65536) @@ -171,10 +173,11 @@ async def test_save_restore(self, tmp_dir, db_version): # in the DB since we keep loading until we find a match of both hash # and sub epoch summary. In this test we have a sub epoch summary # every 20 blocks, so we generate the 30 last blocks only - if db_version == 2: - await db_wrapper.db.execute("DROP TABLE full_blocks") - else: - await db_wrapper.db.execute("DROP TABLE block_records") + async with db_wrapper.write_db() as conn: + if db_version == 2: + await conn.execute("DROP TABLE full_blocks") + else: + await conn.execute("DROP TABLE block_records") await setup_db(db_wrapper) await setup_chain(db_wrapper, 10000, ses_every=20, start_height=9970) height_map = await BlockHeightMap.create(tmp_dir, db_wrapper) diff --git a/tests/core/full_node/test_conditions.py b/tests/core/full_node/test_conditions.py index 266a5210915c..2cbae3bb1f00 100644 --- a/tests/core/full_node/test_conditions.py +++ b/tests/core/full_node/test_conditions.py @@ -73,7 +73,7 @@ async def check_spend_bundle_validity( `SpendBundle`, and then invokes `receive_block` to ensure that it's accepted (if `expected_err=None`) or fails with the correct error code. """ - connection, blockchain = await create_ram_blockchain(constants) + db_wrapper, blockchain = await create_ram_blockchain(constants) try: for block in blocks: await _validate_and_add_block(blockchain, block) @@ -98,8 +98,8 @@ async def check_spend_bundle_validity( return coins_added, coins_removed finally: - # if we don't close the connection, the test process doesn't exit cleanly - await connection.close() + # if we don't close the db_wrapper, the test process doesn't exit cleanly + await db_wrapper.close() # we must call `shut_down` or the executor in `Blockchain` doesn't stop blockchain.shut_down() diff --git a/tests/core/test_db_conversion.py b/tests/core/test_db_conversion.py index 4a9e94366605..5d85efa26ef4 100644 --- a/tests/core/test_db_conversion.py +++ b/tests/core/test_db_conversion.py @@ -10,7 +10,7 @@ from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.ints import uint32, uint64 from chia.cmds.db_upgrade_func import convert_v1_to_v2 -from chia.util.db_wrapper import DBWrapper +from chia.util.db_wrapper import DBWrapper2 from chia.full_node.block_store import BlockStore from chia.full_node.coin_store import CoinStore from chia.full_node.hint_store import HintStore @@ -54,13 +54,13 @@ async def test_blocks(self, default_1000_blocks, with_hints: bool): with TempFile() as in_file, TempFile() as out_file: - async with aiosqlite.connect(in_file) as conn: + conn = await aiosqlite.connect(in_file) + await conn.execute("pragma journal_mode=OFF") + await conn.execute("pragma synchronous=OFF") - await conn.execute("pragma journal_mode=OFF") - await conn.execute("pragma synchronous=OFF") - await conn.execute("pragma locking_mode=exclusive") - - db_wrapper1 = DBWrapper(conn, 1) + db_wrapper1 = DBWrapper2(conn, 1) + await db_wrapper1.add_connection(await aiosqlite.connect(in_file)) + try: block_store1 = await BlockStore.create(db_wrapper1) coin_store1 = await CoinStore.create(db_wrapper1, uint32(0)) if with_hints: @@ -73,20 +73,27 @@ async def test_blocks(self, default_1000_blocks, with_hints: bool): bc = await Blockchain.create( coin_store1, block_store1, test_constants, hint_store1, Path("."), reserved_cores=0 ) - await db_wrapper1.commit_transaction() for block in blocks: # await _validate_and_add_block(bc, block) results = PreValidationResult(None, uint64(1), None, False) result, err, _, _ = await bc.receive_block(block, results) assert err is None + finally: + await db_wrapper1.close() # now, convert v1 in_file to v2 out_file convert_v1_to_v2(in_file, out_file) - async with aiosqlite.connect(in_file) as conn, aiosqlite.connect(out_file) as conn2: + conn = await aiosqlite.connect(in_file) + db_wrapper1 = DBWrapper2(conn, 1) + await db_wrapper1.add_connection(await aiosqlite.connect(in_file)) + + conn2 = await aiosqlite.connect(out_file) + db_wrapper2 = DBWrapper2(conn2, 2) + await db_wrapper2.add_connection(await aiosqlite.connect(out_file)) - db_wrapper1 = DBWrapper(conn, 1) + try: block_store1 = await BlockStore.create(db_wrapper1) coin_store1 = await CoinStore.create(db_wrapper1, uint32(0)) if with_hints: @@ -94,7 +101,6 @@ async def test_blocks(self, default_1000_blocks, with_hints: bool): else: hint_store1 = None - db_wrapper2 = DBWrapper(conn2, 2) block_store2 = await BlockStore.create(db_wrapper2) coin_store2 = await CoinStore.create(db_wrapper2, uint32(0)) hint_store2 = await HintStore.create(db_wrapper2) @@ -133,3 +139,6 @@ async def test_blocks(self, default_1000_blocks, with_hints: bool): for c in coins: n = c.coin.name() assert await coin_store1.get_coin_record(n) == await coin_store2.get_coin_record(n) + finally: + await db_wrapper1.close() + await db_wrapper2.close() diff --git a/tests/core/test_db_validation.py b/tests/core/test_db_validation.py index 39e28d42fe13..aa5fb1bf2314 100644 --- a/tests/core/test_db_validation.py +++ b/tests/core/test_db_validation.py @@ -16,7 +16,7 @@ from chia.full_node.hint_store import HintStore from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.full_block import FullBlock -from chia.util.db_wrapper import DBWrapper +from chia.util.db_wrapper import DBWrapper2 from chia.util.ints import uint32, uint64 from tests.setup_nodes import test_constants from tests.util.temp_file import TempFile @@ -129,29 +129,27 @@ def test_db_validate_in_main_chain(invalid_in_chain: bool) -> None: async def make_db(db_file: Path, blocks: List[FullBlock]) -> None: - async with aiosqlite.connect(db_file) as conn: + db_wrapper = DBWrapper2(await aiosqlite.connect(db_file), 2) + try: + await db_wrapper.add_connection(await aiosqlite.connect(db_file)) - await conn.execute("pragma journal_mode=OFF") - await conn.execute("pragma synchronous=OFF") - await conn.execute("pragma locking_mode=exclusive") + async with db_wrapper.write_db() as conn: + # this is done by chia init normally + await conn.execute("CREATE TABLE database_version(version int)") + await conn.execute("INSERT INTO database_version VALUES (2)") - # this is done by chia init normally - await conn.execute("CREATE TABLE database_version(version int)") - await conn.execute("INSERT INTO database_version VALUES (2)") - await conn.commit() - - db_wrapper = DBWrapper(conn, 2) block_store = await BlockStore.create(db_wrapper) coin_store = await CoinStore.create(db_wrapper, uint32(0)) hint_store = await HintStore.create(db_wrapper) bc = await Blockchain.create(coin_store, block_store, test_constants, hint_store, Path("."), reserved_cores=0) - await db_wrapper.commit_transaction() for block in blocks: results = PreValidationResult(None, uint64(1), None, False) result, err, _, _ = await bc.receive_block(block, results) assert err is None + finally: + await db_wrapper.close() @pytest.mark.asyncio diff --git a/tests/core/util/test_db_wrapper.py b/tests/core/util/test_db_wrapper.py new file mode 100644 index 000000000000..ab7b02a07ecd --- /dev/null +++ b/tests/core/util/test_db_wrapper.py @@ -0,0 +1,225 @@ +import asyncio +import contextlib +from typing import List + +import aiosqlite +import pytest + +from chia.util.db_wrapper import DBWrapper2 +from tests.util.db_connection import DBConnection + + +async def increment_counter(db_wrapper: DBWrapper2) -> None: + async with db_wrapper.write_db() as connection: + async with connection.execute("SELECT value FROM counter") as cursor: + row = await cursor.fetchone() + + assert row is not None + [old_value] = row + + await asyncio.sleep(0) + + new_value = old_value + 1 + await connection.execute("UPDATE counter SET value = :value", {"value": new_value}) + + +async def sum_counter(db_wrapper: DBWrapper2, output: List[int]) -> None: + async with db_wrapper.read_db() as connection: + async with connection.execute("SELECT value FROM counter") as cursor: + row = await cursor.fetchone() + + assert row is not None + [value] = row + + for i in range(5): + await asyncio.sleep(0) + output.append(value) + + +async def setup_table(db: DBWrapper2) -> None: + async with db.write_db() as conn: + await conn.execute("CREATE TABLE counter(value INTEGER NOT NULL)") + await conn.execute("INSERT INTO counter(value) VALUES(0)") + + +async def get_value(cursor: aiosqlite.Cursor) -> int: + row = await cursor.fetchone() + assert row + return int(row[0]) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + argnames="acquire_outside", + argvalues=[pytest.param(False, id="not acquired outside"), pytest.param(True, id="acquired outside")], +) +async def test_concurrent_writers(acquire_outside: bool) -> None: + + async with DBConnection(2) as db_wrapper: + await setup_table(db_wrapper) + + concurrent_task_count = 200 + + async with contextlib.AsyncExitStack() as exit_stack: + if acquire_outside: + await exit_stack.enter_async_context(db_wrapper.write_db()) + + tasks = [] + for index in range(concurrent_task_count): + task = asyncio.create_task(increment_counter(db_wrapper)) + tasks.append(task) + + await asyncio.wait_for(asyncio.gather(*tasks), timeout=None) + + async with db_wrapper.read_db() as connection: + async with connection.execute("SELECT value FROM counter") as cursor: + row = await cursor.fetchone() + + assert row is not None + [value] = row + + assert value == concurrent_task_count + + +@pytest.mark.asyncio +async def test_writers_nests() -> None: + async with DBConnection(2) as db_wrapper: + await setup_table(db_wrapper) + async with db_wrapper.write_db() as conn1: + async with conn1.execute("SELECT value FROM counter") as cursor: + value = await get_value(cursor) + async with db_wrapper.write_db() as conn2: + assert conn1 == conn2 + value += 1 + await conn2.execute("UPDATE counter SET value = :value", {"value": value}) + async with db_wrapper.write_db() as conn3: + assert conn1 == conn3 + async with conn3.execute("SELECT value FROM counter") as cursor: + value = await get_value(cursor) + + assert value == 1 + + +@pytest.mark.asyncio +async def test_partial_failure() -> None: + values = [] + async with DBConnection(2) as db_wrapper: + await setup_table(db_wrapper) + async with db_wrapper.write_db() as conn1: + await conn1.execute("UPDATE counter SET value = 42") + async with conn1.execute("SELECT value FROM counter") as cursor: + values.append(await get_value(cursor)) + try: + async with db_wrapper.write_db() as conn2: + await conn2.execute("UPDATE counter SET value = 1337") + async with conn1.execute("SELECT value FROM counter") as cursor: + values.append(await get_value(cursor)) + # this simulates a failure, which will cause a rollback of the + # write we just made, back to 42 + raise RuntimeError("failure within a sub-transaction") + except RuntimeError: + # we expect to get here + values.append(1) + async with conn1.execute("SELECT value FROM counter") as cursor: + values.append(await get_value(cursor)) + + # the write of 1337 failed, and was restored to 42 + assert values == [42, 1337, 1, 42] + + +@pytest.mark.asyncio +async def test_readers_nests() -> None: + async with DBConnection(2) as db_wrapper: + await setup_table(db_wrapper) + + async with db_wrapper.read_db() as conn1: + async with db_wrapper.read_db() as conn2: + assert conn1 == conn2 + async with db_wrapper.read_db() as conn3: + assert conn1 == conn3 + async with conn3.execute("SELECT value FROM counter") as cursor: + value = await get_value(cursor) + + assert value == 0 + + +@pytest.mark.asyncio +async def test_readers_nests_writer() -> None: + async with DBConnection(2) as db_wrapper: + await setup_table(db_wrapper) + + async with db_wrapper.write_db() as conn1: + async with db_wrapper.read_db() as conn2: + assert conn1 == conn2 + async with db_wrapper.write_db() as conn3: + assert conn1 == conn3 + async with conn3.execute("SELECT value FROM counter") as cursor: + value = await get_value(cursor) + + assert value == 0 + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + argnames="acquire_outside", + argvalues=[pytest.param(False, id="not acquired outside"), pytest.param(True, id="acquired outside")], +) +async def test_concurrent_readers(acquire_outside: bool) -> None: + + async with DBConnection(2) as db_wrapper: + await setup_table(db_wrapper) + + async with db_wrapper.write_db() as connection: + await connection.execute("UPDATE counter SET value = 1") + + concurrent_task_count = 200 + + async with contextlib.AsyncExitStack() as exit_stack: + if acquire_outside: + await exit_stack.enter_async_context(db_wrapper.read_db()) + + tasks = [] + values: List[int] = [] + for index in range(concurrent_task_count): + task = asyncio.create_task(sum_counter(db_wrapper, values)) + tasks.append(task) + + await asyncio.wait_for(asyncio.gather(*tasks), timeout=None) + + assert values == [1] * concurrent_task_count + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + argnames="acquire_outside", + argvalues=[pytest.param(False, id="not acquired outside"), pytest.param(True, id="acquired outside")], +) +async def test_mixed_readers_writers(acquire_outside: bool) -> None: + + async with DBConnection(2) as db_wrapper: + await setup_table(db_wrapper) + + async with db_wrapper.write_db() as connection: + await connection.execute("UPDATE counter SET value = 1") + + concurrent_task_count = 200 + + async with contextlib.AsyncExitStack() as exit_stack: + if acquire_outside: + await exit_stack.enter_async_context(db_wrapper.read_db()) + + tasks = [] + values: List[int] = [] + for index in range(concurrent_task_count): + if index == 100: + task = asyncio.create_task(increment_counter(db_wrapper)) + task = asyncio.create_task(sum_counter(db_wrapper, values)) + tasks.append(task) + + await asyncio.wait_for(asyncio.gather(*tasks), timeout=None) + + # at some unspecified place between the first and the last reads, the value + # was updated from 1 to 2 + assert values[0] == 1 + assert values[-1] == 2 + assert len(values) == concurrent_task_count diff --git a/tests/util/blockchain.py b/tests/util/blockchain.py index 4b38e0238a9a..ebb65a1367cb 100644 --- a/tests/util/blockchain.py +++ b/tests/util/blockchain.py @@ -12,7 +12,7 @@ from chia.full_node.coin_store import CoinStore from chia.full_node.hint_store import HintStore from chia.types.full_block import FullBlock -from chia.util.db_wrapper import DBWrapper +from chia.util.db_wrapper import DBWrapper2 from chia.util.default_root import DEFAULT_ROOT_PATH from chia.util.path import mkdir from tests.block_tools import BlockTools @@ -24,13 +24,15 @@ async def create_blockchain(constants: ConsensusConstants, db_version: int): if db_path.exists(): db_path.unlink() connection = await aiosqlite.connect(db_path) - wrapper = DBWrapper(connection, db_version) + wrapper = DBWrapper2(connection, db_version) + await wrapper.add_connection(await aiosqlite.connect(db_path)) + coin_store = await CoinStore.create(wrapper) store = await BlockStore.create(wrapper) hint_store = await HintStore.create(wrapper) bc1 = await Blockchain.create(coin_store, store, constants, hint_store, Path("."), 2) assert bc1.get_peak() is None - return bc1, connection, db_path + return bc1, wrapper, db_path def persistent_blocks( diff --git a/tests/util/db_connection.py b/tests/util/db_connection.py index 724316e03e3b..a10d91b6026c 100644 --- a/tests/util/db_connection.py +++ b/tests/util/db_connection.py @@ -1,20 +1,36 @@ from pathlib import Path -from chia.util.db_wrapper import DBWrapper +from chia.util.db_wrapper import DBWrapper2 import tempfile import aiosqlite +from datetime import datetime +import sys + + +async def log_conn(c: aiosqlite.Connection, name: str) -> aiosqlite.Connection: + def sql_trace_callback(req: str): + timestamp = datetime.now().strftime("%H:%M:%S.%f") + sys.stdout.write(timestamp + " " + name + " " + req + "\n") + + # uncomment this to debug sqlite interactions + # await c.set_trace_callback(sql_trace_callback) + return c class DBConnection: - def __init__(self, db_version): + def __init__(self, db_version: int) -> None: self.db_version = db_version - async def __aenter__(self) -> DBWrapper: + async def __aenter__(self) -> DBWrapper2: self.db_path = Path(tempfile.NamedTemporaryFile().name) if self.db_path.exists(): self.db_path.unlink() - self.connection = await aiosqlite.connect(self.db_path) - return DBWrapper(self.connection, self.db_version) + connection = await aiosqlite.connect(self.db_path) + self._db_wrapper = DBWrapper2(await log_conn(connection, "writer"), self.db_version) + + for i in range(4): + await self._db_wrapper.add_connection(await log_conn(await aiosqlite.connect(self.db_path), f"reader-{i}")) + return self._db_wrapper - async def __aexit__(self, exc_t, exc_v, exc_tb): - await self.connection.close() + async def __aexit__(self, exc_t, exc_v, exc_tb) -> None: + await self._db_wrapper.close() self.db_path.unlink() From 7a82f0d29978b92a70418679b801a9c4c5ed8fc4 Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Mon, 28 Mar 2022 20:58:59 +0200 Subject: [PATCH 267/378] use rust clvm in Program.run() (#10878) * remove Program.from_serialized_program * run the rust clvm implementation (instead of python) even for wallet programs --- chia/types/blockchain_format/program.py | 24 +----------------------- tests/util/network_protocol_data.py | 8 ++------ 2 files changed, 3 insertions(+), 29 deletions(-) diff --git a/chia/types/blockchain_format/program.py b/chia/types/blockchain_format/program.py index 52c06ef0e1b2..3b29e6cd7833 100644 --- a/chia/types/blockchain_format/program.py +++ b/chia/types/blockchain_format/program.py @@ -2,10 +2,8 @@ from typing import List, Set, Tuple, Optional, Any from clvm import SExp -from clvm import run_program as default_run_program from clvm.casts import int_from_bytes from clvm.EvalError import EvalError -from clvm.operators import OPERATOR_LOOKUP from clvm.serialize import sexp_from_stream, sexp_to_stream from clvm_rs import MEMPOOL_MODE, run_chia_program, serialized_length, run_generator2 from clvm_tools.curry import curry, uncurry @@ -18,22 +16,6 @@ from .tree_hash import sha256_treehash -def run_program( - program, - args, - max_cost, - operator_lookup=OPERATOR_LOOKUP, - pre_eval_f=None, -): - return default_run_program( - program, - args, - operator_lookup, - max_cost, - pre_eval_f=pre_eval_f, - ) - - INFINITE_COST = 0x7FFFFFFFFFFFFFFF @@ -63,10 +45,6 @@ def fromhex(cls, hexstr: str) -> "Program": def to_serialized_program(self) -> "SerializedProgram": return SerializedProgram.from_bytes(bytes(self)) - @classmethod - def from_serialized_program(cls, sp: "SerializedProgram") -> "Program": - return cls.from_bytes(bytes(sp)) - def __bytes__(self) -> bytes: f = io.BytesIO() self.stream(f) # noqa @@ -103,7 +81,7 @@ def get_tree_hash(self, *args: bytes32) -> bytes32: def run_with_cost(self, max_cost: int, args) -> Tuple[int, "Program"]: prog_args = Program.to(args) - cost, r = run_program(self, prog_args, max_cost) + cost, r = run_chia_program(self.as_bin(), prog_args.as_bin(), max_cost, 0) return cost, Program.to(r) def run(self, args) -> "Program": diff --git a/tests/util/network_protocol_data.py b/tests/util/network_protocol_data.py index ae053e942810..8d8c818ec323 100644 --- a/tests/util/network_protocol_data.py +++ b/tests/util/network_protocol_data.py @@ -488,12 +488,8 @@ uint32(3905474497), ) -program = Program.from_serialized_program( - SerializedProgram.from_bytes( - bytes.fromhex( - "ff01ffff33ffa0f8912302fb33b8188046662785704afc3dd945074e4b45499a7173946e044695ff8203e880ffff33ffa03eaa52e850322dbc281c6b922e9d8819c7b4120ee054c4aa79db50be516a2bcaff8207d08080" - ) - ), +program = Program.fromhex( + "ff01ffff33ffa0f8912302fb33b8188046662785704afc3dd945074e4b45499a7173946e044695ff8203e880ffff33ffa03eaa52e850322dbc281c6b922e9d8819c7b4120ee054c4aa79db50be516a2bcaff8207d08080" ) puzzle_solution_response = wallet_protocol.PuzzleSolutionResponse( From 6997adeab6728967bca55bdb0aa762f5f1a96fd1 Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Mon, 28 Mar 2022 12:46:13 -0700 Subject: [PATCH 268/378] Fix flaky trade test (#10921) --- tests/wallet/cat_wallet/test_trades.py | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/tests/wallet/cat_wallet/test_trades.py b/tests/wallet/cat_wallet/test_trades.py index f2366362a8a5..826cd786ede7 100644 --- a/tests/wallet/cat_wallet/test_trades.py +++ b/tests/wallet/cat_wallet/test_trades.py @@ -200,14 +200,12 @@ async def get_trade_and_status(trade_manager, trade) -> TradeStatus: await time_out_assert(15, get_trade_and_status, TradeStatus.CONFIRMED, trade_manager_maker, trade_make) await time_out_assert(15, get_trade_and_status, TradeStatus.CONFIRMED, trade_manager_taker, trade_take) - maker_txs = await wallet_node_maker.wallet_state_manager.tx_store.get_transactions_by_trade_id( - trade_make.trade_id - ) - taker_txs = await wallet_node_taker.wallet_state_manager.tx_store.get_transactions_by_trade_id( - trade_take.trade_id - ) - assert len(maker_txs) == 1 # The other side will show up as a regular incoming transaction - assert len(taker_txs) == 3 # One for each: the outgoing CAT, the incoming chia, and the outgoing chia fee + async def assert_trade_tx_number(wallet_node, trade_id, number): + txs = await wallet_node.wallet_state_manager.tx_store.get_transactions_by_trade_id(trade_id) + return len(txs) == number + + await time_out_assert(15, assert_trade_tx_number, True, wallet_node_maker, trade_make.trade_id, 1) + await time_out_assert(15, assert_trade_tx_number, True, wallet_node_taker, trade_take.trade_id, 3) # cat_for_chia success, trade_make, error = await trade_manager_maker.create_offer_for_ids(cat_for_chia) @@ -247,15 +245,8 @@ async def get_trade_and_status(trade_manager, trade) -> TradeStatus: await time_out_assert(15, cat_wallet_taker.get_unconfirmed_balance, TAKER_CAT_BALANCE) await time_out_assert(15, get_trade_and_status, TradeStatus.CONFIRMED, trade_manager_maker, trade_make) await time_out_assert(15, get_trade_and_status, TradeStatus.CONFIRMED, trade_manager_taker, trade_take) - - maker_txs = await wallet_node_maker.wallet_state_manager.tx_store.get_transactions_by_trade_id( - trade_make.trade_id - ) - taker_txs = await wallet_node_taker.wallet_state_manager.tx_store.get_transactions_by_trade_id( - trade_take.trade_id - ) - assert len(maker_txs) == 1 # The other side will show up as a regular incoming transaction - assert len(taker_txs) == 2 # One for each: the outgoing chia, the incoming CAT + await time_out_assert(15, assert_trade_tx_number, True, wallet_node_maker, trade_make.trade_id, 1) + await time_out_assert(15, assert_trade_tx_number, True, wallet_node_taker, trade_take.trade_id, 2) # cat_for_cat success, trade_make, error = await trade_manager_maker.create_offer_for_ids(cat_for_cat) From 1bcf409ee17f0a7495d23034c48d426e44f363cc Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Mon, 28 Mar 2022 21:47:46 +0200 Subject: [PATCH 269/378] single thread executor (#10919) * add inline executor and an option to run single-threaded * add option to run test_full_sync in single-thread mode, to include block validation in profiles. Also attempt to speed it up by disabling db_sync --- chia/consensus/blockchain.py | 31 ++++++++++++++--------- chia/consensus/multiprocess_validation.py | 4 +-- chia/full_node/full_node.py | 3 +++ chia/full_node/mempool_manager.py | 21 ++++++++++----- chia/util/initial-config.yaml | 5 ++++ chia/util/inline_executor.py | 24 ++++++++++++++++++ tests/tools/test_full_sync.py | 2 +- tools/test_full_sync.py | 26 +++++++++++++------ 8 files changed, 87 insertions(+), 29 deletions(-) create mode 100644 chia/util/inline_executor.py diff --git a/chia/consensus/blockchain.py b/chia/consensus/blockchain.py index e6ed0519b288..fbbf4cc02d94 100644 --- a/chia/consensus/blockchain.py +++ b/chia/consensus/blockchain.py @@ -3,6 +3,7 @@ import logging import multiprocessing import traceback +from concurrent.futures import Executor from concurrent.futures.process import ProcessPoolExecutor from enum import Enum from multiprocessing.context import BaseContext @@ -47,6 +48,7 @@ from chia.types.weight_proof import SubEpochChallengeSegment from chia.util.errors import ConsensusError, Err from chia.util.generator_tools import get_block_header, tx_removals_and_additions +from chia.util.inline_executor import InlineExecutor from chia.util.ints import uint16, uint32, uint64, uint128 from chia.util.setproctitle import getproctitle, setproctitle from chia.util.streamable import recurse_jsonify @@ -86,7 +88,7 @@ class Blockchain(BlockchainInterface): # Store block_store: BlockStore # Used to verify blocks in parallel - pool: ProcessPoolExecutor + pool: Executor # Set holding seen compact proofs, in order to avoid duplicates. _seen_compact_proofs: Set[Tuple[VDFInfo, uint32]] @@ -107,6 +109,8 @@ async def create( blockchain_dir: Path, reserved_cores: int, multiprocessing_context: Optional[BaseContext] = None, + *, + single_threaded: bool = False, ): """ Initializes a blockchain with the BlockRecords from disk, assuming they have all been @@ -116,17 +120,20 @@ async def create( self = Blockchain() self.lock = asyncio.Lock() # External lock handled by full node self.compact_proof_lock = asyncio.Lock() - cpu_count = multiprocessing.cpu_count() - if cpu_count > 61: - cpu_count = 61 # Windows Server 2016 has an issue https://bugs.python.org/issue26903 - num_workers = max(cpu_count - reserved_cores, 1) - self.pool = ProcessPoolExecutor( - max_workers=num_workers, - mp_context=multiprocessing_context, - initializer=setproctitle, - initargs=(f"{getproctitle()}_worker",), - ) - log.info(f"Started {num_workers} processes for block validation") + if single_threaded: + self.pool = InlineExecutor() + else: + cpu_count = multiprocessing.cpu_count() + if cpu_count > 61: + cpu_count = 61 # Windows Server 2016 has an issue https://bugs.python.org/issue26903 + num_workers = max(cpu_count - reserved_cores, 1) + self.pool = ProcessPoolExecutor( + max_workers=num_workers, + mp_context=multiprocessing_context, + initializer=setproctitle, + initargs=(f"{getproctitle()}_worker",), + ) + log.info(f"Started {num_workers} processes for block validation") self.constants = consensus_constants self.coin_store = coin_store diff --git a/chia/consensus/multiprocess_validation.py b/chia/consensus/multiprocess_validation.py index 2ac7740fd45a..9fe54858290a 100644 --- a/chia/consensus/multiprocess_validation.py +++ b/chia/consensus/multiprocess_validation.py @@ -1,7 +1,7 @@ import asyncio import logging import traceback -from concurrent.futures.process import ProcessPoolExecutor +from concurrent.futures import Executor from dataclasses import dataclass from typing import Awaitable, Callable, Dict, List, Optional, Sequence, Tuple @@ -171,7 +171,7 @@ async def pre_validate_blocks_multiprocessing( constants_json: Dict, block_records: BlockchainInterface, blocks: Sequence[FullBlock], - pool: ProcessPoolExecutor, + pool: Executor, check_filter: bool, npc_results: Dict[uint32, NPCResult], get_block_generator: Callable[[BlockInfo, Optional[Dict[bytes32, FullBlock]]], Awaitable[Optional[BlockGenerator]]], diff --git a/chia/full_node/full_node.py b/chia/full_node/full_node.py index 74037b760069..d03b4bda2df8 100644 --- a/chia/full_node/full_node.py +++ b/chia/full_node/full_node.py @@ -214,6 +214,7 @@ def sql_trace_callback(req: str): self.log.info("Initializing blockchain from disk") start_time = time.time() reserved_cores = self.config.get("reserved_cores", 0) + single_threaded = self.config.get("single_threaded", False) multiprocessing_start_method = process_config_start_method(config=self.config, log=self.log) self.multiprocessing_context = multiprocessing.get_context(method=multiprocessing_start_method) self.blockchain = await Blockchain.create( @@ -224,11 +225,13 @@ def sql_trace_callback(req: str): blockchain_dir=self.db_path.parent, reserved_cores=reserved_cores, multiprocessing_context=self.multiprocessing_context, + single_threaded=single_threaded, ) self.mempool_manager = MempoolManager( coin_store=self.coin_store, consensus_constants=self.constants, multiprocessing_context=self.multiprocessing_context, + single_threaded=single_threaded, ) # Blocks are validated under high priority, and transactions under low priority. This guarantees blocks will diff --git a/chia/full_node/mempool_manager.py b/chia/full_node/mempool_manager.py index fa52d06fe034..6c5f7aa49e50 100644 --- a/chia/full_node/mempool_manager.py +++ b/chia/full_node/mempool_manager.py @@ -2,9 +2,11 @@ import collections import dataclasses import logging +from concurrent.futures import Executor from multiprocessing.context import BaseContext import time from concurrent.futures.process import ProcessPoolExecutor +from chia.util.inline_executor import InlineExecutor from typing import Dict, List, Optional, Set, Tuple from blspy import GTElement from chiabip158 import PyBIP158 @@ -79,11 +81,15 @@ def validate_clvm_and_signature( class MempoolManager: + pool: Executor + def __init__( self, coin_store: CoinStore, consensus_constants: ConsensusConstants, multiprocessing_context: Optional[BaseContext] = None, + *, + single_threaded: bool = False, ): self.constants: ConsensusConstants = consensus_constants self.constants_json = recurse_jsonify(dataclasses.asdict(self.constants)) @@ -105,12 +111,15 @@ def __init__( # Transactions that were unable to enter mempool, used for retry. (they were invalid) self.potential_cache = PendingTxCache(self.constants.MAX_BLOCK_COST_CLVM * 1) self.seen_cache_size = 10000 - self.pool = ProcessPoolExecutor( - max_workers=2, - mp_context=multiprocessing_context, - initializer=setproctitle, - initargs=(f"{getproctitle()}_worker",), - ) + if single_threaded: + self.pool = InlineExecutor() + else: + self.pool = ProcessPoolExecutor( + max_workers=2, + mp_context=multiprocessing_context, + initializer=setproctitle, + initargs=(f"{getproctitle()}_worker",), + ) # The mempool will correspond to a certain peak self.peak: Optional[BlockRecord] = None diff --git a/chia/util/initial-config.yaml b/chia/util/initial-config.yaml index ce386c7a2e57..ef6aa9874c35 100644 --- a/chia/util/initial-config.yaml +++ b/chia/util/initial-config.yaml @@ -366,6 +366,11 @@ full_node: # this reserved core count. reserved_cores: 0 + # set this to true to not offload heavy lifting into separate child processes. + # this option is mostly useful when profiling, since only the main process is + # profiled. + single_threaded: False + # How often to initiate outbound connections to other full nodes. peer_connect_interval: 30 # How long to wait for a peer connection diff --git a/chia/util/inline_executor.py b/chia/util/inline_executor.py new file mode 100644 index 000000000000..499671bb81aa --- /dev/null +++ b/chia/util/inline_executor.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from concurrent.futures import Executor, Future +from typing import Callable, TypeVar + +_T = TypeVar("_T") + + +class InlineExecutor(Executor): + _closing: bool = False + + def submit(self, fn: Callable[..., _T], *args, **kwargs) -> Future[_T]: # type: ignore + if self._closing: + raise RuntimeError("executor shutting down") + + f: Future[_T] = Future() + try: + f.set_result(fn(*args, **kwargs)) + except BaseException as e: # lgtm[py/catch-base-exception] + f.set_exception(e) + return f + + def close(self) -> None: + self._closing = True diff --git a/tests/tools/test_full_sync.py b/tests/tools/test_full_sync.py index e3d0842ce79e..5b6dcbfa8e44 100644 --- a/tests/tools/test_full_sync.py +++ b/tests/tools/test_full_sync.py @@ -10,4 +10,4 @@ def test_full_sync_test(): file_path = os.path.realpath(__file__) db_file = Path(file_path).parent / "test-blockchain-db.sqlite" - asyncio.run(run_sync_test(db_file, db_version=2, profile=False)) + asyncio.run(run_sync_test(db_file, db_version=2, profile=False, single_thread=False)) diff --git a/tools/test_full_sync.py b/tools/test_full_sync.py index 3b8716aa215f..afd3197d960b 100755 --- a/tools/test_full_sync.py +++ b/tools/test_full_sync.py @@ -46,7 +46,7 @@ def enable_profiler(profile: bool, counter: int) -> Iterator[None]: pr.dump_stats(f"slow-batch-{counter:05d}.profile") -async def run_sync_test(file: Path, db_version, profile: bool) -> None: +async def run_sync_test(file: Path, db_version, profile: bool, single_thread: bool) -> None: logger = logging.getLogger() logger.setLevel(logging.WARNING) @@ -69,6 +69,9 @@ async def run_sync_test(file: Path, db_version, profile: bool) -> None: overrides = config["network_overrides"]["constants"][config["selected_network"]] constants = DEFAULT_CONSTANTS.replace_str_to_bytes(**overrides) + if single_thread: + config["full_node"]["single_threaded"] = True + config["full_node"]["db_sync"] = "off" full_node = FullNode( config["full_node"], root_path=root_path, @@ -88,13 +91,13 @@ async def run_sync_test(file: Path, db_version, profile: bool) -> None: start_time = time.monotonic() async for r in rows: - block = FullBlock.from_bytes(zstd.decompress(r[2])) + with enable_profiler(profile, counter): + block = FullBlock.from_bytes(zstd.decompress(r[2])) - block_batch.append(block) - if len(block_batch) < 32: - continue + block_batch.append(block) + if len(block_batch) < 32: + continue - with enable_profiler(profile, counter): success, advanced_peak, fork_height, coin_changes = await full_node.receive_block_batch( block_batch, None, None # type: ignore[arg-type] ) @@ -121,8 +124,15 @@ def main() -> None: @click.argument("file", type=click.Path(), required=True) @click.option("--db-version", type=int, required=False, default=2, help="the version of the specified db file") @click.option("--profile", is_flag=True, required=False, default=False, help="dump CPU profiles for slow batches") -def run(file: Path, db_version: int, profile: bool) -> None: - asyncio.run(run_sync_test(Path(file), db_version, profile)) +@click.option( + "--single-thread", + is_flag=True, + required=False, + default=False, + help="run node in a single process, to include validation in profiles", +) +def run(file: Path, db_version: int, profile: bool, single_thread: bool) -> None: + asyncio.run(run_sync_test(Path(file), db_version, profile, single_thread)) @main.command("analyze", short_help="generate call stacks for all profiles dumped to current directory") From efcf0cf0ad8e7398a7a133aa956ebfc283f04bec Mon Sep 17 00:00:00 2001 From: Adam Kelly <338792+aqk@users.noreply.github.com> Date: Mon, 28 Mar 2022 12:48:14 -0700 Subject: [PATCH 270/378] await the db commit in the async version of set_db_version (#10906) --- chia/util/db_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia/util/db_version.py b/chia/util/db_version.py index 379f47267a4f..c4a8e3a88c40 100644 --- a/chia/util/db_version.py +++ b/chia/util/db_version.py @@ -19,7 +19,7 @@ async def lookup_db_version(db: aiosqlite.Connection) -> int: async def set_db_version_async(db: aiosqlite.Connection, version: int) -> None: await db.execute("CREATE TABLE database_version(version int)") await db.execute("INSERT INTO database_version VALUES (?)", (version,)) - db.commit() + await db.commit() def set_db_version(db: sqlite3.Connection, version: int) -> None: From f0e5b5814ba77090e56879eee8580a2544c33979 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Mon, 28 Mar 2022 15:48:36 -0400 Subject: [PATCH 271/378] bump pre-commit mypy to v0.942 (#10902) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c181b0b36e47..147715627fde 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -59,7 +59,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.941 + rev: v0.942 hooks: - id: mypy additional_dependencies: [filelock, pytest, pytest-asyncio, types-aiofiles, types-click, types-setuptools, types-PyYAML] From 84cdd609e938b1ff3216fdb6b63c9c1bf39f3feb Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Mon, 28 Mar 2022 21:49:14 +0200 Subject: [PATCH 272/378] bump clvm_tools dependency to make every chia-blockchain installation get the new brun that reports cost accurately (#10880) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a4856bf5b125..b279c9a7efa4 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ "chiabip158==1.1", # bip158-style wallet filters "chiapos==1.0.9", # proof of space "clvm==0.9.7", - "clvm_tools==0.4.3", # Currying, Program.to, other conveniences + "clvm_tools==0.4.4", # Currying, Program.to, other conveniences "clvm_rs==0.1.19", "clvm-tools-rs==0.1.7", # Rust implementation of clvm_tools "aiohttp==3.7.4", # HTTP server for full node rpc From fbd38450284e367048e09dd30e5b3436ddc93bc3 Mon Sep 17 00:00:00 2001 From: dustinface <35775977+xdustinface@users.noreply.github.com> Date: Mon, 28 Mar 2022 21:50:50 +0200 Subject: [PATCH 273/378] wallet: Drop unused `WalletStateManager.load_wallets` (#10756) --- chia/wallet/wallet_state_manager.py | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index aed351a13be3..1a51de491ce4 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -223,31 +223,6 @@ def get_public_key(self, index: uint32) -> G1Element: def get_public_key_unhardened(self, index: uint32) -> G1Element: return master_sk_to_wallet_sk_unhardened(self.private_key, index).get_g1() - async def load_wallets(self): - for wallet_info in await self.get_all_wallet_info_entries(): - if wallet_info.id in self.wallets: - continue - if wallet_info.type == WalletType.STANDARD_WALLET: - if wallet_info.id == 1: - continue - wallet = await Wallet.create(self.config, wallet_info) - self.wallets[wallet_info.id] = wallet - # TODO add RL AND DiD WALLETS HERE - elif wallet_info.type == WalletType.CAT: - wallet = await CATWallet.create( - self, - self.main_wallet, - wallet_info, - ) - self.wallets[wallet_info.id] = wallet - elif wallet_info.type == WalletType.DISTRIBUTED_ID: - wallet = await DIDWallet.create( - self, - self.main_wallet, - wallet_info, - ) - self.wallets[wallet_info.id] = wallet - async def get_keys(self, puzzle_hash: bytes32) -> Optional[Tuple[G1Element, PrivateKey]]: record = await self.puzzle_store.record_for_puzzle_hash(puzzle_hash) if record is None: From e0a1fc4eafe2cbb8d59150917a6d986eab778c85 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Mon, 28 Mar 2022 15:52:51 -0400 Subject: [PATCH 274/378] Switch to integrated lock_and_load_config() context manager (#10698) * minor lock scope reduction * use the lock in tests * Use the passed root_path in configure CLI command * switch to lock_and_load_config() * oops * cleanup * make _load_config_maybe_locked() private * black * Remove future improvement opportunity TODO comment --- chia/cmds/configure.py | 7 ++--- chia/cmds/db_upgrade_func.py | 5 ++-- chia/cmds/init_funcs.py | 29 +++++++++---------- chia/farmer/farmer.py | 8 ++---- chia/plotting/util.py | 8 ++---- chia/pools/pool_config.py | 8 ++---- chia/util/config.py | 37 +++++++++++++++++++++---- tests/block_tools.py | 6 ++-- tests/core/test_farmer_harvester_rpc.py | 5 ++-- tests/core/util/test_config.py | 16 +++++------ tests/pools/test_pool_config.py | 6 ++-- tests/wallet/rpc/test_wallet_rpc.py | 5 ++-- 12 files changed, 77 insertions(+), 63 deletions(-) diff --git a/chia/cmds/configure.py b/chia/cmds/configure.py index 0123fb439683..3b3e336f6bf0 100644 --- a/chia/cmds/configure.py +++ b/chia/cmds/configure.py @@ -1,9 +1,9 @@ from pathlib import Path -from typing import Dict, Optional +from typing import Optional import click -from chia.util.config import get_config_lock, load_config, save_config, str2bool +from chia.util.config import lock_and_load_config, save_config, str2bool def configure( @@ -23,8 +23,7 @@ def configure( seeder_domain_name: str, seeder_nameserver: str, ): - with get_config_lock(root_path, "config.yaml"): - config: Dict = load_config(root_path, "config.yaml", acquire_lock=False) + with lock_and_load_config(root_path, "config.yaml") as config: change_made = False if set_node_introducer: try: diff --git a/chia/cmds/db_upgrade_func.py b/chia/cmds/db_upgrade_func.py index e4e6442c1d1f..e12ca79f3441 100644 --- a/chia/cmds/db_upgrade_func.py +++ b/chia/cmds/db_upgrade_func.py @@ -7,7 +7,7 @@ import textwrap import os -from chia.util.config import load_config, save_config, get_config_lock +from chia.util.config import load_config, lock_and_load_config, save_config from chia.util.path import mkdir, path_from_root from chia.util.ints import uint32 from chia.types.blockchain_format.sized_bytes import bytes32 @@ -70,8 +70,7 @@ def db_upgrade_func( if update_config: print("updating config.yaml") - with get_config_lock(root_path, "config.yaml"): - config = load_config(root_path, "config.yaml", acquire_lock=False) + with lock_and_load_config(root_path, "config.yaml") as config: new_db_path = db_pattern.replace("_v1_", "_v2_") config["full_node"]["database_path"] = new_db_path print(f"database_path: {new_db_path}") diff --git a/chia/cmds/init_funcs.py b/chia/cmds/init_funcs.py index f64eb4c2200f..1bf2dea4f2e6 100644 --- a/chia/cmds/init_funcs.py +++ b/chia/cmds/init_funcs.py @@ -20,9 +20,9 @@ create_default_chia_config, initial_config_file, load_config, + lock_and_load_config, save_config, unflatten_properties, - get_config_lock, ) from chia.util.db_version import set_db_version from chia.util.keychain import Keychain @@ -77,8 +77,7 @@ def check_keys(new_root: Path, keychain: Optional[Keychain] = None) -> None: print("No keys are present in the keychain. Generate them with 'chia keys generate'") return None - with get_config_lock(new_root, "config.yaml"): - config: Dict = load_config(new_root, "config.yaml", acquire_lock=False) + with lock_and_load_config(new_root, "config.yaml") as config: pool_child_pubkeys = [master_sk_to_pool_sk(sk).get_g1() for sk, _ in all_sks] all_targets = [] stop_searching_for_farmer = "xch_target_address" not in config["farmer"] @@ -199,8 +198,7 @@ def migrate_from( # update config yaml with new keys - with get_config_lock(new_root, "config.yaml"): - config: Dict = load_config(new_root, "config.yaml", acquire_lock=False) + with lock_and_load_config(new_root, "config.yaml") as config: config_str: str = initial_config_file("config.yaml") default_config: Dict = yaml.safe_load(config_str) flattened_keys = unflatten_properties({k: "" for k in do_not_migrate_settings}) @@ -495,10 +493,9 @@ def chia_init( config: Dict - with get_config_lock(root_path, "config.yaml"): - db_path_replaced: str - if v1_db: - config = load_config(root_path, "config.yaml", acquire_lock=False) + db_path_replaced: str + if v1_db: + with lock_and_load_config(root_path, "config.yaml") as config: db_pattern = config["full_node"]["database_path"] new_db_path = db_pattern.replace("_v2_", "_v1_") config["full_node"]["database_path"] = new_db_path @@ -510,14 +507,14 @@ def chia_init( save_config(root_path, "config.yaml", config) - else: - config = load_config(root_path, "config.yaml", acquire_lock=False)["full_node"] - db_path_replaced = config["database_path"].replace("CHALLENGE", config["selected_network"]) - db_path = path_from_root(root_path, db_path_replaced) - mkdir(db_path.parent) + else: + config = load_config(root_path, "config.yaml")["full_node"] + db_path_replaced = config["database_path"].replace("CHALLENGE", config["selected_network"]) + db_path = path_from_root(root_path, db_path_replaced) + mkdir(db_path.parent) - with sqlite3.connect(db_path) as connection: - set_db_version(connection, 2) + with sqlite3.connect(db_path) as connection: + set_db_version(connection, 2) print("") print("To see your keys, run 'chia keys show --show-mnemonic-seed'") diff --git a/chia/farmer/farmer.py b/chia/farmer/farmer.py index 8eede4e6e6a5..3bb68a6fa524 100644 --- a/chia/farmer/farmer.py +++ b/chia/farmer/farmer.py @@ -40,7 +40,7 @@ from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.bech32m import decode_puzzle_hash from chia.util.byte_types import hexstr_to_bytes -from chia.util.config import load_config, save_config, config_path_for_filename, get_config_lock +from chia.util.config import load_config, lock_and_load_config, save_config, config_path_for_filename from chia.util.hash import std_hash from chia.util.ints import uint8, uint16, uint32, uint64 from chia.util.keychain import Keychain @@ -581,8 +581,7 @@ async def get_reward_targets(self, search_for_private_key: bool) -> Dict: } def set_reward_targets(self, farmer_target_encoded: Optional[str], pool_target_encoded: Optional[str]): - with get_config_lock(self._root_path, "config.yaml"): - config = load_config(self._root_path, "config.yaml", acquire_lock=False) + with lock_and_load_config(self._root_path, "config.yaml") as config: if farmer_target_encoded is not None: self.farmer_target_encoded = farmer_target_encoded self.farmer_target = decode_puzzle_hash(farmer_target_encoded) @@ -596,8 +595,7 @@ def set_reward_targets(self, farmer_target_encoded: Optional[str], pool_target_e async def set_payout_instructions(self, launcher_id: bytes32, payout_instructions: str): for p2_singleton_puzzle_hash, pool_state_dict in self.pool_state.items(): if launcher_id == pool_state_dict["pool_config"].launcher_id: - with get_config_lock(self._root_path, "config.yaml"): - config = load_config(self._root_path, "config.yaml", acquire_lock=False) + with lock_and_load_config(self._root_path, "config.yaml") as config: new_list = [] pool_list = config["pool"].get("pool_list", []) if pool_list is not None: diff --git a/chia/plotting/util.py b/chia/plotting/util.py index c777c2c418a1..132f0d67ee3a 100644 --- a/chia/plotting/util.py +++ b/chia/plotting/util.py @@ -9,7 +9,7 @@ from chiapos import DiskProver from chia.types.blockchain_format.sized_bytes import bytes32 -from chia.util.config import load_config, save_config, get_config_lock +from chia.util.config import load_config, lock_and_load_config, save_config log = logging.getLogger(__name__) @@ -79,8 +79,7 @@ def get_plot_filenames(root_path: Path) -> Dict[Path, List[Path]]: def add_plot_directory(root_path: Path, str_path: str) -> Dict: log.debug(f"add_plot_directory {str_path}") - with get_config_lock(root_path, "config.yaml"): - config = load_config(root_path, "config.yaml", acquire_lock=False) + with lock_and_load_config(root_path, "config.yaml") as config: if str(Path(str_path).resolve()) not in get_plot_directories(root_path, config): config["harvester"]["plot_directories"].append(str(Path(str_path).resolve())) save_config(root_path, "config.yaml", config) @@ -89,8 +88,7 @@ def add_plot_directory(root_path: Path, str_path: str) -> Dict: def remove_plot_directory(root_path: Path, str_path: str) -> None: log.debug(f"remove_plot_directory {str_path}") - with get_config_lock(root_path, "config.yaml"): - config = load_config(root_path, "config.yaml", acquire_lock=False) + with lock_and_load_config(root_path, "config.yaml") as config: str_paths: List[str] = get_plot_directories(root_path, config) # If path str matches exactly, remove if str_path in str_paths: diff --git a/chia/pools/pool_config.py b/chia/pools/pool_config.py index d9d15733872b..ffa07e962e17 100644 --- a/chia/pools/pool_config.py +++ b/chia/pools/pool_config.py @@ -7,7 +7,7 @@ from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.byte_types import hexstr_to_bytes -from chia.util.config import get_config_lock, load_config, save_config +from chia.util.config import load_config, lock_and_load_config, save_config from chia.util.streamable import Streamable, streamable """ @@ -61,8 +61,7 @@ def load_pool_config(root_path: Path) -> List[PoolWalletConfig]: # TODO: remove this a few versions after 1.3, since authentication_public_key is deprecated. This is here to support # downgrading to versions older than 1.3. def add_auth_key(root_path: Path, config_entry: PoolWalletConfig, auth_key: G1Element): - with get_config_lock(root_path, "config.yaml"): - config = load_config(root_path, "config.yaml", acquire_lock=False) + with lock_and_load_config(root_path, "config.yaml") as config: pool_list = config["pool"].get("pool_list", []) updated = False if pool_list is not None: @@ -82,7 +81,6 @@ def add_auth_key(root_path: Path, config_entry: PoolWalletConfig, auth_key: G1El async def update_pool_config(root_path: Path, pool_config_list: List[PoolWalletConfig]): - with get_config_lock(root_path, "config.yaml"): - full_config = load_config(root_path, "config.yaml", acquire_lock=False) + with lock_and_load_config(root_path, "config.yaml") as full_config: full_config["pool"]["pool_list"] = [c.to_json_dict() for c in pool_config_list] save_config(root_path, "config.yaml", full_config) diff --git a/chia/util/config.py b/chia/util/config.py index 60c14a18a626..1405cacaadd0 100644 --- a/chia/util/config.py +++ b/chia/util/config.py @@ -8,11 +8,11 @@ import time import traceback from pathlib import Path -from typing import Any, Callable, Dict, Optional, Union +from typing import Any, Callable, Dict, Iterator, Optional, Union import pkg_resources import yaml -from filelock import BaseFileLock, FileLock +from filelock import FileLock from typing_extensions import Literal from chia.util.path import mkdir @@ -48,10 +48,22 @@ def config_path_for_filename(root_path: Path, filename: Union[str, Path]) -> Pat return root_path / "config" / filename -def get_config_lock(root_path: Path, filename: Union[str, Path]) -> BaseFileLock: +@contextlib.contextmanager +def lock_config(root_path: Path, filename: Union[str, Path]) -> Iterator[None]: + # TODO: This is presently used in some tests to lock the saving of the + # configuration file without having loaded it right there. This usage + # should probably be removed and this function made private. config_path = config_path_for_filename(root_path, filename) lock_path: Path = config_path.with_name(config_path.name + ".lock") - return FileLock(lock_path) + with FileLock(lock_path): + yield + + +@contextlib.contextmanager +def lock_and_load_config(root_path: Path, filename: Union[str, Path]) -> Iterator[Dict[str, Any]]: + with lock_config(root_path=root_path, filename=filename): + config = _load_config_maybe_locked(root_path=root_path, filename=filename, acquire_lock=False) + yield config def save_config(root_path: Path, filename: Union[str, Path], config_data: Any): @@ -72,6 +84,21 @@ def load_config( filename: Union[str, Path], sub_config: Optional[str] = None, exit_on_error: bool = True, +) -> Dict: + return _load_config_maybe_locked( + root_path=root_path, + filename=filename, + sub_config=sub_config, + exit_on_error=exit_on_error, + acquire_lock=True, + ) + + +def _load_config_maybe_locked( + root_path: Path, + filename: Union[str, Path], + sub_config: Optional[str] = None, + exit_on_error: bool = True, acquire_lock: bool = True, ) -> Dict: # This must be called under an acquired config lock, or acquire_lock should be True @@ -90,7 +117,7 @@ def load_config( try: with contextlib.ExitStack() as exit_stack: if acquire_lock: - exit_stack.enter_context(get_config_lock(root_path, filename)) + exit_stack.enter_context(lock_config(root_path, filename)) with open(path, "r") as opened_config_file: r = yaml.safe_load(opened_config_file) if r is None: diff --git a/tests/block_tools.py b/tests/block_tools.py index 2174912e1ffe..dedd9d421263 100644 --- a/tests/block_tools.py +++ b/tests/block_tools.py @@ -74,7 +74,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 get_config_lock, load_config, save_config +from chia.util.config import load_config, lock_config, save_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 @@ -161,7 +161,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") - with get_config_lock(self.root_path, "config.yaml"): + with lock_config(self.root_path, "config.yaml"): save_config(self.root_path, "config.yaml", self._config) overrides = self._config["network_overrides"]["constants"][self._config["selected_network"]] updated_constants = constants.replace_str_to_bytes(**overrides) @@ -240,7 +240,7 @@ def change_config(self, new_config: Dict): overrides = self._config["network_overrides"]["constants"][self._config["selected_network"]] updated_constants = self.constants.replace_str_to_bytes(**overrides) self.constants = updated_constants - with get_config_lock(self.root_path, "config.yaml"): + with lock_config(self.root_path, "config.yaml"): save_config(self.root_path, "config.yaml", self._config) async def setup_plots(self): diff --git a/tests/core/test_farmer_harvester_rpc.py b/tests/core/test_farmer_harvester_rpc.py index 0b81bdb4654b..226c6542beb6 100644 --- a/tests/core/test_farmer_harvester_rpc.py +++ b/tests/core/test_farmer_harvester_rpc.py @@ -14,7 +14,7 @@ from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.bech32m import decode_puzzle_hash, encode_puzzle_hash from chia.util.byte_types import hexstr_to_bytes -from chia.util.config import get_config_lock, load_config, save_config +from chia.util.config import load_config, lock_and_load_config, save_config from chia.util.hash import std_hash from chia.util.ints import uint8, uint16, uint32, uint64 from chia.wallet.derive_keys import master_sk_to_wallet_sk @@ -244,8 +244,7 @@ async def test_farmer_get_pool_state(harvester_farmer_environment, self_hostname ] root_path = farmer_api.farmer._root_path - with get_config_lock(root_path, "config.yaml"): - config = load_config(root_path, "config.yaml", acquire_lock=False) + with lock_and_load_config(root_path, "config.yaml") as config: config["pool"]["pool_list"] = pool_list save_config(root_path, "config.yaml", config) await farmer_api.farmer.update_pool_state() diff --git a/tests/core/util/test_config.py b/tests/core/util/test_config.py index c3484ad650e6..e2493f8c861a 100644 --- a/tests/core/util/test_config.py +++ b/tests/core/util/test_config.py @@ -11,9 +11,10 @@ from chia.util.config import ( config_path_for_filename, create_default_chia_config, - get_config_lock, initial_config_file, load_config, + lock_and_load_config, + lock_config, save_config, ) from chia.util.path import mkdir @@ -54,11 +55,11 @@ def write_config( if atomic_write: # Note that this is usually atomic but in certain circumstances in Windows it can copy the file, # leading to a non-atomic operation. - with get_config_lock(root_path, "config.yaml"): + with lock_config(root_path, "config.yaml"): save_config(root_path=root_path, filename="config.yaml", config_data=config) else: path: Path = config_path_for_filename(root_path, filename="config.yaml") - with get_config_lock(root_path, "config.yaml"): + with lock_config(root_path, "config.yaml"): with tempfile.TemporaryDirectory(dir=path.parent) as tmp_dir: tmp_path: Path = Path(tmp_dir) / Path("config.yaml") with open(tmp_path, "w") as f: @@ -88,8 +89,7 @@ def read_and_compare_config( if do_sleep: sleep(random.random()) - with get_config_lock(root_path, "config.yaml"): - config: Dict = load_config(root_path=root_path, filename="config.yaml", acquire_lock=False) + with lock_and_load_config(root_path, "config.yaml") as config: assert config == default_config except Exception as e: if error_queue is not None: @@ -257,7 +257,7 @@ def test_save_config(self, root_path_populated_with_config, default_config_dict) # Sanity check that we didn't modify the default config assert config["harvester"]["farmer_peer"]["host"] != default_config_dict["harvester"]["farmer_peer"]["host"] # When: saving the modified config - with get_config_lock(root_path, "config.yaml"): + with lock_config(root_path, "config.yaml"): save_config(root_path=root_path, filename="config.yaml", config_data=config) # Expect: modifications should be preserved in the config read from disk @@ -274,7 +274,7 @@ def test_multiple_writers(self, root_path_populated_with_config, default_config_ # multiple writes were observed, leading to read failures when data was partially written. default_config_dict["xyz"] = "x" * 32768 root_path: Path = root_path_populated_with_config - with get_config_lock(root_path, "config.yaml"): + with lock_config(root_path, "config.yaml"): save_config(root_path=root_path, filename="config.yaml", config_data=default_config_dict) num_workers: int = 30 args = list(map(lambda _: (root_path, default_config_dict), range(num_workers))) @@ -296,7 +296,7 @@ async def test_non_atomic_writes(self, root_path_populated_with_config, default_ default_config_dict["xyz"] = "x" * 32768 root_path: Path = root_path_populated_with_config - with get_config_lock(root_path, "config.yaml"): + with lock_config(root_path, "config.yaml"): save_config(root_path=root_path, filename="config.yaml", config_data=default_config_dict) with ProcessPoolExecutor(max_workers=4) as pool: diff --git a/tests/pools/test_pool_config.py b/tests/pools/test_pool_config.py index 5dde4aec0424..798d6071f613 100644 --- a/tests/pools/test_pool_config.py +++ b/tests/pools/test_pool_config.py @@ -4,7 +4,7 @@ from blspy import AugSchemeMPL, PrivateKey from chia.pools.pool_config import PoolWalletConfig -from chia.util.config import get_config_lock, load_config, save_config, create_default_chia_config +from chia.util.config import load_config, lock_config, save_config, create_default_chia_config def test_pool_config(): @@ -37,8 +37,8 @@ def test_pool_config(): config_b["wallet"]["pool_list"] = [pwc.to_json_dict()] print(config["wallet"]["pool_list"]) - with get_config_lock(test_root, "test_pool_config_a.yaml"): + with lock_config(test_root, "test_pool_config_a.yaml"): save_config(test_root, "test_pool_config_a.yaml", config_a) - with get_config_lock(test_root, "test_pool_config_b.yaml"): + with lock_config(test_root, "test_pool_config_b.yaml"): save_config(test_root, "test_pool_config_b.yaml", config_b) assert config_a == config_b diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index bcfbb80ecef2..ee8ec4c855ff 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -6,7 +6,7 @@ from chia.types.coin_record import CoinRecord from chia.types.coin_spend import CoinSpend from chia.types.spend_bundle import SpendBundle -from chia.util.config import get_config_lock, load_config, save_config +from chia.util.config import lock_and_load_config, save_config from operator import attrgetter import logging @@ -626,8 +626,7 @@ async def tx_in_mempool_2(): # set farmer to first private key sk = await wallet_node.get_key_for_fingerprint(pks[0]) test_ph = create_puzzlehash_for_pk(master_sk_to_wallet_sk(sk, uint32(0)).get_g1()) - with get_config_lock(wallet_node.root_path, "config.yaml"): - test_config = load_config(wallet_node.root_path, "config.yaml", acquire_lock=False) + with lock_and_load_config(wallet_node.root_path, "config.yaml") as test_config: test_config["farmer"]["xch_target_address"] = encode_puzzle_hash(test_ph, "txch") # set pool to second private key sk = await wallet_node.get_key_for_fingerprint(pks[1]) From 655b27fb44acca42b7a0dfdd2293f74ccad47a99 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Mon, 28 Mar 2022 16:13:58 -0400 Subject: [PATCH 275/378] move pytest.ini to the root directory (#10892) * move pytest.ini to the root directory * pytest.ini: testpaths = tests https://docs.pytest.org/en/7.1.x/reference/reference.html?highlight=testpaths#confval-testpaths --- tests/pytest.ini => pytest.ini | 1 + 1 file changed, 1 insertion(+) rename tests/pytest.ini => pytest.ini (98%) diff --git a/tests/pytest.ini b/pytest.ini similarity index 98% rename from tests/pytest.ini rename to pytest.ini index c5834bc821fa..953e5cbf4d8a 100644 --- a/tests/pytest.ini +++ b/pytest.ini @@ -7,6 +7,7 @@ console_output_style = count log_format = %(asctime)s %(name)s: %(levelname)s %(message)s asyncio_mode = strict markers=benchmark +testpaths = tests filterwarnings = error ignore:ssl_context is deprecated:DeprecationWarning From 219e815fc4d41ada9c6b363614df8b6b9913011c Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Mon, 28 Mar 2022 16:15:24 -0400 Subject: [PATCH 276/378] set CHIA_ROOT in tests instead of symlinking (#10682) * attempt to checkout test-cache directly to desired location * rebuild workflows * maybe we can use CHIA_ROOT * use CHIA_ROOT to find blocks and plots for tests * oops * more informational printing * oops * --capture no for debugging * flake8 * import os * undo some unrelated changes now covered elsewhere * undo some debug changes * rebuild workflows --- .github/workflows/build-test-macos-blockchain.yml | 10 +++------- .github/workflows/build-test-macos-clvm.yml | 3 +++ .github/workflows/build-test-macos-core-cmds.yml | 10 +++------- .github/workflows/build-test-macos-core-consensus.yml | 10 +++------- .../workflows/build-test-macos-core-custom_types.yml | 10 +++------- .github/workflows/build-test-macos-core-daemon.yml | 10 +++------- .../build-test-macos-core-full_node-full_sync.yml | 10 +++------- .../build-test-macos-core-full_node-stores.yml | 10 +++------- .github/workflows/build-test-macos-core-full_node.yml | 10 +++------- .github/workflows/build-test-macos-core-server.yml | 10 +++------- .github/workflows/build-test-macos-core-ssl.yml | 10 +++------- .github/workflows/build-test-macos-core-util.yml | 10 +++------- .github/workflows/build-test-macos-core.yml | 10 +++------- .../workflows/build-test-macos-farmer_harvester.yml | 10 +++------- .github/workflows/build-test-macos-generator.yml | 10 +++------- .github/workflows/build-test-macos-plotting.yml | 10 +++------- .github/workflows/build-test-macos-pools.yml | 10 +++------- .github/workflows/build-test-macos-simulation.yml | 10 +++------- .github/workflows/build-test-macos-tools.yml | 10 +++------- .github/workflows/build-test-macos-util.yml | 10 +++------- .../workflows/build-test-macos-wallet-cat_wallet.yml | 10 +++------- .../workflows/build-test-macos-wallet-did_wallet.yml | 10 +++------- .../workflows/build-test-macos-wallet-rl_wallet.yml | 10 +++------- .github/workflows/build-test-macos-wallet-rpc.yml | 10 +++------- .../workflows/build-test-macos-wallet-simple_sync.yml | 10 +++------- .github/workflows/build-test-macos-wallet-sync.yml | 10 +++------- .github/workflows/build-test-macos-wallet.yml | 10 +++------- .github/workflows/build-test-macos-weight_proof.yml | 10 +++------- .github/workflows/build-test-ubuntu-blockchain.yml | 10 +++------- .github/workflows/build-test-ubuntu-clvm.yml | 3 +++ .github/workflows/build-test-ubuntu-core-cmds.yml | 10 +++------- .github/workflows/build-test-ubuntu-core-consensus.yml | 10 +++------- .../workflows/build-test-ubuntu-core-custom_types.yml | 10 +++------- .github/workflows/build-test-ubuntu-core-daemon.yml | 10 +++------- .../build-test-ubuntu-core-full_node-full_sync.yml | 10 +++------- .../build-test-ubuntu-core-full_node-stores.yml | 10 +++------- .github/workflows/build-test-ubuntu-core-full_node.yml | 10 +++------- .github/workflows/build-test-ubuntu-core-server.yml | 10 +++------- .github/workflows/build-test-ubuntu-core-ssl.yml | 10 +++------- .github/workflows/build-test-ubuntu-core-util.yml | 10 +++------- .github/workflows/build-test-ubuntu-core.yml | 10 +++------- .../workflows/build-test-ubuntu-farmer_harvester.yml | 10 +++------- .github/workflows/build-test-ubuntu-generator.yml | 10 +++------- .github/workflows/build-test-ubuntu-plotting.yml | 10 +++------- .github/workflows/build-test-ubuntu-pools.yml | 10 +++------- .github/workflows/build-test-ubuntu-simulation.yml | 10 +++------- .github/workflows/build-test-ubuntu-tools.yml | 10 +++------- .github/workflows/build-test-ubuntu-util.yml | 10 +++------- .../workflows/build-test-ubuntu-wallet-cat_wallet.yml | 10 +++------- .../workflows/build-test-ubuntu-wallet-did_wallet.yml | 10 +++------- .../workflows/build-test-ubuntu-wallet-rl_wallet.yml | 10 +++------- .github/workflows/build-test-ubuntu-wallet-rpc.yml | 10 +++------- .../workflows/build-test-ubuntu-wallet-simple_sync.yml | 10 +++------- .github/workflows/build-test-ubuntu-wallet-sync.yml | 10 +++------- .github/workflows/build-test-ubuntu-wallet.yml | 10 +++------- .github/workflows/build-test-ubuntu-weight_proof.yml | 10 +++------- tests/runner_templates/build-test-macos | 3 +++ tests/runner_templates/build-test-ubuntu | 3 +++ tests/runner_templates/checkout-test-plots.include.yml | 7 ------- tests/util/blockchain.py | 4 ++++ 60 files changed, 178 insertions(+), 385 deletions(-) diff --git a/.github/workflows/build-test-macos-blockchain.yml b/.github/workflows/build-test-macos-blockchain.yml index 7bbe162bf030..5e2b7263af10 100644 --- a/.github/workflows/build-test-macos-blockchain.yml +++ b/.github/workflows/build-test-macos-blockchain.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.8, 3.9] os: [macOS-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-macos-clvm.yml b/.github/workflows/build-test-macos-clvm.yml index 301c14f4c593..2c6ddee38f3e 100644 --- a/.github/workflows/build-test-macos-clvm.yml +++ b/.github/workflows/build-test-macos-clvm.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.8, 3.9] os: [macOS-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 diff --git a/.github/workflows/build-test-macos-core-cmds.yml b/.github/workflows/build-test-macos-core-cmds.yml index 56f21050a0a0..67078b533d06 100644 --- a/.github/workflows/build-test-macos-core-cmds.yml +++ b/.github/workflows/build-test-macos-core-cmds.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.8, 3.9] os: [macOS-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-macos-core-consensus.yml b/.github/workflows/build-test-macos-core-consensus.yml index 8a4270a491af..40ffe08f9c0a 100644 --- a/.github/workflows/build-test-macos-core-consensus.yml +++ b/.github/workflows/build-test-macos-core-consensus.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.8, 3.9] os: [macOS-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-macos-core-custom_types.yml b/.github/workflows/build-test-macos-core-custom_types.yml index 05f219790556..d11a3e87c59a 100644 --- a/.github/workflows/build-test-macos-core-custom_types.yml +++ b/.github/workflows/build-test-macos-core-custom_types.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.8, 3.9] os: [macOS-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-macos-core-daemon.yml b/.github/workflows/build-test-macos-core-daemon.yml index f37805790d38..c4dd208d72e5 100644 --- a/.github/workflows/build-test-macos-core-daemon.yml +++ b/.github/workflows/build-test-macos-core-daemon.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.8, 3.9] os: [macOS-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-macos-core-full_node-full_sync.yml b/.github/workflows/build-test-macos-core-full_node-full_sync.yml index b63cd18ef95d..23fdb659611a 100644 --- a/.github/workflows/build-test-macos-core-full_node-full_sync.yml +++ b/.github/workflows/build-test-macos-core-full_node-full_sync.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.8, 3.9] os: [macOS-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-macos-core-full_node-stores.yml b/.github/workflows/build-test-macos-core-full_node-stores.yml index 67987142564e..28aa46fe56b5 100644 --- a/.github/workflows/build-test-macos-core-full_node-stores.yml +++ b/.github/workflows/build-test-macos-core-full_node-stores.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.8, 3.9] os: [macOS-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-macos-core-full_node.yml b/.github/workflows/build-test-macos-core-full_node.yml index 1fcb01a02bca..2a6a4bd7444a 100644 --- a/.github/workflows/build-test-macos-core-full_node.yml +++ b/.github/workflows/build-test-macos-core-full_node.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.8, 3.9] os: [macOS-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-macos-core-server.yml b/.github/workflows/build-test-macos-core-server.yml index 009e3e45ac6b..6828b76a3ab7 100644 --- a/.github/workflows/build-test-macos-core-server.yml +++ b/.github/workflows/build-test-macos-core-server.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.8, 3.9] os: [macOS-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-macos-core-ssl.yml b/.github/workflows/build-test-macos-core-ssl.yml index 933578daf19f..0b3ec9c7533e 100644 --- a/.github/workflows/build-test-macos-core-ssl.yml +++ b/.github/workflows/build-test-macos-core-ssl.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.8, 3.9] os: [macOS-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-macos-core-util.yml b/.github/workflows/build-test-macos-core-util.yml index 60f894125084..576834057918 100644 --- a/.github/workflows/build-test-macos-core-util.yml +++ b/.github/workflows/build-test-macos-core-util.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.8, 3.9] os: [macOS-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-macos-core.yml b/.github/workflows/build-test-macos-core.yml index 2bff2a8443fb..dad18e1296d0 100644 --- a/.github/workflows/build-test-macos-core.yml +++ b/.github/workflows/build-test-macos-core.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.8, 3.9] os: [macOS-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-macos-farmer_harvester.yml b/.github/workflows/build-test-macos-farmer_harvester.yml index 0c47da04bcbe..61011714ba64 100644 --- a/.github/workflows/build-test-macos-farmer_harvester.yml +++ b/.github/workflows/build-test-macos-farmer_harvester.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.8, 3.9] os: [macOS-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-macos-generator.yml b/.github/workflows/build-test-macos-generator.yml index 8de17022e362..527d1d879083 100644 --- a/.github/workflows/build-test-macos-generator.yml +++ b/.github/workflows/build-test-macos-generator.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.8, 3.9] os: [macOS-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-macos-plotting.yml b/.github/workflows/build-test-macos-plotting.yml index 2538b5b578a2..33c121c484c0 100644 --- a/.github/workflows/build-test-macos-plotting.yml +++ b/.github/workflows/build-test-macos-plotting.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.8, 3.9] os: [macOS-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-macos-pools.yml b/.github/workflows/build-test-macos-pools.yml index 5be9e30f6df3..65360ebeebff 100644 --- a/.github/workflows/build-test-macos-pools.yml +++ b/.github/workflows/build-test-macos-pools.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.8, 3.9] os: [macOS-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-macos-simulation.yml b/.github/workflows/build-test-macos-simulation.yml index 740787646f62..94233dd47a40 100644 --- a/.github/workflows/build-test-macos-simulation.yml +++ b/.github/workflows/build-test-macos-simulation.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.8, 3.9] os: [macOS-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-macos-tools.yml b/.github/workflows/build-test-macos-tools.yml index b540fec94654..e9a8dad38e93 100644 --- a/.github/workflows/build-test-macos-tools.yml +++ b/.github/workflows/build-test-macos-tools.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.8, 3.9] os: [macOS-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-macos-util.yml b/.github/workflows/build-test-macos-util.yml index a09e4abd9058..558db6cb822a 100644 --- a/.github/workflows/build-test-macos-util.yml +++ b/.github/workflows/build-test-macos-util.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.8, 3.9] os: [macOS-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-macos-wallet-cat_wallet.yml b/.github/workflows/build-test-macos-wallet-cat_wallet.yml index 5fd931b6127b..30d66e8c7347 100644 --- a/.github/workflows/build-test-macos-wallet-cat_wallet.yml +++ b/.github/workflows/build-test-macos-wallet-cat_wallet.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.8, 3.9] os: [macOS-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-macos-wallet-did_wallet.yml b/.github/workflows/build-test-macos-wallet-did_wallet.yml index 6198c5b90f99..02feade15f7a 100644 --- a/.github/workflows/build-test-macos-wallet-did_wallet.yml +++ b/.github/workflows/build-test-macos-wallet-did_wallet.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.8, 3.9] os: [macOS-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-macos-wallet-rl_wallet.yml b/.github/workflows/build-test-macos-wallet-rl_wallet.yml index 77b8f8a32bde..519bfd5a9968 100644 --- a/.github/workflows/build-test-macos-wallet-rl_wallet.yml +++ b/.github/workflows/build-test-macos-wallet-rl_wallet.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.8, 3.9] os: [macOS-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-macos-wallet-rpc.yml b/.github/workflows/build-test-macos-wallet-rpc.yml index a81cf224cd81..9a811c42b1e6 100644 --- a/.github/workflows/build-test-macos-wallet-rpc.yml +++ b/.github/workflows/build-test-macos-wallet-rpc.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.8, 3.9] os: [macOS-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-macos-wallet-simple_sync.yml b/.github/workflows/build-test-macos-wallet-simple_sync.yml index 772cad839a95..71cbecc57090 100644 --- a/.github/workflows/build-test-macos-wallet-simple_sync.yml +++ b/.github/workflows/build-test-macos-wallet-simple_sync.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.8, 3.9] os: [macOS-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-macos-wallet-sync.yml b/.github/workflows/build-test-macos-wallet-sync.yml index 93dd7ea98680..835d1e1b3ce7 100644 --- a/.github/workflows/build-test-macos-wallet-sync.yml +++ b/.github/workflows/build-test-macos-wallet-sync.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.8, 3.9] os: [macOS-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-macos-wallet.yml b/.github/workflows/build-test-macos-wallet.yml index 5f9a43ac6f33..28ddfb38c2cc 100644 --- a/.github/workflows/build-test-macos-wallet.yml +++ b/.github/workflows/build-test-macos-wallet.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.8, 3.9] os: [macOS-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-macos-weight_proof.yml b/.github/workflows/build-test-macos-weight_proof.yml index eb960d9d2555..8739ef165bb7 100644 --- a/.github/workflows/build-test-macos-weight_proof.yml +++ b/.github/workflows/build-test-macos-weight_proof.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.8, 3.9] os: [macOS-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-blockchain.yml b/.github/workflows/build-test-ubuntu-blockchain.yml index b12ddf152b3a..7c4c1ed39698 100644 --- a/.github/workflows/build-test-ubuntu-blockchain.yml +++ b/.github/workflows/build-test-ubuntu-blockchain.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-clvm.yml b/.github/workflows/build-test-ubuntu-clvm.yml index a248ee202315..028af0f9031f 100644 --- a/.github/workflows/build-test-ubuntu-clvm.yml +++ b/.github/workflows/build-test-ubuntu-clvm.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 diff --git a/.github/workflows/build-test-ubuntu-core-cmds.yml b/.github/workflows/build-test-ubuntu-core-cmds.yml index fbc0e5ce3a0a..2afd49c477a8 100644 --- a/.github/workflows/build-test-ubuntu-core-cmds.yml +++ b/.github/workflows/build-test-ubuntu-core-cmds.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-core-consensus.yml b/.github/workflows/build-test-ubuntu-core-consensus.yml index f39dde36407c..5d133523a3e7 100644 --- a/.github/workflows/build-test-ubuntu-core-consensus.yml +++ b/.github/workflows/build-test-ubuntu-core-consensus.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-core-custom_types.yml b/.github/workflows/build-test-ubuntu-core-custom_types.yml index 64b298ac0bce..0d5d3f8b1ea7 100644 --- a/.github/workflows/build-test-ubuntu-core-custom_types.yml +++ b/.github/workflows/build-test-ubuntu-core-custom_types.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-core-daemon.yml b/.github/workflows/build-test-ubuntu-core-daemon.yml index 08b2070a9cf5..5efbea182ee2 100644 --- a/.github/workflows/build-test-ubuntu-core-daemon.yml +++ b/.github/workflows/build-test-ubuntu-core-daemon.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml b/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml index 359a4ea7c13d..189ece884e35 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-core-full_node-stores.yml b/.github/workflows/build-test-ubuntu-core-full_node-stores.yml index 4dad8b797cf9..27da46589643 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node-stores.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node-stores.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-core-full_node.yml b/.github/workflows/build-test-ubuntu-core-full_node.yml index 858baa51f443..2c722cb6c7cb 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-core-server.yml b/.github/workflows/build-test-ubuntu-core-server.yml index 6988285a465e..293f1bc8d8bd 100644 --- a/.github/workflows/build-test-ubuntu-core-server.yml +++ b/.github/workflows/build-test-ubuntu-core-server.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-core-ssl.yml b/.github/workflows/build-test-ubuntu-core-ssl.yml index e57213856df4..157f3d9f7c53 100644 --- a/.github/workflows/build-test-ubuntu-core-ssl.yml +++ b/.github/workflows/build-test-ubuntu-core-ssl.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-core-util.yml b/.github/workflows/build-test-ubuntu-core-util.yml index 78720a60fc14..585ed817033a 100644 --- a/.github/workflows/build-test-ubuntu-core-util.yml +++ b/.github/workflows/build-test-ubuntu-core-util.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-core.yml b/.github/workflows/build-test-ubuntu-core.yml index d5a16c31292e..a1bc0e59ff05 100644 --- a/.github/workflows/build-test-ubuntu-core.yml +++ b/.github/workflows/build-test-ubuntu-core.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-farmer_harvester.yml b/.github/workflows/build-test-ubuntu-farmer_harvester.yml index 0e334124a252..3f19f228a032 100644 --- a/.github/workflows/build-test-ubuntu-farmer_harvester.yml +++ b/.github/workflows/build-test-ubuntu-farmer_harvester.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-generator.yml b/.github/workflows/build-test-ubuntu-generator.yml index 5930963c7c7c..69fdbe1401ec 100644 --- a/.github/workflows/build-test-ubuntu-generator.yml +++ b/.github/workflows/build-test-ubuntu-generator.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-plotting.yml b/.github/workflows/build-test-ubuntu-plotting.yml index 5dfb0eb749e1..5db8bfd40e88 100644 --- a/.github/workflows/build-test-ubuntu-plotting.yml +++ b/.github/workflows/build-test-ubuntu-plotting.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-pools.yml b/.github/workflows/build-test-ubuntu-pools.yml index a4a3fa2cddbb..3d2d78bdb9ff 100644 --- a/.github/workflows/build-test-ubuntu-pools.yml +++ b/.github/workflows/build-test-ubuntu-pools.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-simulation.yml b/.github/workflows/build-test-ubuntu-simulation.yml index e978bc111882..354145da0ef3 100644 --- a/.github/workflows/build-test-ubuntu-simulation.yml +++ b/.github/workflows/build-test-ubuntu-simulation.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-tools.yml b/.github/workflows/build-test-ubuntu-tools.yml index 8cada89c8edf..5d58a6499c9a 100644 --- a/.github/workflows/build-test-ubuntu-tools.yml +++ b/.github/workflows/build-test-ubuntu-tools.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-util.yml b/.github/workflows/build-test-ubuntu-util.yml index e45442742a1f..f0c0c7ef7fa4 100644 --- a/.github/workflows/build-test-ubuntu-util.yml +++ b/.github/workflows/build-test-ubuntu-util.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml index 0c7e4ebaa269..0dac4ebdeea2 100644 --- a/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml index 2372455c433f..33d0efec8df1 100644 --- a/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml index b7c4a16d874b..71c71b5ebff2 100644 --- a/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-wallet-rpc.yml b/.github/workflows/build-test-ubuntu-wallet-rpc.yml index 51de656d5981..0e59373f2eb4 100644 --- a/.github/workflows/build-test-ubuntu-wallet-rpc.yml +++ b/.github/workflows/build-test-ubuntu-wallet-rpc.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml b/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml index e0f54f9584b4..29b100bb4b32 100644 --- a/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml +++ b/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-wallet-sync.yml b/.github/workflows/build-test-ubuntu-wallet-sync.yml index b87e69eb2ed8..faf60238d8db 100644 --- a/.github/workflows/build-test-ubuntu-wallet-sync.yml +++ b/.github/workflows/build-test-ubuntu-wallet-sync.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-wallet.yml b/.github/workflows/build-test-ubuntu-wallet.yml index 6f04b4e5f2ea..d828f5ed7a7d 100644 --- a/.github/workflows/build-test-ubuntu-wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.github/workflows/build-test-ubuntu-weight_proof.yml b/.github/workflows/build-test-ubuntu-weight_proof.yml index c20171149264..207b284eeef0 100644 --- a/.github/workflows/build-test-ubuntu-weight_proof.yml +++ b/.github/workflows/build-test-ubuntu-weight_proof.yml @@ -30,6 +30,9 @@ jobs: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 @@ -70,13 +73,6 @@ jobs: ref: '0.28.0' fetch-depth: 1 - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia - - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/tests/runner_templates/build-test-macos b/tests/runner_templates/build-test-macos index 88d648bc5081..e12855b8d18c 100644 --- a/tests/runner_templates/build-test-macos +++ b/tests/runner_templates/build-test-macos @@ -30,6 +30,9 @@ jobs: python-version: [3.8, 3.9] os: [macOS-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 diff --git a/tests/runner_templates/build-test-ubuntu b/tests/runner_templates/build-test-ubuntu index 75e1b6c580c8..893fd6678b1e 100644 --- a/tests/runner_templates/build-test-ubuntu +++ b/tests/runner_templates/build-test-ubuntu @@ -30,6 +30,9 @@ jobs: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + steps: - name: Checkout Code uses: actions/checkout@v3 diff --git a/tests/runner_templates/checkout-test-plots.include.yml b/tests/runner_templates/checkout-test-plots.include.yml index 59f47e577d43..1118ef00b035 100644 --- a/tests/runner_templates/checkout-test-plots.include.yml +++ b/tests/runner_templates/checkout-test-plots.include.yml @@ -5,10 +5,3 @@ path: '.chia' ref: '0.28.0' fetch-depth: 1 - - - name: Link home directory - run: | - cd $HOME - ln -s $GITHUB_WORKSPACE/.chia - echo "$HOME/.chia" - ls -al $HOME/.chia diff --git a/tests/util/blockchain.py b/tests/util/blockchain.py index ebb65a1367cb..95d6908b0603 100644 --- a/tests/util/blockchain.py +++ b/tests/util/blockchain.py @@ -58,6 +58,7 @@ def persistent_blocks( mkdir(block_path_dir) if file_path.exists(): + print(f"File found at: {file_path}") try: bytes_list = file_path.read_bytes() block_bytes_list: List[bytes] = pickle.loads(bytes_list) @@ -69,7 +70,10 @@ def persistent_blocks( return blocks except EOFError: print("\n error reading db file") + else: + print(f"File not found at: {file_path}") + print("Creating a new test db") return new_test_db( file_path, num_of_blocks, From 287bf6c0cca6393cbc847068f8ed2efe1ab24e45 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Mon, 28 Mar 2022 16:16:16 -0400 Subject: [PATCH 277/378] Remove sys.exit() from chia daemon /exit endpoint (#10454) --- chia/daemon/server.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/chia/daemon/server.py b/chia/daemon/server.py index e9cda8fdffce..de7a26a82561 100644 --- a/chia/daemon/server.py +++ b/chia/daemon/server.py @@ -1156,9 +1156,7 @@ async def exit(self) -> Dict[str, Any]: await asyncio.wait(jobs) self.services.clear() - # TODO: fix this hack - asyncio.get_event_loop().call_later(5, lambda *args: sys.exit(0)) - log.info("chia daemon exiting in 5 seconds") + log.info("chia daemon exiting") response = {"success": True} return response From a691d3c4b2aad546b8d9058b6a3882416edaee9b Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Mon, 28 Mar 2022 16:20:50 -0400 Subject: [PATCH 278/378] asyncio.get_event_loop() is deprecated in 3.10, stop using it (mostly) (#10418) * asyncio.get_event_loop() is deprecated in 3.10, stop using it https://docs.python.org/3.10/library/asyncio-eventloop.html#asyncio.get_event_loop > Deprecated since version 3.10: Deprecation warning is emitted if there is no running event loop. In future Python releases, this function will be an alias of get_running_loop(). * black --- chia/cmds/chia.py | 2 +- chia/cmds/passphrase.py | 12 ++-------- chia/cmds/plots.py | 4 ++-- chia/cmds/start.py | 2 +- chia/cmds/stop.py | 2 +- chia/daemon/server.py | 8 ++++--- chia/full_node/full_node.py | 5 +---- chia/harvester/harvester.py | 2 +- chia/plotters/bladebit.py | 5 ++--- chia/plotters/chiapos.py | 4 ++-- chia/plotters/madmax.py | 5 ++--- chia/seeder/dns_server.py | 36 ++++++++++++++++-------------- chia/timelord/timelord_launcher.py | 32 ++++++++++++++------------ chia/util/keyring_wrapper.py | 2 +- 14 files changed, 58 insertions(+), 63 deletions(-) diff --git a/chia/cmds/chia.py b/chia/cmds/chia.py index 1a02749ef848..00fe6e6fb107 100644 --- a/chia/cmds/chia.py +++ b/chia/cmds/chia.py @@ -123,7 +123,7 @@ def run_daemon_cmd(ctx: click.Context, wait_for_unlock: bool) -> None: wait_for_unlock = wait_for_unlock and Keychain.is_keyring_locked() - asyncio.get_event_loop().run_until_complete(async_run_daemon(ctx.obj["root_path"], wait_for_unlock=wait_for_unlock)) + asyncio.run(async_run_daemon(ctx.obj["root_path"], wait_for_unlock=wait_for_unlock)) cli.add_command(keys_cmd) diff --git a/chia/cmds/passphrase.py b/chia/cmds/passphrase.py index 7e55d17ed05e..dff0fbeb2906 100644 --- a/chia/cmds/passphrase.py +++ b/chia/cmds/passphrase.py @@ -65,11 +65,7 @@ def set_cmd( if success: # Attempt to update the daemon's passphrase cache - sys.exit( - asyncio.get_event_loop().run_until_complete( - async_update_daemon_passphrase_cache_if_running(ctx.obj["root_path"]) - ) - ) + sys.exit(asyncio.run(async_update_daemon_passphrase_cache_if_running(ctx.obj["root_path"]))) @passphrase_cmd.command( @@ -95,11 +91,7 @@ def remove_cmd(ctx: click.Context, current_passphrase_file: Optional[TextIOWrapp if remove_passphrase(current_passphrase): # Attempt to update the daemon's passphrase cache - sys.exit( - asyncio.get_event_loop().run_until_complete( - async_update_daemon_passphrase_cache_if_running(ctx.obj["root_path"]) - ) - ) + sys.exit(asyncio.run(async_update_daemon_passphrase_cache_if_running(ctx.obj["root_path"]))) @passphrase_cmd.group("hint", short_help="Manage the optional keyring passphrase hint") diff --git a/chia/cmds/plots.py b/chia/cmds/plots.py index 6259cb8bf508..247f603edc1d 100644 --- a/chia/cmds/plots.py +++ b/chia/cmds/plots.py @@ -138,7 +138,7 @@ def __init__(self): print("Error: The minimum k size allowed from the cli is k=25.") sys.exit(1) - plot_keys = asyncio.get_event_loop().run_until_complete( + plot_keys = asyncio.run( resolve_plot_keys( farmer_public_key, alt_fingerprint, @@ -150,7 +150,7 @@ def __init__(self): ) ) - asyncio.get_event_loop().run_until_complete(create_plots(Params(), plot_keys, ctx.obj["root_path"])) + asyncio.run(create_plots(Params(), plot_keys, ctx.obj["root_path"])) @plots_cmd.command("check", short_help="Checks plots") diff --git a/chia/cmds/start.py b/chia/cmds/start.py index 624b43007823..0dd5acebe91c 100644 --- a/chia/cmds/start.py +++ b/chia/cmds/start.py @@ -11,4 +11,4 @@ def start_cmd(ctx: click.Context, restart: bool, group: str) -> None: import asyncio from .start_funcs import async_start - asyncio.get_event_loop().run_until_complete(async_start(ctx.obj["root_path"], group, restart)) + asyncio.run(async_start(ctx.obj["root_path"], group, restart)) diff --git a/chia/cmds/stop.py b/chia/cmds/stop.py index cfe91411b358..832a199bb742 100644 --- a/chia/cmds/stop.py +++ b/chia/cmds/stop.py @@ -43,4 +43,4 @@ async def async_stop(root_path: Path, group: str, stop_daemon: bool) -> int: def stop_cmd(ctx: click.Context, daemon: bool, group: str) -> None: import asyncio - sys.exit(asyncio.get_event_loop().run_until_complete(async_stop(ctx.obj["root_path"], group, daemon))) + sys.exit(asyncio.run(async_stop(ctx.obj["root_path"], group, daemon))) diff --git a/chia/daemon/server.py b/chia/daemon/server.py index de7a26a82561..c1216c3f12dd 100644 --- a/chia/daemon/server.py +++ b/chia/daemon/server.py @@ -1015,7 +1015,8 @@ async def start_plotting(self, request: Dict[str, Any]): if parallel is True or can_start_serial_plotting: log.info(f"Plotting will start in {config['delay']} seconds") - loop = asyncio.get_event_loop() + # TODO: loop gets passed down a lot, review for potential removal + loop = asyncio.get_running_loop() loop.create_task(self._start_plotting(id, loop, queue)) else: log.info("Plotting will start automatically when previous plotting finish") @@ -1058,7 +1059,8 @@ async def stop_plotting(self, request: Dict[str, Any]) -> Dict[str, Any]: self.plots_queue.remove(config) if run_next: - loop = asyncio.get_event_loop() + # TODO: review to see if we can remove this + loop = asyncio.get_running_loop() self._run_next_serial_plotting(loop, queue) return {"success": True} @@ -1486,7 +1488,7 @@ async def async_run_daemon(root_path: Path, wait_for_unlock: bool = False) -> in def run_daemon(root_path: Path, wait_for_unlock: bool = False) -> int: - result = asyncio.get_event_loop().run_until_complete(async_run_daemon(root_path, wait_for_unlock)) + result = asyncio.run(async_run_daemon(root_path, wait_for_unlock)) return result diff --git a/chia/full_node/full_node.py b/chia/full_node/full_node.py index d03b4bda2df8..172abd23b152 100644 --- a/chia/full_node/full_node.py +++ b/chia/full_node/full_node.py @@ -997,10 +997,7 @@ async def validate_block_batches(batch_queue): await self.send_peak_to_wallets() self.blockchain.clean_block_record(end_height - self.constants.BLOCKS_CACHE_SIZE) - loop = asyncio.get_event_loop() - batch_queue: asyncio.Queue[Tuple[ws.WSChiaConnection, List[FullBlock]]] = asyncio.Queue( - loop=loop, maxsize=buffer_size - ) + batch_queue: asyncio.Queue[Tuple[ws.WSChiaConnection, List[FullBlock]]] = asyncio.Queue(maxsize=buffer_size) fetch_task = asyncio.Task(fetch_block_batches(batch_queue, peers_with_peak)) validate_task = asyncio.Task(validate_block_batches(batch_queue)) try: diff --git a/chia/harvester/harvester.py b/chia/harvester/harvester.py index d2dcd1eed5ca..b63b0c7a16d7 100644 --- a/chia/harvester/harvester.py +++ b/chia/harvester/harvester.py @@ -64,7 +64,7 @@ def __init__(self, root_path: Path, config: Dict, constants: ConsensusConstants) async def _start(self): self._refresh_lock = asyncio.Lock() - self.event_loop = asyncio.get_event_loop() + self.event_loop = asyncio.get_running_loop() def _close(self): self._is_shutdown = True diff --git a/chia/plotters/bladebit.py b/chia/plotters/bladebit.py index e085cc47c1b3..0fc826e671c9 100644 --- a/chia/plotters/bladebit.py +++ b/chia/plotters/bladebit.py @@ -173,7 +173,7 @@ def plot_bladebit(args, chia_root_path, root_path): except Exception as e: print(f"Exception while installing bladebit plotter: {e}") return - plot_keys = asyncio.get_event_loop().run_until_complete( + plot_keys = asyncio.run( resolve_plot_keys( None if args.farmerkey == b"" else args.farmerkey.hex(), None, @@ -209,8 +209,7 @@ def plot_bladebit(args, chia_root_path, root_path): call_args.append("-m") call_args.append(args.finaldir) try: - loop = asyncio.get_event_loop() - loop.run_until_complete(run_plotter(call_args, progress)) + asyncio.run(run_plotter(call_args, progress)) except Exception as e: print(f"Exception while plotting: {e} {type(e)}") print(f"Traceback: {traceback.format_exc()}") diff --git a/chia/plotters/chiapos.py b/chia/plotters/chiapos.py index f402e7c174cb..fc24acbdf199 100644 --- a/chia/plotters/chiapos.py +++ b/chia/plotters/chiapos.py @@ -43,7 +43,7 @@ def plot_chia(args, root_path): print("Error: The minimum k size allowed from the cli is k=25.") return - plot_keys = asyncio.get_event_loop().run_until_complete( + plot_keys = asyncio.run( resolve_plot_keys( None if args.farmerkey == b"" else args.farmerkey.hex(), args.alt_fingerprint, @@ -54,4 +54,4 @@ def plot_chia(args, root_path): args.connect_to_daemon, ) ) - asyncio.get_event_loop().run_until_complete(create_plots(Params(args), plot_keys, root_path)) + asyncio.run(create_plots(Params(args), plot_keys, root_path)) diff --git a/chia/plotters/madmax.py b/chia/plotters/madmax.py index ce75682b08ab..c3ec020e3009 100644 --- a/chia/plotters/madmax.py +++ b/chia/plotters/madmax.py @@ -180,7 +180,7 @@ def plot_madmax(args, chia_root_path: Path, plotters_root_path: Path): except Exception as e: print(f"Exception while installing madmax plotter: {e}") return - plot_keys = asyncio.get_event_loop().run_until_complete( + plot_keys = asyncio.run( resolve_plot_keys( None if args.farmerkey == b"" else args.farmerkey.hex(), None, @@ -227,8 +227,7 @@ def plot_madmax(args, chia_root_path: Path, plotters_root_path: Path): call_args.append("-k") call_args.append(str(args.size)) try: - loop = asyncio.get_event_loop() - loop.run_until_complete(run_plotter(call_args, progress)) + asyncio.run(run_plotter(call_args, progress)) except Exception as e: print(f"Exception while plotting: {type(e)} {e}") print(f"Traceback: {traceback.format_exc()}") diff --git a/chia/seeder/dns_server.py b/chia/seeder/dns_server.py index 24dc5b7b10b1..85466cadfc8d 100644 --- a/chia/seeder/dns_server.py +++ b/chia/seeder/dns_server.py @@ -36,7 +36,7 @@ def __getattr__(self, item): class EchoServerProtocol(asyncio.DatagramProtocol): def __init__(self, callback): - self.data_queue = asyncio.Queue(loop=asyncio.get_event_loop()) + self.data_queue = asyncio.Queue() self.callback = callback asyncio.ensure_future(self.respond()) @@ -44,7 +44,7 @@ def connection_made(self, transport): self.transport = transport def datagram_received(self, data, addr): - asyncio.ensure_future(self.handler(data, addr), loop=asyncio.get_event_loop()) + asyncio.ensure_future(self.handler(data, addr)) async def respond(self): while True: @@ -242,6 +242,22 @@ async def kill_processes(): pass +def signal_received(): + asyncio.create_task(kill_processes()) + + +async def async_main(config, root_path): + loop = asyncio.get_running_loop() + + try: + loop.add_signal_handler(signal.SIGINT, signal_received) + loop.add_signal_handler(signal.SIGTERM, signal_received) + except NotImplementedError: + log.info("signal handlers unsupported") + + await serve_dns(config, root_path) + + def main(): root_path = DEFAULT_ROOT_PATH config = load_config(root_path, "config.yaml", SERVICE_NAME) @@ -267,21 +283,7 @@ def main(): ) ns_records = [NS(ns)] - def signal_received(): - asyncio.create_task(kill_processes()) - - loop = asyncio.get_event_loop() - - try: - loop.add_signal_handler(signal.SIGINT, signal_received) - loop.add_signal_handler(signal.SIGTERM, signal_received) - except NotImplementedError: - log.info("signal handlers unsupported") - - try: - loop.run_until_complete(serve_dns(config, root_path)) - finally: - loop.close() + asyncio.run(async_main(config=config, root_path=root_path)) if __name__ == "__main__": diff --git a/chia/timelord/timelord_launcher.py b/chia/timelord/timelord_launcher.py index 88f0e11a5ba4..657cb6eb1e43 100644 --- a/chia/timelord/timelord_launcher.py +++ b/chia/timelord/timelord_launcher.py @@ -90,20 +90,12 @@ async def spawn_all_processes(config: Dict, net_config: Dict): await asyncio.gather(*awaitables) -def main(): - if os.name == "nt": - log.info("Timelord launcher not supported on Windows.") - return - root_path = DEFAULT_ROOT_PATH - setproctitle("chia_timelord_launcher") - net_config = load_config(root_path, "config.yaml") - config = net_config["timelord_launcher"] - initialize_logging("TLauncher", config["logging"], root_path) +def signal_received(): + asyncio.create_task(kill_processes()) - def signal_received(): - asyncio.create_task(kill_processes()) - loop = asyncio.get_event_loop() +async def async_main(config, net_config): + loop = asyncio.get_running_loop() try: loop.add_signal_handler(signal.SIGINT, signal_received) @@ -112,10 +104,22 @@ def signal_received(): log.info("signal handlers unsupported") try: - loop.run_until_complete(spawn_all_processes(config, net_config)) + await spawn_all_processes(config, net_config) finally: log.info("Launcher fully closed.") - loop.close() + + +def main(): + if os.name == "nt": + log.info("Timelord launcher not supported on Windows.") + return + root_path = DEFAULT_ROOT_PATH + setproctitle("chia_timelord_launcher") + net_config = load_config(root_path, "config.yaml") + config = net_config["timelord_launcher"] + initialize_logging("TLauncher", config["logging"], root_path) + + asyncio.run(async_main(config=config, net_config=net_config)) if __name__ == "__main__": diff --git a/chia/util/keyring_wrapper.py b/chia/util/keyring_wrapper.py index 0cb3c162b0ae..f830de1fc85e 100644 --- a/chia/util/keyring_wrapper.py +++ b/chia/util/keyring_wrapper.py @@ -548,7 +548,7 @@ def migrate_legacy_keyring_interactive(self): print("Keys in old keyring left intact") # Notify the daemon (if running) that migration has completed - asyncio.get_event_loop().run_until_complete(async_update_daemon_migration_completed_if_running()) + asyncio.run(async_update_daemon_migration_completed_if_running()) # Keyring interface From 36bee1b8caccda8bcf5d2ecf6645e7a46e130696 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Mon, 28 Mar 2022 16:22:23 -0400 Subject: [PATCH 279/378] run tests in CI via coverage (#9704) * Add coverage (without collection) * Separate test_block_compression() to avoid coverage-related hangs * Revert "Separate test_block_compression() to avoid coverage-related hangs" This reverts commit ebad3d001778b2344f0d2a651be0206d7f6dc847. * multiprocessing.set_start_method("spawn") * multiprocessing.set_start_method() in conftest.py * hand hold cc wallet tests * lint * spawn for running chia as well * handle already set start method case * a bit more timeout for test_multiple_writers * more timeout for test_writer_lock_blocked_by_readers * 45 minute tieout for tests/pools/ * 45 minute tieout for tests/pools/ * some more hand holding sleeps * report coverage in each workflow only really useful to make sure it is capturing something * oops * complete the job name and the JOB_NAME * better coverage result file names * reset worker process titles * rebuild workflows * rebuild workflows * black * black * rebuild workflows * push timeouts * actually include the updated workflows... * push more workflow timeouts * parallel=True * rebuild workflows --- .coveragerc | 17 ++++++++++++++ .../workflows/build-test-macos-blockchain.yml | 20 ++++++++++++++++- .github/workflows/build-test-macos-clvm.yml | 20 ++++++++++++++++- .../workflows/build-test-macos-core-cmds.yml | 20 ++++++++++++++++- .../build-test-macos-core-consensus.yml | 20 ++++++++++++++++- .../build-test-macos-core-custom_types.yml | 20 ++++++++++++++++- .../build-test-macos-core-daemon.yml | 20 ++++++++++++++++- ...ld-test-macos-core-full_node-full_sync.yml | 20 ++++++++++++++++- ...build-test-macos-core-full_node-stores.yml | 20 ++++++++++++++++- .../build-test-macos-core-full_node.yml | 20 ++++++++++++++++- .../build-test-macos-core-server.yml | 20 ++++++++++++++++- .../workflows/build-test-macos-core-ssl.yml | 20 ++++++++++++++++- .../workflows/build-test-macos-core-util.yml | 20 ++++++++++++++++- .github/workflows/build-test-macos-core.yml | 20 ++++++++++++++++- .../build-test-macos-farmer_harvester.yml | 20 ++++++++++++++++- .../workflows/build-test-macos-generator.yml | 20 ++++++++++++++++- .../workflows/build-test-macos-plotting.yml | 20 ++++++++++++++++- .github/workflows/build-test-macos-pools.yml | 20 ++++++++++++++++- .../workflows/build-test-macos-simulation.yml | 20 ++++++++++++++++- .github/workflows/build-test-macos-tools.yml | 20 ++++++++++++++++- .github/workflows/build-test-macos-util.yml | 20 ++++++++++++++++- .../build-test-macos-wallet-cat_wallet.yml | 20 ++++++++++++++++- .../build-test-macos-wallet-did_wallet.yml | 20 ++++++++++++++++- .../build-test-macos-wallet-rl_wallet.yml | 20 ++++++++++++++++- .../workflows/build-test-macos-wallet-rpc.yml | 20 ++++++++++++++++- .../build-test-macos-wallet-simple_sync.yml | 20 ++++++++++++++++- .../build-test-macos-wallet-sync.yml | 20 ++++++++++++++++- .github/workflows/build-test-macos-wallet.yml | 22 +++++++++++++++++-- .../build-test-macos-weight_proof.yml | 20 ++++++++++++++++- .../build-test-ubuntu-blockchain.yml | 20 ++++++++++++++++- .github/workflows/build-test-ubuntu-clvm.yml | 20 ++++++++++++++++- .../workflows/build-test-ubuntu-core-cmds.yml | 20 ++++++++++++++++- .../build-test-ubuntu-core-consensus.yml | 20 ++++++++++++++++- .../build-test-ubuntu-core-custom_types.yml | 20 ++++++++++++++++- .../build-test-ubuntu-core-daemon.yml | 20 ++++++++++++++++- ...d-test-ubuntu-core-full_node-full_sync.yml | 20 ++++++++++++++++- ...uild-test-ubuntu-core-full_node-stores.yml | 20 ++++++++++++++++- .../build-test-ubuntu-core-full_node.yml | 20 ++++++++++++++++- .../build-test-ubuntu-core-server.yml | 20 ++++++++++++++++- .../workflows/build-test-ubuntu-core-ssl.yml | 20 ++++++++++++++++- .../workflows/build-test-ubuntu-core-util.yml | 20 ++++++++++++++++- .github/workflows/build-test-ubuntu-core.yml | 20 ++++++++++++++++- .../build-test-ubuntu-farmer_harvester.yml | 20 ++++++++++++++++- .../workflows/build-test-ubuntu-generator.yml | 20 ++++++++++++++++- .../workflows/build-test-ubuntu-plotting.yml | 20 ++++++++++++++++- .github/workflows/build-test-ubuntu-pools.yml | 20 ++++++++++++++++- .../build-test-ubuntu-simulation.yml | 20 ++++++++++++++++- .github/workflows/build-test-ubuntu-tools.yml | 20 ++++++++++++++++- .github/workflows/build-test-ubuntu-util.yml | 20 ++++++++++++++++- .../build-test-ubuntu-wallet-cat_wallet.yml | 20 ++++++++++++++++- .../build-test-ubuntu-wallet-did_wallet.yml | 20 ++++++++++++++++- .../build-test-ubuntu-wallet-rl_wallet.yml | 20 ++++++++++++++++- .../build-test-ubuntu-wallet-rpc.yml | 20 ++++++++++++++++- .../build-test-ubuntu-wallet-simple_sync.yml | 20 ++++++++++++++++- .../build-test-ubuntu-wallet-sync.yml | 20 ++++++++++++++++- .../workflows/build-test-ubuntu-wallet.yml | 22 +++++++++++++++++-- .../build-test-ubuntu-weight_proof.yml | 20 ++++++++++++++++- setup.py | 1 + tests/runner_templates/build-test-macos | 20 ++++++++++++++++- tests/runner_templates/build-test-ubuntu | 20 ++++++++++++++++- tests/wallet/cat_wallet/config.py | 1 + tests/wallet/config.py | 2 ++ 62 files changed, 1125 insertions(+), 60 deletions(-) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 000000000000..574fbb4b7b56 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,17 @@ +[run] +branch=True +relative_files=True +source= + chia + tests +concurrency=multiprocessing +parallel=True + +[report] +precision = 1 +exclude_lines = + pragma: no cover + abc\.abstractmethod + typing\.overload + ^\s*\.\.\.\s*$ + if typing.TYPE_CHECKING: diff --git a/.github/workflows/build-test-macos-blockchain.yml b/.github/workflows/build-test-macos-blockchain.yml index 5e2b7263af10..b8bda1f29cf2 100644 --- a/.github/workflows/build-test-macos-blockchain.yml +++ b/.github/workflows/build-test-macos-blockchain.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_blockchain env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -85,7 +87,23 @@ jobs: - name: Test blockchain code with pytest run: | . ./activate - ./venv/bin/py.test tests/blockchain/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/blockchain/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-clvm.yml b/.github/workflows/build-test-macos-clvm.yml index 2c6ddee38f3e..483fe50a6678 100644 --- a/.github/workflows/build-test-macos-clvm.yml +++ b/.github/workflows/build-test-macos-clvm.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_clvm env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -79,7 +81,23 @@ jobs: - name: Test clvm code with pytest run: | . ./activate - ./venv/bin/py.test tests/clvm/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/clvm/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-core-cmds.yml b/.github/workflows/build-test-macos-core-cmds.yml index 67078b533d06..1cd1557853a7 100644 --- a/.github/workflows/build-test-macos-core-cmds.yml +++ b/.github/workflows/build-test-macos-core-cmds.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-cmds env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -85,7 +87,23 @@ jobs: - name: Test core-cmds code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/cmds/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/cmds/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-core-consensus.yml b/.github/workflows/build-test-macos-core-consensus.yml index 40ffe08f9c0a..85f4b2e0323b 100644 --- a/.github/workflows/build-test-macos-core-consensus.yml +++ b/.github/workflows/build-test-macos-core-consensus.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-consensus env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -85,7 +87,23 @@ jobs: - name: Test core-consensus code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/consensus/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/consensus/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-core-custom_types.yml b/.github/workflows/build-test-macos-core-custom_types.yml index d11a3e87c59a..2902458e4dc2 100644 --- a/.github/workflows/build-test-macos-core-custom_types.yml +++ b/.github/workflows/build-test-macos-core-custom_types.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-custom_types env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -85,7 +87,23 @@ jobs: - name: Test core-custom_types code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/custom_types/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/custom_types/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-core-daemon.yml b/.github/workflows/build-test-macos-core-daemon.yml index c4dd208d72e5..4a2d05d06681 100644 --- a/.github/workflows/build-test-macos-core-daemon.yml +++ b/.github/workflows/build-test-macos-core-daemon.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-daemon env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -97,7 +99,23 @@ jobs: - name: Test core-daemon code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/daemon/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/daemon/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-core-full_node-full_sync.yml b/.github/workflows/build-test-macos-core-full_node-full_sync.yml index 23fdb659611a..f0584dc5cbea 100644 --- a/.github/workflows/build-test-macos-core-full_node-full_sync.yml +++ b/.github/workflows/build-test-macos-core-full_node-full_sync.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-full_node-full_sync env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -85,7 +87,23 @@ jobs: - name: Test core-full_node-full_sync code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/full_node/full_sync/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/full_node/full_sync/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-core-full_node-stores.yml b/.github/workflows/build-test-macos-core-full_node-stores.yml index 28aa46fe56b5..32900f68bc9c 100644 --- a/.github/workflows/build-test-macos-core-full_node-stores.yml +++ b/.github/workflows/build-test-macos-core-full_node-stores.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-full_node-stores env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -85,7 +87,23 @@ jobs: - name: Test core-full_node-stores code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/full_node/stores/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/full_node/stores/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-core-full_node.yml b/.github/workflows/build-test-macos-core-full_node.yml index 2a6a4bd7444a..59f24ca4c244 100644 --- a/.github/workflows/build-test-macos-core-full_node.yml +++ b/.github/workflows/build-test-macos-core-full_node.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-full_node env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -85,7 +87,23 @@ jobs: - name: Test core-full_node code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/full_node/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/full_node/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-core-server.yml b/.github/workflows/build-test-macos-core-server.yml index 6828b76a3ab7..54076aaf7f35 100644 --- a/.github/workflows/build-test-macos-core-server.yml +++ b/.github/workflows/build-test-macos-core-server.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-server env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -85,7 +87,23 @@ jobs: - name: Test core-server code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/server/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/server/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-core-ssl.yml b/.github/workflows/build-test-macos-core-ssl.yml index 0b3ec9c7533e..aa91b12ea9fd 100644 --- a/.github/workflows/build-test-macos-core-ssl.yml +++ b/.github/workflows/build-test-macos-core-ssl.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-ssl env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -85,7 +87,23 @@ jobs: - name: Test core-ssl code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/ssl/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/ssl/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-core-util.yml b/.github/workflows/build-test-macos-core-util.yml index 576834057918..404fb7f4873f 100644 --- a/.github/workflows/build-test-macos-core-util.yml +++ b/.github/workflows/build-test-macos-core-util.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-util env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -85,7 +87,23 @@ jobs: - name: Test core-util code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/util/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/util/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-core.yml b/.github/workflows/build-test-macos-core.yml index dad18e1296d0..13473b76e115 100644 --- a/.github/workflows/build-test-macos-core.yml +++ b/.github/workflows/build-test-macos-core.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -85,7 +87,23 @@ jobs: - name: Test core code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-farmer_harvester.yml b/.github/workflows/build-test-macos-farmer_harvester.yml index 61011714ba64..9a2c21f83101 100644 --- a/.github/workflows/build-test-macos-farmer_harvester.yml +++ b/.github/workflows/build-test-macos-farmer_harvester.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_farmer_harvester env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -85,7 +87,23 @@ jobs: - name: Test farmer_harvester code with pytest run: | . ./activate - ./venv/bin/py.test tests/farmer_harvester/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/farmer_harvester/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-generator.yml b/.github/workflows/build-test-macos-generator.yml index 527d1d879083..9e71599c3411 100644 --- a/.github/workflows/build-test-macos-generator.yml +++ b/.github/workflows/build-test-macos-generator.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_generator env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -85,7 +87,23 @@ jobs: - name: Test generator code with pytest run: | . ./activate - ./venv/bin/py.test tests/generator/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/generator/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-plotting.yml b/.github/workflows/build-test-macos-plotting.yml index 33c121c484c0..236c1ab6be19 100644 --- a/.github/workflows/build-test-macos-plotting.yml +++ b/.github/workflows/build-test-macos-plotting.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_plotting env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -85,7 +87,23 @@ jobs: - name: Test plotting code with pytest run: | . ./activate - ./venv/bin/py.test tests/plotting/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/plotting/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-pools.yml b/.github/workflows/build-test-macos-pools.yml index 65360ebeebff..0c25fd68dab1 100644 --- a/.github/workflows/build-test-macos-pools.yml +++ b/.github/workflows/build-test-macos-pools.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_pools env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -85,7 +87,23 @@ jobs: - name: Test pools code with pytest run: | . ./activate - ./venv/bin/py.test tests/pools/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/pools/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-simulation.yml b/.github/workflows/build-test-macos-simulation.yml index 94233dd47a40..b1d07590dd62 100644 --- a/.github/workflows/build-test-macos-simulation.yml +++ b/.github/workflows/build-test-macos-simulation.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_simulation env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -97,7 +99,23 @@ jobs: - name: Test simulation code with pytest run: | . ./activate - ./venv/bin/py.test tests/simulation/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/simulation/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-tools.yml b/.github/workflows/build-test-macos-tools.yml index e9a8dad38e93..f35f5b8bc29b 100644 --- a/.github/workflows/build-test-macos-tools.yml +++ b/.github/workflows/build-test-macos-tools.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_tools env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -85,7 +87,23 @@ jobs: - name: Test tools code with pytest run: | . ./activate - ./venv/bin/py.test tests/tools/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/tools/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-util.yml b/.github/workflows/build-test-macos-util.yml index 558db6cb822a..1000fa3b3bfa 100644 --- a/.github/workflows/build-test-macos-util.yml +++ b/.github/workflows/build-test-macos-util.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_util env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -85,7 +87,23 @@ jobs: - name: Test util code with pytest run: | . ./activate - ./venv/bin/py.test tests/util/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/util/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-wallet-cat_wallet.yml b/.github/workflows/build-test-macos-wallet-cat_wallet.yml index 30d66e8c7347..acfe17e6b4ba 100644 --- a/.github/workflows/build-test-macos-wallet-cat_wallet.yml +++ b/.github/workflows/build-test-macos-wallet-cat_wallet.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet-cat_wallet env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -85,7 +87,23 @@ jobs: - name: Test wallet-cat_wallet code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/cat_wallet/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/cat_wallet/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-wallet-did_wallet.yml b/.github/workflows/build-test-macos-wallet-did_wallet.yml index 02feade15f7a..342fcbcd6d04 100644 --- a/.github/workflows/build-test-macos-wallet-did_wallet.yml +++ b/.github/workflows/build-test-macos-wallet-did_wallet.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet-did_wallet env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -85,7 +87,23 @@ jobs: - name: Test wallet-did_wallet code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/did_wallet/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/did_wallet/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-wallet-rl_wallet.yml b/.github/workflows/build-test-macos-wallet-rl_wallet.yml index 519bfd5a9968..21eb833471b0 100644 --- a/.github/workflows/build-test-macos-wallet-rl_wallet.yml +++ b/.github/workflows/build-test-macos-wallet-rl_wallet.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet-rl_wallet env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -85,7 +87,23 @@ jobs: - name: Test wallet-rl_wallet code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/rl_wallet/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/rl_wallet/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-wallet-rpc.yml b/.github/workflows/build-test-macos-wallet-rpc.yml index 9a811c42b1e6..eb7dae2d3037 100644 --- a/.github/workflows/build-test-macos-wallet-rpc.yml +++ b/.github/workflows/build-test-macos-wallet-rpc.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet-rpc env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -85,7 +87,23 @@ jobs: - name: Test wallet-rpc code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/rpc/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/rpc/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-wallet-simple_sync.yml b/.github/workflows/build-test-macos-wallet-simple_sync.yml index 71cbecc57090..aa1168533c91 100644 --- a/.github/workflows/build-test-macos-wallet-simple_sync.yml +++ b/.github/workflows/build-test-macos-wallet-simple_sync.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet-simple_sync env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -85,7 +87,23 @@ jobs: - name: Test wallet-simple_sync code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/simple_sync/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/simple_sync/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-wallet-sync.yml b/.github/workflows/build-test-macos-wallet-sync.yml index 835d1e1b3ce7..13ad462b3a51 100644 --- a/.github/workflows/build-test-macos-wallet-sync.yml +++ b/.github/workflows/build-test-macos-wallet-sync.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet-sync env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -85,7 +87,23 @@ jobs: - name: Test wallet-sync code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/sync/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/sync/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-wallet.yml b/.github/workflows/build-test-macos-wallet.yml index 28ddfb38c2cc..30a91af2a263 100644 --- a/.github/workflows/build-test-macos-wallet.yml +++ b/.github/workflows/build-test-macos-wallet.yml @@ -22,13 +22,15 @@ jobs: build: name: MacOS wallet Tests runs-on: ${{ matrix.os }} - timeout-minutes: 30 + timeout-minutes: 40 strategy: fail-fast: false max-parallel: 4 matrix: python-version: [3.8, 3.9] os: [macOS-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -85,7 +87,23 @@ jobs: - name: Test wallet code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-macos-weight_proof.yml b/.github/workflows/build-test-macos-weight_proof.yml index 8739ef165bb7..c4bce4aaa37d 100644 --- a/.github/workflows/build-test-macos-weight_proof.yml +++ b/.github/workflows/build-test-macos-weight_proof.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_weight_proof env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -85,7 +87,23 @@ jobs: - name: Test weight_proof code with pytest run: | . ./activate - ./venv/bin/py.test tests/weight_proof/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/weight_proof/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/.github/workflows/build-test-ubuntu-blockchain.yml b/.github/workflows/build-test-ubuntu-blockchain.yml index 7c4c1ed39698..4beb1112b7ad 100644 --- a/.github/workflows/build-test-ubuntu-blockchain.yml +++ b/.github/workflows/build-test-ubuntu-blockchain.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_blockchain env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -84,7 +86,23 @@ jobs: - name: Test blockchain code with pytest run: | . ./activate - ./venv/bin/py.test tests/blockchain/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/blockchain/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # Omitted resource usage check diff --git a/.github/workflows/build-test-ubuntu-clvm.yml b/.github/workflows/build-test-ubuntu-clvm.yml index 028af0f9031f..a593d7ad72f4 100644 --- a/.github/workflows/build-test-ubuntu-clvm.yml +++ b/.github/workflows/build-test-ubuntu-clvm.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_clvm env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -78,7 +80,23 @@ jobs: - name: Test clvm code with pytest run: | . ./activate - ./venv/bin/py.test tests/clvm/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/clvm/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # Omitted resource usage check diff --git a/.github/workflows/build-test-ubuntu-core-cmds.yml b/.github/workflows/build-test-ubuntu-core-cmds.yml index 2afd49c477a8..fa0b2f7027ff 100644 --- a/.github/workflows/build-test-ubuntu-core-cmds.yml +++ b/.github/workflows/build-test-ubuntu-core-cmds.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-cmds env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -84,7 +86,23 @@ jobs: - name: Test core-cmds code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/cmds/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/cmds/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # Omitted resource usage check diff --git a/.github/workflows/build-test-ubuntu-core-consensus.yml b/.github/workflows/build-test-ubuntu-core-consensus.yml index 5d133523a3e7..5e97ae6a51e8 100644 --- a/.github/workflows/build-test-ubuntu-core-consensus.yml +++ b/.github/workflows/build-test-ubuntu-core-consensus.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-consensus env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -84,7 +86,23 @@ jobs: - name: Test core-consensus code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/consensus/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/consensus/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # Omitted resource usage check diff --git a/.github/workflows/build-test-ubuntu-core-custom_types.yml b/.github/workflows/build-test-ubuntu-core-custom_types.yml index 0d5d3f8b1ea7..c5daa75b52b0 100644 --- a/.github/workflows/build-test-ubuntu-core-custom_types.yml +++ b/.github/workflows/build-test-ubuntu-core-custom_types.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-custom_types env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -84,7 +86,23 @@ jobs: - name: Test core-custom_types code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/custom_types/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/custom_types/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # Omitted resource usage check diff --git a/.github/workflows/build-test-ubuntu-core-daemon.yml b/.github/workflows/build-test-ubuntu-core-daemon.yml index 5efbea182ee2..fa6fca7cd8dd 100644 --- a/.github/workflows/build-test-ubuntu-core-daemon.yml +++ b/.github/workflows/build-test-ubuntu-core-daemon.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-daemon env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -96,7 +98,23 @@ jobs: - name: Test core-daemon code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/daemon/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/daemon/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # Omitted resource usage check diff --git a/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml b/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml index 189ece884e35..bf7751215beb 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-full_node-full_sync env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -84,7 +86,23 @@ jobs: - name: Test core-full_node-full_sync code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/full_node/full_sync/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/full_node/full_sync/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # Omitted resource usage check diff --git a/.github/workflows/build-test-ubuntu-core-full_node-stores.yml b/.github/workflows/build-test-ubuntu-core-full_node-stores.yml index 27da46589643..ba6c1c11ffd0 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node-stores.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node-stores.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-full_node-stores env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -84,7 +86,23 @@ jobs: - name: Test core-full_node-stores code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/full_node/stores/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/full_node/stores/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error - name: Check resource usage run: | diff --git a/.github/workflows/build-test-ubuntu-core-full_node.yml b/.github/workflows/build-test-ubuntu-core-full_node.yml index 2c722cb6c7cb..bccbd5c2d933 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-full_node env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -84,7 +86,23 @@ jobs: - name: Test core-full_node code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/full_node/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/full_node/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error - name: Check resource usage run: | diff --git a/.github/workflows/build-test-ubuntu-core-server.yml b/.github/workflows/build-test-ubuntu-core-server.yml index 293f1bc8d8bd..38b6affa4638 100644 --- a/.github/workflows/build-test-ubuntu-core-server.yml +++ b/.github/workflows/build-test-ubuntu-core-server.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-server env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -84,7 +86,23 @@ jobs: - name: Test core-server code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/server/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/server/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # Omitted resource usage check diff --git a/.github/workflows/build-test-ubuntu-core-ssl.yml b/.github/workflows/build-test-ubuntu-core-ssl.yml index 157f3d9f7c53..d891c750c8b4 100644 --- a/.github/workflows/build-test-ubuntu-core-ssl.yml +++ b/.github/workflows/build-test-ubuntu-core-ssl.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-ssl env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -84,7 +86,23 @@ jobs: - name: Test core-ssl code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/ssl/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/ssl/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # Omitted resource usage check diff --git a/.github/workflows/build-test-ubuntu-core-util.yml b/.github/workflows/build-test-ubuntu-core-util.yml index 585ed817033a..7a834e0c5182 100644 --- a/.github/workflows/build-test-ubuntu-core-util.yml +++ b/.github/workflows/build-test-ubuntu-core-util.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-util env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -84,7 +86,23 @@ jobs: - name: Test core-util code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/util/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/util/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # Omitted resource usage check diff --git a/.github/workflows/build-test-ubuntu-core.yml b/.github/workflows/build-test-ubuntu-core.yml index a1bc0e59ff05..6a4a7619960c 100644 --- a/.github/workflows/build-test-ubuntu-core.yml +++ b/.github/workflows/build-test-ubuntu-core.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -84,7 +86,23 @@ jobs: - name: Test core code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # Omitted resource usage check diff --git a/.github/workflows/build-test-ubuntu-farmer_harvester.yml b/.github/workflows/build-test-ubuntu-farmer_harvester.yml index 3f19f228a032..512d448de7c4 100644 --- a/.github/workflows/build-test-ubuntu-farmer_harvester.yml +++ b/.github/workflows/build-test-ubuntu-farmer_harvester.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_farmer_harvester env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -84,7 +86,23 @@ jobs: - name: Test farmer_harvester code with pytest run: | . ./activate - ./venv/bin/py.test tests/farmer_harvester/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/farmer_harvester/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # Omitted resource usage check diff --git a/.github/workflows/build-test-ubuntu-generator.yml b/.github/workflows/build-test-ubuntu-generator.yml index 69fdbe1401ec..b70f4b37f7c0 100644 --- a/.github/workflows/build-test-ubuntu-generator.yml +++ b/.github/workflows/build-test-ubuntu-generator.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_generator env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -84,7 +86,23 @@ jobs: - name: Test generator code with pytest run: | . ./activate - ./venv/bin/py.test tests/generator/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/generator/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # Omitted resource usage check diff --git a/.github/workflows/build-test-ubuntu-plotting.yml b/.github/workflows/build-test-ubuntu-plotting.yml index 5db8bfd40e88..fd8911cfc44f 100644 --- a/.github/workflows/build-test-ubuntu-plotting.yml +++ b/.github/workflows/build-test-ubuntu-plotting.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_plotting env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -84,7 +86,23 @@ jobs: - name: Test plotting code with pytest run: | . ./activate - ./venv/bin/py.test tests/plotting/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/plotting/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # Omitted resource usage check diff --git a/.github/workflows/build-test-ubuntu-pools.yml b/.github/workflows/build-test-ubuntu-pools.yml index 3d2d78bdb9ff..4991c4786414 100644 --- a/.github/workflows/build-test-ubuntu-pools.yml +++ b/.github/workflows/build-test-ubuntu-pools.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_pools env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -84,7 +86,23 @@ jobs: - name: Test pools code with pytest run: | . ./activate - ./venv/bin/py.test tests/pools/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/pools/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # Omitted resource usage check diff --git a/.github/workflows/build-test-ubuntu-simulation.yml b/.github/workflows/build-test-ubuntu-simulation.yml index 354145da0ef3..7ad32c794d0d 100644 --- a/.github/workflows/build-test-ubuntu-simulation.yml +++ b/.github/workflows/build-test-ubuntu-simulation.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_simulation env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -96,7 +98,23 @@ jobs: - name: Test simulation code with pytest run: | . ./activate - ./venv/bin/py.test tests/simulation/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/simulation/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # Omitted resource usage check diff --git a/.github/workflows/build-test-ubuntu-tools.yml b/.github/workflows/build-test-ubuntu-tools.yml index 5d58a6499c9a..b0ac226bf836 100644 --- a/.github/workflows/build-test-ubuntu-tools.yml +++ b/.github/workflows/build-test-ubuntu-tools.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_tools env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -84,7 +86,23 @@ jobs: - name: Test tools code with pytest run: | . ./activate - ./venv/bin/py.test tests/tools/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/tools/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # Omitted resource usage check diff --git a/.github/workflows/build-test-ubuntu-util.yml b/.github/workflows/build-test-ubuntu-util.yml index f0c0c7ef7fa4..78570ab4cc54 100644 --- a/.github/workflows/build-test-ubuntu-util.yml +++ b/.github/workflows/build-test-ubuntu-util.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_util env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -84,7 +86,23 @@ jobs: - name: Test util code with pytest run: | . ./activate - ./venv/bin/py.test tests/util/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/util/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # Omitted resource usage check diff --git a/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml index 0dac4ebdeea2..b9502d357d31 100644 --- a/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet-cat_wallet env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -84,7 +86,23 @@ jobs: - name: Test wallet-cat_wallet code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/cat_wallet/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/cat_wallet/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # Omitted resource usage check diff --git a/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml index 33d0efec8df1..1f115d79e732 100644 --- a/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet-did_wallet env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -84,7 +86,23 @@ jobs: - name: Test wallet-did_wallet code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/did_wallet/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/did_wallet/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # Omitted resource usage check diff --git a/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml index 71c71b5ebff2..3833bc9c48f3 100644 --- a/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet-rl_wallet env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -84,7 +86,23 @@ jobs: - name: Test wallet-rl_wallet code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/rl_wallet/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/rl_wallet/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # Omitted resource usage check diff --git a/.github/workflows/build-test-ubuntu-wallet-rpc.yml b/.github/workflows/build-test-ubuntu-wallet-rpc.yml index 0e59373f2eb4..927174b7fa1b 100644 --- a/.github/workflows/build-test-ubuntu-wallet-rpc.yml +++ b/.github/workflows/build-test-ubuntu-wallet-rpc.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet-rpc env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -84,7 +86,23 @@ jobs: - name: Test wallet-rpc code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/rpc/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/rpc/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # Omitted resource usage check diff --git a/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml b/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml index 29b100bb4b32..e2e64e55cfa9 100644 --- a/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml +++ b/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet-simple_sync env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -84,7 +86,23 @@ jobs: - name: Test wallet-simple_sync code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/simple_sync/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/simple_sync/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # Omitted resource usage check diff --git a/.github/workflows/build-test-ubuntu-wallet-sync.yml b/.github/workflows/build-test-ubuntu-wallet-sync.yml index faf60238d8db..4b1dd983c64b 100644 --- a/.github/workflows/build-test-ubuntu-wallet-sync.yml +++ b/.github/workflows/build-test-ubuntu-wallet-sync.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet-sync env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -84,7 +86,23 @@ jobs: - name: Test wallet-sync code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/sync/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/sync/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # Omitted resource usage check diff --git a/.github/workflows/build-test-ubuntu-wallet.yml b/.github/workflows/build-test-ubuntu-wallet.yml index d828f5ed7a7d..abad7c4ab7cc 100644 --- a/.github/workflows/build-test-ubuntu-wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet.yml @@ -22,13 +22,15 @@ jobs: build: name: Ubuntu wallet Test runs-on: ${{ matrix.os }} - timeout-minutes: 30 + timeout-minutes: 40 strategy: fail-fast: false max-parallel: 4 matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -84,7 +86,23 @@ jobs: - name: Test wallet code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # Omitted resource usage check diff --git a/.github/workflows/build-test-ubuntu-weight_proof.yml b/.github/workflows/build-test-ubuntu-weight_proof.yml index 207b284eeef0..ca47d7d5c83c 100644 --- a/.github/workflows/build-test-ubuntu-weight_proof.yml +++ b/.github/workflows/build-test-ubuntu-weight_proof.yml @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_weight_proof env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -84,7 +86,23 @@ jobs: - name: Test weight_proof code with pytest run: | . ./activate - ./venv/bin/py.test tests/weight_proof/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/weight_proof/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # Omitted resource usage check diff --git a/setup.py b/setup.py index b279c9a7efa4..2c55afe27838 100644 --- a/setup.py +++ b/setup.py @@ -44,6 +44,7 @@ dev_dependencies = [ "build", + "coverage", "pre-commit", "pytest", "pytest-asyncio>=0.18.1", # require attribute 'fixture' diff --git a/tests/runner_templates/build-test-macos b/tests/runner_templates/build-test-macos index e12855b8d18c..458f95ed4138 100644 --- a/tests/runner_templates/build-test-macos +++ b/tests/runner_templates/build-test-macos @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_TEST_NAME env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -79,7 +81,23 @@ INSTALL_TIMELORD - name: Test TEST_NAME code with pytest run: | . ./activate - ./venv/bin/py.test TEST_DIR -s -v --durations 0PYTEST_PARALLEL_ARGS -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test TEST_DIR -s -v --durations 0PYTEST_PARALLEL_ARGS -m "not benchmark" + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error # # THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme # diff --git a/tests/runner_templates/build-test-ubuntu b/tests/runner_templates/build-test-ubuntu index 893fd6678b1e..4bcfff29947b 100644 --- a/tests/runner_templates/build-test-ubuntu +++ b/tests/runner_templates/build-test-ubuntu @@ -29,6 +29,8 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] + env: + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_TEST_NAME env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet @@ -78,7 +80,23 @@ INSTALL_TIMELORD - name: Test TEST_NAME code with pytest run: | . ./activate - ./venv/bin/py.test TEST_DIR -s -v --durations 0PYTEST_PARALLEL_ARGS -m "not benchmark" DISABLE_PYTEST_MONITOR + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test TEST_DIR -s -v --durations 0PYTEST_PARALLEL_ARGS -m "not benchmark" DISABLE_PYTEST_MONITOR + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error CHECK_RESOURCE_USAGE diff --git a/tests/wallet/cat_wallet/config.py b/tests/wallet/cat_wallet/config.py index a805fb7b880c..eb21fe13cd3b 100644 --- a/tests/wallet/cat_wallet/config.py +++ b/tests/wallet/cat_wallet/config.py @@ -1 +1,2 @@ +# flake8: noqa: E501 job_timeout = 50 diff --git a/tests/wallet/config.py b/tests/wallet/config.py index 7f9e1e1a76e4..e90fb48eaaee 100644 --- a/tests/wallet/config.py +++ b/tests/wallet/config.py @@ -1 +1,3 @@ +# flake8: noqa: E501 +job_timeout = 40 parallel = True From fda14b5b47c82c8d999946244c33732ea1c395b2 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Mon, 28 Mar 2022 22:49:22 -0400 Subject: [PATCH 280/378] restrict click to < 8.1 for black (#10923) https://github.com/pallets/click/issues/2225 Doing this instead of updating since updating black will change several files due to some formatting change. I would like to take that on separately from unbreaking CI. --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 147715627fde..eb6206b15fd9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -54,6 +54,7 @@ repos: rev: 21.12b0 hooks: - id: black + additional_dependencies: ['click<8.1'] - repo: https://gitlab.com/pycqa/flake8 rev: 3.9.2 hooks: From fb68bebfc3d99b8f3a1f8f3b6c880ec527502e42 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Mon, 28 Mar 2022 22:50:42 -0400 Subject: [PATCH 281/378] fixup workflow template merge env duplication (#10925) --- .github/workflows/build-test-macos-blockchain.yml | 4 +--- .github/workflows/build-test-macos-clvm.yml | 4 +--- .github/workflows/build-test-macos-core-cmds.yml | 4 +--- .github/workflows/build-test-macos-core-consensus.yml | 4 +--- .github/workflows/build-test-macos-core-custom_types.yml | 4 +--- .github/workflows/build-test-macos-core-daemon.yml | 4 +--- .../workflows/build-test-macos-core-full_node-full_sync.yml | 4 +--- .github/workflows/build-test-macos-core-full_node-stores.yml | 4 +--- .github/workflows/build-test-macos-core-full_node.yml | 4 +--- .github/workflows/build-test-macos-core-server.yml | 4 +--- .github/workflows/build-test-macos-core-ssl.yml | 4 +--- .github/workflows/build-test-macos-core-util.yml | 4 +--- .github/workflows/build-test-macos-core.yml | 4 +--- .github/workflows/build-test-macos-farmer_harvester.yml | 4 +--- .github/workflows/build-test-macos-generator.yml | 4 +--- .github/workflows/build-test-macos-plotting.yml | 4 +--- .github/workflows/build-test-macos-pools.yml | 4 +--- .github/workflows/build-test-macos-simulation.yml | 4 +--- .github/workflows/build-test-macos-tools.yml | 4 +--- .github/workflows/build-test-macos-util.yml | 4 +--- .github/workflows/build-test-macos-wallet-cat_wallet.yml | 4 +--- .github/workflows/build-test-macos-wallet-did_wallet.yml | 4 +--- .github/workflows/build-test-macos-wallet-rl_wallet.yml | 4 +--- .github/workflows/build-test-macos-wallet-rpc.yml | 4 +--- .github/workflows/build-test-macos-wallet-simple_sync.yml | 4 +--- .github/workflows/build-test-macos-wallet-sync.yml | 4 +--- .github/workflows/build-test-macos-wallet.yml | 4 +--- .github/workflows/build-test-macos-weight_proof.yml | 4 +--- .github/workflows/build-test-ubuntu-blockchain.yml | 4 +--- .github/workflows/build-test-ubuntu-clvm.yml | 4 +--- .github/workflows/build-test-ubuntu-core-cmds.yml | 4 +--- .github/workflows/build-test-ubuntu-core-consensus.yml | 4 +--- .github/workflows/build-test-ubuntu-core-custom_types.yml | 4 +--- .github/workflows/build-test-ubuntu-core-daemon.yml | 4 +--- .../workflows/build-test-ubuntu-core-full_node-full_sync.yml | 4 +--- .github/workflows/build-test-ubuntu-core-full_node-stores.yml | 4 +--- .github/workflows/build-test-ubuntu-core-full_node.yml | 4 +--- .github/workflows/build-test-ubuntu-core-server.yml | 4 +--- .github/workflows/build-test-ubuntu-core-ssl.yml | 4 +--- .github/workflows/build-test-ubuntu-core-util.yml | 4 +--- .github/workflows/build-test-ubuntu-core.yml | 4 +--- .github/workflows/build-test-ubuntu-farmer_harvester.yml | 4 +--- .github/workflows/build-test-ubuntu-generator.yml | 4 +--- .github/workflows/build-test-ubuntu-plotting.yml | 4 +--- .github/workflows/build-test-ubuntu-pools.yml | 4 +--- .github/workflows/build-test-ubuntu-simulation.yml | 4 +--- .github/workflows/build-test-ubuntu-tools.yml | 4 +--- .github/workflows/build-test-ubuntu-util.yml | 4 +--- .github/workflows/build-test-ubuntu-wallet-cat_wallet.yml | 4 +--- .github/workflows/build-test-ubuntu-wallet-did_wallet.yml | 4 +--- .github/workflows/build-test-ubuntu-wallet-rl_wallet.yml | 4 +--- .github/workflows/build-test-ubuntu-wallet-rpc.yml | 4 +--- .github/workflows/build-test-ubuntu-wallet-simple_sync.yml | 4 +--- .github/workflows/build-test-ubuntu-wallet-sync.yml | 4 +--- .github/workflows/build-test-ubuntu-wallet.yml | 4 +--- .github/workflows/build-test-ubuntu-weight_proof.yml | 4 +--- tests/runner_templates/build-test-macos | 4 +--- tests/runner_templates/build-test-ubuntu | 4 +--- 58 files changed, 58 insertions(+), 174 deletions(-) diff --git a/.github/workflows/build-test-macos-blockchain.yml b/.github/workflows/build-test-macos-blockchain.yml index b8bda1f29cf2..5cbbac4aa598 100644 --- a/.github/workflows/build-test-macos-blockchain.yml +++ b/.github/workflows/build-test-macos-blockchain.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_blockchain - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_blockchain steps: - name: Checkout Code diff --git a/.github/workflows/build-test-macos-clvm.yml b/.github/workflows/build-test-macos-clvm.yml index 483fe50a6678..c038440ec818 100644 --- a/.github/workflows/build-test-macos-clvm.yml +++ b/.github/workflows/build-test-macos-clvm.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_clvm - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_clvm steps: - name: Checkout Code diff --git a/.github/workflows/build-test-macos-core-cmds.yml b/.github/workflows/build-test-macos-core-cmds.yml index 1cd1557853a7..2060a7821603 100644 --- a/.github/workflows/build-test-macos-core-cmds.yml +++ b/.github/workflows/build-test-macos-core-cmds.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-cmds - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-cmds steps: - name: Checkout Code diff --git a/.github/workflows/build-test-macos-core-consensus.yml b/.github/workflows/build-test-macos-core-consensus.yml index 85f4b2e0323b..ab1390a0c1c4 100644 --- a/.github/workflows/build-test-macos-core-consensus.yml +++ b/.github/workflows/build-test-macos-core-consensus.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-consensus - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-consensus steps: - name: Checkout Code diff --git a/.github/workflows/build-test-macos-core-custom_types.yml b/.github/workflows/build-test-macos-core-custom_types.yml index 2902458e4dc2..b5b1355f8a43 100644 --- a/.github/workflows/build-test-macos-core-custom_types.yml +++ b/.github/workflows/build-test-macos-core-custom_types.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-custom_types - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-custom_types steps: - name: Checkout Code diff --git a/.github/workflows/build-test-macos-core-daemon.yml b/.github/workflows/build-test-macos-core-daemon.yml index 4a2d05d06681..d4e29bfb11ac 100644 --- a/.github/workflows/build-test-macos-core-daemon.yml +++ b/.github/workflows/build-test-macos-core-daemon.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-daemon - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-daemon steps: - name: Checkout Code diff --git a/.github/workflows/build-test-macos-core-full_node-full_sync.yml b/.github/workflows/build-test-macos-core-full_node-full_sync.yml index f0584dc5cbea..74649df60444 100644 --- a/.github/workflows/build-test-macos-core-full_node-full_sync.yml +++ b/.github/workflows/build-test-macos-core-full_node-full_sync.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-full_node-full_sync - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-full_node-full_sync steps: - name: Checkout Code diff --git a/.github/workflows/build-test-macos-core-full_node-stores.yml b/.github/workflows/build-test-macos-core-full_node-stores.yml index 32900f68bc9c..ec7598f98718 100644 --- a/.github/workflows/build-test-macos-core-full_node-stores.yml +++ b/.github/workflows/build-test-macos-core-full_node-stores.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-full_node-stores - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-full_node-stores steps: - name: Checkout Code diff --git a/.github/workflows/build-test-macos-core-full_node.yml b/.github/workflows/build-test-macos-core-full_node.yml index 59f24ca4c244..518abcd2885c 100644 --- a/.github/workflows/build-test-macos-core-full_node.yml +++ b/.github/workflows/build-test-macos-core-full_node.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-full_node - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-full_node steps: - name: Checkout Code diff --git a/.github/workflows/build-test-macos-core-server.yml b/.github/workflows/build-test-macos-core-server.yml index 54076aaf7f35..d8b860df1c60 100644 --- a/.github/workflows/build-test-macos-core-server.yml +++ b/.github/workflows/build-test-macos-core-server.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-server - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-server steps: - name: Checkout Code diff --git a/.github/workflows/build-test-macos-core-ssl.yml b/.github/workflows/build-test-macos-core-ssl.yml index aa91b12ea9fd..b40750dfb8a7 100644 --- a/.github/workflows/build-test-macos-core-ssl.yml +++ b/.github/workflows/build-test-macos-core-ssl.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-ssl - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-ssl steps: - name: Checkout Code diff --git a/.github/workflows/build-test-macos-core-util.yml b/.github/workflows/build-test-macos-core-util.yml index 404fb7f4873f..5b98525573d7 100644 --- a/.github/workflows/build-test-macos-core-util.yml +++ b/.github/workflows/build-test-macos-core-util.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-util - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-util steps: - name: Checkout Code diff --git a/.github/workflows/build-test-macos-core.yml b/.github/workflows/build-test-macos-core.yml index 13473b76e115..dd171dafb4df 100644 --- a/.github/workflows/build-test-macos-core.yml +++ b/.github/workflows/build-test-macos-core.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core steps: - name: Checkout Code diff --git a/.github/workflows/build-test-macos-farmer_harvester.yml b/.github/workflows/build-test-macos-farmer_harvester.yml index 9a2c21f83101..dd9d6e7cef06 100644 --- a/.github/workflows/build-test-macos-farmer_harvester.yml +++ b/.github/workflows/build-test-macos-farmer_harvester.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_farmer_harvester - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_farmer_harvester steps: - name: Checkout Code diff --git a/.github/workflows/build-test-macos-generator.yml b/.github/workflows/build-test-macos-generator.yml index 9e71599c3411..efe583420f9a 100644 --- a/.github/workflows/build-test-macos-generator.yml +++ b/.github/workflows/build-test-macos-generator.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_generator - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_generator steps: - name: Checkout Code diff --git a/.github/workflows/build-test-macos-plotting.yml b/.github/workflows/build-test-macos-plotting.yml index 236c1ab6be19..46500a11c662 100644 --- a/.github/workflows/build-test-macos-plotting.yml +++ b/.github/workflows/build-test-macos-plotting.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_plotting - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_plotting steps: - name: Checkout Code diff --git a/.github/workflows/build-test-macos-pools.yml b/.github/workflows/build-test-macos-pools.yml index 0c25fd68dab1..ac449d50ff85 100644 --- a/.github/workflows/build-test-macos-pools.yml +++ b/.github/workflows/build-test-macos-pools.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_pools - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_pools steps: - name: Checkout Code diff --git a/.github/workflows/build-test-macos-simulation.yml b/.github/workflows/build-test-macos-simulation.yml index b1d07590dd62..a7638f59b188 100644 --- a/.github/workflows/build-test-macos-simulation.yml +++ b/.github/workflows/build-test-macos-simulation.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_simulation - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_simulation steps: - name: Checkout Code diff --git a/.github/workflows/build-test-macos-tools.yml b/.github/workflows/build-test-macos-tools.yml index f35f5b8bc29b..a2a6dc99f8d6 100644 --- a/.github/workflows/build-test-macos-tools.yml +++ b/.github/workflows/build-test-macos-tools.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_tools - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_tools steps: - name: Checkout Code diff --git a/.github/workflows/build-test-macos-util.yml b/.github/workflows/build-test-macos-util.yml index 1000fa3b3bfa..1945eac5a0f4 100644 --- a/.github/workflows/build-test-macos-util.yml +++ b/.github/workflows/build-test-macos-util.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_util - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_util steps: - name: Checkout Code diff --git a/.github/workflows/build-test-macos-wallet-cat_wallet.yml b/.github/workflows/build-test-macos-wallet-cat_wallet.yml index acfe17e6b4ba..887ff1ce5e0e 100644 --- a/.github/workflows/build-test-macos-wallet-cat_wallet.yml +++ b/.github/workflows/build-test-macos-wallet-cat_wallet.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet-cat_wallet - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet-cat_wallet steps: - name: Checkout Code diff --git a/.github/workflows/build-test-macos-wallet-did_wallet.yml b/.github/workflows/build-test-macos-wallet-did_wallet.yml index 342fcbcd6d04..1e721d18a0f6 100644 --- a/.github/workflows/build-test-macos-wallet-did_wallet.yml +++ b/.github/workflows/build-test-macos-wallet-did_wallet.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet-did_wallet - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet-did_wallet steps: - name: Checkout Code diff --git a/.github/workflows/build-test-macos-wallet-rl_wallet.yml b/.github/workflows/build-test-macos-wallet-rl_wallet.yml index 21eb833471b0..bfe48cda34aa 100644 --- a/.github/workflows/build-test-macos-wallet-rl_wallet.yml +++ b/.github/workflows/build-test-macos-wallet-rl_wallet.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet-rl_wallet - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet-rl_wallet steps: - name: Checkout Code diff --git a/.github/workflows/build-test-macos-wallet-rpc.yml b/.github/workflows/build-test-macos-wallet-rpc.yml index eb7dae2d3037..07f461874e2c 100644 --- a/.github/workflows/build-test-macos-wallet-rpc.yml +++ b/.github/workflows/build-test-macos-wallet-rpc.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet-rpc - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet-rpc steps: - name: Checkout Code diff --git a/.github/workflows/build-test-macos-wallet-simple_sync.yml b/.github/workflows/build-test-macos-wallet-simple_sync.yml index aa1168533c91..6c4436da04d5 100644 --- a/.github/workflows/build-test-macos-wallet-simple_sync.yml +++ b/.github/workflows/build-test-macos-wallet-simple_sync.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet-simple_sync - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet-simple_sync steps: - name: Checkout Code diff --git a/.github/workflows/build-test-macos-wallet-sync.yml b/.github/workflows/build-test-macos-wallet-sync.yml index 13ad462b3a51..62012d1243d9 100644 --- a/.github/workflows/build-test-macos-wallet-sync.yml +++ b/.github/workflows/build-test-macos-wallet-sync.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet-sync - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet-sync steps: - name: Checkout Code diff --git a/.github/workflows/build-test-macos-wallet.yml b/.github/workflows/build-test-macos-wallet.yml index 30a91af2a263..d35d8527f189 100644 --- a/.github/workflows/build-test-macos-wallet.yml +++ b/.github/workflows/build-test-macos-wallet.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet steps: - name: Checkout Code diff --git a/.github/workflows/build-test-macos-weight_proof.yml b/.github/workflows/build-test-macos-weight_proof.yml index c4bce4aaa37d..2cb097479867 100644 --- a/.github/workflows/build-test-macos-weight_proof.yml +++ b/.github/workflows/build-test-macos-weight_proof.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_weight_proof - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_weight_proof steps: - name: Checkout Code diff --git a/.github/workflows/build-test-ubuntu-blockchain.yml b/.github/workflows/build-test-ubuntu-blockchain.yml index 4beb1112b7ad..ab8e3792afb6 100644 --- a/.github/workflows/build-test-ubuntu-blockchain.yml +++ b/.github/workflows/build-test-ubuntu-blockchain.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_blockchain - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_blockchain steps: - name: Checkout Code diff --git a/.github/workflows/build-test-ubuntu-clvm.yml b/.github/workflows/build-test-ubuntu-clvm.yml index a593d7ad72f4..7746a4224053 100644 --- a/.github/workflows/build-test-ubuntu-clvm.yml +++ b/.github/workflows/build-test-ubuntu-clvm.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_clvm - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_clvm steps: - name: Checkout Code diff --git a/.github/workflows/build-test-ubuntu-core-cmds.yml b/.github/workflows/build-test-ubuntu-core-cmds.yml index fa0b2f7027ff..7a20b1a60dfd 100644 --- a/.github/workflows/build-test-ubuntu-core-cmds.yml +++ b/.github/workflows/build-test-ubuntu-core-cmds.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-cmds - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-cmds steps: - name: Checkout Code diff --git a/.github/workflows/build-test-ubuntu-core-consensus.yml b/.github/workflows/build-test-ubuntu-core-consensus.yml index 5e97ae6a51e8..ef302ab4c636 100644 --- a/.github/workflows/build-test-ubuntu-core-consensus.yml +++ b/.github/workflows/build-test-ubuntu-core-consensus.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-consensus - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-consensus steps: - name: Checkout Code diff --git a/.github/workflows/build-test-ubuntu-core-custom_types.yml b/.github/workflows/build-test-ubuntu-core-custom_types.yml index c5daa75b52b0..75d1372afd57 100644 --- a/.github/workflows/build-test-ubuntu-core-custom_types.yml +++ b/.github/workflows/build-test-ubuntu-core-custom_types.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-custom_types - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-custom_types steps: - name: Checkout Code diff --git a/.github/workflows/build-test-ubuntu-core-daemon.yml b/.github/workflows/build-test-ubuntu-core-daemon.yml index fa6fca7cd8dd..a04ecba0a95c 100644 --- a/.github/workflows/build-test-ubuntu-core-daemon.yml +++ b/.github/workflows/build-test-ubuntu-core-daemon.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-daemon - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-daemon steps: - name: Checkout Code diff --git a/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml b/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml index bf7751215beb..9c48f4af0640 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-full_node-full_sync - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-full_node-full_sync steps: - name: Checkout Code diff --git a/.github/workflows/build-test-ubuntu-core-full_node-stores.yml b/.github/workflows/build-test-ubuntu-core-full_node-stores.yml index ba6c1c11ffd0..34640f47778e 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node-stores.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node-stores.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-full_node-stores - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-full_node-stores steps: - name: Checkout Code diff --git a/.github/workflows/build-test-ubuntu-core-full_node.yml b/.github/workflows/build-test-ubuntu-core-full_node.yml index bccbd5c2d933..0aff8bf7aacb 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-full_node - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-full_node steps: - name: Checkout Code diff --git a/.github/workflows/build-test-ubuntu-core-server.yml b/.github/workflows/build-test-ubuntu-core-server.yml index 38b6affa4638..adbdc95719d2 100644 --- a/.github/workflows/build-test-ubuntu-core-server.yml +++ b/.github/workflows/build-test-ubuntu-core-server.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-server - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-server steps: - name: Checkout Code diff --git a/.github/workflows/build-test-ubuntu-core-ssl.yml b/.github/workflows/build-test-ubuntu-core-ssl.yml index d891c750c8b4..bf62b5c2c399 100644 --- a/.github/workflows/build-test-ubuntu-core-ssl.yml +++ b/.github/workflows/build-test-ubuntu-core-ssl.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-ssl - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-ssl steps: - name: Checkout Code diff --git a/.github/workflows/build-test-ubuntu-core-util.yml b/.github/workflows/build-test-ubuntu-core-util.yml index 7a834e0c5182..4c099f34d362 100644 --- a/.github/workflows/build-test-ubuntu-core-util.yml +++ b/.github/workflows/build-test-ubuntu-core-util.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-util - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core-util steps: - name: Checkout Code diff --git a/.github/workflows/build-test-ubuntu-core.yml b/.github/workflows/build-test-ubuntu-core.yml index 6a4a7619960c..be05a9a3f789 100644 --- a/.github/workflows/build-test-ubuntu-core.yml +++ b/.github/workflows/build-test-ubuntu-core.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_core steps: - name: Checkout Code diff --git a/.github/workflows/build-test-ubuntu-farmer_harvester.yml b/.github/workflows/build-test-ubuntu-farmer_harvester.yml index 512d448de7c4..e526cf84a748 100644 --- a/.github/workflows/build-test-ubuntu-farmer_harvester.yml +++ b/.github/workflows/build-test-ubuntu-farmer_harvester.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_farmer_harvester - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_farmer_harvester steps: - name: Checkout Code diff --git a/.github/workflows/build-test-ubuntu-generator.yml b/.github/workflows/build-test-ubuntu-generator.yml index b70f4b37f7c0..5dbd49056597 100644 --- a/.github/workflows/build-test-ubuntu-generator.yml +++ b/.github/workflows/build-test-ubuntu-generator.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_generator - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_generator steps: - name: Checkout Code diff --git a/.github/workflows/build-test-ubuntu-plotting.yml b/.github/workflows/build-test-ubuntu-plotting.yml index fd8911cfc44f..3208a1d9bd4a 100644 --- a/.github/workflows/build-test-ubuntu-plotting.yml +++ b/.github/workflows/build-test-ubuntu-plotting.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_plotting - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_plotting steps: - name: Checkout Code diff --git a/.github/workflows/build-test-ubuntu-pools.yml b/.github/workflows/build-test-ubuntu-pools.yml index 4991c4786414..7cf0a0197377 100644 --- a/.github/workflows/build-test-ubuntu-pools.yml +++ b/.github/workflows/build-test-ubuntu-pools.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_pools - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_pools steps: - name: Checkout Code diff --git a/.github/workflows/build-test-ubuntu-simulation.yml b/.github/workflows/build-test-ubuntu-simulation.yml index 7ad32c794d0d..e38742a9703d 100644 --- a/.github/workflows/build-test-ubuntu-simulation.yml +++ b/.github/workflows/build-test-ubuntu-simulation.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_simulation - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_simulation steps: - name: Checkout Code diff --git a/.github/workflows/build-test-ubuntu-tools.yml b/.github/workflows/build-test-ubuntu-tools.yml index b0ac226bf836..0afd52ad5572 100644 --- a/.github/workflows/build-test-ubuntu-tools.yml +++ b/.github/workflows/build-test-ubuntu-tools.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_tools - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_tools steps: - name: Checkout Code diff --git a/.github/workflows/build-test-ubuntu-util.yml b/.github/workflows/build-test-ubuntu-util.yml index 78570ab4cc54..ae66db1c1fe0 100644 --- a/.github/workflows/build-test-ubuntu-util.yml +++ b/.github/workflows/build-test-ubuntu-util.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_util - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_util steps: - name: Checkout Code diff --git a/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml index b9502d357d31..f2fd6d521dee 100644 --- a/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet-cat_wallet - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet-cat_wallet steps: - name: Checkout Code diff --git a/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml index 1f115d79e732..5403775a3071 100644 --- a/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet-did_wallet - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet-did_wallet steps: - name: Checkout Code diff --git a/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml index 3833bc9c48f3..3180a86eefe5 100644 --- a/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet-rl_wallet - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet-rl_wallet steps: - name: Checkout Code diff --git a/.github/workflows/build-test-ubuntu-wallet-rpc.yml b/.github/workflows/build-test-ubuntu-wallet-rpc.yml index 927174b7fa1b..1661b890921d 100644 --- a/.github/workflows/build-test-ubuntu-wallet-rpc.yml +++ b/.github/workflows/build-test-ubuntu-wallet-rpc.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet-rpc - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet-rpc steps: - name: Checkout Code diff --git a/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml b/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml index e2e64e55cfa9..1b863ad7bc72 100644 --- a/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml +++ b/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet-simple_sync - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet-simple_sync steps: - name: Checkout Code diff --git a/.github/workflows/build-test-ubuntu-wallet-sync.yml b/.github/workflows/build-test-ubuntu-wallet-sync.yml index 4b1dd983c64b..8700dcde52db 100644 --- a/.github/workflows/build-test-ubuntu-wallet-sync.yml +++ b/.github/workflows/build-test-ubuntu-wallet-sync.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet-sync - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet-sync steps: - name: Checkout Code diff --git a/.github/workflows/build-test-ubuntu-wallet.yml b/.github/workflows/build-test-ubuntu-wallet.yml index abad7c4ab7cc..fe1298353888 100644 --- a/.github/workflows/build-test-ubuntu-wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet steps: - name: Checkout Code diff --git a/.github/workflows/build-test-ubuntu-weight_proof.yml b/.github/workflows/build-test-ubuntu-weight_proof.yml index ca47d7d5c83c..b194db2ac346 100644 --- a/.github/workflows/build-test-ubuntu-weight_proof.yml +++ b/.github/workflows/build-test-ubuntu-weight_proof.yml @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_weight_proof - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_weight_proof steps: - name: Checkout Code diff --git a/tests/runner_templates/build-test-macos b/tests/runner_templates/build-test-macos index 458f95ed4138..e1e5b0beb2ba 100644 --- a/tests/runner_templates/build-test-macos +++ b/tests/runner_templates/build-test-macos @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.8, 3.9] os: [macOS-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_TEST_NAME - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_TEST_NAME steps: - name: Checkout Code diff --git a/tests/runner_templates/build-test-ubuntu b/tests/runner_templates/build-test-ubuntu index 4bcfff29947b..5583347709a1 100644 --- a/tests/runner_templates/build-test-ubuntu +++ b/tests/runner_templates/build-test-ubuntu @@ -29,11 +29,9 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] os: [ubuntu-latest] - env: - JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_TEST_NAME - env: CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_TEST_NAME steps: - name: Checkout Code From 94241078efd1539acf78890e7ac833ade80211b4 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Mon, 28 Mar 2022 22:50:53 -0400 Subject: [PATCH 282/378] ignore lack of hinting on clvm_tools.binutils.assemble() (#10926) --- chia/wallet/puzzles/prefarm/make_prefarm_ph.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/chia/wallet/puzzles/prefarm/make_prefarm_ph.py b/chia/wallet/puzzles/prefarm/make_prefarm_ph.py index 5d5ebf75cb9a..f393bc69c78b 100644 --- a/chia/wallet/puzzles/prefarm/make_prefarm_ph.py +++ b/chia/wallet/puzzles/prefarm/make_prefarm_ph.py @@ -26,7 +26,9 @@ def make_puzzle(amount: int) -> int: puzzle = f"(q . ((51 0x{ph1.hex()} {amount}) (51 0x{ph2.hex()} {amount})))" # print(puzzle) - puzzle_prog = Program.to(binutils.assemble(puzzle)) + # TODO: properly type hint clvm_tools + assembled_puzzle = binutils.assemble(puzzle) # type: ignore[no-untyped-call] + puzzle_prog = Program.to(assembled_puzzle) print("Program: ", puzzle_prog) puzzle_hash = puzzle_prog.get_tree_hash() From 94a4cb2e99928ef2e1ef17c176f237ea29478a47 Mon Sep 17 00:00:00 2001 From: Amine Khaldi Date: Tue, 29 Mar 2022 18:15:02 +0100 Subject: [PATCH 283/378] Contextualize some store test db names. (#10942) --- tests/pools/test_wallet_pool_store.py | 2 +- tests/wallet/test_wallet_blockchain.py | 2 +- tests/wallet/test_wallet_interested_store.py | 2 +- tests/wallet/test_wallet_key_val_store.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/pools/test_wallet_pool_store.py b/tests/pools/test_wallet_pool_store.py index a35e285933ee..8d5c43c7ae6e 100644 --- a/tests/pools/test_wallet_pool_store.py +++ b/tests/pools/test_wallet_pool_store.py @@ -38,7 +38,7 @@ def make_child_solution(coin_spend: CoinSpend, new_coin: Optional[Coin] = None) class TestWalletPoolStore: @pytest.mark.asyncio async def test_store(self): - db_filename = Path("wallet_store_test.db") + db_filename = Path("wallet_pool_store_test.db") if db_filename.exists(): db_filename.unlink() diff --git a/tests/wallet/test_wallet_blockchain.py b/tests/wallet/test_wallet_blockchain.py index f2dd424f09a6..7b5c1e3b3669 100644 --- a/tests/wallet/test_wallet_blockchain.py +++ b/tests/wallet/test_wallet_blockchain.py @@ -50,7 +50,7 @@ async def test_wallet_blockchain(self, wallet_node, default_1000_blocks): weight_proof_short: WeightProof = full_node_protocol.RespondProofOfWeight.from_bytes(res_2.data).wp weight_proof_long: WeightProof = full_node_protocol.RespondProofOfWeight.from_bytes(res_3.data).wp - db_filename = Path("wallet_store_test.db") + db_filename = Path("wallet_blockchain_store_test.db") if db_filename.exists(): db_filename.unlink() diff --git a/tests/wallet/test_wallet_interested_store.py b/tests/wallet/test_wallet_interested_store.py index 11b4b8bb0bcf..112ed558d4de 100644 --- a/tests/wallet/test_wallet_interested_store.py +++ b/tests/wallet/test_wallet_interested_store.py @@ -13,7 +13,7 @@ class TestWalletInterestedStore: @pytest.mark.asyncio async def test_store(self): - db_filename = Path("wallet_store_test.db") + db_filename = Path("wallet_interested_store_test.db") if db_filename.exists(): db_filename.unlink() diff --git a/tests/wallet/test_wallet_key_val_store.py b/tests/wallet/test_wallet_key_val_store.py index 9ae34e0e9314..4f688e0eda07 100644 --- a/tests/wallet/test_wallet_key_val_store.py +++ b/tests/wallet/test_wallet_key_val_store.py @@ -11,7 +11,7 @@ class TestWalletKeyValStore: @pytest.mark.asyncio async def test_store(self, bt): - db_filename = Path("wallet_store_test.db") + db_filename = Path("wallet_kv_store_test.db") if db_filename.exists(): db_filename.unlink() From 62c9670999b85b073716f136d08fdbf6b008e86a Mon Sep 17 00:00:00 2001 From: Adam Kelly <338792+aqk@users.noreply.github.com> Date: Tue, 29 Mar 2022 10:29:41 -0700 Subject: [PATCH 284/378] Type check values in RL Wallet (#10935) --- chia/wallet/rl_wallet/rl_wallet.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/chia/wallet/rl_wallet/rl_wallet.py b/chia/wallet/rl_wallet/rl_wallet.py index abfd7126f9b1..e51fe841c7e8 100644 --- a/chia/wallet/rl_wallet/rl_wallet.py +++ b/chia/wallet/rl_wallet/rl_wallet.py @@ -267,7 +267,7 @@ async def set_user_info( index = await self.wallet_state_manager.puzzle_store.index_for_pubkey(user_pubkey) assert index is not None record = DerivationRecord( - index, + uint32(index), rl_puzzle_hash, user_pubkey, WalletType.RATE_LIMITED, @@ -277,7 +277,7 @@ async def set_user_info( aggregation_puzzlehash = self.rl_get_aggregation_puzzlehash(new_rl_info.rl_puzzle_hash) record2 = DerivationRecord( - index + 1, + uint32(index + 1), aggregation_puzzlehash, user_pubkey, WalletType.RATE_LIMITED, @@ -332,6 +332,8 @@ async def rl_available_balance(self) -> uint64: peak = self.wallet_state_manager.blockchain.get_peak() height = peak.height if peak else 0 assert self.rl_info.limit is not None + if self.rl_info.interval is None: + raise RuntimeError("rl_available_balance: rl_info.interval is undefined") unlocked = int( ((height - self.rl_coin_record.confirmed_block_height) / self.rl_info.interval) * int(self.rl_info.limit) ) @@ -445,6 +447,8 @@ async def get_keys_pk(self, clawback_pubkey: bytes): return pubkey, private async def _get_rl_coin(self) -> Optional[Coin]: + if self.rl_info.rl_puzzle_hash is None: + return None rl_coins = await self.wallet_state_manager.coin_store.get_coin_records_by_puzzle_hash( self.rl_info.rl_puzzle_hash ) @@ -455,6 +459,8 @@ async def _get_rl_coin(self) -> Optional[Coin]: return None async def _get_rl_coin_record(self) -> Optional[WalletCoinRecord]: + if self.rl_info.rl_puzzle_hash is None: + return None rl_coins = await self.wallet_state_manager.coin_store.get_coin_records_by_puzzle_hash( self.rl_info.rl_puzzle_hash ) From 9005e4e5959310c4d6261c4ea8016a30b9337679 Mon Sep 17 00:00:00 2001 From: Adam Kelly <338792+aqk@users.noreply.github.com> Date: Tue, 29 Mar 2022 10:30:09 -0700 Subject: [PATCH 285/378] Use uint128 for wallet balances (#10936) --- chia/pools/pool_wallet.py | 16 ++++++++-------- chia/wallet/cat_wallet/cat_wallet.py | 4 ++-- chia/wallet/did_wallet/did_wallet.py | 14 +++++++------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/chia/pools/pool_wallet.py b/chia/pools/pool_wallet.py index 5f6698625cb4..a2df61234926 100644 --- a/chia/pools/pool_wallet.py +++ b/chia/pools/pool_wallet.py @@ -43,7 +43,7 @@ get_delayed_puz_info_from_launcher_spend, ) -from chia.util.ints import uint8, uint32, uint64 +from chia.util.ints import uint8, uint32, uint64, uint128 from chia.wallet.derive_keys import ( find_owner_sk, ) @@ -919,25 +919,25 @@ async def have_unconfirmed_transaction(self) -> bool: ) return len(unconfirmed) > 0 - async def get_confirmed_balance(self, _=None) -> uint64: - amount: uint64 = uint64(0) + async def get_confirmed_balance(self, _=None) -> uint128: + amount: uint128 = uint128(0) if (await self.get_current_state()).current.state == SELF_POOLING: unspent_coin_records: List[WalletCoinRecord] = list( await self.wallet_state_manager.coin_store.get_unspent_coins_for_wallet(self.wallet_id) ) for record in unspent_coin_records: if record.coinbase: - amount = uint64(amount + record.coin.amount) + amount = uint128(amount + record.coin.amount) return amount - async def get_unconfirmed_balance(self, record_list=None) -> uint64: + async def get_unconfirmed_balance(self, record_list=None) -> uint128: return await self.get_confirmed_balance(record_list) - async def get_spendable_balance(self, record_list=None) -> uint64: + async def get_spendable_balance(self, record_list=None) -> uint128: return await self.get_confirmed_balance(record_list) async def get_pending_change_balance(self) -> uint64: return uint64(0) - async def get_max_send_amount(self, record_list=None) -> uint64: - return uint64(0) + async def get_max_send_amount(self, record_list=None) -> uint128: + return uint128(0) diff --git a/chia/wallet/cat_wallet/cat_wallet.py b/chia/wallet/cat_wallet/cat_wallet.py index 244849200f22..a9d7b6c3c8b9 100644 --- a/chia/wallet/cat_wallet/cat_wallet.py +++ b/chia/wallet/cat_wallet/cat_wallet.py @@ -381,13 +381,13 @@ def puzzle_for_pk(self, pubkey) -> Program: async def get_new_cat_puzzle_hash(self): return (await self.wallet_state_manager.get_unused_derivation_record(self.id())).puzzle_hash - async def get_spendable_balance(self, records=None) -> uint64: + async def get_spendable_balance(self, records=None) -> uint128: coins = await self.get_cat_spendable_coins(records) amount = 0 for record in coins: amount += record.coin.amount - return uint64(amount) + return uint128(amount) async def get_pending_change_balance(self) -> uint64: unconfirmed_tx = await self.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(self.id()) diff --git a/chia/wallet/did_wallet/did_wallet.py b/chia/wallet/did_wallet/did_wallet.py index 363cffe5e898..c957157804c6 100644 --- a/chia/wallet/did_wallet/did_wallet.py +++ b/chia/wallet/did_wallet/did_wallet.py @@ -13,7 +13,7 @@ from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.coin_spend import CoinSpend from chia.types.spend_bundle import SpendBundle -from chia.util.ints import uint64, uint32, uint8 +from chia.util.ints import uint64, uint32, uint8, uint128 from chia.wallet.util.transaction_type import TransactionType from chia.wallet.did_wallet.did_info import DIDInfo @@ -189,18 +189,18 @@ def type(cls) -> uint8: def id(self): return self.wallet_info.id - async def get_confirmed_balance(self, record_list=None) -> uint64: + async def get_confirmed_balance(self, record_list=None) -> uint128: if record_list is None: record_list = await self.wallet_state_manager.coin_store.get_unspent_coins_for_wallet(self.id()) - amount: uint64 = uint64(0) + amount: uint128 = uint128(0) for record in record_list: parent = self.get_parent_for_coin(record.coin) if parent is not None: - amount = uint64(amount + record.coin.amount) + amount = uint128(amount + record.coin.amount) self.log.info(f"Confirmed balance for did wallet is {amount}") - return uint64(amount) + return uint128(amount) async def get_pending_change_balance(self) -> uint64: unconfirmed_tx = await self.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(self.id()) @@ -222,7 +222,7 @@ async def get_pending_change_balance(self) -> uint64: return uint64(addition_amount) - async def get_unconfirmed_balance(self, record_list=None) -> uint64: + async def get_unconfirmed_balance(self, record_list=None) -> uint128: confirmed = await self.get_confirmed_balance(record_list) return await self.wallet_state_manager._get_unconfirmed_balance(self.id(), confirmed) @@ -1015,7 +1015,7 @@ async def generate_eve_spend(self, coin: Coin, full_puzzle: Program, innerpuz: P async def get_frozen_amount(self) -> uint64: return await self.wallet_state_manager.get_frozen_balance(self.wallet_info.id) - async def get_spendable_balance(self, unspent_records=None) -> uint64: + async def get_spendable_balance(self, unspent_records=None) -> uint128: spendable_am = await self.wallet_state_manager.get_confirmed_spendable_balance_for_wallet( self.wallet_info.id, unspent_records ) From aaba00c3549883450728db476424df1412cf86ca Mon Sep 17 00:00:00 2001 From: Adam Kelly <338792+aqk@users.noreply.github.com> Date: Tue, 29 Mar 2022 10:30:46 -0700 Subject: [PATCH 286/378] Add more type checks to CAT Wallet (#10934) --- chia/wallet/cat_wallet/cat_wallet.py | 33 ++++++++++++++++++---------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/chia/wallet/cat_wallet/cat_wallet.py b/chia/wallet/cat_wallet/cat_wallet.py index a9d7b6c3c8b9..4b9f2c19a17e 100644 --- a/chia/wallet/cat_wallet/cat_wallet.py +++ b/chia/wallet/cat_wallet/cat_wallet.py @@ -91,10 +91,12 @@ async def create_new_cat_wallet( if name is None: name = "CAT WALLET" - self.wallet_info = await wallet_state_manager.user_store.create_wallet(name, WalletType.CAT, info_as_string) - if self.wallet_info is None: + new_wallet_info = await wallet_state_manager.user_store.create_wallet(name, WalletType.CAT, info_as_string) + if new_wallet_info is None: raise ValueError("Internal Error") + self.wallet_info = new_wallet_info + self.lineage_store = await CATLineageStore.create(self.wallet_state_manager.db_wrapper, self.get_asset_id()) try: @@ -192,11 +194,12 @@ async def create_wallet_for_cat( limitations_program_hash = bytes32(hexstr_to_bytes(limitations_program_hash_hex)) self.cat_info = CATInfo(limitations_program_hash, None) info_as_string = bytes(self.cat_info).hex() - self.wallet_info = await wallet_state_manager.user_store.create_wallet( + new_wallet_info = await wallet_state_manager.user_store.create_wallet( name, WalletType.CAT, info_as_string, in_transaction=in_transaction ) - if self.wallet_info is None: + if new_wallet_info is None: raise Exception("wallet_info is None") + self.wallet_info = new_wallet_info self.lineage_store = await CATLineageStore.create( self.wallet_state_manager.db_wrapper, self.get_asset_id(), in_transaction=in_transaction @@ -477,7 +480,11 @@ async def sign(self, spend_bundle: SpendBundle) -> SpendBundle: if matched: _, _, inner_puzzle = puzzle_args puzzle_hash = inner_puzzle.get_tree_hash() - pubkey, private = await self.wallet_state_manager.get_keys(puzzle_hash) + ret = await self.wallet_state_manager.get_keys(puzzle_hash) + if ret is None: + # Abort signing the entire SpendBundle - sign all or none + raise RuntimeError(f"Failed to get keys for puzzle_hash {puzzle_hash}") + pubkey, private = ret synthetic_secret_key = calculate_synthetic_secret_key(private, DEFAULT_HIDDEN_PUZZLE_HASH) error, conditions, cost = conditions_dict_for_solution( spend.puzzle_reveal.to_program(), @@ -499,18 +506,20 @@ async def sign(self, spend_bundle: SpendBundle) -> SpendBundle: return SpendBundle.aggregate([spend_bundle, SpendBundle([], agg_sig)]) async def inner_puzzle_for_cat_puzhash(self, cat_hash: bytes32) -> Program: - record: DerivationRecord = await self.wallet_state_manager.puzzle_store.get_derivation_record_for_puzzle_hash( - cat_hash - ) + record: Optional[ + DerivationRecord + ] = await self.wallet_state_manager.puzzle_store.get_derivation_record_for_puzzle_hash(cat_hash) + if record is None: + raise RuntimeError(f"Missing Derivation Record for CAT puzzle_hash {cat_hash}") inner_puzzle: Program = self.standard_wallet.puzzle_for_pk(bytes(record.pubkey)) return inner_puzzle async def convert_puzzle_hash(self, puzzle_hash: bytes32) -> bytes32: - record: DerivationRecord = await self.wallet_state_manager.puzzle_store.get_derivation_record_for_puzzle_hash( - puzzle_hash - ) + record: Optional[ + DerivationRecord + ] = await self.wallet_state_manager.puzzle_store.get_derivation_record_for_puzzle_hash(puzzle_hash) if record is None: - return puzzle_hash + return puzzle_hash # TODO: check if we have a test for this case! else: return (await self.inner_puzzle_for_cat_puzhash(puzzle_hash)).get_tree_hash() From 9a0d4775ae1ed5a39c55f97147dacbdf8c3099b5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Mar 2022 10:34:12 -0700 Subject: [PATCH 287/378] Bump actions/github-script from 4 to 6 (#10246) * Bump actions/github-script from 4 to 6 Bumps [actions/github-script](https://github.com/actions/github-script) from 4 to 6. - [Release notes](https://github.com/actions/github-script/releases) - [Commits](https://github.com/actions/github-script/compare/v4...v6) --- updated-dependencies: - dependency-name: actions/github-script dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] * Update to github.rest.* for calls to API for compat w/ github-script@v5+ Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Chris Marslender --- .github/workflows/build-linux-arm64-installer.yml | 8 ++++---- .github/workflows/build-linux-installer-deb.yml | 8 ++++---- .github/workflows/build-linux-installer-rpm.yml | 8 ++++---- .github/workflows/build-macos-installer.yml | 4 ++-- .github/workflows/build-macos-m1-installer.yml | 4 ++-- .github/workflows/build-windows-installer.yml | 8 ++++---- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/.github/workflows/build-linux-arm64-installer.yml b/.github/workflows/build-linux-arm64-installer.yml index 109d981c2c17..fad14fa525cd 100644 --- a/.github/workflows/build-linux-arm64-installer.yml +++ b/.github/workflows/build-linux-arm64-installer.yml @@ -62,13 +62,13 @@ jobs: SECRET: "${{ secrets.INSTALLER_UPLOAD_SECRET }}" # Get the most recent release from chia-plotter-madmax - - uses: actions/github-script@v4 + - uses: actions/github-script@v6 id: 'latest-madmax' with: github-token: ${{ secrets.GITHUB_TOKEN }} result-encoding: string script: | - const releases = await github.repos.listReleases({ + const releases = await github.rest.repos.listReleases({ owner: 'Chia-Network', repo: 'chia-plotter-madmax', }); @@ -83,13 +83,13 @@ jobs: chmod +x "$GITHUB_WORKSPACE/madmax/chia_plot_k34" # Get the most recent release from bladebit - - uses: actions/github-script@v4 + - uses: actions/github-script@v6 id: 'latest-bladebit' with: github-token: ${{ secrets.GITHUB_TOKEN }} result-encoding: string script: | - const releases = await github.repos.listReleases({ + const releases = await github.rest.repos.listReleases({ owner: 'Chia-Network', repo: 'bladebit', }); diff --git a/.github/workflows/build-linux-installer-deb.yml b/.github/workflows/build-linux-installer-deb.yml index 00f835ed45b5..786ed3dd249b 100644 --- a/.github/workflows/build-linux-installer-deb.yml +++ b/.github/workflows/build-linux-installer-deb.yml @@ -96,13 +96,13 @@ jobs: SECRET: "${{ secrets.INSTALLER_UPLOAD_SECRET }}" # Get the most recent release from chia-plotter-madmax - - uses: actions/github-script@v4 + - uses: actions/github-script@v6 id: 'latest-madmax' with: github-token: ${{ secrets.GITHUB_TOKEN }} result-encoding: string script: | - const releases = await github.repos.listReleases({ + const releases = await github.rest.repos.listReleases({ owner: 'Chia-Network', repo: 'chia-plotter-madmax', }); @@ -117,13 +117,13 @@ jobs: chmod +x "$GITHUB_WORKSPACE/madmax/chia_plot_k34" # Get the most recent release from bladebit - - uses: actions/github-script@v4 + - uses: actions/github-script@v6 id: 'latest-bladebit' with: github-token: ${{ secrets.GITHUB_TOKEN }} result-encoding: string script: | - const releases = await github.repos.listReleases({ + const releases = await github.rest.repos.listReleases({ owner: 'Chia-Network', repo: 'bladebit', }); diff --git a/.github/workflows/build-linux-installer-rpm.yml b/.github/workflows/build-linux-installer-rpm.yml index a287600d9321..5564b472e503 100644 --- a/.github/workflows/build-linux-installer-rpm.yml +++ b/.github/workflows/build-linux-installer-rpm.yml @@ -65,13 +65,13 @@ jobs: SECRET: "${{ secrets.INSTALLER_UPLOAD_SECRET }}" # Get the most recent release from chia-plotter-madmax - - uses: actions/github-script@v4 + - uses: actions/github-script@v6 id: 'latest-madmax' with: github-token: ${{ secrets.GITHUB_TOKEN }} result-encoding: string script: | - const releases = await github.repos.listReleases({ + const releases = await github.rest.repos.listReleases({ owner: 'Chia-Network', repo: 'chia-plotter-madmax', }); @@ -86,13 +86,13 @@ jobs: chmod +x "$GITHUB_WORKSPACE/madmax/chia_plot_k34" # Get the most recent release from bladebit - - uses: actions/github-script@v4 + - uses: actions/github-script@v6 id: 'latest-bladebit' with: github-token: ${{ secrets.GITHUB_TOKEN }} result-encoding: string script: | - const releases = await github.repos.listReleases({ + const releases = await github.rest.repos.listReleases({ owner: 'Chia-Network', repo: 'bladebit', }); diff --git a/.github/workflows/build-macos-installer.yml b/.github/workflows/build-macos-installer.yml index 545933c9004c..216459ad6cfa 100644 --- a/.github/workflows/build-macos-installer.yml +++ b/.github/workflows/build-macos-installer.yml @@ -104,13 +104,13 @@ jobs: p12-password: ${{ secrets.APPLE_DEV_ID_APP_PASS }} # Get the most recent release from chia-plotter-madmax - - uses: actions/github-script@v4 + - uses: actions/github-script@v6 id: 'latest-madmax' with: github-token: ${{ secrets.GITHUB_TOKEN }} result-encoding: string script: | - const releases = await github.repos.listReleases({ + const releases = await github.rest.repos.listReleases({ owner: 'Chia-Network', repo: 'chia-plotter-madmax', }); diff --git a/.github/workflows/build-macos-m1-installer.yml b/.github/workflows/build-macos-m1-installer.yml index aeb329a5e6c9..7a178382e303 100644 --- a/.github/workflows/build-macos-m1-installer.yml +++ b/.github/workflows/build-macos-m1-installer.yml @@ -78,13 +78,13 @@ jobs: p12-password: ${{ secrets.APPLE_DEV_ID_APP_PASS }} # Get the most recent release from chia-plotter-madmax - - uses: actions/github-script@v4 + - uses: actions/github-script@v6 id: 'latest-madmax' with: github-token: ${{ secrets.GITHUB_TOKEN }} result-encoding: string script: | - const releases = await github.repos.listReleases({ + const releases = await github.rest.repos.listReleases({ owner: 'Chia-Network', repo: 'chia-plotter-madmax', }); diff --git a/.github/workflows/build-windows-installer.yml b/.github/workflows/build-windows-installer.yml index d621ef0ff971..77344505bf48 100644 --- a/.github/workflows/build-windows-installer.yml +++ b/.github/workflows/build-windows-installer.yml @@ -105,13 +105,13 @@ jobs: deactivate # Get the most recent release from chia-plotter-madmax - - uses: actions/github-script@v4 + - uses: actions/github-script@v6 id: 'latest-madmax' with: github-token: ${{ secrets.GITHUB_TOKEN }} result-encoding: string script: | - const releases = await github.repos.listReleases({ + const releases = await github.rest.repos.listReleases({ owner: 'Chia-Network', repo: 'chia-plotter-madmax', }); @@ -124,13 +124,13 @@ jobs: Invoke-WebRequest https://github.com/Chia-Network/chia-plotter-madmax/releases/download/${{ steps.latest-madmax.outputs.result }}/chia_plot_k34-${{ steps.latest-madmax.outputs.result }}.exe -OutFile "$env:GITHUB_WORKSPACE\madmax\chia_plot_k34.exe" # Get the most recent release from bladebit - - uses: actions/github-script@v4 + - uses: actions/github-script@v6 id: 'latest-bladebit' with: github-token: ${{ secrets.GITHUB_TOKEN }} result-encoding: string script: | - const releases = await github.repos.listReleases({ + const releases = await github.rest.repos.listReleases({ owner: 'Chia-Network', repo: 'bladebit', }); From 81c60993f6e2bba723fe8e01509052ed0a99128e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Mar 2022 10:34:48 -0700 Subject: [PATCH 288/378] Bump actions/setup-node from 2.4.1 to 3 (#10506) Bumps [actions/setup-node](https://github.com/actions/setup-node) from 2.4.1 to 3. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v2.4.1...v3) --- updated-dependencies: - dependency-name: actions/setup-node dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-linux-installer-deb.yml | 2 +- .github/workflows/build-macos-installer.yml | 2 +- .github/workflows/build-windows-installer.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-linux-installer-deb.yml b/.github/workflows/build-linux-installer-deb.yml index 786ed3dd249b..11894ab51f6e 100644 --- a/.github/workflows/build-linux-installer-deb.yml +++ b/.github/workflows/build-linux-installer-deb.yml @@ -144,7 +144,7 @@ jobs: sh install.sh - name: Setup Node 16.x - uses: actions/setup-node@v2.4.1 + uses: actions/setup-node@v3 with: node-version: '16.x' diff --git a/.github/workflows/build-macos-installer.yml b/.github/workflows/build-macos-installer.yml index 216459ad6cfa..a2161dbf1d7f 100644 --- a/.github/workflows/build-macos-installer.yml +++ b/.github/workflows/build-macos-installer.yml @@ -132,7 +132,7 @@ jobs: sh install.sh - name: Setup Node 16.x - uses: actions/setup-node@v2.4.1 + uses: actions/setup-node@v3 with: node-version: '16.x' diff --git a/.github/workflows/build-windows-installer.yml b/.github/workflows/build-windows-installer.yml index 77344505bf48..77e67b1d2029 100644 --- a/.github/workflows/build-windows-installer.yml +++ b/.github/workflows/build-windows-installer.yml @@ -65,7 +65,7 @@ jobs: python-version: "3.9" - name: Setup Node 16.x - uses: actions/setup-node@v2.4.1 + uses: actions/setup-node@v3 with: node-version: '16.x' From 817ebe7d6d7e9edf63d463aadd4d92078895a470 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Mar 2022 10:35:06 -0700 Subject: [PATCH 289/378] Bump actions/cache from 2.1.6 to 3 (#10846) * Bump actions/cache from 2.1.6 to 3 Bumps [actions/cache](https://github.com/actions/cache) from 2.1.6 to 3. - [Release notes](https://github.com/actions/cache/releases) - [Commits](https://github.com/actions/cache/compare/v2.1.6...v3) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] * Update actions in templates also Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Gene Hoffman --- .github/workflows/benchmarks.yml | 2 +- .github/workflows/build-linux-installer-deb.yml | 4 ++-- .github/workflows/build-macos-installer.yml | 4 ++-- .github/workflows/build-test-macos-blockchain.yml | 2 +- .github/workflows/build-test-macos-clvm.yml | 2 +- .github/workflows/build-test-macos-core-cmds.yml | 2 +- .github/workflows/build-test-macos-core-consensus.yml | 2 +- .github/workflows/build-test-macos-core-custom_types.yml | 2 +- .github/workflows/build-test-macos-core-daemon.yml | 2 +- .../workflows/build-test-macos-core-full_node-full_sync.yml | 2 +- .github/workflows/build-test-macos-core-full_node-stores.yml | 2 +- .github/workflows/build-test-macos-core-full_node.yml | 2 +- .github/workflows/build-test-macos-core-server.yml | 2 +- .github/workflows/build-test-macos-core-ssl.yml | 2 +- .github/workflows/build-test-macos-core-util.yml | 2 +- .github/workflows/build-test-macos-core.yml | 2 +- .github/workflows/build-test-macos-farmer_harvester.yml | 2 +- .github/workflows/build-test-macos-generator.yml | 2 +- .github/workflows/build-test-macos-plotting.yml | 2 +- .github/workflows/build-test-macos-pools.yml | 2 +- .github/workflows/build-test-macos-simulation.yml | 2 +- .github/workflows/build-test-macos-tools.yml | 2 +- .github/workflows/build-test-macos-util.yml | 2 +- .github/workflows/build-test-macos-wallet-cat_wallet.yml | 2 +- .github/workflows/build-test-macos-wallet-did_wallet.yml | 2 +- .github/workflows/build-test-macos-wallet-rl_wallet.yml | 2 +- .github/workflows/build-test-macos-wallet-rpc.yml | 2 +- .github/workflows/build-test-macos-wallet-simple_sync.yml | 2 +- .github/workflows/build-test-macos-wallet-sync.yml | 2 +- .github/workflows/build-test-macos-wallet.yml | 2 +- .github/workflows/build-test-macos-weight_proof.yml | 2 +- .github/workflows/build-test-ubuntu-blockchain.yml | 4 ++-- .github/workflows/build-test-ubuntu-clvm.yml | 4 ++-- .github/workflows/build-test-ubuntu-core-cmds.yml | 4 ++-- .github/workflows/build-test-ubuntu-core-consensus.yml | 4 ++-- .github/workflows/build-test-ubuntu-core-custom_types.yml | 4 ++-- .github/workflows/build-test-ubuntu-core-daemon.yml | 4 ++-- .../workflows/build-test-ubuntu-core-full_node-full_sync.yml | 4 ++-- .github/workflows/build-test-ubuntu-core-full_node-stores.yml | 4 ++-- .github/workflows/build-test-ubuntu-core-full_node.yml | 4 ++-- .github/workflows/build-test-ubuntu-core-server.yml | 4 ++-- .github/workflows/build-test-ubuntu-core-ssl.yml | 4 ++-- .github/workflows/build-test-ubuntu-core-util.yml | 4 ++-- .github/workflows/build-test-ubuntu-core.yml | 4 ++-- .github/workflows/build-test-ubuntu-farmer_harvester.yml | 4 ++-- .github/workflows/build-test-ubuntu-generator.yml | 4 ++-- .github/workflows/build-test-ubuntu-plotting.yml | 4 ++-- .github/workflows/build-test-ubuntu-pools.yml | 4 ++-- .github/workflows/build-test-ubuntu-simulation.yml | 4 ++-- .github/workflows/build-test-ubuntu-tools.yml | 4 ++-- .github/workflows/build-test-ubuntu-util.yml | 4 ++-- .github/workflows/build-test-ubuntu-wallet-cat_wallet.yml | 4 ++-- .github/workflows/build-test-ubuntu-wallet-did_wallet.yml | 4 ++-- .github/workflows/build-test-ubuntu-wallet-rl_wallet.yml | 4 ++-- .github/workflows/build-test-ubuntu-wallet-rpc.yml | 4 ++-- .github/workflows/build-test-ubuntu-wallet-simple_sync.yml | 4 ++-- .github/workflows/build-test-ubuntu-wallet-sync.yml | 4 ++-- .github/workflows/build-test-ubuntu-wallet.yml | 4 ++-- .github/workflows/build-test-ubuntu-weight_proof.yml | 4 ++-- .github/workflows/build-windows-installer.yml | 4 ++-- tests/runner_templates/build-test-macos | 2 +- tests/runner_templates/build-test-ubuntu | 4 ++-- 62 files changed, 94 insertions(+), 94 deletions(-) diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 9759b1f5c576..90bea4ef84e9 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -43,7 +43,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ${{ steps.pip-cache.outputs.dir }} key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} diff --git a/.github/workflows/build-linux-installer-deb.yml b/.github/workflows/build-linux-installer-deb.yml index 11894ab51f6e..5734d6204f64 100644 --- a/.github/workflows/build-linux-installer-deb.yml +++ b/.github/workflows/build-linux-installer-deb.yml @@ -49,7 +49,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Cache npm - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 env: cache-name: cache-node-modules with: @@ -67,7 +67,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: # Note that new runners may break this https://github.com/actions/cache/issues/292 path: ${{ steps.pip-cache.outputs.dir }} diff --git a/.github/workflows/build-macos-installer.yml b/.github/workflows/build-macos-installer.yml index a2161dbf1d7f..f9d3fc3faeec 100644 --- a/.github/workflows/build-macos-installer.yml +++ b/.github/workflows/build-macos-installer.yml @@ -70,7 +70,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Cache npm - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 env: cache-name: cache-node-modules with: @@ -88,7 +88,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: # Note that new runners may break this https://github.com/actions/cache/issues/292 path: ${{ steps.pip-cache.outputs.dir }} diff --git a/.github/workflows/build-test-macos-blockchain.yml b/.github/workflows/build-test-macos-blockchain.yml index 5cbbac4aa598..6edf41d2d4fa 100644 --- a/.github/workflows/build-test-macos-blockchain.yml +++ b/.github/workflows/build-test-macos-blockchain.yml @@ -57,7 +57,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: # Note that new runners may break this https://github.com/actions/cache/issues/292 path: ${{ steps.pip-cache.outputs.dir }} diff --git a/.github/workflows/build-test-macos-clvm.yml b/.github/workflows/build-test-macos-clvm.yml index c038440ec818..414ff342c744 100644 --- a/.github/workflows/build-test-macos-clvm.yml +++ b/.github/workflows/build-test-macos-clvm.yml @@ -57,7 +57,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: # Note that new runners may break this https://github.com/actions/cache/issues/292 path: ${{ steps.pip-cache.outputs.dir }} diff --git a/.github/workflows/build-test-macos-core-cmds.yml b/.github/workflows/build-test-macos-core-cmds.yml index 2060a7821603..6b5548a526e8 100644 --- a/.github/workflows/build-test-macos-core-cmds.yml +++ b/.github/workflows/build-test-macos-core-cmds.yml @@ -57,7 +57,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: # Note that new runners may break this https://github.com/actions/cache/issues/292 path: ${{ steps.pip-cache.outputs.dir }} diff --git a/.github/workflows/build-test-macos-core-consensus.yml b/.github/workflows/build-test-macos-core-consensus.yml index ab1390a0c1c4..0a95110a9841 100644 --- a/.github/workflows/build-test-macos-core-consensus.yml +++ b/.github/workflows/build-test-macos-core-consensus.yml @@ -57,7 +57,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: # Note that new runners may break this https://github.com/actions/cache/issues/292 path: ${{ steps.pip-cache.outputs.dir }} diff --git a/.github/workflows/build-test-macos-core-custom_types.yml b/.github/workflows/build-test-macos-core-custom_types.yml index b5b1355f8a43..d08ed4e638ef 100644 --- a/.github/workflows/build-test-macos-core-custom_types.yml +++ b/.github/workflows/build-test-macos-core-custom_types.yml @@ -57,7 +57,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: # Note that new runners may break this https://github.com/actions/cache/issues/292 path: ${{ steps.pip-cache.outputs.dir }} diff --git a/.github/workflows/build-test-macos-core-daemon.yml b/.github/workflows/build-test-macos-core-daemon.yml index d4e29bfb11ac..b7c94c874314 100644 --- a/.github/workflows/build-test-macos-core-daemon.yml +++ b/.github/workflows/build-test-macos-core-daemon.yml @@ -57,7 +57,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: # Note that new runners may break this https://github.com/actions/cache/issues/292 path: ${{ steps.pip-cache.outputs.dir }} diff --git a/.github/workflows/build-test-macos-core-full_node-full_sync.yml b/.github/workflows/build-test-macos-core-full_node-full_sync.yml index 74649df60444..f0ae27d1d65f 100644 --- a/.github/workflows/build-test-macos-core-full_node-full_sync.yml +++ b/.github/workflows/build-test-macos-core-full_node-full_sync.yml @@ -57,7 +57,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: # Note that new runners may break this https://github.com/actions/cache/issues/292 path: ${{ steps.pip-cache.outputs.dir }} diff --git a/.github/workflows/build-test-macos-core-full_node-stores.yml b/.github/workflows/build-test-macos-core-full_node-stores.yml index ec7598f98718..c971b17215c2 100644 --- a/.github/workflows/build-test-macos-core-full_node-stores.yml +++ b/.github/workflows/build-test-macos-core-full_node-stores.yml @@ -57,7 +57,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: # Note that new runners may break this https://github.com/actions/cache/issues/292 path: ${{ steps.pip-cache.outputs.dir }} diff --git a/.github/workflows/build-test-macos-core-full_node.yml b/.github/workflows/build-test-macos-core-full_node.yml index 518abcd2885c..4c13a79b5dbc 100644 --- a/.github/workflows/build-test-macos-core-full_node.yml +++ b/.github/workflows/build-test-macos-core-full_node.yml @@ -57,7 +57,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: # Note that new runners may break this https://github.com/actions/cache/issues/292 path: ${{ steps.pip-cache.outputs.dir }} diff --git a/.github/workflows/build-test-macos-core-server.yml b/.github/workflows/build-test-macos-core-server.yml index d8b860df1c60..e4bceaf93d6a 100644 --- a/.github/workflows/build-test-macos-core-server.yml +++ b/.github/workflows/build-test-macos-core-server.yml @@ -57,7 +57,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: # Note that new runners may break this https://github.com/actions/cache/issues/292 path: ${{ steps.pip-cache.outputs.dir }} diff --git a/.github/workflows/build-test-macos-core-ssl.yml b/.github/workflows/build-test-macos-core-ssl.yml index b40750dfb8a7..6b4b17483290 100644 --- a/.github/workflows/build-test-macos-core-ssl.yml +++ b/.github/workflows/build-test-macos-core-ssl.yml @@ -57,7 +57,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: # Note that new runners may break this https://github.com/actions/cache/issues/292 path: ${{ steps.pip-cache.outputs.dir }} diff --git a/.github/workflows/build-test-macos-core-util.yml b/.github/workflows/build-test-macos-core-util.yml index 5b98525573d7..2a01174dcd11 100644 --- a/.github/workflows/build-test-macos-core-util.yml +++ b/.github/workflows/build-test-macos-core-util.yml @@ -57,7 +57,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: # Note that new runners may break this https://github.com/actions/cache/issues/292 path: ${{ steps.pip-cache.outputs.dir }} diff --git a/.github/workflows/build-test-macos-core.yml b/.github/workflows/build-test-macos-core.yml index dd171dafb4df..db9288eda47a 100644 --- a/.github/workflows/build-test-macos-core.yml +++ b/.github/workflows/build-test-macos-core.yml @@ -57,7 +57,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: # Note that new runners may break this https://github.com/actions/cache/issues/292 path: ${{ steps.pip-cache.outputs.dir }} diff --git a/.github/workflows/build-test-macos-farmer_harvester.yml b/.github/workflows/build-test-macos-farmer_harvester.yml index dd9d6e7cef06..d22814b66060 100644 --- a/.github/workflows/build-test-macos-farmer_harvester.yml +++ b/.github/workflows/build-test-macos-farmer_harvester.yml @@ -57,7 +57,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: # Note that new runners may break this https://github.com/actions/cache/issues/292 path: ${{ steps.pip-cache.outputs.dir }} diff --git a/.github/workflows/build-test-macos-generator.yml b/.github/workflows/build-test-macos-generator.yml index efe583420f9a..339d88dcaa90 100644 --- a/.github/workflows/build-test-macos-generator.yml +++ b/.github/workflows/build-test-macos-generator.yml @@ -57,7 +57,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: # Note that new runners may break this https://github.com/actions/cache/issues/292 path: ${{ steps.pip-cache.outputs.dir }} diff --git a/.github/workflows/build-test-macos-plotting.yml b/.github/workflows/build-test-macos-plotting.yml index 46500a11c662..0d614d04282a 100644 --- a/.github/workflows/build-test-macos-plotting.yml +++ b/.github/workflows/build-test-macos-plotting.yml @@ -57,7 +57,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: # Note that new runners may break this https://github.com/actions/cache/issues/292 path: ${{ steps.pip-cache.outputs.dir }} diff --git a/.github/workflows/build-test-macos-pools.yml b/.github/workflows/build-test-macos-pools.yml index ac449d50ff85..80ff69a5db91 100644 --- a/.github/workflows/build-test-macos-pools.yml +++ b/.github/workflows/build-test-macos-pools.yml @@ -57,7 +57,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: # Note that new runners may break this https://github.com/actions/cache/issues/292 path: ${{ steps.pip-cache.outputs.dir }} diff --git a/.github/workflows/build-test-macos-simulation.yml b/.github/workflows/build-test-macos-simulation.yml index a7638f59b188..3859585c0511 100644 --- a/.github/workflows/build-test-macos-simulation.yml +++ b/.github/workflows/build-test-macos-simulation.yml @@ -57,7 +57,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: # Note that new runners may break this https://github.com/actions/cache/issues/292 path: ${{ steps.pip-cache.outputs.dir }} diff --git a/.github/workflows/build-test-macos-tools.yml b/.github/workflows/build-test-macos-tools.yml index a2a6dc99f8d6..3e4e981a3791 100644 --- a/.github/workflows/build-test-macos-tools.yml +++ b/.github/workflows/build-test-macos-tools.yml @@ -57,7 +57,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: # Note that new runners may break this https://github.com/actions/cache/issues/292 path: ${{ steps.pip-cache.outputs.dir }} diff --git a/.github/workflows/build-test-macos-util.yml b/.github/workflows/build-test-macos-util.yml index 1945eac5a0f4..3fa4a0c2ee06 100644 --- a/.github/workflows/build-test-macos-util.yml +++ b/.github/workflows/build-test-macos-util.yml @@ -57,7 +57,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: # Note that new runners may break this https://github.com/actions/cache/issues/292 path: ${{ steps.pip-cache.outputs.dir }} diff --git a/.github/workflows/build-test-macos-wallet-cat_wallet.yml b/.github/workflows/build-test-macos-wallet-cat_wallet.yml index 887ff1ce5e0e..16be450bd0fd 100644 --- a/.github/workflows/build-test-macos-wallet-cat_wallet.yml +++ b/.github/workflows/build-test-macos-wallet-cat_wallet.yml @@ -57,7 +57,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: # Note that new runners may break this https://github.com/actions/cache/issues/292 path: ${{ steps.pip-cache.outputs.dir }} diff --git a/.github/workflows/build-test-macos-wallet-did_wallet.yml b/.github/workflows/build-test-macos-wallet-did_wallet.yml index 1e721d18a0f6..29efd583d71c 100644 --- a/.github/workflows/build-test-macos-wallet-did_wallet.yml +++ b/.github/workflows/build-test-macos-wallet-did_wallet.yml @@ -57,7 +57,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: # Note that new runners may break this https://github.com/actions/cache/issues/292 path: ${{ steps.pip-cache.outputs.dir }} diff --git a/.github/workflows/build-test-macos-wallet-rl_wallet.yml b/.github/workflows/build-test-macos-wallet-rl_wallet.yml index bfe48cda34aa..af3cb0d9d1ef 100644 --- a/.github/workflows/build-test-macos-wallet-rl_wallet.yml +++ b/.github/workflows/build-test-macos-wallet-rl_wallet.yml @@ -57,7 +57,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: # Note that new runners may break this https://github.com/actions/cache/issues/292 path: ${{ steps.pip-cache.outputs.dir }} diff --git a/.github/workflows/build-test-macos-wallet-rpc.yml b/.github/workflows/build-test-macos-wallet-rpc.yml index 07f461874e2c..9629b8d628dd 100644 --- a/.github/workflows/build-test-macos-wallet-rpc.yml +++ b/.github/workflows/build-test-macos-wallet-rpc.yml @@ -57,7 +57,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: # Note that new runners may break this https://github.com/actions/cache/issues/292 path: ${{ steps.pip-cache.outputs.dir }} diff --git a/.github/workflows/build-test-macos-wallet-simple_sync.yml b/.github/workflows/build-test-macos-wallet-simple_sync.yml index 6c4436da04d5..7c6ae1eee6ec 100644 --- a/.github/workflows/build-test-macos-wallet-simple_sync.yml +++ b/.github/workflows/build-test-macos-wallet-simple_sync.yml @@ -57,7 +57,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: # Note that new runners may break this https://github.com/actions/cache/issues/292 path: ${{ steps.pip-cache.outputs.dir }} diff --git a/.github/workflows/build-test-macos-wallet-sync.yml b/.github/workflows/build-test-macos-wallet-sync.yml index 62012d1243d9..ad293617eb27 100644 --- a/.github/workflows/build-test-macos-wallet-sync.yml +++ b/.github/workflows/build-test-macos-wallet-sync.yml @@ -57,7 +57,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: # Note that new runners may break this https://github.com/actions/cache/issues/292 path: ${{ steps.pip-cache.outputs.dir }} diff --git a/.github/workflows/build-test-macos-wallet.yml b/.github/workflows/build-test-macos-wallet.yml index d35d8527f189..8073f213289f 100644 --- a/.github/workflows/build-test-macos-wallet.yml +++ b/.github/workflows/build-test-macos-wallet.yml @@ -57,7 +57,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: # Note that new runners may break this https://github.com/actions/cache/issues/292 path: ${{ steps.pip-cache.outputs.dir }} diff --git a/.github/workflows/build-test-macos-weight_proof.yml b/.github/workflows/build-test-macos-weight_proof.yml index 2cb097479867..2c45969587ae 100644 --- a/.github/workflows/build-test-macos-weight_proof.yml +++ b/.github/workflows/build-test-macos-weight_proof.yml @@ -57,7 +57,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: # Note that new runners may break this https://github.com/actions/cache/issues/292 path: ${{ steps.pip-cache.outputs.dir }} diff --git a/.github/workflows/build-test-ubuntu-blockchain.yml b/.github/workflows/build-test-ubuntu-blockchain.yml index ab8e3792afb6..94109d800d5e 100644 --- a/.github/workflows/build-test-ubuntu-blockchain.yml +++ b/.github/workflows/build-test-ubuntu-blockchain.yml @@ -45,7 +45,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Cache npm - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} @@ -58,7 +58,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ${{ steps.pip-cache.outputs.dir }} key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} diff --git a/.github/workflows/build-test-ubuntu-clvm.yml b/.github/workflows/build-test-ubuntu-clvm.yml index 7746a4224053..de7660a4887d 100644 --- a/.github/workflows/build-test-ubuntu-clvm.yml +++ b/.github/workflows/build-test-ubuntu-clvm.yml @@ -45,7 +45,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Cache npm - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} @@ -58,7 +58,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ${{ steps.pip-cache.outputs.dir }} key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} diff --git a/.github/workflows/build-test-ubuntu-core-cmds.yml b/.github/workflows/build-test-ubuntu-core-cmds.yml index 7a20b1a60dfd..7939d45b1f28 100644 --- a/.github/workflows/build-test-ubuntu-core-cmds.yml +++ b/.github/workflows/build-test-ubuntu-core-cmds.yml @@ -45,7 +45,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Cache npm - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} @@ -58,7 +58,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ${{ steps.pip-cache.outputs.dir }} key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} diff --git a/.github/workflows/build-test-ubuntu-core-consensus.yml b/.github/workflows/build-test-ubuntu-core-consensus.yml index ef302ab4c636..8083c05ad54b 100644 --- a/.github/workflows/build-test-ubuntu-core-consensus.yml +++ b/.github/workflows/build-test-ubuntu-core-consensus.yml @@ -45,7 +45,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Cache npm - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} @@ -58,7 +58,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ${{ steps.pip-cache.outputs.dir }} key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} diff --git a/.github/workflows/build-test-ubuntu-core-custom_types.yml b/.github/workflows/build-test-ubuntu-core-custom_types.yml index 75d1372afd57..4684f2c25d82 100644 --- a/.github/workflows/build-test-ubuntu-core-custom_types.yml +++ b/.github/workflows/build-test-ubuntu-core-custom_types.yml @@ -45,7 +45,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Cache npm - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} @@ -58,7 +58,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ${{ steps.pip-cache.outputs.dir }} key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} diff --git a/.github/workflows/build-test-ubuntu-core-daemon.yml b/.github/workflows/build-test-ubuntu-core-daemon.yml index a04ecba0a95c..524ab9ad30d9 100644 --- a/.github/workflows/build-test-ubuntu-core-daemon.yml +++ b/.github/workflows/build-test-ubuntu-core-daemon.yml @@ -45,7 +45,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Cache npm - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} @@ -58,7 +58,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ${{ steps.pip-cache.outputs.dir }} key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} diff --git a/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml b/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml index 9c48f4af0640..40f05d1bfb67 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml @@ -45,7 +45,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Cache npm - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} @@ -58,7 +58,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ${{ steps.pip-cache.outputs.dir }} key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} diff --git a/.github/workflows/build-test-ubuntu-core-full_node-stores.yml b/.github/workflows/build-test-ubuntu-core-full_node-stores.yml index 34640f47778e..d28a1294e97b 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node-stores.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node-stores.yml @@ -45,7 +45,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Cache npm - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} @@ -58,7 +58,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ${{ steps.pip-cache.outputs.dir }} key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} diff --git a/.github/workflows/build-test-ubuntu-core-full_node.yml b/.github/workflows/build-test-ubuntu-core-full_node.yml index 0aff8bf7aacb..d19fb71f320f 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node.yml @@ -45,7 +45,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Cache npm - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} @@ -58,7 +58,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ${{ steps.pip-cache.outputs.dir }} key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} diff --git a/.github/workflows/build-test-ubuntu-core-server.yml b/.github/workflows/build-test-ubuntu-core-server.yml index adbdc95719d2..6a24118aa758 100644 --- a/.github/workflows/build-test-ubuntu-core-server.yml +++ b/.github/workflows/build-test-ubuntu-core-server.yml @@ -45,7 +45,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Cache npm - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} @@ -58,7 +58,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ${{ steps.pip-cache.outputs.dir }} key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} diff --git a/.github/workflows/build-test-ubuntu-core-ssl.yml b/.github/workflows/build-test-ubuntu-core-ssl.yml index bf62b5c2c399..e5074ab76b26 100644 --- a/.github/workflows/build-test-ubuntu-core-ssl.yml +++ b/.github/workflows/build-test-ubuntu-core-ssl.yml @@ -45,7 +45,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Cache npm - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} @@ -58,7 +58,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ${{ steps.pip-cache.outputs.dir }} key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} diff --git a/.github/workflows/build-test-ubuntu-core-util.yml b/.github/workflows/build-test-ubuntu-core-util.yml index 4c099f34d362..0b23005959af 100644 --- a/.github/workflows/build-test-ubuntu-core-util.yml +++ b/.github/workflows/build-test-ubuntu-core-util.yml @@ -45,7 +45,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Cache npm - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} @@ -58,7 +58,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ${{ steps.pip-cache.outputs.dir }} key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} diff --git a/.github/workflows/build-test-ubuntu-core.yml b/.github/workflows/build-test-ubuntu-core.yml index be05a9a3f789..eb06af8df66a 100644 --- a/.github/workflows/build-test-ubuntu-core.yml +++ b/.github/workflows/build-test-ubuntu-core.yml @@ -45,7 +45,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Cache npm - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} @@ -58,7 +58,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ${{ steps.pip-cache.outputs.dir }} key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} diff --git a/.github/workflows/build-test-ubuntu-farmer_harvester.yml b/.github/workflows/build-test-ubuntu-farmer_harvester.yml index e526cf84a748..37eaa651f7d2 100644 --- a/.github/workflows/build-test-ubuntu-farmer_harvester.yml +++ b/.github/workflows/build-test-ubuntu-farmer_harvester.yml @@ -45,7 +45,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Cache npm - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} @@ -58,7 +58,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ${{ steps.pip-cache.outputs.dir }} key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} diff --git a/.github/workflows/build-test-ubuntu-generator.yml b/.github/workflows/build-test-ubuntu-generator.yml index 5dbd49056597..707deb5adb11 100644 --- a/.github/workflows/build-test-ubuntu-generator.yml +++ b/.github/workflows/build-test-ubuntu-generator.yml @@ -45,7 +45,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Cache npm - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} @@ -58,7 +58,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ${{ steps.pip-cache.outputs.dir }} key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} diff --git a/.github/workflows/build-test-ubuntu-plotting.yml b/.github/workflows/build-test-ubuntu-plotting.yml index 3208a1d9bd4a..9561180be15b 100644 --- a/.github/workflows/build-test-ubuntu-plotting.yml +++ b/.github/workflows/build-test-ubuntu-plotting.yml @@ -45,7 +45,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Cache npm - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} @@ -58,7 +58,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ${{ steps.pip-cache.outputs.dir }} key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} diff --git a/.github/workflows/build-test-ubuntu-pools.yml b/.github/workflows/build-test-ubuntu-pools.yml index 7cf0a0197377..d083a12d7c20 100644 --- a/.github/workflows/build-test-ubuntu-pools.yml +++ b/.github/workflows/build-test-ubuntu-pools.yml @@ -45,7 +45,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Cache npm - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} @@ -58,7 +58,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ${{ steps.pip-cache.outputs.dir }} key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} diff --git a/.github/workflows/build-test-ubuntu-simulation.yml b/.github/workflows/build-test-ubuntu-simulation.yml index e38742a9703d..385fce0ed930 100644 --- a/.github/workflows/build-test-ubuntu-simulation.yml +++ b/.github/workflows/build-test-ubuntu-simulation.yml @@ -45,7 +45,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Cache npm - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} @@ -58,7 +58,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ${{ steps.pip-cache.outputs.dir }} key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} diff --git a/.github/workflows/build-test-ubuntu-tools.yml b/.github/workflows/build-test-ubuntu-tools.yml index 0afd52ad5572..4a738a726eb7 100644 --- a/.github/workflows/build-test-ubuntu-tools.yml +++ b/.github/workflows/build-test-ubuntu-tools.yml @@ -45,7 +45,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Cache npm - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} @@ -58,7 +58,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ${{ steps.pip-cache.outputs.dir }} key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} diff --git a/.github/workflows/build-test-ubuntu-util.yml b/.github/workflows/build-test-ubuntu-util.yml index ae66db1c1fe0..9637c59bc4e9 100644 --- a/.github/workflows/build-test-ubuntu-util.yml +++ b/.github/workflows/build-test-ubuntu-util.yml @@ -45,7 +45,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Cache npm - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} @@ -58,7 +58,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ${{ steps.pip-cache.outputs.dir }} key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} diff --git a/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml index f2fd6d521dee..b83b9aa9532d 100644 --- a/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml @@ -45,7 +45,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Cache npm - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} @@ -58,7 +58,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ${{ steps.pip-cache.outputs.dir }} key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} diff --git a/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml index 5403775a3071..084dca1d9d81 100644 --- a/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml @@ -45,7 +45,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Cache npm - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} @@ -58,7 +58,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ${{ steps.pip-cache.outputs.dir }} key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} diff --git a/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml index 3180a86eefe5..75200f14233c 100644 --- a/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml @@ -45,7 +45,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Cache npm - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} @@ -58,7 +58,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ${{ steps.pip-cache.outputs.dir }} key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} diff --git a/.github/workflows/build-test-ubuntu-wallet-rpc.yml b/.github/workflows/build-test-ubuntu-wallet-rpc.yml index 1661b890921d..6f9a5e2277c0 100644 --- a/.github/workflows/build-test-ubuntu-wallet-rpc.yml +++ b/.github/workflows/build-test-ubuntu-wallet-rpc.yml @@ -45,7 +45,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Cache npm - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} @@ -58,7 +58,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ${{ steps.pip-cache.outputs.dir }} key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} diff --git a/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml b/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml index 1b863ad7bc72..85fd306fc81c 100644 --- a/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml +++ b/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml @@ -45,7 +45,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Cache npm - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} @@ -58,7 +58,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ${{ steps.pip-cache.outputs.dir }} key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} diff --git a/.github/workflows/build-test-ubuntu-wallet-sync.yml b/.github/workflows/build-test-ubuntu-wallet-sync.yml index 8700dcde52db..924485cb7e9c 100644 --- a/.github/workflows/build-test-ubuntu-wallet-sync.yml +++ b/.github/workflows/build-test-ubuntu-wallet-sync.yml @@ -45,7 +45,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Cache npm - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} @@ -58,7 +58,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ${{ steps.pip-cache.outputs.dir }} key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} diff --git a/.github/workflows/build-test-ubuntu-wallet.yml b/.github/workflows/build-test-ubuntu-wallet.yml index fe1298353888..88c1c1bfa56c 100644 --- a/.github/workflows/build-test-ubuntu-wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet.yml @@ -45,7 +45,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Cache npm - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} @@ -58,7 +58,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ${{ steps.pip-cache.outputs.dir }} key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} diff --git a/.github/workflows/build-test-ubuntu-weight_proof.yml b/.github/workflows/build-test-ubuntu-weight_proof.yml index b194db2ac346..3a43ec6b3c51 100644 --- a/.github/workflows/build-test-ubuntu-weight_proof.yml +++ b/.github/workflows/build-test-ubuntu-weight_proof.yml @@ -45,7 +45,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Cache npm - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} @@ -58,7 +58,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ${{ steps.pip-cache.outputs.dir }} key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} diff --git a/.github/workflows/build-windows-installer.yml b/.github/workflows/build-windows-installer.yml index 77e67b1d2029..7f7c6bd8cb44 100644 --- a/.github/workflows/build-windows-installer.yml +++ b/.github/workflows/build-windows-installer.yml @@ -38,7 +38,7 @@ jobs: echo "::set-output name=dir::$(npm config get cache)" - name: Cache npm - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ${{ steps.npm-cache.outputs.dir }} key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} @@ -51,7 +51,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ${{ steps.pip-cache.outputs.dir }} key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} diff --git a/tests/runner_templates/build-test-macos b/tests/runner_templates/build-test-macos index e1e5b0beb2ba..6031c07ef48a 100644 --- a/tests/runner_templates/build-test-macos +++ b/tests/runner_templates/build-test-macos @@ -57,7 +57,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: # Note that new runners may break this https://github.com/actions/cache/issues/292 path: ${{ steps.pip-cache.outputs.dir }} diff --git a/tests/runner_templates/build-test-ubuntu b/tests/runner_templates/build-test-ubuntu index 5583347709a1..512d848740b7 100644 --- a/tests/runner_templates/build-test-ubuntu +++ b/tests/runner_templates/build-test-ubuntu @@ -45,7 +45,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Cache npm - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} @@ -58,7 +58,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.6 + uses: actions/cache@v3 with: path: ${{ steps.pip-cache.outputs.dir }} key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} From 92897ffc39976f2935fb3a25ed4e6a810e2a8049 Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 29 Mar 2022 13:24:55 -0700 Subject: [PATCH 290/378] Fix trailing bytes shown in CAT asset ID row when using `chia wallet show` (#10924) * Truncate CAT asset_id output to 32 bytes. A wallet RPC change is needed to properly separate out the asset ID from the TAIL program returned by `get_all_wallet_info_entries()` * Move the truncation to the assignment location --- chia/cmds/wallet_funcs.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index 3f51051b50a0..7a8b42baab4e 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -484,7 +484,9 @@ async def print_balances(args: dict, wallet_client: WalletRpcClient, fingerprint print(f"Balances, fingerprint: {fingerprint}") for summary in summaries_response: indent: str = " " - asset_id = summary["data"] + # asset_id currently contains both the asset ID and TAIL program bytes concatenated together. + # A future RPC update may split them apart, but for now we'll show the first 32 bytes (64 chars) + asset_id = summary["data"][:64] wallet_id = summary["id"] balances = await wallet_client.get_wallet_balance(wallet_id) typ = WalletType(int(summary["type"])) From c5426068aaed59efeb9fc3c763935d96e4f8df42 Mon Sep 17 00:00:00 2001 From: Adam Kelly <338792+aqk@users.noreply.github.com> Date: Tue, 29 Mar 2022 13:25:30 -0700 Subject: [PATCH 291/378] Consolidate test fixtures (#10778) * Rename confusing fixtures, especially ones with the same name but different implementation * revert premature fixture rename: two_wallet_nodes_start_height_1 * Consolidate test fixtures --- .../test_blockchain_transactions.py | 2 +- tests/conftest.py | 314 +++++++++++++++++- tests/core/daemon/test_daemon.py | 48 +-- .../full_node/full_sync/test_full_sync.py | 1 - .../full_node/stores/test_full_node_store.py | 8 +- tests/core/full_node/test_full_node.py | 65 +--- .../full_node/test_mempool_performance.py | 12 +- tests/core/full_node/test_performance.py | 23 +- tests/core/full_node/test_transactions.py | 20 -- tests/core/server/test_dos.py | 10 +- tests/core/ssl/test_ssl.py | 33 +- tests/core/test_daemon_rpc.py | 10 +- tests/core/test_filter.py | 9 - tests/core/test_full_node_rpc.py | 16 +- tests/pools/test_pool_rpc.py | 6 - tests/wallet/cat_wallet/test_cat_lifecycle.py | 22 +- tests/wallet/cat_wallet/test_cat_wallet.py | 24 +- .../wallet/cat_wallet/test_offer_lifecycle.py | 21 +- tests/wallet/cat_wallet/test_trades.py | 55 +-- tests/wallet/did_wallet/test_did.py | 35 +- tests/wallet/did_wallet/test_did_rpc.py | 12 +- tests/wallet/rl_wallet/test_rl_rpc.py | 8 - tests/wallet/rl_wallet/test_rl_wallet.py | 8 - tests/wallet/rpc/test_wallet_rpc.py | 28 +- .../simple_sync/test_simple_sync_protocol.py | 18 +- tests/wallet/sync/test_wallet_sync.py | 21 +- tests/wallet/test_wallet.py | 46 +-- tests/wallet/test_wallet_blockchain.py | 9 +- 28 files changed, 373 insertions(+), 511 deletions(-) diff --git a/tests/blockchain/test_blockchain_transactions.py b/tests/blockchain/test_blockchain_transactions.py index 1d0c830ac096..4f39638ce92d 100644 --- a/tests/blockchain/test_blockchain_transactions.py +++ b/tests/blockchain/test_blockchain_transactions.py @@ -11,9 +11,9 @@ from chia.util.errors import ConsensusError, Err from chia.util.ints import uint64 from tests.blockchain.blockchain_test_utils import _validate_and_add_block -from tests.wallet_tools import WalletTool from tests.setup_nodes import test_constants from tests.util.generator_tools_testing import run_and_get_removals_and_additions +from tests.wallet_tools import WalletTool BURN_PUZZLE_HASH = b"0" * 32 diff --git a/tests/conftest.py b/tests/conftest.py index c59b5f458946..f2d3933a5f8d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,8 @@ # flake8: noqa E402 # See imports after multiprocessing.set_start_method import multiprocessing import os +from secrets import token_bytes + import pytest import pytest_asyncio import tempfile @@ -8,11 +10,34 @@ from tests.setup_nodes import setup_node_and_wallet, setup_n_nodes, setup_two_nodes # Set spawn after stdlib imports, but before other imports +from chia.clvm.spend_sim import SimClient, SpendSim +from chia.protocols import full_node_protocol +from chia.simulator.simulator_protocol import FarmNewBlockProtocol +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.peer_info import PeerInfo +from chia.util.ints import uint16 +from tests.core.node_height import node_height_at_least +from tests.pools.test_pool_rpc import wallet_is_synced +from tests.setup_nodes import ( + setup_simulators_and_wallets, + setup_node_and_wallet, + setup_full_system, + setup_daemon, + setup_n_nodes, + setup_introducer, + setup_timelord, + setup_two_nodes, +) +from tests.simulation.test_simulation import test_constants_modified +from tests.time_out_assert import time_out_assert +from tests.util.socket import find_available_listen_port +from tests.wallet_tools import WalletTool + multiprocessing.set_start_method("spawn") from pathlib import Path from chia.util.keyring_wrapper import KeyringWrapper -from tests.block_tools import BlockTools, test_constants, create_block_tools +from tests.block_tools import BlockTools, test_constants, create_block_tools, create_block_tools_async from tests.util.keyring import TempKeyring @@ -157,6 +182,12 @@ async def two_nodes(db_version, self_hostname): yield _ +@pytest_asyncio.fixture(scope="function") +async def setup_two_nodes_fixture(db_version): + async for _ in setup_simulators_and_wallets(2, 0, {}, db_version=db_version): + yield _ + + @pytest_asyncio.fixture(scope="function") async def three_nodes(db_version, self_hostname): async for _ in setup_n_nodes(test_constants, 3, db_version=db_version, self_hostname=self_hostname): @@ -173,3 +204,284 @@ async def four_nodes(db_version, self_hostname): async def five_nodes(db_version, self_hostname): async for _ in setup_n_nodes(test_constants, 5, db_version=db_version, self_hostname=self_hostname): yield _ + + +@pytest_asyncio.fixture(scope="module") +async def wallet_nodes(bt): + async_gen = setup_simulators_and_wallets(2, 1, {"MEMPOOL_BLOCK_BUFFER": 2, "MAX_BLOCK_COST_CLVM": 400000000}) + nodes, wallets = await async_gen.__anext__() + full_node_1 = nodes[0] + full_node_2 = nodes[1] + server_1 = full_node_1.full_node.server + server_2 = full_node_2.full_node.server + wallet_a = bt.get_pool_wallet_tool() + wallet_receiver = WalletTool(full_node_1.full_node.constants) + yield full_node_1, full_node_2, server_1, server_2, wallet_a, wallet_receiver + + async for _ in async_gen: + yield _ + + +@pytest_asyncio.fixture(scope="function") +async def setup_four_nodes(db_version): + async for _ in setup_simulators_and_wallets(5, 0, {}, db_version=db_version): + yield _ + + +@pytest_asyncio.fixture(scope="function") +async def two_nodes_sim_and_wallets(): + async for _ in setup_simulators_and_wallets(2, 0, {}): + yield _ + + +@pytest_asyncio.fixture(scope="function") +async def wallet_node_sim_and_wallet(): + async for _ in setup_simulators_and_wallets(1, 1, {}): + yield _ + + +@pytest_asyncio.fixture(scope="function") +async def wallet_node_100_pk(): + async for _ in setup_simulators_and_wallets(1, 1, {}, initial_num_public_keys=100): + yield _ + + +@pytest_asyncio.fixture(scope="function") +async def two_wallet_nodes(): + async for _ in setup_simulators_and_wallets(1, 2, {}): + yield _ + + +@pytest_asyncio.fixture(scope="function") +async def three_sim_two_wallets(): + async for _ in setup_simulators_and_wallets(3, 2, {}): + yield _ + + +@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 + yield _ + + +@pytest_asyncio.fixture(scope="function") +async def three_wallet_nodes(): + async for _ in setup_simulators_and_wallets(1, 3, {}): + yield _ + + +@pytest_asyncio.fixture(scope="function") +async def two_wallet_nodes_five_freeze(): + async for _ in setup_simulators_and_wallets(1, 2, {}): + yield _ + + +@pytest_asyncio.fixture(scope="function") +async def wallet_node_simulator(): + async for _ in setup_simulators_and_wallets(1, 1, {}): + yield _ + + +@pytest_asyncio.fixture(scope="function") +async def wallet_two_node_simulator(): + async for _ in setup_simulators_and_wallets(2, 1, {}): + yield _ + + +@pytest_asyncio.fixture(scope="module") +async def wallet_nodes_mempool_perf(bt): + key_seed = bt.farmer_master_sk_entropy + async for _ in setup_simulators_and_wallets(2, 1, {}, key_seed=key_seed): + yield _ + + +@pytest_asyncio.fixture(scope="module") +async def wallet_nodes_perf(bt): + async_gen = setup_simulators_and_wallets(1, 1, {"MEMPOOL_BLOCK_BUFFER": 1, "MAX_BLOCK_COST_CLVM": 11000000000}) + nodes, wallets = await async_gen.__anext__() + full_node_1 = nodes[0] + server_1 = full_node_1.full_node.server + wallet_a = bt.get_pool_wallet_tool() + wallet_receiver = WalletTool(full_node_1.full_node.constants) + yield full_node_1, server_1, wallet_a, wallet_receiver + + async for _ in async_gen: + yield _ + + +@pytest_asyncio.fixture(scope="function") +async def wallet_node_starting_height(self_hostname): + async for _ in setup_node_and_wallet(test_constants, self_hostname, starting_height=100): + yield _ + + +@pytest_asyncio.fixture(scope="function") +async def wallet_nodes_mainnet(bt, db_version): + async_gen = setup_simulators_and_wallets(2, 1, {"NETWORK_TYPE": 0}, db_version=db_version) + nodes, wallets = await async_gen.__anext__() + full_node_1 = nodes[0] + full_node_2 = nodes[1] + server_1 = full_node_1.full_node.server + server_2 = full_node_2.full_node.server + wallet_a = bt.get_pool_wallet_tool() + wallet_receiver = WalletTool(full_node_1.full_node.constants) + yield full_node_1, full_node_2, server_1, server_2, wallet_a, wallet_receiver + + async for _ in async_gen: + yield _ + + +@pytest_asyncio.fixture(scope="function") +async def three_nodes_two_wallets(): + async for _ in setup_simulators_and_wallets(3, 2, {}): + yield _ + + +@pytest_asyncio.fixture(scope="function") +async def wallet_and_node(): + async for _ in setup_simulators_and_wallets(1, 1, {}): + yield _ + + +# TODO: Ideally, the db_version should be the (parameterized) db_version +# fixture, to test all versions of the database schema. This doesn't work +# because of a hack in shutting down the full node, which means you cannot run +# more than one simulations per process. +@pytest_asyncio.fixture(scope="function") +async def daemon_simulation(bt, get_b_tools, get_b_tools_1): + async for _ in setup_full_system( + test_constants_modified, + bt, + b_tools=get_b_tools, + b_tools_1=get_b_tools_1, + connect_to_daemon=True, + db_version=1, + ): + yield _ + + +@pytest_asyncio.fixture(scope="function") +async def get_daemon(bt): + async for _ in setup_daemon(btools=bt): + yield _ + + +@pytest_asyncio.fixture(scope="function") +async def get_temp_keyring(): + with TempKeyring() as keychain: + yield keychain + + +@pytest_asyncio.fixture(scope="function") +async def get_b_tools_1(get_temp_keyring): + return await create_block_tools_async(constants=test_constants_modified, keychain=get_temp_keyring) + + +@pytest_asyncio.fixture(scope="function") +async def get_b_tools(get_temp_keyring): + local_b_tools = await create_block_tools_async(constants=test_constants_modified, keychain=get_temp_keyring) + new_config = local_b_tools._config + local_b_tools.change_config(new_config) + return local_b_tools + + +@pytest_asyncio.fixture(scope="function") +async def get_daemon_with_temp_keyring(get_b_tools): + async for daemon in setup_daemon(btools=get_b_tools): + yield get_b_tools, daemon + + +@pytest_asyncio.fixture(scope="function") +async def wallets_prefarm(two_wallet_nodes, self_hostname, trusted): + """ + Sets up the node with 10 blocks, and returns a payer and payee wallet. + """ + farm_blocks = 10 + buffer = 4 + full_nodes, wallets = two_wallet_nodes + full_node_api = full_nodes[0] + full_node_server = full_node_api.server + wallet_node_0, wallet_server_0 = wallets[0] + wallet_node_1, wallet_server_1 = wallets[1] + wallet_0 = wallet_node_0.wallet_state_manager.main_wallet + wallet_1 = wallet_node_1.wallet_state_manager.main_wallet + + ph0 = await wallet_0.get_new_puzzlehash() + ph1 = await wallet_1.get_new_puzzlehash() + + if trusted: + wallet_node_0.config["trusted_peers"] = {full_node_server.node_id.hex(): full_node_server.node_id.hex()} + wallet_node_1.config["trusted_peers"] = {full_node_server.node_id.hex(): full_node_server.node_id.hex()} + else: + wallet_node_0.config["trusted_peers"] = {} + wallet_node_1.config["trusted_peers"] = {} + + await wallet_server_0.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) + await wallet_server_1.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) + + for i in range(0, farm_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph0)) + + for i in range(0, farm_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph1)) + + for i in range(0, buffer): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(bytes32(token_bytes(nbytes=32)))) + + await time_out_assert(10, wallet_is_synced, True, wallet_node_0, full_node_api) + await time_out_assert(10, wallet_is_synced, True, wallet_node_1, full_node_api) + + return wallet_node_0, wallet_node_1, full_node_api + + +@pytest_asyncio.fixture(scope="function") +async def introducer(bt): + introducer_port = find_available_listen_port("introducer") + async for _ in setup_introducer(bt, introducer_port): + yield _ + + +@pytest_asyncio.fixture(scope="function") +async def timelord(bt): + timelord_port = find_available_listen_port("timelord") + node_port = find_available_listen_port("node") + rpc_port = find_available_listen_port("rpc") + vdf_port = find_available_listen_port("vdf") + async for _ in setup_timelord(timelord_port, node_port, rpc_port, vdf_port, False, test_constants, bt): + yield _ + + +@pytest_asyncio.fixture(scope="module") +async def two_nodes_mempool(bt, wallet_a): + async_gen = setup_simulators_and_wallets(2, 1, {}) + nodes, _ = await async_gen.__anext__() + full_node_1 = nodes[0] + full_node_2 = nodes[1] + server_1 = full_node_1.full_node.server + server_2 = full_node_2.full_node.server + + reward_ph = wallet_a.get_new_puzzlehash() + blocks = bt.get_consecutive_blocks( + 3, + 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, blocks[-1].height) + + yield full_node_1, full_node_2, server_1, server_2 + + async for _ in async_gen: + yield _ + + +@pytest_asyncio.fixture(scope="function") +async def setup_sim(): + sim = await SpendSim.create() + sim_client = SimClient(sim) + await sim.farm_block() + return sim, sim_client diff --git a/tests/core/daemon/test_daemon.py b/tests/core/daemon/test_daemon.py index fb6fe11465ed..28d4226d5cd7 100644 --- a/tests/core/daemon/test_daemon.py +++ b/tests/core/daemon/test_daemon.py @@ -3,62 +3,16 @@ import json import logging import pytest -import pytest_asyncio from chia.daemon.server import WebSocketServer from chia.server.outbound_message import NodeType from chia.types.peer_info import PeerInfo -from tests.block_tools import BlockTools, create_block_tools_async +from tests.block_tools import BlockTools from chia.util.ints import uint16 from chia.util.keyring_wrapper import DEFAULT_PASSPHRASE_IF_NO_MASTER_PASSPHRASE from chia.util.ws_message import create_payload from tests.core.node_height import node_height_at_least -from tests.setup_nodes import setup_daemon, setup_full_system -from tests.simulation.test_simulation import test_constants_modified from tests.time_out_assert import time_out_assert_custom_interval, time_out_assert -from tests.util.keyring import TempKeyring - - -@pytest_asyncio.fixture(scope="function") -async def get_temp_keyring(): - with TempKeyring() as keychain: - yield keychain - - -@pytest_asyncio.fixture(scope="function") -async def get_b_tools_1(get_temp_keyring): - return await create_block_tools_async(constants=test_constants_modified, keychain=get_temp_keyring) - - -@pytest_asyncio.fixture(scope="function") -async def get_b_tools(get_temp_keyring): - local_b_tools = await create_block_tools_async(constants=test_constants_modified, keychain=get_temp_keyring) - new_config = local_b_tools._config - local_b_tools.change_config(new_config) - return local_b_tools - - -@pytest_asyncio.fixture(scope="function") -async def get_daemon_with_temp_keyring(get_b_tools): - async for daemon in setup_daemon(btools=get_b_tools): - yield get_b_tools, daemon - - -# TODO: Ideally, the db_version should be the (parameterized) db_version -# fixture, to test all versions of the database schema. This doesn't work -# because of a hack in shutting down the full node, which means you cannot run -# more than one simulations per process. -@pytest_asyncio.fixture(scope="function") -async def daemon_simulation(bt, get_b_tools, get_b_tools_1): - async for _ in setup_full_system( - test_constants_modified, - bt, - b_tools=get_b_tools, - b_tools_1=get_b_tools_1, - connect_to_daemon=True, - db_version=1, - ): - yield _ class TestDaemon: diff --git a/tests/core/full_node/full_sync/test_full_sync.py b/tests/core/full_node/full_sync/test_full_sync.py index 0444f827a10c..df244e7bbe85 100644 --- a/tests/core/full_node/full_sync/test_full_sync.py +++ b/tests/core/full_node/full_sync/test_full_sync.py @@ -17,7 +17,6 @@ from tests.setup_nodes import test_constants from tests.time_out_assert import time_out_assert - log = logging.getLogger(__name__) diff --git a/tests/core/full_node/stores/test_full_node_store.py b/tests/core/full_node/stores/test_full_node_store.py index c96307e28326..ca6c14cb14b3 100644 --- a/tests/core/full_node/stores/test_full_node_store.py +++ b/tests/core/full_node/stores/test_full_node_store.py @@ -1,5 +1,3 @@ -# flake8: noqa: F811, F401 -import asyncio import atexit import logging from secrets import token_bytes @@ -8,7 +6,6 @@ import pytest import pytest_asyncio -from chia.consensus.block_record import BlockRecord from chia.consensus.blockchain import ReceiveBlockResult from chia.consensus.find_fork_point import find_fork_point_in_chain from chia.consensus.multiprocess_validation import PreValidationResult @@ -20,12 +17,11 @@ from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.unfinished_block import UnfinishedBlock from chia.util.block_cache import BlockCache -from tests.block_tools import get_signage_point, create_block_tools from chia.util.hash import std_hash from chia.util.ints import uint8, uint32, uint64, uint128 +from tests.block_tools import get_signage_point, create_block_tools from tests.blockchain.blockchain_test_utils import ( _validate_and_add_block, - _validate_and_add_block_multi_result, _validate_and_add_block_no_error, ) from tests.setup_nodes import test_constants as test_constants_original @@ -627,7 +623,7 @@ async def test_basic_store(self, empty_blockchain, normalized_to_identity: bool for block in blocks: await _validate_and_add_block_no_error(blockchain, block) - log.warning(f"Starting loop") + log.warning("Starting loop") while True: log.warning("Looping") blocks = bt.get_consecutive_blocks(1, block_list_input=blocks, skip_slots=1) diff --git a/tests/core/full_node/test_full_node.py b/tests/core/full_node/test_full_node.py index 4d02d7a37526..5c324c35402c 100644 --- a/tests/core/full_node/test_full_node.py +++ b/tests/core/full_node/test_full_node.py @@ -5,11 +5,10 @@ import time from secrets import token_bytes from typing import Dict, Optional, List -from blspy import G2Element -from clvm.casts import int_to_bytes import pytest -import pytest_asyncio +from blspy import G2Element +from clvm.casts import int_to_bytes from chia.consensus.pot_iterations import is_overflow_block from chia.full_node.bundle_tools import detect_potential_template_generator @@ -33,26 +32,24 @@ from chia.types.peer_info import PeerInfo, TimestampedPeerInfo from chia.types.spend_bundle import SpendBundle from chia.types.unfinished_block import UnfinishedBlock -from tests.block_tools import get_signage_point from chia.util.errors import Err from chia.util.hash import std_hash from chia.util.ints import uint8, uint16, uint32, uint64 from chia.util.recursive_replace import recursive_replace from chia.util.vdf_prover import get_vdf_info_and_proof +from chia.wallet.transaction_record import TransactionRecord +from tests.block_tools import get_signage_point from tests.blockchain.blockchain_test_utils import ( _validate_and_add_block, _validate_and_add_block_no_error, ) -from tests.pools.test_pool_rpc import wallet_is_synced -from tests.wallet_tools import WalletTool -from chia.wallet.transaction_record import TransactionRecord - from tests.connection_utils import add_dummy_connection, connect_and_get_peer from tests.core.full_node.stores.test_coin_store import get_future_reward_coins from tests.core.full_node.test_mempool_performance import wallet_height_at_least from tests.core.make_block_generator import make_spend_bundle from tests.core.node_height import node_height_at_least -from tests.setup_nodes import setup_simulators_and_wallets, test_constants +from tests.pools.test_pool_rpc import wallet_is_synced +from tests.setup_nodes import test_constants from tests.time_out_assert import time_out_assert, time_out_assert_custom_interval, time_out_messages log = logging.getLogger(__name__) @@ -98,56 +95,6 @@ async def get_block_path(full_node: FullNodeAPI): return blocks_list -@pytest_asyncio.fixture(scope="module") -async def wallet_nodes(bt): - async_gen = setup_simulators_and_wallets(2, 1, {"MEMPOOL_BLOCK_BUFFER": 2, "MAX_BLOCK_COST_CLVM": 400000000}) - nodes, wallets = await async_gen.__anext__() - full_node_1 = nodes[0] - full_node_2 = nodes[1] - server_1 = full_node_1.full_node.server - server_2 = full_node_2.full_node.server - wallet_a = bt.get_pool_wallet_tool() - wallet_receiver = WalletTool(full_node_1.full_node.constants) - yield full_node_1, full_node_2, server_1, server_2, wallet_a, wallet_receiver - - async for _ in async_gen: - yield _ - - -@pytest_asyncio.fixture(scope="function") -async def setup_four_nodes(db_version): - async for _ in setup_simulators_and_wallets(5, 0, {}, db_version=db_version): - yield _ - - -@pytest_asyncio.fixture(scope="function") -async def setup_two_nodes_fixture(db_version): - async for _ in setup_simulators_and_wallets(2, 0, {}, db_version=db_version): - yield _ - - -@pytest_asyncio.fixture(scope="function") -async def setup_two_nodes_and_wallet(): - async for _ in setup_simulators_and_wallets(2, 1, {}, db_version=2): - yield _ - - -@pytest_asyncio.fixture(scope="function") -async def wallet_nodes_mainnet(bt, db_version): - async_gen = setup_simulators_and_wallets(2, 1, {"NETWORK_TYPE": 0}, db_version=db_version) - nodes, wallets = await async_gen.__anext__() - full_node_1 = nodes[0] - full_node_2 = nodes[1] - server_1 = full_node_1.full_node.server - server_2 = full_node_2.full_node.server - wallet_a = bt.get_pool_wallet_tool() - wallet_receiver = WalletTool(full_node_1.full_node.constants) - yield full_node_1, full_node_2, server_1, server_2, wallet_a, wallet_receiver - - async for _ in async_gen: - yield _ - - class TestFullNodeBlockCompression: @pytest.mark.asyncio @pytest.mark.parametrize("tx_size", [10000, 3000000000000]) diff --git a/tests/core/full_node/test_mempool_performance.py b/tests/core/full_node/test_mempool_performance.py index a8a7766affbb..96b44d6a8ca0 100644 --- a/tests/core/full_node/test_mempool_performance.py +++ b/tests/core/full_node/test_mempool_performance.py @@ -1,11 +1,9 @@ # flake8: noqa: F811, F401 -import asyncio +import logging import time import pytest -import pytest_asyncio -import logging from chia.protocols import full_node_protocol from chia.types.peer_info import PeerInfo @@ -13,7 +11,6 @@ from chia.wallet.transaction_record import TransactionRecord from chia.wallet.wallet_node import WalletNode from tests.connection_utils import connect_and_get_peer -from tests.setup_nodes import setup_simulators_and_wallets from tests.time_out_assert import time_out_assert @@ -35,13 +32,6 @@ async def wallet_balance_at_least(wallet_node: WalletNode, balance): log = logging.getLogger(__name__) -@pytest_asyncio.fixture(scope="module") -async def wallet_nodes_mempool_perf(bt): - key_seed = bt.farmer_master_sk_entropy - async for _ in setup_simulators_and_wallets(2, 1, {}, key_seed=key_seed): - yield _ - - class TestMempoolPerformance: @pytest.mark.asyncio @pytest.mark.benchmark diff --git a/tests/core/full_node/test_performance.py b/tests/core/full_node/test_performance.py index 4a1ce2260c0e..58e1edeac358 100644 --- a/tests/core/full_node/test_performance.py +++ b/tests/core/full_node/test_performance.py @@ -1,15 +1,13 @@ # flake8: noqa: F811, F401 -import asyncio +import cProfile import dataclasses import logging import random import time from typing import Dict -from clvm.casts import int_to_bytes import pytest -import pytest_asyncio -import cProfile +from clvm.casts import int_to_bytes from chia.consensus.block_record import BlockRecord from chia.full_node.full_node_api import FullNodeAPI @@ -18,12 +16,9 @@ from chia.types.condition_with_args import ConditionWithArgs from chia.types.unfinished_block import UnfinishedBlock from chia.util.ints import uint64 -from tests.wallet_tools import WalletTool - from tests.connection_utils import add_dummy_connection from tests.core.full_node.stores.test_coin_store import get_future_reward_coins from tests.core.node_height import node_height_at_least -from tests.setup_nodes import setup_simulators_and_wallets from tests.time_out_assert import time_out_assert log = logging.getLogger(__name__) @@ -39,20 +34,6 @@ async def get_block_path(full_node: FullNodeAPI): return blocks_list -@pytest_asyncio.fixture(scope="module") -async def wallet_nodes_perf(bt): - async_gen = setup_simulators_and_wallets(1, 1, {"MEMPOOL_BLOCK_BUFFER": 1, "MAX_BLOCK_COST_CLVM": 11000000000}) - nodes, wallets = await async_gen.__anext__() - full_node_1 = nodes[0] - server_1 = full_node_1.full_node.server - wallet_a = bt.get_pool_wallet_tool() - wallet_receiver = WalletTool(full_node_1.full_node.constants) - yield full_node_1, server_1, wallet_a, wallet_receiver - - async for _ in async_gen: - yield _ - - class TestPerformance: @pytest.mark.asyncio @pytest.mark.benchmark diff --git a/tests/core/full_node/test_transactions.py b/tests/core/full_node/test_transactions.py index e00dacd22a09..b2c88a2a7880 100644 --- a/tests/core/full_node/test_transactions.py +++ b/tests/core/full_node/test_transactions.py @@ -3,7 +3,6 @@ from typing import Optional import pytest -import pytest_asyncio from chia.consensus.block_record import BlockRecord from chia.consensus.block_rewards import calculate_base_farmer_reward, calculate_pool_reward @@ -12,28 +11,9 @@ from chia.simulator.simulator_protocol import FarmNewBlockProtocol from chia.types.peer_info import PeerInfo from chia.util.ints import uint16, uint32 -from tests.setup_nodes import setup_simulators_and_wallets from tests.time_out_assert import time_out_assert -@pytest_asyncio.fixture(scope="function") -async def wallet_node_sim_and_wallet(): - async for _ in setup_simulators_and_wallets(1, 1, {}): - yield _ - - -@pytest_asyncio.fixture(scope="function") -async def two_wallet_nodes(): - async for _ in setup_simulators_and_wallets(1, 2, {}): - yield _ - - -@pytest_asyncio.fixture(scope="function") -async def three_nodes_two_wallets(): - async for _ in setup_simulators_and_wallets(3, 2, {}): - yield _ - - class TestTransactions: @pytest.mark.asyncio async def test_wallet_coinbase(self, wallet_node_sim_and_wallet, self_hostname): diff --git a/tests/core/server/test_dos.py b/tests/core/server/test_dos.py index 9985e2999317..de87635e356c 100644 --- a/tests/core/server/test_dos.py +++ b/tests/core/server/test_dos.py @@ -3,7 +3,6 @@ import logging import pytest -import pytest_asyncio from aiohttp import ClientSession, ClientTimeout, ServerDisconnectedError, WSCloseCode, WSMessage, WSMsgType from chia.full_node.full_node_api import FullNodeAPI @@ -15,9 +14,8 @@ from chia.server.server import ssl_context_for_client from chia.server.ws_connection import WSChiaConnection from chia.types.peer_info import PeerInfo -from chia.util.ints import uint16, uint64 from chia.util.errors import Err -from tests.setup_nodes import setup_simulators_and_wallets +from chia.util.ints import uint16, uint64 from tests.time_out_assert import time_out_assert log = logging.getLogger(__name__) @@ -33,12 +31,6 @@ async def get_block_path(full_node: FullNodeAPI): return blocks_list -@pytest_asyncio.fixture(scope="function") -async def setup_two_nodes_fixture(db_version): - async for _ in setup_simulators_and_wallets(2, 0, {}, db_version=db_version): - yield _ - - class FakeRateLimiter: def process_msg_and_check(self, msg): return True diff --git a/tests/core/ssl/test_ssl.py b/tests/core/ssl/test_ssl.py index 798df4c64c2f..24a20efcfce6 100644 --- a/tests/core/ssl/test_ssl.py +++ b/tests/core/ssl/test_ssl.py @@ -10,15 +10,9 @@ from chia.server.ws_connection import WSChiaConnection from chia.ssl.create_ssl import generate_ca_signed_cert from chia.types.peer_info import PeerInfo -from tests.block_tools import test_constants from chia.util.ints import uint16 -from tests.setup_nodes import ( - setup_harvester_farmer, - setup_introducer, - setup_simulators_and_wallets, - setup_timelord, -) -from tests.util.socket import find_available_listen_port +from tests.block_tools import test_constants +from tests.setup_nodes import setup_harvester_farmer async def establish_connection(server: ChiaServer, self_hostname: str, ssl_context) -> bool: @@ -57,29 +51,6 @@ async def harvester_farmer(bt): yield _ -@pytest_asyncio.fixture(scope="function") -async def wallet_node_sim_and_wallet(): - async for _ in setup_simulators_and_wallets(1, 1, {}): - yield _ - - -@pytest_asyncio.fixture(scope="function") -async def introducer(bt): - introducer_port = find_available_listen_port("introducer") - async for _ in setup_introducer(bt, introducer_port): - yield _ - - -@pytest_asyncio.fixture(scope="function") -async def timelord(bt): - timelord_port = find_available_listen_port("timelord") - node_port = find_available_listen_port("node") - rpc_port = find_available_listen_port("rpc") - vdf_port = find_available_listen_port("vdf") - async for _ in setup_timelord(timelord_port, node_port, rpc_port, vdf_port, False, test_constants, bt): - yield _ - - class TestSSL: @pytest.mark.asyncio async def test_public_connections(self, wallet_node_sim_and_wallet, self_hostname): diff --git a/tests/core/test_daemon_rpc.py b/tests/core/test_daemon_rpc.py index f5100eed8990..5cf5780eae58 100644 --- a/tests/core/test_daemon_rpc.py +++ b/tests/core/test_daemon_rpc.py @@ -1,15 +1,7 @@ import pytest -import pytest_asyncio -from tests.setup_nodes import setup_daemon -from chia.daemon.client import connect_to_daemon from chia import __version__ - - -@pytest_asyncio.fixture(scope="function") -async def get_daemon(bt): - async for _ in setup_daemon(btools=bt): - yield _ +from chia.daemon.client import connect_to_daemon class TestDaemonRpc: diff --git a/tests/core/test_filter.py b/tests/core/test_filter.py index 3448b4b42e66..db953e4befd0 100644 --- a/tests/core/test_filter.py +++ b/tests/core/test_filter.py @@ -1,17 +1,8 @@ from typing import List import pytest -import pytest_asyncio from chiabip158 import PyBIP158 -from tests.setup_nodes import setup_simulators_and_wallets - - -@pytest_asyncio.fixture(scope="function") -async def wallet_and_node(): - async for _ in setup_simulators_and_wallets(1, 1, {}): - yield _ - class TestFilter: @pytest.mark.asyncio diff --git a/tests/core/test_full_node_rpc.py b/tests/core/test_full_node_rpc.py index ee6a58c30ecd..e2bff665c9b2 100644 --- a/tests/core/test_full_node_rpc.py +++ b/tests/core/test_full_node_rpc.py @@ -1,9 +1,7 @@ # flake8: noqa: F811, F401 -import logging from typing import List import pytest -import pytest_asyncio from blspy import AugSchemeMPL from chia.consensus.pot_iterations import is_overflow_block @@ -16,22 +14,16 @@ from chia.types.full_block import FullBlock from chia.types.spend_bundle import SpendBundle from chia.types.unfinished_block import UnfinishedBlock -from tests.block_tools import get_signage_point from chia.util.hash import std_hash -from chia.util.ints import uint16, uint8 +from chia.util.ints import uint8 +from tests.block_tools import get_signage_point from tests.blockchain.blockchain_test_utils import _validate_and_add_block -from tests.wallet_tools import WalletTool from tests.connection_utils import connect_and_get_peer -from tests.setup_nodes import setup_simulators_and_wallets, test_constants +from tests.setup_nodes import test_constants from tests.time_out_assert import time_out_assert from tests.util.rpc import validate_get_routes from tests.util.socket import find_available_listen_port - - -@pytest_asyncio.fixture(scope="function") -async def two_nodes_sim_and_wallets(): - async for _ in setup_simulators_and_wallets(2, 0, {}): - yield _ +from tests.wallet_tools import WalletTool class TestRpc: diff --git a/tests/pools/test_pool_rpc.py b/tests/pools/test_pool_rpc.py index 20f627bc3124..0a4d9968599a 100644 --- a/tests/pools/test_pool_rpc.py +++ b/tests/pools/test_pool_rpc.py @@ -91,12 +91,6 @@ async def wallet_is_synced(wallet_node: WalletNode, full_node_api): PREFARMED_BLOCKS = 4 -@pytest_asyncio.fixture(scope="function") -async def two_wallet_nodes(): - async for _ in setup_simulators_and_wallets(1, 2, {}): - yield _ - - @pytest_asyncio.fixture(scope="function") async def one_wallet_node_and_rpc(bt, self_hostname): rmtree(get_pool_plot_dir(), ignore_errors=True) diff --git a/tests/wallet/cat_wallet/test_cat_lifecycle.py b/tests/wallet/cat_wallet/test_cat_lifecycle.py index b4bdf889b126..c56a6d95ab49 100644 --- a/tests/wallet/cat_wallet/test_cat_lifecycle.py +++ b/tests/wallet/cat_wallet/test_cat_lifecycle.py @@ -1,49 +1,39 @@ -import pytest -import pytest_asyncio - from typing import List, Tuple, Optional, Dict + +import pytest from blspy import PrivateKey, AugSchemeMPL, G2Element from clvm.casts import int_to_bytes from chia.clvm.spend_sim import SpendSim, SimClient -from chia.types.blockchain_format.program import Program from chia.types.blockchain_format.coin import Coin +from chia.types.blockchain_format.program import Program from chia.types.blockchain_format.sized_bytes import bytes32 -from chia.types.spend_bundle import SpendBundle from chia.types.coin_spend import CoinSpend from chia.types.mempool_inclusion_status import MempoolInclusionStatus +from chia.types.spend_bundle import SpendBundle from chia.util.errors import Err from chia.util.ints import uint64 -from chia.wallet.lineage_proof import LineageProof from chia.wallet.cat_wallet.cat_utils import ( CAT_MOD, SpendableCAT, construct_cat_puzzle, unsigned_spend_bundle_for_spendable_cats, ) +from chia.wallet.lineage_proof import LineageProof from chia.wallet.puzzles.tails import ( GenesisById, GenesisByPuzhash, EverythingWithSig, DelegatedLimitations, ) - -from tests.clvm.test_puzzles import secret_exponent_for_index from tests.clvm.benchmark_costs import cost_of_spend_bundle +from tests.clvm.test_puzzles import secret_exponent_for_index acs = Program.to(1) acs_ph = acs.get_tree_hash() NO_LINEAGE_PROOF = LineageProof() -@pytest_asyncio.fixture(scope="function") -async def setup_sim(): - sim = await SpendSim.create() - sim_client = SimClient(sim) - await sim.farm_block() - return sim, sim_client - - async def do_spend( sim: SpendSim, sim_client: SimClient, diff --git a/tests/wallet/cat_wallet/test_cat_wallet.py b/tests/wallet/cat_wallet/test_cat_wallet.py index 808a76c5291a..a8c57100a84c 100644 --- a/tests/wallet/cat_wallet/test_cat_wallet.py +++ b/tests/wallet/cat_wallet/test_cat_wallet.py @@ -2,7 +2,6 @@ from typing import List import pytest -import pytest_asyncio from chia.consensus.block_rewards import calculate_base_farmer_reward, calculate_pool_reward from chia.full_node.mempool_manager import MempoolManager @@ -11,15 +10,14 @@ from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.peer_info import PeerInfo from chia.util.ints import uint16, uint32, uint64 -from chia.wallet.cat_wallet.cat_utils import construct_cat_puzzle -from chia.wallet.cat_wallet.cat_wallet import CATWallet from chia.wallet.cat_wallet.cat_constants import DEFAULT_CATS from chia.wallet.cat_wallet.cat_info import LegacyCATInfo +from chia.wallet.cat_wallet.cat_utils import construct_cat_puzzle +from chia.wallet.cat_wallet.cat_wallet import CATWallet from chia.wallet.puzzles.cat_loader import CAT_MOD from chia.wallet.transaction_record import TransactionRecord from chia.wallet.wallet_info import WalletInfo from tests.pools.test_pool_rpc import wallet_is_synced -from tests.setup_nodes import setup_simulators_and_wallets from tests.time_out_assert import time_out_assert @@ -30,24 +28,6 @@ async def tx_in_pool(mempool: MempoolManager, tx_id: bytes32): return True -@pytest_asyncio.fixture(scope="function") -async def wallet_node_sim_and_wallet(): - async for _ in setup_simulators_and_wallets(1, 1, {}): - yield _ - - -@pytest_asyncio.fixture(scope="function") -async def two_wallet_nodes(): - async for _ in setup_simulators_and_wallets(1, 2, {}): - yield _ - - -@pytest_asyncio.fixture(scope="function") -async def three_wallet_nodes(): - async for _ in setup_simulators_and_wallets(1, 3, {}): - yield _ - - class TestCATWallet: @pytest.mark.parametrize( "trusted", diff --git a/tests/wallet/cat_wallet/test_offer_lifecycle.py b/tests/wallet/cat_wallet/test_offer_lifecycle.py index 7f1e9272e39c..a2777376e8c7 100644 --- a/tests/wallet/cat_wallet/test_offer_lifecycle.py +++ b/tests/wallet/cat_wallet/test_offer_lifecycle.py @@ -1,17 +1,15 @@ -import pytest -import pytest_asyncio - from typing import Dict, Optional, List + +import pytest from blspy import G2Element -from chia.clvm.spend_sim import SpendSim, SimClient +from chia.types.announcement import Announcement from chia.types.blockchain_format.coin import Coin -from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.blockchain_format.program import Program -from chia.types.announcement import Announcement -from chia.types.spend_bundle import SpendBundle +from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.coin_spend import CoinSpend from chia.types.mempool_inclusion_status import MempoolInclusionStatus +from chia.types.spend_bundle import SpendBundle from chia.util.ints import uint64 from chia.wallet.cat_wallet.cat_utils import ( CAT_MOD, @@ -21,7 +19,6 @@ ) from chia.wallet.payment import Payment from chia.wallet.trading.offer import Offer, NotarizedPayment - from tests.clvm.benchmark_costs import cost_of_spend_bundle acs = Program.to(1) @@ -41,14 +38,6 @@ def str_to_cat_hash(tail_str: str) -> bytes32: return construct_cat_puzzle(CAT_MOD, str_to_tail_hash(tail_str), acs).get_tree_hash() -@pytest_asyncio.fixture(scope="function") -async def setup_sim(): - sim = await SpendSim.create() - sim_client = SimClient(sim) - await sim.farm_block() - return sim, sim_client - - # This method takes a dictionary of strings mapping to amounts and generates the appropriate CAT/XCH coins async def generate_coins( sim, diff --git a/tests/wallet/cat_wallet/test_trades.py b/tests/wallet/cat_wallet/test_trades.py index 826cd786ede7..2ac4701bb25f 100644 --- a/tests/wallet/cat_wallet/test_trades.py +++ b/tests/wallet/cat_wallet/test_trades.py @@ -3,19 +3,15 @@ from typing import List import pytest -import pytest_asyncio from chia.full_node.mempool_manager import MempoolManager from chia.simulator.simulator_protocol import FarmNewBlockProtocol -from chia.types.peer_info import PeerInfo -from chia.util.ints import uint16, uint64 +from chia.util.ints import uint64 from chia.wallet.cat_wallet.cat_wallet import CATWallet from chia.wallet.trading.offer import Offer from chia.wallet.trading.trade_status import TradeStatus from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.transaction_type import TransactionType -from tests.pools.test_pool_rpc import wallet_is_synced -from tests.setup_nodes import setup_simulators_and_wallets from tests.time_out_assert import time_out_assert @@ -26,58 +22,9 @@ async def tx_in_pool(mempool: MempoolManager, tx_id): return True -@pytest_asyncio.fixture(scope="function") -async def two_wallet_nodes(): - async for _ in setup_simulators_and_wallets(1, 2, {}): - yield _ - - buffer_blocks = 4 -@pytest_asyncio.fixture(scope="function") -async def wallets_prefarm(two_wallet_nodes, self_hostname, trusted): - """ - Sets up the node with 10 blocks, and returns a payer and payee wallet. - """ - farm_blocks = 10 - buffer = 4 - full_nodes, wallets = two_wallet_nodes - full_node_api = full_nodes[0] - full_node_server = full_node_api.server - wallet_node_0, wallet_server_0 = wallets[0] - wallet_node_1, wallet_server_1 = wallets[1] - wallet_0 = wallet_node_0.wallet_state_manager.main_wallet - wallet_1 = wallet_node_1.wallet_state_manager.main_wallet - - ph0 = await wallet_0.get_new_puzzlehash() - ph1 = await wallet_1.get_new_puzzlehash() - - if trusted: - wallet_node_0.config["trusted_peers"] = {full_node_server.node_id.hex(): full_node_server.node_id.hex()} - wallet_node_1.config["trusted_peers"] = {full_node_server.node_id.hex(): full_node_server.node_id.hex()} - else: - wallet_node_0.config["trusted_peers"] = {} - wallet_node_1.config["trusted_peers"] = {} - - await wallet_server_0.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) - await wallet_server_1.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) - - for i in range(0, farm_blocks): - await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph0)) - - for i in range(0, farm_blocks): - await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph1)) - - for i in range(0, buffer): - await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(token_bytes())) - - await time_out_assert(10, wallet_is_synced, True, wallet_node_0, full_node_api) - await time_out_assert(10, wallet_is_synced, True, wallet_node_1, full_node_api) - - return wallet_node_0, wallet_node_1, full_node_api - - @pytest.mark.parametrize( "trusted", [True, False], diff --git a/tests/wallet/did_wallet/test_did.py b/tests/wallet/did_wallet/test_did.py index 11b9bdd80055..221a194583c0 100644 --- a/tests/wallet/did_wallet/test_did.py +++ b/tests/wallet/did_wallet/test_did.py @@ -1,43 +1,18 @@ import pytest -import pytest_asyncio +from blspy import AugSchemeMPL + +from chia.consensus.block_rewards import calculate_pool_reward, calculate_base_farmer_reward from chia.simulator.simulator_protocol import FarmNewBlockProtocol +from chia.types.blockchain_format.program import Program from chia.types.peer_info import PeerInfo +from chia.types.spend_bundle import SpendBundle from chia.util.ints import uint16, uint32, uint64 -from tests.setup_nodes import setup_simulators_and_wallets from chia.wallet.did_wallet.did_wallet import DIDWallet -from chia.types.blockchain_format.program import Program -from blspy import AugSchemeMPL -from chia.types.spend_bundle import SpendBundle -from chia.consensus.block_rewards import calculate_pool_reward, calculate_base_farmer_reward from tests.time_out_assert import time_out_assert, time_out_assert_not_none pytestmark = pytest.mark.skip("TODO: Fix tests") -@pytest_asyncio.fixture(scope="function") -async def two_wallet_nodes(): - async for _ in setup_simulators_and_wallets(1, 2, {}): - yield _ - - -@pytest_asyncio.fixture(scope="function") -async def three_wallet_nodes(): - async for _ in setup_simulators_and_wallets(1, 3, {}): - yield _ - - -@pytest_asyncio.fixture(scope="function") -async def two_wallet_nodes_five_freeze(): - async for _ in setup_simulators_and_wallets(1, 2, {}): - yield _ - - -@pytest_asyncio.fixture(scope="function") -async def three_sim_two_wallets(): - async for _ in setup_simulators_and_wallets(3, 2, {}): - yield _ - - class TestDIDWallet: @pytest.mark.asyncio async def test_creation_from_backup_file(self, self_hostname, three_wallet_nodes): diff --git a/tests/wallet/did_wallet/test_did_rpc.py b/tests/wallet/did_wallet/test_did_rpc.py index f393c3164aae..75f8cc1b4ac1 100644 --- a/tests/wallet/did_wallet/test_did_rpc.py +++ b/tests/wallet/did_wallet/test_did_rpc.py @@ -1,6 +1,6 @@ import logging + import pytest -import pytest_asyncio from chia.rpc.rpc_server import start_rpc_server from chia.rpc.wallet_rpc_api import WalletRpcApi @@ -8,24 +8,16 @@ from chia.simulator.simulator_protocol import FarmNewBlockProtocol from chia.types.peer_info import PeerInfo from chia.util.ints import uint16, uint64 +from chia.wallet.did_wallet.did_wallet import DIDWallet from chia.wallet.util.wallet_types import WalletType -from tests.setup_nodes import setup_simulators_and_wallets from tests.time_out_assert import time_out_assert -from chia.wallet.did_wallet.did_wallet import DIDWallet from tests.util.socket import find_available_listen_port - log = logging.getLogger(__name__) pytestmark = pytest.mark.skip("TODO: Fix tests") -@pytest_asyncio.fixture(scope="function") -async def three_wallet_nodes(): - async for _ in setup_simulators_and_wallets(1, 3, {}): - yield _ - - class TestDIDWallet: @pytest.mark.asyncio async def test_create_did(self, bt, three_wallet_nodes, self_hostname): diff --git a/tests/wallet/rl_wallet/test_rl_rpc.py b/tests/wallet/rl_wallet/test_rl_rpc.py index e387646e79cc..78a6f58da7e7 100644 --- a/tests/wallet/rl_wallet/test_rl_rpc.py +++ b/tests/wallet/rl_wallet/test_rl_rpc.py @@ -1,7 +1,6 @@ import asyncio import pytest -import pytest_asyncio from chia.rpc.wallet_rpc_api import WalletRpcApi from chia.simulator.simulator_protocol import FarmNewBlockProtocol @@ -13,7 +12,6 @@ from chia.util.ints import uint16 from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.wallet_types import WalletType -from tests.setup_nodes import setup_simulators_and_wallets from tests.time_out_assert import time_out_assert from tests.wallet.sync.test_wallet_sync import wallet_height_at_least @@ -46,12 +44,6 @@ async def check_balance(api, wallet_id): return balance -@pytest_asyncio.fixture(scope="function") -async def three_wallet_nodes(): - async for _ in setup_simulators_and_wallets(1, 3, {}): - yield _ - - class TestRLWallet: @pytest.mark.asyncio @pytest.mark.skip diff --git a/tests/wallet/rl_wallet/test_rl_wallet.py b/tests/wallet/rl_wallet/test_rl_wallet.py index cee43b28fcb6..ebf0ddd595f5 100644 --- a/tests/wallet/rl_wallet/test_rl_wallet.py +++ b/tests/wallet/rl_wallet/test_rl_wallet.py @@ -1,20 +1,12 @@ import pytest -import pytest_asyncio from chia.simulator.simulator_protocol import FarmNewBlockProtocol from chia.types.peer_info import PeerInfo from chia.util.ints import uint16, uint64 from chia.wallet.rl_wallet.rl_wallet import RLWallet -from tests.setup_nodes import setup_simulators_and_wallets from tests.time_out_assert import time_out_assert -@pytest_asyncio.fixture(scope="function") -async def two_wallet_nodes(): - async for _ in setup_simulators_and_wallets(1, 2, {}): - yield _ - - class TestCATWallet: @pytest.mark.asyncio @pytest.mark.skip diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index ee8ec4c855ff..f92570b31ab6 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -1,19 +1,13 @@ import asyncio -from typing import Dict, Optional - -from blspy import G2Element - -from chia.types.coin_record import CoinRecord -from chia.types.coin_spend import CoinSpend -from chia.types.spend_bundle import SpendBundle -from chia.util.config import lock_and_load_config, save_config -from operator import attrgetter import logging +from operator import attrgetter +from typing import Dict, Optional import pytest -import pytest_asyncio +from blspy import G2Element from chia.consensus.block_rewards import calculate_base_farmer_reward, calculate_pool_reward +from chia.consensus.coinbase import create_puzzlehash_for_pk from chia.rpc.full_node_rpc_api import FullNodeRpcApi from chia.rpc.full_node_rpc_client import FullNodeRpcClient from chia.rpc.rpc_server import start_rpc_server @@ -22,33 +16,29 @@ from chia.simulator.simulator_protocol import FarmNewBlockProtocol from chia.types.announcement import Announcement from chia.types.blockchain_format.program import Program +from chia.types.coin_record import CoinRecord +from chia.types.coin_spend import CoinSpend from chia.types.peer_info import PeerInfo +from chia.types.spend_bundle import SpendBundle from chia.util.bech32m import decode_puzzle_hash, encode_puzzle_hash -from chia.consensus.coinbase import create_puzzlehash_for_pk +from chia.util.config import lock_and_load_config, save_config from chia.util.hash import std_hash -from chia.wallet.derive_keys import master_sk_to_wallet_sk from chia.util.ints import uint16, uint32, uint64 from chia.wallet.cat_wallet.cat_constants import DEFAULT_CATS from chia.wallet.cat_wallet.cat_wallet import CATWallet +from chia.wallet.derive_keys import master_sk_to_wallet_sk from chia.wallet.trading.trade_status import TradeStatus from chia.wallet.transaction_record import TransactionRecord from chia.wallet.transaction_sorting import SortKey from chia.wallet.util.compute_memos import compute_memos from chia.wallet.util.wallet_types import WalletType from tests.pools.test_pool_rpc import wallet_is_synced -from tests.setup_nodes import setup_simulators_and_wallets from tests.time_out_assert import time_out_assert from tests.util.socket import find_available_listen_port log = logging.getLogger(__name__) -@pytest_asyncio.fixture(scope="function") -async def two_wallet_nodes(): - async for _ in setup_simulators_and_wallets(1, 2, {}): - yield _ - - async def assert_wallet_types(client: WalletRpcClient, expected: Dict[WalletType, int]) -> None: for wallet_type in WalletType: wallets = await client.get_wallets(wallet_type) diff --git a/tests/wallet/simple_sync/test_simple_sync_protocol.py b/tests/wallet/simple_sync/test_simple_sync_protocol.py index 2e45359f5730..bcbd59c5b756 100644 --- a/tests/wallet/simple_sync/test_simple_sync_protocol.py +++ b/tests/wallet/simple_sync/test_simple_sync_protocol.py @@ -3,15 +3,14 @@ from typing import List, Optional import pytest -import pytest_asyncio from clvm.casts import int_to_bytes from colorlog import getLogger from chia.consensus.block_rewards import calculate_pool_reward, calculate_base_farmer_reward -from chia.protocols import wallet_protocol, full_node_protocol +from chia.protocols import wallet_protocol from chia.protocols.full_node_protocol import RespondTransaction from chia.protocols.protocol_message_types import ProtocolMessageTypes -from chia.protocols.wallet_protocol import RespondToCoinUpdates, CoinStateUpdate, RespondToPhUpdates, CoinState +from chia.protocols.wallet_protocol import RespondToCoinUpdates, CoinStateUpdate, RespondToPhUpdates from chia.server.outbound_message import NodeType from chia.simulator.simulator_protocol import FarmNewBlockProtocol, ReorgProtocol from chia.types.blockchain_format.coin import Coin @@ -25,7 +24,6 @@ from chia.wallet.wallet_state_manager import WalletStateManager from tests.connection_utils import add_dummy_connection from tests.pools.test_pool_rpc import wallet_is_synced -from tests.setup_nodes import setup_simulators_and_wallets from tests.time_out_assert import time_out_assert from tests.wallet.cat_wallet.test_cat_wallet import tx_in_pool from tests.wallet_tools import WalletTool @@ -41,18 +39,6 @@ def wallet_height_at_least(wallet_node, h): log = getLogger(__name__) -@pytest_asyncio.fixture(scope="function") -async def wallet_node_simulator(): - async for _ in setup_simulators_and_wallets(1, 1, {}): - yield _ - - -@pytest_asyncio.fixture(scope="function") -async def wallet_two_node_simulator(): - async for _ in setup_simulators_and_wallets(2, 1, {}): - yield _ - - async def get_all_messages_in_queue(queue): all_messages = [] await asyncio.sleep(2) diff --git a/tests/wallet/sync/test_wallet_sync.py b/tests/wallet/sync/test_wallet_sync.py index e904a1a8d11d..c98f78354cc7 100644 --- a/tests/wallet/sync/test_wallet_sync.py +++ b/tests/wallet/sync/test_wallet_sync.py @@ -2,7 +2,6 @@ import asyncio import pytest -import pytest_asyncio from colorlog import getLogger from chia.consensus.block_rewards import calculate_base_farmer_reward, calculate_pool_reward @@ -13,7 +12,7 @@ from chia.wallet.wallet_state_manager import WalletStateManager from tests.connection_utils import disconnect_all_and_reconnect from tests.pools.test_pool_rpc import wallet_is_synced -from tests.setup_nodes import setup_node_and_wallet, setup_simulators_and_wallets, test_constants +from tests.setup_nodes import test_constants from tests.time_out_assert import time_out_assert @@ -27,24 +26,6 @@ def wallet_height_at_least(wallet_node, h): log = getLogger(__name__) -@pytest_asyncio.fixture(scope="function") -async def wallet_node(self_hostname): - async for _ in setup_node_and_wallet(test_constants, self_hostname): - yield _ - - -@pytest_asyncio.fixture(scope="function") -async def wallet_node_simulator(): - async for _ in setup_simulators_and_wallets(1, 1, {}): - yield _ - - -@pytest_asyncio.fixture(scope="function") -async def wallet_node_starting_height(self_hostname): - async for _ in setup_node_and_wallet(test_constants, self_hostname, starting_height=100): - yield _ - - class TestWalletSync: @pytest.mark.parametrize( "trusted", diff --git a/tests/wallet/test_wallet.py b/tests/wallet/test_wallet.py index bd75233fcbb4..02467334ac4b 100644 --- a/tests/wallet/test_wallet.py +++ b/tests/wallet/test_wallet.py @@ -1,7 +1,8 @@ import asyncio -import pytest -import pytest_asyncio import time + +import pytest + from chia.consensus.block_rewards import calculate_base_farmer_reward, calculate_pool_reward from chia.protocols.full_node_protocol import RespondBlock from chia.server.server import ChiaServer @@ -11,52 +12,15 @@ from chia.types.peer_info import PeerInfo from chia.util.ints import uint16, uint32, uint64 from chia.wallet.derive_keys import master_sk_to_wallet_sk -from chia.wallet.util.transaction_type import TransactionType -from chia.wallet.util.compute_memos import compute_memos from chia.wallet.transaction_record import TransactionRecord +from chia.wallet.util.compute_memos import compute_memos +from chia.wallet.util.transaction_type import TransactionType from chia.wallet.wallet_node import WalletNode from chia.wallet.wallet_state_manager import WalletStateManager -from tests.setup_nodes import setup_simulators_and_wallets from tests.time_out_assert import time_out_assert, time_out_assert_not_none from tests.wallet.cat_wallet.test_cat_wallet import tx_in_pool -@pytest_asyncio.fixture(scope="function") -async def wallet_node(): - async for _ in setup_simulators_and_wallets(1, 1, {}): - yield _ - - -@pytest_asyncio.fixture(scope="function") -async def wallet_node_sim_and_wallet(): - async for _ in setup_simulators_and_wallets(1, 1, {}): - yield _ - - -@pytest_asyncio.fixture(scope="function") -async def wallet_node_100_pk(): - async for _ in setup_simulators_and_wallets(1, 1, {}, initial_num_public_keys=100): - yield _ - - -@pytest_asyncio.fixture(scope="function") -async def two_wallet_nodes(): - async for _ in setup_simulators_and_wallets(1, 2, {}): - yield _ - - -@pytest_asyncio.fixture(scope="function") -async def two_wallet_nodes_five_freeze(): - async for _ in setup_simulators_and_wallets(1, 2, {}): - yield _ - - -@pytest_asyncio.fixture(scope="function") -async def three_sim_two_wallets(): - async for _ in setup_simulators_and_wallets(3, 2, {}): - yield _ - - class TestWalletSimulator: @pytest.mark.parametrize( "trusted", diff --git a/tests/wallet/test_wallet_blockchain.py b/tests/wallet/test_wallet_blockchain.py index 7b5c1e3b3669..e5a84f5984f8 100644 --- a/tests/wallet/test_wallet_blockchain.py +++ b/tests/wallet/test_wallet_blockchain.py @@ -3,7 +3,6 @@ import aiosqlite import pytest -import pytest_asyncio from chia.consensus.blockchain import ReceiveBlockResult from chia.protocols import full_node_protocol @@ -13,13 +12,7 @@ from chia.util.generator_tools import get_block_header from chia.wallet.key_val_store import KeyValStore from chia.wallet.wallet_blockchain import WalletBlockchain -from tests.setup_nodes import test_constants, setup_node_and_wallet - - -@pytest_asyncio.fixture(scope="function") -async def wallet_node(self_hostname): - async for _ in setup_node_and_wallet(test_constants, self_hostname): - yield _ +from tests.setup_nodes import test_constants class TestWalletBlockchain: From 53d8e2c5190f646345ed840ee41cc214ea341161 Mon Sep 17 00:00:00 2001 From: Jeff Date: Wed, 30 Mar 2022 09:16:51 -0700 Subject: [PATCH 292/378] Quick fix for improper v2 DB initialization when targeting testnet (#10952) --- chia/full_node/full_node.py | 1 + 1 file changed, 1 insertion(+) diff --git a/chia/full_node/full_node.py b/chia/full_node/full_node.py index 172abd23b152..0eeeede68835 100644 --- a/chia/full_node/full_node.py +++ b/chia/full_node/full_node.py @@ -202,6 +202,7 @@ def sql_trace_callback(req: str): # this is a new DB file. Make it v2 async with self.db_wrapper.write_db() as w_conn: await set_db_version_async(w_conn, 2) + self.db_wrapper.db_version = 2 except sqlite3.OperationalError: # it could be a database created with "chia init", which is # empty except it has the database_version table From 40f2d42c73abb7fa855827806a346d43a4ac2c3a Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Wed, 30 Mar 2022 19:01:23 +0200 Subject: [PATCH 293/378] bump timing threashold for mempool performance test (#10953) --- tests/core/full_node/test_mempool_performance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/core/full_node/test_mempool_performance.py b/tests/core/full_node/test_mempool_performance.py index 96b44d6a8ca0..b887863e55e6 100644 --- a/tests/core/full_node/test_mempool_performance.py +++ b/tests/core/full_node/test_mempool_performance.py @@ -77,4 +77,4 @@ async def test_mempool_update_performance(self, bt, wallet_nodes_mempool_perf, d if idx >= len(blocks) - 3: assert duration < 0.1 else: - assert duration < 0.0002 + assert duration < 0.0003 From 02bf8489d2d9ca5350ba36f4f8bee499006d26d6 Mon Sep 17 00:00:00 2001 From: Mariano Sorgente <3069354+mariano54@users.noreply.github.com> Date: Thu, 31 Mar 2022 11:23:34 -0400 Subject: [PATCH 294/378] Ms.parallel pool t (#10966) * Try parallel pool tests * Also change workflow files * Run less combinations * Todo for bad test * Try lower n --- .github/workflows/build-test-macos-pools.yml | 2 +- .github/workflows/build-test-ubuntu-pools.yml | 2 +- tests/block_tools.py | 7 +- tests/build-workflows.py | 5 +- tests/pools/config.py | 2 + tests/pools/test_pool_rpc.py | 83 +++++++++---------- 6 files changed, 54 insertions(+), 47 deletions(-) diff --git a/.github/workflows/build-test-macos-pools.yml b/.github/workflows/build-test-macos-pools.yml index 80ff69a5db91..ced34bc30787 100644 --- a/.github/workflows/build-test-macos-pools.yml +++ b/.github/workflows/build-test-macos-pools.yml @@ -85,7 +85,7 @@ jobs: - name: Test pools code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/pools/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/pools/test_*.py -s -v --durations 0 -n 2 -m "not benchmark" - name: Process coverage data run: | diff --git a/.github/workflows/build-test-ubuntu-pools.yml b/.github/workflows/build-test-ubuntu-pools.yml index d083a12d7c20..91e5d819dc0e 100644 --- a/.github/workflows/build-test-ubuntu-pools.yml +++ b/.github/workflows/build-test-ubuntu-pools.yml @@ -84,7 +84,7 @@ jobs: - name: Test pools code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/pools/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/pools/test_*.py -s -v --durations 0 -n 2 -m "not benchmark" -p no:monitor - name: Process coverage data run: | diff --git a/tests/block_tools.py b/tests/block_tools.py index dedd9d421263..f358b955cf08 100644 --- a/tests/block_tools.py +++ b/tests/block_tools.py @@ -265,6 +265,7 @@ async def new_plot( self, pool_contract_puzzle_hash: Optional[bytes32] = None, path: Path = None, + tmp_dir: Path = None, plot_keys: Optional[PlotKeys] = None, exclude_final_dir: bool = False, ) -> Optional[bytes32]: @@ -272,14 +273,16 @@ async def new_plot( if path is not None: final_dir = path mkdir(final_dir) + if tmp_dir is None: + tmp_dir = self.temp_dir args = Namespace() # Can't go much lower than 20, since plots start having no solutions and more buggy args.size = 22 # Uses many plots for testing, in order to guarantee proofs of space at every height args.num = 1 args.buffer = 100 - args.tmp_dir = self.temp_dir - args.tmp2_dir = self.temp_dir + args.tmp_dir = tmp_dir + args.tmp2_dir = tmp_dir args.final_dir = final_dir args.plotid = None args.memo = None diff --git a/tests/build-workflows.py b/tests/build-workflows.py index b9c8e2c0d428..3898390a48ad 100755 --- a/tests/build-workflows.py +++ b/tests/build-workflows.py @@ -83,7 +83,10 @@ def generate_replacements(conf, dir): ] = "# Omitted checking out blocks and plots repo Chia-Network/test-cache" if not conf["install_timelord"]: replacements["INSTALL_TIMELORD"] = "# Omitted installing Timelord" - replacements["PYTEST_PARALLEL_ARGS"] = " -n 4" if conf["parallel"] else " -n 0" + if conf.get("custom_parallel_n", None): + replacements["PYTEST_PARALLEL_ARGS"] = f" -n {conf['custom_parallel_n']}" + else: + replacements["PYTEST_PARALLEL_ARGS"] = " -n 4" if conf["parallel"] else " -n 0" if conf["job_timeout"]: replacements["JOB_TIMEOUT"] = str(conf["job_timeout"]) replacements["TEST_DIR"] = "/".join([*dir.relative_to(root_path.parent).parts, "test_*.py"]) diff --git a/tests/pools/config.py b/tests/pools/config.py index d9b815b24cb2..bc4811c2c558 100644 --- a/tests/pools/config.py +++ b/tests/pools/config.py @@ -1 +1,3 @@ +custom_parallel_n = 2 +parallel = True job_timeout = 60 diff --git a/tests/pools/test_pool_rpc.py b/tests/pools/test_pool_rpc.py index 0a4d9968599a..d49b0b31038b 100644 --- a/tests/pools/test_pool_rpc.py +++ b/tests/pools/test_pool_rpc.py @@ -1,5 +1,6 @@ import asyncio import logging +import tempfile from dataclasses import dataclass from pathlib import Path from shutil import rmtree @@ -64,7 +65,9 @@ class TemporaryPoolPlot: plot_id: Optional[bytes32] = None async def __aenter__(self): - plot_id: bytes32 = await self.bt.new_plot(self.p2_singleton_puzzle_hash, get_pool_plot_dir()) + self._tmpdir = tempfile.TemporaryDirectory() + dirname = self._tmpdir.__enter__() + plot_id: bytes32 = await self.bt.new_plot(self.p2_singleton_puzzle_hash, Path(dirname), tmp_dir=Path(dirname)) assert plot_id is not None await self.bt.refresh_plots() self.plot_id = plot_id @@ -72,12 +75,7 @@ async def __aenter__(self): async def __aexit__(self, exc_type, exc_value, exc_traceback): await self.bt.delete_plot(self.plot_id) - - -async def create_pool_plot(bt: BlockTools, p2_singleton_puzzle_hash: bytes32) -> Optional[bytes32]: - plot_id = await bt.new_plot(p2_singleton_puzzle_hash, get_pool_plot_dir()) - await bt.refresh_plots() - return plot_id + self._tmpdir.__exit__(None, None, None) async def wallet_is_synced(wallet_node: WalletNode, full_node_api): @@ -165,9 +163,9 @@ async def setup(two_wallet_nodes, bt, self_hostname): class TestPoolWalletRpc: @pytest.mark.asyncio - @pytest.mark.parametrize("trusted", [True, False]) - @pytest.mark.parametrize("fee", [0, FEE_AMOUNT]) - async def test_create_new_pool_wallet_self_farm(self, one_wallet_node_and_rpc, fee, trusted, self_hostname): + @pytest.mark.parametrize("trusted_and_fee", [(True, FEE_AMOUNT), (False, 0)]) + async def test_create_new_pool_wallet_self_farm(self, one_wallet_node_and_rpc, trusted_and_fee, self_hostname): + trusted, fee = trusted_and_fee client, wallet_node_0, full_node_api = one_wallet_node_and_rpc wallet_0 = wallet_node_0.wallet_state_manager.main_wallet if trusted: @@ -235,9 +233,9 @@ async def test_create_new_pool_wallet_self_farm(self, one_wallet_node_and_rpc, f assert pool_config["pool_url"] == "" @pytest.mark.asyncio - @pytest.mark.parametrize("trusted", [True, False]) - @pytest.mark.parametrize("fee", [0, FEE_AMOUNT]) - async def test_create_new_pool_wallet_farm_to_pool(self, one_wallet_node_and_rpc, fee, trusted, self_hostname): + @pytest.mark.parametrize("trusted_and_fee", [(True, FEE_AMOUNT), (False, 0)]) + async def test_create_new_pool_wallet_farm_to_pool(self, one_wallet_node_and_rpc, trusted_and_fee, self_hostname): + trusted, fee = trusted_and_fee client, wallet_node_0, full_node_api = one_wallet_node_and_rpc wallet_0 = wallet_node_0.wallet_state_manager.main_wallet if trusted: @@ -308,9 +306,9 @@ async def test_create_new_pool_wallet_farm_to_pool(self, one_wallet_node_and_rpc assert pool_config["pool_url"] == "http://pool.example.com" @pytest.mark.asyncio - @pytest.mark.parametrize("trusted", [True, False]) - @pytest.mark.parametrize("fee", [0, FEE_AMOUNT]) - async def test_create_multiple_pool_wallets(self, one_wallet_node_and_rpc, fee, trusted, self_hostname): + @pytest.mark.parametrize("trusted_and_fee", [(True, FEE_AMOUNT), (False, 0)]) + async def test_create_multiple_pool_wallets(self, one_wallet_node_and_rpc, trusted_and_fee, self_hostname): + trusted, fee = trusted_and_fee client, wallet_node_0, full_node_api = one_wallet_node_and_rpc if trusted: wallet_node_0.config["trusted_peers"] = { @@ -374,7 +372,7 @@ async def test_create_multiple_pool_wallets(self, one_wallet_node_and_rpc, fee, assert len(await wallet_node_0.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(3)) == 0 # Doing a reorg reverts and removes the pool wallets await full_node_api.reorg_from_index_to_new_index(ReorgProtocol(uint32(0), uint32(20), our_ph_2)) - await asyncio.sleep(5) + await time_out_assert(30, wallet_is_synced, True, wallet_node_0, full_node_api) summaries_response = await client.get_wallets() assert len(summaries_response) == 1 @@ -400,7 +398,7 @@ async def test_create_multiple_pool_wallets(self, one_wallet_node_and_rpc, fee, # Test creation of many pool wallets. Use untrusted since that is the more complicated protocol, but don't # run this code more than once, since it's slow. - if fee == 0 and not trusted: + if not trusted: for i in range(22): await time_out_assert(10, wallet_is_synced, True, wallet_node_0, full_node_api) creation_tx_3: TransactionRecord = await client.create_new_pool_wallet( @@ -435,9 +433,9 @@ async def test_create_multiple_pool_wallets(self, one_wallet_node_and_rpc, fee, assert owner_sk != auth_sk @pytest.mark.asyncio - @pytest.mark.parametrize("trusted", [True, False]) - @pytest.mark.parametrize("fee", [0, FEE_AMOUNT]) - async def test_absorb_self(self, one_wallet_node_and_rpc, fee, trusted, bt, self_hostname): + @pytest.mark.parametrize("trusted_and_fee", [(True, FEE_AMOUNT), (False, 0)]) + async def test_absorb_self(self, one_wallet_node_and_rpc, trusted_and_fee, bt, self_hostname): + trusted, fee = trusted_and_fee client, wallet_node_0, full_node_api = one_wallet_node_and_rpc if trusted: wallet_node_0.config["trusted_peers"] = { @@ -548,9 +546,9 @@ async def test_absorb_self(self, one_wallet_node_and_rpc, fee, trusted, bt, self # await time_out_assert(10, wallet_0.get_confirmed_balance, total_block_rewards) @pytest.mark.asyncio - @pytest.mark.parametrize("trusted", [True, False]) - @pytest.mark.parametrize("fee", [0, FEE_AMOUNT]) - async def test_absorb_pooling(self, one_wallet_node_and_rpc, fee, trusted, bt, self_hostname): + @pytest.mark.parametrize("trusted_and_fee", [(True, FEE_AMOUNT), (False, 0)]) + async def test_absorb_pooling(self, one_wallet_node_and_rpc, trusted_and_fee, bt, self_hostname): + trusted, fee = trusted_and_fee client, wallet_node_0, full_node_api = one_wallet_node_and_rpc if trusted: wallet_node_0.config["trusted_peers"] = { @@ -693,10 +691,14 @@ async def test_absorb_pooling(self, one_wallet_node_and_rpc, fee, trusted, bt, s assert (250000000000 + fee) in [tx.additions[0].amount for tx in tx1] @pytest.mark.asyncio - @pytest.mark.parametrize("trusted", [True]) - @pytest.mark.parametrize("fee", [0]) - async def test_self_pooling_to_pooling(self, setup, fee, trusted, self_hostname): - """This tests self-pooling -> pooling""" + @pytest.mark.parametrize("trusted_and_fee", [(True, 0), (False, 0)]) + async def test_self_pooling_to_pooling(self, setup, trusted_and_fee, self_hostname): + """ + This tests self-pooling -> pooling + TODO: Fix this test for a positive fee value + """ + + trusted, fee = trusted_and_fee num_blocks = 4 # Num blocks to farm at a time total_blocks = 0 # Total blocks farmed so far full_nodes, wallet_nodes, receive_address, client, rpc_cleanup = setup @@ -793,8 +795,8 @@ async def tx_is_in_mempool(wid, tx: TransactionRecord): fetched: Optional[TransactionRecord] = await client.get_transaction(wid, tx.name) return fetched is not None and fetched.is_in_mempool() - await time_out_assert(5, tx_is_in_mempool, True, wallet_id, join_pool_tx) - await time_out_assert(5, tx_is_in_mempool, True, wallet_id_2, join_pool_tx_2) + await time_out_assert(10, tx_is_in_mempool, True, wallet_id, join_pool_tx) + await time_out_assert(10, tx_is_in_mempool, True, wallet_id_2, join_pool_tx_2) assert status.current.state == PoolSingletonState.SELF_POOLING.value assert status.target is not None @@ -821,13 +823,10 @@ async def status_is_farming_to_pool(w_id: int): await rpc_cleanup() @pytest.mark.asyncio - @pytest.mark.parametrize("trusted", [True, False]) - @pytest.mark.parametrize( - "fee", - [0, FEE_AMOUNT], - ) - async def test_leave_pool(self, setup, fee, trusted, self_hostname): + @pytest.mark.parametrize("trusted_and_fee", [(True, FEE_AMOUNT), (False, 0)]) + async def test_leave_pool(self, setup, trusted_and_fee, self_hostname): """This tests self-pooling -> pooling -> escaping -> self pooling""" + trusted, fee = trusted_and_fee full_nodes, wallet_nodes, receive_address, client, rpc_cleanup = setup our_ph = receive_address[0] wallets = [wallet_n.wallet_state_manager.main_wallet for wallet_n in wallet_nodes] @@ -942,10 +941,10 @@ async def status_is_self_pooling(): await rpc_cleanup() @pytest.mark.asyncio - @pytest.mark.parametrize("trusted", [True, False]) - @pytest.mark.parametrize("fee", [0, FEE_AMOUNT]) - async def test_change_pools(self, setup, fee, trusted, self_hostname): + @pytest.mark.parametrize("trusted_and_fee", [(True, FEE_AMOUNT), (False, 0)]) + async def test_change_pools(self, setup, trusted_and_fee, self_hostname): """This tests Pool A -> escaping -> Pool B""" + trusted, fee = trusted_and_fee full_nodes, wallet_nodes, receive_address, client, rpc_cleanup = setup our_ph = receive_address[0] pool_a_ph = receive_address[1] @@ -1042,10 +1041,10 @@ async def status_is_leaving(): await rpc_cleanup() @pytest.mark.asyncio - @pytest.mark.parametrize("trusted", [True, False]) - @pytest.mark.parametrize("fee", [0, FEE_AMOUNT]) - async def test_change_pools_reorg(self, setup, fee, trusted, bt, self_hostname): + @pytest.mark.parametrize("trusted_and_fee", [(True, FEE_AMOUNT), (False, 0)]) + async def test_change_pools_reorg(self, setup, trusted_and_fee, bt, self_hostname): """This tests Pool A -> escaping -> reorg -> escaping -> Pool B""" + trusted, fee = trusted_and_fee full_nodes, wallet_nodes, receive_address, client, rpc_cleanup = setup our_ph = receive_address[0] pool_a_ph = receive_address[1] From bf15087c7997f7fef83d612c6b41f37123ae5389 Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Thu, 31 Mar 2022 17:23:59 +0200 Subject: [PATCH 295/378] run more tests in parallel on CI (#10960) * run more tests in parallel on CI * fix test_farmer_get_harvesters to wait for plots to be loaded before asking about them --- .../build-test-macos-core-full_node-full_sync.yml | 2 +- .github/workflows/build-test-macos-core-ssl.yml | 2 +- .github/workflows/build-test-macos-core-util.yml | 2 +- .github/workflows/build-test-macos-core.yml | 2 +- .../build-test-ubuntu-core-full_node-full_sync.yml | 2 +- .github/workflows/build-test-ubuntu-core-ssl.yml | 2 +- .github/workflows/build-test-ubuntu-core-util.yml | 2 +- .github/workflows/build-test-ubuntu-core.yml | 2 +- tests/build-workflows.py | 4 +++- tests/core/config.py | 1 + tests/core/full_node/full_sync/config.py | 1 + tests/core/ssl/config.py | 1 + tests/core/test_farmer_harvester_rpc.py | 12 +++++++++--- tests/core/util/config.py | 1 + 14 files changed, 24 insertions(+), 12 deletions(-) create mode 100644 tests/core/config.py create mode 100644 tests/core/ssl/config.py create mode 100644 tests/core/util/config.py diff --git a/.github/workflows/build-test-macos-core-full_node-full_sync.yml b/.github/workflows/build-test-macos-core-full_node-full_sync.yml index f0ae27d1d65f..d8aa450771cb 100644 --- a/.github/workflows/build-test-macos-core-full_node-full_sync.yml +++ b/.github/workflows/build-test-macos-core-full_node-full_sync.yml @@ -85,7 +85,7 @@ jobs: - name: Test core-full_node-full_sync code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/full_node/full_sync/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/full_node/full_sync/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" - name: Process coverage data run: | diff --git a/.github/workflows/build-test-macos-core-ssl.yml b/.github/workflows/build-test-macos-core-ssl.yml index 6b4b17483290..3c624b9a514b 100644 --- a/.github/workflows/build-test-macos-core-ssl.yml +++ b/.github/workflows/build-test-macos-core-ssl.yml @@ -85,7 +85,7 @@ jobs: - name: Test core-ssl code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/ssl/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/ssl/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" - name: Process coverage data run: | diff --git a/.github/workflows/build-test-macos-core-util.yml b/.github/workflows/build-test-macos-core-util.yml index 2a01174dcd11..40ae0bd4255c 100644 --- a/.github/workflows/build-test-macos-core-util.yml +++ b/.github/workflows/build-test-macos-core-util.yml @@ -85,7 +85,7 @@ jobs: - name: Test core-util code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/util/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/util/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" - name: Process coverage data run: | diff --git a/.github/workflows/build-test-macos-core.yml b/.github/workflows/build-test-macos-core.yml index db9288eda47a..2924c0fe4912 100644 --- a/.github/workflows/build-test-macos-core.yml +++ b/.github/workflows/build-test-macos-core.yml @@ -85,7 +85,7 @@ jobs: - name: Test core code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" - name: Process coverage data run: | diff --git a/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml b/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml index 40f05d1bfb67..f43e01760ccb 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml @@ -84,7 +84,7 @@ jobs: - name: Test core-full_node-full_sync code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/full_node/full_sync/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/full_node/full_sync/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor - name: Process coverage data run: | diff --git a/.github/workflows/build-test-ubuntu-core-ssl.yml b/.github/workflows/build-test-ubuntu-core-ssl.yml index e5074ab76b26..fe58d94a7c72 100644 --- a/.github/workflows/build-test-ubuntu-core-ssl.yml +++ b/.github/workflows/build-test-ubuntu-core-ssl.yml @@ -84,7 +84,7 @@ jobs: - name: Test core-ssl code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/ssl/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/ssl/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor - name: Process coverage data run: | diff --git a/.github/workflows/build-test-ubuntu-core-util.yml b/.github/workflows/build-test-ubuntu-core-util.yml index 0b23005959af..8e845db65082 100644 --- a/.github/workflows/build-test-ubuntu-core-util.yml +++ b/.github/workflows/build-test-ubuntu-core-util.yml @@ -84,7 +84,7 @@ jobs: - name: Test core-util code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/util/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/util/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor - name: Process coverage data run: | diff --git a/.github/workflows/build-test-ubuntu-core.yml b/.github/workflows/build-test-ubuntu-core.yml index eb06af8df66a..345e07b5047f 100644 --- a/.github/workflows/build-test-ubuntu-core.yml +++ b/.github/workflows/build-test-ubuntu-core.yml @@ -84,7 +84,7 @@ jobs: - name: Test core code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor - name: Process coverage data run: | diff --git a/tests/build-workflows.py b/tests/build-workflows.py index 3898390a48ad..51cb0dd4fa1a 100755 --- a/tests/build-workflows.py +++ b/tests/build-workflows.py @@ -132,7 +132,9 @@ def dir_path(string): # main test_dirs = subdirs() -current_workflows: Dict[Path, str] = {file: read_file(file) for file in args.output_dir.iterdir()} +current_workflows: Dict[Path, str] = { + file: read_file(file) for file in args.output_dir.iterdir() if str(file).endswith(".yml") +} changed: bool = False for os in testconfig.oses: diff --git a/tests/core/config.py b/tests/core/config.py new file mode 100644 index 000000000000..7f9e1e1a76e4 --- /dev/null +++ b/tests/core/config.py @@ -0,0 +1 @@ +parallel = True diff --git a/tests/core/full_node/full_sync/config.py b/tests/core/full_node/full_sync/config.py index d9b815b24cb2..251dd4220dc5 100644 --- a/tests/core/full_node/full_sync/config.py +++ b/tests/core/full_node/full_sync/config.py @@ -1 +1,2 @@ job_timeout = 60 +parallel = True diff --git a/tests/core/ssl/config.py b/tests/core/ssl/config.py new file mode 100644 index 000000000000..7f9e1e1a76e4 --- /dev/null +++ b/tests/core/ssl/config.py @@ -0,0 +1 @@ +parallel = True diff --git a/tests/core/test_farmer_harvester_rpc.py b/tests/core/test_farmer_harvester_rpc.py index 226c6542beb6..72a16ba9e44d 100644 --- a/tests/core/test_farmer_harvester_rpc.py +++ b/tests/core/test_farmer_harvester_rpc.py @@ -115,9 +115,15 @@ async def test_farmer_get_harvesters(harvester_farmer_environment): farmer_api = farmer_service._api harvester = harvester_service._node - res = await harvester_rpc_client.get_plots() - num_plots = len(res["plots"]) - assert num_plots > 0 + num_plots = 0 + + async def non_zero_plots() -> bool: + res = await harvester_rpc_client.get_plots() + nonlocal num_plots + num_plots = len(res["plots"]) + return num_plots > 0 + + await time_out_assert(10, non_zero_plots) # Reset cache and force updates cache every second to make sure the farmer gets the most recent data update_interval_before = farmer_api.farmer.update_harvester_cache_interval diff --git a/tests/core/util/config.py b/tests/core/util/config.py new file mode 100644 index 000000000000..7f9e1e1a76e4 --- /dev/null +++ b/tests/core/util/config.py @@ -0,0 +1 @@ +parallel = True From 6075027e6d03d9eb68daf547eb2c17b9455c06de Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Thu, 31 Mar 2022 17:24:37 +0200 Subject: [PATCH 296/378] improve error message when a block is missing from the blockchain database (#10958) * improve error message when a block is missing from the blockchain database * Update chia/full_node/block_height_map.py Co-authored-by: Kyle Altendorf Co-authored-by: Kyle Altendorf --- chia/full_node/block_height_map.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/chia/full_node/block_height_map.py b/chia/full_node/block_height_map.py index 98a4d2826df5..ce9d2a308542 100644 --- a/chia/full_node/block_height_map.py +++ b/chia/full_node/block_height_map.py @@ -184,6 +184,10 @@ async def _load_blocks_from(self, height: uint32, prev_hash: bytes32): ordered[bytes32.fromhex(r[0])] = (r[2], bytes32.fromhex(r[1]), r[3]) while height > window_end: + if prev_hash not in ordered: + raise ValueError( + f"block with header hash is missing from your blockchain database: {prev_hash.hex()}" + ) entry = ordered[prev_hash] assert height == entry[0] + 1 height = entry[0] From 908f186c1e66356d97b6132f00573363a8ba5e84 Mon Sep 17 00:00:00 2001 From: Mariano Sorgente <3069354+mariano54@users.noreply.github.com> Date: Thu, 31 Mar 2022 11:25:57 -0400 Subject: [PATCH 297/378] Also throw DB error on double spending a coin (#10947) * Throw error on double spending a coin * Throw error on double spending a coin * Improve test --- chia/full_node/coin_store.py | 30 ++++++++++---- .../core/full_node/stores/test_coin_store.py | 40 +++++++++++++------ 2 files changed, 50 insertions(+), 20 deletions(-) diff --git a/chia/full_node/coin_store.py b/chia/full_node/coin_store.py index 4b91d80f730a..3fec82d41186 100644 --- a/chia/full_node/coin_store.py +++ b/chia/full_node/coin_store.py @@ -1,4 +1,7 @@ from typing import List, Optional, Set, Dict, Any, Tuple + +from aiosqlite import Cursor + from chia.protocols.wallet_protocol import CoinState from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.sized_bytes import bytes32 @@ -515,16 +518,27 @@ async def _set_spent(self, coin_names: List[bytes32], index: uint32): for coin_name in coin_names: r = self.coin_record_cache.get(coin_name) if r is not None: + if r.spent_block_index != uint32(0): + raise ValueError(f"Coin already spent in cache: {coin_name}") + self.coin_record_cache.put( r.name, CoinRecord(r.coin, r.confirmed_block_index, index, r.coinbase, r.timestamp) ) updates.append((index, self.maybe_to_hex(coin_name))) - if updates != []: - async with self.db_wrapper.write_db() as conn: - if self.db_wrapper.db_version == 2: - await conn.executemany("UPDATE OR FAIL coin_record SET spent_index=? WHERE coin_name=?", updates) - else: - await conn.executemany( - "UPDATE OR FAIL coin_record SET spent=1,spent_index=? WHERE coin_name=?", updates - ) + assert len(updates) == len(coin_names) + async with self.db_wrapper.write_db() as conn: + if self.db_wrapper.db_version == 2: + ret: Cursor = await conn.executemany( + "UPDATE OR FAIL coin_record SET spent_index=? WHERE coin_name=? AND spent_index=0", updates + ) + + else: + ret = await conn.executemany( + "UPDATE OR FAIL coin_record SET spent=1,spent_index=? WHERE coin_name=? AND spent_index=0", + updates, + ) + if ret.rowcount != len(coin_names): + raise ValueError( + f"Invalid operation to set spent, total updates {ret.rowcount} expected {len(coin_names)}" + ) diff --git a/tests/core/full_node/stores/test_coin_store.py b/tests/core/full_node/stores/test_coin_store.py index 883aa7d9735b..ca3564ebbc85 100644 --- a/tests/core/full_node/stores/test_coin_store.py +++ b/tests/core/full_node/stores/test_coin_store.py @@ -161,22 +161,38 @@ async def test_set_spent(self, cache_size: uint32, db_version, bt): if block.is_transaction_block(): removals: List[bytes32] = [] additions: List[Coin] = [] + async with db_wrapper.write_db(): + if block.is_transaction_block(): + assert block.foliage_transaction_block is not None + await coin_store.new_block( + block.height, + block.foliage_transaction_block.timestamp, + block.get_included_reward_coins(), + additions, + removals, + ) - if block.is_transaction_block(): - assert block.foliage_transaction_block is not None - await coin_store.new_block( - block.height, - block.foliage_transaction_block.timestamp, - block.get_included_reward_coins(), - additions, - removals, - ) - - coins = block.get_included_reward_coins() - records = [await coin_store.get_coin_record(coin.name()) for coin in coins] + coins = block.get_included_reward_coins() + records = [await coin_store.get_coin_record(coin.name()) for coin in coins] await coin_store._set_spent([r.name for r in records], block.height) + if len(records) > 0: + for r in records: + assert (await coin_store.get_coin_record(r.name)) is not None + + if cache_size > 0: + # Check that we can't spend a coin twice in cache + with pytest.raises(ValueError, match="Coin already spent"): + await coin_store._set_spent([r.name for r in records], block.height) + + for r in records: + coin_store.coin_record_cache.remove(r.name) + + # Check that we can't spend a coin twice in DB + with pytest.raises(ValueError, match="Invalid operation to set spent"): + await coin_store._set_spent([r.name for r in records], block.height) + records = [await coin_store.get_coin_record(coin.name()) for coin in coins] for record in records: assert record.spent From 8833cc351c49a7e9340626c7c9e4715a0ecd3626 Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Thu, 31 Mar 2022 17:26:23 +0200 Subject: [PATCH 298/378] reorg fixes (#10943) * when going through a reorg, maintain all chain state until the very end, when the new fork has been fully validated and added * when rolling back the chain, also rollback the height-to-hash map * add tests --- chia/consensus/blockchain.py | 8 +- chia/full_node/block_height_map.py | 1 + tests/block_tools.py | 10 +- tests/blockchain/blockchain_test_utils.py | 5 +- tests/blockchain/test_blockchain.py | 299 ++++++++++++++++++ tests/core/full_node/test_block_height_map.py | 14 +- 6 files changed, 326 insertions(+), 11 deletions(-) diff --git a/chia/consensus/blockchain.py b/chia/consensus/blockchain.py index fbbf4cc02d94..45910f8a555e 100644 --- a/chia/consensus/blockchain.py +++ b/chia/consensus/blockchain.py @@ -381,10 +381,6 @@ async def _reconsider_peak( for coin_record in roll_changes: latest_coin_state[coin_record.name] = coin_record - # Rollback sub_epoch_summaries - self.__height_map.rollback(fork_height) - await self.block_store.rollback(fork_height) - # Collect all blocks from fork point to new peak blocks_to_add: List[Tuple[FullBlock, BlockRecord]] = [] curr = block_record.header_hash @@ -445,6 +441,10 @@ async def _reconsider_peak( hint_coin_state[key] = {} hint_coin_state[key][coin_id] = latest_coin_state[coin_id] + # we made it to the end successfully + # Rollback sub_epoch_summaries + self.__height_map.rollback(fork_height) + await self.block_store.rollback(fork_height) await self.block_store.set_in_chain([(br.header_hash,) for br in records_to_add]) # Changes the peak to be the new peak diff --git a/chia/full_node/block_height_map.py b/chia/full_node/block_height_map.py index ce9d2a308542..49e60df16461 100644 --- a/chia/full_node/block_height_map.py +++ b/chia/full_node/block_height_map.py @@ -230,6 +230,7 @@ def rollback(self, fork_height: int): heights_to_delete.append(ses_included_height) for height in heights_to_delete: del self.__sub_epoch_summaries[height] + del self.__height_to_hash[(fork_height + 1) * 32 :] def get_ses(self, height: uint32) -> SubEpochSummary: return SubEpochSummary.from_bytes(self.__sub_epoch_summaries[height]) diff --git a/tests/block_tools.py b/tests/block_tools.py index f358b955cf08..e2fb177bcc5f 100644 --- a/tests/block_tools.py +++ b/tests/block_tools.py @@ -11,7 +11,7 @@ from argparse import Namespace from dataclasses import replace from pathlib import Path -from typing import Callable, Dict, List, Optional, Tuple, Any +from typing import Callable, Dict, List, Optional, Tuple, Any, Union from blspy import AugSchemeMPL, G1Element, G2Element, PrivateKey from chiabip158 import PyBIP158 @@ -435,7 +435,7 @@ def get_consecutive_blocks( normalized_to_identity_cc_sp: bool = False, normalized_to_identity_cc_ip: bool = False, current_time: bool = False, - previous_generator: CompressorArg = None, + previous_generator: Optional[Union[CompressorArg, List[uint32]]] = None, genesis_timestamp: Optional[uint64] = None, force_plot_id: Optional[bytes32] = None, ) -> List[FullBlock]: @@ -588,12 +588,14 @@ def get_consecutive_blocks( pool_target = PoolTarget(self.pool_ph, uint32(0)) if transaction_data is not None: - if previous_generator is not None: + if type(previous_generator) is CompressorArg: block_generator: Optional[BlockGenerator] = best_solution_generator_from_template( previous_generator, transaction_data ) else: block_generator = simple_solution_generator(transaction_data) + if type(previous_generator) is list: + block_generator = BlockGenerator(block_generator.program, [], previous_generator) aggregate_signature = transaction_data.aggregated_signature else: @@ -861,7 +863,7 @@ def get_consecutive_blocks( else: pool_target = PoolTarget(self.pool_ph, uint32(0)) if transaction_data is not None: - if previous_generator is not None: + if previous_generator is not None and type(previous_generator) is CompressorArg: block_generator = best_solution_generator_from_template( previous_generator, transaction_data ) diff --git a/tests/blockchain/blockchain_test_utils.py b/tests/blockchain/blockchain_test_utils.py index 93152782cf3c..ee991e74c7ce 100644 --- a/tests/blockchain/blockchain_test_utils.py +++ b/tests/blockchain/blockchain_test_utils.py @@ -4,7 +4,7 @@ from chia.consensus.multiprocess_validation import PreValidationResult from chia.types.full_block import FullBlock from chia.util.errors import Err -from chia.util.ints import uint64 +from chia.util.ints import uint64, uint32 async def check_block_store_invariant(bc: Blockchain): @@ -42,6 +42,7 @@ async def _validate_and_add_block( expected_result: Optional[ReceiveBlockResult] = None, expected_error: Optional[Err] = None, skip_prevalidation: bool = False, + fork_point_with_peak: Optional[uint32] = None, ) -> None: # Tries to validate and add the block, and checks that there are no errors in the process and that the # block is added to the peak. @@ -74,7 +75,7 @@ async def _validate_and_add_block( await check_block_store_invariant(blockchain) return None - result, err, _, _ = await blockchain.receive_block(block, results) + result, err, _, _ = await blockchain.receive_block(block, results, fork_point_with_peak=fork_point_with_peak) await check_block_store_invariant(blockchain) if expected_error is None and expected_result != ReceiveBlockResult.INVALID_BLOCK: diff --git a/tests/blockchain/test_blockchain.py b/tests/blockchain/test_blockchain.py index 99927fc0ff56..f4fb02e93091 100644 --- a/tests/blockchain/test_blockchain.py +++ b/tests/blockchain/test_blockchain.py @@ -2949,3 +2949,302 @@ async def test_get_blocks_at(self, empty_blockchain, default_1000_blocks): assert blocks assert len(blocks) == 200 assert blocks[-1].height == 199 + + +@pytest.mark.asyncio +async def test_reorg_new_ref(empty_blockchain, bt): + b = empty_blockchain + wallet_a = WalletTool(b.constants) + WALLET_A_PUZZLE_HASHES = [wallet_a.get_new_puzzlehash() for _ in range(5)] + coinbase_puzzlehash = WALLET_A_PUZZLE_HASHES[0] + receiver_puzzlehash = WALLET_A_PUZZLE_HASHES[1] + + blocks = bt.get_consecutive_blocks( + 5, + farmer_reward_puzzle_hash=coinbase_puzzlehash, + pool_reward_puzzle_hash=receiver_puzzlehash, + guarantee_transaction_block=True, + ) + + all_coins = [] + for spend_block in blocks[:5]: + for coin in list(spend_block.get_included_reward_coins()): + if coin.puzzle_hash == coinbase_puzzlehash: + all_coins.append(coin) + spend_bundle_0 = wallet_a.generate_signed_transaction(1000, receiver_puzzlehash, all_coins.pop()) + blocks = bt.get_consecutive_blocks( + 15, + block_list_input=blocks, + farmer_reward_puzzle_hash=coinbase_puzzlehash, + pool_reward_puzzle_hash=receiver_puzzlehash, + transaction_data=spend_bundle_0, + guarantee_transaction_block=True, + ) + + for block in blocks: + await _validate_and_add_block(b, block) + assert b.get_peak().height == 19 + + print("first chain done") + + # Make sure a ref back into the reorg chain itself works as expected + + blocks_reorg_chain = bt.get_consecutive_blocks( + 1, + blocks[:10], + seed=b"2", + farmer_reward_puzzle_hash=coinbase_puzzlehash, + pool_reward_puzzle_hash=receiver_puzzlehash, + ) + spend_bundle = wallet_a.generate_signed_transaction(1000, receiver_puzzlehash, all_coins.pop()) + + blocks_reorg_chain = bt.get_consecutive_blocks( + 2, + blocks_reorg_chain, + seed=b"2", + farmer_reward_puzzle_hash=coinbase_puzzlehash, + pool_reward_puzzle_hash=receiver_puzzlehash, + transaction_data=spend_bundle, + guarantee_transaction_block=True, + ) + + spend_bundle2 = wallet_a.generate_signed_transaction(1000, receiver_puzzlehash, all_coins.pop()) + blocks_reorg_chain = bt.get_consecutive_blocks( + 4, blocks_reorg_chain, seed=b"2", previous_generator=[uint32(5), uint32(11)], transaction_data=spend_bundle2 + ) + blocks_reorg_chain = bt.get_consecutive_blocks(4, blocks_reorg_chain, seed=b"2") + + for i, block in enumerate(blocks_reorg_chain): + fork_point_with_peak = None + if i < 10: + expected = ReceiveBlockResult.ALREADY_HAVE_BLOCK + elif i < 20: + expected = ReceiveBlockResult.ADDED_AS_ORPHAN + else: + expected = ReceiveBlockResult.NEW_PEAK + fork_point_with_peak = uint32(1) + await _validate_and_add_block(b, block, expected_result=expected, fork_point_with_peak=fork_point_with_peak) + assert b.get_peak().height == 20 + + +# this test doesn't reorg, but _reconsider_peak() is passed a stale +# "fork_height" to make it look like it's in a reorg, but all the same blocks +# are just added back. +@pytest.mark.asyncio +async def test_reorg_stale_fork_height(empty_blockchain, bt): + b = empty_blockchain + wallet_a = WalletTool(b.constants) + WALLET_A_PUZZLE_HASHES = [wallet_a.get_new_puzzlehash() for _ in range(5)] + coinbase_puzzlehash = WALLET_A_PUZZLE_HASHES[0] + receiver_puzzlehash = WALLET_A_PUZZLE_HASHES[1] + + blocks = bt.get_consecutive_blocks( + 5, + farmer_reward_puzzle_hash=coinbase_puzzlehash, + pool_reward_puzzle_hash=receiver_puzzlehash, + guarantee_transaction_block=True, + ) + + all_coins = [] + for spend_block in blocks: + for coin in list(spend_block.get_included_reward_coins()): + if coin.puzzle_hash == coinbase_puzzlehash: + all_coins.append(coin) + spend_bundle_0 = wallet_a.generate_signed_transaction(1000, receiver_puzzlehash, all_coins.pop()) + + # Make sure a ref back into the reorg chain itself works as expected + spend_bundle = wallet_a.generate_signed_transaction(1000, receiver_puzzlehash, all_coins.pop()) + + # make sure we have a transaction block, with at least one transaction in it + blocks = bt.get_consecutive_blocks( + 5, + blocks, + farmer_reward_puzzle_hash=coinbase_puzzlehash, + pool_reward_puzzle_hash=receiver_puzzlehash, + transaction_data=spend_bundle, + guarantee_transaction_block=True, + ) + + # this block (height 10) refers back to the generator in block 5 + spend_bundle2 = wallet_a.generate_signed_transaction(1000, receiver_puzzlehash, all_coins.pop()) + blocks = bt.get_consecutive_blocks(4, blocks, previous_generator=[uint32(5)], transaction_data=spend_bundle2) + + for block in blocks[:5]: + await _validate_and_add_block(b, block, expected_result=ReceiveBlockResult.NEW_PEAK) + + # fake the fork_height to make every new block look like a reorg + for block in blocks[5:]: + await _validate_and_add_block(b, block, expected_result=ReceiveBlockResult.NEW_PEAK, fork_point_with_peak=2) + assert b.get_peak().height == 13 + + +@pytest.mark.asyncio +async def test_chain_failed_rollback(empty_blockchain, bt): + b = empty_blockchain + wallet_a = WalletTool(b.constants) + WALLET_A_PUZZLE_HASHES = [wallet_a.get_new_puzzlehash() for _ in range(5)] + coinbase_puzzlehash = WALLET_A_PUZZLE_HASHES[0] + receiver_puzzlehash = WALLET_A_PUZZLE_HASHES[1] + + blocks = bt.get_consecutive_blocks( + 20, + farmer_reward_puzzle_hash=coinbase_puzzlehash, + pool_reward_puzzle_hash=receiver_puzzlehash, + ) + + for block in blocks: + await _validate_and_add_block(b, block) + assert b.get_peak().height == 19 + + print("first chain done") + + # Make sure a ref back into the reorg chain itself works as expected + + all_coins = [] + for spend_block in blocks[:10]: + for coin in list(spend_block.get_included_reward_coins()): + if coin.puzzle_hash == coinbase_puzzlehash: + all_coins.append(coin) + + spend_bundle = wallet_a.generate_signed_transaction(1000, receiver_puzzlehash, all_coins.pop()) + + blocks_reorg_chain = bt.get_consecutive_blocks( + 11, + blocks[:10], + seed=b"2", + farmer_reward_puzzle_hash=coinbase_puzzlehash, + pool_reward_puzzle_hash=receiver_puzzlehash, + transaction_data=spend_bundle, + guarantee_transaction_block=True, + ) + + for block in blocks_reorg_chain[10:-1]: + await _validate_and_add_block(b, block, expected_result=ReceiveBlockResult.ADDED_AS_ORPHAN) + + # Incorrectly set the height as spent in DB to trigger an error + print(f"{await b.coin_store.get_coin_record(spend_bundle.coin_spends[0].coin.name())}") + print(spend_bundle.coin_spends[0].coin.name()) + # await b.coin_store._set_spent([spend_bundle.coin_spends[0].coin.name()], 8) + await b.coin_store.rollback_to_block(2) + print(f"{await b.coin_store.get_coin_record(spend_bundle.coin_spends[0].coin.name())}") + + try: + await _validate_and_add_block(b, blocks_reorg_chain[-1]) + except AssertionError: + pass + + assert b.get_peak().height == 19 + + +@pytest.mark.asyncio +async def test_reorg_flip_flop(empty_blockchain, bt): + b = empty_blockchain + wallet_a = WalletTool(b.constants) + WALLET_A_PUZZLE_HASHES = [wallet_a.get_new_puzzlehash() for _ in range(5)] + coinbase_puzzlehash = WALLET_A_PUZZLE_HASHES[0] + receiver_puzzlehash = WALLET_A_PUZZLE_HASHES[1] + + chain_a = bt.get_consecutive_blocks( + 10, + farmer_reward_puzzle_hash=coinbase_puzzlehash, + pool_reward_puzzle_hash=receiver_puzzlehash, + guarantee_transaction_block=True, + ) + + all_coins = [] + for spend_block in chain_a: + for coin in list(spend_block.get_included_reward_coins()): + if coin.puzzle_hash == coinbase_puzzlehash: + all_coins.append(coin) + + # this is a transaction block at height 10 + spend_bundle = wallet_a.generate_signed_transaction(1000, receiver_puzzlehash, all_coins.pop()) + chain_a = bt.get_consecutive_blocks( + 5, + chain_a, + farmer_reward_puzzle_hash=coinbase_puzzlehash, + pool_reward_puzzle_hash=receiver_puzzlehash, + transaction_data=spend_bundle, + guarantee_transaction_block=True, + ) + + spend_bundle = wallet_a.generate_signed_transaction(1000, receiver_puzzlehash, all_coins.pop()) + chain_a = bt.get_consecutive_blocks(5, chain_a, previous_generator=[uint32(10)], transaction_data=spend_bundle) + + spend_bundle = wallet_a.generate_signed_transaction(1000, receiver_puzzlehash, all_coins.pop()) + chain_a = bt.get_consecutive_blocks( + 20, + chain_a, + farmer_reward_puzzle_hash=coinbase_puzzlehash, + pool_reward_puzzle_hash=receiver_puzzlehash, + transaction_data=spend_bundle, + guarantee_transaction_block=True, + ) + + # chain A is 40 blocks deep + # chain B share the first 20 blocks with chain A + + # add 5 blocks on top of the first 20, to form chain B + chain_b = bt.get_consecutive_blocks( + 5, + chain_a[:20], + seed=b"2", + farmer_reward_puzzle_hash=coinbase_puzzlehash, + pool_reward_puzzle_hash=receiver_puzzlehash, + ) + spend_bundle = wallet_a.generate_signed_transaction(1000, receiver_puzzlehash, all_coins.pop()) + + # this is a transaction block at height 15 (in Chain B) + chain_b = bt.get_consecutive_blocks( + 5, + chain_b, + seed=b"2", + farmer_reward_puzzle_hash=coinbase_puzzlehash, + pool_reward_puzzle_hash=receiver_puzzlehash, + transaction_data=spend_bundle, + guarantee_transaction_block=True, + ) + + spend_bundle = wallet_a.generate_signed_transaction(1000, receiver_puzzlehash, all_coins.pop()) + chain_b = bt.get_consecutive_blocks( + 10, chain_b, seed=b"2", previous_generator=[uint32(15)], transaction_data=spend_bundle + ) + + assert len(chain_a) == len(chain_b) + + counter = 0 + for b1, b2 in zip(chain_a, chain_b): + + # alternate the order we add blocks from the two chains, to ensure one + # chain overtakes the other one in weight every other time + if counter % 2 == 0: + block1, block2 = b2, b1 + else: + block1, block2 = b1, b2 + counter += 1 + + fork_height = 2 if counter > 3 else None + + preval: List[PreValidationResult] = await b.pre_validate_blocks_multiprocessing( + [block1], {}, validate_signatures=False + ) + result, err, _, _ = await b.receive_block(block1, preval[0], fork_point_with_peak=fork_height) + assert not err + preval: List[PreValidationResult] = await b.pre_validate_blocks_multiprocessing( + [block2], {}, validate_signatures=False + ) + result, err, _, _ = await b.receive_block(block2, preval[0], fork_point_with_peak=fork_height) + assert not err + + assert b.get_peak().height == 39 + + chain_b = bt.get_consecutive_blocks( + 10, + chain_b, + seed=b"2", + farmer_reward_puzzle_hash=coinbase_puzzlehash, + pool_reward_puzzle_hash=receiver_puzzlehash, + ) + + for block in chain_b[40:]: + await _validate_and_add_block(b, block) diff --git a/tests/core/full_node/test_block_height_map.py b/tests/core/full_node/test_block_height_map.py index ac5bf5c5499a..dfc34cf88c72 100644 --- a/tests/core/full_node/test_block_height_map.py +++ b/tests/core/full_node/test_block_height_map.py @@ -371,7 +371,15 @@ async def test_rollback(self, tmp_dir, db_version): assert height_map.get_hash(5) == gen_block_hash(5) height_map.rollback(5) - + assert height_map.contains_height(0) + assert height_map.contains_height(1) + assert height_map.contains_height(2) + assert height_map.contains_height(3) + assert height_map.contains_height(4) + assert height_map.contains_height(5) + assert not height_map.contains_height(6) + assert not height_map.contains_height(7) + assert not height_map.contains_height(8) assert height_map.get_hash(5) == gen_block_hash(5) assert height_map.get_ses(0) == gen_ses(0) @@ -401,8 +409,12 @@ async def test_rollback2(self, tmp_dir, db_version): assert height_map.get_hash(6) == gen_block_hash(6) height_map.rollback(6) + assert height_map.contains_height(6) + assert not height_map.contains_height(7) assert height_map.get_hash(6) == gen_block_hash(6) + with pytest.raises(AssertionError) as _: + height_map.get_hash(7) assert height_map.get_ses(0) == gen_ses(0) assert height_map.get_ses(2) == gen_ses(2) From a2490e07656d6fff95d36102dcc24ecbf6c3b9ae Mon Sep 17 00:00:00 2001 From: Mariano Sorgente <3069354+mariano54@users.noreply.github.com> Date: Thu, 31 Mar 2022 14:27:01 -0400 Subject: [PATCH 299/378] Fix the issues in main (failing tests) (#10977) * Fix one of the issues in test_blockchain * Only rollback after all async operations are finished --- chia/consensus/blockchain.py | 3 ++- tests/blockchain/test_blockchain.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/chia/consensus/blockchain.py b/chia/consensus/blockchain.py index 45910f8a555e..91fcd4458eb3 100644 --- a/chia/consensus/blockchain.py +++ b/chia/consensus/blockchain.py @@ -275,6 +275,8 @@ async def receive_block( # Then update the memory cache. It is important that this task is not cancelled and does not throw self.add_block_record(block_record) + if fork_height is not None: + self.__height_map.rollback(fork_height) for fetched_block_record in records: self.__height_map.update_height( fetched_block_record.height, @@ -443,7 +445,6 @@ async def _reconsider_peak( # we made it to the end successfully # Rollback sub_epoch_summaries - self.__height_map.rollback(fork_height) await self.block_store.rollback(fork_height) await self.block_store.set_in_chain([(br.header_hash,) for br in records_to_add]) diff --git a/tests/blockchain/test_blockchain.py b/tests/blockchain/test_blockchain.py index f4fb02e93091..59631b7614ba 100644 --- a/tests/blockchain/test_blockchain.py +++ b/tests/blockchain/test_blockchain.py @@ -3130,7 +3130,7 @@ async def test_chain_failed_rollback(empty_blockchain, bt): try: await _validate_and_add_block(b, blocks_reorg_chain[-1]) - except AssertionError: + except ValueError: pass assert b.get_peak().height == 19 From e5ab4cd842b7173a967c3a889437f139bf937836 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 1 Apr 2022 02:01:20 -0400 Subject: [PATCH 300/378] back to a single option for workflow parallel config (#10979) --- tests/build-workflows.py | 7 +++---- tests/pools/config.py | 3 +-- tests/testconfig.py | 9 +++++++-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/tests/build-workflows.py b/tests/build-workflows.py index 51cb0dd4fa1a..9be0e79336cc 100755 --- a/tests/build-workflows.py +++ b/tests/build-workflows.py @@ -77,16 +77,15 @@ def generate_replacements(conf, dir): "PYTEST_PARALLEL_ARGS": "", } + xdist_numprocesses = {False: 0, True: 4}.get(conf["parallel"], conf["parallel"]) + replacements["PYTEST_PARALLEL_ARGS"] = f" -n {xdist_numprocesses}" + if not conf["checkout_blocks_and_plots"]: replacements[ "CHECKOUT_TEST_BLOCKS_AND_PLOTS" ] = "# Omitted checking out blocks and plots repo Chia-Network/test-cache" if not conf["install_timelord"]: replacements["INSTALL_TIMELORD"] = "# Omitted installing Timelord" - if conf.get("custom_parallel_n", None): - replacements["PYTEST_PARALLEL_ARGS"] = f" -n {conf['custom_parallel_n']}" - else: - replacements["PYTEST_PARALLEL_ARGS"] = " -n 4" if conf["parallel"] else " -n 0" if conf["job_timeout"]: replacements["JOB_TIMEOUT"] = str(conf["job_timeout"]) replacements["TEST_DIR"] = "/".join([*dir.relative_to(root_path.parent).parts, "test_*.py"]) diff --git a/tests/pools/config.py b/tests/pools/config.py index bc4811c2c558..be5a59232c86 100644 --- a/tests/pools/config.py +++ b/tests/pools/config.py @@ -1,3 +1,2 @@ -custom_parallel_n = 2 -parallel = True +parallel = 2 job_timeout = 60 diff --git a/tests/testconfig.py b/tests/testconfig.py index 41e1720b2998..3aae8f4cea1f 100644 --- a/tests/testconfig.py +++ b/tests/testconfig.py @@ -1,10 +1,15 @@ -from typing import List +from __future__ import annotations + +from typing import TYPE_CHECKING, List, Union + +if TYPE_CHECKING: + from typing_extensions import Literal # Github actions template config. oses = ["ubuntu", "macos"] # Defaults are conservative. -parallel = False +parallel: Union[bool, int, Literal["auto"]] = False checkout_blocks_and_plots = True install_timelord = False check_resource_usage = False From a571c7889993ccba39f5fba96cffec87fc8f5822 Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Fri, 1 Apr 2022 08:01:50 +0200 Subject: [PATCH 301/378] limit test output on CI by dropping -s and -v. Also, only print the 10 slowest tests, instead of all (#10959) --- .github/workflows/build-test-macos-blockchain.yml | 2 +- .github/workflows/build-test-macos-clvm.yml | 2 +- .github/workflows/build-test-macos-core-cmds.yml | 2 +- .github/workflows/build-test-macos-core-consensus.yml | 2 +- .github/workflows/build-test-macos-core-custom_types.yml | 2 +- .github/workflows/build-test-macos-core-daemon.yml | 2 +- .github/workflows/build-test-macos-core-full_node-full_sync.yml | 2 +- .github/workflows/build-test-macos-core-full_node-stores.yml | 2 +- .github/workflows/build-test-macos-core-full_node.yml | 2 +- .github/workflows/build-test-macos-core-server.yml | 2 +- .github/workflows/build-test-macos-core-ssl.yml | 2 +- .github/workflows/build-test-macos-core-util.yml | 2 +- .github/workflows/build-test-macos-core.yml | 2 +- .github/workflows/build-test-macos-farmer_harvester.yml | 2 +- .github/workflows/build-test-macos-generator.yml | 2 +- .github/workflows/build-test-macos-plotting.yml | 2 +- .github/workflows/build-test-macos-pools.yml | 2 +- .github/workflows/build-test-macos-simulation.yml | 2 +- .github/workflows/build-test-macos-tools.yml | 2 +- .github/workflows/build-test-macos-util.yml | 2 +- .github/workflows/build-test-macos-wallet-cat_wallet.yml | 2 +- .github/workflows/build-test-macos-wallet-did_wallet.yml | 2 +- .github/workflows/build-test-macos-wallet-rl_wallet.yml | 2 +- .github/workflows/build-test-macos-wallet-rpc.yml | 2 +- .github/workflows/build-test-macos-wallet-simple_sync.yml | 2 +- .github/workflows/build-test-macos-wallet-sync.yml | 2 +- .github/workflows/build-test-macos-wallet.yml | 2 +- .github/workflows/build-test-macos-weight_proof.yml | 2 +- .github/workflows/build-test-ubuntu-blockchain.yml | 2 +- .github/workflows/build-test-ubuntu-clvm.yml | 2 +- .github/workflows/build-test-ubuntu-core-cmds.yml | 2 +- .github/workflows/build-test-ubuntu-core-consensus.yml | 2 +- .github/workflows/build-test-ubuntu-core-custom_types.yml | 2 +- .github/workflows/build-test-ubuntu-core-daemon.yml | 2 +- .../workflows/build-test-ubuntu-core-full_node-full_sync.yml | 2 +- .github/workflows/build-test-ubuntu-core-full_node-stores.yml | 2 +- .github/workflows/build-test-ubuntu-core-full_node.yml | 2 +- .github/workflows/build-test-ubuntu-core-server.yml | 2 +- .github/workflows/build-test-ubuntu-core-ssl.yml | 2 +- .github/workflows/build-test-ubuntu-core-util.yml | 2 +- .github/workflows/build-test-ubuntu-core.yml | 2 +- .github/workflows/build-test-ubuntu-farmer_harvester.yml | 2 +- .github/workflows/build-test-ubuntu-generator.yml | 2 +- .github/workflows/build-test-ubuntu-plotting.yml | 2 +- .github/workflows/build-test-ubuntu-pools.yml | 2 +- .github/workflows/build-test-ubuntu-simulation.yml | 2 +- .github/workflows/build-test-ubuntu-tools.yml | 2 +- .github/workflows/build-test-ubuntu-util.yml | 2 +- .github/workflows/build-test-ubuntu-wallet-cat_wallet.yml | 2 +- .github/workflows/build-test-ubuntu-wallet-did_wallet.yml | 2 +- .github/workflows/build-test-ubuntu-wallet-rl_wallet.yml | 2 +- .github/workflows/build-test-ubuntu-wallet-rpc.yml | 2 +- .github/workflows/build-test-ubuntu-wallet-simple_sync.yml | 2 +- .github/workflows/build-test-ubuntu-wallet-sync.yml | 2 +- .github/workflows/build-test-ubuntu-wallet.yml | 2 +- .github/workflows/build-test-ubuntu-weight_proof.yml | 2 +- tests/runner_templates/build-test-macos | 2 +- tests/runner_templates/build-test-ubuntu | 2 +- 58 files changed, 58 insertions(+), 58 deletions(-) diff --git a/.github/workflows/build-test-macos-blockchain.yml b/.github/workflows/build-test-macos-blockchain.yml index 6edf41d2d4fa..b39022abb98f 100644 --- a/.github/workflows/build-test-macos-blockchain.yml +++ b/.github/workflows/build-test-macos-blockchain.yml @@ -85,7 +85,7 @@ jobs: - name: Test blockchain code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/blockchain/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/blockchain/test_*.py --durations=10 -n 4 -m "not benchmark" - name: Process coverage data run: | diff --git a/.github/workflows/build-test-macos-clvm.yml b/.github/workflows/build-test-macos-clvm.yml index 414ff342c744..f2d496da9384 100644 --- a/.github/workflows/build-test-macos-clvm.yml +++ b/.github/workflows/build-test-macos-clvm.yml @@ -79,7 +79,7 @@ jobs: - name: Test clvm code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/clvm/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/clvm/test_*.py --durations=10 -n 4 -m "not benchmark" - name: Process coverage data run: | diff --git a/.github/workflows/build-test-macos-core-cmds.yml b/.github/workflows/build-test-macos-core-cmds.yml index 6b5548a526e8..6d64dfaa4ff4 100644 --- a/.github/workflows/build-test-macos-core-cmds.yml +++ b/.github/workflows/build-test-macos-core-cmds.yml @@ -85,7 +85,7 @@ jobs: - name: Test core-cmds code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/cmds/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/cmds/test_*.py --durations=10 -n 4 -m "not benchmark" - name: Process coverage data run: | diff --git a/.github/workflows/build-test-macos-core-consensus.yml b/.github/workflows/build-test-macos-core-consensus.yml index 0a95110a9841..d85ee8a28124 100644 --- a/.github/workflows/build-test-macos-core-consensus.yml +++ b/.github/workflows/build-test-macos-core-consensus.yml @@ -85,7 +85,7 @@ jobs: - name: Test core-consensus code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/consensus/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/consensus/test_*.py --durations=10 -n 4 -m "not benchmark" - name: Process coverage data run: | diff --git a/.github/workflows/build-test-macos-core-custom_types.yml b/.github/workflows/build-test-macos-core-custom_types.yml index d08ed4e638ef..b6f1aa512eee 100644 --- a/.github/workflows/build-test-macos-core-custom_types.yml +++ b/.github/workflows/build-test-macos-core-custom_types.yml @@ -85,7 +85,7 @@ jobs: - name: Test core-custom_types code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/custom_types/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/custom_types/test_*.py --durations=10 -n 4 -m "not benchmark" - name: Process coverage data run: | diff --git a/.github/workflows/build-test-macos-core-daemon.yml b/.github/workflows/build-test-macos-core-daemon.yml index b7c94c874314..043fb03b9122 100644 --- a/.github/workflows/build-test-macos-core-daemon.yml +++ b/.github/workflows/build-test-macos-core-daemon.yml @@ -97,7 +97,7 @@ jobs: - name: Test core-daemon code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/daemon/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/daemon/test_*.py --durations=10 -n 0 -m "not benchmark" - name: Process coverage data run: | diff --git a/.github/workflows/build-test-macos-core-full_node-full_sync.yml b/.github/workflows/build-test-macos-core-full_node-full_sync.yml index d8aa450771cb..bd45bb6dc294 100644 --- a/.github/workflows/build-test-macos-core-full_node-full_sync.yml +++ b/.github/workflows/build-test-macos-core-full_node-full_sync.yml @@ -85,7 +85,7 @@ jobs: - name: Test core-full_node-full_sync code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/full_node/full_sync/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/full_node/full_sync/test_*.py --durations=10 -n 4 -m "not benchmark" - name: Process coverage data run: | diff --git a/.github/workflows/build-test-macos-core-full_node-stores.yml b/.github/workflows/build-test-macos-core-full_node-stores.yml index c971b17215c2..65a81bb92375 100644 --- a/.github/workflows/build-test-macos-core-full_node-stores.yml +++ b/.github/workflows/build-test-macos-core-full_node-stores.yml @@ -85,7 +85,7 @@ jobs: - name: Test core-full_node-stores code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/full_node/stores/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/full_node/stores/test_*.py --durations=10 -n 4 -m "not benchmark" - name: Process coverage data run: | diff --git a/.github/workflows/build-test-macos-core-full_node.yml b/.github/workflows/build-test-macos-core-full_node.yml index 4c13a79b5dbc..5e833ac6df69 100644 --- a/.github/workflows/build-test-macos-core-full_node.yml +++ b/.github/workflows/build-test-macos-core-full_node.yml @@ -85,7 +85,7 @@ jobs: - name: Test core-full_node code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/full_node/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/full_node/test_*.py --durations=10 -n 4 -m "not benchmark" - name: Process coverage data run: | diff --git a/.github/workflows/build-test-macos-core-server.yml b/.github/workflows/build-test-macos-core-server.yml index e4bceaf93d6a..f81fc0de334a 100644 --- a/.github/workflows/build-test-macos-core-server.yml +++ b/.github/workflows/build-test-macos-core-server.yml @@ -85,7 +85,7 @@ jobs: - name: Test core-server code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/server/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/server/test_*.py --durations=10 -n 0 -m "not benchmark" - name: Process coverage data run: | diff --git a/.github/workflows/build-test-macos-core-ssl.yml b/.github/workflows/build-test-macos-core-ssl.yml index 3c624b9a514b..95f52c10eccb 100644 --- a/.github/workflows/build-test-macos-core-ssl.yml +++ b/.github/workflows/build-test-macos-core-ssl.yml @@ -85,7 +85,7 @@ jobs: - name: Test core-ssl code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/ssl/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/ssl/test_*.py --durations=10 -n 4 -m "not benchmark" - name: Process coverage data run: | diff --git a/.github/workflows/build-test-macos-core-util.yml b/.github/workflows/build-test-macos-core-util.yml index 40ae0bd4255c..984c43b7f054 100644 --- a/.github/workflows/build-test-macos-core-util.yml +++ b/.github/workflows/build-test-macos-core-util.yml @@ -85,7 +85,7 @@ jobs: - name: Test core-util code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/util/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/util/test_*.py --durations=10 -n 4 -m "not benchmark" - name: Process coverage data run: | diff --git a/.github/workflows/build-test-macos-core.yml b/.github/workflows/build-test-macos-core.yml index 2924c0fe4912..52a592a174ce 100644 --- a/.github/workflows/build-test-macos-core.yml +++ b/.github/workflows/build-test-macos-core.yml @@ -85,7 +85,7 @@ jobs: - name: Test core code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/test_*.py --durations=10 -n 4 -m "not benchmark" - name: Process coverage data run: | diff --git a/.github/workflows/build-test-macos-farmer_harvester.yml b/.github/workflows/build-test-macos-farmer_harvester.yml index d22814b66060..60bcf838f1d8 100644 --- a/.github/workflows/build-test-macos-farmer_harvester.yml +++ b/.github/workflows/build-test-macos-farmer_harvester.yml @@ -85,7 +85,7 @@ jobs: - name: Test farmer_harvester code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/farmer_harvester/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/farmer_harvester/test_*.py --durations=10 -n 0 -m "not benchmark" - name: Process coverage data run: | diff --git a/.github/workflows/build-test-macos-generator.yml b/.github/workflows/build-test-macos-generator.yml index 339d88dcaa90..77c89e44d98f 100644 --- a/.github/workflows/build-test-macos-generator.yml +++ b/.github/workflows/build-test-macos-generator.yml @@ -85,7 +85,7 @@ jobs: - name: Test generator code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/generator/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/generator/test_*.py --durations=10 -n 4 -m "not benchmark" - name: Process coverage data run: | diff --git a/.github/workflows/build-test-macos-plotting.yml b/.github/workflows/build-test-macos-plotting.yml index 0d614d04282a..83ef742b8215 100644 --- a/.github/workflows/build-test-macos-plotting.yml +++ b/.github/workflows/build-test-macos-plotting.yml @@ -85,7 +85,7 @@ jobs: - name: Test plotting code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/plotting/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/plotting/test_*.py --durations=10 -n 4 -m "not benchmark" - name: Process coverage data run: | diff --git a/.github/workflows/build-test-macos-pools.yml b/.github/workflows/build-test-macos-pools.yml index ced34bc30787..e83532a8ff08 100644 --- a/.github/workflows/build-test-macos-pools.yml +++ b/.github/workflows/build-test-macos-pools.yml @@ -85,7 +85,7 @@ jobs: - name: Test pools code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/pools/test_*.py -s -v --durations 0 -n 2 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/pools/test_*.py --durations=10 -n 2 -m "not benchmark" - name: Process coverage data run: | diff --git a/.github/workflows/build-test-macos-simulation.yml b/.github/workflows/build-test-macos-simulation.yml index 3859585c0511..5a1f7f7c3e36 100644 --- a/.github/workflows/build-test-macos-simulation.yml +++ b/.github/workflows/build-test-macos-simulation.yml @@ -97,7 +97,7 @@ jobs: - name: Test simulation code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/simulation/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/simulation/test_*.py --durations=10 -n 0 -m "not benchmark" - name: Process coverage data run: | diff --git a/.github/workflows/build-test-macos-tools.yml b/.github/workflows/build-test-macos-tools.yml index 3e4e981a3791..d304aca53d73 100644 --- a/.github/workflows/build-test-macos-tools.yml +++ b/.github/workflows/build-test-macos-tools.yml @@ -85,7 +85,7 @@ jobs: - name: Test tools code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/tools/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/tools/test_*.py --durations=10 -n 4 -m "not benchmark" - name: Process coverage data run: | diff --git a/.github/workflows/build-test-macos-util.yml b/.github/workflows/build-test-macos-util.yml index 3fa4a0c2ee06..8661fbff15f1 100644 --- a/.github/workflows/build-test-macos-util.yml +++ b/.github/workflows/build-test-macos-util.yml @@ -85,7 +85,7 @@ jobs: - name: Test util code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/util/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/util/test_*.py --durations=10 -n 4 -m "not benchmark" - name: Process coverage data run: | diff --git a/.github/workflows/build-test-macos-wallet-cat_wallet.yml b/.github/workflows/build-test-macos-wallet-cat_wallet.yml index 16be450bd0fd..32bc83d1ba00 100644 --- a/.github/workflows/build-test-macos-wallet-cat_wallet.yml +++ b/.github/workflows/build-test-macos-wallet-cat_wallet.yml @@ -85,7 +85,7 @@ jobs: - name: Test wallet-cat_wallet code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/cat_wallet/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/cat_wallet/test_*.py --durations=10 -n 0 -m "not benchmark" - name: Process coverage data run: | diff --git a/.github/workflows/build-test-macos-wallet-did_wallet.yml b/.github/workflows/build-test-macos-wallet-did_wallet.yml index 29efd583d71c..fbb4fcb43262 100644 --- a/.github/workflows/build-test-macos-wallet-did_wallet.yml +++ b/.github/workflows/build-test-macos-wallet-did_wallet.yml @@ -85,7 +85,7 @@ jobs: - name: Test wallet-did_wallet code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/did_wallet/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/did_wallet/test_*.py --durations=10 -n 0 -m "not benchmark" - name: Process coverage data run: | diff --git a/.github/workflows/build-test-macos-wallet-rl_wallet.yml b/.github/workflows/build-test-macos-wallet-rl_wallet.yml index af3cb0d9d1ef..5d74c4e6bc44 100644 --- a/.github/workflows/build-test-macos-wallet-rl_wallet.yml +++ b/.github/workflows/build-test-macos-wallet-rl_wallet.yml @@ -85,7 +85,7 @@ jobs: - name: Test wallet-rl_wallet code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/rl_wallet/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/rl_wallet/test_*.py --durations=10 -n 0 -m "not benchmark" - name: Process coverage data run: | diff --git a/.github/workflows/build-test-macos-wallet-rpc.yml b/.github/workflows/build-test-macos-wallet-rpc.yml index 9629b8d628dd..63de5e94b081 100644 --- a/.github/workflows/build-test-macos-wallet-rpc.yml +++ b/.github/workflows/build-test-macos-wallet-rpc.yml @@ -85,7 +85,7 @@ jobs: - name: Test wallet-rpc code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/rpc/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/rpc/test_*.py --durations=10 -n 0 -m "not benchmark" - name: Process coverage data run: | diff --git a/.github/workflows/build-test-macos-wallet-simple_sync.yml b/.github/workflows/build-test-macos-wallet-simple_sync.yml index 7c6ae1eee6ec..0b5ee26a5241 100644 --- a/.github/workflows/build-test-macos-wallet-simple_sync.yml +++ b/.github/workflows/build-test-macos-wallet-simple_sync.yml @@ -85,7 +85,7 @@ jobs: - name: Test wallet-simple_sync code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/simple_sync/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/simple_sync/test_*.py --durations=10 -n 0 -m "not benchmark" - name: Process coverage data run: | diff --git a/.github/workflows/build-test-macos-wallet-sync.yml b/.github/workflows/build-test-macos-wallet-sync.yml index ad293617eb27..af8635d6006a 100644 --- a/.github/workflows/build-test-macos-wallet-sync.yml +++ b/.github/workflows/build-test-macos-wallet-sync.yml @@ -85,7 +85,7 @@ jobs: - name: Test wallet-sync code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/sync/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/sync/test_*.py --durations=10 -n 0 -m "not benchmark" - name: Process coverage data run: | diff --git a/.github/workflows/build-test-macos-wallet.yml b/.github/workflows/build-test-macos-wallet.yml index 8073f213289f..a756ecb9fae6 100644 --- a/.github/workflows/build-test-macos-wallet.yml +++ b/.github/workflows/build-test-macos-wallet.yml @@ -85,7 +85,7 @@ jobs: - name: Test wallet code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/test_*.py --durations=10 -n 4 -m "not benchmark" - name: Process coverage data run: | diff --git a/.github/workflows/build-test-macos-weight_proof.yml b/.github/workflows/build-test-macos-weight_proof.yml index 2c45969587ae..019df2603309 100644 --- a/.github/workflows/build-test-macos-weight_proof.yml +++ b/.github/workflows/build-test-macos-weight_proof.yml @@ -85,7 +85,7 @@ jobs: - name: Test weight_proof code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/weight_proof/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/weight_proof/test_*.py --durations=10 -n 4 -m "not benchmark" - name: Process coverage data run: | diff --git a/.github/workflows/build-test-ubuntu-blockchain.yml b/.github/workflows/build-test-ubuntu-blockchain.yml index 94109d800d5e..fda7296d918a 100644 --- a/.github/workflows/build-test-ubuntu-blockchain.yml +++ b/.github/workflows/build-test-ubuntu-blockchain.yml @@ -84,7 +84,7 @@ jobs: - name: Test blockchain code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/blockchain/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/blockchain/test_*.py --durations=10 -n 4 -m "not benchmark" -p no:monitor - name: Process coverage data run: | diff --git a/.github/workflows/build-test-ubuntu-clvm.yml b/.github/workflows/build-test-ubuntu-clvm.yml index de7660a4887d..35275d29e874 100644 --- a/.github/workflows/build-test-ubuntu-clvm.yml +++ b/.github/workflows/build-test-ubuntu-clvm.yml @@ -78,7 +78,7 @@ jobs: - name: Test clvm code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/clvm/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/clvm/test_*.py --durations=10 -n 4 -m "not benchmark" -p no:monitor - name: Process coverage data run: | diff --git a/.github/workflows/build-test-ubuntu-core-cmds.yml b/.github/workflows/build-test-ubuntu-core-cmds.yml index 7939d45b1f28..88ee7ab3cb29 100644 --- a/.github/workflows/build-test-ubuntu-core-cmds.yml +++ b/.github/workflows/build-test-ubuntu-core-cmds.yml @@ -84,7 +84,7 @@ jobs: - name: Test core-cmds code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/cmds/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/cmds/test_*.py --durations=10 -n 4 -m "not benchmark" -p no:monitor - name: Process coverage data run: | diff --git a/.github/workflows/build-test-ubuntu-core-consensus.yml b/.github/workflows/build-test-ubuntu-core-consensus.yml index 8083c05ad54b..307f85716f7a 100644 --- a/.github/workflows/build-test-ubuntu-core-consensus.yml +++ b/.github/workflows/build-test-ubuntu-core-consensus.yml @@ -84,7 +84,7 @@ jobs: - name: Test core-consensus code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/consensus/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/consensus/test_*.py --durations=10 -n 4 -m "not benchmark" -p no:monitor - name: Process coverage data run: | diff --git a/.github/workflows/build-test-ubuntu-core-custom_types.yml b/.github/workflows/build-test-ubuntu-core-custom_types.yml index 4684f2c25d82..94f493f64188 100644 --- a/.github/workflows/build-test-ubuntu-core-custom_types.yml +++ b/.github/workflows/build-test-ubuntu-core-custom_types.yml @@ -84,7 +84,7 @@ jobs: - name: Test core-custom_types code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/custom_types/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/custom_types/test_*.py --durations=10 -n 4 -m "not benchmark" -p no:monitor - name: Process coverage data run: | diff --git a/.github/workflows/build-test-ubuntu-core-daemon.yml b/.github/workflows/build-test-ubuntu-core-daemon.yml index 524ab9ad30d9..9b69c941e912 100644 --- a/.github/workflows/build-test-ubuntu-core-daemon.yml +++ b/.github/workflows/build-test-ubuntu-core-daemon.yml @@ -96,7 +96,7 @@ jobs: - name: Test core-daemon code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/daemon/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/daemon/test_*.py --durations=10 -n 0 -m "not benchmark" -p no:monitor - name: Process coverage data run: | diff --git a/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml b/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml index f43e01760ccb..8e5769be2dcc 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml @@ -84,7 +84,7 @@ jobs: - name: Test core-full_node-full_sync code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/full_node/full_sync/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/full_node/full_sync/test_*.py --durations=10 -n 4 -m "not benchmark" -p no:monitor - name: Process coverage data run: | diff --git a/.github/workflows/build-test-ubuntu-core-full_node-stores.yml b/.github/workflows/build-test-ubuntu-core-full_node-stores.yml index d28a1294e97b..d9cc30278ce1 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node-stores.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node-stores.yml @@ -84,7 +84,7 @@ jobs: - name: Test core-full_node-stores code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/full_node/stores/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/full_node/stores/test_*.py --durations=10 -n 4 -m "not benchmark" - name: Process coverage data run: | diff --git a/.github/workflows/build-test-ubuntu-core-full_node.yml b/.github/workflows/build-test-ubuntu-core-full_node.yml index d19fb71f320f..9e703a06b356 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node.yml @@ -84,7 +84,7 @@ jobs: - name: Test core-full_node code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/full_node/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/full_node/test_*.py --durations=10 -n 4 -m "not benchmark" - name: Process coverage data run: | diff --git a/.github/workflows/build-test-ubuntu-core-server.yml b/.github/workflows/build-test-ubuntu-core-server.yml index 6a24118aa758..47bb7e7f4f90 100644 --- a/.github/workflows/build-test-ubuntu-core-server.yml +++ b/.github/workflows/build-test-ubuntu-core-server.yml @@ -84,7 +84,7 @@ jobs: - name: Test core-server code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/server/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/server/test_*.py --durations=10 -n 0 -m "not benchmark" -p no:monitor - name: Process coverage data run: | diff --git a/.github/workflows/build-test-ubuntu-core-ssl.yml b/.github/workflows/build-test-ubuntu-core-ssl.yml index fe58d94a7c72..ba3c7945f58c 100644 --- a/.github/workflows/build-test-ubuntu-core-ssl.yml +++ b/.github/workflows/build-test-ubuntu-core-ssl.yml @@ -84,7 +84,7 @@ jobs: - name: Test core-ssl code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/ssl/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/ssl/test_*.py --durations=10 -n 4 -m "not benchmark" -p no:monitor - name: Process coverage data run: | diff --git a/.github/workflows/build-test-ubuntu-core-util.yml b/.github/workflows/build-test-ubuntu-core-util.yml index 8e845db65082..938fd0f403f0 100644 --- a/.github/workflows/build-test-ubuntu-core-util.yml +++ b/.github/workflows/build-test-ubuntu-core-util.yml @@ -84,7 +84,7 @@ jobs: - name: Test core-util code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/util/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/util/test_*.py --durations=10 -n 4 -m "not benchmark" -p no:monitor - name: Process coverage data run: | diff --git a/.github/workflows/build-test-ubuntu-core.yml b/.github/workflows/build-test-ubuntu-core.yml index 345e07b5047f..01be4c71813b 100644 --- a/.github/workflows/build-test-ubuntu-core.yml +++ b/.github/workflows/build-test-ubuntu-core.yml @@ -84,7 +84,7 @@ jobs: - name: Test core code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/core/test_*.py --durations=10 -n 4 -m "not benchmark" -p no:monitor - name: Process coverage data run: | diff --git a/.github/workflows/build-test-ubuntu-farmer_harvester.yml b/.github/workflows/build-test-ubuntu-farmer_harvester.yml index 37eaa651f7d2..6cd3a81af0fc 100644 --- a/.github/workflows/build-test-ubuntu-farmer_harvester.yml +++ b/.github/workflows/build-test-ubuntu-farmer_harvester.yml @@ -84,7 +84,7 @@ jobs: - name: Test farmer_harvester code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/farmer_harvester/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/farmer_harvester/test_*.py --durations=10 -n 0 -m "not benchmark" -p no:monitor - name: Process coverage data run: | diff --git a/.github/workflows/build-test-ubuntu-generator.yml b/.github/workflows/build-test-ubuntu-generator.yml index 707deb5adb11..1aa74d5daf97 100644 --- a/.github/workflows/build-test-ubuntu-generator.yml +++ b/.github/workflows/build-test-ubuntu-generator.yml @@ -84,7 +84,7 @@ jobs: - name: Test generator code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/generator/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/generator/test_*.py --durations=10 -n 4 -m "not benchmark" -p no:monitor - name: Process coverage data run: | diff --git a/.github/workflows/build-test-ubuntu-plotting.yml b/.github/workflows/build-test-ubuntu-plotting.yml index 9561180be15b..26c186f95479 100644 --- a/.github/workflows/build-test-ubuntu-plotting.yml +++ b/.github/workflows/build-test-ubuntu-plotting.yml @@ -84,7 +84,7 @@ jobs: - name: Test plotting code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/plotting/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/plotting/test_*.py --durations=10 -n 4 -m "not benchmark" -p no:monitor - name: Process coverage data run: | diff --git a/.github/workflows/build-test-ubuntu-pools.yml b/.github/workflows/build-test-ubuntu-pools.yml index 91e5d819dc0e..9ae59262f92c 100644 --- a/.github/workflows/build-test-ubuntu-pools.yml +++ b/.github/workflows/build-test-ubuntu-pools.yml @@ -84,7 +84,7 @@ jobs: - name: Test pools code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/pools/test_*.py -s -v --durations 0 -n 2 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/pools/test_*.py --durations=10 -n 2 -m "not benchmark" -p no:monitor - name: Process coverage data run: | diff --git a/.github/workflows/build-test-ubuntu-simulation.yml b/.github/workflows/build-test-ubuntu-simulation.yml index 385fce0ed930..be0a0f55c3e4 100644 --- a/.github/workflows/build-test-ubuntu-simulation.yml +++ b/.github/workflows/build-test-ubuntu-simulation.yml @@ -96,7 +96,7 @@ jobs: - name: Test simulation code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/simulation/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/simulation/test_*.py --durations=10 -n 0 -m "not benchmark" -p no:monitor - name: Process coverage data run: | diff --git a/.github/workflows/build-test-ubuntu-tools.yml b/.github/workflows/build-test-ubuntu-tools.yml index 4a738a726eb7..8b8ff6cfbe47 100644 --- a/.github/workflows/build-test-ubuntu-tools.yml +++ b/.github/workflows/build-test-ubuntu-tools.yml @@ -84,7 +84,7 @@ jobs: - name: Test tools code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/tools/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/tools/test_*.py --durations=10 -n 4 -m "not benchmark" -p no:monitor - name: Process coverage data run: | diff --git a/.github/workflows/build-test-ubuntu-util.yml b/.github/workflows/build-test-ubuntu-util.yml index 9637c59bc4e9..6f4768ccee6f 100644 --- a/.github/workflows/build-test-ubuntu-util.yml +++ b/.github/workflows/build-test-ubuntu-util.yml @@ -84,7 +84,7 @@ jobs: - name: Test util code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/util/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/util/test_*.py --durations=10 -n 4 -m "not benchmark" -p no:monitor - name: Process coverage data run: | diff --git a/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml index b83b9aa9532d..b52dfde0c584 100644 --- a/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml @@ -84,7 +84,7 @@ jobs: - name: Test wallet-cat_wallet code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/cat_wallet/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/cat_wallet/test_*.py --durations=10 -n 0 -m "not benchmark" -p no:monitor - name: Process coverage data run: | diff --git a/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml index 084dca1d9d81..f7e9f1012f18 100644 --- a/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml @@ -84,7 +84,7 @@ jobs: - name: Test wallet-did_wallet code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/did_wallet/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/did_wallet/test_*.py --durations=10 -n 0 -m "not benchmark" -p no:monitor - name: Process coverage data run: | diff --git a/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml index 75200f14233c..3d241ad1bbba 100644 --- a/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml @@ -84,7 +84,7 @@ jobs: - name: Test wallet-rl_wallet code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/rl_wallet/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/rl_wallet/test_*.py --durations=10 -n 0 -m "not benchmark" -p no:monitor - name: Process coverage data run: | diff --git a/.github/workflows/build-test-ubuntu-wallet-rpc.yml b/.github/workflows/build-test-ubuntu-wallet-rpc.yml index 6f9a5e2277c0..05b4cef7a947 100644 --- a/.github/workflows/build-test-ubuntu-wallet-rpc.yml +++ b/.github/workflows/build-test-ubuntu-wallet-rpc.yml @@ -84,7 +84,7 @@ jobs: - name: Test wallet-rpc code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/rpc/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/rpc/test_*.py --durations=10 -n 0 -m "not benchmark" -p no:monitor - name: Process coverage data run: | diff --git a/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml b/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml index 85fd306fc81c..fdaa81b6dd29 100644 --- a/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml +++ b/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml @@ -84,7 +84,7 @@ jobs: - name: Test wallet-simple_sync code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/simple_sync/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/simple_sync/test_*.py --durations=10 -n 0 -m "not benchmark" -p no:monitor - name: Process coverage data run: | diff --git a/.github/workflows/build-test-ubuntu-wallet-sync.yml b/.github/workflows/build-test-ubuntu-wallet-sync.yml index 924485cb7e9c..797731d3f315 100644 --- a/.github/workflows/build-test-ubuntu-wallet-sync.yml +++ b/.github/workflows/build-test-ubuntu-wallet-sync.yml @@ -84,7 +84,7 @@ jobs: - name: Test wallet-sync code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/sync/test_*.py -s -v --durations 0 -n 0 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/sync/test_*.py --durations=10 -n 0 -m "not benchmark" -p no:monitor - name: Process coverage data run: | diff --git a/.github/workflows/build-test-ubuntu-wallet.yml b/.github/workflows/build-test-ubuntu-wallet.yml index 88c1c1bfa56c..28f2b1da0a36 100644 --- a/.github/workflows/build-test-ubuntu-wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet.yml @@ -84,7 +84,7 @@ jobs: - name: Test wallet code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/wallet/test_*.py --durations=10 -n 4 -m "not benchmark" -p no:monitor - name: Process coverage data run: | diff --git a/.github/workflows/build-test-ubuntu-weight_proof.yml b/.github/workflows/build-test-ubuntu-weight_proof.yml index 3a43ec6b3c51..34f33582e22f 100644 --- a/.github/workflows/build-test-ubuntu-weight_proof.yml +++ b/.github/workflows/build-test-ubuntu-weight_proof.yml @@ -84,7 +84,7 @@ jobs: - name: Test weight_proof code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/weight_proof/test_*.py -s -v --durations 0 -n 4 -m "not benchmark" -p no:monitor + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/weight_proof/test_*.py --durations=10 -n 4 -m "not benchmark" -p no:monitor - name: Process coverage data run: | diff --git a/tests/runner_templates/build-test-macos b/tests/runner_templates/build-test-macos index 6031c07ef48a..536ab47fcfa5 100644 --- a/tests/runner_templates/build-test-macos +++ b/tests/runner_templates/build-test-macos @@ -79,7 +79,7 @@ INSTALL_TIMELORD - name: Test TEST_NAME code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test TEST_DIR -s -v --durations 0PYTEST_PARALLEL_ARGS -m "not benchmark" + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test TEST_DIR --durations=10 PYTEST_PARALLEL_ARGS -m "not benchmark" - name: Process coverage data run: | diff --git a/tests/runner_templates/build-test-ubuntu b/tests/runner_templates/build-test-ubuntu index 512d848740b7..bc587a89aef1 100644 --- a/tests/runner_templates/build-test-ubuntu +++ b/tests/runner_templates/build-test-ubuntu @@ -78,7 +78,7 @@ INSTALL_TIMELORD - name: Test TEST_NAME code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test TEST_DIR -s -v --durations 0PYTEST_PARALLEL_ARGS -m "not benchmark" DISABLE_PYTEST_MONITOR + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test TEST_DIR --durations=10 PYTEST_PARALLEL_ARGS -m "not benchmark" DISABLE_PYTEST_MONITOR - name: Process coverage data run: | From 685744218e9f89044525e2209d29ecd5e8764a26 Mon Sep 17 00:00:00 2001 From: Mariano Sorgente <3069354+mariano54@users.noreply.github.com> Date: Fri, 1 Apr 2022 02:03:22 -0400 Subject: [PATCH 302/378] Ms.flaky gen speed (#10965) * Flaky test sometimes goes slower than 1 second * Add sleep to reduce flakiness * Increase timeout instead of sleeping to hopefully reduce flakiness --- tests/core/test_cost_calculation.py | 2 +- tests/wallet/test_wallet.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/core/test_cost_calculation.py b/tests/core/test_cost_calculation.py index af62892f7052..2dfdc59c49b6 100644 --- a/tests/core/test_cost_calculation.py +++ b/tests/core/test_cost_calculation.py @@ -207,7 +207,7 @@ async def test_tx_generator_speed(self, softfork_height): assert len(npc_result.npc_list) == LARGE_BLOCK_COIN_CONSUMED_COUNT log.info(f"Time spent: {duration}") - assert duration < 1 + assert duration < 2 @pytest.mark.asyncio async def test_clvm_max_cost(self, softfork_height): diff --git a/tests/wallet/test_wallet.py b/tests/wallet/test_wallet.py index 02467334ac4b..89dd3dd8bfae 100644 --- a/tests/wallet/test_wallet.py +++ b/tests/wallet/test_wallet.py @@ -733,17 +733,17 @@ async def test_address_sliding_window(self, wallet_node_100_pk, trusted, self_ho await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(puzzle_hashes[114])) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0")) - await time_out_assert(15, wallet.get_confirmed_balance, 2 * 10 ** 12) + await time_out_assert(60, wallet.get_confirmed_balance, 2 * 10 ** 12) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(puzzle_hashes[50])) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0")) - await time_out_assert(15, wallet.get_confirmed_balance, 8 * 10 ** 12) + await time_out_assert(60, wallet.get_confirmed_balance, 8 * 10 ** 12) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(puzzle_hashes[113])) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(puzzle_hashes[209])) await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0")) - await time_out_assert(15, wallet.get_confirmed_balance, 12 * 10 ** 12) + await time_out_assert(60, wallet.get_confirmed_balance, 12 * 10 ** 12) @pytest.mark.parametrize( "trusted", From 8ca0633d9f5f9cfe5cbe4bc634973ea026c4188d Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Fri, 1 Apr 2022 08:03:59 +0200 Subject: [PATCH 303/378] fix test_full_sync.py to only feed the blocks in the main chain to the node (#10974) --- chia/cmds/init_funcs.py | 1 + tools/test_full_sync.py | 11 ++++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/chia/cmds/init_funcs.py b/chia/cmds/init_funcs.py index 1bf2dea4f2e6..70dd0c1ce37f 100644 --- a/chia/cmds/init_funcs.py +++ b/chia/cmds/init_funcs.py @@ -502,6 +502,7 @@ def chia_init( db_path_replaced = new_db_path.replace("CHALLENGE", config["selected_network"]) db_path = path_from_root(root_path, db_path_replaced) + mkdir(db_path.parent) with sqlite3.connect(db_path) as connection: set_db_version(connection, 1) diff --git a/tools/test_full_sync.py b/tools/test_full_sync.py index afd3197d960b..e28a9d2f1fdd 100755 --- a/tools/test_full_sync.py +++ b/tools/test_full_sync.py @@ -64,7 +64,7 @@ async def run_sync_test(file: Path, db_version, profile: bool, single_thread: bo with tempfile.TemporaryDirectory() as root_dir: root_path = Path(root_dir) - chia_init(root_path, should_check_keys=False) + chia_init(root_path, should_check_keys=False, v1_db=(db_version == 1)) config = load_config(root_path, "config.yaml") overrides = config["network_overrides"]["constants"][config["selected_network"]] @@ -85,7 +85,9 @@ async def run_sync_test(file: Path, db_version, profile: bool, single_thread: bo counter = 0 async with aiosqlite.connect(file) as in_db: - rows = await in_db.execute("SELECT header_hash, height, block FROM full_blocks ORDER BY height") + rows = await in_db.execute( + "SELECT header_hash, height, block FROM full_blocks WHERE in_main_chain=1 ORDER BY height" + ) block_batch = [] @@ -122,7 +124,7 @@ def main() -> None: @main.command("run", short_help="run simulated full sync from an existing blockchain db") @click.argument("file", type=click.Path(), required=True) -@click.option("--db-version", type=int, required=False, default=2, help="the version of the specified db file") +@click.option("--db-version", type=int, required=False, default=2, help="the DB version to use in simulated node") @click.option("--profile", is_flag=True, required=False, default=False, help="dump CPU profiles for slow batches") @click.option( "--single-thread", @@ -132,6 +134,9 @@ def main() -> None: help="run node in a single process, to include validation in profiles", ) def run(file: Path, db_version: int, profile: bool, single_thread: bool) -> None: + """ + The FILE parameter should point to an existing blockchain database file (in v2 format) + """ asyncio.run(run_sync_test(Path(file), db_version, profile, single_thread)) From fc1d52de6a8b1ec49bbbe0910fa1ee04c9e89cef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 31 Mar 2022 23:04:48 -0700 Subject: [PATCH 304/378] Bump peter-evans/create-pull-request from 3 to 4 (#10950) Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 3 to 4. - [Release notes](https://github.com/peter-evans/create-pull-request/releases) - [Commits](https://github.com/peter-evans/create-pull-request/compare/v3...v4) --- updated-dependencies: - dependency-name: peter-evans/create-pull-request dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/mozilla-ca-cert.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/mozilla-ca-cert.yml b/.github/workflows/mozilla-ca-cert.yml index 73edbaa78a86..a637f430e1bd 100644 --- a/.github/workflows/mozilla-ca-cert.yml +++ b/.github/workflows/mozilla-ca-cert.yml @@ -20,7 +20,7 @@ jobs: cd ./mozilla-ca git pull origin main - name: "Create Pull Request" - uses: peter-evans/create-pull-request@v3 + uses: peter-evans/create-pull-request@v4 with: base: main body: "Newest Mozilla CA cert" From 441823f0da6dd15ea8570fc80d478293f1bf41d2 Mon Sep 17 00:00:00 2001 From: Adam Kelly <338792+aqk@users.noreply.github.com> Date: Thu, 31 Mar 2022 23:05:30 -0700 Subject: [PATCH 305/378] normalized_to_identity_cc_ip from get_consecutive_blocks was being passed in as overflow_cc_challenge in get_full_block_and_block_record (#10941) --- tests/block_tools.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/block_tools.py b/tests/block_tools.py index e2fb177bcc5f..f67a5d5c9304 100644 --- a/tests/block_tools.py +++ b/tests/block_tools.py @@ -602,9 +602,6 @@ def get_consecutive_blocks( block_generator = None aggregate_signature = G2Element() - # TODO: address hint error and remove ignore - # error: Argument 27 to "get_full_block_and_block_record" has incompatible type "bool"; - # expected "Optional[bytes32]" [arg-type] full_block, block_record = get_full_block_and_block_record( constants, blocks, @@ -632,7 +629,7 @@ def get_consecutive_blocks( signage_point, latest_block, seed, - normalized_to_identity_cc_ip, # type: ignore[arg-type] + normalized_to_identity_cc_ip=normalized_to_identity_cc_ip, current_time=current_time, ) if block_record.is_transaction_block: @@ -1504,6 +1501,7 @@ def get_full_block_and_block_record( signage_point: SignagePoint, prev_block: BlockRecord, seed: bytes = b"", + *, overflow_cc_challenge: bytes32 = None, overflow_rc_challenge: bytes32 = None, normalized_to_identity_cc_ip: bool = False, From 647cf6b52b3b85415b9ba3804be3d67644693938 Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Fri, 1 Apr 2022 16:12:10 +0200 Subject: [PATCH 306/378] fix performance tests (#10983) --- tests/core/full_node/test_mempool_performance.py | 2 +- tests/core/test_cost_calculation.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/core/full_node/test_mempool_performance.py b/tests/core/full_node/test_mempool_performance.py index b887863e55e6..4e8ed60496ee 100644 --- a/tests/core/full_node/test_mempool_performance.py +++ b/tests/core/full_node/test_mempool_performance.py @@ -77,4 +77,4 @@ async def test_mempool_update_performance(self, bt, wallet_nodes_mempool_perf, d if idx >= len(blocks) - 3: assert duration < 0.1 else: - assert duration < 0.0003 + assert duration < 0.001 diff --git a/tests/core/test_cost_calculation.py b/tests/core/test_cost_calculation.py index 2dfdc59c49b6..ac24ddf55c17 100644 --- a/tests/core/test_cost_calculation.py +++ b/tests/core/test_cost_calculation.py @@ -187,6 +187,7 @@ async def test_clvm_mempool_mode(self, softfork_height): assert npc_result.error is None @pytest.mark.asyncio + @pytest.mark.benchmark async def test_tx_generator_speed(self, softfork_height): LARGE_BLOCK_COIN_CONSUMED_COUNT = 687 generator_bytes = large_block_generator(LARGE_BLOCK_COIN_CONSUMED_COUNT) @@ -207,7 +208,7 @@ async def test_tx_generator_speed(self, softfork_height): assert len(npc_result.npc_list) == LARGE_BLOCK_COIN_CONSUMED_COUNT log.info(f"Time spent: {duration}") - assert duration < 2 + assert duration < 0.5 @pytest.mark.asyncio async def test_clvm_max_cost(self, softfork_height): @@ -246,6 +247,7 @@ async def test_clvm_max_cost(self, softfork_height): assert npc_result.cost > 10000000 @pytest.mark.asyncio + @pytest.mark.benchmark async def test_standard_tx(self): # this isn't a real public key, but we don't care public_key = bytes.fromhex( @@ -270,4 +272,4 @@ async def test_standard_tx(self): duration = time_end - time_start log.info(f"Time spent: {duration}") - assert duration < 3 + assert duration < 0.1 From 58abb351145ab940390dc84c44c51425597561fb Mon Sep 17 00:00:00 2001 From: Gene Hoffman <30377676+hoffmang9@users.noreply.github.com> Date: Fri, 1 Apr 2022 13:20:29 -0700 Subject: [PATCH 307/378] Check for vulnerable openssl (#10988) * Check for vulnerable openssl * Update OpenSSL on MacOS * First attempt - openssl Ubuntu 18.04 and 20.04 * place local/bin ahead in PATH * specify install openssl * correct path * run ldconfig * stop building and check for patched openssl * spell sudo right by removing it * Remove openssl building - 1st attempt RHs * Test Windows OpenSSL version HT @AmineKhaldi --- Install.ps1 | 8 ++++++++ install.sh | 31 ++++++++++++++++++++++--------- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/Install.ps1 b/Install.ps1 index ca69e4c41e2b..c97d6d737e28 100644 --- a/Install.ps1 +++ b/Install.ps1 @@ -41,6 +41,14 @@ if ([version]$pythonVersion -lt [version]"3.7.0") } Write-Output "Python version is:" $pythonVersion +$openSSLVersionStr = (py -c 'import ssl; print(ssl.OPENSSL_VERSION)') +$openSSLVersion = (py -c 'import ssl; print(ssl.OPENSSL_VERSION_NUMBER)') +if ($openSSLVersion -lt 269488367) +{ + Write-Output "Found Python with OpenSSL version:" $openSSLVersionStr + Write-Output "Anything before 1.1.1n is vulnerable to CVE-2022-0778." +} + py -m venv venv venv\scripts\python -m pip install --upgrade pip setuptools wheel diff --git a/install.sh b/install.sh index 3dc27ae13f5f..e9a4c728abdd 100755 --- a/install.sh +++ b/install.sh @@ -75,8 +75,8 @@ install_python3_and_sqlite3_from_source_with_yum() { # Preparing installing Python echo 'yum groupinstall -y "Development Tools"' sudo yum groupinstall -y "Development Tools" - echo "sudo yum install -y openssl-devel libffi-devel bzip2-devel wget" - sudo yum install -y openssl-devel libffi-devel bzip2-devel wget + echo "sudo yum install -y openssl-devel openssl libffi-devel bzip2-devel wget" + sudo yum install -y openssl-devel openssl libffi-devel bzip2-devel wget echo "cd $TMP_PATH" cd "$TMP_PATH" @@ -111,7 +111,6 @@ install_python3_and_sqlite3_from_source_with_yum() { cd "$CURRENT_WD" } - # Manage npm and other install requirements on an OS specific basis if [ "$(uname)" = "Linux" ]; then #LINUX=1 @@ -119,19 +118,21 @@ if [ "$(uname)" = "Linux" ]; then # Ubuntu echo "Installing on Ubuntu pre 20.04 LTS." sudo apt-get update - sudo apt-get install -y python3.7-venv python3.7-distutils + sudo apt-get install -y python3.7-venv python3.7-distutils openssl + apt show openssl elif [ "$UBUNTU" = "true" ] && [ "$UBUNTU_PRE_2004" = "0" ] && [ "$UBUNTU_2100" = "0" ]; then echo "Installing on Ubuntu 20.04 LTS." sudo apt-get update - sudo apt-get install -y python3.8-venv python3-distutils + sudo apt-get install -y python3.8-venv python3-distutils openssl + apt show openssl elif [ "$UBUNTU" = "true" ] && [ "$UBUNTU_2100" = "1" ]; then echo "Installing on Ubuntu 21.04 or newer." sudo apt-get update - sudo apt-get install -y python3.9-venv python3-distutils + sudo apt-get install -y python3.9-venv python3-distutils openssl elif [ "$DEBIAN" = "true" ]; then echo "Installing on Debian." sudo apt-get update - sudo apt-get install -y python3-venv + sudo apt-get install -y python3-venv openssl elif type pacman >/dev/null 2>&1 && [ -f "/etc/arch-release" ]; then # Arch Linux # Arch provides latest python version. User will need to manually install python 3.9 if it is not present @@ -160,16 +161,17 @@ if [ "$(uname)" = "Linux" ]; then elif type yum >/dev/null 2>&1 && [ -f "/etc/redhat-release" ] && grep Rocky /etc/redhat-release; then echo "Installing on Rocky." # TODO: make this smarter about getting the latest version - sudo yum install --assumeyes python39 + sudo yum install --assumeyes python39 openssl elif type yum >/dev/null 2>&1 && [ -f "/etc/redhat-release" ] || [ -f "/etc/fedora-release" ]; then # Redhat or Fedora echo "Installing on Redhat/Fedora." if ! command -v python3.9 >/dev/null 2>&1; then - sudo yum install -y python39 + sudo yum install -y python39 openssl fi fi elif [ "$(uname)" = "Darwin" ] && ! type brew >/dev/null 2>&1; then echo "Installation currently requires brew on MacOS - https://brew.sh/" + brew install openssl elif [ "$(uname)" = "OpenBSD" ]; then export MAKE=${MAKE:-gmake} export BUILD_VDF_CLIENT=${BUILD_VDF_CLIENT:-N} @@ -231,6 +233,17 @@ if [ "$SQLITE_MAJOR_VER" -lt "3" ] || [ "$SQLITE_MAJOR_VER" = "3" ] && [ "$SQLIT exit 1 fi +# Check openssl version python will use +OPENSSL_VERSION_STRING=$($INSTALL_PYTHON_PATH -c 'import ssl; print(ssl.OPENSSL_VERSION)') +OPENSSL_VERSION_INT=$($INSTALL_PYTHON_PATH -c 'import ssl; print(ssl.OPENSSL_VERSION_NUMBER)') +# There is also ssl.OPENSSL_VERSION_INFO returning a tuple +# 1.1.1n corresponds to 269488367 as an integer +echo "OpenSSL version for Python is ${OPENSSL_VERSION_STRING}" +if [ "$OPENSSL_VERSION_INT" -lt "269488367" ]; then + echo "WARNING: OpenSSL versions before 3.0.2, 1.1.1n, or 1.0.2zd are vulnerable to CVE-2022-0778" + echo "Your OS may have patched OpenSSL and not updated the version to 1.1.1n" +fi + # If version of `python` and "$INSTALL_PYTHON_VERSION" does not match, clear old version VENV_CLEAR="" if [ -e venv/bin/python ]; then From 8cbf96d73cdb00d81b4bcd334fabd71422b43efe Mon Sep 17 00:00:00 2001 From: Gene Hoffman <30377676+hoffmang9@users.noreply.github.com> Date: Fri, 1 Apr 2022 16:31:00 -0700 Subject: [PATCH 308/378] Non Hobo patch the winstaller for CVE-2022-0778 (#10995) --- .github/workflows/build-windows-installer.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-windows-installer.yml b/.github/workflows/build-windows-installer.yml index 7f7c6bd8cb44..67ee617b5e52 100644 --- a/.github/workflows/build-windows-installer.yml +++ b/.github/workflows/build-windows-installer.yml @@ -62,7 +62,7 @@ jobs: - uses: actions/setup-python@v2 name: Install Python 3.9 with: - python-version: "3.9" + python-version: "3.9.11" - name: Setup Node 16.x uses: actions/setup-node@v3 From 14627d8a6efe5515393948acfbd740e6f7bdf58b Mon Sep 17 00:00:00 2001 From: Gene Hoffman <30377676+hoffmang9@users.noreply.github.com> Date: Fri, 1 Apr 2022 18:01:30 -0700 Subject: [PATCH 309/378] apt show not needed (#10997) --- install.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/install.sh b/install.sh index e9a4c728abdd..9d73724a9d78 100755 --- a/install.sh +++ b/install.sh @@ -119,12 +119,10 @@ if [ "$(uname)" = "Linux" ]; then echo "Installing on Ubuntu pre 20.04 LTS." sudo apt-get update sudo apt-get install -y python3.7-venv python3.7-distutils openssl - apt show openssl elif [ "$UBUNTU" = "true" ] && [ "$UBUNTU_PRE_2004" = "0" ] && [ "$UBUNTU_2100" = "0" ]; then echo "Installing on Ubuntu 20.04 LTS." sudo apt-get update sudo apt-get install -y python3.8-venv python3-distutils openssl - apt show openssl elif [ "$UBUNTU" = "true" ] && [ "$UBUNTU_2100" = "1" ]; then echo "Installing on Ubuntu 21.04 or newer." sudo apt-get update From 3e2b979751b52bb43b13c57a579d2c2484b451b0 Mon Sep 17 00:00:00 2001 From: Gene Hoffman <30377676+hoffmang9@users.noreply.github.com> Date: Fri, 1 Apr 2022 19:28:20 -0700 Subject: [PATCH 310/378] install/upgrade openssl on Arch Linux also (#10999) --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 9d73724a9d78..0bd6c7266de1 100755 --- a/install.sh +++ b/install.sh @@ -137,7 +137,7 @@ if [ "$(uname)" = "Linux" ]; then echo "Installing on Arch Linux." case $(uname -m) in x86_64|aarch64) - sudo pacman ${PACMAN_AUTOMATED} -S --needed git + sudo pacman ${PACMAN_AUTOMATED} -S --needed git openssl ;; *) echo "Incompatible CPU architecture. Must be x86_64 or aarch64." From e3e814b9ecdbc63c711ad9f2ca6b0b5fc1b76d8d Mon Sep 17 00:00:00 2001 From: Gene Hoffman <30377676+hoffmang9@users.noreply.github.com> Date: Fri, 1 Apr 2022 21:06:32 -0700 Subject: [PATCH 311/378] Compile python 3.9.11 which is aware of the openssl issue (#11001) --- install.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/install.sh b/install.sh index 0bd6c7266de1..1a1b0afc4a18 100755 --- a/install.sh +++ b/install.sh @@ -96,11 +96,11 @@ install_python3_and_sqlite3_from_source_with_yum() { sudo make install | stdbuf -o0 cut -b1-"$(tput cols)" | sed -u 'i\\o033[2K' | stdbuf -o0 tr '\n' '\r'; echo # yum install python3 brings Python3.6 which is not supported by chia cd .. - echo "wget https://www.python.org/ftp/python/3.9.9/Python-3.9.9.tgz" - wget https://www.python.org/ftp/python/3.9.9/Python-3.9.9.tgz - tar xf Python-3.9.9.tgz - echo "cd Python-3.9.9" - cd Python-3.9.9 + echo "wget https://www.python.org/ftp/python/3.9.11/Python-3.9.11.tgz" + wget https://www.python.org/ftp/python/3.9.11/Python-3.9.11.tgz + tar xf Python-3.9.11.tgz + echo "cd Python-3.9.11" + cd Python-3.9.11 echo "LD_RUN_PATH=/usr/local/lib ./configure --prefix=/usr/local" # '| stdbuf ...' seems weird but this makes command outputs stay in single line. LD_RUN_PATH=/usr/local/lib ./configure --prefix=/usr/local | stdbuf -o0 cut -b1-"$(tput cols)" | sed -u 'i\\o033[2K' | stdbuf -o0 tr '\n' '\r'; echo From 131bd4e2f32c0ac30af9638e85e2c3455c1b5381 Mon Sep 17 00:00:00 2001 From: Gene Hoffman <30377676+hoffmang9@users.noreply.github.com> Date: Fri, 1 Apr 2022 21:22:56 -0700 Subject: [PATCH 312/378] install.sh is not upgrading OpenSSL on MacOS (#11003) * MacOS isn't updating OpenSSL in install.sh * Exit if no brew on MacOS * Code the if tree like a pro instead. Co-authored-by: Kyle Altendorf Co-authored-by: Kyle Altendorf --- install.sh | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/install.sh b/install.sh index 1a1b0afc4a18..5ce4095fed60 100755 --- a/install.sh +++ b/install.sh @@ -167,8 +167,13 @@ if [ "$(uname)" = "Linux" ]; then sudo yum install -y python39 openssl fi fi -elif [ "$(uname)" = "Darwin" ] && ! type brew >/dev/null 2>&1; then - echo "Installation currently requires brew on MacOS - https://brew.sh/" +elif [ "$(uname)" = "Darwin" ]; then + echo "Installing on macOS." + if ! type brew >/dev/null 2>&1; then + echo "Installation currently requires brew on macOS - https://brew.sh/" + exit 1 + fi + echo "Installing OpenSSL" brew install openssl elif [ "$(uname)" = "OpenBSD" ]; then export MAKE=${MAKE:-gmake} From 8b5c7012dc01dac1842651db3b0e0f3e98f7612e Mon Sep 17 00:00:00 2001 From: roseiliend <90035993+roseiliend@users.noreply.github.com> Date: Sun, 3 Apr 2022 04:21:54 +0800 Subject: [PATCH 313/378] force index in get_coin_records_by_names (#10987) * force index in get_coin_records_by_names * fix lint --- chia/full_node/coin_store.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/chia/full_node/coin_store.py b/chia/full_node/coin_store.py index 3fec82d41186..9f24e68e45fe 100644 --- a/chia/full_node/coin_store.py +++ b/chia/full_node/coin_store.py @@ -288,7 +288,8 @@ async def get_coin_records_by_names( async with self.db_wrapper.read_db() as conn: async with conn.execute( f"SELECT confirmed_index, spent_index, coinbase, puzzle_hash, " - f'coin_parent, amount, timestamp FROM coin_record WHERE coin_name in ({"?," * (len(names) - 1)}?) ' + f"coin_parent, amount, timestamp FROM coin_record INDEXED BY sqlite_autoindex_coin_record_1 " + f'WHERE coin_name in ({"?," * (len(names) - 1)}?) ' f"AND confirmed_index>=? AND confirmed_index Date: Sat, 2 Apr 2022 16:22:55 -0400 Subject: [PATCH 314/378] Fix remaining linting issues (#10962) * FIx remaining linting issues * Revert type:ignore * Revert token_bytes change --- benchmarks/utils.py | 10 +- chia/consensus/block_body_validation.py | 12 +- chia/consensus/block_creation.py | 22 +--- chia/consensus/blockchain.py | 49 ++++---- chia/consensus/multiprocess_validation.py | 14 +-- chia/full_node/block_store.py | 7 +- chia/full_node/full_node.py | 7 +- chia/full_node/full_node_api.py | 107 ++++++++---------- chia/rpc/full_node_rpc_api.py | 7 +- chia/server/server.py | 10 +- chia/simulator/simulator_constants.py | 5 +- chia/timelord/timelord_state.py | 9 +- .../types/blockchain_format/proof_of_space.py | 6 +- chia/util/block_cache.py | 11 +- chia/util/generator_tools.py | 12 +- tests/block_tools.py | 73 ++++-------- .../full_node/stores/test_full_node_store.py | 6 +- tests/core/full_node/test_block_height_map.py | 4 +- tests/core/full_node/test_mempool.py | 6 +- tests/core/make_block_generator.py | 20 ++-- tests/pools/test_wallet_pool_store.py | 5 +- tests/util/key_tool.py | 12 +- tests/util/network_protocol_data.py | 4 +- tests/wallet_tools.py | 34 +++--- tests/weight_proof/test_weight_proof.py | 5 +- 25 files changed, 170 insertions(+), 287 deletions(-) diff --git a/benchmarks/utils.py b/benchmarks/utils.py index 3646db14f381..af9db5e23e66 100644 --- a/benchmarks/utils.py +++ b/benchmarks/utils.py @@ -3,7 +3,7 @@ from chia.consensus.coinbase import create_farmer_coin, create_pool_coin from chia.types.blockchain_format.classgroup import ClassgroupElement from chia.types.blockchain_format.coin import Coin -from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.blockchain_format.sized_bytes import bytes32, bytes100 from chia.types.blockchain_format.vdf import VDFInfo, VDFProof from chia.types.blockchain_format.foliage import Foliage, FoliageBlockData, FoliageTransactionBlock, TransactionsInfo from chia.types.blockchain_format.pool_target import PoolTarget @@ -56,9 +56,7 @@ def rand_bytes(num) -> bytes: def rand_hash() -> bytes32: - # TODO: address hint errors and remove ignores - # error: Incompatible return value type (got "bytes", expected "bytes32") [return-value] - return rand_bytes(32) # type: ignore[return-value] + return bytes32(rand_bytes(32)) def rand_g1() -> G1Element: @@ -72,9 +70,7 @@ def rand_g2() -> G2Element: def rand_class_group_element() -> ClassgroupElement: - # TODO: address hint errors and remove ignores - # error: Argument 1 to "ClassgroupElement" has incompatible type "bytes"; expected "bytes100" [arg-type] - return ClassgroupElement(rand_bytes(100)) # type: ignore[arg-type] + return ClassgroupElement(bytes100(rand_bytes(100))) def rand_vdf() -> VDFInfo: diff --git a/chia/consensus/block_body_validation.py b/chia/consensus/block_body_validation.py index 5ce7cad6f590..9bb704c73000 100644 --- a/chia/consensus/block_body_validation.py +++ b/chia/consensus/block_body_validation.py @@ -245,18 +245,12 @@ async def validate_block_body( return root_error, None # 12. The additions and removals must result in the correct filter - byte_array_tx: List[bytes32] = [] + byte_array_tx: List[bytearray] = [] for coin in additions + coinbase_additions: - # TODO: address hint error and remove ignore - # error: Argument 1 to "append" of "list" has incompatible type "bytearray"; expected "bytes32" - # [arg-type] - byte_array_tx.append(bytearray(coin.puzzle_hash)) # type: ignore[arg-type] + byte_array_tx.append(bytearray(coin.puzzle_hash)) for coin_name in removals: - # TODO: address hint error and remove ignore - # error: Argument 1 to "append" of "list" has incompatible type "bytearray"; expected "bytes32" - # [arg-type] - byte_array_tx.append(bytearray(coin_name)) # type: ignore[arg-type] + byte_array_tx.append(bytearray(coin_name)) bip158: PyBIP158 = PyBIP158(byte_array_tx) encoded_filter = bytes(bip158.GetEncoded()) diff --git a/chia/consensus/block_creation.py b/chia/consensus/block_creation.py index 1b57abce0ebf..a30855127474 100644 --- a/chia/consensus/block_creation.py +++ b/chia/consensus/block_creation.py @@ -35,9 +35,6 @@ log = logging.getLogger(__name__) -# TODO: address hint error and remove ignore -# error: Incompatible default for argument "seed" (default has type "bytes", argument has type "bytes32") -# [assignment] def create_foliage( constants: ConsensusConstants, reward_block_unfinished: RewardChainBlockUnfinished, @@ -53,7 +50,7 @@ def create_foliage( pool_target: PoolTarget, get_plot_signature: Callable[[bytes32, G1Element], G2Element], get_pool_signature: Callable[[PoolTarget, Optional[G1Element]], Optional[G2Element]], - seed: bytes32 = b"", # type: ignore[assignment] + seed: bytes = b"", ) -> Tuple[Foliage, Optional[FoliageTransactionBlock], Optional[TransactionsInfo]]: """ Creates a foliage for a given reward chain block. This may or may not be a tx block. In the case of a tx block, @@ -95,7 +92,7 @@ def create_foliage( height = uint32(prev_block.height + 1) # Create filter - byte_array_tx: List[bytes32] = [] + byte_array_tx: List[bytearray] = [] tx_additions: List[Coin] = [] tx_removals: List[bytes32] = [] @@ -192,16 +189,10 @@ def create_foliage( additions.extend(reward_claims_incorporated.copy()) for coin in additions: tx_additions.append(coin) - # TODO: address hint error and remove ignore - # error: Argument 1 to "append" of "list" has incompatible type "bytearray"; expected "bytes32" - # [arg-type] - byte_array_tx.append(bytearray(coin.puzzle_hash)) # type: ignore[arg-type] + byte_array_tx.append(bytearray(coin.puzzle_hash)) for coin in removals: tx_removals.append(coin.name()) - # TODO: address hint error and remove ignore - # error: Argument 1 to "append" of "list" has incompatible type "bytearray"; expected "bytes32" - # [arg-type] - byte_array_tx.append(bytearray(coin.name())) # type: ignore[arg-type] + byte_array_tx.append(bytearray(coin.name())) bip158: PyBIP158 = PyBIP158(byte_array_tx) encoded = bytes(bip158.GetEncoded()) @@ -289,9 +280,6 @@ def create_foliage( return foliage, foliage_transaction_block, transactions_info -# TODO: address hint error and remove ignore -# error: Incompatible default for argument "seed" (default has type "bytes", argument has type "bytes32") -# [assignment] def create_unfinished_block( constants: ConsensusConstants, sub_slot_start_total_iters: uint128, @@ -308,7 +296,7 @@ def create_unfinished_block( signage_point: SignagePoint, timestamp: uint64, blocks: BlockchainInterface, - seed: bytes32 = b"", # type: ignore[assignment] + seed: bytes = b"", block_generator: Optional[BlockGenerator] = None, aggregate_sig: G2Element = G2Element(), additions: Optional[List[Coin]] = None, diff --git a/chia/consensus/blockchain.py b/chia/consensus/blockchain.py index 91fcd4458eb3..4f8dccfc5018 100644 --- a/chia/consensus/blockchain.py +++ b/chia/consensus/blockchain.py @@ -182,10 +182,9 @@ async def get_full_peak(self) -> Optional[FullBlock]: if self._peak_height is None: return None """ Return list of FullBlocks that are peaks""" - # TODO: address hint error and remove ignore - # error: Argument 1 to "get_full_block" of "BlockStore" has incompatible type "Optional[bytes32]"; - # expected "bytes32" [arg-type] - block = await self.block_store.get_full_block(self.height_to_hash(self._peak_height)) # type: ignore[arg-type] + peak_hash: Optional[bytes32] = self.height_to_hash(self._peak_height) + assert peak_hash is not None # Since we must have the peak block + block = await self.block_store.get_full_block(peak_hash) assert block is not None return block @@ -308,12 +307,9 @@ def get_hint_list(self, npc_result: NPCResult) -> List[Tuple[bytes32, bytes]]: if opcode == ConditionOpcode.CREATE_COIN: for condition in conditions: if len(condition.vars) > 2 and condition.vars[2] != b"": - puzzle_hash, amount_bin = condition.vars[0], condition.vars[1] - amount = int_from_bytes(amount_bin) - # TODO: address hint error and remove ignore - # error: Argument 2 to "Coin" has incompatible type "bytes"; expected "bytes32" - # [arg-type] - coin_id = Coin(npc.coin_name, puzzle_hash, amount).name() # type: ignore[arg-type] + puzzle_hash, amount_bin = bytes32(condition.vars[0]), condition.vars[1] + amount: uint64 = uint64(int_from_bytes(amount_bin)) + coin_id: bytes32 = Coin(npc.coin_name, puzzle_hash, amount).name() h_list.append((coin_id, condition.vars[2])) return h_list @@ -679,11 +675,11 @@ def block_record(self, header_hash: bytes32) -> BlockRecord: return self.__block_records[header_hash] def height_to_block_record(self, height: uint32) -> BlockRecord: - header_hash = self.height_to_hash(height) - # TODO: address hint error and remove ignore - # error: Argument 1 to "block_record" of "Blockchain" has incompatible type "Optional[bytes32]"; expected - # "bytes32" [arg-type] - return self.block_record(header_hash) # type: ignore[arg-type] + # Precondition: height is in the blockchain + header_hash: Optional[bytes32] = self.height_to_hash(height) + if header_hash is None: + raise ValueError(f"Height is not in blockchain: {height}") + return self.block_record(header_hash) def get_ses_heights(self) -> List[uint32]: return self.__height_map.get_ses_heights() @@ -762,10 +758,8 @@ async def get_header_blocks_in_range( hashes = [] for height in range(start, stop + 1): if self.contains_height(uint32(height)): - # TODO: address hint error and remove ignore - # error: Incompatible types in assignment (expression has type "Optional[bytes32]", variable has - # type "bytes32") [assignment] - header_hash: bytes32 = self.height_to_hash(uint32(height)) # type: ignore[assignment] + header_hash: Optional[bytes32] = self.height_to_hash(uint32(height)) + assert header_hash is not None hashes.append(header_hash) blocks: List[FullBlock] = [] @@ -810,23 +804,20 @@ async def get_block_records_at(self, heights: List[uint32], batch_size=900) -> L gets block records by height (only blocks that are part of the chain) """ records: List[BlockRecord] = [] - hashes = [] + hashes: List[bytes32] = [] assert batch_size < 999 # sqlite in python 3.7 has a limit on 999 variables in queries for height in heights: - hashes.append(self.height_to_hash(height)) + header_hash: Optional[bytes32] = self.height_to_hash(height) + if header_hash is None: + raise ValueError(f"Do not have block at height {height}") + hashes.append(header_hash) if len(hashes) > batch_size: - # TODO: address hint error and remove ignore - # error: Argument 1 to "get_block_records_by_hash" of "BlockStore" has incompatible type - # "List[Optional[bytes32]]"; expected "List[bytes32]" [arg-type] - res = await self.block_store.get_block_records_by_hash(hashes) # type: ignore[arg-type] + res = await self.block_store.get_block_records_by_hash(hashes) records.extend(res) hashes = [] if len(hashes) > 0: - # TODO: address hint error and remove ignore - # error: Argument 1 to "get_block_records_by_hash" of "BlockStore" has incompatible type - # "List[Optional[bytes32]]"; expected "List[bytes32]" [arg-type] - res = await self.block_store.get_block_records_by_hash(hashes) # type: ignore[arg-type] + res = await self.block_store.get_block_records_by_hash(hashes) records.extend(res) return records diff --git a/chia/consensus/multiprocess_validation.py b/chia/consensus/multiprocess_validation.py index 9fe54858290a..0e0ad3ff85fd 100644 --- a/chia/consensus/multiprocess_validation.py +++ b/chia/consensus/multiprocess_validation.py @@ -56,9 +56,9 @@ def batch_pre_validate_blocks( expected_sub_slot_iters: List[uint64], validate_signatures: bool, ) -> List[bytes]: - blocks: Dict[bytes, BlockRecord] = {} + blocks: Dict[bytes32, BlockRecord] = {} for k, v in blocks_pickled.items(): - blocks[k] = BlockRecord.from_bytes(v) + blocks[bytes32(k)] = BlockRecord.from_bytes(v) results: List[PreValidationResult] = [] constants: ConsensusConstants = dataclass_from_dict(ConsensusConstants, constants_dict) if full_blocks_pickled is not None and header_blocks_pickled is not None: @@ -99,12 +99,9 @@ def batch_pre_validate_blocks( continue header_block = get_block_header(block, tx_additions, removals) - # TODO: address hint error and remove ignore - # error: Argument 1 to "BlockCache" has incompatible type "Dict[bytes, BlockRecord]"; expected - # "Dict[bytes32, BlockRecord]" [arg-type] required_iters, error = validate_finished_header_block( constants, - BlockCache(blocks), # type: ignore[arg-type] + BlockCache(blocks), header_block, check_filter, expected_difficulty[i], @@ -144,12 +141,9 @@ def batch_pre_validate_blocks( for i in range(len(header_blocks_pickled)): try: header_block = HeaderBlock.from_bytes(header_blocks_pickled[i]) - # TODO: address hint error and remove ignore - # error: Argument 1 to "BlockCache" has incompatible type "Dict[bytes, BlockRecord]"; expected - # "Dict[bytes32, BlockRecord]" [arg-type] required_iters, error = validate_finished_header_block( constants, - BlockCache(blocks), # type: ignore[arg-type] + BlockCache(blocks), header_block, check_filter, expected_difficulty[i], diff --git a/chia/full_node/block_store.py b/chia/full_node/block_store.py index 3da53d27adaa..aad306cf620f 100644 --- a/chia/full_node/block_store.py +++ b/chia/full_node/block_store.py @@ -421,12 +421,9 @@ async def get_blocks_by_hash(self, header_hashes: List[bytes32]) -> List[FullBlo async with self.db_wrapper.read_db() as conn: async with conn.execute(formatted_str, header_hashes_db) as cursor: for row in await cursor.fetchall(): - header_hash = self.maybe_from_hex(row[0]) + header_hash = bytes32(self.maybe_from_hex(row[0])) full_block: FullBlock = self.maybe_decompress(row[1]) - # TODO: address hint error and remove ignore - # error: Invalid index type "bytes" for "Dict[bytes32, FullBlock]"; - # expected type "bytes32" [index] - all_blocks[header_hash] = full_block # type: ignore[index] + all_blocks[header_hash] = full_block self.block_cache.put(header_hash, full_block) ret: List[FullBlock] = [] for hh in header_hashes: diff --git a/chia/full_node/full_node.py b/chia/full_node/full_node.py index 0eeeede68835..5ac784804f39 100644 --- a/chia/full_node/full_node.py +++ b/chia/full_node/full_node.py @@ -1297,10 +1297,9 @@ async def peak_post_processing( fork_block: Optional[BlockRecord] = None if fork_height != block.height - 1 and block.height != 0: # This is a reorg - # TODO: address hint error and remove ignore - # error: Argument 1 to "block_record" of "Blockchain" has incompatible type "Optional[bytes32]"; - # expected "bytes32" [arg-type] - fork_block = self.blockchain.block_record(self.blockchain.height_to_hash(fork_height)) # type: ignore[arg-type] # noqa: E501 + fork_hash: Optional[bytes32] = self.blockchain.height_to_hash(fork_height) + assert fork_hash is not None + fork_block = self.blockchain.block_record(fork_hash) fns_peak_result: FullNodeStorePeakResult = self.full_node_store.new_peak( record, diff --git a/chia/full_node/full_node_api.py b/chia/full_node/full_node_api.py index 38e2debdda43..081178480626 100644 --- a/chia/full_node/full_node_api.py +++ b/chia/full_node/full_node_api.py @@ -199,13 +199,11 @@ async def tx_request_and_timeout(full_node: FullNode, transaction_id, task_id): if task_id in full_node.full_node_store.tx_fetch_tasks: full_node.full_node_store.tx_fetch_tasks.pop(task_id) - task_id = token_bytes() + task_id: bytes32 = bytes32(token_bytes(32)) fetch_task = asyncio.create_task( tx_request_and_timeout(self.full_node, transaction.transaction_id, task_id) ) - # TODO: address hint error and remove ignore - # error: Invalid index type "bytes" for "Dict[bytes32, Task[Any]]"; expected type "bytes32" [index] - self.full_node.full_node_store.tx_fetch_tasks[task_id] = fetch_task # type: ignore[index] + self.full_node.full_node_store.tx_fetch_tasks[task_id] = fetch_task return None return None @@ -311,18 +309,16 @@ async def request_block(self, request: full_node_protocol.RequestBlock) -> Optio reject = RejectBlock(request.height) msg = make_msg(ProtocolMessageTypes.reject_block, reject) return msg - header_hash = self.full_node.blockchain.height_to_hash(request.height) - # TODO: address hint error and remove ignore - # error: Argument 1 to "get_full_block" of "BlockStore" has incompatible type "Optional[bytes32]"; - # expected "bytes32" [arg-type] - block: Optional[FullBlock] = await self.full_node.block_store.get_full_block(header_hash) # type: ignore[arg-type] # noqa: E501 + header_hash: Optional[bytes32] = self.full_node.blockchain.height_to_hash(request.height) + if header_hash is None: + return make_msg(ProtocolMessageTypes.reject_block, RejectBlock(request.height)) + + block: Optional[FullBlock] = await self.full_node.block_store.get_full_block(header_hash) if block is not None: if not request.include_transaction_block and block.transactions_generator is not None: block = dataclasses.replace(block, transactions_generator=None) return make_msg(ProtocolMessageTypes.respond_block, full_node_protocol.RespondBlock(block)) - reject = RejectBlock(request.height) - msg = make_msg(ProtocolMessageTypes.reject_block, reject) - return msg + return make_msg(ProtocolMessageTypes.reject_block, RejectBlock(request.height)) @api_request @reply_type([ProtocolMessageTypes.respond_blocks, ProtocolMessageTypes.reject_blocks]) @@ -340,16 +336,15 @@ async def request_blocks(self, request: full_node_protocol.RequestBlocks) -> Opt if not request.include_transaction_block: blocks: List[FullBlock] = [] for i in range(request.start_height, request.end_height + 1): - # TODO: address hint error and remove ignore - # error: Argument 1 to "get_full_block" of "BlockStore" has incompatible type "Optional[bytes32]"; - # expected "bytes32" [arg-type] - block: Optional[FullBlock] = await self.full_node.block_store.get_full_block( - self.full_node.blockchain.height_to_hash(uint32(i)) # type: ignore[arg-type] - ) + header_hash_i: Optional[bytes32] = self.full_node.blockchain.height_to_hash(uint32(i)) + if header_hash_i is None: + reject = RejectBlocks(request.start_height, request.end_height) + return make_msg(ProtocolMessageTypes.reject_blocks, reject) + + block: Optional[FullBlock] = await self.full_node.block_store.get_full_block(header_hash_i) if block is None: reject = RejectBlocks(request.start_height, request.end_height) - msg = make_msg(ProtocolMessageTypes.reject_blocks, reject) - return msg + return make_msg(ProtocolMessageTypes.reject_blocks, reject) block = dataclasses.replace(block, transactions_generator=None) blocks.append(block) msg = make_msg( @@ -359,12 +354,11 @@ async def request_blocks(self, request: full_node_protocol.RequestBlocks) -> Opt else: blocks_bytes: List[bytes] = [] for i in range(request.start_height, request.end_height + 1): - # TODO: address hint error and remove ignore - # error: Argument 1 to "get_full_block_bytes" of "BlockStore" has incompatible type - # "Optional[bytes32]"; expected "bytes32" [arg-type] - block_bytes: Optional[bytes] = await self.full_node.block_store.get_full_block_bytes( - self.full_node.blockchain.height_to_hash(uint32(i)) # type: ignore[arg-type] - ) + header_hash_i = self.full_node.blockchain.height_to_hash(uint32(i)) + if header_hash_i is None: + reject = RejectBlocks(request.start_height, request.end_height) + return make_msg(ProtocolMessageTypes.reject_blocks, reject) + block_bytes: Optional[bytes] = await self.full_node.block_store.get_full_block_bytes(header_hash_i) if block_bytes is None: reject = RejectBlocks(request.start_height, request.end_height) msg = make_msg(ProtocolMessageTypes.reject_blocks, reject) @@ -898,9 +892,6 @@ def get_pool_sig(_1, _2) -> Optional[G2Element]: timestamp = uint64(int(curr.timestamp + 1)) self.log.info("Starting to make the unfinished block") - # TODO: address hint error and remove ignore - # error: Argument 16 to "create_unfinished_block" has incompatible type "bytes"; expected "bytes32" - # [arg-type] unfinished_block: UnfinishedBlock = create_unfinished_block( self.full_node.constants, total_iters_pos_slot, @@ -917,7 +908,7 @@ def get_pool_sig(_1, _2) -> Optional[G2Element]: sp_vdfs, timestamp, self.full_node.blockchain, - b"", # type: ignore[arg-type] + b"", block_generator, aggregate_signature, additions, @@ -937,22 +928,17 @@ def get_pool_sig(_1, _2) -> Optional[G2Element]: foliage_transaction_block_hash = unfinished_block.foliage.foliage_transaction_block_hash else: foliage_transaction_block_hash = bytes32([0] * 32) + assert foliage_transaction_block_hash is not None - # TODO: address hint error and remove ignore - # error: Argument 3 to "RequestSignedValues" has incompatible type "Optional[bytes32]"; expected - # "bytes32" [arg-type] message = farmer_protocol.RequestSignedValues( quality_string, foliage_sb_data_hash, - foliage_transaction_block_hash, # type: ignore[arg-type] + foliage_transaction_block_hash, ) await peer.send_message(make_msg(ProtocolMessageTypes.request_signed_values, message)) # Adds backup in case the first one fails if unfinished_block.is_transaction_block() and unfinished_block.transactions_generator is not None: - # TODO: address hint error and remove ignore - # error: Argument 16 to "create_unfinished_block" has incompatible type "bytes"; expected - # "bytes32" [arg-type] unfinished_block_backup = create_unfinished_block( self.full_node.constants, total_iters_pos_slot, @@ -969,7 +955,7 @@ def get_pool_sig(_1, _2) -> Optional[G2Element]: sp_vdfs, timestamp, self.full_node.blockchain, - b"", # type: ignore[arg-type] + b"", None, G2Element(), None, @@ -1039,13 +1025,12 @@ async def signed_values( self.full_node.full_node_store.add_candidate_block( farmer_request.quality_string, height, unfinished_block, False ) - # TODO: address hint error and remove ignore - # error: Argument 3 to "RequestSignedValues" has incompatible type "Optional[bytes32]"; expected - # "bytes32" [arg-type] + # All unfinished blocks that we create will have the foliage transaction block and hash + assert unfinished_block.foliage.foliage_transaction_block_hash is not None message = farmer_protocol.RequestSignedValues( farmer_request.quality_string, unfinished_block.foliage.foliage_block_data.get_hash(), - unfinished_block.foliage.foliage_transaction_block_hash, # type: ignore[arg-type] + unfinished_block.foliage.foliage_transaction_block_hash, ) await peer.send_message(make_msg(ProtocolMessageTypes.request_signed_values, message)) return None @@ -1124,10 +1109,14 @@ async def request_block_header(self, request: wallet_protocol.RequestBlockHeader @api_request async def request_additions(self, request: wallet_protocol.RequestAdditions) -> Optional[Message]: - # TODO: address hint error and remove ignore - # error: Argument 1 to "get_full_block" of "BlockStore" has incompatible type "Optional[bytes32]"; - # expected "bytes32" [arg-type] - block: Optional[FullBlock] = await self.full_node.block_store.get_full_block(request.header_hash) # type: ignore[arg-type] # noqa: E501 + if request.header_hash is None: + header_hash: Optional[bytes32] = self.full_node.blockchain.height_to_hash(request.height) + else: + header_hash = request.header_hash + if header_hash is None: + raise ValueError(f"Block at height {request.height} not found") + + block: Optional[FullBlock] = await self.full_node.block_store.get_full_block(header_hash) # We lock so that the coin store does not get modified if ( @@ -1135,10 +1124,7 @@ async def request_additions(self, request: wallet_protocol.RequestAdditions) -> or block.is_transaction_block() is False or self.full_node.blockchain.height_to_hash(block.height) != request.header_hash ): - # TODO: address hint error and remove ignore - # error: Argument 2 to "RejectAdditionsRequest" has incompatible type "Optional[bytes32]"; expected - # "bytes32" [arg-type] - reject = wallet_protocol.RejectAdditionsRequest(request.height, request.header_hash) # type: ignore[arg-type] # noqa: E501 + reject = wallet_protocol.RejectAdditionsRequest(request.height, header_hash) msg = make_msg(ProtocolMessageTypes.reject_additions_request, reject) return msg @@ -1300,11 +1286,11 @@ async def request_puzzle_solution(self, request: wallet_protocol.RequestPuzzleSo if coin_record is None or coin_record.spent_block_index != height: return reject_msg - header_hash = self.full_node.blockchain.height_to_hash(height) - # TODO: address hint error and remove ignore - # error: Argument 1 to "get_full_block" of "BlockStore" has incompatible type "Optional[bytes32]"; - # expected "bytes32" [arg-type] - block: Optional[FullBlock] = await self.full_node.block_store.get_full_block(header_hash) # type: ignore[arg-type] # noqa: E501 + header_hash: Optional[bytes32] = self.full_node.blockchain.height_to_hash(height) + if header_hash is None: + return reject_msg + + block: Optional[FullBlock] = await self.full_node.block_store.get_full_block(header_hash) if block is None or block.transactions_generator is None: return reject_msg @@ -1331,18 +1317,17 @@ async def request_header_blocks(self, request: wallet_protocol.RequestHeaderBloc if request.end_height < request.start_height or request.end_height - request.start_height > 32: return None - header_hashes = [] + header_hashes: List[bytes32] = [] for i in range(request.start_height, request.end_height + 1): if not self.full_node.blockchain.contains_height(uint32(i)): reject = RejectHeaderBlocks(request.start_height, request.end_height) msg = make_msg(ProtocolMessageTypes.reject_header_blocks, reject) return msg - header_hashes.append(self.full_node.blockchain.height_to_hash(uint32(i))) + header_hash: Optional[bytes32] = self.full_node.blockchain.height_to_hash(uint32(i)) + assert header_hash is not None + header_hashes.append(header_hash) - # TODO: address hint error and remove ignore - # error: Argument 1 to "get_blocks_by_hash" of "BlockStore" has incompatible type - # "List[Optional[bytes32]]"; expected "List[bytes32]" [arg-type] - blocks: List[FullBlock] = await self.full_node.block_store.get_blocks_by_hash(header_hashes) # type: ignore[arg-type] # noqa: E501 + blocks: List[FullBlock] = await self.full_node.block_store.get_blocks_by_hash(header_hashes) header_blocks = [] for block in blocks: added_coins_records = await self.full_node.coin_store.get_coins_added_at_height(block.height) diff --git a/chia/rpc/full_node_rpc_api.py b/chia/rpc/full_node_rpc_api.py index 4b843fa0bb99..04e84a9bf5d8 100644 --- a/chia/rpc/full_node_rpc_api.py +++ b/chia/rpc/full_node_rpc_api.py @@ -385,10 +385,9 @@ async def get_block_records(self, request: Dict) -> Optional[Dict]: if peak_height < uint32(a): self.service.log.warning("requested block is higher than known peak ") break - # TODO: address hint error and remove ignore - # error: Incompatible types in assignment (expression has type "Optional[bytes32]", variable has type - # "bytes32") [assignment] - header_hash: bytes32 = self.service.blockchain.height_to_hash(uint32(a)) # type: ignore[assignment] + header_hash: Optional[bytes32] = self.service.blockchain.height_to_hash(uint32(a)) + if header_hash is None: + raise ValueError(f"Height not in blockchain: {a}") record: Optional[BlockRecord] = self.service.blockchain.try_block_record(header_hash) if record is None: # Fetch from DB diff --git a/chia/server/server.py b/chia/server/server.py index 0a8983951c5a..948985165c97 100644 --- a/chia/server/server.py +++ b/chia/server/server.py @@ -632,16 +632,12 @@ async def wrapped_coroutine() -> Optional[Message]: if task_id in self.execute_tasks: self.execute_tasks.remove(task_id) - task_id = token_bytes() + task_id: bytes32 = bytes32(token_bytes(32)) api_task = asyncio.create_task(api_call(payload_inc, connection_inc, task_id)) - # TODO: address hint error and remove ignore - # error: Invalid index type "bytes" for "Dict[bytes32, Task[Any]]"; expected type "bytes32" [index] - self.api_tasks[task_id] = api_task # type: ignore[index] + self.api_tasks[task_id] = api_task if connection_inc.peer_node_id not in self.tasks_from_peer: self.tasks_from_peer[connection_inc.peer_node_id] = set() - # TODO: address hint error and remove ignore - # error: Argument 1 to "add" of "set" has incompatible type "bytes"; expected "bytes32" [arg-type] - self.tasks_from_peer[connection_inc.peer_node_id].add(task_id) # type: ignore[arg-type] + self.tasks_from_peer[connection_inc.peer_node_id].add(task_id) async def send_to_others( self, diff --git a/chia/simulator/simulator_constants.py b/chia/simulator/simulator_constants.py index 34c854a16ed3..ae09b6a34874 100644 --- a/chia/simulator/simulator_constants.py +++ b/chia/simulator/simulator_constants.py @@ -6,9 +6,6 @@ with TempKeyring() as keychain: # TODO: mariano: fix this with new consensus bt = create_block_tools(root_path=DEFAULT_ROOT_PATH, keychain=keychain) - # TODO: address hint error and remove ignore - # error: Argument 2 to "create_genesis_block" of "BlockTools" has incompatible type "bytes"; expected - # "bytes32" [arg-type] - new_genesis_block = bt.create_genesis_block(test_constants, b"0") # type: ignore[arg-type] + new_genesis_block = bt.create_genesis_block(test_constants, b"0") print(bytes(new_genesis_block)) diff --git a/chia/timelord/timelord_state.py b/chia/timelord/timelord_state.py index 72e6c7e68dd8..6431d336b880 100644 --- a/chia/timelord/timelord_state.py +++ b/chia/timelord/timelord_state.py @@ -104,11 +104,10 @@ def set_state(self, state: Union[timelord_protocol.NewPeakTimelord, EndOfSubSlot else: assert False - # TODO: address hint error and remove ignore - # error: Argument 1 to "append" of "list" has incompatible type "Tuple[Optional[bytes32], uint128]"; - # expected "Tuple[bytes32, uint128]" [arg-type] - self.reward_challenge_cache.append((self.get_challenge(Chain.REWARD_CHAIN), self.total_iters)) # type: ignore[arg-type] # noqa: E501 - log.info(f"Updated timelord peak to {self.get_challenge(Chain.REWARD_CHAIN)}, total iters: {self.total_iters}") + reward_challenge: Optional[bytes32] = self.get_challenge(Chain.REWARD_CHAIN) + assert reward_challenge is not None # Reward chain always has VDFs + self.reward_challenge_cache.append((reward_challenge, self.total_iters)) + log.info(f"Updated timelord peak to {reward_challenge}, total iters: {self.total_iters}") while len(self.reward_challenge_cache) > 2 * self.constants.MAX_SUB_SLOT_BLOCKS: self.reward_challenge_cache.pop(0) diff --git a/chia/types/blockchain_format/proof_of_space.py b/chia/types/blockchain_format/proof_of_space.py index 1b4617c8a33e..72d90a7a9f6b 100644 --- a/chia/types/blockchain_format/proof_of_space.py +++ b/chia/types/blockchain_format/proof_of_space.py @@ -28,10 +28,8 @@ class ProofOfSpace(Streamable): def get_plot_id(self) -> bytes32: assert self.pool_public_key is None or self.pool_contract_puzzle_hash is None if self.pool_public_key is None: - # TODO: address hint error and remove ignore - # error: Argument 1 to "calculate_plot_id_ph" of "ProofOfSpace" has incompatible type - # "Optional[bytes32]"; expected "bytes32" [arg-type] - return self.calculate_plot_id_ph(self.pool_contract_puzzle_hash, self.plot_public_key) # type: ignore[arg-type] # noqa: E501 + assert self.pool_contract_puzzle_hash is not None + return self.calculate_plot_id_ph(self.pool_contract_puzzle_hash, self.plot_public_key) return self.calculate_plot_id_pk(self.pool_public_key, self.plot_public_key) def verify_and_get_quality_string( diff --git a/chia/util/block_cache.py b/chia/util/block_cache.py index 17d4c74a486f..a07dc2081022 100644 --- a/chia/util/block_cache.py +++ b/chia/util/block_cache.py @@ -35,11 +35,12 @@ def block_record(self, header_hash: bytes32) -> BlockRecord: return self._block_records[header_hash] def height_to_block_record(self, height: uint32, check_db: bool = False) -> BlockRecord: - header_hash = self.height_to_hash(height) - # TODO: address hint error and remove ignore - # error: Argument 1 to "block_record" of "BlockCache" has incompatible type "Optional[bytes32]"; expected - # "bytes32" [arg-type] - return self.block_record(header_hash) # type: ignore[arg-type] + # Precondition: height is < peak height + + header_hash: Optional[bytes32] = self.height_to_hash(height) + assert header_hash is not None + + return self.block_record(header_hash) def get_ses_heights(self) -> List[uint32]: return sorted(self._sub_epoch_summaries.keys()) diff --git a/chia/util/generator_tools.py b/chia/util/generator_tools.py index 9c2780a2fff2..3034c9ec6407 100644 --- a/chia/util/generator_tools.py +++ b/chia/util/generator_tools.py @@ -11,19 +11,13 @@ def get_block_header(block: FullBlock, tx_addition_coins: List[Coin], removals_names: List[bytes32]) -> HeaderBlock: # Create filter - byte_array_tx: List[bytes32] = [] + byte_array_tx: List[bytearray] = [] addition_coins = tx_addition_coins + list(block.get_included_reward_coins()) if block.is_transaction_block(): for coin in addition_coins: - # TODO: address hint error and remove ignore - # error: Argument 1 to "append" of "list" has incompatible type "bytearray"; expected "bytes32" - # [arg-type] - byte_array_tx.append(bytearray(coin.puzzle_hash)) # type: ignore[arg-type] + byte_array_tx.append(bytearray(coin.puzzle_hash)) for name in removals_names: - # TODO: address hint error and remove ignore - # error: Argument 1 to "append" of "list" has incompatible type "bytearray"; expected "bytes32" - # [arg-type] - byte_array_tx.append(bytearray(name)) # type: ignore[arg-type] + byte_array_tx.append(bytearray(name)) bip158: PyBIP158 = PyBIP158(byte_array_tx) encoded_filter: bytes = bytes(bip158.GetEncoded()) diff --git a/tests/block_tools.py b/tests/block_tools.py index f67a5d5c9304..6e480fdd256c 100644 --- a/tests/block_tools.py +++ b/tests/block_tools.py @@ -314,7 +314,7 @@ async def new_plot( self.created_plots += 1 plot_id_new: Optional[bytes32] = None - path_new: Path = Path() + path_new: Optional[Path] = None if len(created): assert len(existed) == 0 @@ -323,12 +323,11 @@ async def new_plot( if len(existed): assert len(created) == 0 plot_id_new, path_new = list(existed.items())[0] + assert plot_id_new is not None + assert path_new is not None if not exclude_final_dir: - # TODO: address hint error and remove ignore - # error: Invalid index type "Optional[bytes32]" for "Dict[bytes32, Path]"; expected type "bytes32" - # [index] - self.expected_plots[plot_id_new] = path_new # type: ignore[index] + self.expected_plots[plot_id_new] = path_new # create_plots() updates plot_directories. Ensure we refresh our config to reflect the updated value self._config["harvester"]["plot_directories"] = load_config(self.root_path, "config.yaml", "harvester")[ @@ -456,12 +455,9 @@ def get_consecutive_blocks( if force_plot_id is not None: raise ValueError("Cannot specify plot_id for genesis block") initial_block_list_len = 0 - # TODO: address hint error and remove ignore - # error: Argument 2 to "create_genesis_block" of "BlockTools" has incompatible type "bytes"; expected - # "bytes32" [arg-type] genesis = self.create_genesis_block( constants, - seed, # type: ignore[arg-type] + seed, force_overflow=force_overflow, skip_slots=skip_slots, timestamp=(uint64(int(time.time())) if genesis_timestamp is None else genesis_timestamp), @@ -477,6 +473,7 @@ def get_consecutive_blocks( if num_blocks == 0: return block_list + blocks: Dict[bytes32, BlockRecord] height_to_hash, difficulty, blocks = load_block_list(block_list, constants) latest_block: BlockRecord = blocks[block_list[-1].header_hash] @@ -709,14 +706,9 @@ def get_consecutive_blocks( if pending_ses: sub_epoch_summary: Optional[SubEpochSummary] = None else: - # TODO: address hint error and remove ignore - # error: Argument 1 to "BlockCache" has incompatible type "Dict[uint32, BlockRecord]"; expected - # "Dict[bytes32, BlockRecord]" [arg-type] - # error: Argument 2 to "BlockCache" has incompatible type "Dict[uint32, bytes32]"; expected - # "Optional[Dict[bytes32, HeaderBlock]]" [arg-type] sub_epoch_summary = next_sub_epoch_summary( constants, - BlockCache(blocks, height_to_hash), # type: ignore[arg-type] + BlockCache(blocks, height_to_hash=height_to_hash), latest_block.required_iters, block_list[-1], False, @@ -940,13 +932,10 @@ def get_consecutive_blocks( sub_slot_iters = new_sub_slot_iters difficulty = new_difficulty - # TODO: address hint error and remove ignore - # error: Incompatible default for argument "seed" (default has type "bytes", argument has type "bytes32") - # [assignment] def create_genesis_block( self, constants: ConsensusConstants, - seed: bytes32 = b"", # type: ignore[assignment] + seed: bytes = b"", timestamp: Optional[uint64] = None, force_overflow: bool = False, skip_slots: int = 0, @@ -1395,12 +1384,10 @@ def load_block_list( quality_str = full_block.reward_chain_block.proof_of_space.verify_and_get_quality_string( constants, challenge, sp_hash ) - # TODO: address hint error and remove ignore - # error: Argument 2 to "calculate_iterations_quality" has incompatible type "Optional[bytes32]"; expected - # "bytes32" [arg-type] + assert quality_str is not None required_iters: uint64 = calculate_iterations_quality( constants.DIFFICULTY_CONSTANT_FACTOR, - quality_str, # type: ignore[arg-type] + quality_str, full_block.reward_chain_block.proof_of_space.size, uint64(difficulty), sp_hash, @@ -1533,7 +1520,7 @@ def get_full_block_and_block_record( signage_point, timestamp, BlockCache(blocks), - seed, # type: ignore[arg-type] + seed, block_generator, aggregate_signature, additions, @@ -1589,9 +1576,6 @@ def compute_cost_test(generator: BlockGenerator, cost_per_byte: int) -> Tuple[Op return uint16(Err.GENERATOR_RUNTIME_ERROR.value), uint64(0) -# TODO: address hint error and remove ignore -# error: Incompatible default for argument "seed" (default has type "bytes", argument has type "bytes32") -# [assignment] def create_test_foliage( constants: ConsensusConstants, reward_block_unfinished: RewardChainBlockUnfinished, @@ -1607,7 +1591,7 @@ def create_test_foliage( pool_target: PoolTarget, get_plot_signature: Callable[[bytes32, G1Element], G2Element], get_pool_signature: Callable[[PoolTarget, Optional[G1Element]], Optional[G2Element]], - seed: bytes32 = b"", # type: ignore[assignment] + seed: bytes = b"", ) -> Tuple[Foliage, Optional[FoliageTransactionBlock], Optional[TransactionsInfo]]: """ Creates a foliage for a given reward chain block. This may or may not be a tx block. In the case of a tx block, @@ -1642,17 +1626,14 @@ def create_test_foliage( random.seed(seed) # Use the extension data to create different blocks based on header hash - # TODO: address hint error and remove ignore - # error: Incompatible types in assignment (expression has type "bytes", variable has type "bytes32") - # [assignment] - extension_data: bytes32 = random.randint(0, 100000000).to_bytes(32, "big") # type: ignore[assignment] + extension_data: bytes32 = bytes32(random.randint(0, 100000000).to_bytes(32, "big")) if prev_block is None: height: uint32 = uint32(0) else: height = uint32(prev_block.height + 1) # Create filter - byte_array_tx: List[bytes32] = [] + byte_array_tx: List[bytearray] = [] tx_additions: List[Coin] = [] tx_removals: List[bytes32] = [] @@ -1746,16 +1727,10 @@ def create_test_foliage( additions.extend(reward_claims_incorporated.copy()) for coin in additions: tx_additions.append(coin) - # TODO: address hint error and remove ignore - # error: Argument 1 to "append" of "list" has incompatible type "bytearray"; expected "bytes32" - # [arg-type] - byte_array_tx.append(bytearray(coin.puzzle_hash)) # type: ignore[arg-type] + byte_array_tx.append(bytearray(coin.puzzle_hash)) for coin in removals: tx_removals.append(coin.name()) - # TODO: address hint error and remove ignore - # error: Argument 1 to "append" of "list" has incompatible type "bytearray"; expected "bytes32" - # [arg-type] - byte_array_tx.append(bytearray(coin.name())) # type: ignore[arg-type] + byte_array_tx.append(bytearray(coin.name())) bip158: PyBIP158 = PyBIP158(byte_array_tx) encoded = bytes(bip158.GetEncoded()) @@ -1820,10 +1795,9 @@ def create_test_foliage( assert foliage_transaction_block is not None foliage_transaction_block_hash: Optional[bytes32] = foliage_transaction_block.get_hash() - # TODO: address hint error and remove ignore - # error: Argument 1 has incompatible type "Optional[bytes32]"; expected "bytes32" [arg-type] + assert foliage_transaction_block_hash is not None foliage_transaction_block_signature: Optional[G2Element] = get_plot_signature( - foliage_transaction_block_hash, # type: ignore[arg-type] + foliage_transaction_block_hash, reward_block_unfinished.proof_of_space.plot_public_key, ) assert foliage_transaction_block_signature is not None @@ -1846,9 +1820,6 @@ def create_test_foliage( return foliage, foliage_transaction_block, transactions_info -# TODO: address hint error and remove ignore -# error: Incompatible default for argument "seed" (default has type "bytes", argument has type "bytes32") -# [assignment] def create_test_unfinished_block( constants: ConsensusConstants, sub_slot_start_total_iters: uint128, @@ -1865,7 +1836,7 @@ def create_test_unfinished_block( signage_point: SignagePoint, timestamp: uint64, blocks: BlockchainInterface, - seed: bytes32 = b"", # type: ignore[assignment] + seed: bytes = b"", block_generator: Optional[BlockGenerator] = None, aggregate_sig: G2Element = G2Element(), additions: Optional[List[Coin]] = None, @@ -1914,7 +1885,7 @@ def create_test_unfinished_block( new_sub_slot: bool = len(finished_sub_slots) > 0 - cc_sp_hash: Optional[bytes32] = slot_cc_challenge + cc_sp_hash: bytes32 = slot_cc_challenge # Only enters this if statement if we are in testing mode (making VDF proofs here) if signage_point.cc_vdf is not None: @@ -1937,10 +1908,8 @@ def create_test_unfinished_block( rc_sp_hash = curr.finished_reward_slot_hashes[-1] signage_point = SignagePoint(None, None, None, None) - # TODO: address hint error and remove ignore - # error: Argument 1 has incompatible type "Optional[bytes32]"; expected "bytes32" [arg-type] cc_sp_signature: Optional[G2Element] = get_plot_signature( - cc_sp_hash, # type: ignore[arg-type] + cc_sp_hash, proof_of_space.plot_public_key, ) rc_sp_signature: Optional[G2Element] = get_plot_signature(rc_sp_hash, proof_of_space.plot_public_key) diff --git a/tests/core/full_node/stores/test_full_node_store.py b/tests/core/full_node/stores/test_full_node_store.py index ca6c14cb14b3..f3d6c441aa8f 100644 --- a/tests/core/full_node/stores/test_full_node_store.py +++ b/tests/core/full_node/stores/test_full_node_store.py @@ -422,10 +422,8 @@ async def test_basic_store(self, empty_blockchain, normalized_to_identity: bool ) # Get signage point by hash - # TODO: address hint error and remove ignore - # error: Argument 1 to "get_signage_point" of "FullNodeStore" has incompatible type "Optional[bytes32]"; - # expected "bytes32" [arg-type] - assert store.get_signage_point(saved_sp_hash) is not None # type: ignore[arg-type] + assert saved_sp_hash is not None + assert store.get_signage_point(saved_sp_hash) is not None assert store.get_signage_point(std_hash(b"2")) is None # Test adding signage points before genesis diff --git a/tests/core/full_node/test_block_height_map.py b/tests/core/full_node/test_block_height_map.py index dfc34cf88c72..835023c0fa0e 100644 --- a/tests/core/full_node/test_block_height_map.py +++ b/tests/core/full_node/test_block_height_map.py @@ -12,9 +12,7 @@ def gen_block_hash(height: int) -> bytes32: - # TODO: address hint errors and remove ignores - # error: Incompatible return value type (got "bytes", expected "bytes32") [return-value] - return struct.pack(">I", height + 1) * (32 // 4) # type: ignore[return-value] + return bytes32(struct.pack(">I", height + 1) * (32 // 4)) def gen_ses(height: int) -> SubEpochSummary: diff --git a/tests/core/full_node/test_mempool.py b/tests/core/full_node/test_mempool.py index 0961a6153240..ad5ea9cce796 100644 --- a/tests/core/full_node/test_mempool.py +++ b/tests/core/full_node/test_mempool.py @@ -111,15 +111,13 @@ async def two_nodes_mempool(bt, wallet_a): def make_item(idx: int, cost: uint64 = uint64(80)) -> MempoolItem: - spend_bundle_name = bytes([idx] * 32) - # TODO: address hint error and remove ignore - # error: Argument 5 to "MempoolItem" has incompatible type "bytes"; expected "bytes32" [arg-type] + spend_bundle_name = bytes32([idx] * 32) return MempoolItem( SpendBundle([], G2Element()), uint64(0), NPCResult(None, [], cost), cost, - spend_bundle_name, # type: ignore[arg-type] + spend_bundle_name, [], [], SerializedProgram(), diff --git a/tests/core/make_block_generator.py b/tests/core/make_block_generator.py index cea8464366a9..274b623843bb 100644 --- a/tests/core/make_block_generator.py +++ b/tests/core/make_block_generator.py @@ -5,6 +5,7 @@ from chia.full_node.bundle_tools import simple_solution_generator from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.program import Program +from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.coin_spend import CoinSpend from chia.types.condition_opcodes import ConditionOpcode from chia.types.generator_types import BlockGenerator @@ -21,10 +22,10 @@ def int_to_public_key(index: int) -> blspy.G1Element: return private_key_from_int.get_g1() -def puzzle_hash_for_index(index: int, puzzle_hash_db: dict) -> bytes: - public_key = bytes(int_to_public_key(index)) - puzzle = puzzle_for_pk(public_key) - puzzle_hash = puzzle.get_tree_hash() +def puzzle_hash_for_index(index: int, puzzle_hash_db: dict) -> bytes32: + public_key: blspy.G1Element = int_to_public_key(index) + puzzle: Program = puzzle_for_pk(public_key) + puzzle_hash: bytes32 = puzzle.get_tree_hash() puzzle_hash_db[puzzle_hash] = puzzle return puzzle_hash @@ -34,13 +35,10 @@ def make_fake_coin(index: int, puzzle_hash_db: dict) -> Coin: Make a fake coin with parent id equal to the index (ie. a genesis block coin) """ - parent = index.to_bytes(32, "big") - puzzle_hash = puzzle_hash_for_index(index, puzzle_hash_db) - amount = 100000 - # TODO: address hint error and remove ignore - # error: Argument 1 to "Coin" has incompatible type "bytes"; expected "bytes32" [arg-type] - # error: Argument 2 to "Coin" has incompatible type "bytes"; expected "bytes32" [arg-type] - return Coin(parent, puzzle_hash, uint64(amount)) # type: ignore[arg-type] + parent: bytes32 = bytes32(index.to_bytes(32, "big")) + puzzle_hash: bytes32 = puzzle_hash_for_index(index, puzzle_hash_db) + amount: uint64 = uint64(100000) + return Coin(parent, puzzle_hash, amount) def conditions_for_payment(coin) -> Program: diff --git a/tests/pools/test_wallet_pool_store.py b/tests/pools/test_wallet_pool_store.py index 8d5c43c7ae6e..27c3152a2c29 100644 --- a/tests/pools/test_wallet_pool_store.py +++ b/tests/pools/test_wallet_pool_store.py @@ -17,10 +17,7 @@ def make_child_solution(coin_spend: CoinSpend, new_coin: Optional[Coin] = None) -> CoinSpend: - # TODO: address hint error and remove ignore - # error: Incompatible types in assignment (expression has type "bytes", variable has type "bytes32") - # [assignment] - new_puzzle_hash: bytes32 = token_bytes(32) # type: ignore[assignment] + new_puzzle_hash: bytes32 = bytes32(token_bytes(32)) solution = "()" puzzle = f"(q . ((51 0x{new_puzzle_hash.hex()} 1)))" puzzle_prog = Program.to(binutils.assemble(puzzle)) diff --git a/tests/util/key_tool.py b/tests/util/key_tool.py index 157223d8e1c6..8b74ff6ef6f8 100644 --- a/tests/util/key_tool.py +++ b/tests/util/key_tool.py @@ -2,7 +2,6 @@ from blspy import AugSchemeMPL, G2Element, PrivateKey -from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.coin_spend import CoinSpend from chia.util.condition_tools import conditions_by_opcode, conditions_for_solution, pkm_pairs_for_conditions_dict from tests.core.make_block_generator import GROUP_ORDER, int_to_public_key @@ -18,12 +17,12 @@ def add_secret_exponents(self, secret_exponents: List[int]) -> None: for _ in secret_exponents: self[bytes(int_to_public_key(_))] = _ % GROUP_ORDER - def sign(self, public_key: bytes, message_hash: bytes32) -> G2Element: + def sign(self, public_key: bytes, message: bytes) -> G2Element: secret_exponent = self.get(public_key) if not secret_exponent: raise ValueError("unknown pubkey %s" % public_key.hex()) bls_private_key = PrivateKey.from_bytes(secret_exponent.to_bytes(32, "big")) - return AugSchemeMPL.sign(bls_private_key, message_hash) + return AugSchemeMPL.sign(bls_private_key, message) def signature_for_solution(self, coin_spend: CoinSpend, additional_data: bytes) -> AugSchemeMPL: signatures = [] @@ -32,12 +31,9 @@ def signature_for_solution(self, coin_spend: CoinSpend, additional_data: bytes) ) assert conditions is not None conditions_dict = conditions_by_opcode(conditions) - for public_key, message_hash in pkm_pairs_for_conditions_dict( + for public_key, message in pkm_pairs_for_conditions_dict( conditions_dict, coin_spend.coin.name(), additional_data ): - # TODO: address hint error and remove ignore - # error: Argument 2 to "sign" of "KeyTool" has incompatible type "bytes"; expected "bytes32" - # [arg-type] - signature = self.sign(public_key, message_hash) # type: ignore[arg-type] + signature = self.sign(public_key, message) signatures.append(signature) return AugSchemeMPL.aggregate(signatures) diff --git a/tests/util/network_protocol_data.py b/tests/util/network_protocol_data.py index 8d8c818ec323..0de8ddf76638 100644 --- a/tests/util/network_protocol_data.py +++ b/tests/util/network_protocol_data.py @@ -66,10 +66,8 @@ ), ) -# TODO: address hint error and remove ignore -# error: Argument 1 to "PoolTarget" has incompatible type "bytes"; expected "bytes32" [arg-type] pool_target = PoolTarget( - bytes.fromhex("d23da14695a188ae5708dd152263c4db883eb27edeb936178d4d988b8f3ce5fc"), # type: ignore[arg-type] + bytes32.from_hexstr("d23da14695a188ae5708dd152263c4db883eb27edeb936178d4d988b8f3ce5fc"), uint32(421941852), ) g2_element = G2Element( diff --git a/tests/wallet_tools.py b/tests/wallet_tools.py index e1df8bb8792a..6600dee0e36d 100644 --- a/tests/wallet_tools.py +++ b/tests/wallet_tools.py @@ -1,13 +1,13 @@ from typing import Dict, List, Optional, Tuple, Any -from blspy import AugSchemeMPL, G2Element, PrivateKey +from blspy import AugSchemeMPL, G2Element, PrivateKey, G1Element from clvm.casts import int_from_bytes, int_to_bytes from chia.consensus.constants import ConsensusConstants from chia.util.hash import std_hash from chia.types.announcement import Announcement from chia.types.blockchain_format.coin import Coin -from chia.types.blockchain_format.program import Program +from chia.types.blockchain_format.program import Program, SerializedProgram from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.coin_spend import CoinSpend from chia.types.condition_opcodes import ConditionOpcode @@ -63,21 +63,19 @@ def get_private_key_for_puzzle_hash(self, puzzle_hash: bytes32) -> PrivateKey: def puzzle_for_pk(self, pubkey: bytes) -> Program: return puzzle_for_pk(pubkey) - def get_new_puzzle(self) -> bytes32: + def get_new_puzzle(self) -> Program: next_address_index: uint32 = self.get_next_address_index() - pubkey = master_sk_to_wallet_sk(self.private_key, next_address_index).get_g1() + pubkey: G1Element = master_sk_to_wallet_sk(self.private_key, next_address_index).get_g1() self.pubkey_num_lookup[bytes(pubkey)] = next_address_index - puzzle = puzzle_for_pk(bytes(pubkey)) + puzzle: Program = puzzle_for_pk(pubkey) self.puzzle_pk_cache[puzzle.get_tree_hash()] = next_address_index return puzzle def get_new_puzzlehash(self) -> bytes32: puzzle = self.get_new_puzzle() - # TODO: address hint error and remove ignore - # error: "bytes32" has no attribute "get_tree_hash" [attr-defined] - return puzzle.get_tree_hash() # type: ignore[attr-defined] + return puzzle.get_tree_hash() def sign(self, value: bytes, pubkey: bytes) -> G2Element: privatekey: PrivateKey = master_sk_to_wallet_sk(self.private_key, self.pubkey_num_lookup[pubkey]) @@ -141,15 +139,13 @@ def generate_unsigned_transaction( if secret_key is None: secret_key = self.get_private_key_for_puzzle_hash(puzzle_hash) pubkey = secret_key.get_g1() - puzzle = puzzle_for_pk(bytes(pubkey)) + puzzle: Program = puzzle_for_pk(pubkey) if n == 0: message_list = [c.name() for c in coins] for outputs in condition_dic[ConditionOpcode.CREATE_COIN]: - # TODO: address hint error and remove ignore - # error: Argument 2 to "Coin" has incompatible type "bytes"; expected "bytes32" [arg-type] coin_to_append = Coin( coin.name(), - outputs.vars[0], # type: ignore[arg-type] + bytes32(outputs.vars[0]), int_from_bytes(outputs.vars[1]), ) message_list.append(coin_to_append.name()) @@ -162,9 +158,19 @@ def generate_unsigned_transaction( ConditionWithArgs(ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT, [primary_announcement_hash]) ) main_solution = self.make_solution(condition_dic) - spends.append(CoinSpend(coin, puzzle, main_solution)) + spends.append( + CoinSpend( + coin, SerializedProgram.from_program(puzzle), SerializedProgram.from_program(main_solution) + ) + ) else: - spends.append(CoinSpend(coin, puzzle, self.make_solution(secondary_coins_cond_dic))) + spends.append( + CoinSpend( + coin, + SerializedProgram.from_program(puzzle), + SerializedProgram.from_program(self.make_solution(secondary_coins_cond_dic)), + ) + ) return spends def sign_transaction(self, coin_spends: List[CoinSpend]) -> SpendBundle: diff --git a/tests/weight_proof/test_weight_proof.py b/tests/weight_proof/test_weight_proof.py index e7e7b86e6c85..843016f9cb7c 100644 --- a/tests/weight_proof/test_weight_proof.py +++ b/tests/weight_proof/test_weight_proof.py @@ -107,12 +107,9 @@ async def load_blocks_dont_validate( cc_sp, ) - # TODO: address hint error and remove ignore - # error: Argument 2 to "BlockCache" has incompatible type "Dict[uint32, bytes32]"; expected - # "Optional[Dict[bytes32, HeaderBlock]]" [arg-type] sub_block = block_to_block_record( test_constants, - BlockCache(sub_blocks, height_to_hash), # type: ignore[arg-type] + BlockCache(sub_blocks, height_to_hash=height_to_hash), required_iters, block, None, From 62de4a883cdca69a63498b3c03e15872c2cb206b Mon Sep 17 00:00:00 2001 From: dustinface <35775977+xdustinface@users.noreply.github.com> Date: Mon, 4 Apr 2022 20:50:59 +0200 Subject: [PATCH 315/378] streamable|pools: Fix `Optional` parsing in `dataclass_from_dict` (#10573) * Test more `Optional` parsing in `dataclass_from_dict` * Fix optional parsing in `dataclass_from_dict` * Fix pool wallet / tests --- chia/pools/pool_wallet.py | 2 +- chia/util/streamable.py | 2 +- tests/core/util/test_streamable.py | 27 +++++++++++++++++++++++++++ tests/pools/test_pool_rpc.py | 4 ++-- 4 files changed, 31 insertions(+), 4 deletions(-) diff --git a/chia/pools/pool_wallet.py b/chia/pools/pool_wallet.py index a2df61234926..341cb758d9b1 100644 --- a/chia/pools/pool_wallet.py +++ b/chia/pools/pool_wallet.py @@ -127,7 +127,7 @@ def id(self): @classmethod def _verify_self_pooled(cls, state) -> Optional[str]: err = "" - if state.pool_url != "": + if state.pool_url not in [None, ""]: err += " Unneeded pool_url for self-pooling" if state.relative_lock_height != 0: diff --git a/chia/util/streamable.py b/chia/util/streamable.py index 73584bac7ed1..2602709600be 100644 --- a/chia/util/streamable.py +++ b/chia/util/streamable.py @@ -55,7 +55,7 @@ def dataclass_from_dict(klass, d): """ if is_type_SpecificOptional(klass): # Type is optional, data is either None, or Any - if not d: + if d is None: return None return dataclass_from_dict(get_args(klass)[0], d) elif is_type_Tuple(klass): diff --git a/tests/core/util/test_streamable.py b/tests/core/util/test_streamable.py index 7b945d502c67..5562d03d15b3 100644 --- a/tests/core/util/test_streamable.py +++ b/tests/core/util/test_streamable.py @@ -1,6 +1,7 @@ from dataclasses import dataclass from typing import List, Optional, Tuple import io +import pytest from clvm_tools import binutils from pytest import raises @@ -71,6 +72,32 @@ def test_json(bt): assert FullBlock.from_json_dict(dict_block) == block +@dataclass(frozen=True) +@streamable +class OptionalTestClass(Streamable): + a: Optional[str] + b: Optional[bool] + c: Optional[List[Optional[str]]] + + +@pytest.mark.parametrize( + "a, b, c", + [ + ("", True, ["1"]), + ("1", False, ["1"]), + ("1", True, []), + ("1", True, [""]), + ("1", True, ["1"]), + (None, None, None), + ], +) +def test_optional_json(a: Optional[str], b: Optional[bool], c: Optional[List[Optional[str]]]): + obj: OptionalTestClass = OptionalTestClass.from_json_dict({"a": a, "b": b, "c": c}) + assert obj.a == a + assert obj.b == b + assert obj.c == c + + def test_recursive_json(): @dataclass(frozen=True) @streamable diff --git a/tests/pools/test_pool_rpc.py b/tests/pools/test_pool_rpc.py index d49b0b31038b..504431f5e979 100644 --- a/tests/pools/test_pool_rpc.py +++ b/tests/pools/test_pool_rpc.py @@ -211,7 +211,7 @@ async def test_create_new_pool_wallet_self_farm(self, one_wallet_node_and_rpc, t "b286bbf7a10fa058d2a2a758921377ef00bb7f8143e1bd40dd195ae918dbef42cfc481140f01b9eae13b430a0c8fe304" ) ) - assert status.current.pool_url is None + assert status.current.pool_url == "" assert status.current.relative_lock_height == 0 assert status.current.version == 1 # Check that config has been written properly @@ -893,7 +893,7 @@ async def have_chia(): status: PoolWalletInfo = (await client.pw_status(wallet_id))[0] assert status.current.state == PoolSingletonState.SELF_POOLING.value - assert status.current.pool_url is None + assert status.current.pool_url == "" assert status.current.relative_lock_height == 0 assert status.current.state == 1 assert status.current.version == 1 From fe77c690182e97f7ef13d1fb383481f32efe2e87 Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Mon, 4 Apr 2022 20:53:13 +0200 Subject: [PATCH 316/378] run_generator2 rust call and compact conditions data structure (#8862) * use run_generator2 rust call and compact spend bundle conditions data structure pervasively. * address review comments --- chia/consensus/block_body_validation.py | 59 ++-- chia/consensus/blockchain.py | 23 +- chia/consensus/cost_calculator.py | 6 +- chia/consensus/multiprocess_validation.py | 13 +- chia/full_node/full_node.py | 5 +- chia/full_node/mempool_check_conditions.py | 187 ++----------- chia/full_node/mempool_manager.py | 82 +++--- chia/types/blockchain_format/program.py | 32 ++- chia/types/name_puzzle_condition.py | 23 -- chia/types/spend_bundle_conditions.py | 30 ++ chia/util/condition_tools.py | 73 +---- chia/util/generator_tools.py | 28 +- tests/clvm/coin_store.py | 32 +-- .../core/full_node/stores/test_coin_store.py | 2 +- tests/core/full_node/test_mempool.py | 263 +++++++----------- tests/core/test_cost_calculation.py | 9 +- tests/generator/test_rom.py | 20 +- tests/util/generator_tools_testing.py | 9 +- tools/run_block.py | 9 +- 19 files changed, 346 insertions(+), 559 deletions(-) delete mode 100644 chia/types/name_puzzle_condition.py create mode 100644 chia/types/spend_bundle_conditions.py diff --git a/chia/consensus/block_body_validation.py b/chia/consensus/block_body_validation.py index 9bb704c73000..7ba5510ff501 100644 --- a/chia/consensus/block_body_validation.py +++ b/chia/consensus/block_body_validation.py @@ -3,7 +3,6 @@ from typing import Awaitable, Callable, Dict, List, Optional, Set, Tuple, Union from chiabip158 import PyBIP158 -from clvm.casts import int_from_bytes from chia.consensus.block_record import BlockRecord from chia.consensus.block_rewards import calculate_base_farmer_reward, calculate_pool_reward @@ -15,23 +14,20 @@ from chia.consensus.find_fork_point import find_fork_point_in_chain from chia.full_node.block_store import BlockStore from chia.full_node.coin_store import CoinStore -from chia.full_node.mempool_check_conditions import get_name_puzzle_conditions, mempool_check_conditions_dict +from chia.full_node.mempool_check_conditions import get_name_puzzle_conditions, mempool_check_time_locks from chia.types.block_protocol import BlockInfo from chia.types.blockchain_format.coin import Coin -from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.blockchain_format.sized_bytes import bytes32, bytes48 from chia.types.coin_record import CoinRecord -from chia.types.condition_opcodes import ConditionOpcode -from chia.types.condition_with_args import ConditionWithArgs from chia.types.full_block import FullBlock from chia.types.generator_types import BlockGenerator -from chia.types.name_puzzle_condition import NPC from chia.types.unfinished_block import UnfinishedBlock from chia.util import cached_bls from chia.util.condition_tools import pkm_pairs from chia.util.errors import Err -from chia.util.generator_tools import additions_for_npc, tx_removals_and_additions +from chia.util.generator_tools import tx_removals_and_additions from chia.util.hash import std_hash -from chia.util.ints import uint32, uint64, uint128 +from chia.util.ints import uint32, uint64 log = logging.getLogger(__name__) @@ -153,7 +149,6 @@ async def validate_block_body( removals: List[bytes32] = [] coinbase_additions: List[Coin] = list(expected_reward_coins) additions: List[Coin] = [] - npc_list: List[NPC] = [] removals_puzzle_dic: Dict[bytes32, bytes32] = {} cost: uint64 = uint64(0) @@ -196,7 +191,6 @@ async def validate_block_body( assert npc_result is not None cost = npc_result.cost - npc_list = npc_result.npc_list # 7. Check that cost <= MAX_BLOCK_COST_CLVM log.debug( @@ -210,11 +204,13 @@ async def validate_block_body( if npc_result.error is not None: return Err(npc_result.error), None - for npc in npc_list: - removals.append(npc.coin_name) - removals_puzzle_dic[npc.coin_name] = npc.puzzle_hash + assert npc_result.conds is not None - additions = additions_for_npc(npc_list) + for spend in npc_result.conds.spends: + removals.append(spend.coin_id) + removals_puzzle_dic[spend.coin_id] = spend.puzzle_hash + for puzzle_hash, amount, _ in spend.create_coin: + additions.append(Coin(spend.coin_id, puzzle_hash, uint64(amount))) else: assert npc_result is None @@ -317,7 +313,7 @@ async def validate_block_body( mempool_mode=False, height=curr.height, ) - removals_in_curr, additions_in_curr = tx_removals_and_additions(curr_npc_result.npc_list) + removals_in_curr, additions_in_curr = tx_removals_and_additions(curr_npc_result.conds) else: removals_in_curr = [] additions_in_curr = [] @@ -401,16 +397,13 @@ async def validate_block_body( fees = removed - added assert fees >= 0 - assert_fee_sum: uint128 = uint128(0) - for npc in npc_list: - if ConditionOpcode.RESERVE_FEE in npc.condition_dict: - fee_list: List[ConditionWithArgs] = npc.condition_dict[ConditionOpcode.RESERVE_FEE] - for cvp in fee_list: - fee = int_from_bytes(cvp.vars[0]) - if fee < 0: - return Err.RESERVE_FEE_CONDITION_FAILED, None - assert_fee_sum = uint128(assert_fee_sum + fee) + # reserve fee cannot be greater than UINT64_MAX per consensus rule. + # run_generator() would fail + assert_fee_sum: uint64 = uint64(0) + if npc_result: + assert npc_result.conds is not None + assert_fee_sum = npc_result.conds.reserve_fee # 17. Check that the assert fee sum <= fees, and that each reserved fee is non-negative if fees < assert_fee_sum: @@ -430,12 +423,12 @@ async def validate_block_body( return Err.WRONG_PUZZLE_HASH, None # 21. Verify conditions - for npc in npc_list: - assert height is not None - unspent = removal_coin_records[npc.coin_name] - error = mempool_check_conditions_dict( - unspent, - npc.condition_dict, + # verify absolute/relative height/time conditions + if npc_result is not None: + assert npc_result.conds is not None + error = mempool_check_time_locks( + removal_coin_records, + npc_result.conds, prev_transaction_block_height, block.foliage_transaction_block.timestamp, ) @@ -443,7 +436,11 @@ async def validate_block_body( return error, None # create hash_key list for aggsig check - pairs_pks, pairs_msgs = pkm_pairs(npc_list, constants.AGG_SIG_ME_ADDITIONAL_DATA) + pairs_pks: List[bytes48] = [] + pairs_msgs: List[bytes] = [] + if npc_result: + assert npc_result.conds is not None + pairs_pks, pairs_msgs = pkm_pairs(npc_result.conds, constants.AGG_SIG_ME_ADDITIONAL_DATA) # 22. Verify aggregated signature # TODO: move this to pre_validate_blocks_multiprocessing so we can sync faster diff --git a/chia/consensus/blockchain.py b/chia/consensus/blockchain.py index 4f8dccfc5018..f436572bc752 100644 --- a/chia/consensus/blockchain.py +++ b/chia/consensus/blockchain.py @@ -10,8 +10,6 @@ from pathlib import Path from typing import Dict, List, Optional, Set, Tuple -from clvm.casts import int_from_bytes - from chia.consensus.block_body_validation import validate_block_body from chia.consensus.block_header_validation import validate_unfinished_header_block from chia.consensus.block_record import BlockRecord @@ -38,7 +36,6 @@ from chia.types.blockchain_format.sub_epoch_summary import SubEpochSummary from chia.types.blockchain_format.vdf import VDFInfo from chia.types.coin_record import CoinRecord -from chia.types.condition_opcodes import ConditionOpcode from chia.types.end_of_slot_bundle import EndOfSubSlotBundle from chia.types.full_block import FullBlock from chia.types.generator_types import BlockGenerator @@ -301,16 +298,14 @@ async def receive_block( return ReceiveBlockResult.ADDED_AS_ORPHAN, None, None, ([], {}) def get_hint_list(self, npc_result: NPCResult) -> List[Tuple[bytes32, bytes]]: + if npc_result.conds is None: + return [] h_list = [] - for npc in npc_result.npc_list: - for opcode, conditions in npc.conditions: - if opcode == ConditionOpcode.CREATE_COIN: - for condition in conditions: - if len(condition.vars) > 2 and condition.vars[2] != b"": - puzzle_hash, amount_bin = bytes32(condition.vars[0]), condition.vars[1] - amount: uint64 = uint64(int_from_bytes(amount_bin)) - coin_id: bytes32 = Coin(npc.coin_name, puzzle_hash, amount).name() - h_list.append((coin_id, condition.vars[2])) + for spend in npc_result.conds.spends: + for puzzle_hash, amount, hint in spend.create_coin: + if hint != b"": + coin_id = Coin(spend.coin_id, puzzle_hash, amount).name() + h_list.append((coin_id, hint)) return h_list async def _reconsider_peak( @@ -341,7 +336,7 @@ async def _reconsider_peak( assert block is not None if npc_result is not None: - tx_removals, tx_additions = tx_removals_and_additions(npc_result.npc_list) + tx_removals, tx_additions = tx_removals_and_additions(npc_result.conds) else: tx_removals, tx_additions = [], [] if block.is_transaction_block(): @@ -473,7 +468,7 @@ async def get_tx_removals_and_additions( mempool_mode=False, height=block.height, ) - tx_removals, tx_additions = tx_removals_and_additions(npc_result.npc_list) + tx_removals, tx_additions = tx_removals_and_additions(npc_result.conds) return tx_removals, tx_additions, npc_result def get_next_difficulty(self, header_hash: bytes32, new_slot: bool) -> uint64: diff --git a/chia/consensus/cost_calculator.py b/chia/consensus/cost_calculator.py index d090116d56e1..c207ccf56123 100644 --- a/chia/consensus/cost_calculator.py +++ b/chia/consensus/cost_calculator.py @@ -1,7 +1,7 @@ from dataclasses import dataclass -from typing import List, Optional +from typing import Optional -from chia.types.name_puzzle_condition import NPC +from chia.types.spend_bundle_conditions import SpendBundleConditions from chia.util.ints import uint16, uint64 from chia.util.streamable import Streamable, streamable @@ -10,6 +10,6 @@ @streamable class NPCResult(Streamable): error: Optional[uint16] - npc_list: List[NPC] + conds: Optional[SpendBundleConditions] cost: uint64 # The total cost of the block, including CLVM cost, cost of # conditions and cost of bytes diff --git a/chia/consensus/multiprocess_validation.py b/chia/consensus/multiprocess_validation.py index 0e0ad3ff85fd..d908ab5808a7 100644 --- a/chia/consensus/multiprocess_validation.py +++ b/chia/consensus/multiprocess_validation.py @@ -75,8 +75,8 @@ def batch_pre_validate_blocks( if block.height in npc_results: npc_result = NPCResult.from_bytes(npc_results[block.height]) assert npc_result is not None - if npc_result.npc_list is not None: - removals, tx_additions = tx_removals_and_additions(npc_result.npc_list) + if npc_result.conds is not None: + removals, tx_additions = tx_removals_and_additions(npc_result.conds) else: removals, tx_additions = [], [] @@ -93,7 +93,7 @@ def batch_pre_validate_blocks( mempool_mode=False, height=block.height, ) - removals, tx_additions = tx_removals_and_additions(npc_result.npc_list) + removals, tx_additions = tx_removals_and_additions(npc_result.conds) if npc_result is not None and npc_result.error is not None: results.append(PreValidationResult(uint16(npc_result.error), None, npc_result, False)) continue @@ -120,7 +120,8 @@ def batch_pre_validate_blocks( # validate it later. receive_block will attempt to validate the signature later. if validate_signatures: if npc_result is not None and block.transactions_info is not None: - pairs_pks, pairs_msgs = pkm_pairs(npc_result.npc_list, constants.AGG_SIG_ME_ADDITIONAL_DATA) + assert npc_result.conds + pairs_pks, pairs_msgs = pkm_pairs(npc_result.conds, constants.AGG_SIG_ME_ADDITIONAL_DATA) pks_objects: List[G1Element] = [G1Element.from_bytes(pk) for pk in pairs_pks] if not AugSchemeMPL.aggregate_verify( pks_objects, pairs_msgs, block.transactions_info.aggregated_signature @@ -387,6 +388,6 @@ def _run_generator( ) return bytes(npc_result) except ValidationError as e: - return bytes(NPCResult(uint16(e.code.value), [], uint64(0))) + return bytes(NPCResult(uint16(e.code.value), None, uint64(0))) except Exception: - return bytes(NPCResult(uint16(Err.UNKNOWN.value), [], uint64(0))) + return bytes(NPCResult(uint16(Err.UNKNOWN.value), None, uint64(0))) diff --git a/chia/full_node/full_node.py b/chia/full_node/full_node.py index 5ac784804f39..4c5ebbe54143 100644 --- a/chia/full_node/full_node.py +++ b/chia/full_node/full_node.py @@ -1736,7 +1736,10 @@ async def respond_unfinished_block( npc_result = await self.blockchain.run_generator(block_bytes, block_generator, height) pre_validation_time = time.time() - pre_validation_start - pairs_pks, pairs_msgs = pkm_pairs(npc_result.npc_list, self.constants.AGG_SIG_ME_ADDITIONAL_DATA) + # blockchain.run_generator throws on errors, so npc_result is + # guaranteed to represent a successful run + assert npc_result.conds is not None + pairs_pks, pairs_msgs = pkm_pairs(npc_result.conds, self.constants.AGG_SIG_ME_ADDITIONAL_DATA) if not cached_bls.aggregate_verify( pairs_pks, pairs_msgs, block.transactions_info.aggregated_signature, True ): diff --git a/chia/full_node/mempool_check_conditions.py b/chia/full_node/mempool_check_conditions.py index d14500ffbbbd..10c44cd1e329 100644 --- a/chia/full_node/mempool_check_conditions.py +++ b/chia/full_node/mempool_check_conditions.py @@ -1,17 +1,15 @@ import logging -import time -from typing import Dict, List, Optional + +from typing import Dict, Optional from clvm_rs import MEMPOOL_MODE, COND_CANON_INTS, NO_NEG_DIV -from clvm.casts import int_from_bytes, int_to_bytes -from chia.consensus.cost_calculator import NPCResult from chia.consensus.default_constants import DEFAULT_CONSTANTS +from chia.consensus.cost_calculator import NPCResult +from chia.types.spend_bundle_conditions import SpendBundleConditions from chia.full_node.generator import create_generator_args, setup_generator_args from chia.types.coin_record import CoinRecord -from chia.types.condition_opcodes import ConditionOpcode -from chia.types.condition_with_args import ConditionWithArgs from chia.types.generator_types import BlockGenerator -from chia.types.name_puzzle_condition import NPC +from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.errors import Err from chia.util.ints import uint32, uint64, uint16 from chia.wallet.puzzles.generator_loader import GENERATOR_FOR_SINGLE_COIN_MOD @@ -22,91 +20,6 @@ log = logging.getLogger(__name__) -def mempool_assert_absolute_block_height_exceeds( - condition: ConditionWithArgs, prev_transaction_block_height: uint32 -) -> Optional[Err]: - """ - Checks if the next block index exceeds the block index from the condition - """ - try: - block_index_exceeds_this = int_from_bytes(condition.vars[0]) - except ValueError: - return Err.INVALID_CONDITION - if prev_transaction_block_height < block_index_exceeds_this: - return Err.ASSERT_HEIGHT_ABSOLUTE_FAILED - return None - - -def mempool_assert_relative_block_height_exceeds( - condition: ConditionWithArgs, unspent: CoinRecord, prev_transaction_block_height: uint32 -) -> Optional[Err]: - """ - Checks if the coin age exceeds the age from the condition - """ - try: - expected_block_age = int_from_bytes(condition.vars[0]) - block_index_exceeds_this = expected_block_age + unspent.confirmed_block_index - except ValueError: - return Err.INVALID_CONDITION - if prev_transaction_block_height < block_index_exceeds_this: - return Err.ASSERT_HEIGHT_RELATIVE_FAILED - return None - - -def mempool_assert_absolute_time_exceeds(condition: ConditionWithArgs, timestamp: uint64) -> Optional[Err]: - """ - Check if the current time in seconds exceeds the time specified by condition - """ - try: - expected_seconds = int_from_bytes(condition.vars[0]) - except ValueError: - return Err.INVALID_CONDITION - - if timestamp is None: - timestamp = uint64(int(time.time())) - if timestamp < expected_seconds: - return Err.ASSERT_SECONDS_ABSOLUTE_FAILED - return None - - -def mempool_assert_relative_time_exceeds( - condition: ConditionWithArgs, unspent: CoinRecord, timestamp: uint64 -) -> Optional[Err]: - """ - Check if the current time in seconds exceeds the time specified by condition - """ - try: - expected_seconds = int_from_bytes(condition.vars[0]) - except ValueError: - return Err.INVALID_CONDITION - - if timestamp is None: - timestamp = uint64(int(time.time())) - if timestamp < expected_seconds + unspent.timestamp: - return Err.ASSERT_SECONDS_RELATIVE_FAILED - return None - - -def add_int_cond( - conds: Dict[ConditionOpcode, List[ConditionWithArgs]], - op: ConditionOpcode, - arg: int, -): - if op not in conds: - conds[op] = [] - conds[op].append(ConditionWithArgs(op, [int_to_bytes(arg)])) - - -def add_cond( - conds: Dict[ConditionOpcode, List[ConditionWithArgs]], - op: ConditionOpcode, - args: List[bytes], -): - if op not in conds: - conds[op] = [] - conds[op].append(ConditionWithArgs(op, args)) - - def unwrap(x: Optional[uint32]) -> uint32: assert x is not None return x @@ -119,7 +32,7 @@ def get_name_puzzle_conditions( size_cost = len(bytes(generator.program)) * cost_per_byte max_cost -= size_cost if max_cost < 0: - return NPCResult(uint16(Err.INVALID_BLOCK_COST.value), [], uint64(0)) + return NPCResult(uint16(Err.INVALID_BLOCK_COST.value), None, uint64(0)) # in mempool mode, the height doesn't matter, because it's always strict. # But otherwise, height must be specified to know which rules to apply @@ -141,46 +54,14 @@ def get_name_puzzle_conditions( try: err, result = GENERATOR_MOD.run_as_generator(max_cost, flags, block_program, block_program_args) - + assert (err is None) != (result is None) if err is not None: - assert err != 0 - return NPCResult(uint16(err), [], uint64(0)) - - first = True - npc_list = [] - for r in result.spends: - conditions: Dict[ConditionOpcode, List[ConditionWithArgs]] = {} - if r.height_relative is not None: - add_int_cond(conditions, ConditionOpcode.ASSERT_HEIGHT_RELATIVE, r.height_relative) - if r.seconds_relative > 0: - add_int_cond(conditions, ConditionOpcode.ASSERT_SECONDS_RELATIVE, r.seconds_relative) - for cc in r.create_coin: - if cc[2] == b"": - add_cond(conditions, ConditionOpcode.CREATE_COIN, [cc[0], int_to_bytes(cc[1])]) - else: - add_cond(conditions, ConditionOpcode.CREATE_COIN, [cc[0], int_to_bytes(cc[1]), cc[2]]) - for sig in r.agg_sig_me: - add_cond(conditions, ConditionOpcode.AGG_SIG_ME, [sig[0], sig[1]]) - - # all conditions that aren't tied to a specific spent coin, we roll into the first one - if first: - first = False - if result.reserve_fee > 0: - add_int_cond(conditions, ConditionOpcode.RESERVE_FEE, result.reserve_fee) - if result.height_absolute > 0: - add_int_cond(conditions, ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, result.height_absolute) - if result.seconds_absolute > 0: - add_int_cond(conditions, ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, result.seconds_absolute) - for sig in result.agg_sig_unsafe: - add_cond(conditions, ConditionOpcode.AGG_SIG_UNSAFE, [sig[0], sig[1]]) - - npc_list.append(NPC(r.coin_id, r.puzzle_hash, [(op, cond) for op, cond in conditions.items()])) - - return NPCResult(None, npc_list, uint64(result.cost + size_cost)) - + return NPCResult(uint16(err), None, uint64(0)) + else: + return NPCResult(None, result, uint64(result.cost + size_cost)) except BaseException as e: log.debug(f"get_name_puzzle_condition failed: {e}") - return NPCResult(uint16(Err.GENERATOR_RUNTIME_ERROR.value), [], uint64(0)) + return NPCResult(uint16(Err.GENERATOR_RUNTIME_ERROR.value), None, uint64(0)) def get_puzzle_and_solution_for_coin(generator: BlockGenerator, coin_name: bytes, max_cost: int): @@ -198,40 +79,26 @@ def get_puzzle_and_solution_for_coin(generator: BlockGenerator, coin_name: bytes return e, None, None -def mempool_check_conditions_dict( - unspent: CoinRecord, - conditions_dict: Dict[ConditionOpcode, List[ConditionWithArgs]], +def mempool_check_time_locks( + removal_coin_records: Dict[bytes32, CoinRecord], + bundle_conds: SpendBundleConditions, prev_transaction_block_height: uint32, timestamp: uint64, ) -> Optional[Err]: """ - Check all conditions against current state. + Check all time and height conditions against current state. """ - for con_list in conditions_dict.values(): - cvp: ConditionWithArgs - for cvp in con_list: - error: Optional[Err] = None - if cvp.opcode is ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE: - error = mempool_assert_absolute_block_height_exceeds(cvp, prev_transaction_block_height) - elif cvp.opcode is ConditionOpcode.ASSERT_HEIGHT_RELATIVE: - error = mempool_assert_relative_block_height_exceeds(cvp, unspent, prev_transaction_block_height) - elif cvp.opcode is ConditionOpcode.ASSERT_SECONDS_ABSOLUTE: - error = mempool_assert_absolute_time_exceeds(cvp, timestamp) - elif cvp.opcode is ConditionOpcode.ASSERT_SECONDS_RELATIVE: - error = mempool_assert_relative_time_exceeds(cvp, unspent, timestamp) - elif cvp.opcode is ConditionOpcode.ASSERT_MY_COIN_ID: - assert False - elif cvp.opcode is ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT: - assert False - elif cvp.opcode is ConditionOpcode.ASSERT_PUZZLE_ANNOUNCEMENT: - assert False - elif cvp.opcode is ConditionOpcode.ASSERT_MY_PARENT_ID: - assert False - elif cvp.opcode is ConditionOpcode.ASSERT_MY_PUZZLEHASH: - assert False - elif cvp.opcode is ConditionOpcode.ASSERT_MY_AMOUNT: - assert False - if error: - return error + if prev_transaction_block_height < bundle_conds.height_absolute: + return Err.ASSERT_HEIGHT_ABSOLUTE_FAILED + if timestamp < bundle_conds.seconds_absolute: + return Err.ASSERT_SECONDS_ABSOLUTE_FAILED + + for spend in bundle_conds.spends: + unspent = removal_coin_records[spend.coin_id] + if spend.height_relative is not None: + if prev_transaction_block_height < unspent.confirmed_block_index + spend.height_relative: + return Err.ASSERT_HEIGHT_RELATIVE_FAILED + if timestamp < unspent.timestamp + spend.seconds_relative: + return Err.ASSERT_SECONDS_RELATIVE_FAILED return None diff --git a/chia/full_node/mempool_manager.py b/chia/full_node/mempool_manager.py index 6c5f7aa49e50..f1a4ba514bc1 100644 --- a/chia/full_node/mempool_manager.py +++ b/chia/full_node/mempool_manager.py @@ -10,7 +10,6 @@ from typing import Dict, List, Optional, Set, Tuple from blspy import GTElement from chiabip158 import PyBIP158 -from clvm.casts import int_from_bytes from chia.util import cached_bls from chia.consensus.block_record import BlockRecord @@ -19,14 +18,12 @@ from chia.full_node.bundle_tools import simple_solution_generator from chia.full_node.coin_store import CoinStore from chia.full_node.mempool import Mempool -from chia.full_node.mempool_check_conditions import mempool_check_conditions_dict, get_name_puzzle_conditions +from chia.full_node.mempool_check_conditions import get_name_puzzle_conditions from chia.full_node.pending_tx_cache import PendingTxCache from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.program import SerializedProgram from chia.types.blockchain_format.sized_bytes import bytes32, bytes48 from chia.types.coin_record import CoinRecord -from chia.types.condition_opcodes import ConditionOpcode -from chia.types.condition_with_args import ConditionWithArgs from chia.types.mempool_inclusion_status import MempoolInclusionStatus from chia.types.mempool_item import MempoolItem from chia.types.spend_bundle import SpendBundle @@ -38,6 +35,7 @@ from chia.util.lru_cache import LRUCache from chia.util.setproctitle import getproctitle, setproctitle from chia.util.streamable import recurse_jsonify +from chia.full_node.mempool_check_conditions import mempool_check_time_locks log = logging.getLogger(__name__) @@ -61,9 +59,10 @@ def validate_clvm_and_signature( if result.error is not None: return Err(result.error), b"", {} - pks: List[bytes48] - msgs: List[bytes] - pks, msgs = pkm_pairs(result.npc_list, additional_data) + pks: List[bytes48] = [] + msgs: List[bytes] = [] + assert result.conds is not None + pks, msgs = pkm_pairs(result.conds, additional_data) # Verify aggregated signature cache: LRUCache = LRUCache(10000) @@ -301,8 +300,10 @@ async def add_spendbundle( if self.peak is None: return None, MempoolInclusionStatus.FAILED, Err.MEMPOOL_NOT_INITIALIZED - npc_list = npc_result.npc_list assert npc_result.error is None + if npc_result.error is not None: + return None, MempoolInclusionStatus.FAILED, Err(npc_result.error) + if program is None: program = simple_solution_generator(new_spend).program cost = npc_result.cost @@ -314,12 +315,13 @@ async def add_spendbundle( # execute the CLVM program. return None, MempoolInclusionStatus.FAILED, Err.BLOCK_COST_EXCEEDS_MAX + assert npc_result.conds is not None # build removal list - removal_names: List[bytes32] = [npc.coin_name for npc in npc_list] + removal_names: List[bytes32] = [spend.coin_id for spend in npc_result.conds.spends] if set(removal_names) != set([s.name() for s in new_spend.removals()]): return None, MempoolInclusionStatus.FAILED, Err.INVALID_SPEND_BUNDLE - additions = additions_for_npc(npc_list) + additions = additions_for_npc(npc_result) additions_dict: Dict[bytes32, Coin] = {} for add in additions: @@ -372,7 +374,7 @@ async def add_spendbundle( assert self.peak.timestamp is not None removal_record = CoinRecord( removal_coin, - uint32(self.peak.height + 1), # In mempool, so will be included in next height + uint32(self.peak.height + 1), uint32(0), False, self.peak.timestamp, @@ -388,16 +390,8 @@ async def add_spendbundle( return None, MempoolInclusionStatus.FAILED, Err.MINTING_COIN fees = uint64(removal_amount - addition_amount) - assert_fee_sum: uint64 = uint64(0) - - for npc in npc_list: - if ConditionOpcode.RESERVE_FEE in npc.condition_dict: - fee_list: List[ConditionWithArgs] = npc.condition_dict[ConditionOpcode.RESERVE_FEE] - for cvp in fee_list: - fee = int_from_bytes(cvp.vars[0]) - if fee < 0: - return None, MempoolInclusionStatus.FAILED, Err.RESERVE_FEE_CONDITION_FAILED - assert_fee_sum = assert_fee_sum + fee + assert_fee_sum: uint64 = uint64(npc_result.conds.reserve_fee) + if fees < assert_fee_sum: return ( None, @@ -439,37 +433,35 @@ async def add_spendbundle( return None, MempoolInclusionStatus.FAILED, fail_reason # Verify conditions, create hash_key list for aggsig check - error: Optional[Err] = None - for npc in npc_list: - coin_record: CoinRecord = removal_record_dict[npc.coin_name] + for spend in npc_result.conds.spends: + coin_record: CoinRecord = removal_record_dict[spend.coin_id] # Check that the revealed removal puzzles actually match the puzzle hash - if npc.puzzle_hash != coin_record.coin.puzzle_hash: + if spend.puzzle_hash != coin_record.coin.puzzle_hash: log.warning("Mempool rejecting transaction because of wrong puzzle_hash") - log.warning(f"{npc.puzzle_hash} != {coin_record.coin.puzzle_hash}") + log.warning(f"{spend.puzzle_hash} != {coin_record.coin.puzzle_hash}") return None, MempoolInclusionStatus.FAILED, Err.WRONG_PUZZLE_HASH - chialisp_height = ( - self.peak.prev_transaction_block_height if not self.peak.is_transaction_block else self.peak.height - ) - assert self.peak.timestamp is not None - error = mempool_check_conditions_dict( - coin_record, - npc.condition_dict, - uint32(chialisp_height), - self.peak.timestamp, - ) + chialisp_height = ( + self.peak.prev_transaction_block_height if not self.peak.is_transaction_block else self.peak.height + ) - if error: - if error is Err.ASSERT_HEIGHT_ABSOLUTE_FAILED or error is Err.ASSERT_HEIGHT_RELATIVE_FAILED: - potential = MempoolItem( - new_spend, uint64(fees), npc_result, cost, spend_name, additions, removals, program - ) - self.potential_cache.add(potential) - return uint64(cost), MempoolInclusionStatus.PENDING, error - break + assert self.peak.timestamp is not None + error: Optional[Err] = mempool_check_time_locks( + removal_record_dict, + npc_result.conds, + uint32(chialisp_height), + self.peak.timestamp, + ) if error: - return None, MempoolInclusionStatus.FAILED, error + if error is Err.ASSERT_HEIGHT_ABSOLUTE_FAILED or error is Err.ASSERT_HEIGHT_RELATIVE_FAILED: + potential = MempoolItem( + new_spend, uint64(fees), npc_result, cost, spend_name, additions, removals, program + ) + self.potential_cache.add(potential) + return uint64(cost), MempoolInclusionStatus.PENDING, error + else: + return None, MempoolInclusionStatus.FAILED, error # Remove all conflicting Coins and SpendBundles if fail_reason: diff --git a/chia/types/blockchain_format/program.py b/chia/types/blockchain_format/program.py index 3b29e6cd7833..36278ef7ec4f 100644 --- a/chia/types/blockchain_format/program.py +++ b/chia/types/blockchain_format/program.py @@ -1,5 +1,5 @@ import io -from typing import List, Set, Tuple, Optional, Any +from typing import List, Set, Tuple, Optional from clvm import SExp from clvm.casts import int_from_bytes @@ -10,8 +10,8 @@ from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.hash import std_hash -from chia.util.ints import uint16 from chia.util.byte_types import hexstr_to_bytes +from chia.types.spend_bundle_conditions import SpendBundleConditions, Spend from .tree_hash import sha256_treehash @@ -222,9 +222,12 @@ def run_mempool_with_cost(self, max_cost: int, *args) -> Tuple[int, Program]: def run_with_cost(self, max_cost: int, *args) -> Tuple[int, Program]: return self._run(max_cost, 0, *args) - # returns an optional error code and an optional PySpendBundleConditions (from clvm_rs) + # returns an optional error code and an optional SpendBundleConditions # exactly one of those will hold a value - def run_as_generator(self, max_cost: int, flags: int, *args) -> Tuple[Optional[uint16], Optional[Any]]: + def run_as_generator( + self, max_cost: int, flags: int, *args + ) -> Tuple[Optional[int], Optional[SpendBundleConditions]]: + serialized_args = b"" if len(args) > 1: # when we have more than one argument, serialize them into a list @@ -235,12 +238,31 @@ def run_as_generator(self, max_cost: int, flags: int, *args) -> Tuple[Optional[u else: serialized_args += _serialize(args[0]) - return run_generator2( + err, conds = run_generator2( self._buf, serialized_args, max_cost, flags, ) + if err is not None: + assert err != 0 + return err, None + + # for now, we need to copy this data into python objects, in order to + # support streamable. This will become simpler and faster once we can + # implement streamable in rust + spends = [] + for s in conds.spends: + spends.append( + Spend(s.coin_id, s.puzzle_hash, s.height_relative, s.seconds_relative, s.create_coin, s.agg_sig_me) + ) + + ret = SpendBundleConditions( + spends, conds.reserve_fee, conds.height_absolute, conds.seconds_absolute, conds.agg_sig_unsafe, conds.cost + ) + + assert ret is not None + return None, ret def _run(self, max_cost: int, flags, *args) -> Tuple[int, Program]: # when multiple arguments are passed, concatenate them into a serialized diff --git a/chia/types/name_puzzle_condition.py b/chia/types/name_puzzle_condition.py deleted file mode 100644 index e3ff30f903ac..000000000000 --- a/chia/types/name_puzzle_condition.py +++ /dev/null @@ -1,23 +0,0 @@ -from dataclasses import dataclass -from typing import Dict, List, Tuple - -from chia.types.blockchain_format.sized_bytes import bytes32 -from chia.types.condition_with_args import ConditionWithArgs -from chia.types.condition_opcodes import ConditionOpcode -from chia.util.streamable import Streamable, streamable - - -@dataclass(frozen=True) -@streamable -class NPC(Streamable): - coin_name: bytes32 - puzzle_hash: bytes32 - conditions: List[Tuple[ConditionOpcode, List[ConditionWithArgs]]] - - @property - def condition_dict(self): - d: Dict[ConditionOpcode, List[ConditionWithArgs]] = {} - for opcode, l in self.conditions: - assert opcode not in d - d[opcode] = l - return d diff --git a/chia/types/spend_bundle_conditions.py b/chia/types/spend_bundle_conditions.py new file mode 100644 index 000000000000..0c59fb732eaf --- /dev/null +++ b/chia/types/spend_bundle_conditions.py @@ -0,0 +1,30 @@ +from dataclasses import dataclass +from typing import List, Optional, Tuple + +from chia.types.blockchain_format.sized_bytes import bytes32, bytes48 +from chia.util.ints import uint32, uint64 +from chia.util.streamable import Streamable, streamable + + +# the Spend and SpendBundleConditions classes are mirrors of native types, returned by +# run_generator2 +@dataclass(frozen=True) +@streamable +class Spend(Streamable): + coin_id: bytes32 + puzzle_hash: bytes32 + height_relative: Optional[uint32] + seconds_relative: uint64 + create_coin: List[Tuple[bytes32, uint64, bytes]] + agg_sig_me: List[Tuple[bytes48, bytes]] + + +@dataclass(frozen=True) +@streamable +class SpendBundleConditions(Streamable): + spends: List[Spend] + reserve_fee: uint64 + height_absolute: uint32 + seconds_absolute: uint64 + agg_sig_unsafe: List[Tuple[bytes48, bytes]] + cost: uint64 diff --git a/chia/util/condition_tools.py b/chia/util/condition_tools.py index 74a1e5fda7d9..8d756ccd9f8a 100644 --- a/chia/util/condition_tools.py +++ b/chia/util/condition_tools.py @@ -1,9 +1,7 @@ -from typing import Dict, List, Optional, Tuple, Set +from typing import Dict, List, Optional, Tuple from clvm.casts import int_from_bytes -from chia.types.announcement import Announcement -from chia.types.name_puzzle_condition import NPC from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.program import Program, SerializedProgram from chia.types.blockchain_format.sized_bytes import bytes32, bytes48 @@ -11,6 +9,7 @@ from chia.types.condition_with_args import ConditionWithArgs from chia.util.errors import ConsensusError, Err from chia.util.ints import uint64 +from chia.types.spend_bundle_conditions import SpendBundleConditions # TODO: review each `assert` and consider replacing with explicit checks # since asserts can be stripped with python `-OO` flag @@ -65,25 +64,17 @@ def conditions_by_opcode( return d -def pkm_pairs(npc_list: List[NPC], additional_data: bytes) -> Tuple[List[bytes48], List[bytes]]: +def pkm_pairs(conditions: SpendBundleConditions, additional_data: bytes) -> Tuple[List[bytes48], List[bytes]]: ret: Tuple[List[bytes48], List[bytes]] = ([], []) - for npc in npc_list: - for opcode, l in npc.conditions: - if opcode == ConditionOpcode.AGG_SIG_UNSAFE: - for cwa in l: - assert len(cwa.vars) == 2 - assert len(cwa.vars[0]) == 48 and len(cwa.vars[1]) <= 1024 - assert cwa.vars[0] is not None and cwa.vars[1] is not None - ret[0].append(bytes48(cwa.vars[0])) - ret[1].append(cwa.vars[1]) - elif opcode == ConditionOpcode.AGG_SIG_ME: - for cwa in l: - assert len(cwa.vars) == 2 - assert len(cwa.vars[0]) == 48 and len(cwa.vars[1]) <= 1024 - assert cwa.vars[0] is not None and cwa.vars[1] is not None - ret[0].append(bytes48(cwa.vars[0])) - ret[1].append(cwa.vars[1] + npc.coin_name + additional_data) + for pk, msg in conditions.agg_sig_unsafe: + ret[0].append(bytes48(pk)) + ret[1].append(msg) + + for spend in conditions.spends: + for pk, msg in spend.agg_sig_me: + ret[0].append(bytes48(pk)) + ret[1].append(msg + spend.coin_id + additional_data) return ret @@ -120,48 +111,6 @@ def created_outputs_for_conditions_dict( return output_coins -def coin_announcements_for_conditions_dict( - conditions_dict: Dict[ConditionOpcode, List[ConditionWithArgs]], - input_coin: Coin, -) -> Set[Announcement]: - output_announcements: Set[Announcement] = set() - for cvp in conditions_dict.get(ConditionOpcode.CREATE_COIN_ANNOUNCEMENT, []): - message = cvp.vars[0] - assert len(message) <= 1024 - announcement = Announcement(input_coin.name(), message) - output_announcements.add(announcement) - return output_announcements - - -def puzzle_announcements_for_conditions_dict( - conditions_dict: Dict[ConditionOpcode, List[ConditionWithArgs]], - input_coin: Coin, -) -> Set[Announcement]: - output_announcements: Set[Announcement] = set() - for cvp in conditions_dict.get(ConditionOpcode.CREATE_PUZZLE_ANNOUNCEMENT, []): - message = cvp.vars[0] - assert len(message) <= 1024 - announcement = Announcement(input_coin.puzzle_hash, message) - output_announcements.add(announcement) - return output_announcements - - -def coin_announcement_names_for_conditions_dict( - conditions_dict: Dict[ConditionOpcode, List[ConditionWithArgs]], - input_coin: Coin, -) -> List[bytes32]: - output = [an.name() for an in coin_announcements_for_conditions_dict(conditions_dict, input_coin)] - return output - - -def puzzle_announcement_names_for_conditions_dict( - conditions_dict: Dict[ConditionOpcode, List[ConditionWithArgs]], - input_coin: Coin, -) -> List[bytes32]: - output = [an.name() for an in puzzle_announcements_for_conditions_dict(conditions_dict, input_coin)] - return output - - def conditions_dict_for_solution( puzzle_reveal: SerializedProgram, solution: SerializedProgram, diff --git a/chia/util/generator_tools.py b/chia/util/generator_tools.py index 3034c9ec6407..526051022cc5 100644 --- a/chia/util/generator_tools.py +++ b/chia/util/generator_tools.py @@ -1,12 +1,13 @@ -from typing import Any, Iterator, List, Tuple +from typing import Any, Iterator, List, Tuple, Optional from chiabip158 import PyBIP158 from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.full_block import FullBlock from chia.types.header_block import HeaderBlock -from chia.types.name_puzzle_condition import NPC -from chia.util.condition_tools import created_outputs_for_conditions_dict +from chia.types.spend_bundle_conditions import SpendBundleConditions +from chia.consensus.cost_calculator import NPCResult +from chia.util.ints import uint64 def get_block_header(block: FullBlock, tx_addition_coins: List[Coin], removals_names: List[bytes32]) -> HeaderBlock: @@ -37,17 +38,20 @@ def get_block_header(block: FullBlock, tx_addition_coins: List[Coin], removals_n ) -def additions_for_npc(npc_list: List[NPC]) -> List[Coin]: +def additions_for_npc(npc_result: NPCResult) -> List[Coin]: additions: List[Coin] = [] - for npc in npc_list: - for coin in created_outputs_for_conditions_dict(npc.condition_dict, npc.coin_name): + if npc_result.conds is None: + return [] + for spend in npc_result.conds.spends: + for puzzle_hash, amount, _ in spend.create_coin: + coin = Coin(spend.coin_id, puzzle_hash, uint64(amount)) additions.append(coin) return additions -def tx_removals_and_additions(npc_list: List[NPC]) -> Tuple[List[bytes32], List[Coin]]: +def tx_removals_and_additions(results: Optional[SpendBundleConditions]) -> Tuple[List[bytes32], List[Coin]]: """ Doesn't return farmer and pool reward. """ @@ -56,12 +60,12 @@ def tx_removals_and_additions(npc_list: List[NPC]) -> Tuple[List[bytes32], List[ additions: List[Coin] = [] # build removals list - if npc_list is None: + if results is None: return [], [] - for npc in npc_list: - removals.append(npc.coin_name) - - additions.extend(additions_for_npc(npc_list)) + for spend in results.spends: + removals.append(spend.coin_id) + for puzzle_hash, amount, _ in spend.create_coin: + additions.append(Coin(spend.coin_id, puzzle_hash, uint64(amount))) return removals, additions diff --git a/tests/clvm/coin_store.py b/tests/clvm/coin_store.py index afe8ccc9a9de..7e609812285d 100644 --- a/tests/clvm/coin_store.py +++ b/tests/clvm/coin_store.py @@ -2,8 +2,7 @@ from dataclasses import dataclass, replace from typing import Dict, Iterator, Optional -from chia.util.condition_tools import created_outputs_for_conditions_dict -from chia.full_node.mempool_check_conditions import mempool_check_conditions_dict, get_name_puzzle_conditions +from chia.full_node.mempool_check_conditions import mempool_check_time_locks, get_name_puzzle_conditions from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.coin_record import CoinRecord @@ -71,8 +70,10 @@ def validate_spend_bundle( raise BadSpendBundleError(f"condition validation failure {Err(result.error)}") ephemeral_db = dict(self._db) - for npc in result.npc_list: - for coin in created_outputs_for_conditions_dict(npc.condition_dict, npc.coin_name): + assert result.conds is not None + for spend in result.conds.spends: + for puzzle_hash, amount, hint in spend.create_coin: + coin = Coin(spend.coin_id, puzzle_hash, amount) name = coin.name() ephemeral_db[name] = CoinRecord( coin, @@ -82,20 +83,15 @@ def validate_spend_bundle( uint64(now.seconds), ) - for npc in result.npc_list: - prev_transaction_block_height = uint32(now.height) - timestamp = uint64(now.seconds) - coin_record = ephemeral_db.get(npc.coin_name) - if coin_record is None: - raise BadSpendBundleError(f"coin not found for id 0x{npc.coin_name.hex()}") # noqa - err = mempool_check_conditions_dict( - coin_record, - npc.condition_dict, - prev_transaction_block_height, - timestamp, - ) - if err is not None: - raise BadSpendBundleError(f"condition validation failure {Err(err)}") + err = mempool_check_time_locks( + ephemeral_db, + result.conds, + uint32(now.height), + uint64(now.seconds), + ) + + if err is not None: + raise BadSpendBundleError(f"condition validation failure {Err(err)}") return 0 diff --git a/tests/core/full_node/stores/test_coin_store.py b/tests/core/full_node/stores/test_coin_store.py index ca3564ebbc85..169ce66210f4 100644 --- a/tests/core/full_node/stores/test_coin_store.py +++ b/tests/core/full_node/stores/test_coin_store.py @@ -103,7 +103,7 @@ async def test_basic_coin_store(self, cache_size: uint32, db_version, softfork_h mempool_mode=False, height=softfork_height, ) - tx_removals, tx_additions = tx_removals_and_additions(npc_result.npc_list) + tx_removals, tx_additions = tx_removals_and_additions(npc_result.conds) else: tx_removals, tx_additions = [], [] diff --git a/tests/core/full_node/test_mempool.py b/tests/core/full_node/test_mempool.py index ad5ea9cce796..0fc7f504bddc 100644 --- a/tests/core/full_node/test_mempool.py +++ b/tests/core/full_node/test_mempool.py @@ -18,7 +18,7 @@ from chia.simulator.simulator_protocol import FarmNewBlockProtocol from chia.types.announcement import Announcement from chia.types.blockchain_format.coin import Coin -from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.blockchain_format.sized_bytes import bytes32, bytes48 from chia.types.coin_spend import CoinSpend from chia.types.condition_opcodes import ConditionOpcode from chia.types.condition_with_args import ConditionWithArgs @@ -31,7 +31,6 @@ from chia.types.mempool_inclusion_status import MempoolInclusionStatus from chia.util.api_decorators import api_request, peer_required, bytes_required from chia.full_node.mempool_check_conditions import get_name_puzzle_conditions -from chia.types.name_puzzle_condition import NPC from chia.full_node.pending_tx_cache import PendingTxCache from blspy import G2Element @@ -47,8 +46,8 @@ from chia.types.blockchain_format.program import SerializedProgram from clvm_tools import binutils from chia.types.generator_types import BlockGenerator -from clvm.casts import int_from_bytes from blspy import G1Element +from chia.types.spend_bundle_conditions import SpendBundleConditions, Spend from tests.wallet_tools import WalletTool @@ -115,7 +114,7 @@ def make_item(idx: int, cost: uint64 = uint64(80)) -> MempoolItem: return MempoolItem( SpendBundle([], G2Element()), uint64(0), - NPCResult(None, [], cost), + NPCResult(None, None, cost), cost, spend_bundle_name, [], @@ -1829,13 +1828,8 @@ def test_invalid_condition_args_terminator(self, softfork_height): # at are ignored, including the termination of the list npc_result = generator_condition_tester("(80 50 . 1)", height=softfork_height) assert npc_result.error is None - assert len(npc_result.npc_list) == 1 - opcode = ConditionOpcode(bytes([80])) - assert len(npc_result.npc_list[0].conditions) == 1 - assert npc_result.npc_list[0].conditions[0][0] == opcode - assert len(npc_result.npc_list[0].conditions[0][1]) == 1 - c = npc_result.npc_list[0].conditions[0][1][0] - assert c == ConditionWithArgs(opcode=ConditionOpcode.ASSERT_SECONDS_RELATIVE, vars=[bytes([50])]) + assert len(npc_result.conds.spends) == 1 + assert npc_result.conds.spends[0].seconds_relative == 50 @pytest.mark.parametrize( "mempool,height,operand,expected", @@ -1881,13 +1875,17 @@ def test_duplicate_height_time_conditions(self, opcode, softfork_height): ) print(npc_result) assert npc_result.error is None - assert len(npc_result.npc_list) == 1 - max_arg = 0 - assert npc_result.npc_list[0].conditions[0][0] == opcode - for c in npc_result.npc_list[0].conditions[0][1]: - assert c.opcode == opcode - max_arg = max(max_arg, int_from_bytes(c.vars[0])) - assert max_arg == 100 + assert len(npc_result.conds.spends) == 1 + + assert len(npc_result.conds.spends) == 1 + if opcode == ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE: + assert npc_result.conds.height_absolute == 100 + elif opcode == ConditionOpcode.ASSERT_HEIGHT_RELATIVE: + assert npc_result.conds.spends[0].height_relative == 100 + elif opcode == ConditionOpcode.ASSERT_SECONDS_ABSOLUTE: + assert npc_result.conds.seconds_absolute == 100 + elif opcode == ConditionOpcode.ASSERT_SECONDS_RELATIVE: + assert npc_result.conds.spends[0].seconds_relative == 100 @pytest.mark.parametrize( "opcode", @@ -1902,10 +1900,9 @@ def test_just_announcement(self, opcode, softfork_height): # back. They are either satisified or cause an immediate failure npc_result = generator_condition_tester(f'({opcode.value[0]} "{message}") ' * 50, height=softfork_height) assert npc_result.error is None - assert len(npc_result.npc_list) == 1 + assert len(npc_result.conds.spends) == 1 # create-announcements and assert-announcements are dropped once # validated - assert npc_result.npc_list[0].conditions == [] @pytest.mark.parametrize( "opcode", @@ -1923,7 +1920,6 @@ def test_assert_announcement_fail(self, opcode, softfork_height): npc_result = generator_condition_tester(f'({opcode.value[0]} "{message}") ', height=softfork_height) print(npc_result) assert npc_result.error == Err.ASSERT_ANNOUNCE_CONSUMED_FAILED.value - assert npc_result.npc_list == [] def test_multiple_reserve_fee(self, softfork_height): # RESERVE_FEE @@ -1932,17 +1928,8 @@ def test_multiple_reserve_fee(self, softfork_height): # with all the fees accumulated npc_result = generator_condition_tester(f"({cond} 100) " * 3, height=softfork_height) assert npc_result.error is None - assert len(npc_result.npc_list) == 1 - opcode = ConditionOpcode(bytes([cond])) - reserve_fee = 0 - assert len(npc_result.npc_list[0].conditions) == 1 - assert npc_result.npc_list[0].conditions[0][0] == opcode - for c in npc_result.npc_list[0].conditions[0][1]: - assert c.opcode == opcode - reserve_fee += int_from_bytes(c.vars[0]) - - assert reserve_fee == 300 - assert len(npc_result.npc_list[0].conditions[0][1]) == 1 + assert npc_result.conds.reserve_fee == 300 + assert len(npc_result.conds.spends) == 1 def test_duplicate_outputs(self, softfork_height): # CREATE_COIN @@ -1952,7 +1939,6 @@ def test_duplicate_outputs(self, softfork_height): puzzle_hash = "abababababababababababababababab" npc_result = generator_condition_tester(f'(51 "{puzzle_hash}" 10) ' * 2, height=softfork_height) assert npc_result.error == Err.DUPLICATE_OUTPUT.value - assert npc_result.npc_list == [] def test_create_coin_cost(self, softfork_height): # CREATE_COIN @@ -1966,7 +1952,8 @@ def test_create_coin_cost(self, softfork_height): ) assert npc_result.error is None assert npc_result.cost == 20470 + 95 * COST_PER_BYTE + ConditionCost.CREATE_COIN.value - assert len(npc_result.npc_list) == 1 + assert len(npc_result.conds.spends) == 1 + assert len(npc_result.conds.spends[0].create_coin) == 1 # if we subtract one from max cost, this should fail npc_result = generator_condition_tester( @@ -1988,7 +1975,7 @@ def test_agg_sig_cost(self, softfork_height): ) assert npc_result.error is None assert npc_result.cost == 20512 + 117 * COST_PER_BYTE + ConditionCost.AGG_SIG.value - assert len(npc_result.npc_list) == 1 + assert len(npc_result.conds.spends) == 1 # if we subtract one from max cost, this should fail npc_result = generator_condition_tester( @@ -2014,15 +2001,9 @@ def test_create_coin_different_parent(self, softfork_height): generator, MAX_BLOCK_COST_CLVM, cost_per_byte=COST_PER_BYTE, mempool_mode=False, height=softfork_height ) assert npc_result.error is None - assert len(npc_result.npc_list) == 2 - opcode = ConditionOpcode.CREATE_COIN - for c in npc_result.npc_list: - assert c.conditions == [ - ( - opcode.value, - [ConditionWithArgs(opcode, [puzzle_hash.encode("ascii"), bytes([10])])], - ) - ] + assert len(npc_result.conds.spends) == 2 + for s in npc_result.conds.spends: + assert s.create_coin == [(puzzle_hash.encode("ascii"), 10, b"")] def test_create_coin_different_puzzhash(self, softfork_height): # CREATE_COIN @@ -2033,16 +2014,9 @@ def test_create_coin_different_puzzhash(self, softfork_height): f'(51 "{puzzle_hash_1}" 5) (51 "{puzzle_hash_2}" 5)', height=softfork_height ) assert npc_result.error is None - assert len(npc_result.npc_list) == 1 - opcode = ConditionOpcode.CREATE_COIN - assert ( - ConditionWithArgs(opcode, [puzzle_hash_1.encode("ascii"), bytes([5])]) - in npc_result.npc_list[0].conditions[0][1] - ) - assert ( - ConditionWithArgs(opcode, [puzzle_hash_2.encode("ascii"), bytes([5])]) - in npc_result.npc_list[0].conditions[0][1] - ) + assert len(npc_result.conds.spends) == 1 + assert (puzzle_hash_1.encode("ascii"), 5, b"") in npc_result.conds.spends[0].create_coin + assert (puzzle_hash_2.encode("ascii"), 5, b"") in npc_result.conds.spends[0].create_coin def test_create_coin_different_amounts(self, softfork_height): # CREATE_COIN @@ -2052,16 +2026,10 @@ def test_create_coin_different_amounts(self, softfork_height): f'(51 "{puzzle_hash}" 5) (51 "{puzzle_hash}" 4)', height=softfork_height ) assert npc_result.error is None - assert len(npc_result.npc_list) == 1 - opcode = ConditionOpcode.CREATE_COIN - assert ( - ConditionWithArgs(opcode, [puzzle_hash.encode("ascii"), bytes([5])]) - in npc_result.npc_list[0].conditions[0][1] - ) - assert ( - ConditionWithArgs(opcode, [puzzle_hash.encode("ascii"), bytes([4])]) - in npc_result.npc_list[0].conditions[0][1] - ) + assert len(npc_result.conds.spends) == 1 + coins = npc_result.conds.spends[0].create_coin + assert (puzzle_hash.encode("ascii"), 5, b"") in coins + assert (puzzle_hash.encode("ascii"), 4, b"") in coins def test_create_coin_with_hint(self, softfork_height): # CREATE_COIN @@ -2069,11 +2037,9 @@ def test_create_coin_with_hint(self, softfork_height): hint = "12341234123412341234213421341234" npc_result = generator_condition_tester(f'(51 "{puzzle_hash_1}" 5 ("{hint}"))', height=softfork_height) assert npc_result.error is None - assert len(npc_result.npc_list) == 1 - opcode = ConditionOpcode.CREATE_COIN - assert npc_result.npc_list[0].conditions[0][1][0] == ConditionWithArgs( - opcode, [puzzle_hash_1.encode("ascii"), bytes([5]), hint.encode("ascii")] - ) + assert len(npc_result.conds.spends) == 1 + coins = npc_result.conds.spends[0].create_coin + assert coins == [(puzzle_hash_1.encode("ascii"), 5, hint.encode("ascii"))] @pytest.mark.parametrize( "mempool,height", @@ -2089,10 +2055,8 @@ def test_unknown_condition(self, mempool: bool, height: uint32): print(npc_result) if mempool: assert npc_result.error == Err.INVALID_CONDITION.value - assert npc_result.npc_list == [] else: assert npc_result.error is None - assert npc_result.npc_list[0].conditions == [] # the tests below are malicious generator programs @@ -2231,13 +2195,16 @@ def test_duplicate_large_integer_ladder(self, opcode, softfork_height): assert npc_result.error == error_for_condition(opcode) else: assert npc_result.error is None - assert len(npc_result.npc_list) == 1 - assert npc_result.npc_list[0].conditions == [ - ( - opcode, - [ConditionWithArgs(opcode, [int_to_bytes(28)])], - ) - ] + assert len(npc_result.conds.spends) == 1 + if opcode == ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE: + assert npc_result.conds.height_absolute == 28 + elif opcode == ConditionOpcode.ASSERT_HEIGHT_RELATIVE: + assert npc_result.conds.spends[0].height_relative == 28 + elif opcode == ConditionOpcode.ASSERT_SECONDS_ABSOLUTE: + assert npc_result.conds.seconds_absolute == 28 + elif opcode == ConditionOpcode.ASSERT_SECONDS_RELATIVE: + assert npc_result.conds.spends[0].seconds_relative == 28 + print(f"run time:{run_time}") assert run_time < 0.7 @@ -2260,13 +2227,17 @@ def test_duplicate_large_integer(self, opcode, softfork_height): assert npc_result.error == error_for_condition(opcode) else: assert npc_result.error is None - assert len(npc_result.npc_list) == 1 - assert npc_result.npc_list[0].conditions == [ - ( - opcode, - [ConditionWithArgs(opcode, [bytes([100])])], - ) - ] + assert len(npc_result.conds.spends) == 1 + + if opcode == ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE: + assert npc_result.conds.height_absolute == 100 + elif opcode == ConditionOpcode.ASSERT_HEIGHT_RELATIVE: + assert npc_result.conds.spends[0].height_relative == 100 + elif opcode == ConditionOpcode.ASSERT_SECONDS_ABSOLUTE: + assert npc_result.conds.seconds_absolute == 100 + elif opcode == ConditionOpcode.ASSERT_SECONDS_RELATIVE: + assert npc_result.conds.spends[0].seconds_relative == 100 + print(f"run time:{run_time}") assert run_time < 1.1 @@ -2289,13 +2260,17 @@ def test_duplicate_large_integer_substr(self, opcode, softfork_height): assert npc_result.error == error_for_condition(opcode) else: assert npc_result.error is None - assert len(npc_result.npc_list) == 1 - assert npc_result.npc_list[0].conditions == [ - ( - opcode, - [ConditionWithArgs(opcode, [bytes([100])])], - ) - ] + assert len(npc_result.conds.spends) == 1 + + if opcode == ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE: + assert npc_result.conds.height_absolute == 100 + elif opcode == ConditionOpcode.ASSERT_HEIGHT_RELATIVE: + assert npc_result.conds.spends[0].height_relative == 100 + elif opcode == ConditionOpcode.ASSERT_SECONDS_ABSOLUTE: + assert npc_result.conds.seconds_absolute == 100 + elif opcode == ConditionOpcode.ASSERT_SECONDS_RELATIVE: + assert npc_result.conds.spends[0].seconds_relative == 100 + print(f"run time:{run_time}") assert run_time < 1.1 @@ -2320,10 +2295,17 @@ def test_duplicate_large_integer_substr_tail(self, opcode, softfork_height): assert npc_result.error == error_for_condition(opcode) else: assert npc_result.error is None - assert len(npc_result.npc_list) == 1 + assert len(npc_result.conds.spends) == 1 + + if opcode == ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE: + assert npc_result.conds.height_absolute == 0xFFFFFFFF + elif opcode == ConditionOpcode.ASSERT_HEIGHT_RELATIVE: + assert npc_result.conds.spends[0].height_relative == 0xFFFFFFFF + elif opcode == ConditionOpcode.ASSERT_SECONDS_ABSOLUTE: + assert npc_result.conds.seconds_absolute == 0xFFFFFFFF + elif opcode == ConditionOpcode.ASSERT_SECONDS_RELATIVE: + assert npc_result.conds.spends[0].seconds_relative == 0xFFFFFFFF - print(npc_result.npc_list[0].conditions[0][1]) - assert ConditionWithArgs(opcode, [int_to_bytes(0xFFFFFFFF)]) in npc_result.npc_list[0].conditions[0][1] print(f"run time:{run_time}") assert run_time < 0.3 @@ -2343,8 +2325,7 @@ def test_duplicate_large_integer_negative(self, opcode, softfork_height): npc_result = generator_condition_tester(condition, quote=False, height=softfork_height) run_time = time() - start_time assert npc_result.error is None - assert len(npc_result.npc_list) == 1 - assert npc_result.npc_list[0].conditions == [] + assert len(npc_result.conds.spends) == 1 print(f"run time:{run_time}") assert run_time < 1 @@ -2359,13 +2340,9 @@ def test_duplicate_reserve_fee(self, softfork_height): assert npc_result.error == error_for_condition(opcode) else: assert npc_result.error is None - assert len(npc_result.npc_list) == 1 - assert npc_result.npc_list[0].conditions == [ - ( - opcode.value, - [ConditionWithArgs(opcode, [int_to_bytes(100 * 280000)])], - ) - ] + assert len(npc_result.conds.spends) == 1 + assert npc_result.conds.reserve_fee == 100 * 280000 + print(f"run time:{run_time}") assert run_time < 1 @@ -2379,7 +2356,7 @@ def test_duplicate_reserve_fee_negative(self, softfork_height): # RESERVE_FEE conditions fail unconditionally if they have a negative # amount assert npc_result.error == Err.RESERVE_FEE_CONDITION_FAILED.value - assert len(npc_result.npc_list) == 0 + assert npc_result.conds is None print(f"run time:{run_time}") assert run_time < 0.8 @@ -2393,9 +2370,8 @@ def test_duplicate_coin_announces(self, opcode, softfork_height): npc_result = generator_condition_tester(condition, quote=False, height=softfork_height) run_time = time() - start_time assert npc_result.error is None - assert len(npc_result.npc_list) == 1 + assert len(npc_result.conds.spends) == 1 # coin announcements are not propagated to python, but validated in rust - assert len(npc_result.npc_list[0].conditions) == 0 # TODO: optimize clvm to make this run in < 1 second print(f"run time:{run_time}") assert run_time < 7 @@ -2411,7 +2387,7 @@ def test_create_coin_duplicates(self, softfork_height): npc_result = generator_condition_tester(condition, quote=False, height=softfork_height) run_time = time() - start_time assert npc_result.error == Err.DUPLICATE_OUTPUT.value - assert len(npc_result.npc_list) == 0 + assert npc_result.conds is None print(f"run time:{run_time}") assert run_time < 0.8 @@ -2426,10 +2402,9 @@ def test_many_create_coin(self, softfork_height): npc_result = generator_condition_tester(condition, quote=False, height=softfork_height) run_time = time() - start_time assert npc_result.error is None - assert len(npc_result.npc_list) == 1 - assert len(npc_result.npc_list[0].conditions) == 1 - assert npc_result.npc_list[0].conditions[0][0] == ConditionOpcode.CREATE_COIN.value - assert len(npc_result.npc_list[0].conditions[0][1]) == 6094 + assert len(npc_result.conds.spends) == 1 + spend = npc_result.conds.spends[0] + assert len(spend.create_coin) == 6094 print(f"run time:{run_time}") assert run_time < 0.2 @@ -2473,65 +2448,37 @@ class TestPkmPairs: ASU = ConditionOpcode.AGG_SIG_UNSAFE def test_empty_list(self): - npc_list = [] - pks, msgs = pkm_pairs(npc_list, b"foobar") + conds = SpendBundleConditions([], 0, 0, 0, [], 0) + pks, msgs = pkm_pairs(conds, b"foobar") assert pks == [] assert msgs == [] def test_no_agg_sigs(self): - npc_list = [ - NPC(self.h1, self.h2, [(self.CCA, [ConditionWithArgs(self.CCA, [b"msg"])])]), - NPC(self.h3, self.h4, [(self.CC, [ConditionWithArgs(self.CCA, [self.h1, bytes([1])])])]), - ] - pks, msgs = pkm_pairs(npc_list, b"foobar") + # one create coin: h1 amount: 1 and not hint + spends = [Spend(self.h3, self.h4, None, 0, [(self.h1, 1, b"")], [])] + conds = SpendBundleConditions(spends, 0, 0, 0, [], 0) + pks, msgs = pkm_pairs(conds, b"foobar") assert pks == [] assert msgs == [] def test_agg_sig_me(self): - npc_list = [ - NPC( - self.h1, - self.h2, - [ - ( - self.ASM, - [ - ConditionWithArgs(self.ASM, [bytes(self.pk1), b"msg1"]), - ConditionWithArgs(self.ASM, [bytes(self.pk2), b"msg2"]), - ], - ) - ], - ) - ] - pks, msgs = pkm_pairs(npc_list, b"foobar") + + spends = [Spend(self.h1, self.h2, None, 0, [], [(bytes48(self.pk1), b"msg1"), (bytes48(self.pk2), b"msg2")])] + conds = SpendBundleConditions(spends, 0, 0, 0, [], 0) + pks, msgs = pkm_pairs(conds, b"foobar") assert [bytes(pk) for pk in pks] == [bytes(self.pk1), bytes(self.pk2)] assert msgs == [b"msg1" + self.h1 + b"foobar", b"msg2" + self.h1 + b"foobar"] def test_agg_sig_unsafe(self): - npc_list = [ - NPC( - self.h1, - self.h2, - [ - ( - self.ASU, - [ - ConditionWithArgs(self.ASU, [bytes(self.pk1), b"msg1"]), - ConditionWithArgs(self.ASU, [bytes(self.pk2), b"msg2"]), - ], - ) - ], - ) - ] - pks, msgs = pkm_pairs(npc_list, b"foobar") + conds = SpendBundleConditions([], 0, 0, 0, [(bytes48(self.pk1), b"msg1"), (bytes48(self.pk2), b"msg2")], 0) + pks, msgs = pkm_pairs(conds, b"foobar") assert [bytes(pk) for pk in pks] == [bytes(self.pk1), bytes(self.pk2)] assert msgs == [b"msg1", b"msg2"] def test_agg_sig_mixed(self): - npc_list = [ - NPC(self.h1, self.h2, [(self.ASM, [ConditionWithArgs(self.ASM, [bytes(self.pk1), b"msg1"])])]), - NPC(self.h1, self.h2, [(self.ASU, [ConditionWithArgs(self.ASU, [bytes(self.pk2), b"msg2"])])]), - ] - pks, msgs = pkm_pairs(npc_list, b"foobar") - assert [bytes(pk) for pk in pks] == [bytes(self.pk1), bytes(self.pk2)] - assert msgs == [b"msg1" + self.h1 + b"foobar", b"msg2"] + + spends = [Spend(self.h1, self.h2, None, 0, [], [(bytes48(self.pk1), b"msg1")])] + conds = SpendBundleConditions(spends, 0, 0, 0, [(bytes48(self.pk2), b"msg2")], 0) + pks, msgs = pkm_pairs(conds, b"foobar") + assert [bytes(pk) for pk in pks] == [bytes(self.pk2), bytes(self.pk1)] + assert msgs == [b"msg2", b"msg1" + self.h1 + b"foobar"] diff --git a/tests/core/test_cost_calculation.py b/tests/core/test_cost_calculation.py index ac24ddf55c17..164a3f12a9e6 100644 --- a/tests/core/test_cost_calculation.py +++ b/tests/core/test_cost_calculation.py @@ -79,12 +79,15 @@ async def test_basics(self, softfork_height, bt): assert npc_result.error is None assert len(bytes(program.program)) == 433 - coin_name = npc_result.npc_list[0].coin_name + coin_name = npc_result.conds.spends[0].coin_id error, puzzle, solution = get_puzzle_and_solution_for_coin( program, coin_name, test_constants.MAX_BLOCK_COST_CLVM ) assert error is None + assert npc_result.conds.cost == ConditionCost.CREATE_COIN.value + ConditionCost.AGG_SIG.value + 404560 + + # Create condition + agg_sig_condition + length + cpu_cost assert ( npc_result.cost == 404560 @@ -154,7 +157,7 @@ async def test_mempool_mode(self, softfork_height, bt): ) assert npc_result.error is None - coin_name = npc_result.npc_list[0].coin_name + coin_name = npc_result.conds.spends[0].coin_id error, puzzle, solution = get_puzzle_and_solution_for_coin( generator, coin_name, test_constants.MAX_BLOCK_COST_CLVM ) @@ -205,7 +208,7 @@ async def test_tx_generator_speed(self, softfork_height): end_time = time.time() duration = end_time - start_time assert npc_result.error is None - assert len(npc_result.npc_list) == LARGE_BLOCK_COIN_CONSUMED_COUNT + assert len(npc_result.conds.spends) == LARGE_BLOCK_COIN_CONSUMED_COUNT log.info(f"Time spent: {duration}") assert duration < 0.5 diff --git a/tests/generator/test_rom.py b/tests/generator/test_rom.py index 36e7b74a2944..955ae2564ba2 100644 --- a/tests/generator/test_rom.py +++ b/tests/generator/test_rom.py @@ -1,4 +1,3 @@ -from clvm.casts import int_to_bytes from clvm_tools import binutils from clvm_tools.clvmc import compile_clvm_text @@ -6,13 +5,11 @@ from chia.full_node.mempool_check_conditions import get_name_puzzle_conditions from chia.types.blockchain_format.program import Program, SerializedProgram from chia.types.blockchain_format.sized_bytes import bytes32 -from chia.types.condition_opcodes import ConditionOpcode -from chia.types.condition_with_args import ConditionWithArgs -from chia.types.name_puzzle_condition import NPC from chia.types.generator_types import BlockGenerator from chia.util.ints import uint32 from chia.wallet.puzzles.load_clvm import load_clvm from chia.consensus.condition_costs import ConditionCost +from chia.types.spend_bundle_conditions import Spend MAX_COST = int(1e15) COST_PER_BYTE = int(12000) @@ -109,18 +106,17 @@ def test_get_name_puzzle_conditions(self, softfork_height): assert npc_result.cost == EXPECTED_COST + ConditionCost.CREATE_COIN.value + ( len(bytes(gen.program)) * COST_PER_BYTE ) - cond_1 = ConditionWithArgs(ConditionOpcode.CREATE_COIN, [bytes([0] * 31 + [1]), int_to_bytes(500)]) - CONDITIONS = [ - (ConditionOpcode.CREATE_COIN, [cond_1]), - ] - npc = NPC( - coin_name=bytes32.fromhex("e8538c2d14f2a7defae65c5c97f5d4fae7ee64acef7fec9d28ad847a0880fd03"), + spend = Spend( + coin_id=bytes32.fromhex("e8538c2d14f2a7defae65c5c97f5d4fae7ee64acef7fec9d28ad847a0880fd03"), puzzle_hash=bytes32.fromhex("9dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2"), - conditions=CONDITIONS, + height_relative=None, + seconds_relative=0, + create_coin=[(bytes([0] * 31 + [1]), 500, b"")], + agg_sig_me=[], ) - assert npc_result.npc_list == [npc] + assert npc_result.conds.spends == [spend] def test_coin_extras(self): # the ROM supports extra data after a coin. This test checks that it actually gets passed through diff --git a/tests/util/generator_tools_testing.py b/tests/util/generator_tools_testing.py index 3bb3c024e4c7..a4eefdf51805 100644 --- a/tests/util/generator_tools_testing.py +++ b/tests/util/generator_tools_testing.py @@ -5,7 +5,7 @@ from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.full_block import FullBlock from chia.types.generator_types import BlockGenerator -from chia.util.generator_tools import additions_for_npc +from chia.util.generator_tools import tx_removals_and_additions from chia.util.ints import uint32 @@ -27,10 +27,11 @@ def run_and_get_removals_and_additions( mempool_mode=mempool_mode, height=height, ) + assert npc_result.error is None + rem, add = tx_removals_and_additions(npc_result.conds) # build removals list - for npc in npc_result.npc_list: - removals.append(npc.coin_name) - additions.extend(additions_for_npc(npc_result.npc_list)) + removals.extend(rem) + additions.extend(add) rewards = block.get_included_reward_coins() additions.extend(rewards) diff --git a/tools/run_block.py b/tools/run_block.py index 90956da0ce90..c8fe416adf5c 100644 --- a/tools/run_block.py +++ b/tools/run_block.py @@ -49,10 +49,10 @@ from chia.full_node.generator import create_generator_args from chia.types.blockchain_format.program import SerializedProgram from chia.types.blockchain_format.coin import Coin +from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.condition_opcodes import ConditionOpcode from chia.types.condition_with_args import ConditionWithArgs from chia.types.generator_types import BlockGenerator -from chia.types.name_puzzle_condition import NPC from chia.util.config import load_config from chia.util.default_root import DEFAULT_ROOT_PATH from chia.util.ints import uint32, uint64 @@ -60,6 +60,13 @@ from clvm.casts import int_from_bytes +@dataclass +class NPC: + coin_name: bytes32 + puzzle_hash: bytes32 + conditions: List[Tuple[ConditionOpcode, List[ConditionWithArgs]]] + + @dataclass class CAT: asset_id: str From 35fb7d341c9dba897cc594ff76eae1351dd993ab Mon Sep 17 00:00:00 2001 From: Mariano Sorgente <3069354+mariano54@users.noreply.github.com> Date: Mon, 4 Apr 2022 16:49:33 -0400 Subject: [PATCH 317/378] Faster full node tests (#10986) * Start fast full node tests * Perf improvement on send_transaction * Major performance improvement for mempool test * Speed up another test * Speed up mempool tests startup * Lint * Debug tests * Try function scope for wallet_nodes * Update comment --- chia/full_node/full_node_api.py | 5 +- tests/conftest.py | 92 +++-- tests/core/full_node/test_full_node.py | 141 ++++--- tests/core/full_node/test_mempool.py | 501 +++++++++++++------------ 4 files changed, 391 insertions(+), 348 deletions(-) diff --git a/chia/full_node/full_node_api.py b/chia/full_node/full_node_api.py index 081178480626..ee60e44657d4 100644 --- a/chia/full_node/full_node_api.py +++ b/chia/full_node/full_node_api.py @@ -1250,8 +1250,9 @@ async def send_transaction(self, request: wallet_protocol.SendTransaction, *, te ) # Waits for the transaction to go into the mempool, times out after 45 seconds. status, error = None, None - for i in range(450): - await asyncio.sleep(0.1) + sleep_time = 0.01 + for i in range(int(45 / sleep_time)): + await asyncio.sleep(sleep_time) for potential_name, potential_status, potential_error in self.full_node.transaction_responses: if spend_name == potential_name: status = potential_status diff --git a/tests/conftest.py b/tests/conftest.py index f2d3933a5f8d..9dd58b313a0b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -206,9 +206,9 @@ async def five_nodes(db_version, self_hostname): yield _ -@pytest_asyncio.fixture(scope="module") +@pytest_asyncio.fixture(scope="function") async def wallet_nodes(bt): - async_gen = setup_simulators_and_wallets(2, 1, {"MEMPOOL_BLOCK_BUFFER": 2, "MAX_BLOCK_COST_CLVM": 400000000}) + async_gen = setup_simulators_and_wallets(2, 1, {"MEMPOOL_BLOCK_BUFFER": 1, "MAX_BLOCK_COST_CLVM": 400000000}) nodes, wallets = await async_gen.__anext__() full_node_1 = nodes[0] full_node_2 = nodes[1] @@ -343,6 +343,66 @@ async def wallet_and_node(): yield _ +@pytest_asyncio.fixture(scope="function") +async def one_node_one_block(bt, wallet_a): + async_gen = setup_simulators_and_wallets(1, 0, {}) + nodes, _ = await async_gen.__anext__() + full_node_1 = nodes[0] + server_1 = full_node_1.full_node.server + + reward_ph = wallet_a.get_new_puzzlehash() + blocks = bt.get_consecutive_blocks( + 1, + guarantee_transaction_block=True, + farmer_reward_puzzle_hash=reward_ph, + pool_reward_puzzle_hash=reward_ph, + genesis_timestamp=10000, + time_per_block=10, + ) + assert blocks[0].height == 0 + + 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, blocks[-1].height) + + yield full_node_1, server_1 + + async for _ in async_gen: + yield _ + + +@pytest_asyncio.fixture(scope="function") +async def two_nodes_one_block(bt, wallet_a): + async_gen = setup_simulators_and_wallets(2, 0, {}) + nodes, _ = await async_gen.__anext__() + full_node_1 = nodes[0] + full_node_2 = nodes[1] + server_1 = full_node_1.full_node.server + server_2 = full_node_2.full_node.server + + reward_ph = wallet_a.get_new_puzzlehash() + blocks = bt.get_consecutive_blocks( + 1, + guarantee_transaction_block=True, + farmer_reward_puzzle_hash=reward_ph, + pool_reward_puzzle_hash=reward_ph, + genesis_timestamp=10000, + time_per_block=10, + ) + assert blocks[0].height == 0 + + 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, blocks[-1].height) + + yield full_node_1, full_node_2, server_1, server_2 + + async for _ in async_gen: + yield _ + + # TODO: Ideally, the db_version should be the (parameterized) db_version # fixture, to test all versions of the database schema. This doesn't work # because of a hack in shutting down the full node, which means you cannot run @@ -451,34 +511,6 @@ async def timelord(bt): yield _ -@pytest_asyncio.fixture(scope="module") -async def two_nodes_mempool(bt, wallet_a): - async_gen = setup_simulators_and_wallets(2, 1, {}) - nodes, _ = await async_gen.__anext__() - full_node_1 = nodes[0] - full_node_2 = nodes[1] - server_1 = full_node_1.full_node.server - server_2 = full_node_2.full_node.server - - reward_ph = wallet_a.get_new_puzzlehash() - blocks = bt.get_consecutive_blocks( - 3, - 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, blocks[-1].height) - - yield full_node_1, full_node_2, server_1, server_2 - - async for _ in async_gen: - yield _ - - @pytest_asyncio.fixture(scope="function") async def setup_sim(): sim = await SpendSim.create() diff --git a/tests/core/full_node/test_full_node.py b/tests/core/full_node/test_full_node.py index 5c324c35402c..c71e8b0758aa 100644 --- a/tests/core/full_node/test_full_node.py +++ b/tests/core/full_node/test_full_node.py @@ -1,6 +1,5 @@ import asyncio import dataclasses -import logging import random import time from secrets import token_bytes @@ -52,8 +51,6 @@ from tests.setup_nodes import test_constants from tests.time_out_assert import time_out_assert, time_out_assert_custom_interval, time_out_messages -log = logging.getLogger(__name__) - async def new_transaction_not_requested(incoming, new_spend): await asyncio.sleep(3) @@ -97,7 +94,7 @@ async def get_block_path(full_node: FullNodeAPI): class TestFullNodeBlockCompression: @pytest.mark.asyncio - @pytest.mark.parametrize("tx_size", [10000, 3000000000000]) + @pytest.mark.parametrize("tx_size", [3000000000000]) async def test_block_compression(self, setup_two_nodes_and_wallet, empty_blockchain, tx_size, bt, self_hostname): nodes, wallets = setup_two_nodes_and_wallet server_1 = nodes[0].full_node.server @@ -153,7 +150,6 @@ async def check_transaction_confirmed(transaction) -> bool: return tx.confirmed await time_out_assert(30, check_transaction_confirmed, True, tr) - await asyncio.sleep(2) # Confirm generator is not compressed program: Optional[SerializedProgram] = (await full_node_1.get_all_full_blocks())[-1].transactions_generator @@ -182,7 +178,6 @@ async def check_transaction_confirmed(transaction) -> bool: await time_out_assert(30, wallet_is_synced, True, wallet_node_1, full_node_1) await time_out_assert(10, check_transaction_confirmed, True, tr) - await asyncio.sleep(2) # Confirm generator is compressed program: Optional[SerializedProgram] = (await full_node_1.get_all_full_blocks())[-1].transactions_generator @@ -254,7 +249,6 @@ async def check_transaction_confirmed(transaction) -> bool: await time_out_assert(30, wallet_is_synced, True, wallet_node_1, full_node_1) await time_out_assert(10, check_transaction_confirmed, True, tr) - await asyncio.sleep(2) # Confirm generator is compressed program: Optional[SerializedProgram] = (await full_node_1.get_all_full_blocks())[-1].transactions_generator @@ -300,7 +294,6 @@ async def check_transaction_confirmed(transaction) -> bool: await time_out_assert(30, wallet_is_synced, True, wallet_node_1, full_node_1) await time_out_assert(10, check_transaction_confirmed, True, new_tr) - await asyncio.sleep(2) # Confirm generator is not compressed, #CAT creation has a cat spend all_blocks = await full_node_1.get_all_full_blocks() @@ -356,7 +349,6 @@ async def check_transaction_confirmed(transaction) -> bool: blockchain = empty_blockchain all_blocks: List[FullBlock] = await full_node_1.get_all_full_blocks() assert height == len(all_blocks) - 1 - assert full_node_1.full_node.full_node_store.previous_generator is not None if test_reorgs: reog_blocks = bt.get_consecutive_blocks(14) @@ -364,7 +356,7 @@ async def check_transaction_confirmed(transaction) -> bool: for reorg_block in reog_blocks[:r]: await _validate_and_add_block_no_error(blockchain, reorg_block) for i in range(1, height): - for batch_size in range(1, height): + for batch_size in range(1, height, 3): results = await blockchain.pre_validate_blocks_multiprocessing( all_blocks[:i], {}, batch_size, validate_signatures=False ) @@ -376,7 +368,7 @@ async def check_transaction_confirmed(transaction) -> bool: for block in all_blocks[:r]: await _validate_and_add_block_no_error(blockchain, block) for i in range(1, height): - for batch_size in range(1, height): + for batch_size in range(1, height, 3): results = await blockchain.pre_validate_blocks_multiprocessing( all_blocks[:i], {}, batch_size, validate_signatures=False ) @@ -785,12 +777,9 @@ async def test_new_peak(self, wallet_nodes, bt, self_hostname): @pytest.mark.asyncio async def test_new_transaction_and_mempool(self, wallet_nodes, bt, self_hostname): full_node_1, full_node_2, server_1, server_2, wallet_a, wallet_receiver = wallet_nodes - blocks = await full_node_1.get_all_full_blocks() - wallet_ph = wallet_a.get_new_puzzlehash() blocks = bt.get_consecutive_blocks( - 10, - block_list_input=blocks, + 3, guarantee_transaction_block=True, farmer_reward_puzzle_hash=wallet_ph, pool_reward_puzzle_hash=wallet_ph, @@ -806,55 +795,48 @@ async def test_new_transaction_and_mempool(self, wallet_nodes, bt, self_hostname peer = await connect_and_get_peer(server_1, server_2, self_hostname) incoming_queue, node_id = await add_dummy_connection(server_1, self_hostname, 12312) fake_peer = server_1.all_connections[node_id] - # Mempool has capacity of 100, make 110 unspent coins that we can use puzzle_hashes = [] # Makes a bunch of coins - for i in range(5): - conditions_dict: Dict = {ConditionOpcode.CREATE_COIN: []} - # This should fit in one transaction - for _ in range(100): - receiver_puzzlehash = wallet_receiver.get_new_puzzlehash() - puzzle_hashes.append(receiver_puzzlehash) - output = ConditionWithArgs(ConditionOpcode.CREATE_COIN, [receiver_puzzlehash, int_to_bytes(100000000)]) - - conditions_dict[ConditionOpcode.CREATE_COIN].append(output) - - spend_bundle = wallet_a.generate_signed_transaction( - 100, - puzzle_hashes[0], - get_future_reward_coins(blocks[1 + i])[0], - condition_dic=conditions_dict, - ) - assert spend_bundle is not None - cost_result = await full_node_1.full_node.mempool_manager.pre_validate_spendbundle( - spend_bundle, None, spend_bundle.name() - ) - log.info(f"Cost result: {cost_result.cost}") + conditions_dict: Dict = {ConditionOpcode.CREATE_COIN: []} + # This should fit in one transaction + for _ in range(100): + receiver_puzzlehash = wallet_receiver.get_new_puzzlehash() + puzzle_hashes.append(receiver_puzzlehash) + output = ConditionWithArgs(ConditionOpcode.CREATE_COIN, [receiver_puzzlehash, int_to_bytes(10000000000)]) - new_transaction = fnp.NewTransaction(spend_bundle.get_hash(), uint64(100), uint64(100)) + conditions_dict[ConditionOpcode.CREATE_COIN].append(output) - await full_node_1.new_transaction(new_transaction, fake_peer) - await time_out_assert(10, new_transaction_requested, True, incoming_queue, new_transaction) + spend_bundle = wallet_a.generate_signed_transaction( + 100, + puzzle_hashes[0], + get_future_reward_coins(blocks[1])[0], + condition_dic=conditions_dict, + ) + assert spend_bundle is not None + new_transaction = fnp.NewTransaction(spend_bundle.get_hash(), uint64(100), uint64(100)) - respond_transaction_2 = fnp.RespondTransaction(spend_bundle) - await full_node_1.respond_transaction(respond_transaction_2, peer) + await full_node_1.new_transaction(new_transaction, fake_peer) + await time_out_assert(10, new_transaction_requested, True, incoming_queue, new_transaction) - blocks = bt.get_consecutive_blocks( - 1, - block_list_input=blocks, - guarantee_transaction_block=True, - transaction_data=spend_bundle, - ) - await full_node_1.full_node.respond_block(fnp.RespondBlock(blocks[-1]), peer) + respond_transaction_2 = fnp.RespondTransaction(spend_bundle) + await full_node_1.respond_transaction(respond_transaction_2, peer) - # Already seen - await full_node_1.new_transaction(new_transaction, fake_peer) - await time_out_assert(10, new_transaction_not_requested, True, incoming_queue, new_transaction) + blocks = bt.get_consecutive_blocks( + 1, + block_list_input=blocks, + guarantee_transaction_block=True, + transaction_data=spend_bundle, + ) + await full_node_1.full_node.respond_block(fnp.RespondBlock(blocks[-1]), None) - await time_out_assert(10, node_height_at_least, True, full_node_1, start_height + 5) + # Already seen + await full_node_1.new_transaction(new_transaction, fake_peer) + await time_out_assert(10, new_transaction_not_requested, True, incoming_queue, new_transaction) - spend_bundles = [] + print(f"FULL NODE HEIGHT: {start_height + 1} {full_node_1.full_node.blockchain.get_peak_height()}") + await time_out_assert(10, node_height_at_least, True, full_node_1, start_height + 1) + await time_out_assert(10, node_height_at_least, True, full_node_2, start_height + 1) included_tx = 0 not_included_tx = 0 @@ -862,18 +844,32 @@ async def test_new_transaction_and_mempool(self, wallet_nodes, bt, self_hostname successful_bundle: Optional[SpendBundle] = None # Fill mempool - for puzzle_hash in puzzle_hashes[1:]: - coin_record = (await full_node_1.full_node.coin_store.get_coin_records_by_puzzle_hash(True, puzzle_hash))[0] - receiver_puzzlehash = wallet_receiver.get_new_puzzlehash() - if puzzle_hash == puzzle_hashes[-1]: + receiver_puzzlehash = wallet_receiver.get_new_puzzlehash() + group_size = 3 # We will generate transaction bundles of this size (* standard transaction of around 3-4M cost) + for i in range(1, len(puzzle_hashes), group_size): + phs_to_use = [puzzle_hashes[i + j] for j in range(group_size) if (i + j) < len(puzzle_hashes)] + coin_records = [ + (await full_node_1.full_node.coin_store.get_coin_records_by_puzzle_hash(True, puzzle_hash))[0] + for puzzle_hash in phs_to_use + ] + + last_iteration = (i == len(puzzle_hashes) - group_size) or len(phs_to_use) < group_size + if last_iteration: force_high_fee = True - fee = 100000000 # 100 million (20 fee per cost) + fee = 100000000 * group_size # 100 million * group_size (20 fee per cost) else: force_high_fee = False - fee = random.randint(1, 100000000) - spend_bundle = wallet_receiver.generate_signed_transaction( - uint64(500), receiver_puzzlehash, coin_record.coin, fee=fee - ) + fee = random.randint(1, 100000000 * group_size) + spend_bundles = [ + wallet_receiver.generate_signed_transaction(uint64(500), receiver_puzzlehash, coin_record.coin, fee=0) + for coin_record in coin_records[1:] + ] + [ + wallet_receiver.generate_signed_transaction( + uint64(500), receiver_puzzlehash, coin_records[0].coin, fee=fee + ) + ] + spend_bundle = SpendBundle.aggregate(spend_bundles) + assert spend_bundle.fees() == fee respond_transaction = wallet_protocol.SendTransaction(spend_bundle) await full_node_1.send_transaction(respond_transaction) @@ -881,12 +877,8 @@ async def test_new_transaction_and_mempool(self, wallet_nodes, bt, self_hostname request = fnp.RequestTransaction(spend_bundle.get_hash()) req = await full_node_1.request_transaction(request) - fee_rate_for_small = full_node_1.full_node.mempool_manager.mempool.get_min_fee_rate(10) fee_rate_for_med = full_node_1.full_node.mempool_manager.mempool.get_min_fee_rate(5000000) fee_rate_for_large = full_node_1.full_node.mempool_manager.mempool.get_min_fee_rate(50000000) - log.info(f"Min fee rate (10): {fee_rate_for_small}") - log.info(f"Min fee rate (5000000): {fee_rate_for_med}") - log.info(f"Min fee rate (50000000): {fee_rate_for_large}") if fee_rate_for_large > fee_rate_for_med: seen_bigger_transaction_has_high_fee = True @@ -898,11 +890,11 @@ async def test_new_transaction_and_mempool(self, wallet_nodes, bt, self_hostname if force_high_fee: successful_bundle = spend_bundle else: - assert full_node_1.full_node.mempool_manager.mempool.at_full_capacity(10000000) - assert full_node_1.full_node.mempool_manager.mempool.get_min_fee_rate(10000000) > 0 + assert full_node_1.full_node.mempool_manager.mempool.at_full_capacity(5000000 * group_size) + assert full_node_1.full_node.mempool_manager.mempool.get_min_fee_rate(5000000 * group_size) > 0 assert not force_high_fee not_included_tx += 1 - log.info(f"Included: {included_tx}, not included: {not_included_tx}") + assert full_node_1.full_node.mempool_manager.mempool.at_full_capacity(10000000 * group_size) assert included_tx > 0 assert not_included_tx > 0 @@ -911,6 +903,8 @@ async def test_new_transaction_and_mempool(self, wallet_nodes, bt, self_hostname # Mempool is full new_transaction = fnp.NewTransaction(token_bytes(32), 10000000, uint64(1)) await full_node_1.new_transaction(new_transaction, fake_peer) + assert full_node_1.full_node.mempool_manager.mempool.at_full_capacity(10000000 * group_size) + assert full_node_2.full_node.mempool_manager.mempool.at_full_capacity(10000000 * group_size) await time_out_assert(10, new_transaction_not_requested, True, incoming_queue, new_transaction) @@ -940,11 +934,11 @@ async def test_new_transaction_and_mempool(self, wallet_nodes, bt, self_hostname # Reorg the blockchain blocks = await full_node_1.get_all_full_blocks() blocks = bt.get_consecutive_blocks( - 3, + 2, block_list_input=blocks[:-1], guarantee_transaction_block=True, ) - for block in blocks[-3:]: + for block in blocks[-2:]: await full_node_1.full_node.respond_block(fnp.RespondBlock(block), peer) # Can now resubmit a transaction after the reorg @@ -1228,9 +1222,10 @@ async def test_double_blocks_same_pospace(self, wallet_nodes, bt, self_hostname) block_2 = recursive_replace(block_2, "foliage.foliage_transaction_block_signature", new_fbh_sig) block_2 = recursive_replace(block_2, "transactions_generator", None) - await full_node_2.full_node.respond_block(fnp.RespondBlock(block_2), dummy_peer) + rb_task = asyncio.create_task(full_node_2.full_node.respond_block(fnp.RespondBlock(block_2), dummy_peer)) await time_out_assert(10, time_out_messages(incoming_queue, "request_block", 1)) + rb_task.cancel() @pytest.mark.asyncio async def test_request_unfinished_block(self, wallet_nodes, bt, self_hostname): diff --git a/tests/core/full_node/test_mempool.py b/tests/core/full_node/test_mempool.py index 0fc7f504bddc..a05f5b4e3392 100644 --- a/tests/core/full_node/test_mempool.py +++ b/tests/core/full_node/test_mempool.py @@ -6,7 +6,6 @@ from clvm.casts import int_to_bytes import pytest -import pytest_asyncio import chia.server.ws_connection as ws @@ -36,9 +35,8 @@ from chia.util.recursive_replace import recursive_replace from tests.blockchain.blockchain_test_utils import _validate_and_add_block -from tests.connection_utils import connect_and_get_peer +from tests.connection_utils import connect_and_get_peer, add_dummy_connection from tests.core.node_height import node_height_at_least -from tests.setup_nodes import setup_simulators_and_wallets from tests.time_out_assert import time_out_assert from chia.types.blockchain_format.program import Program, INFINITE_COST from chia.consensus.cost_calculator import NPCResult @@ -78,37 +76,6 @@ def generate_test_spend_bundle( return transaction -@pytest_asyncio.fixture(scope="function") -async def two_nodes_mempool(bt, wallet_a): - async_gen = setup_simulators_and_wallets(2, 1, {}) - nodes, _ = await async_gen.__anext__() - full_node_1 = nodes[0] - full_node_2 = nodes[1] - server_1 = full_node_1.full_node.server - server_2 = full_node_2.full_node.server - - reward_ph = wallet_a.get_new_puzzlehash() - blocks = bt.get_consecutive_blocks( - 3, - guarantee_transaction_block=True, - farmer_reward_puzzle_hash=reward_ph, - pool_reward_puzzle_hash=reward_ph, - genesis_timestamp=10000, - time_per_block=10, - ) - assert blocks[0].height == 0 - - 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, blocks[-1].height) - - yield full_node_1, full_node_2, server_1, server_2 - - async for _ in async_gen: - yield _ - - def make_item(idx: int, cost: uint64 = uint64(80)) -> MempoolItem: spend_bundle_name = bytes32([idx] * 32) return MempoolItem( @@ -184,9 +151,12 @@ def test_cost(self): class TestMempool: @pytest.mark.asyncio - async def test_basic_mempool(self, bt, two_nodes_mempool, wallet_a): + async def test_basic_mempool(self, bt, one_node_one_block, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, server_1 = one_node_one_block + + _ = await next_block(full_node_1, wallet_a, bt) + _ = await next_block(full_node_1, wallet_a, bt) max_mempool_cost = 40000000 * 5 mempool = Mempool(max_mempool_cost) @@ -248,11 +218,12 @@ async def next_block(full_node_1, wallet_a, bt) -> Coin: class TestMempoolManager: @pytest.mark.asyncio - async def test_basic_mempool_manager(self, bt, two_nodes_mempool, wallet_a, self_hostname): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + async def test_basic_mempool_manager(self, bt, two_nodes_one_block, wallet_a, self_hostname): + full_node_1, full_node_2, server_1, server_2 = two_nodes_one_block peer = await connect_and_get_peer(server_1, server_2, self_hostname) + _ = await next_block(full_node_1, wallet_a, bt) coin = await next_block(full_node_1, wallet_a, bt) spend_bundle = generate_test_spend_bundle(wallet_a, coin) assert spend_bundle is not None @@ -279,7 +250,7 @@ async def test_basic_mempool_manager(self, bt, two_nodes_mempool, wallet_a, self (ConditionOpcode.ASSERT_HEIGHT_RELATIVE, 0, MempoolInclusionStatus.PENDING), (ConditionOpcode.ASSERT_HEIGHT_RELATIVE, 1, MempoolInclusionStatus.PENDING), # the absolute height and seconds tests require fresh full nodes to - # run the test on. The fixture (two_nodes_mempool) creates 3 blocks, + # run the test on. The fixture (one_node_one_block) creates a block, # then condition_tester2 creates another 3 blocks (ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, 4, MempoolInclusionStatus.SUCCESS), (ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, 5, MempoolInclusionStatus.SUCCESS), @@ -292,7 +263,7 @@ async def test_basic_mempool_manager(self, bt, two_nodes_mempool, wallet_a, self (ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, 10052, MempoolInclusionStatus.FAILED), ], ) - async def test_ephemeral_timelock(self, bt, two_nodes_mempool, wallet_a, opcode, lock_value, expected): + async def test_ephemeral_timelock(self, bt, one_node_one_block, wallet_a, opcode, lock_value, expected): def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: conditions = {opcode: [ConditionWithArgs(opcode, [int_to_bytes(lock_value)])]} @@ -308,8 +279,10 @@ def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: bundle = SpendBundle.aggregate([tx1, tx2]) return bundle - full_node_1, _, server_1, _ = two_nodes_mempool - blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes_mempool, wallet_a, test_fun) + full_node_1, server_1 = one_node_one_block + _ = await next_block(full_node_1, wallet_a, bt) + _ = await next_block(full_node_1, wallet_a, bt) + blocks, bundle, status, err = await self.condition_tester2(bt, one_node_one_block, wallet_a, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) print(f"status: {status}") @@ -326,7 +299,7 @@ def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: # this test makes sure that one spend successfully asserts the announce from # another spend, even though the assert condition is duplicated 100 times @pytest.mark.asyncio - async def test_coin_announcement_duplicate_consumed(self, bt, two_nodes_mempool, wallet_a): + async def test_coin_announcement_duplicate_consumed(self, bt, one_node_one_block, wallet_a): def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: announce = Announcement(coin_2.name(), b"test") cvp = ConditionWithArgs(ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT, [announce.name()]) @@ -339,8 +312,8 @@ def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: bundle = SpendBundle.aggregate([spend_bundle1, spend_bundle2]) return bundle - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool - blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes_mempool, wallet_a, test_fun) + full_node_1, server_1 = one_node_one_block + blocks, bundle, status, err = await self.condition_tester2(bt, one_node_one_block, wallet_a, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) assert err is None @@ -350,7 +323,7 @@ def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: # this test makes sure that one spend successfully asserts the announce from # another spend, even though the create announcement is duplicated 100 times @pytest.mark.asyncio - async def test_coin_duplicate_announcement_consumed(self, bt, two_nodes_mempool, wallet_a): + async def test_coin_duplicate_announcement_consumed(self, bt, one_node_one_block, wallet_a): def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: announce = Announcement(coin_2.name(), b"test") cvp = ConditionWithArgs(ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT, [announce.name()]) @@ -363,8 +336,8 @@ def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: bundle = SpendBundle.aggregate([spend_bundle1, spend_bundle2]) return bundle - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool - blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes_mempool, wallet_a, test_fun) + full_node_1, server_1 = one_node_one_block + blocks, bundle, status, err = await self.condition_tester2(bt, one_node_one_block, wallet_a, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) assert err is None @@ -372,9 +345,9 @@ def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_double_spend(self, bt, two_nodes_mempool, wallet_a, self_hostname): + async def test_double_spend(self, bt, two_nodes_one_block, wallet_a, self_hostname): reward_ph = wallet_a.get_new_puzzlehash() - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, full_node_2, server_1, server_2 = two_nodes_one_block blocks = await full_node_1.get_all_full_blocks() start_height = blocks[-1].height blocks = bt.get_consecutive_blocks( @@ -433,10 +406,10 @@ def assert_sb_not_in_pool(self, node, sb): assert node.full_node.mempool_manager.get_spendbundle(sb.name()) is None @pytest.mark.asyncio - async def test_double_spend_with_higher_fee(self, bt, two_nodes_mempool, wallet_a, self_hostname): + async def test_double_spend_with_higher_fee(self, bt, two_nodes_one_block, wallet_a, self_hostname): reward_ph = wallet_a.get_new_puzzlehash() - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, full_node_2, server_1, server_2 = two_nodes_one_block 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( @@ -510,10 +483,10 @@ async def test_double_spend_with_higher_fee(self, bt, two_nodes_mempool, wallet_ self.assert_sb_not_in_pool(full_node_1, sb3) @pytest.mark.asyncio - async def test_invalid_signature(self, bt, two_nodes_mempool, wallet_a): + async def test_invalid_signature(self, bt, one_node_one_block, wallet_a): reward_ph = wallet_a.get_new_puzzlehash() - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, server_1 = one_node_one_block 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( @@ -544,7 +517,7 @@ async def test_invalid_signature(self, bt, two_nodes_mempool, wallet_a): async def condition_tester( self, bt, - two_nodes_mempool, + one_node_one_block, wallet_a, dic: Dict[ConditionOpcode, List[ConditionWithArgs]], fee: int = 0, @@ -552,7 +525,7 @@ async def condition_tester( coin: Optional[Coin] = None, ): reward_ph = wallet_a.get_new_puzzlehash() - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, server_1 = one_node_one_block blocks = await full_node_1.get_all_full_blocks() start_height = blocks[-1].height blocks = bt.get_consecutive_blocks( @@ -562,7 +535,12 @@ async def condition_tester( farmer_reward_puzzle_hash=reward_ph, pool_reward_puzzle_hash=reward_ph, ) - peer = await connect_and_get_peer(server_1, server_2, bt.config["self_hostname"]) + _, dummy_node_id = await add_dummy_connection(server_1, bt.config["self_hostname"], 100) + dummy_peer = None + for node_id, wsc in server_1.all_connections.items(): + if node_id == dummy_node_id: + dummy_peer = wsc + break for block in blocks: await full_node_1.full_node.respond_block(full_node_protocol.RespondBlock(block)) @@ -577,13 +555,13 @@ async def condition_tester( tx1: full_node_protocol.RespondTransaction = full_node_protocol.RespondTransaction(spend_bundle1) - status, err = await respond_transaction(full_node_1, tx1, peer, test=True) - return blocks, spend_bundle1, peer, status, err + status, err = await respond_transaction(full_node_1, tx1, dummy_peer, test=True) + return blocks, spend_bundle1, dummy_peer, status, err @pytest.mark.asyncio - async def condition_tester2(self, bt, two_nodes_mempool, wallet_a, test_fun: Callable[[Coin, Coin], SpendBundle]): + async def condition_tester2(self, bt, one_node_one_block, wallet_a, test_fun: Callable[[Coin, Coin], SpendBundle]): reward_ph = wallet_a.get_new_puzzlehash() - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, server_1 = one_node_one_block 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( @@ -594,7 +572,12 @@ async def condition_tester2(self, bt, two_nodes_mempool, wallet_a, test_fun: Cal pool_reward_puzzle_hash=reward_ph, time_per_block=10, ) - peer = await connect_and_get_peer(server_1, server_2, bt.config["self_hostname"]) + _, dummy_node_id = await add_dummy_connection(server_1, bt.config["self_hostname"], 100) + dummy_peer = None + for node_id, wsc in server_1.all_connections.items(): + if node_id == dummy_node_id: + dummy_peer = wsc + break for block in blocks: await full_node_1.full_node.respond_block(full_node_protocol.RespondBlock(block)) @@ -607,14 +590,14 @@ async def condition_tester2(self, bt, two_nodes_mempool, wallet_a, test_fun: Cal bundle = test_fun(coin_1, coin_2) tx1: full_node_protocol.RespondTransaction = full_node_protocol.RespondTransaction(bundle) - status, err = await respond_transaction(full_node_1, tx1, peer, test=True) + status, err = await respond_transaction(full_node_1, tx1, dummy_peer, test=True) return blocks, bundle, status, err @pytest.mark.asyncio - async def test_invalid_block_index(self, bt, two_nodes_mempool, wallet_a): + async def test_invalid_block_index(self, bt, one_node_one_block, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, server_1 = one_node_one_block blocks = await full_node_1.get_all_full_blocks() start_height = blocks[-1].height cvp = ConditionWithArgs( @@ -622,7 +605,7 @@ async def test_invalid_block_index(self, bt, two_nodes_mempool, wallet_a): [int_to_bytes(start_height + 5)], ) dic = {ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, one_node_one_block, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert sb1 is None # the transaction may become valid later @@ -630,13 +613,13 @@ async def test_invalid_block_index(self, bt, two_nodes_mempool, wallet_a): assert status == MempoolInclusionStatus.PENDING @pytest.mark.asyncio - async def test_block_index_missing_arg(self, bt, two_nodes_mempool, wallet_a): + async def test_block_index_missing_arg(self, bt, one_node_one_block, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, server_1 = one_node_one_block blocks = await full_node_1.get_all_full_blocks() cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, []) dic = {ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, one_node_one_block, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert sb1 is None # the transaction may become valid later @@ -644,49 +627,49 @@ async def test_block_index_missing_arg(self, bt, two_nodes_mempool, wallet_a): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_correct_block_index(self, bt, two_nodes_mempool, wallet_a): + async def test_correct_block_index(self, bt, one_node_one_block, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, server_1 = one_node_one_block cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, [int_to_bytes(1)]) dic = {ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, one_node_one_block, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err is None assert sb1 is spend_bundle1 assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_block_index_garbage(self, bt, two_nodes_mempool, wallet_a): + async def test_block_index_garbage(self, bt, one_node_one_block, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, server_1 = one_node_one_block # garbage at the end of the argument list is ignored cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, [int_to_bytes(1), b"garbage"]) dic = {ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, one_node_one_block, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err is None assert sb1 is spend_bundle1 assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_negative_block_index(self, bt, two_nodes_mempool, wallet_a): + async def test_negative_block_index(self, bt, one_node_one_block, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, server_1 = one_node_one_block cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, [int_to_bytes(-1)]) dic = {ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, one_node_one_block, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err is None assert sb1 is spend_bundle1 assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_invalid_block_age(self, bt, two_nodes_mempool, wallet_a): + async def test_invalid_block_age(self, bt, one_node_one_block, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, server_1 = one_node_one_block cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_RELATIVE, [int_to_bytes(5)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, one_node_one_block, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err == Err.ASSERT_HEIGHT_RELATIVE_FAILED assert sb1 is None @@ -694,12 +677,12 @@ async def test_invalid_block_age(self, bt, two_nodes_mempool, wallet_a): assert status == MempoolInclusionStatus.PENDING @pytest.mark.asyncio - async def test_block_age_missing_arg(self, bt, two_nodes_mempool, wallet_a): + async def test_block_age_missing_arg(self, bt, one_node_one_block, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, server_1 = one_node_one_block cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_RELATIVE, []) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, one_node_one_block, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err == Err.INVALID_CONDITION assert sb1 is None @@ -707,13 +690,13 @@ async def test_block_age_missing_arg(self, bt, two_nodes_mempool, wallet_a): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_correct_block_age(self, bt, two_nodes_mempool, wallet_a): + async def test_correct_block_age(self, bt, one_node_one_block, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, server_1 = one_node_one_block cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_RELATIVE, [int_to_bytes(1)]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester( - bt, two_nodes_mempool, wallet_a, dic, num_blocks=4 + bt, one_node_one_block, wallet_a, dic, num_blocks=4 ) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -722,14 +705,14 @@ async def test_correct_block_age(self, bt, two_nodes_mempool, wallet_a): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_block_age_garbage(self, bt, two_nodes_mempool, wallet_a): + async def test_block_age_garbage(self, bt, one_node_one_block, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, server_1 = one_node_one_block # garbage at the end of the argument list is ignored cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_RELATIVE, [int_to_bytes(1), b"garbage"]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester( - bt, two_nodes_mempool, wallet_a, dic, num_blocks=4 + bt, one_node_one_block, wallet_a, dic, num_blocks=4 ) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -738,13 +721,13 @@ async def test_block_age_garbage(self, bt, two_nodes_mempool, wallet_a): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_negative_block_age(self, bt, two_nodes_mempool, wallet_a): + async def test_negative_block_age(self, bt, one_node_one_block, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, server_1 = one_node_one_block cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_RELATIVE, [int_to_bytes(-1)]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester( - bt, two_nodes_mempool, wallet_a, dic, num_blocks=4 + bt, one_node_one_block, wallet_a, dic, num_blocks=4 ) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -753,14 +736,17 @@ async def test_negative_block_age(self, bt, two_nodes_mempool, wallet_a): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_correct_my_id(self, bt, two_nodes_mempool, wallet_a): + async def test_correct_my_id(self, bt, one_node_one_block, wallet_a): + + full_node_1, server_1 = one_node_one_block - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + _ = await next_block(full_node_1, wallet_a, bt) + _ = await next_block(full_node_1, wallet_a, bt) coin = await next_block(full_node_1, wallet_a, bt) cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_COIN_ID, [coin.name()]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester( - bt, two_nodes_mempool, wallet_a, dic, coin=coin + bt, one_node_one_block, wallet_a, dic, coin=coin ) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -769,15 +755,18 @@ async def test_correct_my_id(self, bt, two_nodes_mempool, wallet_a): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_my_id_garbage(self, bt, two_nodes_mempool, wallet_a): + async def test_my_id_garbage(self, bt, one_node_one_block, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, server_1 = one_node_one_block + + _ = await next_block(full_node_1, wallet_a, bt) + _ = await next_block(full_node_1, wallet_a, bt) coin = await next_block(full_node_1, wallet_a, bt) # garbage at the end of the argument list is ignored cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_COIN_ID, [coin.name(), b"garbage"]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester( - bt, two_nodes_mempool, wallet_a, dic, coin=coin + bt, one_node_one_block, wallet_a, dic, coin=coin ) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -786,15 +775,18 @@ async def test_my_id_garbage(self, bt, two_nodes_mempool, wallet_a): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_invalid_my_id(self, bt, two_nodes_mempool, wallet_a): + async def test_invalid_my_id(self, bt, one_node_one_block, wallet_a): + + full_node_1, server_1 = one_node_one_block - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + _ = await next_block(full_node_1, wallet_a, bt) + _ = await next_block(full_node_1, wallet_a, bt) coin = await next_block(full_node_1, wallet_a, bt) coin_2 = await next_block(full_node_1, wallet_a, bt) cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_COIN_ID, [coin_2.name()]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester( - bt, two_nodes_mempool, wallet_a, dic, coin=coin + bt, one_node_one_block, wallet_a, dic, coin=coin ) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -803,13 +795,13 @@ async def test_invalid_my_id(self, bt, two_nodes_mempool, wallet_a): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_my_id_missing_arg(self, bt, two_nodes_mempool, wallet_a): + async def test_my_id_missing_arg(self, bt, one_node_one_block, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, server_1 = one_node_one_block blocks = await full_node_1.get_all_full_blocks() cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_COIN_ID, []) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, one_node_one_block, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err == Err.INVALID_CONDITION @@ -817,100 +809,100 @@ async def test_my_id_missing_arg(self, bt, two_nodes_mempool, wallet_a): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_assert_time_exceeds(self, bt, two_nodes_mempool, wallet_a): + async def test_assert_time_exceeds(self, bt, one_node_one_block, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, server_1 = one_node_one_block # 5 seconds should be before the next block time_now = full_node_1.full_node.blockchain.get_peak().timestamp + 5 cvp = ConditionWithArgs(ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, [int_to_bytes(time_now)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, one_node_one_block, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err is None assert sb1 is spend_bundle1 assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_assert_time_fail(self, bt, two_nodes_mempool, wallet_a): + async def test_assert_time_fail(self, bt, one_node_one_block, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, server_1 = one_node_one_block time_now = full_node_1.full_node.blockchain.get_peak().timestamp + 1000 cvp = ConditionWithArgs(ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, [int_to_bytes(time_now)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, one_node_one_block, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err == Err.ASSERT_SECONDS_ABSOLUTE_FAILED assert sb1 is None assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_assert_height_pending(self, bt, two_nodes_mempool, wallet_a): + async def test_assert_height_pending(self, bt, one_node_one_block, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, server_1 = one_node_one_block print(full_node_1.full_node.blockchain.get_peak()) current_height = full_node_1.full_node.blockchain.get_peak().height cvp = ConditionWithArgs(ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, [int_to_bytes(current_height + 4)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, one_node_one_block, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err == Err.ASSERT_HEIGHT_ABSOLUTE_FAILED assert sb1 is None assert status == MempoolInclusionStatus.PENDING @pytest.mark.asyncio - async def test_assert_time_negative(self, bt, two_nodes_mempool, wallet_a): + async def test_assert_time_negative(self, bt, one_node_one_block, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, server_1 = one_node_one_block time_now = -1 cvp = ConditionWithArgs(ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, [int_to_bytes(time_now)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, one_node_one_block, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err is None assert sb1 is spend_bundle1 assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_assert_time_missing_arg(self, bt, two_nodes_mempool, wallet_a): + async def test_assert_time_missing_arg(self, bt, one_node_one_block, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, server_1 = one_node_one_block cvp = ConditionWithArgs(ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, []) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, one_node_one_block, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err == Err.INVALID_CONDITION assert sb1 is None assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_assert_time_garbage(self, bt, two_nodes_mempool, wallet_a): + async def test_assert_time_garbage(self, bt, one_node_one_block, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, server_1 = one_node_one_block time_now = full_node_1.full_node.blockchain.get_peak().timestamp + 5 # garbage at the end of the argument list is ignored cvp = ConditionWithArgs(ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, [int_to_bytes(time_now), b"garbage"]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, one_node_one_block, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err is None assert sb1 is spend_bundle1 assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_assert_time_relative_exceeds(self, bt, two_nodes_mempool, wallet_a): + async def test_assert_time_relative_exceeds(self, bt, one_node_one_block, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, server_1 = one_node_one_block time_relative = 3 cvp = ConditionWithArgs(ConditionOpcode.ASSERT_SECONDS_RELATIVE, [int_to_bytes(time_relative)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, one_node_one_block, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err == Err.ASSERT_SECONDS_RELATIVE_FAILED @@ -930,15 +922,15 @@ async def test_assert_time_relative_exceeds(self, bt, two_nodes_mempool, wallet_ assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_assert_time_relative_garbage(self, bt, two_nodes_mempool, wallet_a): + async def test_assert_time_relative_garbage(self, bt, one_node_one_block, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, server_1 = one_node_one_block time_relative = 0 # garbage at the end of the arguments is ignored cvp = ConditionWithArgs(ConditionOpcode.ASSERT_SECONDS_RELATIVE, [int_to_bytes(time_relative), b"garbage"]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, one_node_one_block, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err is None @@ -946,13 +938,13 @@ async def test_assert_time_relative_garbage(self, bt, two_nodes_mempool, wallet_ assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_assert_time_relative_missing_arg(self, bt, two_nodes_mempool, wallet_a): + async def test_assert_time_relative_missing_arg(self, bt, one_node_one_block, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, server_1 = one_node_one_block cvp = ConditionWithArgs(ConditionOpcode.ASSERT_SECONDS_RELATIVE, []) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, one_node_one_block, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err == Err.INVALID_CONDITION @@ -960,14 +952,14 @@ async def test_assert_time_relative_missing_arg(self, bt, two_nodes_mempool, wal assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_assert_time_relative_negative(self, bt, two_nodes_mempool, wallet_a): + async def test_assert_time_relative_negative(self, bt, one_node_one_block, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, server_1 = one_node_one_block time_relative = -3 cvp = ConditionWithArgs(ConditionOpcode.ASSERT_SECONDS_RELATIVE, [int_to_bytes(time_relative)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, one_node_one_block, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) assert err is None @@ -976,7 +968,7 @@ async def test_assert_time_relative_negative(self, bt, two_nodes_mempool, wallet # ensure one spend can assert a coin announcement from another spend @pytest.mark.asyncio - async def test_correct_coin_announcement_consumed(self, bt, two_nodes_mempool, wallet_a): + async def test_correct_coin_announcement_consumed(self, bt, one_node_one_block, wallet_a): def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: announce = Announcement(coin_2.name(), b"test") cvp = ConditionWithArgs(ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT, [announce.name()]) @@ -989,8 +981,8 @@ def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: bundle = SpendBundle.aggregate([spend_bundle1, spend_bundle2]) return bundle - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool - blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes_mempool, wallet_a, test_fun) + full_node_1, server_1 = one_node_one_block + blocks, bundle, status, err = await self.condition_tester2(bt, one_node_one_block, wallet_a, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) assert err is None @@ -1000,7 +992,7 @@ def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: # ensure one spend can assert a coin announcement from another spend, even # though the conditions have garbage (ignored) at the end @pytest.mark.asyncio - async def test_coin_announcement_garbage(self, bt, two_nodes_mempool, wallet_a): + async def test_coin_announcement_garbage(self, bt, one_node_one_block, wallet_a): def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: announce = Announcement(coin_2.name(), b"test") # garbage at the end is ignored @@ -1015,8 +1007,8 @@ def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: bundle = SpendBundle.aggregate([spend_bundle1, spend_bundle2]) return bundle - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool - blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes_mempool, wallet_a, test_fun) + full_node_1, server_1 = one_node_one_block + blocks, bundle, status, err = await self.condition_tester2(bt, one_node_one_block, wallet_a, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) assert err is None @@ -1024,8 +1016,8 @@ def test_fun(coin_1: Coin, coin_2: Coin) -> SpendBundle: assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_coin_announcement_missing_arg(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + async def test_coin_announcement_missing_arg(self, bt, one_node_one_block, wallet_a): + full_node_1, server_1 = one_node_one_block def test_fun(coin_1: Coin, coin_2: Coin): # missing arg here @@ -1038,15 +1030,15 @@ def test_fun(coin_1: Coin, coin_2: Coin): return SpendBundle.aggregate([spend_bundle1, spend_bundle2]) - blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes_mempool, wallet_a, test_fun) + blocks, bundle, status, err = await self.condition_tester2(bt, one_node_one_block, wallet_a, test_fun) assert err == Err.INVALID_CONDITION assert full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) is None assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_coin_announcement_missing_arg2(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + async def test_coin_announcement_missing_arg2(self, bt, one_node_one_block, wallet_a): + full_node_1, server_1 = one_node_one_block def test_fun(coin_1: Coin, coin_2: Coin): announce = Announcement(coin_2.name(), b"test") @@ -1060,15 +1052,15 @@ def test_fun(coin_1: Coin, coin_2: Coin): return SpendBundle.aggregate([spend_bundle1, spend_bundle2]) - blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes_mempool, wallet_a, test_fun) + blocks, bundle, status, err = await self.condition_tester2(bt, one_node_one_block, wallet_a, test_fun) assert err == Err.INVALID_CONDITION assert full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) is None assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_coin_announcement_too_big(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + async def test_coin_announcement_too_big(self, bt, one_node_one_block, wallet_a): + full_node_1, server_1 = one_node_one_block def test_fun(coin_1: Coin, coin_2: Coin): announce = Announcement(coin_2.name(), bytes([1] * 10000)) @@ -1084,7 +1076,7 @@ def test_fun(coin_1: Coin, coin_2: Coin): return SpendBundle.aggregate([spend_bundle1, spend_bundle2]) - blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes_mempool, wallet_a, test_fun) + blocks, bundle, status, err = await self.condition_tester2(bt, one_node_one_block, wallet_a, test_fun) assert err == Err.ASSERT_ANNOUNCE_CONSUMED_FAILED assert full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) is None @@ -1102,8 +1094,8 @@ def test_fun(coin_1: Coin, coin_2: Coin): # ensure an assert coin announcement is rejected if it doesn't match the # create announcement @pytest.mark.asyncio - async def test_invalid_coin_announcement_rejected(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + async def test_invalid_coin_announcement_rejected(self, bt, one_node_one_block, wallet_a): + full_node_1, server_1 = one_node_one_block def test_fun(coin_1: Coin, coin_2: Coin): announce = Announcement(coin_2.name(), b"test") @@ -1122,7 +1114,7 @@ def test_fun(coin_1: Coin, coin_2: Coin): return SpendBundle.aggregate([spend_bundle1, spend_bundle2]) - blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes_mempool, wallet_a, test_fun) + blocks, bundle, status, err = await self.condition_tester2(bt, one_node_one_block, wallet_a, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) @@ -1131,8 +1123,8 @@ def test_fun(coin_1: Coin, coin_2: Coin): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_invalid_coin_announcement_rejected_two(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + async def test_invalid_coin_announcement_rejected_two(self, bt, one_node_one_block, wallet_a): + full_node_1, server_1 = one_node_one_block def test_fun(coin_1: Coin, coin_2: Coin): announce = Announcement(coin_1.name(), b"test") @@ -1149,7 +1141,7 @@ def test_fun(coin_1: Coin, coin_2: Coin): return SpendBundle.aggregate([spend_bundle1, spend_bundle2]) - blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes_mempool, wallet_a, test_fun) + blocks, bundle, status, err = await self.condition_tester2(bt, one_node_one_block, wallet_a, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) assert err == Err.ASSERT_ANNOUNCE_CONSUMED_FAILED @@ -1157,8 +1149,8 @@ def test_fun(coin_1: Coin, coin_2: Coin): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_correct_puzzle_announcement(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + async def test_correct_puzzle_announcement(self, bt, one_node_one_block, wallet_a): + full_node_1, server_1 = one_node_one_block def test_fun(coin_1: Coin, coin_2: Coin): announce = Announcement(coin_2.puzzle_hash, bytes(0x80)) @@ -1174,7 +1166,7 @@ def test_fun(coin_1: Coin, coin_2: Coin): return SpendBundle.aggregate([spend_bundle1, spend_bundle2]) - blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes_mempool, wallet_a, test_fun) + blocks, bundle, status, err = await self.condition_tester2(bt, one_node_one_block, wallet_a, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) @@ -1183,8 +1175,8 @@ def test_fun(coin_1: Coin, coin_2: Coin): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_puzzle_announcement_garbage(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + async def test_puzzle_announcement_garbage(self, bt, one_node_one_block, wallet_a): + full_node_1, server_1 = one_node_one_block def test_fun(coin_1: Coin, coin_2: Coin): announce = Announcement(coin_2.puzzle_hash, bytes(0x80)) @@ -1200,7 +1192,7 @@ def test_fun(coin_1: Coin, coin_2: Coin): return SpendBundle.aggregate([spend_bundle1, spend_bundle2]) - blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes_mempool, wallet_a, test_fun) + blocks, bundle, status, err = await self.condition_tester2(bt, one_node_one_block, wallet_a, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) assert err is None @@ -1208,8 +1200,8 @@ def test_fun(coin_1: Coin, coin_2: Coin): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_puzzle_announcement_missing_arg(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + async def test_puzzle_announcement_missing_arg(self, bt, one_node_one_block, wallet_a): + full_node_1, server_1 = one_node_one_block def test_fun(coin_1: Coin, coin_2: Coin): # missing arg here @@ -1225,7 +1217,7 @@ def test_fun(coin_1: Coin, coin_2: Coin): return SpendBundle.aggregate([spend_bundle1, spend_bundle2]) - blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes_mempool, wallet_a, test_fun) + blocks, bundle, status, err = await self.condition_tester2(bt, one_node_one_block, wallet_a, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) @@ -1234,8 +1226,8 @@ def test_fun(coin_1: Coin, coin_2: Coin): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_puzzle_announcement_missing_arg2(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + async def test_puzzle_announcement_missing_arg2(self, bt, one_node_one_block, wallet_a): + full_node_1, server_1 = one_node_one_block def test_fun(coin_1: Coin, coin_2: Coin): announce = Announcement(coin_2.puzzle_hash, b"test") @@ -1253,7 +1245,7 @@ def test_fun(coin_1: Coin, coin_2: Coin): return SpendBundle.aggregate([spend_bundle1, spend_bundle2]) - blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes_mempool, wallet_a, test_fun) + blocks, bundle, status, err = await self.condition_tester2(bt, one_node_one_block, wallet_a, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) @@ -1262,8 +1254,8 @@ def test_fun(coin_1: Coin, coin_2: Coin): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_invalid_puzzle_announcement_rejected(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + async def test_invalid_puzzle_announcement_rejected(self, bt, one_node_one_block, wallet_a): + full_node_1, server_1 = one_node_one_block def test_fun(coin_1: Coin, coin_2: Coin): announce = Announcement(coin_2.puzzle_hash, bytes("test", "utf-8")) @@ -1282,7 +1274,7 @@ def test_fun(coin_1: Coin, coin_2: Coin): return SpendBundle.aggregate([spend_bundle1, spend_bundle2]) - blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes_mempool, wallet_a, test_fun) + blocks, bundle, status, err = await self.condition_tester2(bt, one_node_one_block, wallet_a, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) @@ -1291,8 +1283,8 @@ def test_fun(coin_1: Coin, coin_2: Coin): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_invalid_puzzle_announcement_rejected_two(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + async def test_invalid_puzzle_announcement_rejected_two(self, bt, one_node_one_block, wallet_a): + full_node_1, server_1 = one_node_one_block def test_fun(coin_1: Coin, coin_2: Coin): announce = Announcement(coin_2.puzzle_hash, b"test") @@ -1311,7 +1303,7 @@ def test_fun(coin_1: Coin, coin_2: Coin): return SpendBundle.aggregate([spend_bundle1, spend_bundle2]) - blocks, bundle, status, err = await self.condition_tester2(bt, two_nodes_mempool, wallet_a, test_fun) + blocks, bundle, status, err = await self.condition_tester2(bt, one_node_one_block, wallet_a, test_fun) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(bundle.name()) @@ -1320,13 +1312,13 @@ def test_fun(coin_1: Coin, coin_2: Coin): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_assert_fee_condition(self, bt, two_nodes_mempool, wallet_a): + async def test_assert_fee_condition(self, bt, one_node_one_block, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, server_1 = one_node_one_block cvp = ConditionWithArgs(ConditionOpcode.RESERVE_FEE, [int_to_bytes(10)]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester( - bt, two_nodes_mempool, wallet_a, dic, fee=10 + bt, one_node_one_block, wallet_a, dic, fee=10 ) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1335,14 +1327,14 @@ async def test_assert_fee_condition(self, bt, two_nodes_mempool, wallet_a): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_assert_fee_condition_garbage(self, bt, two_nodes_mempool, wallet_a): + async def test_assert_fee_condition_garbage(self, bt, one_node_one_block, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, server_1 = one_node_one_block # garbage at the end of the arguments is ignored cvp = ConditionWithArgs(ConditionOpcode.RESERVE_FEE, [int_to_bytes(10), b"garbage"]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester( - bt, two_nodes_mempool, wallet_a, dic, fee=10 + bt, one_node_one_block, wallet_a, dic, fee=10 ) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1351,23 +1343,23 @@ async def test_assert_fee_condition_garbage(self, bt, two_nodes_mempool, wallet_ assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_assert_fee_condition_missing_arg(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + async def test_assert_fee_condition_missing_arg(self, bt, one_node_one_block, wallet_a): + full_node_1, server_1 = one_node_one_block cvp = ConditionWithArgs(ConditionOpcode.RESERVE_FEE, []) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester( - bt, two_nodes_mempool, wallet_a, dic, fee=10 + bt, one_node_one_block, wallet_a, dic, fee=10 ) assert err == Err.INVALID_CONDITION assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_assert_fee_condition_negative_fee(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + async def test_assert_fee_condition_negative_fee(self, bt, one_node_one_block, wallet_a): + full_node_1, server_1 = one_node_one_block cvp = ConditionWithArgs(ConditionOpcode.RESERVE_FEE, [int_to_bytes(-1)]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester( - bt, two_nodes_mempool, wallet_a, dic, fee=10 + bt, one_node_one_block, wallet_a, dic, fee=10 ) assert err == Err.RESERVE_FEE_CONDITION_FAILED assert status == MempoolInclusionStatus.FAILED @@ -1380,12 +1372,12 @@ async def test_assert_fee_condition_negative_fee(self, bt, two_nodes_mempool, wa ) @pytest.mark.asyncio - async def test_assert_fee_condition_fee_too_large(self, bt, two_nodes_mempool, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + async def test_assert_fee_condition_fee_too_large(self, bt, one_node_one_block, wallet_a): + full_node_1, server_1 = one_node_one_block cvp = ConditionWithArgs(ConditionOpcode.RESERVE_FEE, [int_to_bytes(2 ** 64)]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester( - bt, two_nodes_mempool, wallet_a, dic, fee=10 + bt, one_node_one_block, wallet_a, dic, fee=10 ) assert err == Err.RESERVE_FEE_CONDITION_FAILED assert status == MempoolInclusionStatus.FAILED @@ -1398,14 +1390,14 @@ async def test_assert_fee_condition_fee_too_large(self, bt, two_nodes_mempool, w ) @pytest.mark.asyncio - async def test_assert_fee_condition_wrong_fee(self, bt, two_nodes_mempool, wallet_a): + async def test_assert_fee_condition_wrong_fee(self, bt, one_node_one_block, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, server_1 = one_node_one_block cvp = ConditionWithArgs(ConditionOpcode.RESERVE_FEE, [int_to_bytes(10)]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester( - bt, two_nodes_mempool, wallet_a, dic, fee=9 + bt, one_node_one_block, wallet_a, dic, fee=9 ) mempool_bundle = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1414,9 +1406,9 @@ async def test_assert_fee_condition_wrong_fee(self, bt, two_nodes_mempool, walle assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_stealing_fee(self, bt, two_nodes_mempool, wallet_a): + async def test_stealing_fee(self, bt, two_nodes_one_block, wallet_a): reward_ph = wallet_a.get_new_puzzlehash() - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, full_node_2, server_1, server_2 = two_nodes_one_block blocks = await full_node_1.get_all_full_blocks() start_height = blocks[-1].height blocks = bt.get_consecutive_blocks( @@ -1427,7 +1419,6 @@ async def test_stealing_fee(self, bt, two_nodes_mempool, wallet_a): pool_reward_puzzle_hash=reward_ph, ) - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool peer = await connect_and_get_peer(server_1, server_2, bt.config["self_hostname"]) for block in blocks: @@ -1471,9 +1462,9 @@ async def test_stealing_fee(self, bt, two_nodes_mempool, wallet_a): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_double_spend_same_bundle(self, bt, two_nodes_mempool, wallet_a): + async def test_double_spend_same_bundle(self, bt, two_nodes_one_block, wallet_a): reward_ph = wallet_a.get_new_puzzlehash() - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, full_node_2, server_1, server_2 = two_nodes_one_block blocks = await full_node_1.get_all_full_blocks() start_height = blocks[-1].height blocks = bt.get_consecutive_blocks( @@ -1517,9 +1508,9 @@ async def test_double_spend_same_bundle(self, bt, two_nodes_mempool, wallet_a): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_agg_sig_condition(self, bt, two_nodes_mempool, wallet_a): + async def test_agg_sig_condition(self, bt, one_node_one_block, wallet_a): reward_ph = wallet_a.get_new_puzzlehash() - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, server_1 = one_node_one_block blocks = await full_node_1.get_all_full_blocks() start_height = blocks[-1].height blocks = bt.get_consecutive_blocks( @@ -1565,14 +1556,17 @@ async def test_agg_sig_condition(self, bt, two_nodes_mempool, wallet_a): # assert sb is spend_bundle @pytest.mark.asyncio - async def test_correct_my_parent(self, bt, two_nodes_mempool, wallet_a): + async def test_correct_my_parent(self, bt, one_node_one_block, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, server_1 = one_node_one_block + + _ = await next_block(full_node_1, wallet_a, bt) + _ = await next_block(full_node_1, wallet_a, bt) coin = await next_block(full_node_1, wallet_a, bt) cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_PARENT_ID, [coin.parent_coin_info]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester( - bt, two_nodes_mempool, wallet_a, dic, coin=coin + bt, one_node_one_block, wallet_a, dic, coin=coin ) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1582,15 +1576,18 @@ async def test_correct_my_parent(self, bt, two_nodes_mempool, wallet_a): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_my_parent_garbage(self, bt, two_nodes_mempool, wallet_a): + async def test_my_parent_garbage(self, bt, one_node_one_block, wallet_a): + + full_node_1, server_1 = one_node_one_block - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + _ = await next_block(full_node_1, wallet_a, bt) + _ = await next_block(full_node_1, wallet_a, bt) coin = await next_block(full_node_1, wallet_a, bt) # garbage at the end of the arguments list is allowed but stripped cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_PARENT_ID, [coin.parent_coin_info, b"garbage"]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester( - bt, two_nodes_mempool, wallet_a, dic, coin=coin + bt, one_node_one_block, wallet_a, dic, coin=coin ) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1600,13 +1597,13 @@ async def test_my_parent_garbage(self, bt, two_nodes_mempool, wallet_a): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_my_parent_missing_arg(self, bt, two_nodes_mempool, wallet_a): + async def test_my_parent_missing_arg(self, bt, one_node_one_block, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, server_1 = one_node_one_block blocks = await full_node_1.get_all_full_blocks() cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_PARENT_ID, []) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, one_node_one_block, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1615,15 +1612,18 @@ async def test_my_parent_missing_arg(self, bt, two_nodes_mempool, wallet_a): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_invalid_my_parent(self, bt, two_nodes_mempool, wallet_a): + async def test_invalid_my_parent(self, bt, one_node_one_block, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, server_1 = one_node_one_block + + _ = await next_block(full_node_1, wallet_a, bt) + _ = await next_block(full_node_1, wallet_a, bt) coin = await next_block(full_node_1, wallet_a, bt) coin_2 = await next_block(full_node_1, wallet_a, bt) cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_PARENT_ID, [coin_2.parent_coin_info]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester( - bt, two_nodes_mempool, wallet_a, dic, coin=coin + bt, one_node_one_block, wallet_a, dic, coin=coin ) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1633,14 +1633,17 @@ async def test_invalid_my_parent(self, bt, two_nodes_mempool, wallet_a): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_correct_my_puzhash(self, bt, two_nodes_mempool, wallet_a): + async def test_correct_my_puzhash(self, bt, one_node_one_block, wallet_a): + + full_node_1, server_1 = one_node_one_block - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + _ = await next_block(full_node_1, wallet_a, bt) + _ = await next_block(full_node_1, wallet_a, bt) coin = await next_block(full_node_1, wallet_a, bt) cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_PUZZLEHASH, [coin.puzzle_hash]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester( - bt, two_nodes_mempool, wallet_a, dic, coin=coin + bt, one_node_one_block, wallet_a, dic, coin=coin ) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1650,15 +1653,18 @@ async def test_correct_my_puzhash(self, bt, two_nodes_mempool, wallet_a): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_my_puzhash_garbage(self, bt, two_nodes_mempool, wallet_a): + async def test_my_puzhash_garbage(self, bt, one_node_one_block, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, server_1 = one_node_one_block + + _ = await next_block(full_node_1, wallet_a, bt) + _ = await next_block(full_node_1, wallet_a, bt) coin = await next_block(full_node_1, wallet_a, bt) # garbage at the end of the arguments list is allowed but stripped cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_PUZZLEHASH, [coin.puzzle_hash, b"garbage"]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester( - bt, two_nodes_mempool, wallet_a, dic, coin=coin + bt, one_node_one_block, wallet_a, dic, coin=coin ) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1668,13 +1674,13 @@ async def test_my_puzhash_garbage(self, bt, two_nodes_mempool, wallet_a): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_my_puzhash_missing_arg(self, bt, two_nodes_mempool, wallet_a): + async def test_my_puzhash_missing_arg(self, bt, one_node_one_block, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, server_1 = one_node_one_block blocks = await full_node_1.get_all_full_blocks() cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_PUZZLEHASH, []) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, one_node_one_block, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1683,14 +1689,17 @@ async def test_my_puzhash_missing_arg(self, bt, two_nodes_mempool, wallet_a): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_invalid_my_puzhash(self, bt, two_nodes_mempool, wallet_a): + async def test_invalid_my_puzhash(self, bt, one_node_one_block, wallet_a): + + full_node_1, server_1 = one_node_one_block - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + _ = await next_block(full_node_1, wallet_a, bt) + _ = await next_block(full_node_1, wallet_a, bt) coin = await next_block(full_node_1, wallet_a, bt) cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_PUZZLEHASH, [Program.to([]).get_tree_hash()]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester( - bt, two_nodes_mempool, wallet_a, dic, coin=coin + bt, one_node_one_block, wallet_a, dic, coin=coin ) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1700,14 +1709,17 @@ async def test_invalid_my_puzhash(self, bt, two_nodes_mempool, wallet_a): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_correct_my_amount(self, bt, two_nodes_mempool, wallet_a): + async def test_correct_my_amount(self, bt, one_node_one_block, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, server_1 = one_node_one_block + + _ = await next_block(full_node_1, wallet_a, bt) + _ = await next_block(full_node_1, wallet_a, bt) coin = await next_block(full_node_1, wallet_a, bt) cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_AMOUNT, [int_to_bytes(coin.amount)]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester( - bt, two_nodes_mempool, wallet_a, dic, coin=coin + bt, one_node_one_block, wallet_a, dic, coin=coin ) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1717,15 +1729,18 @@ async def test_correct_my_amount(self, bt, two_nodes_mempool, wallet_a): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_my_amount_garbage(self, bt, two_nodes_mempool, wallet_a): + async def test_my_amount_garbage(self, bt, one_node_one_block, wallet_a): + + full_node_1, server_1 = one_node_one_block - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + _ = await next_block(full_node_1, wallet_a, bt) + _ = await next_block(full_node_1, wallet_a, bt) coin = await next_block(full_node_1, wallet_a, bt) # garbage at the end of the arguments list is allowed but stripped cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_AMOUNT, [int_to_bytes(coin.amount), b"garbage"]) dic = {cvp.opcode: [cvp]} blocks, spend_bundle1, peer, status, err = await self.condition_tester( - bt, two_nodes_mempool, wallet_a, dic, coin=coin + bt, one_node_one_block, wallet_a, dic, coin=coin ) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1735,13 +1750,13 @@ async def test_my_amount_garbage(self, bt, two_nodes_mempool, wallet_a): assert status == MempoolInclusionStatus.SUCCESS @pytest.mark.asyncio - async def test_my_amount_missing_arg(self, bt, two_nodes_mempool, wallet_a): + async def test_my_amount_missing_arg(self, bt, one_node_one_block, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, server_1 = one_node_one_block blocks = await full_node_1.get_all_full_blocks() cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_AMOUNT, []) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, one_node_one_block, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1750,13 +1765,13 @@ async def test_my_amount_missing_arg(self, bt, two_nodes_mempool, wallet_a): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_invalid_my_amount(self, bt, two_nodes_mempool, wallet_a): + async def test_invalid_my_amount(self, bt, one_node_one_block, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, server_1 = one_node_one_block blocks = await full_node_1.get_all_full_blocks() cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_AMOUNT, [int_to_bytes(1000)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, one_node_one_block, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1765,13 +1780,13 @@ async def test_invalid_my_amount(self, bt, two_nodes_mempool, wallet_a): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_negative_my_amount(self, bt, two_nodes_mempool, wallet_a): + async def test_negative_my_amount(self, bt, one_node_one_block, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, server_1 = one_node_one_block blocks = await full_node_1.get_all_full_blocks() cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_AMOUNT, [int_to_bytes(-1)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, one_node_one_block, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -1780,13 +1795,13 @@ async def test_negative_my_amount(self, bt, two_nodes_mempool, wallet_a): assert status == MempoolInclusionStatus.FAILED @pytest.mark.asyncio - async def test_my_amount_too_large(self, bt, two_nodes_mempool, wallet_a): + async def test_my_amount_too_large(self, bt, one_node_one_block, wallet_a): - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, server_1 = one_node_one_block blocks = await full_node_1.get_all_full_blocks() cvp = ConditionWithArgs(ConditionOpcode.ASSERT_MY_AMOUNT, [int_to_bytes(2 ** 64)]) dic = {cvp.opcode: [cvp]} - blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, two_nodes_mempool, wallet_a, dic) + blocks, spend_bundle1, peer, status, err = await self.condition_tester(bt, one_node_one_block, wallet_a, dic) sb1 = full_node_1.full_node.mempool_manager.get_spendbundle(spend_bundle1.name()) @@ -2409,7 +2424,7 @@ def test_many_create_coin(self, softfork_height): assert run_time < 0.2 @pytest.mark.asyncio - async def test_invalid_coin_spend_coin(self, bt, two_nodes_mempool, wallet_a): + async def test_invalid_coin_spend_coin(self, bt, one_node_one_block, wallet_a): reward_ph = wallet_a.get_new_puzzlehash() blocks = bt.get_consecutive_blocks( 5, @@ -2417,7 +2432,7 @@ async def test_invalid_coin_spend_coin(self, bt, two_nodes_mempool, wallet_a): farmer_reward_puzzle_hash=reward_ph, pool_reward_puzzle_hash=reward_ph, ) - full_node_1, full_node_2, server_1, server_2 = two_nodes_mempool + full_node_1, server_1 = one_node_one_block for block in blocks: await full_node_1.full_node.respond_block(full_node_protocol.RespondBlock(block)) From 22edd733313cdd1b679abf64400c5d52801a38e8 Mon Sep 17 00:00:00 2001 From: Adam Kelly <338792+aqk@users.noreply.github.com> Date: Mon, 4 Apr 2022 17:43:31 -0700 Subject: [PATCH 318/378] Force apt to install the things we asked it to (#11047) * Force apt to install the things we asked it to * Update .github/workflows/benchmarks.yml Co-authored-by: dustinface <35775977+xdustinface@users.noreply.github.com> Co-authored-by: dustinface <35775977+xdustinface@users.noreply.github.com> --- .github/workflows/benchmarks.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 90bea4ef84e9..0dc6ff3bbe86 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -51,11 +51,13 @@ jobs: ${{ runner.os }}-pip- - name: Install ubuntu dependencies + env: + DEBIAN_FRONTEND: noninteractive run: | - sudo apt-get install software-properties-common + sudo apt-get install -y software-properties-common sudo add-apt-repository ppa:deadsnakes/ppa sudo apt-get update - sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y + sudo apt-get install -y python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git - name: Run install script env: From 4a4b14b78b17773566cf4afd21fca98daa2981e3 Mon Sep 17 00:00:00 2001 From: dustinface <35775977+xdustinface@users.noreply.github.com> Date: Tue, 5 Apr 2022 03:43:51 +0200 Subject: [PATCH 319/378] github: Drop unused `BUILD_VDF_CLIENT` variables (#11050) From my understanding this is only used by `chiavdf` source builds which happen only if `install-timelord.sh` gets called but it doesn't in the addressed cases. --- .github/workflows/build-linux-arm64-installer.yml | 1 - .github/workflows/build-linux-installer-deb.yml | 1 - .github/workflows/build-linux-installer-rpm.yml | 1 - .github/workflows/build-macos-installer.yml | 1 - .github/workflows/build-macos-m1-installer.yml | 1 - .github/workflows/test-install-scripts.yml | 2 -- 6 files changed, 7 deletions(-) diff --git a/.github/workflows/build-linux-arm64-installer.yml b/.github/workflows/build-linux-arm64-installer.yml index fad14fa525cd..6b05a2560e40 100644 --- a/.github/workflows/build-linux-arm64-installer.yml +++ b/.github/workflows/build-linux-arm64-installer.yml @@ -105,7 +105,6 @@ jobs: - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} - BUILD_VDF_CLIENT: "N" run: | sh install.sh diff --git a/.github/workflows/build-linux-installer-deb.yml b/.github/workflows/build-linux-installer-deb.yml index 5734d6204f64..e9690e8bfadf 100644 --- a/.github/workflows/build-linux-installer-deb.yml +++ b/.github/workflows/build-linux-installer-deb.yml @@ -139,7 +139,6 @@ jobs: - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} - BUILD_VDF_CLIENT: "N" run: | sh install.sh diff --git a/.github/workflows/build-linux-installer-rpm.yml b/.github/workflows/build-linux-installer-rpm.yml index 5564b472e503..aa9c85ed835c 100644 --- a/.github/workflows/build-linux-installer-rpm.yml +++ b/.github/workflows/build-linux-installer-rpm.yml @@ -108,7 +108,6 @@ jobs: - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} - BUILD_VDF_CLIENT: "N" run: | sh install.sh diff --git a/.github/workflows/build-macos-installer.yml b/.github/workflows/build-macos-installer.yml index f9d3fc3faeec..da60f6f20ab4 100644 --- a/.github/workflows/build-macos-installer.yml +++ b/.github/workflows/build-macos-installer.yml @@ -127,7 +127,6 @@ jobs: - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} - BUILD_VDF_CLIENT: "N" run: | sh install.sh diff --git a/.github/workflows/build-macos-m1-installer.yml b/.github/workflows/build-macos-m1-installer.yml index 7a178382e303..18d08db16d89 100644 --- a/.github/workflows/build-macos-m1-installer.yml +++ b/.github/workflows/build-macos-m1-installer.yml @@ -101,7 +101,6 @@ jobs: - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} - BUILD_VDF_CLIENT: "N" run: | arch -arm64 sh install.sh diff --git a/.github/workflows/test-install-scripts.yml b/.github/workflows/test-install-scripts.yml index 90917ac0f4ef..9b2a7e859673 100644 --- a/.github/workflows/test-install-scripts.yml +++ b/.github/workflows/test-install-scripts.yml @@ -40,7 +40,6 @@ jobs: - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} - BUILD_VDF_CLIENT: "N" run: sh install.sh - name: Run install-gui script @@ -188,7 +187,6 @@ jobs: - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} - BUILD_VDF_CLIENT: "N" run: sh install.sh -a - name: Run chia --help From 261f5baa6ffecb62130e4118f01dd0329378c076 Mon Sep 17 00:00:00 2001 From: wjblanke Date: Mon, 4 Apr 2022 23:35:28 -0700 Subject: [PATCH 320/378] bump up to 2.1.7 to fix inotify issue resolved by 848 (#11042) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2c55afe27838..c746a4cf26ec 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ # TODO: when moving to click 8 remove the pinning of black noted below "click==7.1.2", # For the CLI "dnspythonchia==2.2.0", # Query DNS seeds - "watchdog==2.1.6", # Filesystem event watching - watches keyring.yaml + "watchdog==2.1.7", # Filesystem event watching - watches keyring.yaml "dnslib==0.9.17", # dns lib "typing-extensions==4.0.1", # typing backports like Protocol and TypedDict "zstd==1.5.0.4", From b377339372bc95241b40d4e513e61cf4a9f6db2f Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Tue, 5 Apr 2022 17:53:12 +0200 Subject: [PATCH 321/378] fix memory leak in test_full_sync (#11004) --- tools/test_full_sync.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/test_full_sync.py b/tools/test_full_sync.py index e28a9d2f1fdd..f8956474790b 100755 --- a/tools/test_full_sync.py +++ b/tools/test_full_sync.py @@ -103,6 +103,8 @@ async def run_sync_test(file: Path, db_version, profile: bool, single_thread: bo success, advanced_peak, fork_height, coin_changes = await full_node.receive_block_batch( block_batch, None, None # type: ignore[arg-type] ) + end_height = block_batch[-1].height + full_node.blockchain.clean_block_record(end_height - full_node.constants.BLOCKS_CACHE_SIZE) assert success assert advanced_peak From 9311e58ac4d6c2e7561088f4f66c5352f1347a94 Mon Sep 17 00:00:00 2001 From: dustinface <35775977+xdustinface@users.noreply.github.com> Date: Tue, 5 Apr 2022 17:53:48 +0200 Subject: [PATCH 322/378] full_node: Drop unused `MempoolManager.constants_json` (#11046) --- chia/full_node/mempool_manager.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/chia/full_node/mempool_manager.py b/chia/full_node/mempool_manager.py index f1a4ba514bc1..821e2aa1a370 100644 --- a/chia/full_node/mempool_manager.py +++ b/chia/full_node/mempool_manager.py @@ -1,6 +1,5 @@ import asyncio import collections -import dataclasses import logging from concurrent.futures import Executor from multiprocessing.context import BaseContext @@ -34,7 +33,6 @@ from chia.util.ints import uint32, uint64 from chia.util.lru_cache import LRUCache from chia.util.setproctitle import getproctitle, setproctitle -from chia.util.streamable import recurse_jsonify from chia.full_node.mempool_check_conditions import mempool_check_time_locks log = logging.getLogger(__name__) @@ -91,7 +89,6 @@ def __init__( single_threaded: bool = False, ): self.constants: ConsensusConstants = consensus_constants - self.constants_json = recurse_jsonify(dataclasses.asdict(self.constants)) # Keep track of seen spend_bundles self.seen_bundle_hashes: Dict[bytes32, bytes32] = {} From d87b8ac08766c9cbb4f4577a4819bb592f63123c Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Tue, 5 Apr 2022 11:54:38 -0400 Subject: [PATCH 323/378] simplify some header hash getting and assertions (#11007) --- chia/consensus/blockchain.py | 5 ++--- chia/full_node/full_node_api.py | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/chia/consensus/blockchain.py b/chia/consensus/blockchain.py index f436572bc752..258537d01c62 100644 --- a/chia/consensus/blockchain.py +++ b/chia/consensus/blockchain.py @@ -752,9 +752,8 @@ async def get_header_blocks_in_range( ) -> Dict[bytes32, HeaderBlock]: hashes = [] for height in range(start, stop + 1): - if self.contains_height(uint32(height)): - header_hash: Optional[bytes32] = self.height_to_hash(uint32(height)) - assert header_hash is not None + header_hash: Optional[bytes32] = self.height_to_hash(uint32(height)) + if header_hash is not None: hashes.append(header_hash) blocks: List[FullBlock] = [] diff --git a/chia/full_node/full_node_api.py b/chia/full_node/full_node_api.py index ee60e44657d4..ff91b11858ba 100644 --- a/chia/full_node/full_node_api.py +++ b/chia/full_node/full_node_api.py @@ -1320,12 +1320,11 @@ async def request_header_blocks(self, request: wallet_protocol.RequestHeaderBloc header_hashes: List[bytes32] = [] for i in range(request.start_height, request.end_height + 1): - if not self.full_node.blockchain.contains_height(uint32(i)): + header_hash: Optional[bytes32] = self.full_node.blockchain.height_to_hash(uint32(i)) + if header_hash is None: reject = RejectHeaderBlocks(request.start_height, request.end_height) msg = make_msg(ProtocolMessageTypes.reject_header_blocks, reject) return msg - header_hash: Optional[bytes32] = self.full_node.blockchain.height_to_hash(uint32(i)) - assert header_hash is not None header_hashes.append(header_hash) blocks: List[FullBlock] = await self.full_node.block_store.get_blocks_by_hash(header_hashes) From 9b7d7d2555d3728f178ec81bdae3a7d138942389 Mon Sep 17 00:00:00 2001 From: Jack Nelson Date: Tue, 5 Apr 2022 13:19:09 -0400 Subject: [PATCH 324/378] Remove websockets dependency & do some refactoring (#10611) * remove old ws --- chia/daemon/client.py | 66 +++++++--- chia/daemon/server.py | 220 +++++++++++----------------------- chia/farmer/farmer.py | 3 + chia/rpc/rpc_server.py | 70 +++++------ chia/server/server.py | 6 +- chia/wallet/wallet_node.py | 43 +++---- setup.py | 1 - tests/core/test_daemon_rpc.py | 4 +- tests/setup_nodes.py | 19 ++- tests/setup_services.py | 6 +- 10 files changed, 198 insertions(+), 240 deletions(-) diff --git a/chia/daemon/client.py b/chia/daemon/client.py index c8ca93019a7c..88677d689084 100644 --- a/chia/daemon/client.py +++ b/chia/daemon/client.py @@ -5,7 +5,7 @@ from pathlib import Path from typing import Any, Dict, Optional -import websockets +import aiohttp from chia.util.config import load_config from chia.util.json_util import dict_to_json_str @@ -13,31 +13,52 @@ class DaemonProxy: - def __init__(self, uri: str, ssl_context: Optional[ssl.SSLContext]): + def __init__( + self, + uri: str, + ssl_context: Optional[ssl.SSLContext], + max_message_size: Optional[int] = 50 * 1000 * 1000, + ): self._uri = uri self._request_dict: Dict[str, asyncio.Event] = {} self.response_dict: Dict[str, Any] = {} self.ssl_context = ssl_context + self.client_session: Optional[aiohttp.ClientSession] = None + self.websocket: Optional[aiohttp.ClientWebSocketResponse] = None + self.max_message_size = max_message_size def format_request(self, command: str, data: Dict[str, Any]) -> WsRpcMessage: request = create_payload_dict(command, data, "client", "daemon") return request async def start(self): - self.websocket = await websockets.connect(self._uri, max_size=None, ssl=self.ssl_context) + try: + self.client_session = aiohttp.ClientSession() + self.websocket = await self.client_session.ws_connect( + self._uri, + autoclose=True, + autoping=True, + heartbeat=60, + ssl_context=self.ssl_context, + max_msg_size=self.max_message_size, + ) + except Exception: + await self.close() + raise async def listener(): while True: - try: - message = await self.websocket.recv() - except websockets.exceptions.ConnectionClosedOK: + message = await self.websocket.receive() + if message.type == aiohttp.WSMsgType.TEXT: + decoded = json.loads(message.data) + request_id = decoded["request_id"] + + if request_id in self._request_dict: + self.response_dict[request_id] = decoded + self._request_dict[request_id].set() + else: + await self.close() return None - decoded = json.loads(message) - id = decoded["request_id"] - - if id in self._request_dict: - self.response_dict[id] = decoded - self._request_dict[id].set() asyncio.create_task(listener()) await asyncio.sleep(1) @@ -46,7 +67,9 @@ async def _get(self, request: WsRpcMessage) -> WsRpcMessage: request_id = request["request_id"] self._request_dict[request_id] = asyncio.Event() string = dict_to_json_str(request) - asyncio.create_task(self.websocket.send(string)) + if self.websocket is None: + raise Exception("Websocket is not connected") + asyncio.create_task(self.websocket.send_str(string)) async def timeout(): await asyncio.sleep(30) @@ -117,19 +140,24 @@ async def ping(self) -> WsRpcMessage: return response async def close(self) -> None: - await self.websocket.close() + if self.websocket is not None: + await self.websocket.close() + if self.client_session is not None: + await self.client_session.close() async def exit(self) -> WsRpcMessage: request = self.format_request("exit", {}) return await self._get(request) -async def connect_to_daemon(self_hostname: str, daemon_port: int, ssl_context: ssl.SSLContext) -> DaemonProxy: +async def connect_to_daemon( + self_hostname: str, daemon_port: int, max_message_size: int, ssl_context: ssl.SSLContext +) -> DaemonProxy: """ Connect to the local daemon. """ - client = DaemonProxy(f"wss://{self_hostname}:{daemon_port}", ssl_context) + client = DaemonProxy(f"wss://{self_hostname}:{daemon_port}", ssl_context, max_message_size) await client.start() return client @@ -143,12 +171,15 @@ async def connect_to_daemon_and_validate(root_path: Path, quiet: bool = False) - try: net_config = load_config(root_path, "config.yaml") + daemon_max_message_size = net_config.get("daemon_max_message_size", 50 * 1000 * 1000) crt_path = root_path / net_config["daemon_ssl"]["private_crt"] key_path = root_path / net_config["daemon_ssl"]["private_key"] ca_crt_path = root_path / net_config["private_ssl_ca"]["crt"] ca_key_path = root_path / net_config["private_ssl_ca"]["key"] ssl_context = ssl_context_for_client(ca_crt_path, ca_key_path, crt_path, key_path) - connection = await connect_to_daemon(net_config["self_hostname"], net_config["daemon_port"], ssl_context) + connection = await connect_to_daemon( + net_config["self_hostname"], net_config["daemon_port"], daemon_max_message_size, ssl_context + ) r = await connection.ping() if "value" in r["data"] and r["data"]["value"] == "pong": @@ -168,7 +199,6 @@ async def acquire_connection_to_daemon(root_path: Path, quiet: bool = False): block exits scope, execution resumes in this function, wherein the connection is closed. """ - from chia.daemon.client import connect_to_daemon_and_validate daemon: Optional[DaemonProxy] = None try: diff --git a/chia/daemon/server.py b/chia/daemon/server.py index c1216c3f12dd..5281279dbe2d 100644 --- a/chia/daemon/server.py +++ b/chia/daemon/server.py @@ -14,8 +14,7 @@ from pathlib import Path from typing import Any, Dict, List, Optional, TextIO, Tuple, cast -from websockets import ConnectionClosedOK, WebSocketException, WebSocketServerProtocol, serve - +from chia import __version__ from chia.cmds.init_funcs import check_keys, chia_init from chia.cmds.passphrase_funcs import default_passphrase, using_default_passphrase from chia.daemon.keychain_server import KeychainServer, keychain_commands @@ -39,12 +38,12 @@ from chia.util.service_groups import validate_service from chia.util.setproctitle import setproctitle from chia.util.ws_message import WsRpcMessage, create_payload, format_response -from chia import __version__ io_pool_exc = ThreadPoolExecutor() try: - from aiohttp import ClientSession, web + from aiohttp import ClientSession, WSMsgType, web + from aiohttp.web_ws import WebSocketResponse except ModuleNotFoundError: print("Error: Make sure to run . ./activate from the project folder before starting Chia.") quit() @@ -134,24 +133,24 @@ def __init__( ca_key_path: Path, crt_path: Path, key_path: Path, + shutdown_event: asyncio.Event, run_check_keys_on_unlock: bool = False, ): self.root_path = root_path self.log = log self.services: Dict = dict() self.plots_queue: List[Dict] = [] - self.connections: Dict[str, List[WebSocketServerProtocol]] = dict() # service_name : [WebSocket] - self.remote_address_map: Dict[WebSocketServerProtocol, str] = dict() # socket: service_name - self.ping_job: Optional[asyncio.Task] = None + self.connections: Dict[str, List[WebSocketResponse]] = dict() # service_name : [WebSocket] + self.remote_address_map: Dict[WebSocketResponse, str] = dict() # socket: service_name self.net_config = load_config(root_path, "config.yaml") self.self_hostname = self.net_config["self_hostname"] self.daemon_port = self.net_config["daemon_port"] self.daemon_max_message_size = self.net_config.get("daemon_max_message_size", 50 * 1000 * 1000) - self.websocket_server = None + self.websocket_runner: Optional[web.AppRunner] = None self.ssl_context = ssl_context_for_server(ca_crt_path, ca_key_path, crt_path, key_path, log=self.log) - self.shut_down = False self.keychain_server = KeychainServer() self.run_check_keys_on_unlock = run_check_keys_on_unlock + self.shutdown_event = shutdown_event async def start(self): self.log.info("Starting Daemon Server") @@ -184,16 +183,19 @@ def master_close_cb(): except NotImplementedError: self.log.info("Not implemented") - self.websocket_server = await serve( - self.safe_handle, - self.self_hostname, - self.daemon_port, - max_size=self.daemon_max_message_size, - ping_interval=500, - ping_timeout=300, - ssl=self.ssl_context, + app = web.Application(client_max_size=self.daemon_max_message_size) + app.add_routes([web.get("/", self.incoming_connection)]) + self.websocket_runner = web.AppRunner(app, access_log=None, logger=self.log, keepalive_timeout=300) + await self.websocket_runner.setup() + + site = web.TCPSite( + self.websocket_runner, + host=self.self_hostname, + port=self.daemon_port, + shutdown_timeout=3, + ssl_context=self.ssl_context, ) - self.log.info("Waiting Daemon WebSocketServer closure") + await site.start() def cancel_task_safe(self, task: Optional[asyncio.Task]): if task is not None: @@ -203,22 +205,28 @@ def cancel_task_safe(self, task: Optional[asyncio.Task]): self.log.error(f"Error while canceling task.{e} {task}") async def stop(self) -> Dict[str, Any]: - self.shut_down = True - self.cancel_task_safe(self.ping_job) - await self.exit() - if self.websocket_server is not None: - self.websocket_server.close() + jobs = [] + for service_name in self.services.keys(): + jobs.append(kill_service(self.root_path, self.services, service_name)) + if jobs: + await asyncio.wait(jobs) + self.services.clear() + asyncio.create_task(self.exit()) return {"success": True} - async def safe_handle(self, websocket: WebSocketServerProtocol, path: str): - service_name = "" - try: - async for message in websocket: + async def incoming_connection(self, request): + ws: WebSocketResponse = web.WebSocketResponse(max_msg_size=self.daemon_max_message_size, heartbeat=30) + await ws.prepare(request) + + while True: + msg = await ws.receive() + self.log.debug(f"Received message: {msg}") + if msg.type == WSMsgType.TEXT: try: - decoded = json.loads(message) + decoded = json.loads(msg.data) if "data" not in decoded: decoded["data"] = {} - response, sockets_to_use = await self.handle_message(websocket, decoded) + response, sockets_to_use = await self.handle_message(ws, decoded) except Exception as e: tb = traceback.format_exc() self.log.error(f"Error while handling message: {tb}") @@ -228,28 +236,27 @@ async def safe_handle(self, websocket: WebSocketServerProtocol, path: str): if len(sockets_to_use) > 0: for socket in sockets_to_use: try: - await socket.send(response) + await socket.send_str(response) except Exception as e: tb = traceback.format_exc() self.log.error(f"Unexpected exception trying to send to websocket: {e} {tb}") self.remove_connection(socket) await socket.close() - except Exception as e: - tb = traceback.format_exc() - service_name = "Unknown" - if websocket in self.remote_address_map: - service_name = self.remote_address_map[websocket] - if isinstance(e, ConnectionClosedOK): - self.log.info(f"ConnectionClosedOk. Closing websocket with {service_name} {e}") - elif isinstance(e, WebSocketException): - self.log.info(f"Websocket exception. Closing websocket with {service_name} {e} {tb}") + break else: - self.log.error(f"Unexpected exception in websocket: {e} {tb}") - finally: - self.remove_connection(websocket) - await websocket.close() + service_name = "Unknown" + if ws in self.remote_address_map: + service_name = self.remote_address_map[ws] + if msg.type == WSMsgType.CLOSE: + self.log.info(f"ConnectionClosed. Closing websocket with {service_name}") + elif msg.type == WSMsgType.ERROR: + self.log.info(f"Websocket exception. Closing websocket with {service_name}. {ws.exception()}") + + self.remove_connection(ws) + await ws.close() + break - def remove_connection(self, websocket: WebSocketServerProtocol): + def remove_connection(self, websocket: WebSocketResponse): service_name = None if websocket in self.remote_address_map: service_name = self.remote_address_map[websocket] @@ -263,31 +270,8 @@ def remove_connection(self, websocket: WebSocketServerProtocol): after_removal.append(connection) self.connections[service_name] = after_removal - async def ping_task(self) -> None: - restart = True - await asyncio.sleep(30) - for remote_address, service_name in self.remote_address_map.items(): - if service_name in self.connections: - sockets = self.connections[service_name] - for socket in sockets: - if socket.remote_address[1] == remote_address: - try: - self.log.info(f"About to ping: {service_name}") - await socket.ping() - except asyncio.CancelledError: - self.log.info("Ping task received Cancel") - restart = False - break - except Exception as e: - self.log.info(f"Ping error: {e}") - self.log.warning("Ping failed, connection closed.") - self.remove_connection(socket) - await socket.close() - if restart is True: - self.ping_job = asyncio.create_task(self.ping_task()) - async def handle_message( - self, websocket: WebSocketServerProtocol, message: WsRpcMessage + self, websocket: WebSocketResponse, message: WsRpcMessage ) -> Tuple[Optional[str], List[Any]]: """ This function gets called when new message is received via websocket. @@ -631,7 +615,7 @@ async def _keyring_status_changed(self, keyring_status: Dict[str, Any], destinat for websocket in websockets: try: - await websocket.send(response) + await websocket.send_str(response) except Exception as e: tb = traceback.format_exc() self.log.error(f"Unexpected exception trying to send to websocket: {e} {tb}") @@ -690,7 +674,7 @@ async def _state_changed(self, service: str, message: Dict[str, Any]): for websocket in websockets: try: - await websocket.send(response) + await websocket.send_str(response) except Exception as e: tb = traceback.format_exc() self.log.error(f"Unexpected exception trying to send to websocket: {e} {tb}") @@ -1150,20 +1134,13 @@ async def is_running(self, request: Dict[str, Any]) -> Dict[str, Any]: return response - async def exit(self) -> Dict[str, Any]: - jobs = [] - for k in self.services.keys(): - jobs.append(kill_service(self.root_path, self.services, k)) - if jobs: - await asyncio.wait(jobs) - self.services.clear() - + async def exit(self) -> None: + if self.websocket_runner is not None: + await self.websocket_runner.cleanup() + self.shutdown_event.set() log.info("chia daemon exiting") - response = {"success": True} - return response - - async def register_service(self, websocket: WebSocketServerProtocol, request: Dict[str, Any]) -> Dict[str, Any]: + async def register_service(self, websocket: WebSocketResponse, request: Dict[str, Any]) -> Dict[str, Any]: self.log.info(f"Register service {request}") service = request["service"] if service not in self.connections: @@ -1179,8 +1156,6 @@ async def register_service(self, websocket: WebSocketServerProtocol, request: Di } else: self.remote_address_map[websocket] = service - if self.ping_job is None: - self.ping_job = asyncio.create_task(self.ping_task()) self.log.info(f"registered for service {service}") log.info(f"{response}") return response @@ -1351,7 +1326,6 @@ async def kill_service( if process is None: return False del services[service_name] - result = await kill_process(process, root_path, service_name, "", delay_before_kill) return result @@ -1361,68 +1335,6 @@ def is_running(services: Dict[str, subprocess.Popen], service_name: str) -> bool return process is not None and process.poll() is None -def create_server_for_daemon(root_path: Path): - routes = web.RouteTableDef() - - services: Dict = dict() - - @routes.get("/daemon/ping/") - async def ping(request: web.Request) -> web.Response: - return web.Response(text="pong") - - @routes.get("/daemon/service/start/") - async def start_service(request: web.Request) -> web.Response: - service_name = request.query.get("service") - if service_name is None or not validate_service(service_name): - r = f"{service_name} unknown service" - return web.Response(text=str(r)) - - if is_running(services, service_name): - r = f"{service_name} already running" - return web.Response(text=str(r)) - - try: - process, pid_path = launch_service(root_path, service_name) - services[service_name] = process - r = f"{service_name} started" - except (subprocess.SubprocessError, IOError): - log.exception(f"problem starting {service_name}") - r = f"{service_name} start failed" - - return web.Response(text=str(r)) - - @routes.get("/daemon/service/stop/") - async def stop_service(request: web.Request) -> web.Response: - service_name = request.query.get("service") - if service_name is None: - r = f"{service_name} unknown service" - return web.Response(text=str(r)) - r = str(await kill_service(root_path, services, service_name)) - return web.Response(text=str(r)) - - @routes.get("/daemon/service/is_running/") - async def is_running_handler(request: web.Request) -> web.Response: - service_name = request.query.get("service") - if service_name is None: - r = f"{service_name} unknown service" - return web.Response(text=str(r)) - - r = str(is_running(services, service_name)) - return web.Response(text=str(r)) - - @routes.get("/daemon/exit/") - async def exit(request: web.Request): - jobs = [] - for k in services.keys(): - jobs.append(kill_service(root_path, services, k)) - if jobs: - await asyncio.wait(jobs) - services.clear() - - # we can't await `site.stop()` here because that will cause a deadlock, waiting for this - # request to exit - - def singleton(lockfile: Path, text: str = "semaphore") -> Optional[TextIO]: """ Open a lockfile exclusively. @@ -1474,14 +1386,20 @@ async def async_run_daemon(root_path: Path, wait_for_unlock: bool = False) -> in print("daemon: already launching") return 2 + shutdown_event = asyncio.Event() + # TODO: clean this up, ensuring lockfile isn't removed until the listen port is open - create_server_for_daemon(root_path) ws_server = WebSocketServer( - root_path, ca_crt_path, ca_key_path, crt_path, key_path, run_check_keys_on_unlock=wait_for_unlock + root_path, + ca_crt_path, + ca_key_path, + crt_path, + key_path, + shutdown_event, + run_check_keys_on_unlock=wait_for_unlock, ) await ws_server.start() - assert ws_server.websocket_server is not None - await ws_server.websocket_server.wait_closed() + await shutdown_event.wait() log.info("Daemon WebSocketServer closed") # sys.stdout.close() return 0 diff --git a/chia/farmer/farmer.py b/chia/farmer/farmer.py index 3bb68a6fa524..99799258596f 100644 --- a/chia/farmer/farmer.py +++ b/chia/farmer/farmer.py @@ -217,6 +217,9 @@ async def _await_closed(self): await self.cache_clear_task if self.update_pool_state_task is not None: await self.update_pool_state_task + if self.keychain_proxy is not None: + await self.keychain_proxy.close() + await asyncio.sleep(0.5) # https://docs.aiohttp.org/en/stable/client_advanced.html#graceful-shutdown self.started = False def _set_state_changed_callback(self, callback: Callable): diff --git a/chia/rpc/rpc_server.py b/chia/rpc/rpc_server.py index dbabcea5fa38..4abf16969275 100644 --- a/chia/rpc/rpc_server.py +++ b/chia/rpc/rpc_server.py @@ -5,7 +5,7 @@ from pathlib import Path from typing import Any, Callable, Dict, List, Optional -import aiohttp +from aiohttp import ClientConnectorError, ClientSession, ClientWebSocketResponse, WSMsgType, web from chia.rpc.util import wrap_http_handler from chia.server.outbound_message import NodeType @@ -17,6 +17,7 @@ from chia.util.ws_message import create_payload, create_payload_dict, format_response, pong log = logging.getLogger(__name__) +max_message_size = 50 * 1024 * 1024 # 50MB class RpcServer: @@ -29,7 +30,8 @@ def __init__(self, rpc_api: Any, service_name: str, stop_cb: Callable, root_path self.stop_cb: Callable = stop_cb self.log = log self.shut_down = False - self.websocket: Optional[aiohttp.ClientWebSocketResponse] = None + self.websocket: Optional[ClientWebSocketResponse] = None + self.client_session: Optional[ClientSession] = None self.service_name = service_name self.root_path = root_path self.net_config = net_config @@ -45,6 +47,8 @@ async def stop(self): self.shut_down = True if self.websocket is not None: await self.websocket.close() + if self.client_session is not None: + await self.client_session.close() async def _state_changed(self, *args): if self.websocket is None: @@ -168,7 +172,7 @@ async def open_connection(self, request: Dict): async def close_connection(self, request: Dict): node_id = hexstr_to_bytes(request["node_id"]) if self.rpc_api.service.server is None: - raise aiohttp.web.HTTPInternalServerError() + raise web.HTTPInternalServerError() connections_to_close = [c for c in self.rpc_api.service.server.get_connections() if c.peer_node_id == node_id] if len(connections_to_close) == 0: raise ValueError(f"Connection with node_id {node_id.hex()} does not exist") @@ -243,52 +247,52 @@ async def connection(self, ws): while True: msg = await ws.receive() - if msg.type == aiohttp.WSMsgType.TEXT: + if msg.type == WSMsgType.TEXT: message = msg.data.strip() # self.log.info(f"received message: {message}") await self.safe_handle(ws, message) - elif msg.type == aiohttp.WSMsgType.BINARY: + elif msg.type == WSMsgType.BINARY: self.log.debug("Received binary data") - elif msg.type == aiohttp.WSMsgType.PING: + elif msg.type == WSMsgType.PING: self.log.debug("Ping received") await ws.pong() - elif msg.type == aiohttp.WSMsgType.PONG: + elif msg.type == WSMsgType.PONG: self.log.debug("Pong received") else: - if msg.type == aiohttp.WSMsgType.CLOSE: + if msg.type == WSMsgType.CLOSE: self.log.debug("Closing RPC websocket") await ws.close() - elif msg.type == aiohttp.WSMsgType.ERROR: + elif msg.type == WSMsgType.ERROR: self.log.error("Error during receive %s" % ws.exception()) - elif msg.type == aiohttp.WSMsgType.CLOSED: + elif msg.type == WSMsgType.CLOSED: pass break - await ws.close() - async def connect_to_daemon(self, self_hostname: str, daemon_port: uint16): - while True: + while not self.shut_down: try: - if self.shut_down: - break - async with aiohttp.ClientSession() as session: - async with session.ws_connect( - f"wss://{self_hostname}:{daemon_port}", - autoclose=True, - autoping=True, - heartbeat=60, - ssl_context=self.ssl_context, - max_msg_size=100 * 1024 * 1024, - ) as ws: - self.websocket = ws - await self.connection(ws) - self.websocket = None - except aiohttp.ClientConnectorError: + self.client_session = ClientSession() + self.websocket = await self.client_session.ws_connect( + f"wss://{self_hostname}:{daemon_port}", + autoclose=True, + autoping=True, + heartbeat=60, + ssl_context=self.ssl_context, + max_msg_size=max_message_size, + ) + await self.connection(self.websocket) + except ClientConnectorError: self.log.warning(f"Cannot connect to daemon at ws://{self_hostname}:{daemon_port}") except Exception as e: tb = traceback.format_exc() self.log.warning(f"Exception: {tb} {type(e)}") + if self.websocket is not None: + await self.websocket.close() + if self.client_session is not None: + await self.client_session.close() + self.websocket = None + self.client_session = None await asyncio.sleep(2) @@ -306,17 +310,15 @@ async def start_rpc_server( Starts an HTTP server with the following RPC methods, to be used by local clients to query the node. """ - app = aiohttp.web.Application() + app = web.Application() rpc_server = RpcServer(rpc_api, rpc_api.service_name, stop_cb, root_path, net_config) rpc_server.rpc_api.service._set_state_changed_callback(rpc_server.state_changed) - app.add_routes( - [aiohttp.web.post(route, wrap_http_handler(func)) for (route, func) in rpc_server.get_routes().items()] - ) + app.add_routes([web.post(route, wrap_http_handler(func)) for (route, func) in rpc_server.get_routes().items()]) if connect_to_daemon: daemon_connection = asyncio.create_task(rpc_server.connect_to_daemon(self_hostname, daemon_port)) - runner = aiohttp.web.AppRunner(app, access_log=None) + runner = web.AppRunner(app, access_log=None) await runner.setup() - site = aiohttp.web.TCPSite(runner, self_hostname, int(rpc_port), ssl_context=rpc_server.ssl_context) + site = web.TCPSite(runner, self_hostname, int(rpc_port), ssl_context=rpc_server.ssl_context) await site.start() async def cleanup(): diff --git a/chia/server/server.py b/chia/server/server.py index 948985165c97..9edac98fc118 100644 --- a/chia/server/server.py +++ b/chia/server/server.py @@ -33,6 +33,8 @@ from chia.util.network import is_in_network, is_localhost from chia.util.ssl_check import verify_ssl_certs_and_keys +max_message_size = 50 * 1024 * 1024 # 50MB + def ssl_context_for_server( ca_cert: Path, @@ -277,7 +279,7 @@ async def incoming_connection(self, request): if request.remote in self.banned_peers and time.time() < self.banned_peers[request.remote]: self.log.warning(f"Peer {request.remote} is banned, refusing connection") return None - ws = web.WebSocketResponse(max_msg_size=50 * 1024 * 1024) + ws = web.WebSocketResponse(max_msg_size=max_message_size) await ws.prepare(request) close_event = asyncio.Event() cert_bytes = request.transport._ssl_protocol._extra["ssl_object"].getpeercert(True) @@ -422,7 +424,7 @@ async def start_client( self.log.debug(f"Connecting: {url}, Peer info: {target_node}") try: ws = await session.ws_connect( - url, autoclose=True, autoping=True, heartbeat=60, ssl=ssl_context, max_msg_size=50 * 1024 * 1024 + url, autoclose=True, autoping=True, heartbeat=60, ssl=ssl_context, max_msg_size=max_message_size ) except ServerDisconnectedError: self.log.debug(f"Server disconnected error connecting to {url}. Perhaps we are banned by the peer.") diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index 0ad91aa38398..6b942ee5f10b 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -6,33 +6,32 @@ import traceback from asyncio import CancelledError from pathlib import Path -from typing import Callable, Dict, List, Optional, Set, Tuple, Any, Iterator +from typing import Any, Callable, Dict, Iterator, List, Optional, Set, Tuple -from blspy import PrivateKey, AugSchemeMPL +from blspy import AugSchemeMPL, PrivateKey from packaging.version import Version from chia.consensus.block_record import BlockRecord from chia.consensus.blockchain import ReceiveBlockResult from chia.consensus.constants import ConsensusConstants from chia.daemon.keychain_proxy import ( + KeychainProxy, KeychainProxyConnectionFailure, + KeyringIsEmpty, connect_to_keychain_and_validate, wrap_local_keychain, - KeychainProxy, - KeyringIsEmpty, ) -from chia.util.chunks import chunks from chia.protocols import wallet_protocol from chia.protocols.full_node_protocol import RequestProofOfWeight, RespondProofOfWeight from chia.protocols.protocol_message_types import ProtocolMessageTypes from chia.protocols.wallet_protocol import ( - RespondToCoinUpdates, CoinState, - RespondToPhUpdates, - RespondBlockHeader, + RequestHeaderBlocks, RequestSESInfo, + RespondBlockHeader, RespondSESInfo, - RequestHeaderBlocks, + RespondToCoinUpdates, + RespondToPhUpdates, ) from chia.server.node_discovery import WalletPeers from chia.server.outbound_message import Message, NodeType, make_msg @@ -46,29 +45,30 @@ from chia.types.header_block import HeaderBlock from chia.types.mempool_inclusion_status import MempoolInclusionStatus from chia.types.peer_info import PeerInfo -from chia.types.weight_proof import WeightProof, SubEpochData +from chia.types.weight_proof import SubEpochData, WeightProof from chia.util.byte_types import hexstr_to_bytes +from chia.util.chunks import chunks from chia.util.config import WALLET_PEERS_PATH_KEY_DEPRECATED from chia.util.default_root import STANDALONE_ROOT_PATH from chia.util.ints import uint32, uint64 -from chia.util.keychain import KeyringIsLocked, Keychain +from chia.util.keychain import Keychain, KeyringIsLocked from chia.util.path import mkdir, path_from_root -from chia.wallet.util.new_peak_queue import NewPeakQueue, NewPeakQueueTypes, NewPeakItem +from chia.util.profiler import profile_task +from chia.wallet.transaction_record import TransactionRecord +from chia.wallet.util.new_peak_queue import NewPeakItem, NewPeakQueue, NewPeakQueueTypes from chia.wallet.util.peer_request_cache import PeerRequestCache, can_use_peer_request_cache from chia.wallet.util.wallet_sync_utils import ( - request_and_validate_removals, - request_and_validate_additions, + fetch_header_blocks_in_range, fetch_last_tx_from_peer, - subscribe_to_phs, - subscribe_to_coin_updates, last_change_height_cs, - fetch_header_blocks_in_range, + request_and_validate_additions, + request_and_validate_removals, + subscribe_to_coin_updates, + subscribe_to_phs, ) +from chia.wallet.wallet_action import WalletAction from chia.wallet.wallet_coin_record import WalletCoinRecord from chia.wallet.wallet_state_manager import WalletStateManager -from chia.wallet.transaction_record import TransactionRecord -from chia.wallet.wallet_action import WalletAction -from chia.util.profiler import profile_task class WalletNode: @@ -269,6 +269,9 @@ async def _await_closed(self): if self.wallet_state_manager is not None: await self.wallet_state_manager._await_closed() self.wallet_state_manager = None + if self.keychain_proxy is not None: + await self.keychain_proxy.close() + await asyncio.sleep(0.5) # https://docs.aiohttp.org/en/stable/client_advanced.html#graceful-shutdown self.logged_in = False self.wallet_peers = None diff --git a/setup.py b/setup.py index c746a4cf26ec..df9ef8b411b2 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,6 @@ "PyYAML==5.4.1", # Used for config file format "setproctitle==1.2.2", # Gives the chia processes readable names "sortedcontainers==2.4.0", # For maintaining sorted mempools - "websockets==8.1.0", # For use in wallet RPC and electron UI # TODO: when moving to click 8 remove the pinning of black noted below "click==7.1.2", # For the CLI "dnspythonchia==2.2.0", # Query DNS seeds diff --git a/tests/core/test_daemon_rpc.py b/tests/core/test_daemon_rpc.py index 5cf5780eae58..cec32ef33e44 100644 --- a/tests/core/test_daemon_rpc.py +++ b/tests/core/test_daemon_rpc.py @@ -9,7 +9,9 @@ class TestDaemonRpc: async def test_get_version_rpc(self, get_daemon, bt): ws_server = get_daemon config = bt.config - client = await connect_to_daemon(config["self_hostname"], config["daemon_port"], bt.get_daemon_ssl_context()) + client = await connect_to_daemon( + config["self_hostname"], config["daemon_port"], 50 * 1000 * 1000, bt.get_daemon_ssl_context() + ) response = await client.get_version() assert response["data"]["success"] diff --git a/tests/setup_nodes.py b/tests/setup_nodes.py index abcdc04706b1..4cfb701d9c14 100644 --- a/tests/setup_nodes.py +++ b/tests/setup_nodes.py @@ -1,6 +1,5 @@ -import logging import asyncio - +import logging from secrets import token_bytes from typing import Dict, List @@ -8,23 +7,23 @@ from chia.full_node.full_node_api import FullNodeAPI from chia.server.start_service import Service from chia.server.start_wallet import service_kwargs_for_wallet -from tests.block_tools import create_block_tools_async, test_constants, BlockTools +from chia.util.hash import std_hash +from chia.util.ints import uint16, uint32 +from chia.util.keychain import bytes_to_mnemonic +from tests.block_tools import BlockTools, create_block_tools_async, test_constants from tests.setup_services import ( + setup_daemon, + setup_farmer, setup_full_node, setup_harvester, - setup_farmer, setup_introducer, - setup_vdf_clients, setup_timelord, setup_vdf_client, - setup_daemon, + setup_vdf_clients, ) +from tests.time_out_assert import time_out_assert_custom_interval from tests.util.keyring import TempKeyring from tests.util.socket import find_available_listen_port -from chia.util.hash import std_hash -from chia.util.ints import uint16, uint32 -from chia.util.keychain import bytes_to_mnemonic -from tests.time_out_assert import time_out_assert_custom_interval def cleanup_keyring(keyring: TempKeyring): diff --git a/tests/setup_services.py b/tests/setup_services.py index ebd1db149c63..848506fe7fed 100644 --- a/tests/setup_services.py +++ b/tests/setup_services.py @@ -6,7 +6,7 @@ from typing import AsyncGenerator, Optional from chia.consensus.constants import ConsensusConstants -from chia.daemon.server import WebSocketServer, create_server_for_daemon, daemon_launch_lock_path, singleton +from chia.daemon.server import WebSocketServer, daemon_launch_lock_path, singleton from chia.server.start_farmer import service_kwargs_for_farmer from chia.server.start_full_node import service_kwargs_for_full_node from chia.server.start_harvester import service_kwargs_for_harvester @@ -36,8 +36,8 @@ async def setup_daemon(btools: BlockTools) -> AsyncGenerator[WebSocketServer, No ca_crt_path = root_path / config["private_ssl_ca"]["crt"] ca_key_path = root_path / config["private_ssl_ca"]["key"] assert lockfile is not None - create_server_for_daemon(btools.root_path) - ws_server = WebSocketServer(root_path, ca_crt_path, ca_key_path, crt_path, key_path) + shutdown_event = asyncio.Event() + ws_server = WebSocketServer(root_path, ca_crt_path, ca_key_path, crt_path, key_path, shutdown_event) await ws_server.start() yield ws_server From a2fa8dda01ef9c45fb144c27b30edc80c37a713e Mon Sep 17 00:00:00 2001 From: Amine Khaldi Date: Tue, 5 Apr 2022 18:34:57 +0100 Subject: [PATCH 325/378] Prepare test blocks and plots only for tests that need them. This saves us a couple more hours of CI running time. (#10975) --- .github/workflows/build-test-macos-core-cmds.yml | 8 +------- .github/workflows/build-test-macos-core-consensus.yml | 8 +------- .github/workflows/build-test-macos-core-custom_types.yml | 8 +------- .github/workflows/build-test-macos-generator.yml | 8 +------- .github/workflows/build-test-macos-tools.yml | 8 +------- .github/workflows/build-test-macos-util.yml | 8 +------- .github/workflows/build-test-macos-wallet-did_wallet.yml | 8 +------- .github/workflows/build-test-macos-wallet-rl_wallet.yml | 8 +------- .github/workflows/build-test-ubuntu-core-cmds.yml | 8 +------- .github/workflows/build-test-ubuntu-core-consensus.yml | 8 +------- .github/workflows/build-test-ubuntu-core-custom_types.yml | 8 +------- .github/workflows/build-test-ubuntu-generator.yml | 8 +------- .github/workflows/build-test-ubuntu-tools.yml | 8 +------- .github/workflows/build-test-ubuntu-util.yml | 8 +------- .github/workflows/build-test-ubuntu-wallet-did_wallet.yml | 8 +------- .github/workflows/build-test-ubuntu-wallet-rl_wallet.yml | 8 +------- tests/blockchain/config.py | 1 + tests/core/config.py | 1 + tests/core/daemon/config.py | 1 + tests/core/full_node/config.py | 1 + tests/core/full_node/full_sync/config.py | 1 + tests/core/full_node/stores/config.py | 1 + tests/core/server/config.py | 1 + tests/core/ssl/config.py | 1 + tests/core/util/config.py | 1 + tests/farmer_harvester/config.py | 1 + tests/plotting/config.py | 1 + tests/pools/config.py | 1 + tests/simulation/config.py | 1 + tests/testconfig.py | 2 +- tests/wallet/cat_wallet/config.py | 1 + tests/wallet/config.py | 1 + tests/wallet/rpc/config.py | 1 + tests/wallet/simple_sync/config.py | 1 + tests/wallet/sync/config.py | 1 + tests/weight_proof/config.py | 1 + 36 files changed, 36 insertions(+), 113 deletions(-) create mode 100644 tests/core/server/config.py create mode 100644 tests/farmer_harvester/config.py create mode 100644 tests/wallet/rpc/config.py create mode 100644 tests/wallet/simple_sync/config.py diff --git a/.github/workflows/build-test-macos-core-cmds.yml b/.github/workflows/build-test-macos-core-cmds.yml index 6d64dfaa4ff4..687ae6c94900 100644 --- a/.github/workflows/build-test-macos-core-cmds.yml +++ b/.github/workflows/build-test-macos-core-cmds.yml @@ -65,13 +65,7 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 - with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 +# Omitted checking out blocks and plots repo Chia-Network/test-cache - name: Run install script env: diff --git a/.github/workflows/build-test-macos-core-consensus.yml b/.github/workflows/build-test-macos-core-consensus.yml index d85ee8a28124..f5cd798982e3 100644 --- a/.github/workflows/build-test-macos-core-consensus.yml +++ b/.github/workflows/build-test-macos-core-consensus.yml @@ -65,13 +65,7 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 - with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 +# Omitted checking out blocks and plots repo Chia-Network/test-cache - name: Run install script env: diff --git a/.github/workflows/build-test-macos-core-custom_types.yml b/.github/workflows/build-test-macos-core-custom_types.yml index b6f1aa512eee..130d15420c70 100644 --- a/.github/workflows/build-test-macos-core-custom_types.yml +++ b/.github/workflows/build-test-macos-core-custom_types.yml @@ -65,13 +65,7 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 - with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 +# Omitted checking out blocks and plots repo Chia-Network/test-cache - name: Run install script env: diff --git a/.github/workflows/build-test-macos-generator.yml b/.github/workflows/build-test-macos-generator.yml index 77c89e44d98f..e695c65e96c8 100644 --- a/.github/workflows/build-test-macos-generator.yml +++ b/.github/workflows/build-test-macos-generator.yml @@ -65,13 +65,7 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 - with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 +# Omitted checking out blocks and plots repo Chia-Network/test-cache - name: Run install script env: diff --git a/.github/workflows/build-test-macos-tools.yml b/.github/workflows/build-test-macos-tools.yml index d304aca53d73..e0a4396b6e1e 100644 --- a/.github/workflows/build-test-macos-tools.yml +++ b/.github/workflows/build-test-macos-tools.yml @@ -65,13 +65,7 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 - with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 +# Omitted checking out blocks and plots repo Chia-Network/test-cache - name: Run install script env: diff --git a/.github/workflows/build-test-macos-util.yml b/.github/workflows/build-test-macos-util.yml index 8661fbff15f1..f0f2334a0d0c 100644 --- a/.github/workflows/build-test-macos-util.yml +++ b/.github/workflows/build-test-macos-util.yml @@ -65,13 +65,7 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 - with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 +# Omitted checking out blocks and plots repo Chia-Network/test-cache - name: Run install script env: diff --git a/.github/workflows/build-test-macos-wallet-did_wallet.yml b/.github/workflows/build-test-macos-wallet-did_wallet.yml index fbb4fcb43262..4f97d0ea2f19 100644 --- a/.github/workflows/build-test-macos-wallet-did_wallet.yml +++ b/.github/workflows/build-test-macos-wallet-did_wallet.yml @@ -65,13 +65,7 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 - with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 +# Omitted checking out blocks and plots repo Chia-Network/test-cache - name: Run install script env: diff --git a/.github/workflows/build-test-macos-wallet-rl_wallet.yml b/.github/workflows/build-test-macos-wallet-rl_wallet.yml index 5d74c4e6bc44..1e19516260a4 100644 --- a/.github/workflows/build-test-macos-wallet-rl_wallet.yml +++ b/.github/workflows/build-test-macos-wallet-rl_wallet.yml @@ -65,13 +65,7 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 - with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 +# Omitted checking out blocks and plots repo Chia-Network/test-cache - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-core-cmds.yml b/.github/workflows/build-test-ubuntu-core-cmds.yml index 88ee7ab3cb29..f5f8ed214dbc 100644 --- a/.github/workflows/build-test-ubuntu-core-cmds.yml +++ b/.github/workflows/build-test-ubuntu-core-cmds.yml @@ -65,13 +65,7 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 - with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 +# Omitted checking out blocks and plots repo Chia-Network/test-cache - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-core-consensus.yml b/.github/workflows/build-test-ubuntu-core-consensus.yml index 307f85716f7a..f9045c31ba30 100644 --- a/.github/workflows/build-test-ubuntu-core-consensus.yml +++ b/.github/workflows/build-test-ubuntu-core-consensus.yml @@ -65,13 +65,7 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 - with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 +# Omitted checking out blocks and plots repo Chia-Network/test-cache - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-core-custom_types.yml b/.github/workflows/build-test-ubuntu-core-custom_types.yml index 94f493f64188..447a28627335 100644 --- a/.github/workflows/build-test-ubuntu-core-custom_types.yml +++ b/.github/workflows/build-test-ubuntu-core-custom_types.yml @@ -65,13 +65,7 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 - with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 +# Omitted checking out blocks and plots repo Chia-Network/test-cache - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-generator.yml b/.github/workflows/build-test-ubuntu-generator.yml index 1aa74d5daf97..3f928e2c0443 100644 --- a/.github/workflows/build-test-ubuntu-generator.yml +++ b/.github/workflows/build-test-ubuntu-generator.yml @@ -65,13 +65,7 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 - with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 +# Omitted checking out blocks and plots repo Chia-Network/test-cache - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-tools.yml b/.github/workflows/build-test-ubuntu-tools.yml index 8b8ff6cfbe47..97877660be2d 100644 --- a/.github/workflows/build-test-ubuntu-tools.yml +++ b/.github/workflows/build-test-ubuntu-tools.yml @@ -65,13 +65,7 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 - with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 +# Omitted checking out blocks and plots repo Chia-Network/test-cache - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-util.yml b/.github/workflows/build-test-ubuntu-util.yml index 6f4768ccee6f..1d0567e628d2 100644 --- a/.github/workflows/build-test-ubuntu-util.yml +++ b/.github/workflows/build-test-ubuntu-util.yml @@ -65,13 +65,7 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 - with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 +# Omitted checking out blocks and plots repo Chia-Network/test-cache - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml index f7e9f1012f18..06bf1ad15d55 100644 --- a/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml @@ -65,13 +65,7 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 - with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 +# Omitted checking out blocks and plots repo Chia-Network/test-cache - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml index 3d241ad1bbba..c791e3f32c1e 100644 --- a/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml @@ -65,13 +65,7 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 - with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 +# Omitted checking out blocks and plots repo Chia-Network/test-cache - name: Run install script env: diff --git a/tests/blockchain/config.py b/tests/blockchain/config.py index 93f77cd99ac9..6a5974362bf7 100644 --- a/tests/blockchain/config.py +++ b/tests/blockchain/config.py @@ -1,2 +1,3 @@ parallel = True job_timeout = 60 +checkout_blocks_and_plots = True diff --git a/tests/core/config.py b/tests/core/config.py index 7f9e1e1a76e4..235efb181c1c 100644 --- a/tests/core/config.py +++ b/tests/core/config.py @@ -1 +1,2 @@ parallel = True +checkout_blocks_and_plots = True diff --git a/tests/core/daemon/config.py b/tests/core/daemon/config.py index 685e5b3a49e7..0fbdc6891409 100644 --- a/tests/core/daemon/config.py +++ b/tests/core/daemon/config.py @@ -1 +1,2 @@ install_timelord = True +checkout_blocks_and_plots = True diff --git a/tests/core/full_node/config.py b/tests/core/full_node/config.py index 2cbb1c088328..58f7edfd7afc 100644 --- a/tests/core/full_node/config.py +++ b/tests/core/full_node/config.py @@ -2,3 +2,4 @@ parallel = True job_timeout = 50 check_resource_usage = True +checkout_blocks_and_plots = True diff --git a/tests/core/full_node/full_sync/config.py b/tests/core/full_node/full_sync/config.py index 251dd4220dc5..49234ca992fe 100644 --- a/tests/core/full_node/full_sync/config.py +++ b/tests/core/full_node/full_sync/config.py @@ -1,2 +1,3 @@ job_timeout = 60 parallel = True +checkout_blocks_and_plots = True diff --git a/tests/core/full_node/stores/config.py b/tests/core/full_node/stores/config.py index a36e5dd8a39b..97cd63fb80e6 100644 --- a/tests/core/full_node/stores/config.py +++ b/tests/core/full_node/stores/config.py @@ -2,3 +2,4 @@ parallel = True job_timeout = 40 check_resource_usage = True +checkout_blocks_and_plots = True diff --git a/tests/core/server/config.py b/tests/core/server/config.py new file mode 100644 index 000000000000..0257db4372d0 --- /dev/null +++ b/tests/core/server/config.py @@ -0,0 +1 @@ +checkout_blocks_and_plots = True diff --git a/tests/core/ssl/config.py b/tests/core/ssl/config.py index 7f9e1e1a76e4..235efb181c1c 100644 --- a/tests/core/ssl/config.py +++ b/tests/core/ssl/config.py @@ -1 +1,2 @@ parallel = True +checkout_blocks_and_plots = True diff --git a/tests/core/util/config.py b/tests/core/util/config.py index 7f9e1e1a76e4..235efb181c1c 100644 --- a/tests/core/util/config.py +++ b/tests/core/util/config.py @@ -1 +1,2 @@ parallel = True +checkout_blocks_and_plots = True diff --git a/tests/farmer_harvester/config.py b/tests/farmer_harvester/config.py new file mode 100644 index 000000000000..0257db4372d0 --- /dev/null +++ b/tests/farmer_harvester/config.py @@ -0,0 +1 @@ +checkout_blocks_and_plots = True diff --git a/tests/plotting/config.py b/tests/plotting/config.py index c5495db27705..b0a0afced871 100644 --- a/tests/plotting/config.py +++ b/tests/plotting/config.py @@ -1,2 +1,3 @@ parallel = True install_timelord = False +checkout_blocks_and_plots = True diff --git a/tests/pools/config.py b/tests/pools/config.py index be5a59232c86..244e435f0e3e 100644 --- a/tests/pools/config.py +++ b/tests/pools/config.py @@ -1,2 +1,3 @@ parallel = 2 job_timeout = 60 +checkout_blocks_and_plots = True diff --git a/tests/simulation/config.py b/tests/simulation/config.py index 4ef7da0588bd..c98348440132 100644 --- a/tests/simulation/config.py +++ b/tests/simulation/config.py @@ -1,2 +1,3 @@ job_timeout = 60 install_timelord = True +checkout_blocks_and_plots = True diff --git a/tests/testconfig.py b/tests/testconfig.py index 3aae8f4cea1f..72a3cedac75a 100644 --- a/tests/testconfig.py +++ b/tests/testconfig.py @@ -10,7 +10,7 @@ # Defaults are conservative. parallel: Union[bool, int, Literal["auto"]] = False -checkout_blocks_and_plots = True +checkout_blocks_and_plots = False install_timelord = False check_resource_usage = False job_timeout = 30 diff --git a/tests/wallet/cat_wallet/config.py b/tests/wallet/cat_wallet/config.py index eb21fe13cd3b..671807f010ec 100644 --- a/tests/wallet/cat_wallet/config.py +++ b/tests/wallet/cat_wallet/config.py @@ -1,2 +1,3 @@ # flake8: noqa: E501 job_timeout = 50 +checkout_blocks_and_plots = True diff --git a/tests/wallet/config.py b/tests/wallet/config.py index e90fb48eaaee..dd660d39efae 100644 --- a/tests/wallet/config.py +++ b/tests/wallet/config.py @@ -1,3 +1,4 @@ # flake8: noqa: E501 job_timeout = 40 parallel = True +checkout_blocks_and_plots = True diff --git a/tests/wallet/rpc/config.py b/tests/wallet/rpc/config.py new file mode 100644 index 000000000000..0257db4372d0 --- /dev/null +++ b/tests/wallet/rpc/config.py @@ -0,0 +1 @@ +checkout_blocks_and_plots = True diff --git a/tests/wallet/simple_sync/config.py b/tests/wallet/simple_sync/config.py new file mode 100644 index 000000000000..0257db4372d0 --- /dev/null +++ b/tests/wallet/simple_sync/config.py @@ -0,0 +1 @@ +checkout_blocks_and_plots = True diff --git a/tests/wallet/sync/config.py b/tests/wallet/sync/config.py index d9b815b24cb2..c478c63e00ed 100644 --- a/tests/wallet/sync/config.py +++ b/tests/wallet/sync/config.py @@ -1 +1,2 @@ job_timeout = 60 +checkout_blocks_and_plots = True diff --git a/tests/weight_proof/config.py b/tests/weight_proof/config.py index 7f9e1e1a76e4..235efb181c1c 100644 --- a/tests/weight_proof/config.py +++ b/tests/weight_proof/config.py @@ -1 +1,2 @@ parallel = True +checkout_blocks_and_plots = True From 6ed61bfbe50fb6ab5a612a4ff5ae039c2b80dc29 Mon Sep 17 00:00:00 2001 From: William Allen Date: Tue, 5 Apr 2022 15:28:33 -0500 Subject: [PATCH 326/378] Adding clean-workspace step to benchmarks (#11063) --- .github/workflows/benchmarks.yml | 77 +++++++++++++++++--------------- 1 file changed, 40 insertions(+), 37 deletions(-) diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 0dc6ff3bbe86..527fc9b71af7 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -5,7 +5,7 @@ on: branches: - main tags: - - '**' + - '**' pull_request: branches: - '**' @@ -27,45 +27,48 @@ jobs: python-version: [ 3.9 ] steps: - - name: Checkout Code - uses: actions/checkout@v3 - with: - fetch-depth: 0 + - name: Clean workspace + uses: Chia-Network/actions/clean-workspace@main + + - name: Checkout Code + uses: actions/checkout@v3 + with: + fetch-depth: 0 - - name: Setup Python environment - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} + - name: Setup Python environment + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} - - name: Get pip cache dir - id: pip-cache - run: | - echo "::set-output name=dir::$(pip cache dir)" + - name: Get pip cache dir + id: pip-cache + run: | + echo "::set-output name=dir::$(pip cache dir)" - - name: Cache pip - uses: actions/cache@v3 - with: - path: ${{ steps.pip-cache.outputs.dir }} - key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} - restore-keys: | - ${{ runner.os }}-pip- + - name: Cache pip + uses: actions/cache@v3 + with: + path: ${{ steps.pip-cache.outputs.dir }} + key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- - - name: Install ubuntu dependencies - env: - DEBIAN_FRONTEND: noninteractive - run: | - sudo apt-get install -y software-properties-common - sudo add-apt-repository ppa:deadsnakes/ppa - sudo apt-get update - sudo apt-get install -y python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git + - name: Install ubuntu dependencies + env: + DEBIAN_FRONTEND: noninteractive + run: | + sudo apt-get install -y software-properties-common + sudo add-apt-repository ppa:deadsnakes/ppa + sudo apt-get update + sudo apt-get install -y python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git - - name: Run install script - env: - INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} - run: | - sh install.sh -d + - name: Run install script + env: + INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} + run: | + sh install.sh -d - - name: pytest - run: | - . ./activate - ./venv/bin/py.test -n 0 -m benchmark tests + - name: pytest + run: | + . ./activate + ./venv/bin/py.test -n 0 -m benchmark tests From 45e4c7c1568b48c5f42b7bf2337f03d8128ff37e Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Tue, 5 Apr 2022 18:16:38 -0400 Subject: [PATCH 327/378] Checkout test blocks and plots for benchmarks workflow (#11068) --- .github/workflows/benchmarks.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 527fc9b71af7..d01ed34c8db3 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -25,6 +25,8 @@ jobs: max-parallel: 4 matrix: python-version: [ 3.9 ] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet steps: - name: Clean workspace @@ -62,6 +64,14 @@ jobs: sudo apt-get update sudo apt-get install -y python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git + - name: Checkout test blocks and plots + uses: actions/checkout@v3 + with: + repository: 'Chia-Network/test-cache' + path: '.chia' + ref: '0.28.0' + fetch-depth: 1 + - name: Run install script env: INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} From bb57ccffa942afc654a0874bad3bef6613045756 Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 5 Apr 2022 16:09:00 -0700 Subject: [PATCH 328/378] =?UTF-8?q?Improve=20handling=20of=20unknown=20pen?= =?UTF-8?q?ding=20balances=20(likely=20change=20from=20addi=E2=80=A6=20(#1?= =?UTF-8?q?0984)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Improve handling of unknown pending balances (likely change from adding a maker fee). Minor improvement for fingerprint selection -- enter/return selects the logged-in fingerprint. * Minor output formatting improvements when showing offer summaries. Minor wallet key selection improvements. Added tests for print_offer_summary * Linter fixes * isort fix * Coroutine -> Awaitable * Removed problematic fee calculation from get_pending_amounts per feedback. --- chia/cmds/wallet_funcs.py | 80 ++++++++++++++------- chia/wallet/trading/offer.py | 6 -- tests/core/cmds/test_wallet.py | 124 +++++++++++++++++++++++++++++++++ 3 files changed, 178 insertions(+), 32 deletions(-) create mode 100644 tests/core/cmds/test_wallet.py diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index 7a8b42baab4e..5f5effbadf20 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -4,7 +4,7 @@ import time from datetime import datetime from decimal import Decimal -from typing import Any, Callable, List, Optional, Tuple, Dict +from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple import aiohttp @@ -24,6 +24,8 @@ from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.wallet_types import WalletType +CATNameResolver = Callable[[bytes32], Awaitable[Optional[Tuple[Optional[uint32], str]]]] + def print_transaction(tx: TransactionRecord, verbose: bool, name, address_prefix: str, mojo_per_unit: int) -> None: if verbose: @@ -308,21 +310,37 @@ def timestamp_to_time(timestamp): return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S") -async def print_offer_summary(wallet_client: WalletRpcClient, sum_dict: dict): +async def print_offer_summary(cat_name_resolver: CATNameResolver, sum_dict: Dict[str, int], has_fee: bool = False): for asset_id, amount in sum_dict.items(): - if asset_id == "xch": - wid: str = "1" - name: str = "XCH" - unit: int = units["chia"] - else: - result = await wallet_client.cat_asset_id_to_name(bytes32.from_hexstr(asset_id)) - wid = "Unknown" + description: str = "" + unit: int = units["chia"] + wid: str = "1" if asset_id == "xch" else "" + mojo_amount: int = int(Decimal(amount)) + name: str = "XCH" + if asset_id != "xch": name = asset_id - unit = units["cat"] - if result is not None: - wid = str(result[0]) - name = result[1] - print(f" - {name} (Wallet ID: {wid}): {Decimal(int(amount)) / unit} ({int(Decimal(amount))} mojos)") + if asset_id == "unknown": + name = "Unknown" + unit = units["mojo"] + if has_fee: + description = " [Typically represents change returned from the included fee]" + else: + unit = units["cat"] + result = await cat_name_resolver(bytes32.from_hexstr(asset_id)) + if result is not None: + wid = str(result[0]) + name = result[1] + output: str = f" - {name}" + mojo_str: str = f"{mojo_amount} {'mojo' if mojo_amount == 1 else 'mojos'}" + if len(wid) > 0: + output += f" (Wallet ID: {wid})" + if unit == units["mojo"]: + output += f": {mojo_str}" + else: + output += f": {mojo_amount / unit} ({mojo_str})" + if len(description) > 0: + output += f" {description}" + print(output) async def print_trade_record(record, wallet_client: WalletRpcClient, summaries: bool = False) -> None: @@ -337,13 +355,16 @@ async def print_trade_record(record, wallet_client: WalletRpcClient, summaries: print("Summary:") offer = Offer.from_bytes(record.offer) offered, requested = offer.summary() + outbound_balances: Dict[str, int] = offer.get_pending_amounts() + fees: Decimal = Decimal(offer.bundle.fees()) + cat_name_resolver = wallet_client.cat_asset_id_to_name print(" OFFERED:") - await print_offer_summary(wallet_client, offered) + await print_offer_summary(cat_name_resolver, offered) print(" REQUESTED:") - await print_offer_summary(wallet_client, requested) - print("Pending Balances:") - await print_offer_summary(wallet_client, offer.get_pending_amounts()) - print(f"Fees: {Decimal(offer.bundle.fees()) / units['chia']}") + await print_offer_summary(cat_name_resolver, requested) + print("Pending Outbound Balances:") + await print_offer_summary(cat_name_resolver, outbound_balances, has_fee=(fees > 0)) + print(f"Included Fees: {fees / units['chia']}") print("---------------") @@ -411,12 +432,13 @@ async def take_offer(args: dict, wallet_client: WalletRpcClient, fingerprint: in return offered, requested = offer.summary() + cat_name_resolver = wallet_client.cat_asset_id_to_name print("Summary:") print(" OFFERED:") - await print_offer_summary(wallet_client, offered) + await print_offer_summary(cat_name_resolver, offered) print(" REQUESTED:") - await print_offer_summary(wallet_client, requested) - print(f"Fees: {Decimal(offer.bundle.fees()) / units['chia']}") + await print_offer_summary(cat_name_resolver, requested) + print(f"Included Fees: {Decimal(offer.bundle.fees()) / units['chia']}") if not examine_only: confirmation = input("Would you like to take this offer? (y/n): ") @@ -534,7 +556,7 @@ async def get_wallet(wallet_client: WalletRpcClient, fingerprint: int = None) -> current_sync_status = "Syncing" else: current_sync_status = "Not Synced" - print("Choose wallet key:") + print("Wallet keys:") for i, fp in enumerate(fingerprints): row: str = f"{i+1}) " row += "* " if fp == logged_in_fingerprint else spacing @@ -543,15 +565,21 @@ async def get_wallet(wallet_client: WalletRpcClient, fingerprint: int = None) -> row += f" ({current_sync_status})" print(row) val = None + prompt: str = ( + f"Choose a wallet key [1-{len(fingerprints)}] ('q' to quit, or Enter to use {logged_in_fingerprint}): " + ) while val is None: - val = input("Enter a number to pick or q to quit: ") + val = input(prompt) if val == "q": return None - if not val.isdigit(): + elif val == "" and logged_in_fingerprint is not None: + fingerprint = logged_in_fingerprint + break + elif not val.isdigit(): val = None else: index = int(val) - 1 - if index >= len(fingerprints): + if index < 0 or index >= len(fingerprints): print("Invalid value") val = None continue diff --git a/chia/wallet/trading/offer.py b/chia/wallet/trading/offer.py index 05d742f25c2a..4140a1e67a96 100644 --- a/chia/wallet/trading/offer.py +++ b/chia/wallet/trading/offer.py @@ -193,12 +193,6 @@ def get_pending_amounts(self) -> Dict[str, int]: for addition in filter(lambda c: c.parent_coin_info == root_removal.name(), all_additions): pending_dict[name] += addition.amount - # Then we add a potential fee as pending XCH - fee: int = sum(c.amount for c in all_removals) - sum(c.amount for c in all_additions) - if fee > 0: - pending_dict.setdefault("xch", 0) - pending_dict["xch"] += fee - # Then we gather anything else as unknown sum_of_additions_so_far: int = sum(pending_dict.values()) unknown: int = sum([c.amount for c in non_ephemeral_removals]) - sum_of_additions_so_far diff --git a/tests/core/cmds/test_wallet.py b/tests/core/cmds/test_wallet.py new file mode 100644 index 000000000000..f211c5b50aaf --- /dev/null +++ b/tests/core/cmds/test_wallet.py @@ -0,0 +1,124 @@ +from typing import Any, Dict, Optional, Tuple + +import pytest + +from chia.cmds.wallet_funcs import print_offer_summary +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.util.ints import uint32 + +TEST_DUCKSAUCE_ASSET_ID = "1000000000000000000000000000000000000000000000000000000000000001" +TEST_CRUNCHBERRIES_ASSET_ID = "1000000000000000000000000000000000000000000000000000000000000002" +TEST_UNICORNTEARS_ASSET_ID = "1000000000000000000000000000000000000000000000000000000000000003" + +TEST_ASSET_ID_NAME_MAPPING: Dict[bytes32, Tuple[uint32, str]] = { + bytes32.from_hexstr(TEST_DUCKSAUCE_ASSET_ID): (uint32(2), "DuckSauce"), + bytes32.from_hexstr(TEST_CRUNCHBERRIES_ASSET_ID): (uint32(3), "CrunchBerries"), + bytes32.from_hexstr(TEST_UNICORNTEARS_ASSET_ID): (uint32(4), "UnicornTears"), +} + + +async def cat_name_resolver(asset_id: bytes32) -> Optional[Tuple[Optional[uint32], str]]: + return TEST_ASSET_ID_NAME_MAPPING.get(asset_id) + + +@pytest.mark.asyncio +async def test_print_offer_summary_xch(capsys: Any) -> None: + summary_dict = {"xch": 1_000_000_000_000} + + await print_offer_summary(cat_name_resolver, summary_dict) + + captured = capsys.readouterr() + + assert "XCH (Wallet ID: 1): 1.0 (1000000000000 mojos)" in captured.out + + +@pytest.mark.asyncio +async def test_print_offer_summary_cat(capsys: Any) -> None: + summary_dict = { + TEST_DUCKSAUCE_ASSET_ID: 1_000, + } + + await print_offer_summary(cat_name_resolver, summary_dict) + + captured = capsys.readouterr() + + assert "DuckSauce (Wallet ID: 2): 1.0 (1000 mojos)" in captured.out + + +@pytest.mark.asyncio +async def test_print_offer_summary_multiple_cats(capsys: Any) -> None: + summary_dict = { + TEST_DUCKSAUCE_ASSET_ID: 1_000, + TEST_CRUNCHBERRIES_ASSET_ID: 2_000, + } + + await print_offer_summary(cat_name_resolver, summary_dict) + + captured = capsys.readouterr() + + assert "DuckSauce (Wallet ID: 2): 1.0 (1000 mojos)" in captured.out + assert "CrunchBerries (Wallet ID: 3): 2.0 (2000 mojos)" in captured.out + + +@pytest.mark.asyncio +async def test_print_offer_summary_xch_and_cats(capsys: Any) -> None: + summary_dict = { + "xch": 2_500_000_000_000, + TEST_DUCKSAUCE_ASSET_ID: 1_111, + TEST_CRUNCHBERRIES_ASSET_ID: 2_222, + TEST_UNICORNTEARS_ASSET_ID: 3_333, + } + + await print_offer_summary(cat_name_resolver, summary_dict) + + captured = capsys.readouterr() + + assert "XCH (Wallet ID: 1): 2.5 (2500000000000 mojos)" in captured.out + assert "DuckSauce (Wallet ID: 2): 1.111 (1111 mojos)" in captured.out + assert "CrunchBerries (Wallet ID: 3): 2.222 (2222 mojos)" in captured.out + assert "UnicornTears (Wallet ID: 4): 3.333 (3333 mojos)" in captured.out + + +@pytest.mark.asyncio +async def test_print_offer_summary_xch_and_cats_with_zero_values(capsys: Any) -> None: + summary_dict = { + "xch": 0, + TEST_DUCKSAUCE_ASSET_ID: 0, + TEST_CRUNCHBERRIES_ASSET_ID: 0, + TEST_UNICORNTEARS_ASSET_ID: 0, + } + + await print_offer_summary(cat_name_resolver, summary_dict) + + captured = capsys.readouterr() + + assert "XCH (Wallet ID: 1): 0.0 (0 mojos)" in captured.out + assert "DuckSauce (Wallet ID: 2): 0.0 (0 mojos)" in captured.out + assert "CrunchBerries (Wallet ID: 3): 0.0 (0 mojos)" in captured.out + assert "UnicornTears (Wallet ID: 4): 0.0 (0 mojos)" in captured.out + + +@pytest.mark.asyncio +async def test_print_offer_summary_cat_with_fee_and_change(capsys: Any) -> None: + summary_dict = { + TEST_DUCKSAUCE_ASSET_ID: 1_000, + "unknown": 3_456, + } + + await print_offer_summary(cat_name_resolver, summary_dict, has_fee=True) + + captured = capsys.readouterr() + + assert "DuckSauce (Wallet ID: 2): 1.0 (1000 mojos)" in captured.out + assert "Unknown: 3456 mojos [Typically represents change returned from the included fee]" in captured.out + + +@pytest.mark.asyncio +async def test_print_offer_summary_xch_with_one_mojo(capsys: Any) -> None: + summary_dict = {"xch": 1} + + await print_offer_summary(cat_name_resolver, summary_dict) + + captured = capsys.readouterr() + + assert "XCH (Wallet ID: 1): 1e-12 (1 mojo)" in captured.out From 492503f10975e79635e97ba699f05484f0c167b4 Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Wed, 6 Apr 2022 01:11:03 +0200 Subject: [PATCH 329/378] print average block rate at different block height windows (#11064) --- tools/test_full_sync.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tools/test_full_sync.py b/tools/test_full_sync.py index f8956474790b..d5e1a0741350 100755 --- a/tools/test_full_sync.py +++ b/tools/test_full_sync.py @@ -83,6 +83,7 @@ async def run_sync_test(file: Path, db_version, profile: bool, single_thread: bo print() counter = 0 + height = 0 async with aiosqlite.connect(file) as in_db: rows = await in_db.execute( @@ -109,10 +110,16 @@ async def run_sync_test(file: Path, db_version, profile: bool, single_thread: bo assert success assert advanced_peak counter += len(block_batch) - print(f"\rheight {counter} {counter/(time.monotonic() - start_time):0.2f} blocks/s ", end="") + height += len(block_batch) + print(f"\rheight {height} {counter/(time.monotonic() - start_time):0.2f} blocks/s ", end="") block_batch = [] if check_log.exit_with_failure: raise RuntimeError("error printed to log. exiting") + + if counter >= 100000: + start_time = time.monotonic() + counter = 0 + print() finally: print("closing full node") full_node._close() From c238ce17ba324f67dda4bcad4e2bb7ca2d2086ca Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Tue, 5 Apr 2022 20:00:10 -0400 Subject: [PATCH 330/378] add -d for Install.ps1 (#11062) --- Install.ps1 | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/Install.ps1 b/Install.ps1 index c97d6d737e28..ad6b260861d1 100644 --- a/Install.ps1 +++ b/Install.ps1 @@ -1,5 +1,16 @@ +param( + [Parameter(HelpMessage="install development dependencies")] + [switch]$d = $False +) + $ErrorActionPreference = "Stop" +$extras = @() +if ($d) +{ + $extras += "dev" +} + if ([Environment]::Is64BitOperatingSystem -eq $false) { Write-Output "Chia requires a 64-bit Windows installation" @@ -49,11 +60,21 @@ if ($openSSLVersion -lt 269488367) Write-Output "Anything before 1.1.1n is vulnerable to CVE-2022-0778." } +if ($extras.length -gt 0) +{ + $extras_cli = $extras -join "," + $extras_cli = "[$extras_cli]" +} +else +{ + $extras_cli = "" +} + py -m venv venv venv\scripts\python -m pip install --upgrade pip setuptools wheel venv\scripts\pip install --extra-index-url https://pypi.chia.net/simple/ miniupnpc==2.2.2 -venv\scripts\pip install --editable . --extra-index-url https://pypi.chia.net/simple/ +venv\scripts\pip install --editable ".$extras_cli" --extra-index-url https://pypi.chia.net/simple/ Write-Output "" Write-Output "Chia blockchain .\Install.ps1 complete." From d35c414c09d5befd33fa378738a49db7e3f3db36 Mon Sep 17 00:00:00 2001 From: Jeff Date: Wed, 6 Apr 2022 20:14:49 -0700 Subject: [PATCH 331/378] Set keychain_proxy to None in await_closed() to support reinitialization (#11075) * Set keychain_proxy to None in await_closed() to support reinitialization. * Added `shutting_down` param to _await_closed() to control whether the keychain_proxy is closed. --- chia/farmer/farmer.py | 10 ++++++---- chia/rpc/wallet_rpc_api.py | 2 +- chia/wallet/wallet_node.py | 10 ++++++---- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/chia/farmer/farmer.py b/chia/farmer/farmer.py index 99799258596f..33f3b973a727 100644 --- a/chia/farmer/farmer.py +++ b/chia/farmer/farmer.py @@ -140,7 +140,7 @@ def __init__( self.harvester_cache: Dict[str, Dict[str, HarvesterCacheEntry]] = {} async def ensure_keychain_proxy(self) -> KeychainProxy: - if not self.keychain_proxy: + if self.keychain_proxy is None: if self.local_keychain: self.keychain_proxy = wrap_local_keychain(self.local_keychain, log=self.log) else: @@ -212,13 +212,15 @@ async def start_task(): def _close(self): self._shut_down = True - async def _await_closed(self): + async def _await_closed(self, shutting_down: bool = True): if self.cache_clear_task is not None: await self.cache_clear_task if self.update_pool_state_task is not None: await self.update_pool_state_task - if self.keychain_proxy is not None: - await self.keychain_proxy.close() + if shutting_down and self.keychain_proxy is not None: + proxy = self.keychain_proxy + self.keychain_proxy = None + await proxy.close() await asyncio.sleep(0.5) # https://docs.aiohttp.org/en/stable/client_advanced.html#graceful-shutdown self.started = False diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 9ea76e3d244c..e4439ea3284c 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -157,7 +157,7 @@ async def _stop_wallet(self): """ if self.service is not None: self.service._close() - peers_close_task: Optional[asyncio.Task] = await self.service._await_closed() + peers_close_task: Optional[asyncio.Task] = await self.service._await_closed(shutting_down=False) if peers_close_task is not None: await peers_close_task diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index 6b942ee5f10b..b64f38cf1a7e 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -143,7 +143,7 @@ def __init__( self.LONG_SYNC_THRESHOLD = 200 async def ensure_keychain_proxy(self) -> KeychainProxy: - if not self.keychain_proxy: + if self.keychain_proxy is None: if self.local_keychain: self.keychain_proxy = wrap_local_keychain(self.local_keychain, log=self.log) else: @@ -259,7 +259,7 @@ def _close(self): if self._secondary_peer_sync_task is not None: self._secondary_peer_sync_task.cancel() - async def _await_closed(self): + async def _await_closed(self, shutting_down: bool = True): self.log.info("self._await_closed") if self.server is not None: @@ -269,8 +269,10 @@ async def _await_closed(self): if self.wallet_state_manager is not None: await self.wallet_state_manager._await_closed() self.wallet_state_manager = None - if self.keychain_proxy is not None: - await self.keychain_proxy.close() + if shutting_down and self.keychain_proxy is not None: + proxy = self.keychain_proxy + self.keychain_proxy = None + await proxy.close() await asyncio.sleep(0.5) # https://docs.aiohttp.org/en/stable/client_advanced.html#graceful-shutdown self.logged_in = False self.wallet_peers = None From 6026d734cf4eedf47fdc7250865186c7e09bcd46 Mon Sep 17 00:00:00 2001 From: Amine Khaldi Date: Thu, 7 Apr 2022 04:15:10 +0100 Subject: [PATCH 332/378] Significantly speedup preparing test blocks and plots by opting for a release download instead of a shallow git clone, and also by putting a caching layer on top of that. (#11065) --- .../workflows/build-test-macos-blockchain.yml | 24 ++++++++++++++----- .../build-test-macos-core-daemon.yml | 24 ++++++++++++++----- ...ld-test-macos-core-full_node-full_sync.yml | 24 ++++++++++++++----- ...build-test-macos-core-full_node-stores.yml | 24 ++++++++++++++----- .../build-test-macos-core-full_node.yml | 24 ++++++++++++++----- .../build-test-macos-core-server.yml | 24 ++++++++++++++----- .../workflows/build-test-macos-core-ssl.yml | 24 ++++++++++++++----- .../workflows/build-test-macos-core-util.yml | 24 ++++++++++++++----- .github/workflows/build-test-macos-core.yml | 24 ++++++++++++++----- .../build-test-macos-farmer_harvester.yml | 24 ++++++++++++++----- .../workflows/build-test-macos-plotting.yml | 24 ++++++++++++++----- .github/workflows/build-test-macos-pools.yml | 24 ++++++++++++++----- .../workflows/build-test-macos-simulation.yml | 24 ++++++++++++++----- .../build-test-macos-wallet-cat_wallet.yml | 24 ++++++++++++++----- .../workflows/build-test-macos-wallet-rpc.yml | 24 ++++++++++++++----- .../build-test-macos-wallet-simple_sync.yml | 24 ++++++++++++++----- .../build-test-macos-wallet-sync.yml | 24 ++++++++++++++----- .github/workflows/build-test-macos-wallet.yml | 24 ++++++++++++++----- .../build-test-macos-weight_proof.yml | 24 ++++++++++++++----- .../build-test-ubuntu-blockchain.yml | 24 ++++++++++++++----- .../build-test-ubuntu-core-daemon.yml | 24 ++++++++++++++----- ...d-test-ubuntu-core-full_node-full_sync.yml | 24 ++++++++++++++----- ...uild-test-ubuntu-core-full_node-stores.yml | 24 ++++++++++++++----- .../build-test-ubuntu-core-full_node.yml | 24 ++++++++++++++----- .../build-test-ubuntu-core-server.yml | 24 ++++++++++++++----- .../workflows/build-test-ubuntu-core-ssl.yml | 24 ++++++++++++++----- .../workflows/build-test-ubuntu-core-util.yml | 24 ++++++++++++++----- .github/workflows/build-test-ubuntu-core.yml | 24 ++++++++++++++----- .../build-test-ubuntu-farmer_harvester.yml | 24 ++++++++++++++----- .../workflows/build-test-ubuntu-plotting.yml | 24 ++++++++++++++----- .github/workflows/build-test-ubuntu-pools.yml | 24 ++++++++++++++----- .../build-test-ubuntu-simulation.yml | 24 ++++++++++++++----- .../build-test-ubuntu-wallet-cat_wallet.yml | 24 ++++++++++++++----- .../build-test-ubuntu-wallet-rpc.yml | 24 ++++++++++++++----- .../build-test-ubuntu-wallet-simple_sync.yml | 24 ++++++++++++++----- .../build-test-ubuntu-wallet-sync.yml | 24 ++++++++++++++----- .../workflows/build-test-ubuntu-wallet.yml | 24 ++++++++++++++----- .../build-test-ubuntu-weight_proof.yml | 24 ++++++++++++++----- .../checkout-test-plots.include.yml | 24 ++++++++++++++----- 39 files changed, 702 insertions(+), 234 deletions(-) diff --git a/.github/workflows/build-test-macos-blockchain.yml b/.github/workflows/build-test-macos-blockchain.yml index b39022abb98f..8fcb0b1c1003 100644 --- a/.github/workflows/build-test-macos-blockchain.yml +++ b/.github/workflows/build-test-macos-blockchain.yml @@ -65,13 +65,25 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 + - name: Daily (POC) cache key invalidation for test blocks and plots + id: today-date + run: date +%F > today.txt + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: ${{ hashFiles('today.txt') }} + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-macos-core-daemon.yml b/.github/workflows/build-test-macos-core-daemon.yml index 043fb03b9122..d017da0e3ee8 100644 --- a/.github/workflows/build-test-macos-core-daemon.yml +++ b/.github/workflows/build-test-macos-core-daemon.yml @@ -65,13 +65,25 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 + - name: Daily (POC) cache key invalidation for test blocks and plots + id: today-date + run: date +%F > today.txt + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: ${{ hashFiles('today.txt') }} + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-macos-core-full_node-full_sync.yml b/.github/workflows/build-test-macos-core-full_node-full_sync.yml index bd45bb6dc294..61825197ea0b 100644 --- a/.github/workflows/build-test-macos-core-full_node-full_sync.yml +++ b/.github/workflows/build-test-macos-core-full_node-full_sync.yml @@ -65,13 +65,25 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 + - name: Daily (POC) cache key invalidation for test blocks and plots + id: today-date + run: date +%F > today.txt + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: ${{ hashFiles('today.txt') }} + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-macos-core-full_node-stores.yml b/.github/workflows/build-test-macos-core-full_node-stores.yml index 65a81bb92375..b25f81ea349f 100644 --- a/.github/workflows/build-test-macos-core-full_node-stores.yml +++ b/.github/workflows/build-test-macos-core-full_node-stores.yml @@ -65,13 +65,25 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 + - name: Daily (POC) cache key invalidation for test blocks and plots + id: today-date + run: date +%F > today.txt + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: ${{ hashFiles('today.txt') }} + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-macos-core-full_node.yml b/.github/workflows/build-test-macos-core-full_node.yml index 5e833ac6df69..03b9cb834e1c 100644 --- a/.github/workflows/build-test-macos-core-full_node.yml +++ b/.github/workflows/build-test-macos-core-full_node.yml @@ -65,13 +65,25 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 + - name: Daily (POC) cache key invalidation for test blocks and plots + id: today-date + run: date +%F > today.txt + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: ${{ hashFiles('today.txt') }} + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-macos-core-server.yml b/.github/workflows/build-test-macos-core-server.yml index f81fc0de334a..03a771e6b60e 100644 --- a/.github/workflows/build-test-macos-core-server.yml +++ b/.github/workflows/build-test-macos-core-server.yml @@ -65,13 +65,25 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 + - name: Daily (POC) cache key invalidation for test blocks and plots + id: today-date + run: date +%F > today.txt + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: ${{ hashFiles('today.txt') }} + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-macos-core-ssl.yml b/.github/workflows/build-test-macos-core-ssl.yml index 95f52c10eccb..51c348f8fe30 100644 --- a/.github/workflows/build-test-macos-core-ssl.yml +++ b/.github/workflows/build-test-macos-core-ssl.yml @@ -65,13 +65,25 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 + - name: Daily (POC) cache key invalidation for test blocks and plots + id: today-date + run: date +%F > today.txt + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: ${{ hashFiles('today.txt') }} + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-macos-core-util.yml b/.github/workflows/build-test-macos-core-util.yml index 984c43b7f054..95c8603deb17 100644 --- a/.github/workflows/build-test-macos-core-util.yml +++ b/.github/workflows/build-test-macos-core-util.yml @@ -65,13 +65,25 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 + - name: Daily (POC) cache key invalidation for test blocks and plots + id: today-date + run: date +%F > today.txt + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: ${{ hashFiles('today.txt') }} + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-macos-core.yml b/.github/workflows/build-test-macos-core.yml index 52a592a174ce..df94b8d09dd2 100644 --- a/.github/workflows/build-test-macos-core.yml +++ b/.github/workflows/build-test-macos-core.yml @@ -65,13 +65,25 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 + - name: Daily (POC) cache key invalidation for test blocks and plots + id: today-date + run: date +%F > today.txt + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: ${{ hashFiles('today.txt') }} + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-macos-farmer_harvester.yml b/.github/workflows/build-test-macos-farmer_harvester.yml index 60bcf838f1d8..3017a3579026 100644 --- a/.github/workflows/build-test-macos-farmer_harvester.yml +++ b/.github/workflows/build-test-macos-farmer_harvester.yml @@ -65,13 +65,25 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 + - name: Daily (POC) cache key invalidation for test blocks and plots + id: today-date + run: date +%F > today.txt + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: ${{ hashFiles('today.txt') }} + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-macos-plotting.yml b/.github/workflows/build-test-macos-plotting.yml index 83ef742b8215..80a5c9ceba81 100644 --- a/.github/workflows/build-test-macos-plotting.yml +++ b/.github/workflows/build-test-macos-plotting.yml @@ -65,13 +65,25 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 + - name: Daily (POC) cache key invalidation for test blocks and plots + id: today-date + run: date +%F > today.txt + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: ${{ hashFiles('today.txt') }} + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-macos-pools.yml b/.github/workflows/build-test-macos-pools.yml index e83532a8ff08..39969bddaf7c 100644 --- a/.github/workflows/build-test-macos-pools.yml +++ b/.github/workflows/build-test-macos-pools.yml @@ -65,13 +65,25 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 + - name: Daily (POC) cache key invalidation for test blocks and plots + id: today-date + run: date +%F > today.txt + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: ${{ hashFiles('today.txt') }} + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-macos-simulation.yml b/.github/workflows/build-test-macos-simulation.yml index 5a1f7f7c3e36..44a920ac3160 100644 --- a/.github/workflows/build-test-macos-simulation.yml +++ b/.github/workflows/build-test-macos-simulation.yml @@ -65,13 +65,25 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 + - name: Daily (POC) cache key invalidation for test blocks and plots + id: today-date + run: date +%F > today.txt + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: ${{ hashFiles('today.txt') }} + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-macos-wallet-cat_wallet.yml b/.github/workflows/build-test-macos-wallet-cat_wallet.yml index 32bc83d1ba00..d38bb5a5a532 100644 --- a/.github/workflows/build-test-macos-wallet-cat_wallet.yml +++ b/.github/workflows/build-test-macos-wallet-cat_wallet.yml @@ -65,13 +65,25 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 + - name: Daily (POC) cache key invalidation for test blocks and plots + id: today-date + run: date +%F > today.txt + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: ${{ hashFiles('today.txt') }} + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-macos-wallet-rpc.yml b/.github/workflows/build-test-macos-wallet-rpc.yml index 63de5e94b081..674e246e6e37 100644 --- a/.github/workflows/build-test-macos-wallet-rpc.yml +++ b/.github/workflows/build-test-macos-wallet-rpc.yml @@ -65,13 +65,25 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 + - name: Daily (POC) cache key invalidation for test blocks and plots + id: today-date + run: date +%F > today.txt + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: ${{ hashFiles('today.txt') }} + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-macos-wallet-simple_sync.yml b/.github/workflows/build-test-macos-wallet-simple_sync.yml index 0b5ee26a5241..c80bd4d72bee 100644 --- a/.github/workflows/build-test-macos-wallet-simple_sync.yml +++ b/.github/workflows/build-test-macos-wallet-simple_sync.yml @@ -65,13 +65,25 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 + - name: Daily (POC) cache key invalidation for test blocks and plots + id: today-date + run: date +%F > today.txt + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: ${{ hashFiles('today.txt') }} + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-macos-wallet-sync.yml b/.github/workflows/build-test-macos-wallet-sync.yml index af8635d6006a..bedeaa601659 100644 --- a/.github/workflows/build-test-macos-wallet-sync.yml +++ b/.github/workflows/build-test-macos-wallet-sync.yml @@ -65,13 +65,25 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 + - name: Daily (POC) cache key invalidation for test blocks and plots + id: today-date + run: date +%F > today.txt + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: ${{ hashFiles('today.txt') }} + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-macos-wallet.yml b/.github/workflows/build-test-macos-wallet.yml index a756ecb9fae6..100585f786be 100644 --- a/.github/workflows/build-test-macos-wallet.yml +++ b/.github/workflows/build-test-macos-wallet.yml @@ -65,13 +65,25 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 + - name: Daily (POC) cache key invalidation for test blocks and plots + id: today-date + run: date +%F > today.txt + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: ${{ hashFiles('today.txt') }} + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-macos-weight_proof.yml b/.github/workflows/build-test-macos-weight_proof.yml index 019df2603309..0870d1f95492 100644 --- a/.github/workflows/build-test-macos-weight_proof.yml +++ b/.github/workflows/build-test-macos-weight_proof.yml @@ -65,13 +65,25 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 + - name: Daily (POC) cache key invalidation for test blocks and plots + id: today-date + run: date +%F > today.txt + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: ${{ hashFiles('today.txt') }} + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-blockchain.yml b/.github/workflows/build-test-ubuntu-blockchain.yml index fda7296d918a..e6a5aa872365 100644 --- a/.github/workflows/build-test-ubuntu-blockchain.yml +++ b/.github/workflows/build-test-ubuntu-blockchain.yml @@ -65,13 +65,25 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 + - name: Daily (POC) cache key invalidation for test blocks and plots + id: today-date + run: date +%F > today.txt + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: ${{ hashFiles('today.txt') }} + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-core-daemon.yml b/.github/workflows/build-test-ubuntu-core-daemon.yml index 9b69c941e912..19f60ca1e797 100644 --- a/.github/workflows/build-test-ubuntu-core-daemon.yml +++ b/.github/workflows/build-test-ubuntu-core-daemon.yml @@ -65,13 +65,25 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 + - name: Daily (POC) cache key invalidation for test blocks and plots + id: today-date + run: date +%F > today.txt + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: ${{ hashFiles('today.txt') }} + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml b/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml index 8e5769be2dcc..546c3d21bd7a 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml @@ -65,13 +65,25 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 + - name: Daily (POC) cache key invalidation for test blocks and plots + id: today-date + run: date +%F > today.txt + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: ${{ hashFiles('today.txt') }} + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-core-full_node-stores.yml b/.github/workflows/build-test-ubuntu-core-full_node-stores.yml index d9cc30278ce1..be9cef49ade0 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node-stores.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node-stores.yml @@ -65,13 +65,25 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 + - name: Daily (POC) cache key invalidation for test blocks and plots + id: today-date + run: date +%F > today.txt + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: ${{ hashFiles('today.txt') }} + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-core-full_node.yml b/.github/workflows/build-test-ubuntu-core-full_node.yml index 9e703a06b356..e59db07dedb7 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node.yml @@ -65,13 +65,25 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 + - name: Daily (POC) cache key invalidation for test blocks and plots + id: today-date + run: date +%F > today.txt + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: ${{ hashFiles('today.txt') }} + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-core-server.yml b/.github/workflows/build-test-ubuntu-core-server.yml index 47bb7e7f4f90..a770ae9a40df 100644 --- a/.github/workflows/build-test-ubuntu-core-server.yml +++ b/.github/workflows/build-test-ubuntu-core-server.yml @@ -65,13 +65,25 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 + - name: Daily (POC) cache key invalidation for test blocks and plots + id: today-date + run: date +%F > today.txt + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: ${{ hashFiles('today.txt') }} + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-core-ssl.yml b/.github/workflows/build-test-ubuntu-core-ssl.yml index ba3c7945f58c..050996b89047 100644 --- a/.github/workflows/build-test-ubuntu-core-ssl.yml +++ b/.github/workflows/build-test-ubuntu-core-ssl.yml @@ -65,13 +65,25 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 + - name: Daily (POC) cache key invalidation for test blocks and plots + id: today-date + run: date +%F > today.txt + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: ${{ hashFiles('today.txt') }} + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-core-util.yml b/.github/workflows/build-test-ubuntu-core-util.yml index 938fd0f403f0..68b23bd9c99f 100644 --- a/.github/workflows/build-test-ubuntu-core-util.yml +++ b/.github/workflows/build-test-ubuntu-core-util.yml @@ -65,13 +65,25 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 + - name: Daily (POC) cache key invalidation for test blocks and plots + id: today-date + run: date +%F > today.txt + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: ${{ hashFiles('today.txt') }} + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-core.yml b/.github/workflows/build-test-ubuntu-core.yml index 01be4c71813b..bd32713578c1 100644 --- a/.github/workflows/build-test-ubuntu-core.yml +++ b/.github/workflows/build-test-ubuntu-core.yml @@ -65,13 +65,25 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 + - name: Daily (POC) cache key invalidation for test blocks and plots + id: today-date + run: date +%F > today.txt + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: ${{ hashFiles('today.txt') }} + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-farmer_harvester.yml b/.github/workflows/build-test-ubuntu-farmer_harvester.yml index 6cd3a81af0fc..127ead45c06f 100644 --- a/.github/workflows/build-test-ubuntu-farmer_harvester.yml +++ b/.github/workflows/build-test-ubuntu-farmer_harvester.yml @@ -65,13 +65,25 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 + - name: Daily (POC) cache key invalidation for test blocks and plots + id: today-date + run: date +%F > today.txt + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: ${{ hashFiles('today.txt') }} + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-plotting.yml b/.github/workflows/build-test-ubuntu-plotting.yml index 26c186f95479..099f50f6d7c3 100644 --- a/.github/workflows/build-test-ubuntu-plotting.yml +++ b/.github/workflows/build-test-ubuntu-plotting.yml @@ -65,13 +65,25 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 + - name: Daily (POC) cache key invalidation for test blocks and plots + id: today-date + run: date +%F > today.txt + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: ${{ hashFiles('today.txt') }} + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-pools.yml b/.github/workflows/build-test-ubuntu-pools.yml index 9ae59262f92c..a4284b00e168 100644 --- a/.github/workflows/build-test-ubuntu-pools.yml +++ b/.github/workflows/build-test-ubuntu-pools.yml @@ -65,13 +65,25 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 + - name: Daily (POC) cache key invalidation for test blocks and plots + id: today-date + run: date +%F > today.txt + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: ${{ hashFiles('today.txt') }} + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-simulation.yml b/.github/workflows/build-test-ubuntu-simulation.yml index be0a0f55c3e4..805f646b14e5 100644 --- a/.github/workflows/build-test-ubuntu-simulation.yml +++ b/.github/workflows/build-test-ubuntu-simulation.yml @@ -65,13 +65,25 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 + - name: Daily (POC) cache key invalidation for test blocks and plots + id: today-date + run: date +%F > today.txt + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: ${{ hashFiles('today.txt') }} + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml index b52dfde0c584..25835db6d9a0 100644 --- a/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml @@ -65,13 +65,25 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 + - name: Daily (POC) cache key invalidation for test blocks and plots + id: today-date + run: date +%F > today.txt + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: ${{ hashFiles('today.txt') }} + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-wallet-rpc.yml b/.github/workflows/build-test-ubuntu-wallet-rpc.yml index 05b4cef7a947..4ceb70b32bb4 100644 --- a/.github/workflows/build-test-ubuntu-wallet-rpc.yml +++ b/.github/workflows/build-test-ubuntu-wallet-rpc.yml @@ -65,13 +65,25 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 + - name: Daily (POC) cache key invalidation for test blocks and plots + id: today-date + run: date +%F > today.txt + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: ${{ hashFiles('today.txt') }} + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml b/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml index fdaa81b6dd29..899501734bf7 100644 --- a/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml +++ b/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml @@ -65,13 +65,25 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 + - name: Daily (POC) cache key invalidation for test blocks and plots + id: today-date + run: date +%F > today.txt + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: ${{ hashFiles('today.txt') }} + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-wallet-sync.yml b/.github/workflows/build-test-ubuntu-wallet-sync.yml index 797731d3f315..5da886be8867 100644 --- a/.github/workflows/build-test-ubuntu-wallet-sync.yml +++ b/.github/workflows/build-test-ubuntu-wallet-sync.yml @@ -65,13 +65,25 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 + - name: Daily (POC) cache key invalidation for test blocks and plots + id: today-date + run: date +%F > today.txt + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: ${{ hashFiles('today.txt') }} + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-wallet.yml b/.github/workflows/build-test-ubuntu-wallet.yml index 28f2b1da0a36..a6f734c9ae39 100644 --- a/.github/workflows/build-test-ubuntu-wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet.yml @@ -65,13 +65,25 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 + - name: Daily (POC) cache key invalidation for test blocks and plots + id: today-date + run: date +%F > today.txt + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: ${{ hashFiles('today.txt') }} + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-weight_proof.yml b/.github/workflows/build-test-ubuntu-weight_proof.yml index 34f33582e22f..abccbf822928 100644 --- a/.github/workflows/build-test-ubuntu-weight_proof.yml +++ b/.github/workflows/build-test-ubuntu-weight_proof.yml @@ -65,13 +65,25 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 + - name: Daily (POC) cache key invalidation for test blocks and plots + id: today-date + run: date +%F > today.txt + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: ${{ hashFiles('today.txt') }} + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/tests/runner_templates/checkout-test-plots.include.yml b/tests/runner_templates/checkout-test-plots.include.yml index 1118ef00b035..6d3239bbe662 100644 --- a/tests/runner_templates/checkout-test-plots.include.yml +++ b/tests/runner_templates/checkout-test-plots.include.yml @@ -1,7 +1,19 @@ - - name: Checkout test blocks and plots - uses: actions/checkout@v3 + - name: Daily (POC) cache key invalidation for test blocks and plots + id: today-date + run: date +%F > today.txt + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: ${{ hashFiles('today.txt') }} + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia From 42245d74ebe73e819e1bad4614fa6be41f51e143 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 Apr 2022 09:17:53 -0700 Subject: [PATCH 333/378] Bump github/super-linter from 4.9.1 to 4.9.2 (#11067) Bumps [github/super-linter](https://github.com/github/super-linter) from 4.9.1 to 4.9.2. - [Release notes](https://github.com/github/super-linter/releases) - [Changelog](https://github.com/github/super-linter/blob/main/docs/release-process.md) - [Commits](https://github.com/github/super-linter/compare/v4.9.1...v4.9.2) --- updated-dependencies: - dependency-name: github/super-linter dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/super-linter.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/super-linter.yml b/.github/workflows/super-linter.yml index 8059e9035244..2fa6bf3863a8 100644 --- a/.github/workflows/super-linter.yml +++ b/.github/workflows/super-linter.yml @@ -55,7 +55,7 @@ jobs: # Run Linter against code base # ################################ - name: Lint Code Base - uses: github/super-linter@v4.9.1 + uses: github/super-linter@v4.9.2 # uses: docker://github/super-linter:v3.10.2 env: VALIDATE_ALL_CODEBASE: true From c0d346d428325e6cc4a670b265eabee93dd7f6d4 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 7 Apr 2022 12:18:15 -0400 Subject: [PATCH 334/378] Remove dead snakes usage from benchmark tests (#11053) --- .github/workflows/benchmarks.yml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index d01ed34c8db3..b7589f89c468 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -55,15 +55,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Install ubuntu dependencies - env: - DEBIAN_FRONTEND: noninteractive - run: | - sudo apt-get install -y software-properties-common - sudo add-apt-repository ppa:deadsnakes/ppa - sudo apt-get update - sudo apt-get install -y python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git - - name: Checkout test blocks and plots uses: actions/checkout@v3 with: From d892e14c646511227b4f3dec1676b93ef8314810 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 7 Apr 2022 12:18:54 -0400 Subject: [PATCH 335/378] Handle INSTALL_PYTHON_VERSION in Install.ps1, otherwise search 3.9/3.8/3.7 (#11034) * Handle INSTALL_PYTHON_VERSION in Install.ps1, otherwise search 3.9/3.8/3.7 * fix python availability check in Install.ps1 * when Install.ps1 does not find an acceptable python, list supported versions in order * Update Install.ps1 Co-authored-by: Matt Hauff Co-authored-by: Matt Hauff --- Install.ps1 | 48 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/Install.ps1 b/Install.ps1 index ad6b260861d1..8350d2082c93 100644 --- a/Install.ps1 +++ b/Install.ps1 @@ -43,17 +43,47 @@ if ($null -eq (Get-Command py -ErrorAction SilentlyContinue)) Exit 1 } -$pythonVersion = (py --version).split(" ")[1] -if ([version]$pythonVersion -lt [version]"3.7.0") +$supportedPythonVersions = "3.9", "3.8", "3.7" +if (Test-Path env:INSTALL_PYTHON_VERSION) { - Write-Output "Found Python version:" $pythonVersion - Write-Output "Installation requires Python 3.7 or later" - Exit 1 + $pythonVersion = $env:INSTALL_PYTHON_VERSION } -Write-Output "Python version is:" $pythonVersion +else +{ + foreach ($version in $supportedPythonVersions) + { + try + { + py -$version --version 2>&1 >$null + $result = $? + } + catch + { + $result = $false + } + if ($result) + { + $pythonVersion = $version + break + } + } + + if (-not $pythonVersion) + { + $reversedPythonVersions = $supportedPythonVersions.clone() + [array]::Reverse($reversedPythonVersions) + $reversedPythonVersions = $reversedPythonVersions -join ", " + Write-Output "No usable Python version found, supported versions are: $reversedPythonVersions" + Exit 1 + } +} + +$fullPythonVersion = (py -$pythonVersion --version).split(" ")[1] + +Write-Output "Python version is: $fullPythonVersion" -$openSSLVersionStr = (py -c 'import ssl; print(ssl.OPENSSL_VERSION)') -$openSSLVersion = (py -c 'import ssl; print(ssl.OPENSSL_VERSION_NUMBER)') +$openSSLVersionStr = (py -$pythonVersion -c 'import ssl; print(ssl.OPENSSL_VERSION)') +$openSSLVersion = (py -$pythonVersion -c 'import ssl; print(ssl.OPENSSL_VERSION_NUMBER)') if ($openSSLVersion -lt 269488367) { Write-Output "Found Python with OpenSSL version:" $openSSLVersionStr @@ -70,7 +100,7 @@ else $extras_cli = "" } -py -m venv venv +py -$pythonVersion -m venv venv venv\scripts\python -m pip install --upgrade pip setuptools wheel venv\scripts\pip install --extra-index-url https://pypi.chia.net/simple/ miniupnpc==2.2.2 From 0917d0ae781dfbd3db543e9f4d9fa794895804d2 Mon Sep 17 00:00:00 2001 From: dustinface <35775977+xdustinface@users.noreply.github.com> Date: Thu, 7 Apr 2022 18:19:37 +0200 Subject: [PATCH 336/378] wallet: Drop `puzzles/genesis_checkers.py` and related puzzles (#10790) Its all duplicated code and puzzles as far as i can tell, see `chia/wallet/puzzles/tails.py`. --- .isort.cfg | 1 - chia/wallet/cat_wallet/cat_wallet.py | 2 +- .../puzzles/delegated_genesis_checker.clvm | 25 --- .../delegated_genesis_checker.clvm.hex | 1 - ...egated_genesis_checker.clvm.hex.sha256tree | 1 - .../puzzles/genesis-by-coin-id-with-0.clvm | 26 --- .../genesis-by-coin-id-with-0.clvm.hex | 1 - ...esis-by-coin-id-with-0.clvm.hex.sha256tree | 1 - .../genesis-by-puzzle-hash-with-0.clvm | 24 -- .../genesis-by-puzzle-hash-with-0.clvm.hex | 1 - ...-by-puzzle-hash-with-0.clvm.hex.sha256tree | 1 - chia/wallet/puzzles/genesis_checkers.py | 208 ------------------ mypy.ini | 2 +- tests/clvm/test_clvm_compilation.py | 3 - 14 files changed, 2 insertions(+), 295 deletions(-) delete mode 100644 chia/wallet/puzzles/delegated_genesis_checker.clvm delete mode 100644 chia/wallet/puzzles/delegated_genesis_checker.clvm.hex delete mode 100644 chia/wallet/puzzles/delegated_genesis_checker.clvm.hex.sha256tree delete mode 100644 chia/wallet/puzzles/genesis-by-coin-id-with-0.clvm delete mode 100644 chia/wallet/puzzles/genesis-by-coin-id-with-0.clvm.hex delete mode 100644 chia/wallet/puzzles/genesis-by-coin-id-with-0.clvm.hex.sha256tree delete mode 100644 chia/wallet/puzzles/genesis-by-puzzle-hash-with-0.clvm delete mode 100644 chia/wallet/puzzles/genesis-by-puzzle-hash-with-0.clvm.hex delete mode 100644 chia/wallet/puzzles/genesis-by-puzzle-hash-with-0.clvm.hex.sha256tree delete mode 100644 chia/wallet/puzzles/genesis_checkers.py diff --git a/.isort.cfg b/.isort.cfg index 291c8862f28e..9ed754a63a73 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -120,7 +120,6 @@ extend_skip= chia/wallet/did_wallet/did_wallet.py chia/wallet/lineage_proof.py chia/wallet/payment.py - chia/wallet/puzzles/genesis_checkers.py chia/wallet/puzzles/load_clvm.py chia/wallet/puzzles/prefarm/make_prefarm_ph.py chia/wallet/puzzles/prefarm/spend_prefarm.py diff --git a/chia/wallet/cat_wallet/cat_wallet.py b/chia/wallet/cat_wallet/cat_wallet.py index 4b9f2c19a17e..b58c9b9ce92b 100644 --- a/chia/wallet/cat_wallet/cat_wallet.py +++ b/chia/wallet/cat_wallet/cat_wallet.py @@ -36,7 +36,7 @@ from chia.wallet.cat_wallet.lineage_store import CATLineageStore from chia.wallet.lineage_proof import LineageProof from chia.wallet.payment import Payment -from chia.wallet.puzzles.genesis_checkers import ALL_LIMITATIONS_PROGRAMS +from chia.wallet.puzzles.tails import ALL_LIMITATIONS_PROGRAMS from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import ( DEFAULT_HIDDEN_PUZZLE_HASH, calculate_synthetic_secret_key, diff --git a/chia/wallet/puzzles/delegated_genesis_checker.clvm b/chia/wallet/puzzles/delegated_genesis_checker.clvm deleted file mode 100644 index 57cc677bd499..000000000000 --- a/chia/wallet/puzzles/delegated_genesis_checker.clvm +++ /dev/null @@ -1,25 +0,0 @@ -; This is a "limitations_program" for use with cat.clvm. -(mod ( - PUBKEY - Truths - parent_is_cat - lineage_proof - delta - inner_conditions - ( - delegated_puzzle - delegated_solution - ) - ) - - (include condition_codes.clvm) - - (defun sha256tree1 (TREE) - (if (l TREE) - (sha256 2 (sha256tree1 (f TREE)) (sha256tree1 (r TREE))) - (sha256 1 TREE))) - - (c (list AGG_SIG_UNSAFE PUBKEY (sha256tree1 delegated_puzzle)) - (a delegated_puzzle (c Truths (c parent_is_cat (c lineage_proof (c delta (c inner_conditions delegated_solution)))))) - ) -) \ No newline at end of file diff --git a/chia/wallet/puzzles/delegated_genesis_checker.clvm.hex b/chia/wallet/puzzles/delegated_genesis_checker.clvm.hex deleted file mode 100644 index d131b36b4d49..000000000000 --- a/chia/wallet/puzzles/delegated_genesis_checker.clvm.hex +++ /dev/null @@ -1 +0,0 @@ -ff02ffff01ff04ffff04ff04ffff04ff05ffff04ffff02ff06ffff04ff02ffff04ff82027fff80808080ff80808080ffff02ff82027fffff04ff0bffff04ff17ffff04ff2fffff04ff5fffff04ff81bfff82057f80808080808080ffff04ffff01ff31ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff06ffff04ff02ffff04ff09ff80808080ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080 \ No newline at end of file diff --git a/chia/wallet/puzzles/delegated_genesis_checker.clvm.hex.sha256tree b/chia/wallet/puzzles/delegated_genesis_checker.clvm.hex.sha256tree deleted file mode 100644 index f1d6d7408d04..000000000000 --- a/chia/wallet/puzzles/delegated_genesis_checker.clvm.hex.sha256tree +++ /dev/null @@ -1 +0,0 @@ -999c3696e167f8a79d938adc11feba3a3dcb39ccff69a426d570706e7b8ec399 diff --git a/chia/wallet/puzzles/genesis-by-coin-id-with-0.clvm b/chia/wallet/puzzles/genesis-by-coin-id-with-0.clvm deleted file mode 100644 index c136bb070333..000000000000 --- a/chia/wallet/puzzles/genesis-by-coin-id-with-0.clvm +++ /dev/null @@ -1,26 +0,0 @@ -; This is a "genesis checker" for use with cc.clvm. -; -; This checker allows new CATs to be created if they have a particular coin id as parent -; -; The genesis_id is curried in, making this lineage_check program unique and giving the CAT it's uniqueness -(mod ( - GENESIS_ID - Truths - parent_is_cat - lineage_proof - delta - inner_conditions - _ - ) - - (include cat_truths.clib) - - (if delta - (x) - (if (= (my_parent_cat_truth Truths) GENESIS_ID) - () - (x) - ) - ) - -) diff --git a/chia/wallet/puzzles/genesis-by-coin-id-with-0.clvm.hex b/chia/wallet/puzzles/genesis-by-coin-id-with-0.clvm.hex deleted file mode 100644 index 3f287e448218..000000000000 --- a/chia/wallet/puzzles/genesis-by-coin-id-with-0.clvm.hex +++ /dev/null @@ -1 +0,0 @@ -ff02ffff03ff2fffff01ff0880ffff01ff02ffff03ffff09ff2dff0280ff80ffff01ff088080ff018080ff0180 \ No newline at end of file diff --git a/chia/wallet/puzzles/genesis-by-coin-id-with-0.clvm.hex.sha256tree b/chia/wallet/puzzles/genesis-by-coin-id-with-0.clvm.hex.sha256tree deleted file mode 100644 index f240ff941767..000000000000 --- a/chia/wallet/puzzles/genesis-by-coin-id-with-0.clvm.hex.sha256tree +++ /dev/null @@ -1 +0,0 @@ -493afb89eed93ab86741b2aa61b8f5de495d33ff9b781dfc8919e602b2afa150 \ No newline at end of file diff --git a/chia/wallet/puzzles/genesis-by-puzzle-hash-with-0.clvm b/chia/wallet/puzzles/genesis-by-puzzle-hash-with-0.clvm deleted file mode 100644 index 720465075d0a..000000000000 --- a/chia/wallet/puzzles/genesis-by-puzzle-hash-with-0.clvm +++ /dev/null @@ -1,24 +0,0 @@ -; This is a "limitations_program" for use with cat.clvm. -; -; This checker allows new CATs to be created if their parent has a particular puzzle hash -(mod ( - GENESIS_PUZZLE_HASH - Truths - parent_is_cat - lineage_proof - delta - inner_conditions - (parent_parent_id parent_amount) - ) - - (include cat_truths.clib) - - ; Returns nil since we don't need to add any conditions - (if delta - (x) - (if (= (sha256 parent_parent_id GENESIS_PUZZLE_HASH parent_amount) (my_parent_cat_truth Truths)) - () - (x) - ) - ) -) diff --git a/chia/wallet/puzzles/genesis-by-puzzle-hash-with-0.clvm.hex b/chia/wallet/puzzles/genesis-by-puzzle-hash-with-0.clvm.hex deleted file mode 100644 index 2d367721d1cb..000000000000 --- a/chia/wallet/puzzles/genesis-by-puzzle-hash-with-0.clvm.hex +++ /dev/null @@ -1 +0,0 @@ -ff02ffff03ff2fffff01ff0880ffff01ff02ffff03ffff09ffff0bff82013fff02ff8202bf80ff2d80ff80ffff01ff088080ff018080ff0180 \ No newline at end of file diff --git a/chia/wallet/puzzles/genesis-by-puzzle-hash-with-0.clvm.hex.sha256tree b/chia/wallet/puzzles/genesis-by-puzzle-hash-with-0.clvm.hex.sha256tree deleted file mode 100644 index 69cdc4bce6c8..000000000000 --- a/chia/wallet/puzzles/genesis-by-puzzle-hash-with-0.clvm.hex.sha256tree +++ /dev/null @@ -1 +0,0 @@ -de5a6e06d41518be97ff6365694f4f89475dda773dede267caa33da63b434e36 \ No newline at end of file diff --git a/chia/wallet/puzzles/genesis_checkers.py b/chia/wallet/puzzles/genesis_checkers.py deleted file mode 100644 index b21ac5a7f9ac..000000000000 --- a/chia/wallet/puzzles/genesis_checkers.py +++ /dev/null @@ -1,208 +0,0 @@ -from typing import Tuple, Dict, List, Optional, Any - -from chia.types.blockchain_format.program import Program -from chia.types.blockchain_format.sized_bytes import bytes32 -from chia.types.spend_bundle import SpendBundle -from chia.util.ints import uint64 -from chia.util.byte_types import hexstr_to_bytes -from chia.wallet.lineage_proof import LineageProof -from chia.wallet.puzzles.load_clvm import load_clvm -from chia.wallet.cat_wallet.cat_utils import ( - CAT_MOD, - construct_cat_puzzle, - unsigned_spend_bundle_for_spendable_cats, - SpendableCAT, -) -from chia.wallet.cat_wallet.cat_info import CATInfo -from chia.wallet.transaction_record import TransactionRecord - -GENESIS_BY_ID_MOD = load_clvm("genesis-by-coin-id-with-0.clvm") -GENESIS_BY_PUZHASH_MOD = load_clvm("genesis-by-puzzle-hash-with-0.clvm") -EVERYTHING_WITH_SIG_MOD = load_clvm("everything_with_signature.clvm") -DELEGATED_LIMITATIONS_MOD = load_clvm("delegated_genesis_checker.clvm") - - -class LimitationsProgram: - @staticmethod - def match(uncurried_mod: Program, curried_args: Program) -> Tuple[bool, List[Program]]: - raise NotImplementedError("Need to implement 'match' on limitations programs") - - @staticmethod - def construct(args: List[Program]) -> Program: - raise NotImplementedError("Need to implement 'construct' on limitations programs") - - @staticmethod - def solve(args: List[Program], solution_dict: Dict) -> Program: - raise NotImplementedError("Need to implement 'solve' on limitations programs") - - @classmethod - async def generate_issuance_bundle( - cls, wallet, cat_tail_info: Dict, amount: uint64 - ) -> Tuple[TransactionRecord, SpendBundle]: - raise NotImplementedError("Need to implement 'generate_issuance_bundle' on limitations programs") - - -class GenesisById(LimitationsProgram): - """ - This TAIL allows for coins to be issued only by a specific "genesis" coin ID. - There can therefore only be one issuance. There is no minting or melting allowed. - """ - - @staticmethod - def match(uncurried_mod: Program, curried_args: Program) -> Tuple[bool, List[Program]]: - if uncurried_mod == GENESIS_BY_ID_MOD: - genesis_id = curried_args.first() - return True, [genesis_id] - else: - return False, [] - - @staticmethod - def construct(args: List[Program]) -> Program: - return GENESIS_BY_ID_MOD.curry(args[0]) - - @staticmethod - def solve(args: List[Program], solution_dict: Dict) -> Program: - return Program.to([]) - - @classmethod - async def generate_issuance_bundle(cls, wallet, _: Dict, amount: uint64) -> Tuple[TransactionRecord, SpendBundle]: - coins = await wallet.standard_wallet.select_coins(amount) - - origin = coins.copy().pop() - origin_id = origin.name() - - cat_inner: Program = await wallet.get_new_inner_puzzle() - await wallet.add_lineage(origin_id, LineageProof(), False) - genesis_coin_checker: Program = cls.construct([Program.to(origin_id)]) - - minted_cat_puzzle_hash: bytes32 = construct_cat_puzzle( - CAT_MOD, genesis_coin_checker.get_tree_hash(), cat_inner - ).get_tree_hash() - - tx_record: TransactionRecord = await wallet.standard_wallet.generate_signed_transaction( - amount, minted_cat_puzzle_hash, uint64(0), origin_id, coins - ) - assert tx_record.spend_bundle is not None - - inner_solution = wallet.standard_wallet.add_condition_to_solution( - Program.to([51, 0, -113, genesis_coin_checker, []]), - wallet.standard_wallet.make_solution( - primaries=[{"puzzlehash": cat_inner.get_tree_hash(), "amount": amount}], - ), - ) - eve_spend = unsigned_spend_bundle_for_spendable_cats( - CAT_MOD, - [ - SpendableCAT( - list(filter(lambda a: a.amount == amount, tx_record.additions))[0], - genesis_coin_checker.get_tree_hash(), - cat_inner, - inner_solution, - limitations_program_reveal=genesis_coin_checker, - ) - ], - ) - signed_eve_spend = await wallet.sign(eve_spend) - - if wallet.cat_info.my_tail is None: - await wallet.save_info( - CATInfo(genesis_coin_checker.get_tree_hash(), genesis_coin_checker), - False, - ) - - return tx_record, SpendBundle.aggregate([tx_record.spend_bundle, signed_eve_spend]) - - -class GenesisByPuzhash(LimitationsProgram): - """ - This TAIL allows for issuance of a certain coin only by a specific puzzle hash. - There is no minting or melting allowed. - """ - - @staticmethod - def match(uncurried_mod: Program, curried_args: Program) -> Tuple[bool, List[Program]]: - if uncurried_mod == GENESIS_BY_PUZHASH_MOD: - genesis_puzhash = curried_args.first() - return True, [genesis_puzhash] - else: - return False, [] - - @staticmethod - def construct(args: List[Program]) -> Program: - return GENESIS_BY_PUZHASH_MOD.curry(args[0]) - - @staticmethod - def solve(args: List[Program], solution_dict: Dict) -> Program: - pid = hexstr_to_bytes(solution_dict["parent_coin_info"]) - return Program.to([pid, solution_dict["amount"]]) - - -class EverythingWithSig(LimitationsProgram): - """ - This TAIL allows for issuance, minting, and melting as long as you provide a signature with the spend. - """ - - @staticmethod - def match(uncurried_mod: Program, curried_args: Program) -> Tuple[bool, List[Program]]: - if uncurried_mod == EVERYTHING_WITH_SIG_MOD: - pubkey = curried_args.first() - return True, [pubkey] - else: - return False, [] - - @staticmethod - def construct(args: List[Program]) -> Program: - return EVERYTHING_WITH_SIG_MOD.curry(args[0]) - - @staticmethod - def solve(args: List[Program], solution_dict: Dict) -> Program: - return Program.to([]) - - -class DelegatedLimitations(LimitationsProgram): - """ - This TAIL allows for another TAIL to be used, as long as a signature of that TAIL's puzzlehash is included. - """ - - @staticmethod - def match(uncurried_mod: Program, curried_args: Program) -> Tuple[bool, List[Program]]: - if uncurried_mod == DELEGATED_LIMITATIONS_MOD: - pubkey = curried_args.first() - return True, [pubkey] - else: - return False, [] - - @staticmethod - def construct(args: List[Program]) -> Program: - return DELEGATED_LIMITATIONS_MOD.curry(args[0]) - - @staticmethod - def solve(args: List[Program], solution_dict: Dict) -> Program: - signed_program = ALL_LIMITATIONS_PROGRAMS[solution_dict["signed_program"]["identifier"]] - inner_program_args = [Program.fromhex(item) for item in solution_dict["signed_program"]["args"]] - inner_solution_dict = solution_dict["program_arguments"] - return Program.to( - [ - signed_program.construct(inner_program_args), - signed_program.solve(inner_program_args, inner_solution_dict), - ] - ) - - -# This should probably be much more elegant than just a dictionary with strings as identifiers -# Right now this is small and experimental so it can stay like this -ALL_LIMITATIONS_PROGRAMS: Dict[str, Any] = { - "genesis_by_id": GenesisById, - "genesis_by_puzhash": GenesisByPuzhash, - "everything_with_signature": EverythingWithSig, - "delegated_limitations": DelegatedLimitations, -} - - -def match_limitations_program(limitations_program: Program) -> Tuple[Optional[LimitationsProgram], List[Program]]: - uncurried_mod, curried_args = limitations_program.uncurry() - for key, lp in ALL_LIMITATIONS_PROGRAMS.items(): - matched, args = lp.match(uncurried_mod, curried_args) - if matched: - return lp, args - return None, [] diff --git a/mypy.ini b/mypy.ini index ec9389612df1..4c3283cc77d9 100644 --- a/mypy.ini +++ b/mypy.ini @@ -17,7 +17,7 @@ no_implicit_reexport = True strict_equality = True # list created by: venv/bin/mypy | sed -n 's/.py:.*//p' | sort | uniq | tr '/' '.' | tr '\n' ',' -[mypy-benchmarks.block_ref,benchmarks.block_store,benchmarks.coin_store,benchmarks.utils,build_scripts.installer-version,chia.clvm.spend_sim,chia.cmds.configure,chia.cmds.db,chia.cmds.db_upgrade_func,chia.cmds.farm_funcs,chia.cmds.init,chia.cmds.init_funcs,chia.cmds.keys,chia.cmds.keys_funcs,chia.cmds.passphrase,chia.cmds.passphrase_funcs,chia.cmds.plotnft,chia.cmds.plotnft_funcs,chia.cmds.plots,chia.cmds.plotters,chia.cmds.show,chia.cmds.start_funcs,chia.cmds.wallet,chia.cmds.wallet_funcs,chia.consensus.block_body_validation,chia.consensus.blockchain,chia.consensus.blockchain_interface,chia.consensus.block_creation,chia.consensus.block_header_validation,chia.consensus.block_record,chia.consensus.block_root_validation,chia.consensus.coinbase,chia.consensus.constants,chia.consensus.difficulty_adjustment,chia.consensus.get_block_challenge,chia.consensus.multiprocess_validation,chia.consensus.pos_quality,chia.consensus.vdf_info_computation,chia.daemon.client,chia.daemon.keychain_proxy,chia.daemon.keychain_server,chia.daemon.server,chia.farmer.farmer,chia.farmer.farmer_api,chia.full_node.block_height_map,chia.full_node.block_store,chia.full_node.bundle_tools,chia.full_node.coin_store,chia.full_node.full_node,chia.full_node.full_node_api,chia.full_node.full_node_store,chia.full_node.generator,chia.full_node.hint_store,chia.full_node.lock_queue,chia.full_node.mempool,chia.full_node.mempool_check_conditions,chia.full_node.mempool_manager,chia.full_node.pending_tx_cache,chia.full_node.sync_store,chia.full_node.weight_proof,chia.harvester.harvester,chia.harvester.harvester_api,chia.introducer.introducer,chia.introducer.introducer_api,chia.plotters.bladebit,chia.plotters.chiapos,chia.plotters.install_plotter,chia.plotters.madmax,chia.plotters.plotters,chia.plotters.plotters_util,chia.plotting.check_plots,chia.plotting.create_plots,chia.plotting.manager,chia.plotting.util,chia.pools.pool_config,chia.pools.pool_puzzles,chia.pools.pool_wallet,chia.pools.pool_wallet_info,chia.protocols.pool_protocol,chia.rpc.crawler_rpc_api,chia.rpc.farmer_rpc_api,chia.rpc.farmer_rpc_client,chia.rpc.full_node_rpc_api,chia.rpc.full_node_rpc_client,chia.rpc.harvester_rpc_api,chia.rpc.harvester_rpc_client,chia.rpc.rpc_client,chia.rpc.rpc_server,chia.rpc.timelord_rpc_api,chia.rpc.util,chia.rpc.wallet_rpc_api,chia.rpc.wallet_rpc_client,chia.seeder.crawler,chia.seeder.crawler_api,chia.seeder.crawl_store,chia.seeder.dns_server,chia.seeder.peer_record,chia.seeder.start_crawler,chia.server.address_manager,chia.server.address_manager_store,chia.server.connection_utils,chia.server.introducer_peers,chia.server.node_discovery,chia.server.peer_store_resolver,chia.server.rate_limits,chia.server.reconnect_task,chia.server.server,chia.server.ssl_context,chia.server.start_farmer,chia.server.start_full_node,chia.server.start_harvester,chia.server.start_introducer,chia.server.start_service,chia.server.start_timelord,chia.server.start_wallet,chia.server.upnp,chia.server.ws_connection,chia.simulator.full_node_simulator,chia.simulator.start_simulator,chia.ssl.create_ssl,chia.timelord.iters_from_block,chia.timelord.timelord,chia.timelord.timelord_api,chia.timelord.timelord_launcher,chia.timelord.timelord_state,chia.types.announcement,chia.types.blockchain_format.classgroup,chia.types.blockchain_format.coin,chia.types.blockchain_format.program,chia.types.blockchain_format.proof_of_space,chia.types.blockchain_format.tree_hash,chia.types.blockchain_format.vdf,chia.types.full_block,chia.types.header_block,chia.types.mempool_item,chia.types.name_puzzle_condition,chia.types.peer_info,chia.types.spend_bundle,chia.types.transaction_queue_entry,chia.types.unfinished_block,chia.types.unfinished_header_block,chia.util.api_decorators,chia.util.block_cache,chia.util.byte_types,chia.util.cached_bls,chia.util.check_fork_next_block,chia.util.chia_logging,chia.util.config,chia.util.db_wrapper,chia.util.dump_keyring,chia.util.file_keyring,chia.util.files,chia.util.hash,chia.util.ints,chia.util.json_util,chia.util.keychain,chia.util.keyring_wrapper,chia.util.log_exceptions,chia.util.lru_cache,chia.util.make_test_constants,chia.util.merkle_set,chia.util.network,chia.util.partial_func,chia.util.pip_import,chia.util.profiler,chia.util.safe_cancel_task,chia.util.service_groups,chia.util.ssl_check,chia.util.streamable,chia.util.struct_stream,chia.util.type_checking,chia.util.validate_alert,chia.wallet.block_record,chia.wallet.cat_wallet.cat_utils,chia.wallet.cat_wallet.cat_wallet,chia.wallet.cat_wallet.lineage_store,chia.wallet.chialisp,chia.wallet.did_wallet.did_wallet,chia.wallet.did_wallet.did_wallet_puzzles,chia.wallet.key_val_store,chia.wallet.lineage_proof,chia.wallet.payment,chia.wallet.puzzles.genesis_checkers,chia.wallet.puzzles.load_clvm,chia.wallet.puzzles.p2_conditions,chia.wallet.puzzles.p2_delegated_conditions,chia.wallet.puzzles.p2_delegated_puzzle,chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle,chia.wallet.puzzles.p2_m_of_n_delegate_direct,chia.wallet.puzzles.p2_puzzle_hash,chia.wallet.puzzles.prefarm.spend_prefarm,chia.wallet.puzzles.puzzle_utils,chia.wallet.puzzles.rom_bootstrap_generator,chia.wallet.puzzles.singleton_top_layer,chia.wallet.puzzles.tails,chia.wallet.rl_wallet.rl_wallet,chia.wallet.rl_wallet.rl_wallet_puzzles,chia.wallet.secret_key_store,chia.wallet.settings.user_settings,chia.wallet.trade_manager,chia.wallet.trade_record,chia.wallet.trading.offer,chia.wallet.trading.trade_store,chia.wallet.transaction_record,chia.wallet.util.debug_spend_bundle,chia.wallet.util.new_peak_queue,chia.wallet.util.peer_request_cache,chia.wallet.util.wallet_sync_utils,chia.wallet.wallet,chia.wallet.wallet_action_store,chia.wallet.wallet_blockchain,chia.wallet.wallet_coin_store,chia.wallet.wallet_interested_store,chia.wallet.wallet_node,chia.wallet.wallet_node_api,chia.wallet.wallet_pool_store,chia.wallet.wallet_puzzle_store,chia.wallet.wallet_state_manager,chia.wallet.wallet_sync_store,chia.wallet.wallet_transaction_store,chia.wallet.wallet_user_store,chia.wallet.wallet_weight_proof_handler,installhelper,tests.blockchain.blockchain_test_utils,tests.blockchain.test_blockchain,tests.blockchain.test_blockchain_transactions,tests.block_tools,tests.build-init-files,tests.build-workflows,tests.clvm.coin_store,tests.clvm.test_chialisp_deserialization,tests.clvm.test_clvm_compilation,tests.clvm.test_program,tests.clvm.test_puzzle_compression,tests.clvm.test_puzzles,tests.clvm.test_serialized_program,tests.clvm.test_singletons,tests.clvm.test_spend_sim,tests.conftest,tests.connection_utils,tests.core.cmds.test_keys,tests.core.consensus.test_pot_iterations,tests.core.custom_types.test_coin,tests.core.custom_types.test_proof_of_space,tests.core.custom_types.test_spend_bundle,tests.core.daemon.test_daemon,tests.core.full_node.full_sync.test_full_sync,tests.core.full_node.stores.test_block_store,tests.core.full_node.stores.test_coin_store,tests.core.full_node.stores.test_full_node_store,tests.core.full_node.stores.test_hint_store,tests.core.full_node.stores.test_sync_store,tests.core.full_node.test_address_manager,tests.core.full_node.test_block_height_map,tests.core.full_node.test_conditions,tests.core.full_node.test_full_node,tests.core.full_node.test_mempool,tests.core.full_node.test_mempool_performance,tests.core.full_node.test_node_load,tests.core.full_node.test_peer_store_resolver,tests.core.full_node.test_performance,tests.core.full_node.test_transactions,tests.core.make_block_generator,tests.core.node_height,tests.core.server.test_dos,tests.core.server.test_rate_limits,tests.core.ssl.test_ssl,tests.core.test_cost_calculation,tests.core.test_crawler_rpc,tests.core.test_daemon_rpc,tests.core.test_db_conversion,tests.core.test_farmer_harvester_rpc,tests.core.test_filter,tests.core.test_full_node_rpc,tests.core.test_merkle_set,tests.core.test_setproctitle,tests.core.util.test_cached_bls,tests.core.util.test_config,tests.core.util.test_file_keyring_synchronization,tests.core.util.test_files,tests.core.util.test_keychain,tests.core.util.test_keyring_wrapper,tests.core.util.test_lru_cache,tests.core.util.test_significant_bits,tests.core.util.test_streamable,tests.core.util.test_type_checking,tests.farmer_harvester.test_farmer_harvester,tests.generator.test_compression,tests.generator.test_generator_types,tests.generator.test_list_to_batches,tests.generator.test_rom,tests.generator.test_scan,tests.plotting.test_plot_manager,tests.pools.test_pool_cmdline,tests.pools.test_pool_config,tests.pools.test_pool_puzzles_lifecycle,tests.pools.test_pool_rpc,tests.pools.test_wallet_pool_store,tests.setup_nodes,tests.setup_services,tests.simulation.test_simulation,tests.time_out_assert,tests.tools.test_full_sync,tests.tools.test_run_block,tests.util.alert_server,tests.util.benchmark_cost,tests.util.blockchain,tests.util.build_network_protocol_files,tests.util.db_connection,tests.util.generator_tools_testing,tests.util.keyring,tests.util.key_tool,tests.util.misc,tests.util.network,tests.util.rpc,tests.util.test_full_block_utils,tests.util.test_lock_queue,tests.util.test_network_protocol_files,tests.util.test_struct_stream,tests.wallet.cat_wallet.test_cat_lifecycle,tests.wallet.cat_wallet.test_cat_wallet,tests.wallet.cat_wallet.test_offer_lifecycle,tests.wallet.cat_wallet.test_trades,tests.wallet.did_wallet.test_did,tests.wallet.did_wallet.test_did_rpc,tests.wallet.rl_wallet.test_rl_rpc,tests.wallet.rl_wallet.test_rl_wallet,tests.wallet.rpc.test_wallet_rpc,tests.wallet.simple_sync.test_simple_sync_protocol,tests.wallet.sync.test_wallet_sync,tests.wallet.test_bech32m,tests.wallet.test_chialisp,tests.wallet.test_puzzle_store,tests.wallet.test_singleton,tests.wallet.test_singleton_lifecycle,tests.wallet.test_singleton_lifecycle_fast,tests.wallet.test_taproot,tests.wallet.test_wallet,tests.wallet.test_wallet_blockchain,tests.wallet.test_wallet_interested_store,tests.wallet.test_wallet_key_val_store,tests.wallet.test_wallet_user_store,tests.wallet_tools,tests.weight_proof.test_weight_proof,tools.analyze-chain,tools.run_block,tools.test_full_sync] +[mypy-benchmarks.block_ref,benchmarks.block_store,benchmarks.coin_store,benchmarks.utils,build_scripts.installer-version,chia.clvm.spend_sim,chia.cmds.configure,chia.cmds.db,chia.cmds.db_upgrade_func,chia.cmds.farm_funcs,chia.cmds.init,chia.cmds.init_funcs,chia.cmds.keys,chia.cmds.keys_funcs,chia.cmds.passphrase,chia.cmds.passphrase_funcs,chia.cmds.plotnft,chia.cmds.plotnft_funcs,chia.cmds.plots,chia.cmds.plotters,chia.cmds.show,chia.cmds.start_funcs,chia.cmds.wallet,chia.cmds.wallet_funcs,chia.consensus.block_body_validation,chia.consensus.blockchain,chia.consensus.blockchain_interface,chia.consensus.block_creation,chia.consensus.block_header_validation,chia.consensus.block_record,chia.consensus.block_root_validation,chia.consensus.coinbase,chia.consensus.constants,chia.consensus.difficulty_adjustment,chia.consensus.get_block_challenge,chia.consensus.multiprocess_validation,chia.consensus.pos_quality,chia.consensus.vdf_info_computation,chia.daemon.client,chia.daemon.keychain_proxy,chia.daemon.keychain_server,chia.daemon.server,chia.farmer.farmer,chia.farmer.farmer_api,chia.full_node.block_height_map,chia.full_node.block_store,chia.full_node.bundle_tools,chia.full_node.coin_store,chia.full_node.full_node,chia.full_node.full_node_api,chia.full_node.full_node_store,chia.full_node.generator,chia.full_node.hint_store,chia.full_node.lock_queue,chia.full_node.mempool,chia.full_node.mempool_check_conditions,chia.full_node.mempool_manager,chia.full_node.pending_tx_cache,chia.full_node.sync_store,chia.full_node.weight_proof,chia.harvester.harvester,chia.harvester.harvester_api,chia.introducer.introducer,chia.introducer.introducer_api,chia.plotters.bladebit,chia.plotters.chiapos,chia.plotters.install_plotter,chia.plotters.madmax,chia.plotters.plotters,chia.plotters.plotters_util,chia.plotting.check_plots,chia.plotting.create_plots,chia.plotting.manager,chia.plotting.util,chia.pools.pool_config,chia.pools.pool_puzzles,chia.pools.pool_wallet,chia.pools.pool_wallet_info,chia.protocols.pool_protocol,chia.rpc.crawler_rpc_api,chia.rpc.farmer_rpc_api,chia.rpc.farmer_rpc_client,chia.rpc.full_node_rpc_api,chia.rpc.full_node_rpc_client,chia.rpc.harvester_rpc_api,chia.rpc.harvester_rpc_client,chia.rpc.rpc_client,chia.rpc.rpc_server,chia.rpc.timelord_rpc_api,chia.rpc.util,chia.rpc.wallet_rpc_api,chia.rpc.wallet_rpc_client,chia.seeder.crawler,chia.seeder.crawler_api,chia.seeder.crawl_store,chia.seeder.dns_server,chia.seeder.peer_record,chia.seeder.start_crawler,chia.server.address_manager,chia.server.address_manager_store,chia.server.connection_utils,chia.server.introducer_peers,chia.server.node_discovery,chia.server.peer_store_resolver,chia.server.rate_limits,chia.server.reconnect_task,chia.server.server,chia.server.ssl_context,chia.server.start_farmer,chia.server.start_full_node,chia.server.start_harvester,chia.server.start_introducer,chia.server.start_service,chia.server.start_timelord,chia.server.start_wallet,chia.server.upnp,chia.server.ws_connection,chia.simulator.full_node_simulator,chia.simulator.start_simulator,chia.ssl.create_ssl,chia.timelord.iters_from_block,chia.timelord.timelord,chia.timelord.timelord_api,chia.timelord.timelord_launcher,chia.timelord.timelord_state,chia.types.announcement,chia.types.blockchain_format.classgroup,chia.types.blockchain_format.coin,chia.types.blockchain_format.program,chia.types.blockchain_format.proof_of_space,chia.types.blockchain_format.tree_hash,chia.types.blockchain_format.vdf,chia.types.full_block,chia.types.header_block,chia.types.mempool_item,chia.types.name_puzzle_condition,chia.types.peer_info,chia.types.spend_bundle,chia.types.transaction_queue_entry,chia.types.unfinished_block,chia.types.unfinished_header_block,chia.util.api_decorators,chia.util.block_cache,chia.util.byte_types,chia.util.cached_bls,chia.util.check_fork_next_block,chia.util.chia_logging,chia.util.config,chia.util.db_wrapper,chia.util.dump_keyring,chia.util.file_keyring,chia.util.files,chia.util.hash,chia.util.ints,chia.util.json_util,chia.util.keychain,chia.util.keyring_wrapper,chia.util.log_exceptions,chia.util.lru_cache,chia.util.make_test_constants,chia.util.merkle_set,chia.util.network,chia.util.partial_func,chia.util.pip_import,chia.util.profiler,chia.util.safe_cancel_task,chia.util.service_groups,chia.util.ssl_check,chia.util.streamable,chia.util.struct_stream,chia.util.type_checking,chia.util.validate_alert,chia.wallet.block_record,chia.wallet.cat_wallet.cat_utils,chia.wallet.cat_wallet.cat_wallet,chia.wallet.cat_wallet.lineage_store,chia.wallet.chialisp,chia.wallet.did_wallet.did_wallet,chia.wallet.did_wallet.did_wallet_puzzles,chia.wallet.key_val_store,chia.wallet.lineage_proof,chia.wallet.payment,chia.wallet.puzzles.load_clvm,chia.wallet.puzzles.p2_conditions,chia.wallet.puzzles.p2_delegated_conditions,chia.wallet.puzzles.p2_delegated_puzzle,chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle,chia.wallet.puzzles.p2_m_of_n_delegate_direct,chia.wallet.puzzles.p2_puzzle_hash,chia.wallet.puzzles.prefarm.spend_prefarm,chia.wallet.puzzles.puzzle_utils,chia.wallet.puzzles.rom_bootstrap_generator,chia.wallet.puzzles.singleton_top_layer,chia.wallet.puzzles.tails,chia.wallet.rl_wallet.rl_wallet,chia.wallet.rl_wallet.rl_wallet_puzzles,chia.wallet.secret_key_store,chia.wallet.settings.user_settings,chia.wallet.trade_manager,chia.wallet.trade_record,chia.wallet.trading.offer,chia.wallet.trading.trade_store,chia.wallet.transaction_record,chia.wallet.util.debug_spend_bundle,chia.wallet.util.new_peak_queue,chia.wallet.util.peer_request_cache,chia.wallet.util.wallet_sync_utils,chia.wallet.wallet,chia.wallet.wallet_action_store,chia.wallet.wallet_blockchain,chia.wallet.wallet_coin_store,chia.wallet.wallet_interested_store,chia.wallet.wallet_node,chia.wallet.wallet_node_api,chia.wallet.wallet_pool_store,chia.wallet.wallet_puzzle_store,chia.wallet.wallet_state_manager,chia.wallet.wallet_sync_store,chia.wallet.wallet_transaction_store,chia.wallet.wallet_user_store,chia.wallet.wallet_weight_proof_handler,installhelper,tests.blockchain.blockchain_test_utils,tests.blockchain.test_blockchain,tests.blockchain.test_blockchain_transactions,tests.block_tools,tests.build-init-files,tests.build-workflows,tests.clvm.coin_store,tests.clvm.test_chialisp_deserialization,tests.clvm.test_clvm_compilation,tests.clvm.test_program,tests.clvm.test_puzzle_compression,tests.clvm.test_puzzles,tests.clvm.test_serialized_program,tests.clvm.test_singletons,tests.clvm.test_spend_sim,tests.conftest,tests.connection_utils,tests.core.cmds.test_keys,tests.core.consensus.test_pot_iterations,tests.core.custom_types.test_coin,tests.core.custom_types.test_proof_of_space,tests.core.custom_types.test_spend_bundle,tests.core.daemon.test_daemon,tests.core.full_node.full_sync.test_full_sync,tests.core.full_node.stores.test_block_store,tests.core.full_node.stores.test_coin_store,tests.core.full_node.stores.test_full_node_store,tests.core.full_node.stores.test_hint_store,tests.core.full_node.stores.test_sync_store,tests.core.full_node.test_address_manager,tests.core.full_node.test_block_height_map,tests.core.full_node.test_conditions,tests.core.full_node.test_full_node,tests.core.full_node.test_mempool,tests.core.full_node.test_mempool_performance,tests.core.full_node.test_node_load,tests.core.full_node.test_peer_store_resolver,tests.core.full_node.test_performance,tests.core.full_node.test_transactions,tests.core.make_block_generator,tests.core.node_height,tests.core.server.test_dos,tests.core.server.test_rate_limits,tests.core.ssl.test_ssl,tests.core.test_cost_calculation,tests.core.test_crawler_rpc,tests.core.test_daemon_rpc,tests.core.test_db_conversion,tests.core.test_farmer_harvester_rpc,tests.core.test_filter,tests.core.test_full_node_rpc,tests.core.test_merkle_set,tests.core.test_setproctitle,tests.core.util.test_cached_bls,tests.core.util.test_config,tests.core.util.test_file_keyring_synchronization,tests.core.util.test_files,tests.core.util.test_keychain,tests.core.util.test_keyring_wrapper,tests.core.util.test_lru_cache,tests.core.util.test_significant_bits,tests.core.util.test_streamable,tests.core.util.test_type_checking,tests.farmer_harvester.test_farmer_harvester,tests.generator.test_compression,tests.generator.test_generator_types,tests.generator.test_list_to_batches,tests.generator.test_rom,tests.generator.test_scan,tests.plotting.test_plot_manager,tests.pools.test_pool_cmdline,tests.pools.test_pool_config,tests.pools.test_pool_puzzles_lifecycle,tests.pools.test_pool_rpc,tests.pools.test_wallet_pool_store,tests.setup_nodes,tests.setup_services,tests.simulation.test_simulation,tests.time_out_assert,tests.tools.test_full_sync,tests.tools.test_run_block,tests.util.alert_server,tests.util.benchmark_cost,tests.util.blockchain,tests.util.build_network_protocol_files,tests.util.db_connection,tests.util.generator_tools_testing,tests.util.keyring,tests.util.key_tool,tests.util.misc,tests.util.network,tests.util.rpc,tests.util.test_full_block_utils,tests.util.test_lock_queue,tests.util.test_network_protocol_files,tests.util.test_struct_stream,tests.wallet.cat_wallet.test_cat_lifecycle,tests.wallet.cat_wallet.test_cat_wallet,tests.wallet.cat_wallet.test_offer_lifecycle,tests.wallet.cat_wallet.test_trades,tests.wallet.did_wallet.test_did,tests.wallet.did_wallet.test_did_rpc,tests.wallet.rl_wallet.test_rl_rpc,tests.wallet.rl_wallet.test_rl_wallet,tests.wallet.rpc.test_wallet_rpc,tests.wallet.simple_sync.test_simple_sync_protocol,tests.wallet.sync.test_wallet_sync,tests.wallet.test_bech32m,tests.wallet.test_chialisp,tests.wallet.test_puzzle_store,tests.wallet.test_singleton,tests.wallet.test_singleton_lifecycle,tests.wallet.test_singleton_lifecycle_fast,tests.wallet.test_taproot,tests.wallet.test_wallet,tests.wallet.test_wallet_blockchain,tests.wallet.test_wallet_interested_store,tests.wallet.test_wallet_key_val_store,tests.wallet.test_wallet_user_store,tests.wallet_tools,tests.weight_proof.test_weight_proof,tools.analyze-chain,tools.run_block,tools.test_full_sync] disallow_any_generics = False disallow_subclassing_any = False disallow_untyped_calls = False diff --git a/tests/clvm/test_clvm_compilation.py b/tests/clvm/test_clvm_compilation.py index 412d244da722..b3d052176724 100644 --- a/tests/clvm/test_clvm_compilation.py +++ b/tests/clvm/test_clvm_compilation.py @@ -40,9 +40,6 @@ "chia/wallet/puzzles/delegated_tail.clvm", "chia/wallet/puzzles/settlement_payments.clvm", "chia/wallet/puzzles/genesis_by_coin_id.clvm", - "chia/wallet/puzzles/genesis-by-puzzle-hash-with-0.clvm", - "chia/wallet/puzzles/delegated_genesis_checker.clvm", - "chia/wallet/puzzles/genesis-by-coin-id-with-0.clvm", ] ) From ab712362154672a48024dbf27fbe80e58e363bde Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 Apr 2022 09:20:30 -0700 Subject: [PATCH 337/378] Bump cryptography from 3.4.7 to 36.0.2 (#10787) Bumps [cryptography](https://github.com/pyca/cryptography) from 3.4.7 to 36.0.2. - [Release notes](https://github.com/pyca/cryptography/releases) - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/3.4.7...36.0.2) --- updated-dependencies: - dependency-name: cryptography dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index df9ef8b411b2..8d4d20558bea 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ "colorama==0.4.4", # Colorizes terminal output "colorlog==6.6.0", # Adds color to logs "concurrent-log-handler==0.9.19", # Concurrently log and rotate logs - "cryptography==3.4.7", # Python cryptography library for TLS - keyring conflict + "cryptography==36.0.2", # Python cryptography library for TLS - keyring conflict "fasteners==0.16.3", # For interprocess file locking, expected to be replaced by filelock "filelock==3.4.2", # For reading and writing config multiprocess and multithread safely (non-reentrant locks) "keyring==23.0.1", # Store keys in MacOS Keychain, Windows Credential Locker From c8468bea0709eb7f71d0fc851511015cd6a448ce Mon Sep 17 00:00:00 2001 From: dustinface <35775977+xdustinface@users.noreply.github.com> Date: Thu, 7 Apr 2022 18:21:08 +0200 Subject: [PATCH 338/378] wallet: Improve logging in `create_more_puzzle_hashes` (#10761) * wallet: Improve logging in `create_more_puzzle_hashes` It's pretty spammy currently when scanning puzzle hashes. * scan -> create, Scanning -> Creating * `scanning_msg` -> `creating_msg` --- chia/wallet/wallet_state_manager.py | 76 ++++++++++++++++------------- 1 file changed, 41 insertions(+), 35 deletions(-) diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 1a51de491ce4..02a9a4e91323 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -266,44 +266,50 @@ async def create_more_puzzle_hashes(self, from_zero: bool = False, in_transactio # If the key was replaced (from_zero=True), we should generate the puzzle hashes for the new key if from_zero: start_index = 0 + last_index = unused + to_generate + if start_index >= last_index: + self.log.debug(f"Nothing to create for for wallet_id: {wallet_id}, index: {start_index}") + else: + creating_msg = f"Creating puzzle hashes from {start_index} to {last_index} for wallet_id: {wallet_id}" + self.log.info(f"Start: {creating_msg}") + for index in range(start_index, last_index): + if WalletType(target_wallet.type()) == WalletType.POOLING_WALLET: + continue - for index in range(start_index, unused + to_generate): - if WalletType(target_wallet.type()) == WalletType.POOLING_WALLET: - continue - - # Hardened - pubkey: G1Element = self.get_public_key(uint32(index)) - puzzle: Program = target_wallet.puzzle_for_pk(bytes(pubkey)) - if puzzle is None: - self.log.error(f"Unable to create puzzles with wallet {target_wallet}") - break - puzzlehash: bytes32 = puzzle.get_tree_hash() - self.log.info(f"Puzzle at index {index} wallet ID {wallet_id} puzzle hash {puzzlehash.hex()}") - derivation_paths.append( - DerivationRecord( - uint32(index), puzzlehash, pubkey, target_wallet.type(), uint32(target_wallet.id()), True + # Hardened + pubkey: G1Element = self.get_public_key(uint32(index)) + puzzle: Program = target_wallet.puzzle_for_pk(bytes(pubkey)) + if puzzle is None: + self.log.error(f"Unable to create puzzles with wallet {target_wallet}") + break + puzzlehash: bytes32 = puzzle.get_tree_hash() + self.log.debug(f"Puzzle at index {index} wallet ID {wallet_id} puzzle hash {puzzlehash.hex()}") + derivation_paths.append( + DerivationRecord( + uint32(index), puzzlehash, pubkey, target_wallet.type(), uint32(target_wallet.id()), True + ) ) - ) - # Unhardened - pubkey_unhardened: G1Element = self.get_public_key_unhardened(uint32(index)) - puzzle_unhardened: Program = target_wallet.puzzle_for_pk(bytes(pubkey_unhardened)) - if puzzle_unhardened is None: - self.log.error(f"Unable to create puzzles with wallet {target_wallet}") - break - puzzlehash_unhardened: bytes32 = puzzle_unhardened.get_tree_hash() - self.log.info( - f"Puzzle at index {index} wallet ID {wallet_id} puzzle hash {puzzlehash_unhardened.hex()}" - ) - derivation_paths.append( - DerivationRecord( - uint32(index), - puzzlehash_unhardened, - pubkey_unhardened, - target_wallet.type(), - uint32(target_wallet.id()), - False, + # Unhardened + pubkey_unhardened: G1Element = self.get_public_key_unhardened(uint32(index)) + puzzle_unhardened: Program = target_wallet.puzzle_for_pk(bytes(pubkey_unhardened)) + if puzzle_unhardened is None: + self.log.error(f"Unable to create puzzles with wallet {target_wallet}") + break + puzzlehash_unhardened: bytes32 = puzzle_unhardened.get_tree_hash() + self.log.debug( + f"Puzzle at index {index} wallet ID {wallet_id} puzzle hash {puzzlehash_unhardened.hex()}" ) - ) + derivation_paths.append( + DerivationRecord( + uint32(index), + puzzlehash_unhardened, + pubkey_unhardened, + target_wallet.type(), + uint32(target_wallet.id()), + False, + ) + ) + self.log.info(f"Done: {creating_msg}") await self.puzzle_store.add_derivation_paths(derivation_paths, in_transaction) await self.add_interested_puzzle_hashes( [record.puzzle_hash for record in derivation_paths], From ffd3b19315571ee5a55f3e75e9678964dac5ce1b Mon Sep 17 00:00:00 2001 From: Kronus91 Date: Thu, 7 Apr 2022 09:22:59 -0700 Subject: [PATCH 339/378] Add /cat_get_unacknowledged API for accessing unknown CATs (#10382) * Add /cat_get_unacknowledged API for accessing unknown CATs * Reformat & fix cast issue * Integration tested & add unit test * Handle optional uint32 * Reformat * Reformat * Reformat * Merge PR 10308 * Reformat * Fix concurrent issue * Add state change notification * rename API * Fix failing tests * Updated state_change name Co-authored-by: Jeff Cruikshank --- chia/rpc/wallet_rpc_api.py | 11 +++++ chia/rpc/wallet_rpc_client.py | 4 ++ chia/wallet/wallet_interested_store.py | 59 +++++++++++++++++++++++++- chia/wallet/wallet_state_manager.py | 12 +++++- tests/wallet/rpc/test_wallet_rpc.py | 6 +++ 5 files changed, 90 insertions(+), 2 deletions(-) diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index e4439ea3284c..3f43af809564 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -91,6 +91,7 @@ def get_routes(self) -> Dict[str, Callable]: "/cat_set_name": self.cat_set_name, "/cat_asset_id_to_name": self.cat_asset_id_to_name, "/cat_get_name": self.cat_get_name, + "/get_stray_cats": self.get_stray_cats, "/cat_spend": self.cat_spend, "/cat_get_asset_id": self.cat_get_asset_id, "/create_offer_for_ids": self.create_offer_for_ids, @@ -857,6 +858,16 @@ async def cat_get_name(self, request): name: str = await wallet.get_name() return {"wallet_id": wallet_id, "name": name} + async def get_stray_cats(self, request): + """ + Get a list of all unacknowledged CATs + :param request: RPC request + :return: A list of unacknowledged CATs + """ + assert self.service.wallet_state_manager is not None + cats = await self.service.wallet_state_manager.interested_store.get_unacknowledged_tokens() + return {"stray_cats": cats} + async def cat_spend(self, request): assert self.service.wallet_state_manager is not None diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index 8d8afb6d4f78..82058176ba95 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -363,6 +363,10 @@ async def get_cat_asset_id(self, wallet_id: str) -> bytes: } return bytes.fromhex((await self.fetch("cat_get_asset_id", request))["asset_id"]) + async def get_stray_cats(self) -> Dict: + response = await self.fetch("get_stray_cats", {}) + return response["stray_cats"] + async def cat_asset_id_to_name(self, asset_id: bytes32) -> Optional[Tuple[Optional[uint32], str]]: request: Dict[str, Any] = { "asset_id": asset_id.hex(), diff --git a/chia/wallet/wallet_interested_store.py b/chia/wallet/wallet_interested_store.py index c46437b77fb4..178288783295 100644 --- a/chia/wallet/wallet_interested_store.py +++ b/chia/wallet/wallet_interested_store.py @@ -4,6 +4,7 @@ from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.db_wrapper import DBWrapper +from chia.util.ints import uint32 class WalletInterestedStore: @@ -26,14 +27,21 @@ async def create(cls, wrapper: DBWrapper): await self.db_connection.execute( "CREATE TABLE IF NOT EXISTS interested_puzzle_hashes(puzzle_hash text PRIMARY KEY, wallet_id integer)" ) + + # Table for unknown CATs + fields = "asset_id text PRIMARY KEY, name text, first_seen_height integer, sender_puzzle_hash text" + await self.db_connection.execute(f"CREATE TABLE IF NOT EXISTS unacknowledged_asset_tokens({fields})") + await self.db_connection.commit() return self async def _clear_database(self): - cursor = await self.db_connection.execute("DELETE FROM puzzle_hashes") + cursor = await self.db_connection.execute("DELETE FROM interested_puzzle_hashes") await cursor.close() cursor = await self.db_connection.execute("DELETE FROM interested_coins") await cursor.close() + cursor = await self.db_connection.execute("DELETE FROM unacknowledged_asset_tokens") + await cursor.close() await self.db_connection.commit() async def get_interested_coin_ids(self) -> List[bytes32]: @@ -97,3 +105,52 @@ async def remove_interested_puzzle_hash(self, puzzle_hash: bytes32, in_transacti if not in_transaction: await self.db_connection.commit() self.db_wrapper.lock.release() + + async def add_unacknowledged_token( + self, + asset_id: bytes32, + name: str, + first_seen_height: Optional[uint32], + sender_puzzle_hash: bytes32, + in_transaction: bool = True, + ) -> None: + """ + Add an unacknowledged CAT to the database. It will only be inserted once at the first time. + :param asset_id: CAT asset ID + :param name: Name of the CAT, for now it will be unknown until we integrate the CAT name service + :param first_seen_height: The block height of the wallet received this CAT in the first time + :param sender_puzzle_hash: The puzzle hash of the sender + :param in_transaction: In transaction or not + :return: None + """ + if not in_transaction: + await self.db_wrapper.lock.acquire() + try: + cursor = await self.db_connection.execute( + "INSERT OR IGNORE INTO unacknowledged_asset_tokens VALUES (?, ?, ?, ?)", + ( + asset_id.hex(), + name, + first_seen_height if first_seen_height is not None else 0, + sender_puzzle_hash.hex(), + ), + ) + await cursor.close() + finally: + if not in_transaction: + await self.db_connection.commit() + self.db_wrapper.lock.release() + + async def get_unacknowledged_tokens(self) -> List: + """ + Get a list of all unacknowledged CATs + :return: A json style list of unacknowledged CATs + """ + cursor = await self.db_connection.execute( + "SELECT asset_id, name, first_seen_height, sender_puzzle_hash FROM unacknowledged_asset_tokens" + ) + cats = await cursor.fetchall() + return [ + {"asset_id": cat[0], "name": cat[1], "first_seen_height": cat[2], "sender_puzzle_hash": cat[3]} + for cat in cats + ] diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 02a9a4e91323..e7044beec904 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -572,7 +572,8 @@ async def fetch_parent_and_check_for_cat( self.log.info(f"Received state for the coin that doesn't belong to us {coin_state}") else: our_inner_puzzle: Program = self.main_wallet.puzzle_for_pk(bytes(derivation_record.pubkey)) - cat_puzzle = construct_cat_puzzle(CAT_MOD, bytes32(bytes(tail_hash)[1:]), our_inner_puzzle) + asset_id: bytes32 = bytes32(bytes(tail_hash)[1:]) + cat_puzzle = construct_cat_puzzle(CAT_MOD, asset_id, our_inner_puzzle) if cat_puzzle.get_tree_hash() != coin_state.coin.puzzle_hash: return None, None if bytes(tail_hash).hex()[2:] in self.default_cats or self.config.get( @@ -584,6 +585,15 @@ async def fetch_parent_and_check_for_cat( wallet_id = cat_wallet.id() wallet_type = WalletType(cat_wallet.type()) self.state_changed("wallet_created") + else: + # Found unacknowledged CAT, save it in the database. + await self.interested_store.add_unacknowledged_token( + asset_id, + CATWallet.default_wallet_name_for_unknown_cat(asset_id.hex()), + parent_coin_state.spent_height, + parent_coin_state.coin.puzzle_hash, + ) + self.state_changed("added_stray_cat") return wallet_id, wallet_type diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index f92570b31ab6..87eb78283944 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -477,6 +477,12 @@ async def eventual_balance_det(c, wallet_id: str): for i in range(0, 5): await client.farm_block(encode_puzzle_hash(ph_2, "txch")) await asyncio.sleep(0.5) + # Test unacknowledged CAT + await wallet_node.wallet_state_manager.interested_store.add_unacknowledged_token( + asset_id, "Unknown", uint32(10000), bytes.fromhex("ABCD") + ) + cats = await client.get_stray_cats() + assert len(cats) == 1 await time_out_assert(10, eventual_balance_det, 16, client, cat_0_id) await time_out_assert(10, eventual_balance_det, 4, client_2, cat_1_id) From 4c9a0edc64be00cd6ad2d31fe1628ebfa1fd7948 Mon Sep 17 00:00:00 2001 From: Mariano Sorgente <3069354+mariano54@users.noreply.github.com> Date: Thu, 7 Apr 2022 12:24:59 -0400 Subject: [PATCH 340/378] Increases the probability of connecting to local trusted node (#10633) --- chia/wallet/util/wallet_sync_utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/chia/wallet/util/wallet_sync_utils.py b/chia/wallet/util/wallet_sync_utils.py index 44675ee4d6b5..01e1a66ef6ac 100644 --- a/chia/wallet/util/wallet_sync_utils.py +++ b/chia/wallet/util/wallet_sync_utils.py @@ -41,7 +41,8 @@ async def fetch_last_tx_from_peer(height: uint32, peer: WSChiaConnection) -> Opt if response is not None and isinstance(response, RespondBlockHeader): if response.header_block.is_transaction_block: return response.header_block - else: + elif request_height < height: + # The peer might be slightly behind others but still synced, so we should allow fetching one more TX block break request_height = request_height - 1 return None From 8c0cdda880777bad86c6eaed45a1e061c5c5ee37 Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Thu, 7 Apr 2022 21:04:44 +0200 Subject: [PATCH 341/378] extend tests in test_blockchain to include more conditions, as well as ensuring consensus rules allow unknown condition parameters (#11079) --- tests/blockchain/test_blockchain.py | 127 +++++++++++++++++++++++----- 1 file changed, 106 insertions(+), 21 deletions(-) diff --git a/tests/blockchain/test_blockchain.py b/tests/blockchain/test_blockchain.py index 59631b7614ba..346822d44eb7 100644 --- a/tests/blockchain/test_blockchain.py +++ b/tests/blockchain/test_blockchain.py @@ -1711,6 +1711,86 @@ async def test_pre_validation(self, empty_blockchain, default_1000_blocks, bt): class TestBodyValidation: + + # TODO: add test for + # ASSERT_COIN_ANNOUNCEMENT, + # CREATE_COIN_ANNOUNCEMENT, + # CREATE_PUZZLE_ANNOUNCEMENT, + # ASSERT_PUZZLE_ANNOUNCEMENT, + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "opcode", + [ + ConditionOpcode.ASSERT_MY_AMOUNT, + ConditionOpcode.ASSERT_MY_PUZZLEHASH, + ConditionOpcode.ASSERT_MY_COIN_ID, + ConditionOpcode.ASSERT_MY_PARENT_ID, + ], + ) + @pytest.mark.parametrize("with_garbage", [True, False]) + async def test_conditions(self, empty_blockchain, opcode, with_garbage, bt): + b = empty_blockchain + blocks = bt.get_consecutive_blocks( + 3, + guarantee_transaction_block=True, + farmer_reward_puzzle_hash=bt.pool_ph, + pool_reward_puzzle_hash=bt.pool_ph, + genesis_timestamp=10000, + time_per_block=10, + ) + await _validate_and_add_block(empty_blockchain, blocks[0]) + await _validate_and_add_block(empty_blockchain, blocks[1]) + await _validate_and_add_block(empty_blockchain, blocks[2]) + + wt: WalletTool = bt.get_pool_wallet_tool() + + tx1: SpendBundle = wt.generate_signed_transaction( + 10, wt.get_new_puzzlehash(), list(blocks[-1].get_included_reward_coins())[0] + ) + coin1: Coin = tx1.additions()[0] + secret_key = wt.get_private_key_for_puzzle_hash(coin1.puzzle_hash) + synthetic_secret_key = calculate_synthetic_secret_key(secret_key, DEFAULT_HIDDEN_PUZZLE_HASH) + public_key = synthetic_secret_key.get_g1() + + if opcode == ConditionOpcode.ASSERT_MY_AMOUNT: + args = [int_to_bytes(coin1.amount)] + elif opcode == ConditionOpcode.ASSERT_MY_PUZZLEHASH: + args = [coin1.puzzle_hash] + elif opcode == ConditionOpcode.ASSERT_MY_COIN_ID: + args = [coin1.name()] + elif opcode == ConditionOpcode.ASSERT_MY_PARENT_ID: + args = [coin1.parent_coin_info] + # elif opcode == ConditionOpcode.RESERVE_FEE: + # args = [int_to_bytes(5)] + # TODO: since we use the production wallet code, we can't (easily) + # create a transaction with fee without also including a valid + # RESERVE_FEE condition + else: + assert False + + conditions = {opcode: [ConditionWithArgs(opcode, args + ([b"garbage"] if with_garbage else []))]} + + tx2: SpendBundle = wt.generate_signed_transaction(10, wt.get_new_puzzlehash(), coin1, condition_dic=conditions) + assert coin1 in tx2.removals() + coin2: Coin = tx2.additions()[0] + + bundles = SpendBundle.aggregate([tx1, tx2]) + blocks = bt.get_consecutive_blocks( + 1, + block_list_input=blocks, + guarantee_transaction_block=True, + transaction_data=bundles, + time_per_block=10, + ) + + pre_validation_results: List[PreValidationResult] = await b.pre_validate_blocks_multiprocessing( + [blocks[-1]], {}, validate_signatures=False + ) + # Ignore errors from pre-validation, we are testing block_body_validation + repl_preval_results = dataclasses.replace(pre_validation_results[0], error=None, required_iters=uint64(1)) + assert (await b.receive_block(blocks[-1], repl_preval_results))[0:-1] == (ReceiveBlockResult.NEW_PEAK, None, 2) + @pytest.mark.asyncio @pytest.mark.parametrize("opcode", [ConditionOpcode.AGG_SIG_ME, ConditionOpcode.AGG_SIG_UNSAFE]) @pytest.mark.parametrize( @@ -1744,9 +1824,7 @@ async def test_aggsig_garbage(self, empty_blockchain, opcode, with_garbage, expe synthetic_secret_key = calculate_synthetic_secret_key(secret_key, DEFAULT_HIDDEN_PUZZLE_HASH) public_key = synthetic_secret_key.get_g1() - args = [public_key, b"msg"] - if with_garbage: - args.append(b"garbage") + args = [public_key, b"msg"] + ([b"garbage"] if with_garbage else []) conditions = {opcode: [ConditionWithArgs(opcode, args)]} tx2: SpendBundle = wt.generate_signed_transaction(10, wt.get_new_puzzlehash(), coin1, condition_dic=conditions) @@ -1771,27 +1849,32 @@ async def test_aggsig_garbage(self, empty_blockchain, opcode, with_garbage, expe @pytest.mark.asyncio @pytest.mark.parametrize( - "opcode,lock_value,expected", + "opcode,lock_value,expected,with_garbage", [ - (ConditionOpcode.ASSERT_SECONDS_RELATIVE, -2, ReceiveBlockResult.NEW_PEAK), - (ConditionOpcode.ASSERT_SECONDS_RELATIVE, -1, ReceiveBlockResult.NEW_PEAK), - (ConditionOpcode.ASSERT_SECONDS_RELATIVE, 0, ReceiveBlockResult.NEW_PEAK), - (ConditionOpcode.ASSERT_SECONDS_RELATIVE, 1, ReceiveBlockResult.INVALID_BLOCK), - (ConditionOpcode.ASSERT_HEIGHT_RELATIVE, -2, ReceiveBlockResult.NEW_PEAK), - (ConditionOpcode.ASSERT_HEIGHT_RELATIVE, -1, ReceiveBlockResult.NEW_PEAK), - (ConditionOpcode.ASSERT_HEIGHT_RELATIVE, 0, ReceiveBlockResult.INVALID_BLOCK), - (ConditionOpcode.ASSERT_HEIGHT_RELATIVE, 1, ReceiveBlockResult.INVALID_BLOCK), - (ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, 2, ReceiveBlockResult.NEW_PEAK), - (ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, 3, ReceiveBlockResult.INVALID_BLOCK), - (ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, 4, ReceiveBlockResult.INVALID_BLOCK), + (ConditionOpcode.ASSERT_SECONDS_RELATIVE, -2, ReceiveBlockResult.NEW_PEAK, False), + (ConditionOpcode.ASSERT_SECONDS_RELATIVE, -1, ReceiveBlockResult.NEW_PEAK, False), + (ConditionOpcode.ASSERT_SECONDS_RELATIVE, 0, ReceiveBlockResult.NEW_PEAK, False), + (ConditionOpcode.ASSERT_SECONDS_RELATIVE, 1, ReceiveBlockResult.INVALID_BLOCK, False), + (ConditionOpcode.ASSERT_HEIGHT_RELATIVE, -2, ReceiveBlockResult.NEW_PEAK, False), + (ConditionOpcode.ASSERT_HEIGHT_RELATIVE, -1, ReceiveBlockResult.NEW_PEAK, False), + (ConditionOpcode.ASSERT_HEIGHT_RELATIVE, 0, ReceiveBlockResult.INVALID_BLOCK, False), + (ConditionOpcode.ASSERT_HEIGHT_RELATIVE, 1, ReceiveBlockResult.INVALID_BLOCK, False), + (ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, 2, ReceiveBlockResult.NEW_PEAK, False), + (ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, 3, ReceiveBlockResult.INVALID_BLOCK, False), + (ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, 4, ReceiveBlockResult.INVALID_BLOCK, False), # genesis timestamp is 10000 and each block is 10 seconds - (ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, 10029, ReceiveBlockResult.NEW_PEAK), - (ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, 10030, ReceiveBlockResult.NEW_PEAK), - (ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, 10031, ReceiveBlockResult.INVALID_BLOCK), - (ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, 10032, ReceiveBlockResult.INVALID_BLOCK), + (ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, 10029, ReceiveBlockResult.NEW_PEAK, False), + (ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, 10030, ReceiveBlockResult.NEW_PEAK, False), + (ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, 10031, ReceiveBlockResult.INVALID_BLOCK, False), + (ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, 10032, ReceiveBlockResult.INVALID_BLOCK, False), + # additional garbage at the end of parameters + (ConditionOpcode.ASSERT_SECONDS_RELATIVE, 0, ReceiveBlockResult.NEW_PEAK, True), + (ConditionOpcode.ASSERT_HEIGHT_RELATIVE, -1, ReceiveBlockResult.NEW_PEAK, True), + (ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, 2, ReceiveBlockResult.NEW_PEAK, True), + (ConditionOpcode.ASSERT_SECONDS_ABSOLUTE, 10029, ReceiveBlockResult.NEW_PEAK, True), ], ) - async def test_ephemeral_timelock(self, empty_blockchain, opcode, lock_value, expected, bt): + async def test_ephemeral_timelock(self, empty_blockchain, opcode, lock_value, expected, with_garbage, bt): b = empty_blockchain blocks = bt.get_consecutive_blocks( 3, @@ -1807,7 +1890,9 @@ async def test_ephemeral_timelock(self, empty_blockchain, opcode, lock_value, ex wt: WalletTool = bt.get_pool_wallet_tool() - conditions = {opcode: [ConditionWithArgs(opcode, [int_to_bytes(lock_value)])]} + conditions = { + opcode: [ConditionWithArgs(opcode, [int_to_bytes(lock_value)] + ([b"garbage"] if with_garbage else []))] + } tx1: SpendBundle = wt.generate_signed_transaction( 10, wt.get_new_puzzlehash(), list(blocks[-1].get_included_reward_coins())[0] From ded9f68583f13553ad6723e3c9424f5da1e978d0 Mon Sep 17 00:00:00 2001 From: dustinface <35775977+xdustinface@users.noreply.github.com> Date: Fri, 8 Apr 2022 02:10:44 +0200 Subject: [PATCH 342/378] chia|tests|github: Implement, integrate and test plot sync protocol (#9695) * protocols|server: Define new harvester plot refreshing protocol messages * protocols: Bump `protocol_version` to `0.0.34` * tests: Introduce `setup_farmer_multi_harvester` Allows to run a test setup with 1 farmer and mutiple harvesters. * plotting: Add an initial plot loading indication to `PlotManager` * plotting|tests: Don't add removed duplicates to `total_result.removed` `PlotRefreshResult.removed` should only contain plots that were loaded properly before they were removed. It shouldn't contain e.g. removed duplicates or invalid plots since those are synced in an extra sync step and not as diff but as whole list every time. * harvester: Reset `PlotManager` on shutdown * plot_sync: Implement plot sync protocol * farmer|harvester: Integrate and enable plot sync * tests: Implement tests for the plot sync protocol * farmer|tests: Drop obsolete harvester caching code * setup: Add `chia.plot_sync` to packages * plot_sync: Type hints in `DeltaType` * plot_sync: Drop parameters in `super()` calls * plot_sync: Introduce `send_response` helper in `Receiver._process` * plot_sync: Add some parentheses Co-authored-by: Kyle Altendorf * plot_sync: Additional hint for a `Receiver.process_path_list` parameter * plot_sync: Force named parameters in `Receiver.process_path_list` * test: Fix fixtures after rebase * tests: Fix sorting after rebase * tests: Return type hint for `plot_sync_setup` * tests: Rename `WSChiaConnection` and move it in the outer scope * tests|plot_sync: More type hints * tests: Rework some delta tests * tests: Drop a `range` and iterate over the list directly * tests: Use the proper flags to overwrite * test: More missing duplicates tests * tests: Drop `ExpectedResult.reset` * tests: Reduce some asserts * tests: Add messages to some `assert False` statements * tests: Introduce `ErrorSimulation` enum in `test_sync_simulated.py` * tests: Use `secrects` instead of `Crypto.Random` * Fixes after rebase * Import from `typing_extensions` to support python 3.7 * Drop task name to support python 3.7 * Introduce `Sender.syncing`, `Sender.connected` and a log about the task * Add `tests/plot_sync/config.py` * Align the multi harvester fixture with what we do in other places * Update the workflows Co-authored-by: Kyle Altendorf --- .../workflows/build-test-macos-plot_sync.yml | 107 ++++ .../workflows/build-test-ubuntu-plot_sync.yml | 109 ++++ chia/farmer/farmer.py | 113 +--- chia/farmer/farmer_api.py | 43 +- chia/harvester/harvester.py | 17 +- chia/harvester/harvester_api.py | 14 +- chia/plot_sync/__init__.py | 0 chia/plot_sync/delta.py | 59 ++ chia/plot_sync/exceptions.py | 54 ++ chia/plot_sync/receiver.py | 304 ++++++++++ chia/plot_sync/sender.py | 327 +++++++++++ chia/plot_sync/util.py | 27 + chia/plotting/manager.py | 10 +- chia/protocols/harvester_protocol.py | 79 ++- chia/protocols/protocol_message_types.py | 8 + chia/protocols/shared_protocol.py | 2 +- chia/server/rate_limits.py | 8 + setup.py | 1 + tests/conftest.py | 22 + tests/core/test_farmer_harvester_rpc.py | 10 - tests/plot_sync/__init__.py | 0 tests/plot_sync/config.py | 2 + tests/plot_sync/test_delta.py | 90 +++ tests/plot_sync/test_plot_sync.py | 537 ++++++++++++++++++ tests/plot_sync/test_receiver.py | 376 ++++++++++++ tests/plot_sync/test_sender.py | 102 ++++ tests/plot_sync/test_sync_simulated.py | 433 ++++++++++++++ tests/plot_sync/util.py | 53 ++ tests/plotting/test_plot_manager.py | 17 +- tests/setup_nodes.py | 65 ++- tests/setup_services.py | 20 +- 31 files changed, 2877 insertions(+), 132 deletions(-) create mode 100644 .github/workflows/build-test-macos-plot_sync.yml create mode 100644 .github/workflows/build-test-ubuntu-plot_sync.yml create mode 100644 chia/plot_sync/__init__.py create mode 100644 chia/plot_sync/delta.py create mode 100644 chia/plot_sync/exceptions.py create mode 100644 chia/plot_sync/receiver.py create mode 100644 chia/plot_sync/sender.py create mode 100644 chia/plot_sync/util.py create mode 100644 tests/plot_sync/__init__.py create mode 100644 tests/plot_sync/config.py create mode 100644 tests/plot_sync/test_delta.py create mode 100644 tests/plot_sync/test_plot_sync.py create mode 100644 tests/plot_sync/test_receiver.py create mode 100644 tests/plot_sync/test_sender.py create mode 100644 tests/plot_sync/test_sync_simulated.py create mode 100644 tests/plot_sync/util.py diff --git a/.github/workflows/build-test-macos-plot_sync.yml b/.github/workflows/build-test-macos-plot_sync.yml new file mode 100644 index 000000000000..d18e76cf7d20 --- /dev/null +++ b/.github/workflows/build-test-macos-plot_sync.yml @@ -0,0 +1,107 @@ +# +# THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme +# +name: MacOS plot_sync Tests + +on: + push: + branches: + - main + tags: + - '**' + pull_request: + branches: + - '**' + +concurrency: + # SHA is added to the end if on `main` to let all main workflows run + group: ${{ github.ref }}-${{ github.workflow }}-${{ github.event_name }}-${{ github.ref == 'refs/heads/main' && github.sha || '' }} + cancel-in-progress: true + +jobs: + build: + name: MacOS plot_sync Tests + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + fail-fast: false + max-parallel: 4 + matrix: + python-version: [3.8, 3.9] + os: [macOS-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_plot_sync + + steps: + - name: Checkout Code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Setup Python environment + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Create keychain for CI use + run: | + security create-keychain -p foo chiachain + security default-keychain -s chiachain + security unlock-keychain -p foo chiachain + security set-keychain-settings -t 7200 -u chiachain + + - name: Get pip cache dir + id: pip-cache + run: | + echo "::set-output name=dir::$(pip cache dir)" + + - name: Cache pip + uses: actions/cache@v3 + with: + # Note that new runners may break this https://github.com/actions/cache/issues/292 + path: ${{ steps.pip-cache.outputs.dir }} + key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Checkout test blocks and plots + uses: actions/checkout@v3 + with: + repository: 'Chia-Network/test-cache' + path: '.chia' + ref: '0.28.0' + fetch-depth: 1 + + - name: Run install script + env: + INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} + run: | + brew install boost + sh install.sh -d + +# Omitted installing Timelord + + - name: Test plot_sync code with pytest + run: | + . ./activate + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/plot_sync/test_*.py --durations=10 -n 4 -m "not benchmark" + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error +# +# THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme +# diff --git a/.github/workflows/build-test-ubuntu-plot_sync.yml b/.github/workflows/build-test-ubuntu-plot_sync.yml new file mode 100644 index 000000000000..886573b5743d --- /dev/null +++ b/.github/workflows/build-test-ubuntu-plot_sync.yml @@ -0,0 +1,109 @@ +# +# THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme +# +name: Ubuntu plot_sync Test + +on: + push: + branches: + - main + tags: + - '**' + pull_request: + branches: + - '**' + +concurrency: + # SHA is added to the end if on `main` to let all main workflows run + group: ${{ github.ref }}-${{ github.workflow }}-${{ github.event_name }}-${{ github.ref == 'refs/heads/main' && github.sha || '' }} + cancel-in-progress: true + +jobs: + build: + name: Ubuntu plot_sync Test + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + fail-fast: false + max-parallel: 4 + matrix: + python-version: [3.7, 3.8, 3.9] + os: [ubuntu-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_plot_sync + + steps: + - name: Checkout Code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Setup Python environment + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Cache npm + uses: actions/cache@v3 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + + - name: Get pip cache dir + id: pip-cache + run: | + echo "::set-output name=dir::$(pip cache dir)" + + - name: Cache pip + uses: actions/cache@v3 + with: + path: ${{ steps.pip-cache.outputs.dir }} + key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Checkout test blocks and plots + uses: actions/checkout@v3 + with: + repository: 'Chia-Network/test-cache' + path: '.chia' + ref: '0.28.0' + fetch-depth: 1 + + - name: Run install script + env: + INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} + run: | + sh install.sh -d + +# Omitted installing Timelord + + - name: Test plot_sync code with pytest + run: | + . ./activate + venv/bin/coverage run --rcfile=.coveragerc ./venv/bin/py.test tests/plot_sync/test_*.py --durations=10 -n 4 -m "not benchmark" -p no:monitor + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v2 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error + +# Omitted resource usage check + +# +# THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme +# diff --git a/chia/farmer/farmer.py b/chia/farmer/farmer.py index 33f3b973a727..f7e50e9ed89e 100644 --- a/chia/farmer/farmer.py +++ b/chia/farmer/farmer.py @@ -18,6 +18,8 @@ connect_to_keychain_and_validate, wrap_local_keychain, ) +from chia.plot_sync.receiver import Receiver +from chia.plot_sync.delta import Delta from chia.pools.pool_config import PoolWalletConfig, load_pool_config, add_auth_key from chia.protocols import farmer_protocol, harvester_protocol from chia.protocols.pool_protocol import ( @@ -59,32 +61,12 @@ UPDATE_POOL_INFO_INTERVAL: int = 3600 UPDATE_POOL_FARMER_INFO_INTERVAL: int = 300 -UPDATE_HARVESTER_CACHE_INTERVAL: int = 90 """ HARVESTER PROTOCOL (FARMER <-> HARVESTER) """ -class HarvesterCacheEntry: - def __init__(self): - self.data: Optional[dict] = None - self.last_update: float = 0 - - def bump_last_update(self): - self.last_update = time.time() - - def set_data(self, data): - self.data = data - self.bump_last_update() - - def needs_update(self, update_interval: int): - return time.time() - self.last_update > update_interval - - def expired(self, update_interval: int): - return time.time() - self.last_update > update_interval * 10 - - class Farmer: def __init__( self, @@ -115,8 +97,7 @@ def __init__( # to periodically clear the memory self.cache_add_time: Dict[bytes32, uint64] = {} - # Interval to request plots from connected harvesters - self.update_harvester_cache_interval = UPDATE_HARVESTER_CACHE_INTERVAL + self.plot_sync_receivers: Dict[bytes32, Receiver] = {} self.cache_clear_task: Optional[asyncio.Task] = None self.update_pool_state_task: Optional[asyncio.Task] = None @@ -137,8 +118,6 @@ def __init__( # Last time we updated pool_state based on the config file self.last_config_access_time: uint64 = uint64(0) - self.harvester_cache: Dict[str, Dict[str, HarvesterCacheEntry]] = {} - async def ensure_keychain_proxy(self) -> KeychainProxy: if self.keychain_proxy is None: if self.local_keychain: @@ -256,6 +235,7 @@ async def handshake_task(): self.harvester_handshake_task = None if peer.connection_type is NodeType.HARVESTER: + self.plot_sync_receivers[peer.peer_node_id] = Receiver(peer, self.plot_sync_callback) self.harvester_handshake_task = asyncio.create_task(handshake_task()) def set_server(self, server): @@ -274,6 +254,13 @@ def handle_failed_pool_response(self, p2_singleton_puzzle_hash: bytes32, error_m def on_disconnect(self, connection: ws.WSChiaConnection): self.log.info(f"peer disconnected {connection.get_peer_logging()}") self.state_changed("close_connection", {}) + if connection.connection_type is NodeType.HARVESTER: + del self.plot_sync_receivers[connection.peer_node_id] + + async def plot_sync_callback(self, peer_id: bytes32, delta: Delta) -> None: + log.info(f"plot_sync_callback: peer_id {peer_id}, delta {delta}") + if not delta.empty(): + self.state_changed("new_plots", await self.get_harvesters()) async def _pool_get_pool_info(self, pool_config: PoolWalletConfig) -> Optional[Dict]: try: @@ -642,80 +629,17 @@ async def generate_login_link(self, launcher_id: bytes32) -> Optional[str]: return None - async def update_cached_harvesters(self) -> bool: - # First remove outdated cache entries - self.log.debug(f"update_cached_harvesters cache entries: {len(self.harvester_cache)}") - remove_hosts = [] - for host, host_cache in self.harvester_cache.items(): - remove_peers = [] - for peer_id, peer_cache in host_cache.items(): - # If the peer cache is expired it means the harvester didn't respond for too long - if peer_cache.expired(self.update_harvester_cache_interval): - remove_peers.append(peer_id) - for key in remove_peers: - del host_cache[key] - if len(host_cache) == 0: - self.log.debug(f"update_cached_harvesters remove host: {host}") - remove_hosts.append(host) - for key in remove_hosts: - del self.harvester_cache[key] - # Now query each harvester and update caches - updated = False - for connection in self.server.get_connections(NodeType.HARVESTER): - cache_entry = await self.get_cached_harvesters(connection) - if cache_entry.needs_update(self.update_harvester_cache_interval): - self.log.debug(f"update_cached_harvesters update harvester: {connection.peer_node_id}") - cache_entry.bump_last_update() - response = await connection.request_plots( - harvester_protocol.RequestPlots(), timeout=self.update_harvester_cache_interval - ) - if response is not None: - if isinstance(response, harvester_protocol.RespondPlots): - new_data: Dict = response.to_json_dict() - if cache_entry.data != new_data: - updated = True - self.log.debug(f"update_cached_harvesters cache updated: {connection.peer_node_id}") - else: - self.log.debug(f"update_cached_harvesters no changes for: {connection.peer_node_id}") - cache_entry.set_data(new_data) - else: - self.log.error( - f"Invalid response from harvester:" - f"peer_host {connection.peer_host}, peer_node_id {connection.peer_node_id}" - ) - else: - self.log.error( - f"Harvester '{connection.peer_host}/{connection.peer_node_id}' did not respond: " - f"(version mismatch or time out {UPDATE_HARVESTER_CACHE_INTERVAL}s)" - ) - return updated - - async def get_cached_harvesters(self, connection: WSChiaConnection) -> HarvesterCacheEntry: - host_cache = self.harvester_cache.get(connection.peer_host) - if host_cache is None: - host_cache = {} - self.harvester_cache[connection.peer_host] = host_cache - node_cache = host_cache.get(connection.peer_node_id.hex()) - if node_cache is None: - node_cache = HarvesterCacheEntry() - host_cache[connection.peer_node_id.hex()] = node_cache - return node_cache - async def get_harvesters(self) -> Dict: harvesters: List = [] for connection in self.server.get_connections(NodeType.HARVESTER): self.log.debug(f"get_harvesters host: {connection.peer_host}, node_id: {connection.peer_node_id}") - cache_entry = await self.get_cached_harvesters(connection) - if cache_entry.data is not None: - harvester_object: dict = dict(cache_entry.data) - harvester_object["connection"] = { - "node_id": connection.peer_node_id.hex(), - "host": connection.peer_host, - "port": connection.peer_port, - } - harvesters.append(harvester_object) + receiver = self.plot_sync_receivers.get(connection.peer_node_id) + if receiver is not None: + harvesters.append(receiver.to_dict()) else: - self.log.debug(f"get_harvesters no cache: {connection.peer_host}, node_id: {connection.peer_node_id}") + self.log.debug( + f"get_harvesters invalid peer: {connection.peer_host}, node_id: {connection.peer_node_id}" + ) return {"harvesters": harvesters} @@ -766,9 +690,6 @@ async def _periodically_clear_cache_and_refresh_task(self): self.state_changed("add_connection", {}) refresh_slept = 0 - # Handles harvester plots cache cleanup and updates - if await self.update_cached_harvesters(): - self.state_changed("new_plots", await self.get_harvesters()) except Exception: log.error(f"_periodically_clear_cache_and_refresh_task failed: {traceback.format_exc()}") diff --git a/chia/farmer/farmer_api.py b/chia/farmer/farmer_api.py index d0cb11577e15..9e94a5052334 100644 --- a/chia/farmer/farmer_api.py +++ b/chia/farmer/farmer_api.py @@ -11,7 +11,13 @@ from chia.consensus.pot_iterations import calculate_iterations_quality, calculate_sp_interval_iters from chia.farmer.farmer import Farmer from chia.protocols import farmer_protocol, harvester_protocol -from chia.protocols.harvester_protocol import PoolDifficulty +from chia.protocols.harvester_protocol import ( + PoolDifficulty, + PlotSyncStart, + PlotSyncPlotList, + PlotSyncPathList, + PlotSyncDone, +) from chia.protocols.pool_protocol import ( get_current_authentication_token, PoolErrorCode, @@ -518,3 +524,38 @@ async def farming_info(self, request: farmer_protocol.FarmingInfo): @peer_required async def respond_plots(self, _: harvester_protocol.RespondPlots, peer: ws.WSChiaConnection): self.farmer.log.warning(f"Respond plots came too late from: {peer.get_peer_logging()}") + + @api_request + @peer_required + async def plot_sync_start(self, message: PlotSyncStart, peer: ws.WSChiaConnection): + await self.farmer.plot_sync_receivers[peer.peer_node_id].sync_started(message) + + @api_request + @peer_required + async def plot_sync_loaded(self, message: PlotSyncPlotList, peer: ws.WSChiaConnection): + await self.farmer.plot_sync_receivers[peer.peer_node_id].process_loaded(message) + + @api_request + @peer_required + async def plot_sync_removed(self, message: PlotSyncPathList, peer: ws.WSChiaConnection): + await self.farmer.plot_sync_receivers[peer.peer_node_id].process_removed(message) + + @api_request + @peer_required + async def plot_sync_invalid(self, message: PlotSyncPathList, peer: ws.WSChiaConnection): + await self.farmer.plot_sync_receivers[peer.peer_node_id].process_invalid(message) + + @api_request + @peer_required + async def plot_sync_keys_missing(self, message: PlotSyncPathList, peer: ws.WSChiaConnection): + await self.farmer.plot_sync_receivers[peer.peer_node_id].process_keys_missing(message) + + @api_request + @peer_required + async def plot_sync_duplicates(self, message: PlotSyncPathList, peer: ws.WSChiaConnection): + await self.farmer.plot_sync_receivers[peer.peer_node_id].process_duplicates(message) + + @api_request + @peer_required + async def plot_sync_done(self, message: PlotSyncDone, peer: ws.WSChiaConnection): + await self.farmer.plot_sync_receivers[peer.peer_node_id].sync_done(message) diff --git a/chia/harvester/harvester.py b/chia/harvester/harvester.py index b63b0c7a16d7..fab6a77da9ca 100644 --- a/chia/harvester/harvester.py +++ b/chia/harvester/harvester.py @@ -8,6 +8,7 @@ import chia.server.ws_connection as ws # lgtm [py/import-and-import-from] from chia.consensus.constants import ConsensusConstants +from chia.plot_sync.sender import Sender from chia.plotting.manager import PlotManager from chia.plotting.util import ( add_plot_directory, @@ -25,6 +26,7 @@ class Harvester: plot_manager: PlotManager + plot_sync_sender: Sender root_path: Path _is_shutdown: bool executor: ThreadPoolExecutor @@ -53,6 +55,7 @@ def __init__(self, root_path: Path, config: Dict, constants: ConsensusConstants) self.plot_manager = PlotManager( root_path, refresh_parameter=refresh_parameter, refresh_callback=self._plot_refresh_callback ) + self.plot_sync_sender = Sender(self.plot_manager) self._is_shutdown = False self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=config["num_threads"]) self.state_changed_callback = None @@ -70,9 +73,11 @@ def _close(self): self._is_shutdown = True self.executor.shutdown(wait=True) self.plot_manager.stop_refreshing() + self.plot_manager.reset() + self.plot_sync_sender.stop() async def _await_closed(self): - pass + await self.plot_sync_sender.await_closed() def _set_state_changed_callback(self, callback: Callable): self.state_changed_callback = callback @@ -90,12 +95,18 @@ def _plot_refresh_callback(self, event: PlotRefreshEvents, update_result: PlotRe f"duration: {update_result.duration:.2f} seconds, " f"total plots: {len(self.plot_manager.plots)}" ) - if len(update_result.loaded) > 0: - self.event_loop.call_soon_threadsafe(self._state_changed, "plots") + if event == PlotRefreshEvents.started: + self.plot_sync_sender.sync_start(update_result.remaining, self.plot_manager.initial_refresh()) + if event == PlotRefreshEvents.batch_processed: + self.plot_sync_sender.process_batch(update_result.loaded, update_result.remaining) + if event == PlotRefreshEvents.done: + self.plot_sync_sender.sync_done(update_result.removed, update_result.duration) def on_disconnect(self, connection: ws.WSChiaConnection): self.log.info(f"peer disconnected {connection.get_peer_logging()}") self._state_changed("close_connection") + self.plot_manager.stop_refreshing() + self.plot_sync_sender.stop() def get_plots(self) -> Tuple[List[Dict], List[str], List[str]]: self.log.debug(f"get_plots prover items: {self.plot_manager.plot_count()}") diff --git a/chia/harvester/harvester_api.py b/chia/harvester/harvester_api.py index 760a57cb8c45..88528678594d 100644 --- a/chia/harvester/harvester_api.py +++ b/chia/harvester/harvester_api.py @@ -10,7 +10,7 @@ from chia.plotting.util import PlotInfo, parse_plot_info from chia.protocols import harvester_protocol from chia.protocols.farmer_protocol import FarmingInfo -from chia.protocols.harvester_protocol import Plot +from chia.protocols.harvester_protocol import Plot, PlotSyncResponse from chia.protocols.protocol_message_types import ProtocolMessageTypes from chia.server.outbound_message import make_msg from chia.server.ws_connection import WSChiaConnection @@ -30,8 +30,11 @@ def __init__(self, harvester: Harvester): def _set_state_changed_callback(self, callback: Callable): self.harvester.state_changed_callback = callback + @peer_required @api_request - async def harvester_handshake(self, harvester_handshake: harvester_protocol.HarvesterHandshake): + async def harvester_handshake( + self, harvester_handshake: harvester_protocol.HarvesterHandshake, peer: WSChiaConnection + ): """ Handshake between the harvester and farmer. The harvester receives the pool public keys, as well as the farmer pks, which must be put into the plots, before the plotting process begins. @@ -40,7 +43,8 @@ async def harvester_handshake(self, harvester_handshake: harvester_protocol.Harv self.harvester.plot_manager.set_public_keys( harvester_handshake.farmer_public_keys, harvester_handshake.pool_public_keys ) - + self.harvester.plot_sync_sender.set_connection(peer) + await self.harvester.plot_sync_sender.start() self.harvester.plot_manager.start_refreshing() @peer_required @@ -289,3 +293,7 @@ async def request_plots(self, _: harvester_protocol.RequestPlots): response = harvester_protocol.RespondPlots(plots_response, failed_to_open_filenames, no_key_filenames) return make_msg(ProtocolMessageTypes.respond_plots, response) + + @api_request + async def plot_sync_response(self, response: PlotSyncResponse): + self.harvester.plot_sync_sender.set_response(response) diff --git a/chia/plot_sync/__init__.py b/chia/plot_sync/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/chia/plot_sync/delta.py b/chia/plot_sync/delta.py new file mode 100644 index 000000000000..6a797ffc33f1 --- /dev/null +++ b/chia/plot_sync/delta.py @@ -0,0 +1,59 @@ +from dataclasses import dataclass, field +from typing import Dict, List, Union + +from chia.protocols.harvester_protocol import Plot + + +@dataclass +class DeltaType: + additions: Union[Dict[str, Plot], List[str]] + removals: List[str] + + def __str__(self) -> str: + return f"+{len(self.additions)}/-{len(self.removals)}" + + def clear(self) -> None: + self.additions.clear() + self.removals.clear() + + def empty(self) -> bool: + return len(self.additions) == 0 and len(self.removals) == 0 + + +@dataclass +class PlotListDelta(DeltaType): + additions: Dict[str, Plot] = field(default_factory=dict) + removals: List[str] = field(default_factory=list) + + +@dataclass +class PathListDelta(DeltaType): + additions: List[str] = field(default_factory=list) + removals: List[str] = field(default_factory=list) + + @staticmethod + def from_lists(old: List[str], new: List[str]) -> "PathListDelta": + return PathListDelta([x for x in new if x not in old], [x for x in old if x not in new]) + + +@dataclass +class Delta: + valid: PlotListDelta = field(default_factory=PlotListDelta) + invalid: PathListDelta = field(default_factory=PathListDelta) + keys_missing: PathListDelta = field(default_factory=PathListDelta) + duplicates: PathListDelta = field(default_factory=PathListDelta) + + def empty(self) -> bool: + return self.valid.empty() and self.invalid.empty() and self.keys_missing.empty() and self.duplicates.empty() + + def __str__(self) -> str: + return ( + f"valid {self.valid}, invalid {self.invalid}, keys missing: {self.keys_missing}, " + f"duplicates: {self.duplicates}" + ) + + def clear(self) -> None: + self.valid.clear() + self.invalid.clear() + self.keys_missing.clear() + self.duplicates.clear() diff --git a/chia/plot_sync/exceptions.py b/chia/plot_sync/exceptions.py new file mode 100644 index 000000000000..e972a2a2935d --- /dev/null +++ b/chia/plot_sync/exceptions.py @@ -0,0 +1,54 @@ +from typing import Any + +from chia.plot_sync.util import ErrorCodes, State +from chia.protocols.harvester_protocol import PlotSyncIdentifier +from chia.server.ws_connection import NodeType +from chia.util.ints import uint64 + + +class PlotSyncException(Exception): + def __init__(self, message: str, error_code: ErrorCodes) -> None: + super().__init__(message) + self.error_code = error_code + + +class AlreadyStartedError(Exception): + def __init__(self) -> None: + super().__init__("Already started!") + + +class InvalidValueError(PlotSyncException): + def __init__(self, message: str, actual: Any, expected: Any, error_code: ErrorCodes) -> None: + super().__init__(f"{message}: Actual {actual}, Expected {expected}", error_code) + + +class InvalidIdentifierError(InvalidValueError): + def __init__(self, actual_identifier: PlotSyncIdentifier, expected_identifier: PlotSyncIdentifier) -> None: + super().__init__("Invalid identifier", actual_identifier, expected_identifier, ErrorCodes.invalid_identifier) + self.actual_identifier: PlotSyncIdentifier = actual_identifier + self.expected_identifier: PlotSyncIdentifier = expected_identifier + + +class InvalidLastSyncIdError(InvalidValueError): + def __init__(self, actual: uint64, expected: uint64) -> None: + super().__init__("Invalid last-sync-id", actual, expected, ErrorCodes.invalid_last_sync_id) + + +class InvalidConnectionTypeError(InvalidValueError): + def __init__(self, actual: NodeType, expected: NodeType) -> None: + super().__init__("Unexpected connection type", actual, expected, ErrorCodes.invalid_connection_type) + + +class PlotAlreadyAvailableError(PlotSyncException): + def __init__(self, state: State, path: str) -> None: + super().__init__(f"{state.name}: Plot already available - {path}", ErrorCodes.plot_already_available) + + +class PlotNotAvailableError(PlotSyncException): + def __init__(self, state: State, path: str) -> None: + super().__init__(f"{state.name}: Plot not available - {path}", ErrorCodes.plot_not_available) + + +class SyncIdsMatchError(PlotSyncException): + def __init__(self, state: State, sync_id: uint64) -> None: + super().__init__(f"{state.name}: Sync ids are equal - {sync_id}", ErrorCodes.sync_ids_match) diff --git a/chia/plot_sync/receiver.py b/chia/plot_sync/receiver.py new file mode 100644 index 000000000000..4df791b53112 --- /dev/null +++ b/chia/plot_sync/receiver.py @@ -0,0 +1,304 @@ +import logging +import time +from typing import Any, Callable, Collection, Coroutine, Dict, List, Optional + +from chia.plot_sync.delta import Delta, PathListDelta, PlotListDelta +from chia.plot_sync.exceptions import ( + InvalidIdentifierError, + InvalidLastSyncIdError, + PlotAlreadyAvailableError, + PlotNotAvailableError, + PlotSyncException, + SyncIdsMatchError, +) +from chia.plot_sync.util import ErrorCodes, State +from chia.protocols.harvester_protocol import ( + Plot, + PlotSyncDone, + PlotSyncError, + PlotSyncIdentifier, + PlotSyncPathList, + PlotSyncPlotList, + PlotSyncResponse, + PlotSyncStart, +) +from chia.server.ws_connection import ProtocolMessageTypes, WSChiaConnection, make_msg +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.util.ints import int16, uint64 +from chia.util.streamable import _T_Streamable + +log = logging.getLogger(__name__) + + +class Receiver: + _connection: WSChiaConnection + _sync_state: State + _delta: Delta + _expected_sync_id: uint64 + _expected_message_id: uint64 + _last_sync_id: uint64 + _last_sync_time: float + _plots: Dict[str, Plot] + _invalid: List[str] + _keys_missing: List[str] + _duplicates: List[str] + _update_callback: Callable[[bytes32, Delta], Coroutine[Any, Any, None]] + + def __init__( + self, connection: WSChiaConnection, update_callback: Callable[[bytes32, Delta], Coroutine[Any, Any, None]] + ) -> None: + self._connection = connection + self._sync_state = State.idle + self._delta = Delta() + self._expected_sync_id = uint64(0) + self._expected_message_id = uint64(0) + self._last_sync_id = uint64(0) + self._last_sync_time = 0 + self._plots = {} + self._invalid = [] + self._keys_missing = [] + self._duplicates = [] + self._update_callback = update_callback # type: ignore[assignment, misc] + + def reset(self) -> None: + self._sync_state = State.idle + self._expected_sync_id = uint64(0) + self._expected_message_id = uint64(0) + self._last_sync_id = uint64(0) + self._last_sync_time = 0 + self._plots.clear() + self._invalid.clear() + self._keys_missing.clear() + self._duplicates.clear() + self._delta.clear() + + def bump_expected_message_id(self) -> None: + self._expected_message_id = uint64(self._expected_message_id + 1) + + def connection(self) -> WSChiaConnection: + return self._connection + + def state(self) -> State: + return self._sync_state + + def expected_sync_id(self) -> uint64: + return self._expected_sync_id + + def expected_message_id(self) -> uint64: + return self._expected_message_id + + def last_sync_id(self) -> uint64: + return self._last_sync_id + + def last_sync_time(self) -> float: + return self._last_sync_time + + def plots(self) -> Dict[str, Plot]: + return self._plots + + def invalid(self) -> List[str]: + return self._invalid + + def keys_missing(self) -> List[str]: + return self._keys_missing + + def duplicates(self) -> List[str]: + return self._duplicates + + async def _process( + self, method: Callable[[_T_Streamable], Any], message_type: ProtocolMessageTypes, message: Any + ) -> None: + async def send_response(plot_sync_error: Optional[PlotSyncError] = None) -> None: + if self._connection is not None: + await self._connection.send_message( + make_msg( + ProtocolMessageTypes.plot_sync_response, + PlotSyncResponse(message.identifier, int16(message_type.value), plot_sync_error), + ) + ) + + try: + await method(message) + await send_response() + except InvalidIdentifierError as e: + log.warning(f"_process: InvalidIdentifierError {e}") + await send_response(PlotSyncError(int16(e.error_code), f"{e}", e.expected_identifier)) + except PlotSyncException as e: + log.warning(f"_process: Error {e}") + await send_response(PlotSyncError(int16(e.error_code), f"{e}", None)) + except Exception as e: + log.warning(f"_process: Exception {e}") + await send_response(PlotSyncError(int16(ErrorCodes.unknown), f"{e}", None)) + + def _validate_identifier(self, identifier: PlotSyncIdentifier, start: bool = False) -> None: + sync_id_match = identifier.sync_id == self._expected_sync_id + message_id_match = identifier.message_id == self._expected_message_id + identifier_match = sync_id_match and message_id_match + if (start and not message_id_match) or (not start and not identifier_match): + expected: PlotSyncIdentifier = PlotSyncIdentifier( + identifier.timestamp, self._expected_sync_id, self._expected_message_id + ) + raise InvalidIdentifierError( + identifier, + expected, + ) + + async def _sync_started(self, data: PlotSyncStart) -> None: + if data.initial: + self.reset() + self._validate_identifier(data.identifier, True) + if data.last_sync_id != self.last_sync_id(): + raise InvalidLastSyncIdError(data.last_sync_id, self.last_sync_id()) + if data.last_sync_id == data.identifier.sync_id: + raise SyncIdsMatchError(State.idle, data.last_sync_id) + self._expected_sync_id = data.identifier.sync_id + self._delta.clear() + self._sync_state = State.loaded + self.bump_expected_message_id() + + async def sync_started(self, data: PlotSyncStart) -> None: + await self._process(self._sync_started, ProtocolMessageTypes.plot_sync_start, data) + + async def _process_loaded(self, plot_infos: PlotSyncPlotList) -> None: + self._validate_identifier(plot_infos.identifier) + + for plot_info in plot_infos.data: + if plot_info.filename in self._plots or plot_info.filename in self._delta.valid.additions: + raise PlotAlreadyAvailableError(State.loaded, plot_info.filename) + self._delta.valid.additions[plot_info.filename] = plot_info + + if plot_infos.final: + self._sync_state = State.removed + + self.bump_expected_message_id() + + async def process_loaded(self, plot_infos: PlotSyncPlotList) -> None: + await self._process(self._process_loaded, ProtocolMessageTypes.plot_sync_loaded, plot_infos) + + async def process_path_list( + self, + *, + state: State, + next_state: State, + target: Collection[str], + delta: List[str], + paths: PlotSyncPathList, + is_removal: bool = False, + ) -> None: + self._validate_identifier(paths.identifier) + + for path in paths.data: + if is_removal and (path not in target or path in delta): + raise PlotNotAvailableError(state, path) + if not is_removal and path in delta: + raise PlotAlreadyAvailableError(state, path) + delta.append(path) + + if paths.final: + self._sync_state = next_state + + self.bump_expected_message_id() + + async def _process_removed(self, paths: PlotSyncPathList) -> None: + await self.process_path_list( + state=State.removed, + next_state=State.invalid, + target=self._plots, + delta=self._delta.valid.removals, + paths=paths, + is_removal=True, + ) + + async def process_removed(self, paths: PlotSyncPathList) -> None: + await self._process(self._process_removed, ProtocolMessageTypes.plot_sync_removed, paths) + + async def _process_invalid(self, paths: PlotSyncPathList) -> None: + await self.process_path_list( + state=State.invalid, + next_state=State.keys_missing, + target=self._invalid, + delta=self._delta.invalid.additions, + paths=paths, + ) + + async def process_invalid(self, paths: PlotSyncPathList) -> None: + await self._process(self._process_invalid, ProtocolMessageTypes.plot_sync_invalid, paths) + + async def _process_keys_missing(self, paths: PlotSyncPathList) -> None: + await self.process_path_list( + state=State.keys_missing, + next_state=State.duplicates, + target=self._keys_missing, + delta=self._delta.keys_missing.additions, + paths=paths, + ) + + async def process_keys_missing(self, paths: PlotSyncPathList) -> None: + await self._process(self._process_keys_missing, ProtocolMessageTypes.plot_sync_keys_missing, paths) + + async def _process_duplicates(self, paths: PlotSyncPathList) -> None: + await self.process_path_list( + state=State.duplicates, + next_state=State.done, + target=self._duplicates, + delta=self._delta.duplicates.additions, + paths=paths, + ) + + async def process_duplicates(self, paths: PlotSyncPathList) -> None: + await self._process(self._process_duplicates, ProtocolMessageTypes.plot_sync_duplicates, paths) + + async def _sync_done(self, data: PlotSyncDone) -> None: + self._validate_identifier(data.identifier) + # Update ids + self._last_sync_id = self._expected_sync_id + self._expected_sync_id = uint64(0) + self._expected_message_id = uint64(0) + # First create the update delta (i.e. transform invalid/keys_missing into additions/removals) which we will + # send to the callback receiver below + delta_invalid: PathListDelta = PathListDelta.from_lists(self._invalid, self._delta.invalid.additions) + delta_keys_missing: PathListDelta = PathListDelta.from_lists( + self._keys_missing, self._delta.keys_missing.additions + ) + delta_duplicates: PathListDelta = PathListDelta.from_lists(self._duplicates, self._delta.duplicates.additions) + update = Delta( + PlotListDelta(self._delta.valid.additions.copy(), self._delta.valid.removals.copy()), + delta_invalid, + delta_keys_missing, + delta_duplicates, + ) + # Apply delta + self._plots.update(self._delta.valid.additions) + for removal in self._delta.valid.removals: + del self._plots[removal] + self._invalid = self._delta.invalid.additions.copy() + self._keys_missing = self._delta.keys_missing.additions.copy() + self._duplicates = self._delta.duplicates.additions.copy() + # Update state and bump last sync time + self._sync_state = State.idle + self._last_sync_time = time.time() + # Let the callback receiver know if this sync cycle caused any update + try: + await self._update_callback(self._connection.peer_node_id, update) # type: ignore[misc,call-arg] + except Exception as e: + log.error(f"_update_callback raised: {e}") + self._delta.clear() + + async def sync_done(self, data: PlotSyncDone) -> None: + await self._process(self._sync_done, ProtocolMessageTypes.plot_sync_done, data) + + def to_dict(self) -> Dict[str, Any]: + result: Dict[str, Any] = { + "connection": { + "node_id": self._connection.peer_node_id, + "host": self._connection.peer_host, + "port": self._connection.peer_port, + }, + "plots": list(self._plots.values()), + "failed_to_open_filenames": self._invalid, + "no_key_filenames": self._keys_missing, + "duplicates": self._duplicates, + } + if self._last_sync_time != 0: + result["last_sync_time"] = self._last_sync_time + return result diff --git a/chia/plot_sync/sender.py b/chia/plot_sync/sender.py new file mode 100644 index 000000000000..257051a4c2ed --- /dev/null +++ b/chia/plot_sync/sender.py @@ -0,0 +1,327 @@ +import asyncio +import logging +import threading +import time +import traceback +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Generic, Iterable, List, Optional, Tuple, Type, TypeVar + +from typing_extensions import Protocol + +from chia.plot_sync.exceptions import AlreadyStartedError, InvalidConnectionTypeError +from chia.plot_sync.util import Constants +from chia.plotting.manager import PlotManager +from chia.plotting.util import PlotInfo +from chia.protocols.harvester_protocol import ( + Plot, + PlotSyncDone, + PlotSyncIdentifier, + PlotSyncPathList, + PlotSyncPlotList, + PlotSyncResponse, + PlotSyncStart, +) +from chia.server.ws_connection import NodeType, ProtocolMessageTypes, WSChiaConnection, make_msg +from chia.util.generator_tools import list_to_batches +from chia.util.ints import int16, uint32, uint64 + +log = logging.getLogger(__name__) + + +def _convert_plot_info_list(plot_infos: List[PlotInfo]) -> List[Plot]: + converted: List[Plot] = [] + for plot_info in plot_infos: + converted.append( + Plot( + filename=plot_info.prover.get_filename(), + size=plot_info.prover.get_size(), + plot_id=plot_info.prover.get_id(), + pool_public_key=plot_info.pool_public_key, + pool_contract_puzzle_hash=plot_info.pool_contract_puzzle_hash, + plot_public_key=plot_info.plot_public_key, + file_size=uint64(plot_info.file_size), + time_modified=uint64(int(plot_info.time_modified)), + ) + ) + return converted + + +class PayloadType(Protocol): + def __init__(self, identifier: PlotSyncIdentifier, *args: object) -> None: + ... + + +T = TypeVar("T", bound=PayloadType) + + +@dataclass +class MessageGenerator(Generic[T]): + sync_id: uint64 + message_type: ProtocolMessageTypes + message_id: uint64 + payload_type: Type[T] + args: Iterable[object] + + def generate(self) -> Tuple[PlotSyncIdentifier, T]: + identifier = PlotSyncIdentifier(uint64(int(time.time())), self.sync_id, self.message_id) + payload = self.payload_type(identifier, *self.args) + return identifier, payload + + +@dataclass +class ExpectedResponse: + message_type: ProtocolMessageTypes + identifier: PlotSyncIdentifier + message: Optional[PlotSyncResponse] = None + + def __str__(self) -> str: + return ( + f"expected_message_type: {self.message_type.name}, " + f"expected_identifier: {self.identifier}, message {self.message}" + ) + + +class Sender: + _plot_manager: PlotManager + _connection: Optional[WSChiaConnection] + _sync_id: uint64 + _next_message_id: uint64 + _messages: List[MessageGenerator[PayloadType]] + _last_sync_id: uint64 + _stop_requested = False + _task: Optional[asyncio.Task] # type: ignore[type-arg] # Asks for Task parameter which doesn't work + _lock: threading.Lock + _response: Optional[ExpectedResponse] + + def __init__(self, plot_manager: PlotManager) -> None: + self._plot_manager = plot_manager + self._connection = None + self._sync_id = uint64(0) + self._next_message_id = uint64(0) + self._messages = [] + self._last_sync_id = uint64(0) + self._stop_requested = False + self._task = None + self._lock = threading.Lock() + self._response = None + + def __str__(self) -> str: + return f"sync_id {self._sync_id}, next_message_id {self._next_message_id}, messages {len(self._messages)}" + + async def start(self) -> None: + if self._task is not None and self._stop_requested: + await self.await_closed() + if self._task is None: + self._task = asyncio.create_task(self._run()) + # TODO, Add typing in PlotManager + if not self._plot_manager.initial_refresh() or self._sync_id != 0: # type:ignore[no-untyped-call] + self._reset() + else: + raise AlreadyStartedError() + + def stop(self) -> None: + self._stop_requested = True + + async def await_closed(self) -> None: + if self._task is not None: + await self._task + self._task = None + self._reset() + self._stop_requested = False + + def set_connection(self, connection: WSChiaConnection) -> None: + assert connection.connection_type is not None + if connection.connection_type != NodeType.FARMER: + raise InvalidConnectionTypeError(connection.connection_type, NodeType.HARVESTER) + self._connection = connection + + def bump_next_message_id(self) -> None: + self._next_message_id = uint64(self._next_message_id + 1) + + def _reset(self) -> None: + log.debug(f"_reset {self}") + self._last_sync_id = uint64(0) + self._sync_id = uint64(0) + self._next_message_id = uint64(0) + self._messages.clear() + if self._lock.locked(): + self._lock.release() + if self._task is not None: + # TODO, Add typing in PlotManager + self.sync_start(self._plot_manager.plot_count(), True) # type:ignore[no-untyped-call] + for remaining, batch in list_to_batches( + list(self._plot_manager.plots.values()), self._plot_manager.refresh_parameter.batch_size + ): + self.process_batch(batch, remaining) + self.sync_done([], 0) + + async def _wait_for_response(self) -> bool: + start = time.time() + assert self._response is not None + while time.time() - start < Constants.message_timeout and self._response.message is None: + await asyncio.sleep(0.1) + return self._response.message is not None + + def set_response(self, response: PlotSyncResponse) -> bool: + if self._response is None or self._response.message is not None: + log.warning(f"set_response skip unexpected response: {response}") + return False + if time.time() - float(response.identifier.timestamp) > Constants.message_timeout: + log.warning(f"set_response skip expired response: {response}") + return False + if response.identifier.sync_id != self._response.identifier.sync_id: + log.warning( + "set_response unexpected sync-id: " f"{response.identifier.sync_id}/{self._response.identifier.sync_id}" + ) + return False + if response.identifier.message_id != self._response.identifier.message_id: + log.warning( + "set_response unexpected message-id: " + f"{response.identifier.message_id}/{self._response.identifier.message_id}" + ) + return False + if response.message_type != int16(self._response.message_type.value): + log.warning( + "set_response unexpected message-type: " f"{response.message_type}/{self._response.message_type.value}" + ) + return False + log.debug(f"set_response valid {response}") + self._response.message = response + return True + + def _add_message(self, message_type: ProtocolMessageTypes, payload_type: Any, *args: Any) -> None: + assert self._sync_id != 0 + message_id = uint64(len(self._messages)) + self._messages.append(MessageGenerator(self._sync_id, message_type, message_id, payload_type, args)) + + async def _send_next_message(self) -> bool: + def failed(message: str) -> bool: + # By forcing a reset we try to get back into a normal state if some not recoverable failure came up. + log.warning(message) + self._reset() + return False + + assert len(self._messages) >= self._next_message_id + message_generator = self._messages[self._next_message_id] + identifier, payload = message_generator.generate() + if self._sync_id == 0 or identifier.sync_id != self._sync_id or identifier.message_id != self._next_message_id: + return failed(f"Invalid message generator {message_generator} for {self}") + + self._response = ExpectedResponse(message_generator.message_type, identifier) + log.debug(f"_send_next_message send {message_generator.message_type.name}: {payload}") + if self._connection is None or not await self._connection.send_message( + make_msg(message_generator.message_type, payload) + ): + return failed(f"Send failed {self._connection}") + if not await self._wait_for_response(): + log.info(f"_send_next_message didn't receive response {self._response}") + return False + + assert self._response.message is not None + if self._response.message.error is not None: + recovered = False + expected = self._response.message.error.expected_identifier + # If we have a recoverable error there is a `expected_identifier` included + if expected is not None: + # If the receiver has a zero sync/message id and we already sent all messages from the current event + # we most likely missed the response to the done message. We can finalize the sync and move on here. + all_sent = ( + self._messages[-1].message_type == ProtocolMessageTypes.plot_sync_done + and self._next_message_id == len(self._messages) - 1 + ) + if expected.sync_id == expected.message_id == 0 and all_sent: + self._finalize_sync() + recovered = True + elif self._sync_id == expected.sync_id and expected.message_id < len(self._messages): + self._next_message_id = expected.message_id + recovered = True + if not recovered: + return failed(f"Not recoverable error {self._response.message}") + return True + + if self._response.message_type == ProtocolMessageTypes.plot_sync_done: + self._finalize_sync() + else: + self.bump_next_message_id() + + return True + + def _add_list_batched(self, message_type: ProtocolMessageTypes, payload_type: Any, data: List[Any]) -> None: + if len(data) == 0: + self._add_message(message_type, payload_type, [], True) + return + for remaining, batch in list_to_batches(data, self._plot_manager.refresh_parameter.batch_size): + self._add_message(message_type, payload_type, batch, remaining == 0) + + def sync_start(self, count: float, initial: bool) -> None: + log.debug(f"sync_start {self}: count {count}, initial {initial}") + self._lock.acquire() + sync_id = int(time.time()) + # Make sure we have unique sync-id's even if we restart refreshing within a second (i.e. in tests) + if sync_id == self._last_sync_id: + sync_id = sync_id + 1 + log.debug(f"sync_start {sync_id}") + self._sync_id = uint64(sync_id) + self._add_message( + ProtocolMessageTypes.plot_sync_start, PlotSyncStart, initial, self._last_sync_id, uint32(int(count)) + ) + + def process_batch(self, loaded: List[PlotInfo], remaining: int) -> None: + log.debug(f"process_batch {self}: loaded {len(loaded)}, remaining {remaining}") + if len(loaded) > 0 or remaining == 0: + converted = _convert_plot_info_list(loaded) + self._add_message(ProtocolMessageTypes.plot_sync_loaded, PlotSyncPlotList, converted, remaining == 0) + + def sync_done(self, removed: List[Path], duration: float) -> None: + log.debug(f"sync_done {self}: removed {len(removed)}, duration {duration}") + removed_list = [str(x) for x in removed] + self._add_list_batched( + ProtocolMessageTypes.plot_sync_removed, + PlotSyncPathList, + removed_list, + ) + failed_to_open_list = [str(x) for x in list(self._plot_manager.failed_to_open_filenames)] + self._add_list_batched(ProtocolMessageTypes.plot_sync_invalid, PlotSyncPathList, failed_to_open_list) + no_key_list = [str(x) for x in self._plot_manager.no_key_filenames] + self._add_list_batched(ProtocolMessageTypes.plot_sync_keys_missing, PlotSyncPathList, no_key_list) + # TODO, Add typing in PlotManager + duplicates_list: List[str] = self._plot_manager.get_duplicates().copy() # type:ignore[no-untyped-call] + self._add_list_batched(ProtocolMessageTypes.plot_sync_duplicates, PlotSyncPathList, duplicates_list) + self._add_message(ProtocolMessageTypes.plot_sync_done, PlotSyncDone, uint64(int(duration))) + + def _finalize_sync(self) -> None: + log.debug(f"_finalize_sync {self}") + assert self._sync_id != 0 + self._last_sync_id = self._sync_id + self._sync_id = uint64(0) + self._next_message_id = uint64(0) + self._messages.clear() + self._lock.release() + + def sync_active(self) -> bool: + return self._lock.locked() and self._sync_id != 0 + + def connected(self) -> bool: + return self._connection is not None + + async def _run(self) -> None: + """ + This is the sender task responsible to send new messages during sync as they come into Sender._messages + triggered by the plot manager callback. + """ + while not self._stop_requested: + try: + while not self.connected() or not self.sync_active(): + if self._stop_requested: + return + await asyncio.sleep(0.1) + while not self._stop_requested and self.sync_active(): + if self._next_message_id >= len(self._messages): + await asyncio.sleep(0.1) + continue + if not await self._send_next_message(): + await asyncio.sleep(Constants.message_timeout) + except Exception as e: + log.error(f"Exception: {e} {traceback.format_exc()}") + self._reset() diff --git a/chia/plot_sync/util.py b/chia/plot_sync/util.py new file mode 100644 index 000000000000..5776631a73d8 --- /dev/null +++ b/chia/plot_sync/util.py @@ -0,0 +1,27 @@ +from enum import IntEnum + + +class Constants: + message_timeout: int = 10 + + +class State(IntEnum): + idle = 0 + loaded = 1 + removed = 2 + invalid = 3 + keys_missing = 4 + duplicates = 5 + done = 6 + + +class ErrorCodes(IntEnum): + unknown = -1 + invalid_state = 0 + invalid_peer_id = 1 + invalid_identifier = 2 + invalid_last_sync_id = 3 + invalid_connection_type = 4 + plot_already_available = 5 + plot_not_available = 6 + sync_ids_match = 7 diff --git a/chia/plotting/manager.py b/chia/plotting/manager.py index 6fcd84982eef..7f48cbfc304e 100644 --- a/chia/plotting/manager.py +++ b/chia/plotting/manager.py @@ -131,6 +131,7 @@ class PlotManager: _refresh_thread: Optional[threading.Thread] _refreshing_enabled: bool _refresh_callback: Callable + _initial: bool def __init__( self, @@ -158,6 +159,7 @@ def __init__( self._refresh_thread = None self._refreshing_enabled = False self._refresh_callback = refresh_callback # type: ignore + self._initial = True def __enter__(self): self._lock.acquire() @@ -172,6 +174,7 @@ def reset(self): self.plot_filename_paths.clear() self.failed_to_open_filenames.clear() self.no_key_filenames.clear() + self._initial = True def set_refresh_callback(self, callback: Callable): self._refresh_callback = callback # type: ignore @@ -180,6 +183,9 @@ def set_public_keys(self, farmer_public_keys: List[G1Element], pool_public_keys: self.farmer_public_keys = farmer_public_keys self.pool_public_keys = pool_public_keys + def initial_refresh(self): + return self._initial + def public_keys_available(self): return len(self.farmer_public_keys) and len(self.pool_public_keys) @@ -262,7 +268,6 @@ def _refresh_task(self): loaded_plot = Path(path) / Path(plot_filename) if loaded_plot not in plot_paths: paths_to_remove.append(path) - total_result.removed.append(loaded_plot) for path in paths_to_remove: duplicated_paths.remove(path) @@ -290,6 +295,9 @@ def _refresh_task(self): if self._refreshing_enabled: self._refresh_callback(PlotRefreshEvents.done, total_result) + # Reset the initial refresh indication + self._initial = False + # Cleanup unused cache available_ids = set([plot_info.prover.get_id() for plot_info in self.plots.values()]) invalid_cache_keys = [plot_id for plot_id in self.cache.keys() if plot_id not in available_ids] diff --git a/chia/protocols/harvester_protocol.py b/chia/protocols/harvester_protocol.py index b48165773fc9..4c5cddc1459c 100644 --- a/chia/protocols/harvester_protocol.py +++ b/chia/protocols/harvester_protocol.py @@ -5,7 +5,7 @@ from chia.types.blockchain_format.proof_of_space import ProofOfSpace from chia.types.blockchain_format.sized_bytes import bytes32 -from chia.util.ints import uint8, uint64 +from chia.util.ints import int16, uint8, uint32, uint64 from chia.util.streamable import Streamable, streamable """ @@ -95,3 +95,80 @@ class RespondPlots(Streamable): plots: List[Plot] failed_to_open_filenames: List[str] no_key_filenames: List[str] + + +@dataclass(frozen=True) +@streamable +class PlotSyncIdentifier(Streamable): + timestamp: uint64 + sync_id: uint64 + message_id: uint64 + + +@dataclass(frozen=True) +@streamable +class PlotSyncStart(Streamable): + identifier: PlotSyncIdentifier + initial: bool + last_sync_id: uint64 + plot_file_count: uint32 + + def __str__(self) -> str: + return ( + f"PlotSyncStart: identifier {self.identifier}, initial {self.initial}, " + f"last_sync_id {self.last_sync_id}, plot_file_count {self.plot_file_count}" + ) + + +@dataclass(frozen=True) +@streamable +class PlotSyncPathList(Streamable): + identifier: PlotSyncIdentifier + data: List[str] + final: bool + + def __str__(self) -> str: + return f"PlotSyncPathList: identifier {self.identifier}, count {len(self.data)}, final {self.final}" + + +@dataclass(frozen=True) +@streamable +class PlotSyncPlotList(Streamable): + identifier: PlotSyncIdentifier + data: List[Plot] + final: bool + + def __str__(self) -> str: + return f"PlotSyncPlotList: identifier {self.identifier}, count {len(self.data)}, final {self.final}" + + +@dataclass(frozen=True) +@streamable +class PlotSyncDone(Streamable): + identifier: PlotSyncIdentifier + duration: uint64 + + def __str__(self) -> str: + return f"PlotSyncDone: identifier {self.identifier}, duration {self.duration}" + + +@dataclass(frozen=True) +@streamable +class PlotSyncError(Streamable): + code: int16 + message: str + expected_identifier: Optional[PlotSyncIdentifier] + + def __str__(self) -> str: + return f"PlotSyncError: code {self.code}, count {self.message}, expected_identifier {self.expected_identifier}" + + +@dataclass(frozen=True) +@streamable +class PlotSyncResponse(Streamable): + identifier: PlotSyncIdentifier + message_type: int16 + error: Optional[PlotSyncError] + + def __str__(self) -> str: + return f"PlotSyncResponse: identifier {self.identifier}, message_type {self.message_type}, error {self.error}" diff --git a/chia/protocols/protocol_message_types.py b/chia/protocols/protocol_message_types.py index 7596f4554745..b54e2717b41d 100644 --- a/chia/protocols/protocol_message_types.py +++ b/chia/protocols/protocol_message_types.py @@ -86,6 +86,14 @@ class ProtocolMessageTypes(Enum): new_signage_point_harvester = 66 request_plots = 67 respond_plots = 68 + plot_sync_start = 78 + plot_sync_loaded = 79 + plot_sync_removed = 80 + plot_sync_invalid = 81 + plot_sync_keys_missing = 82 + plot_sync_duplicates = 83 + plot_sync_done = 84 + plot_sync_response = 85 # More wallet protocol coin_state_update = 69 diff --git a/chia/protocols/shared_protocol.py b/chia/protocols/shared_protocol.py index d7c0e6cd94a8..ed1cc9e7d680 100644 --- a/chia/protocols/shared_protocol.py +++ b/chia/protocols/shared_protocol.py @@ -5,7 +5,7 @@ from chia.util.ints import uint8, uint16 from chia.util.streamable import Streamable, streamable -protocol_version = "0.0.33" +protocol_version = "0.0.34" """ Handshake when establishing a connection between two servers. diff --git a/chia/server/rate_limits.py b/chia/server/rate_limits.py index 78f4d69340ff..b70c04b0f8f0 100644 --- a/chia/server/rate_limits.py +++ b/chia/server/rate_limits.py @@ -97,6 +97,14 @@ class RLSettings: ProtocolMessageTypes.farm_new_block: RLSettings(200, 200), ProtocolMessageTypes.request_plots: RLSettings(10, 10 * 1024 * 1024), ProtocolMessageTypes.respond_plots: RLSettings(10, 100 * 1024 * 1024), + ProtocolMessageTypes.plot_sync_start: RLSettings(1000, 100 * 1024 * 1024), + ProtocolMessageTypes.plot_sync_loaded: RLSettings(1000, 100 * 1024 * 1024), + ProtocolMessageTypes.plot_sync_removed: RLSettings(1000, 100 * 1024 * 1024), + ProtocolMessageTypes.plot_sync_invalid: RLSettings(1000, 100 * 1024 * 1024), + ProtocolMessageTypes.plot_sync_keys_missing: RLSettings(1000, 100 * 1024 * 1024), + ProtocolMessageTypes.plot_sync_duplicates: RLSettings(1000, 100 * 1024 * 1024), + ProtocolMessageTypes.plot_sync_done: RLSettings(1000, 100 * 1024 * 1024), + ProtocolMessageTypes.plot_sync_response: RLSettings(3000, 100 * 1024 * 1024), ProtocolMessageTypes.coin_state_update: RLSettings(1000, 100 * 1024 * 1024), ProtocolMessageTypes.register_interest_in_puzzle_hash: RLSettings(1000, 100 * 1024 * 1024), ProtocolMessageTypes.respond_to_ph_update: RLSettings(1000, 100 * 1024 * 1024), diff --git a/setup.py b/setup.py index 8d4d20558bea..9c6e98238dc1 100644 --- a/setup.py +++ b/setup.py @@ -92,6 +92,7 @@ "chia.farmer", "chia.harvester", "chia.introducer", + "chia.plot_sync", "chia.plotters", "chia.plotting", "chia.pools", diff --git a/tests/conftest.py b/tests/conftest.py index 9dd58b313a0b..f5377b57618e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,6 +8,9 @@ import tempfile 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 chia.server.start_service import Service # Set spawn after stdlib imports, but before other imports from chia.clvm.spend_sim import SimClient, SpendSim @@ -39,6 +42,7 @@ from chia.util.keyring_wrapper import KeyringWrapper from tests.block_tools import BlockTools, test_constants, create_block_tools, create_block_tools_async from tests.util.keyring import TempKeyring +from tests.setup_nodes import setup_farmer_multi_harvester @pytest.fixture(scope="session") @@ -403,6 +407,24 @@ async def two_nodes_one_block(bt, wallet_a): yield _ +@pytest_asyncio.fixture(scope="function") +async def farmer_one_harvester(tmp_path: Path, bt: BlockTools) -> AsyncIterator[Tuple[List[Service], Service]]: + async for _ in setup_farmer_multi_harvester(bt, 1, tmp_path, test_constants): + yield _ + + +@pytest_asyncio.fixture(scope="function") +async def farmer_two_harvester(tmp_path: Path, bt: BlockTools) -> AsyncIterator[Tuple[List[Service], Service]]: + async for _ in setup_farmer_multi_harvester(bt, 2, tmp_path, test_constants): + yield _ + + +@pytest_asyncio.fixture(scope="function") +async def farmer_three_harvester(tmp_path: Path, bt: BlockTools) -> AsyncIterator[Tuple[List[Service], Service]]: + async for _ in setup_farmer_multi_harvester(bt, 3, tmp_path, test_constants): + yield _ + + # TODO: Ideally, the db_version should be the (parameterized) db_version # fixture, to test all versions of the database schema. This doesn't work # because of a hack in shutting down the full node, which means you cannot run diff --git a/tests/core/test_farmer_harvester_rpc.py b/tests/core/test_farmer_harvester_rpc.py index 72a16ba9e44d..e312698fe007 100644 --- a/tests/core/test_farmer_harvester_rpc.py +++ b/tests/core/test_farmer_harvester_rpc.py @@ -112,7 +112,6 @@ async def test_farmer_get_harvesters(harvester_farmer_environment): harvester_rpc_api, harvester_rpc_client, ) = harvester_farmer_environment - farmer_api = farmer_service._api harvester = harvester_service._node num_plots = 0 @@ -125,11 +124,6 @@ async def non_zero_plots() -> bool: await time_out_assert(10, non_zero_plots) - # Reset cache and force updates cache every second to make sure the farmer gets the most recent data - update_interval_before = farmer_api.farmer.update_harvester_cache_interval - farmer_api.farmer.update_harvester_cache_interval = 1 - farmer_api.farmer.harvester_cache = {} - async def test_get_harvesters(): harvester.plot_manager.trigger_refresh() await time_out_assert(5, harvester.plot_manager.needs_refresh, value=False) @@ -144,10 +138,6 @@ async def test_get_harvesters(): await time_out_assert_custom_interval(30, 1, test_get_harvesters) - # Reset cache and reset update interval to avoid hitting the rate limit - farmer_api.farmer.update_harvester_cache_interval = update_interval_before - farmer_api.farmer.harvester_cache = {} - @pytest.mark.asyncio async def test_farmer_signage_point_endpoints(harvester_farmer_environment): diff --git a/tests/plot_sync/__init__.py b/tests/plot_sync/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/plot_sync/config.py b/tests/plot_sync/config.py new file mode 100644 index 000000000000..235efb181c1c --- /dev/null +++ b/tests/plot_sync/config.py @@ -0,0 +1,2 @@ +parallel = True +checkout_blocks_and_plots = True diff --git a/tests/plot_sync/test_delta.py b/tests/plot_sync/test_delta.py new file mode 100644 index 000000000000..057e449bda15 --- /dev/null +++ b/tests/plot_sync/test_delta.py @@ -0,0 +1,90 @@ +import logging +from typing import List + +import pytest +from blspy import G1Element + +from chia.plot_sync.delta import Delta, DeltaType, PathListDelta, PlotListDelta +from chia.protocols.harvester_protocol import Plot +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.util.ints import uint8, uint64 + +log = logging.getLogger(__name__) + + +def dummy_plot(path: str) -> Plot: + return Plot(path, uint8(32), bytes32(b"\00" * 32), G1Element(), None, G1Element(), uint64(0), uint64(0)) + + +@pytest.mark.parametrize( + ["delta"], + [ + pytest.param(PathListDelta(), id="path list"), + pytest.param(PlotListDelta(), id="plot list"), + ], +) +def test_list_delta(delta: DeltaType) -> None: + assert delta.empty() + if type(delta) == PathListDelta: + assert delta.additions == [] + elif type(delta) == PlotListDelta: + assert delta.additions == {} + else: + assert False + assert delta.removals == [] + assert delta.empty() + if type(delta) == PathListDelta: + delta.additions.append("0") + elif type(delta) == PlotListDelta: + delta.additions["0"] = dummy_plot("0") + else: + assert False, "Invalid delta type" + assert not delta.empty() + delta.removals.append("0") + assert not delta.empty() + delta.additions.clear() + assert not delta.empty() + delta.clear() + assert delta.empty() + + +@pytest.mark.parametrize( + ["old", "new", "result"], + [ + [[], [], PathListDelta()], + [["1"], ["0"], PathListDelta(["0"], ["1"])], + [["1", "2", "3"], ["1", "2", "3"], PathListDelta([], [])], + [["2", "1", "3"], ["2", "3", "1"], PathListDelta([], [])], + [["2"], ["2", "3", "1"], PathListDelta(["3", "1"], [])], + [["2"], ["1", "3"], PathListDelta(["1", "3"], ["2"])], + [["1"], ["1", "2", "3"], PathListDelta(["2", "3"], [])], + [[], ["1", "2", "3"], PathListDelta(["1", "2", "3"], [])], + [["-1"], ["1", "2", "3"], PathListDelta(["1", "2", "3"], ["-1"])], + [["-1", "1"], ["2", "3"], PathListDelta(["2", "3"], ["-1", "1"])], + [["-1", "1", "2"], ["2", "3"], PathListDelta(["3"], ["-1", "1"])], + [["-1", "2", "3"], ["2", "3"], PathListDelta([], ["-1"])], + [["-1", "2", "3", "-2"], ["2", "3"], PathListDelta([], ["-1", "-2"])], + [["-2", "2", "3", "-1"], ["2", "3"], PathListDelta([], ["-2", "-1"])], + ], +) +def test_path_list_delta_from_lists(old: List[str], new: List[str], result: PathListDelta) -> None: + assert PathListDelta.from_lists(old, new) == result + + +def test_delta_empty() -> None: + delta: Delta = Delta() + all_deltas: List[DeltaType] = [delta.valid, delta.invalid, delta.keys_missing, delta.duplicates] + assert delta.empty() + for d1 in all_deltas: + delta.valid.additions["0"] = dummy_plot("0") + delta.invalid.additions.append("0") + delta.keys_missing.additions.append("0") + delta.duplicates.additions.append("0") + assert not delta.empty() + for d2 in all_deltas: + if d2 is not d1: + d2.clear() + assert not delta.empty() + assert not delta.empty() + d1.clear() + assert delta.empty() diff --git a/tests/plot_sync/test_plot_sync.py b/tests/plot_sync/test_plot_sync.py new file mode 100644 index 000000000000..96e1fcadd832 --- /dev/null +++ b/tests/plot_sync/test_plot_sync.py @@ -0,0 +1,537 @@ +from dataclasses import dataclass, field +from pathlib import Path +from shutil import copy +from typing import List, Optional, Tuple + +import pytest +import pytest_asyncio +from blspy import G1Element + +from chia.farmer.farmer_api import Farmer +from chia.harvester.harvester_api import Harvester +from chia.plot_sync.delta import Delta, PathListDelta, PlotListDelta +from chia.plot_sync.receiver import Receiver +from chia.plot_sync.sender import Sender +from chia.plot_sync.util import State +from chia.plotting.manager import PlotManager +from chia.plotting.util import add_plot_directory, remove_plot_directory +from chia.protocols.harvester_protocol import Plot +from chia.server.start_service import Service +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.util.config import create_default_chia_config +from chia.util.ints import uint8, uint64 +from tests.block_tools import BlockTools +from tests.plot_sync.util import start_harvester_service +from tests.plotting.test_plot_manager import MockPlotInfo, TestDirectory +from tests.plotting.util import get_test_plots +from tests.time_out_assert import time_out_assert + + +def synced(sender: Sender, receiver: Receiver, previous_last_sync_id: int) -> bool: + return ( + sender._last_sync_id != previous_last_sync_id + and sender._last_sync_id == receiver._last_sync_id != 0 + and receiver.state() == State.idle + and not sender._lock.locked() + ) + + +def assert_path_list_matches(expected_list: List[str], actual_list: List[str]) -> None: + assert len(expected_list) == len(actual_list) + for item in expected_list: + assert str(item) in actual_list + + +@dataclass +class ExpectedResult: + valid_count: int = 0 + valid_delta: PlotListDelta = field(default_factory=PlotListDelta) + invalid_count: int = 0 + invalid_delta: PathListDelta = field(default_factory=PathListDelta) + keys_missing_count: int = 0 + keys_missing_delta: PathListDelta = field(default_factory=PathListDelta) + duplicates_count: int = 0 + duplicates_delta: PathListDelta = field(default_factory=PathListDelta) + callback_passed: bool = False + + def add_valid(self, list_plots: List[MockPlotInfo]) -> None: + def create_mock_plot(info: MockPlotInfo) -> Plot: + return Plot( + info.prover.get_filename(), + uint8(0), + bytes32(b"\x00" * 32), + None, + None, + G1Element(), + uint64(0), + uint64(0), + ) + + self.valid_count += len(list_plots) + self.valid_delta.additions.update({x.prover.get_filename(): create_mock_plot(x) for x in list_plots}) + + def remove_valid(self, list_paths: List[Path]) -> None: + self.valid_count -= len(list_paths) + self.valid_delta.removals += [str(x) for x in list_paths] + + def add_invalid(self, list_paths: List[Path]) -> None: + self.invalid_count += len(list_paths) + self.invalid_delta.additions += [str(x) for x in list_paths] + + def remove_invalid(self, list_paths: List[Path]) -> None: + self.invalid_count -= len(list_paths) + self.invalid_delta.removals += [str(x) for x in list_paths] + + def add_keys_missing(self, list_paths: List[Path]) -> None: + self.keys_missing_count += len(list_paths) + self.keys_missing_delta.additions += [str(x) for x in list_paths] + + def remove_keys_missing(self, list_paths: List[Path]) -> None: + self.keys_missing_count -= len(list_paths) + self.keys_missing_delta.removals += [str(x) for x in list_paths] + + def add_duplicates(self, list_paths: List[Path]) -> None: + self.duplicates_count += len(list_paths) + self.duplicates_delta.additions += [str(x) for x in list_paths] + + def remove_duplicates(self, list_paths: List[Path]) -> None: + self.duplicates_count -= len(list_paths) + self.duplicates_delta.removals += [str(x) for x in list_paths] + + +@dataclass +class Environment: + root_path: Path + harvester_services: List[Service] + farmer_service: Service + harvesters: List[Harvester] + farmer: Farmer + dir_1: TestDirectory + dir_2: TestDirectory + dir_3: TestDirectory + dir_4: TestDirectory + dir_invalid: TestDirectory + dir_keys_missing: TestDirectory + dir_duplicates: TestDirectory + expected: List[ExpectedResult] + + def get_harvester(self, peer_id: bytes32) -> Optional[Harvester]: + for harvester in self.harvesters: + assert harvester.server is not None + if harvester.server.node_id == peer_id: + return harvester + return None + + def add_directory(self, harvester_index: int, directory: TestDirectory, state: State = State.loaded) -> None: + add_plot_directory(self.harvesters[harvester_index].root_path, str(directory.path)) + if state == State.loaded: + self.expected[harvester_index].add_valid(directory.plot_info_list()) + elif state == State.invalid: + self.expected[harvester_index].add_invalid(directory.path_list()) + elif state == State.keys_missing: + self.expected[harvester_index].add_keys_missing(directory.path_list()) + elif state == State.duplicates: + self.expected[harvester_index].add_duplicates(directory.path_list()) + else: + assert False, "Invalid state" + + def remove_directory(self, harvester_index: int, directory: TestDirectory, state: State = State.removed) -> None: + remove_plot_directory(self.harvesters[harvester_index].root_path, str(directory.path)) + if state == State.removed: + self.expected[harvester_index].remove_valid(directory.path_list()) + elif state == State.invalid: + self.expected[harvester_index].remove_invalid(directory.path_list()) + elif state == State.keys_missing: + self.expected[harvester_index].remove_keys_missing(directory.path_list()) + elif state == State.duplicates: + self.expected[harvester_index].remove_duplicates(directory.path_list()) + else: + assert False, "Invalid state" + + def add_all_directories(self, harvester_index: int) -> None: + self.add_directory(harvester_index, self.dir_1) + self.add_directory(harvester_index, self.dir_2) + self.add_directory(harvester_index, self.dir_3) + self.add_directory(harvester_index, self.dir_4) + self.add_directory(harvester_index, self.dir_keys_missing, State.keys_missing) + self.add_directory(harvester_index, self.dir_invalid, State.invalid) + # Note: This does not add dir_duplicates since its important that the duplicated plots are loaded after the + # the original ones. + # self.add_directory(harvester_index, self.dir_duplicates, State.duplicates) + + def remove_all_directories(self, harvester_index: int) -> None: + self.remove_directory(harvester_index, self.dir_1) + self.remove_directory(harvester_index, self.dir_2) + self.remove_directory(harvester_index, self.dir_3) + self.remove_directory(harvester_index, self.dir_4) + self.remove_directory(harvester_index, self.dir_keys_missing, State.keys_missing) + self.remove_directory(harvester_index, self.dir_invalid, State.invalid) + self.remove_directory(harvester_index, self.dir_duplicates, State.duplicates) + + async def plot_sync_callback(self, peer_id: bytes32, delta: Delta) -> None: + harvester: Optional[Harvester] = self.get_harvester(peer_id) + assert harvester is not None + expected = self.expected[self.harvesters.index(harvester)] + assert len(expected.valid_delta.additions) == len(delta.valid.additions) + for path, plot_info in expected.valid_delta.additions.items(): + assert path in delta.valid.additions + plot = harvester.plot_manager.plots.get(Path(path), None) + assert plot is not None + assert plot.prover.get_filename() == delta.valid.additions[path].filename + assert plot.prover.get_size() == delta.valid.additions[path].size + assert plot.prover.get_id() == delta.valid.additions[path].plot_id + assert plot.pool_public_key == delta.valid.additions[path].pool_public_key + assert plot.pool_contract_puzzle_hash == delta.valid.additions[path].pool_contract_puzzle_hash + assert plot.plot_public_key == delta.valid.additions[path].plot_public_key + assert plot.file_size == delta.valid.additions[path].file_size + assert int(plot.time_modified) == delta.valid.additions[path].time_modified + + assert_path_list_matches(expected.valid_delta.removals, delta.valid.removals) + assert_path_list_matches(expected.invalid_delta.additions, delta.invalid.additions) + assert_path_list_matches(expected.invalid_delta.removals, delta.invalid.removals) + assert_path_list_matches(expected.keys_missing_delta.additions, delta.keys_missing.additions) + assert_path_list_matches(expected.keys_missing_delta.removals, delta.keys_missing.removals) + assert_path_list_matches(expected.duplicates_delta.additions, delta.duplicates.additions) + assert_path_list_matches(expected.duplicates_delta.removals, delta.duplicates.removals) + expected.valid_delta.clear() + expected.invalid_delta.clear() + expected.keys_missing_delta.clear() + expected.duplicates_delta.clear() + expected.callback_passed = True + + async def run_sync_test(self) -> None: + plot_manager: PlotManager + assert len(self.harvesters) == len(self.expected) + last_sync_ids: List[uint64] = [] + # Run the test in two steps, first trigger the refresh on both harvesters + for harvester in self.harvesters: + plot_manager = harvester.plot_manager + assert harvester.server is not None + receiver = self.farmer.plot_sync_receivers[harvester.server.node_id] + # Make sure to reset the passed flag always before a new run + self.expected[self.harvesters.index(harvester)].callback_passed = False + receiver._update_callback = self.plot_sync_callback + assert harvester.plot_sync_sender._last_sync_id == receiver._last_sync_id + last_sync_ids.append(harvester.plot_sync_sender._last_sync_id) + plot_manager.start_refreshing() + plot_manager.trigger_refresh() + # Then wait for them to be synced with the farmer and validate them + for harvester in self.harvesters: + plot_manager = harvester.plot_manager + assert harvester.server is not None + receiver = self.farmer.plot_sync_receivers[harvester.server.node_id] + await time_out_assert(10, plot_manager.needs_refresh, value=False) + harvester_index = self.harvesters.index(harvester) + await time_out_assert( + 10, synced, True, harvester.plot_sync_sender, receiver, last_sync_ids[harvester_index] + ) + expected = self.expected[harvester_index] + assert plot_manager.plot_count() == len(receiver.plots()) == expected.valid_count + assert len(plot_manager.failed_to_open_filenames) == len(receiver.invalid()) == expected.invalid_count + assert len(plot_manager.no_key_filenames) == len(receiver.keys_missing()) == expected.keys_missing_count + assert len(plot_manager.get_duplicates()) == len(receiver.duplicates()) == expected.duplicates_count + assert expected.callback_passed + assert expected.valid_delta.empty() + assert expected.invalid_delta.empty() + assert expected.keys_missing_delta.empty() + assert expected.duplicates_delta.empty() + for path, plot_info in plot_manager.plots.items(): + assert str(path) in receiver.plots() + assert plot_info.prover.get_filename() == receiver.plots()[str(path)].filename + assert plot_info.prover.get_size() == receiver.plots()[str(path)].size + assert plot_info.prover.get_id() == receiver.plots()[str(path)].plot_id + assert plot_info.pool_public_key == receiver.plots()[str(path)].pool_public_key + assert plot_info.pool_contract_puzzle_hash == receiver.plots()[str(path)].pool_contract_puzzle_hash + assert plot_info.plot_public_key == receiver.plots()[str(path)].plot_public_key + assert plot_info.file_size == receiver.plots()[str(path)].file_size + assert int(plot_info.time_modified) == receiver.plots()[str(path)].time_modified + for path in plot_manager.failed_to_open_filenames: + assert str(path) in receiver.invalid() + for path in plot_manager.no_key_filenames: + assert str(path) in receiver.keys_missing() + for path in plot_manager.get_duplicates(): + assert str(path) in receiver.duplicates() + + async def handshake_done(self, index: int) -> bool: + return ( + self.harvesters[index].plot_manager._refresh_thread is not None + and len(self.harvesters[index].plot_manager.farmer_public_keys) > 0 + ) + + +@pytest_asyncio.fixture(scope="function") +async def environment( + bt: BlockTools, tmp_path: Path, farmer_two_harvester: Tuple[List[Service], Service] +) -> Environment: + def new_test_dir(name: str, plot_list: List[Path]) -> TestDirectory: + return TestDirectory(tmp_path / "plots" / name, plot_list) + + plots: List[Path] = get_test_plots() + plots_invalid: List[Path] = get_test_plots()[0:3] + plots_keys_missing: List[Path] = get_test_plots("not_in_keychain") + # Create 4 directories where: dir_n contains n plots + directories: List[TestDirectory] = [] + offset: int = 0 + while len(directories) < 4: + dir_number = len(directories) + 1 + directories.append(new_test_dir(f"{dir_number}", plots[offset : offset + dir_number])) + offset += dir_number + + dir_invalid: TestDirectory = new_test_dir("invalid", plots_invalid) + dir_keys_missing: TestDirectory = new_test_dir("keys_missing", plots_keys_missing) + dir_duplicates: TestDirectory = new_test_dir("duplicates", directories[3].plots) + create_default_chia_config(tmp_path) + + # Invalidate the plots in `dir_invalid` + for path in dir_invalid.path_list(): + with open(path, "wb") as file: + file.write(bytes(100)) + + harvester_services: List[Service] + farmer_service: Service + harvester_services, farmer_service = farmer_two_harvester + farmer: Farmer = farmer_service._node + harvesters: List[Harvester] = [await start_harvester_service(service) for service in harvester_services] + for harvester in harvesters: + harvester.plot_manager.set_public_keys( + bt.plot_manager.farmer_public_keys.copy(), bt.plot_manager.pool_public_keys.copy() + ) + + assert len(farmer.plot_sync_receivers) == 2 + + return Environment( + tmp_path, + harvester_services, + farmer_service, + harvesters, + farmer, + directories[0], + directories[1], + directories[2], + directories[3], + dir_invalid, + dir_keys_missing, + dir_duplicates, + [ExpectedResult() for _ in harvesters], + ) + + +@pytest.mark.asyncio +async def test_sync_valid(environment: Environment) -> None: + env: Environment = environment + env.add_directory(0, env.dir_1) + env.add_directory(1, env.dir_2) + await env.run_sync_test() + # Run again two times to make sure we still get the same results in repeated refresh intervals + env.expected[0].valid_delta.clear() + env.expected[1].valid_delta.clear() + await env.run_sync_test() + await env.run_sync_test() + env.add_directory(0, env.dir_3) + env.add_directory(1, env.dir_4) + await env.run_sync_test() + while len(env.dir_3.path_list()): + drop_plot = env.dir_3.path_list()[0] + drop_plot.unlink() + env.dir_3.drop(drop_plot) + env.expected[0].remove_valid([drop_plot]) + await env.run_sync_test() + env.remove_directory(0, env.dir_3) + await env.run_sync_test() + env.remove_directory(1, env.dir_4) + await env.run_sync_test() + env.remove_directory(0, env.dir_1) + env.remove_directory(1, env.dir_2) + await env.run_sync_test() + + +@pytest.mark.asyncio +async def test_sync_invalid(environment: Environment) -> None: + env: Environment = environment + assert len(env.farmer.plot_sync_receivers) == 2 + # Use dir_3 and dir_4 in this test because the invalid plots are copies from dir_1 + dir_2 + env.add_directory(0, env.dir_3) + env.add_directory(0, env.dir_invalid, State.invalid) + env.add_directory(1, env.dir_4) + await env.run_sync_test() + # Run again two times to make sure we still get the same results in repeated refresh intervals + await env.run_sync_test() + await env.run_sync_test() + # Drop all but two of the invalid plots + assert len(env.dir_invalid) > 2 + for _ in range(len(env.dir_invalid) - 2): + drop_plot = env.dir_invalid.path_list()[0] + drop_plot.unlink() + env.dir_invalid.drop(drop_plot) + env.expected[0].remove_invalid([drop_plot]) + await env.run_sync_test() + assert len(env.dir_invalid) == 2 + # Add the directory to the first harvester too + env.add_directory(1, env.dir_invalid, State.invalid) + await env.run_sync_test() + # Recover one the remaining invalid plot + for path in get_test_plots(): + if path.name == env.dir_invalid.path_list()[0].name: + copy(path, env.dir_invalid.path) + for i in range(len(env.harvesters)): + env.expected[i].add_valid([env.dir_invalid.plot_info_list()[0]]) + env.expected[i].remove_invalid([env.dir_invalid.path_list()[0]]) + env.harvesters[i].plot_manager.refresh_parameter.retry_invalid_seconds = 0 + await env.run_sync_test() + for i in [0, 1]: + remove_plot_directory(env.harvesters[i].root_path, str(env.dir_invalid.path)) + env.expected[i].remove_valid([env.dir_invalid.path_list()[0]]) + env.expected[i].remove_invalid([env.dir_invalid.path_list()[1]]) + await env.run_sync_test() + + +@pytest.mark.asyncio +async def test_sync_keys_missing(environment: Environment) -> None: + env: Environment = environment + env.add_directory(0, env.dir_1) + env.add_directory(0, env.dir_keys_missing, State.keys_missing) + env.add_directory(1, env.dir_2) + await env.run_sync_test() + # Run again two times to make sure we still get the same results in repeated refresh intervals + await env.run_sync_test() + await env.run_sync_test() + # Drop all but 2 plots with missing keys and test sync inbetween + assert len(env.dir_keys_missing) > 2 + for _ in range(len(env.dir_keys_missing) - 2): + drop_plot = env.dir_keys_missing.path_list()[0] + drop_plot.unlink() + env.dir_keys_missing.drop(drop_plot) + env.expected[0].remove_keys_missing([drop_plot]) + await env.run_sync_test() + assert len(env.dir_keys_missing) == 2 + # Add the plots with missing keys to the other harvester + env.add_directory(0, env.dir_3) + env.add_directory(1, env.dir_keys_missing, State.keys_missing) + await env.run_sync_test() + # Add the missing keys to the first harvester's plot manager + env.harvesters[0].plot_manager.farmer_public_keys.append(G1Element()) + env.harvesters[0].plot_manager.pool_public_keys.append(G1Element()) + # And validate they become valid now + env.expected[0].add_valid(env.dir_keys_missing.plot_info_list()) + env.expected[0].remove_keys_missing(env.dir_keys_missing.path_list()) + await env.run_sync_test() + # Drop the valid plots from one harvester and the keys missing plots from the other harvester + env.remove_directory(0, env.dir_keys_missing) + env.remove_directory(1, env.dir_keys_missing, State.keys_missing) + await env.run_sync_test() + + +@pytest.mark.asyncio +async def test_sync_duplicates(environment: Environment) -> None: + env: Environment = environment + # dir_4 and then dir_duplicates contain the same plots. Load dir_4 first to make sure the plots seen as duplicates + # are from dir_duplicates. + env.add_directory(0, env.dir_4) + await env.run_sync_test() + env.add_directory(0, env.dir_duplicates, State.duplicates) + env.add_directory(1, env.dir_2) + await env.run_sync_test() + # Run again two times to make sure we still get the same results in repeated refresh intervals + await env.run_sync_test() + await env.run_sync_test() + # Drop all but 1 duplicates and test sync in-between + assert len(env.dir_duplicates) > 2 + for _ in range(len(env.dir_duplicates) - 2): + drop_plot = env.dir_duplicates.path_list()[0] + drop_plot.unlink() + env.dir_duplicates.drop(drop_plot) + env.expected[0].remove_duplicates([drop_plot]) + await env.run_sync_test() + assert len(env.dir_duplicates) == 2 + # Removing dir_4 now leads to the plots in dir_duplicates to become loaded instead + env.remove_directory(0, env.dir_4) + env.expected[0].remove_duplicates(env.dir_duplicates.path_list()) + env.expected[0].add_valid(env.dir_duplicates.plot_info_list()) + await env.run_sync_test() + + +async def add_and_validate_all_directories(env: Environment) -> None: + # Add all available directories to both harvesters and make sure they load and get synced + env.add_all_directories(0) + env.add_all_directories(1) + await env.run_sync_test() + env.add_directory(0, env.dir_duplicates, State.duplicates) + env.add_directory(1, env.dir_duplicates, State.duplicates) + await env.run_sync_test() + + +async def remove_and_validate_all_directories(env: Environment) -> None: + # Remove all available directories to both harvesters and make sure they are removed and get synced + env.remove_all_directories(0) + env.remove_all_directories(1) + await env.run_sync_test() + + +@pytest.mark.asyncio +async def test_add_and_remove_all_directories(environment: Environment) -> None: + await add_and_validate_all_directories(environment) + await remove_and_validate_all_directories(environment) + + +@pytest.mark.asyncio +async def test_harvester_restart(environment: Environment) -> None: + env: Environment = environment + # Load all directories for both harvesters + await add_and_validate_all_directories(env) + # Stop the harvester and make sure the receiver gets dropped on the farmer and refreshing gets stopped + env.harvester_services[0].stop() + await env.harvester_services[0].wait_closed() + assert len(env.farmer.plot_sync_receivers) == 1 + assert not env.harvesters[0].plot_manager._refreshing_enabled + assert not env.harvesters[0].plot_manager.needs_refresh() + # Start the harvester, wait for the handshake and make sure the receiver comes back + await env.harvester_services[0].start() + await time_out_assert(5, env.handshake_done, True, 0) + assert len(env.farmer.plot_sync_receivers) == 2 + # Remove the duplicates dir to avoid conflicts with the original plots + env.remove_directory(0, env.dir_duplicates) + # Reset the expected data for harvester 0 and re-add all directories because of the restart + env.expected[0] = ExpectedResult() + env.add_all_directories(0) + # Run the refresh two times and make sure everything recovers and stays recovered after harvester restart + await env.run_sync_test() + env.add_directory(0, env.dir_duplicates, State.duplicates) + await env.run_sync_test() + + +@pytest.mark.asyncio +async def test_farmer_restart(environment: Environment) -> None: + env: Environment = environment + # Load all directories for both harvesters + await add_and_validate_all_directories(env) + last_sync_ids: List[uint64] = [] + for i in range(0, len(env.harvesters)): + last_sync_ids.append(env.harvesters[i].plot_sync_sender._last_sync_id) + # Stop the farmer and make sure both receivers get dropped and refreshing gets stopped on the harvesters + env.farmer_service.stop() + await env.farmer_service.wait_closed() + assert len(env.farmer.plot_sync_receivers) == 0 + assert not env.harvesters[0].plot_manager._refreshing_enabled + assert not env.harvesters[1].plot_manager._refreshing_enabled + # Start the farmer, wait for the handshake and make sure the receivers come back + await env.farmer_service.start() + await time_out_assert(5, env.handshake_done, True, 0) + await time_out_assert(5, env.handshake_done, True, 1) + assert len(env.farmer.plot_sync_receivers) == 2 + # Do not use run_sync_test here, to have a more realistic test scenario just wait for the harvesters to be synced. + # The handshake should trigger re-sync. + for i in range(0, len(env.harvesters)): + harvester: Harvester = env.harvesters[i] + assert harvester.server is not None + receiver = env.farmer.plot_sync_receivers[harvester.server.node_id] + await time_out_assert(10, synced, True, harvester.plot_sync_sender, receiver, last_sync_ids[i]) + # Validate the sync + for harvester in env.harvesters: + plot_manager: PlotManager = harvester.plot_manager + assert harvester.server is not None + receiver = env.farmer.plot_sync_receivers[harvester.server.node_id] + expected = env.expected[env.harvesters.index(harvester)] + assert plot_manager.plot_count() == len(receiver.plots()) == expected.valid_count + assert len(plot_manager.failed_to_open_filenames) == len(receiver.invalid()) == expected.invalid_count + assert len(plot_manager.no_key_filenames) == len(receiver.keys_missing()) == expected.keys_missing_count + assert len(plot_manager.get_duplicates()) == len(receiver.duplicates()) == expected.duplicates_count diff --git a/tests/plot_sync/test_receiver.py b/tests/plot_sync/test_receiver.py new file mode 100644 index 000000000000..5c63ae412640 --- /dev/null +++ b/tests/plot_sync/test_receiver.py @@ -0,0 +1,376 @@ +import logging +import time +from secrets import token_bytes +from typing import Any, Callable, List, Tuple, Type, Union + +import pytest +from blspy import G1Element + +from chia.plot_sync.delta import Delta +from chia.plot_sync.receiver import Receiver +from chia.plot_sync.util import ErrorCodes, State +from chia.protocols.harvester_protocol import ( + Plot, + PlotSyncDone, + PlotSyncIdentifier, + PlotSyncPathList, + PlotSyncPlotList, + PlotSyncResponse, + PlotSyncStart, +) +from chia.server.ws_connection import NodeType +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.util.ints import uint8, uint32, uint64 +from chia.util.streamable import _T_Streamable +from tests.plot_sync.util import get_dummy_connection + +log = logging.getLogger(__name__) + +next_message_id = uint64(0) + + +def assert_default_values(receiver: Receiver) -> None: + assert receiver.state() == State.idle + assert receiver.expected_sync_id() == 0 + assert receiver.expected_message_id() == 0 + assert receiver.last_sync_id() == 0 + assert receiver.last_sync_time() == 0 + assert receiver.plots() == {} + assert receiver.invalid() == [] + assert receiver.keys_missing() == [] + assert receiver.duplicates() == [] + + +async def dummy_callback(_: bytes32, __: Delta) -> None: + pass + + +class SyncStepData: + state: State + function: Any + payload_type: Any + args: Any + + def __init__( + self, state: State, function: Callable[[_T_Streamable], Any], payload_type: Type[_T_Streamable], *args: Any + ) -> None: + self.state = state + self.function = function + self.payload_type = payload_type + self.args = args + + +def plot_sync_identifier(current_sync_id: uint64, message_id: uint64) -> PlotSyncIdentifier: + return PlotSyncIdentifier(uint64(0), current_sync_id, message_id) + + +def create_payload(payload_type: Any, start: bool, *args: Any) -> Any: + global next_message_id + if start: + next_message_id = uint64(0) + next_identifier = plot_sync_identifier(uint64(1), next_message_id) + next_message_id = uint64(next_message_id + 1) + return payload_type(next_identifier, *args) + + +def assert_error_response(plot_sync: Receiver, error_code: ErrorCodes) -> None: + connection = plot_sync.connection() + assert connection is not None + message = connection.last_sent_message + assert message is not None + response: PlotSyncResponse = PlotSyncResponse.from_bytes(message.data) + assert response.error is not None + assert response.error.code == error_code.value + + +def pre_function_validate(receiver: Receiver, data: Union[List[Plot], List[str]], expected_state: State) -> None: + if expected_state == State.loaded: + for plot_info in data: + assert type(plot_info) == Plot + assert plot_info.filename not in receiver.plots() + elif expected_state == State.removed: + for path in data: + assert path in receiver.plots() + elif expected_state == State.invalid: + for path in data: + assert path not in receiver.invalid() + elif expected_state == State.keys_missing: + for path in data: + assert path not in receiver.keys_missing() + elif expected_state == State.duplicates: + for path in data: + assert path not in receiver.duplicates() + + +def post_function_validate(receiver: Receiver, data: Union[List[Plot], List[str]], expected_state: State) -> None: + if expected_state == State.loaded: + for plot_info in data: + assert type(plot_info) == Plot + assert plot_info.filename in receiver._delta.valid.additions + elif expected_state == State.removed: + for path in data: + assert path in receiver._delta.valid.removals + elif expected_state == State.invalid: + for path in data: + assert path in receiver._delta.invalid.additions + elif expected_state == State.keys_missing: + for path in data: + assert path in receiver._delta.keys_missing.additions + elif expected_state == State.duplicates: + for path in data: + assert path in receiver._delta.duplicates.additions + + +@pytest.mark.asyncio +async def run_sync_step(receiver: Receiver, sync_step: SyncStepData, expected_state: State) -> None: + assert receiver.state() == expected_state + last_sync_time_before = receiver._last_sync_time + # For the the list types invoke the trigger function in batches + if sync_step.payload_type == PlotSyncPlotList or sync_step.payload_type == PlotSyncPathList: + step_data, _ = sync_step.args + assert len(step_data) == 10 + # Invoke batches of: 1, 2, 3, 4 items and validate the data against plot store before and after + indexes = [0, 1, 3, 6, 10] + for i in range(0, len(indexes) - 1): + invoke_data = step_data[indexes[i] : indexes[i + 1]] + pre_function_validate(receiver, invoke_data, expected_state) + await sync_step.function( + create_payload(sync_step.payload_type, False, invoke_data, i == (len(indexes) - 2)) + ) + post_function_validate(receiver, invoke_data, expected_state) + else: + # For Start/Done just invoke it.. + await sync_step.function(create_payload(sync_step.payload_type, sync_step.state == State.idle, *sync_step.args)) + # Make sure we moved to the next state + assert receiver.state() != expected_state + if sync_step.payload_type == PlotSyncDone: + assert receiver._last_sync_time != last_sync_time_before + else: + assert receiver._last_sync_time == last_sync_time_before + + +def plot_sync_setup() -> Tuple[Receiver, List[SyncStepData]]: + harvester_connection = get_dummy_connection(NodeType.HARVESTER) + receiver = Receiver(harvester_connection, dummy_callback) # type:ignore[arg-type] + + # Create example plot data + path_list = [str(x) for x in range(0, 40)] + plot_info_list = [ + Plot( + filename=str(x), + size=uint8(0), + plot_id=bytes32(token_bytes(32)), + pool_contract_puzzle_hash=None, + pool_public_key=None, + plot_public_key=G1Element(), + file_size=uint64(0), + time_modified=uint64(0), + ) + for x in path_list + ] + + # Manually add the plots we want to remove in tests + receiver._plots = {plot_info.filename: plot_info for plot_info in plot_info_list[0:10]} + + sync_steps: List[SyncStepData] = [ + SyncStepData(State.idle, receiver.sync_started, PlotSyncStart, False, uint64(0), uint32(len(plot_info_list))), + SyncStepData(State.loaded, receiver.process_loaded, PlotSyncPlotList, plot_info_list[10:20], True), + SyncStepData(State.removed, receiver.process_removed, PlotSyncPathList, path_list[0:10], True), + SyncStepData(State.invalid, receiver.process_invalid, PlotSyncPathList, path_list[20:30], True), + SyncStepData(State.keys_missing, receiver.process_keys_missing, PlotSyncPathList, path_list[30:40], True), + SyncStepData(State.duplicates, receiver.process_duplicates, PlotSyncPathList, path_list[10:20], True), + SyncStepData(State.done, receiver.sync_done, PlotSyncDone, uint64(0)), + ] + + return receiver, sync_steps + + +def test_default_values() -> None: + assert_default_values(Receiver(get_dummy_connection(NodeType.HARVESTER), dummy_callback)) # type:ignore[arg-type] + + +@pytest.mark.asyncio +async def test_reset() -> None: + receiver, sync_steps = plot_sync_setup() + connection_before = receiver.connection() + # Assign some dummy values + receiver._sync_state = State.done + receiver._expected_sync_id = uint64(1) + receiver._expected_message_id = uint64(1) + receiver._last_sync_id = uint64(1) + receiver._last_sync_time = time.time() + receiver._invalid = ["1"] + receiver._keys_missing = ["1"] + receiver._delta.valid.additions = receiver.plots().copy() + receiver._delta.valid.removals = ["1"] + receiver._delta.invalid.additions = ["1"] + receiver._delta.invalid.removals = ["1"] + receiver._delta.keys_missing.additions = ["1"] + receiver._delta.keys_missing.removals = ["1"] + receiver._delta.duplicates.additions = ["1"] + receiver._delta.duplicates.removals = ["1"] + # Call `reset` and make sure all expected values are set back to their defaults. + receiver.reset() + assert_default_values(receiver) + assert receiver._delta == Delta() + # Connection should remain + assert receiver.connection() == connection_before + + +@pytest.mark.asyncio +async def test_to_dict() -> None: + receiver, sync_steps = plot_sync_setup() + plot_sync_dict_1 = receiver.to_dict() + assert "plots" in plot_sync_dict_1 and len(plot_sync_dict_1["plots"]) == 10 + assert "failed_to_open_filenames" in plot_sync_dict_1 and len(plot_sync_dict_1["failed_to_open_filenames"]) == 0 + assert "no_key_filenames" in plot_sync_dict_1 and len(plot_sync_dict_1["no_key_filenames"]) == 0 + assert "last_sync_time" not in plot_sync_dict_1 + assert plot_sync_dict_1["connection"] == { + "node_id": receiver.connection().peer_node_id, + "host": receiver.connection().peer_host, + "port": receiver.connection().peer_port, + } + + # We should get equal dicts + plot_sync_dict_2 = receiver.to_dict() + assert plot_sync_dict_1 == plot_sync_dict_2 + + dict_2_paths = [x.filename for x in plot_sync_dict_2["plots"]] + for plot_info in sync_steps[State.loaded].args[0]: + assert plot_info.filename not in dict_2_paths + + # Walk through all states from idle to done and run them with the test data + for state in State: + await run_sync_step(receiver, sync_steps[state], state) + + plot_sync_dict_3 = receiver.to_dict() + dict_3_paths = [x.filename for x in plot_sync_dict_3["plots"]] + for plot_info in sync_steps[State.loaded].args[0]: + assert plot_info.filename in dict_3_paths + + for path in sync_steps[State.removed].args[0]: + assert path not in plot_sync_dict_3["plots"] + + for path in sync_steps[State.invalid].args[0]: + assert path in plot_sync_dict_3["failed_to_open_filenames"] + + for path in sync_steps[State.keys_missing].args[0]: + assert path in plot_sync_dict_3["no_key_filenames"] + + for path in sync_steps[State.duplicates].args[0]: + assert path in plot_sync_dict_3["duplicates"] + + assert plot_sync_dict_3["last_sync_time"] > 0 + + +@pytest.mark.asyncio +async def test_sync_flow() -> None: + receiver, sync_steps = plot_sync_setup() + + for plot_info in sync_steps[State.loaded].args[0]: + assert plot_info.filename not in receiver.plots() + + for path in sync_steps[State.removed].args[0]: + assert path in receiver.plots() + + for path in sync_steps[State.invalid].args[0]: + assert path not in receiver.invalid() + + for path in sync_steps[State.keys_missing].args[0]: + assert path not in receiver.keys_missing() + + for path in sync_steps[State.duplicates].args[0]: + assert path not in receiver.duplicates() + + # Walk through all states from idle to done and run them with the test data + for state in State: + await run_sync_step(receiver, sync_steps[state], state) + + for plot_info in sync_steps[State.loaded].args[0]: + assert plot_info.filename in receiver.plots() + + for path in sync_steps[State.removed].args[0]: + assert path not in receiver.plots() + + for path in sync_steps[State.invalid].args[0]: + assert path in receiver.invalid() + + for path in sync_steps[State.keys_missing].args[0]: + assert path in receiver.keys_missing() + + for path in sync_steps[State.duplicates].args[0]: + assert path in receiver.duplicates() + + # We should be in idle state again + assert receiver.state() == State.idle + + +@pytest.mark.asyncio +async def test_invalid_ids() -> None: + receiver, sync_steps = plot_sync_setup() + for state in State: + assert receiver.state() == state + current_step = sync_steps[state] + if receiver.state() == State.idle: + # Set last_sync_id for the tests below + receiver._last_sync_id = uint64(1) + # Test "sync_started last doesn't match" + invalid_last_sync_id_param = PlotSyncStart( + plot_sync_identifier(uint64(0), uint64(0)), False, uint64(2), uint32(0) + ) + await current_step.function(invalid_last_sync_id_param) + assert_error_response(receiver, ErrorCodes.invalid_last_sync_id) + # Test "last_sync_id == new_sync_id" + invalid_sync_id_match_param = PlotSyncStart( + plot_sync_identifier(uint64(1), uint64(0)), False, uint64(1), uint32(0) + ) + await current_step.function(invalid_sync_id_match_param) + assert_error_response(receiver, ErrorCodes.sync_ids_match) + # Reset the last_sync_id to the default + receiver._last_sync_id = uint64(0) + else: + # Test invalid sync_id + invalid_sync_id_param = current_step.payload_type( + plot_sync_identifier(uint64(10), uint64(receiver.expected_message_id())), *current_step.args + ) + await current_step.function(invalid_sync_id_param) + assert_error_response(receiver, ErrorCodes.invalid_identifier) + # Test invalid message_id + invalid_message_id_param = current_step.payload_type( + plot_sync_identifier(receiver.expected_sync_id(), uint64(receiver.expected_message_id() + 1)), + *current_step.args, + ) + await current_step.function(invalid_message_id_param) + assert_error_response(receiver, ErrorCodes.invalid_identifier) + payload = create_payload(current_step.payload_type, state == State.idle, *current_step.args) + await current_step.function(payload) + + +@pytest.mark.parametrize( + ["state_to_fail", "expected_error_code"], + [ + pytest.param(State.loaded, ErrorCodes.plot_already_available, id="already available plots"), + pytest.param(State.invalid, ErrorCodes.plot_already_available, id="already available paths"), + pytest.param(State.removed, ErrorCodes.plot_not_available, id="not available"), + ], +) +@pytest.mark.asyncio +async def test_plot_errors(state_to_fail: State, expected_error_code: ErrorCodes) -> None: + receiver, sync_steps = plot_sync_setup() + for state in State: + assert receiver.state() == state + current_step = sync_steps[state] + if state == state_to_fail: + plot_infos, _ = current_step.args + await current_step.function(create_payload(current_step.payload_type, False, plot_infos, False)) + identifier = plot_sync_identifier(receiver.expected_sync_id(), receiver.expected_message_id()) + invalid_payload = current_step.payload_type(identifier, plot_infos, True) + await current_step.function(invalid_payload) + if state == state_to_fail: + assert_error_response(receiver, expected_error_code) + return + else: + await current_step.function( + create_payload(current_step.payload_type, state == State.idle, *current_step.args) + ) + assert False, "Didn't fail in the expected state" diff --git a/tests/plot_sync/test_sender.py b/tests/plot_sync/test_sender.py new file mode 100644 index 000000000000..09747ec8bbaf --- /dev/null +++ b/tests/plot_sync/test_sender.py @@ -0,0 +1,102 @@ +import pytest + +from chia.plot_sync.exceptions import AlreadyStartedError, InvalidConnectionTypeError +from chia.plot_sync.sender import ExpectedResponse, Sender +from chia.plot_sync.util import Constants +from chia.protocols.harvester_protocol import PlotSyncIdentifier, PlotSyncResponse +from chia.server.ws_connection import NodeType, ProtocolMessageTypes +from chia.util.ints import int16, uint64 +from tests.block_tools import BlockTools +from tests.plot_sync.util import get_dummy_connection, plot_sync_identifier + + +def test_default_values(bt: BlockTools) -> None: + sender = Sender(bt.plot_manager) + assert sender._plot_manager == bt.plot_manager + assert sender._connection is None + assert sender._sync_id == uint64(0) + assert sender._next_message_id == uint64(0) + assert sender._messages == [] + assert sender._last_sync_id == uint64(0) + assert not sender._stop_requested + assert sender._task is None + assert not sender._lock.locked() + assert sender._response is None + + +def test_set_connection_values(bt: BlockTools) -> None: + farmer_connection = get_dummy_connection(NodeType.FARMER) + sender = Sender(bt.plot_manager) + # Test invalid NodeType values + for connection_type in NodeType: + if connection_type != NodeType.FARMER: + pytest.raises( + InvalidConnectionTypeError, + sender.set_connection, + get_dummy_connection(connection_type, farmer_connection.peer_node_id), + ) + # Test setting a valid connection works + sender.set_connection(farmer_connection) # type:ignore[arg-type] + assert sender._connection is not None + assert sender._connection == farmer_connection # type: ignore[comparison-overlap] + + +@pytest.mark.asyncio +async def test_start_stop_send_task(bt: BlockTools) -> None: + sender = Sender(bt.plot_manager) + # Make sure starting/restarting works + for _ in range(2): + assert sender._task is None + await sender.start() + assert sender._task is not None + with pytest.raises(AlreadyStartedError): + await sender.start() + assert not sender._stop_requested + sender.stop() + assert sender._stop_requested + await sender.await_closed() + assert not sender._stop_requested + assert sender._task is None + + +def test_set_response(bt: BlockTools) -> None: + sender = Sender(bt.plot_manager) + + def new_expected_response(sync_id: int, message_id: int, message_type: ProtocolMessageTypes) -> ExpectedResponse: + return ExpectedResponse(message_type, plot_sync_identifier(uint64(sync_id), uint64(message_id))) + + def new_response_message(sync_id: int, message_id: int, message_type: ProtocolMessageTypes) -> PlotSyncResponse: + return PlotSyncResponse( + plot_sync_identifier(uint64(sync_id), uint64(message_id)), int16(int(message_type.value)), None + ) + + response_message = new_response_message(0, 1, ProtocolMessageTypes.plot_sync_start) + assert sender._response is None + # Should trigger unexpected response because `Farmer._response` is `None` + assert not sender.set_response(response_message) + # Set `Farmer._response` and make sure the response gets assigned properly + sender._response = new_expected_response(0, 1, ProtocolMessageTypes.plot_sync_start) + assert sender._response.message is None + assert sender.set_response(response_message) + assert sender._response.message is not None + # Should trigger unexpected response because we already received the message for the currently expected response + assert not sender.set_response(response_message) + # Test expired message + expected_response = new_expected_response(1, 0, ProtocolMessageTypes.plot_sync_start) + sender._response = expected_response + expired_identifier = PlotSyncIdentifier( + uint64(expected_response.identifier.timestamp - Constants.message_timeout - 1), + expected_response.identifier.sync_id, + expected_response.identifier.message_id, + ) + expired_message = PlotSyncResponse(expired_identifier, int16(int(ProtocolMessageTypes.plot_sync_start.value)), None) + assert not sender.set_response(expired_message) + # Test invalid sync-id + sender._response = new_expected_response(2, 0, ProtocolMessageTypes.plot_sync_start) + assert not sender.set_response(new_response_message(3, 0, ProtocolMessageTypes.plot_sync_start)) + # Test invalid message-id + sender._response = new_expected_response(2, 1, ProtocolMessageTypes.plot_sync_start) + assert not sender.set_response(new_response_message(2, 2, ProtocolMessageTypes.plot_sync_start)) + # Test invalid message-type + sender._response = new_expected_response(3, 0, ProtocolMessageTypes.plot_sync_start) + assert not sender.set_response(new_response_message(3, 0, ProtocolMessageTypes.plot_sync_loaded)) diff --git a/tests/plot_sync/test_sync_simulated.py b/tests/plot_sync/test_sync_simulated.py new file mode 100644 index 000000000000..ae83dc7b64c4 --- /dev/null +++ b/tests/plot_sync/test_sync_simulated.py @@ -0,0 +1,433 @@ +import asyncio +import functools +import logging +import time +from dataclasses import dataclass, field +from enum import Enum +from pathlib import Path +from secrets import token_bytes +from typing import Any, Dict, List, Optional, Set, Tuple + +import pytest +from blspy import G1Element + +from chia.farmer.farmer_api import Farmer +from chia.harvester.harvester_api import Harvester +from chia.plot_sync.receiver import Receiver +from chia.plot_sync.sender import Sender +from chia.plot_sync.util import Constants +from chia.plotting.manager import PlotManager +from chia.plotting.util import PlotInfo +from chia.protocols.harvester_protocol import PlotSyncError, PlotSyncResponse +from chia.server.start_service import Service +from chia.server.ws_connection import ProtocolMessageTypes, WSChiaConnection, make_msg +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.util.generator_tools import list_to_batches +from chia.util.ints import int16, uint64 +from tests.plot_sync.util import start_harvester_service +from tests.time_out_assert import time_out_assert + +log = logging.getLogger(__name__) + + +class ErrorSimulation(Enum): + DropEveryFourthMessage = 1 + DropThreeMessages = 2 + RespondTooLateEveryFourthMessage = 3 + RespondTwice = 4 + NonRecoverableError = 5 + NotConnected = 6 + + +@dataclass +class TestData: + harvester: Harvester + plot_sync_sender: Sender + plot_sync_receiver: Receiver + event_loop: asyncio.AbstractEventLoop + plots: Dict[Path, PlotInfo] = field(default_factory=dict) + invalid: List[PlotInfo] = field(default_factory=list) + keys_missing: List[PlotInfo] = field(default_factory=list) + duplicates: List[PlotInfo] = field(default_factory=list) + + async def run( + self, + *, + loaded: List[PlotInfo], + removed: List[PlotInfo], + invalid: List[PlotInfo], + keys_missing: List[PlotInfo], + duplicates: List[PlotInfo], + initial: bool, + ) -> None: + for plot_info in loaded: + assert plot_info.prover.get_filename() not in self.plots + for plot_info in removed: + assert plot_info.prover.get_filename() in self.plots + + self.invalid = invalid + self.keys_missing = keys_missing + self.duplicates = duplicates + + removed_paths: List[Path] = [p.prover.get_filename() for p in removed] if removed is not None else [] + invalid_dict: Dict[Path, int] = {p.prover.get_filename(): 0 for p in self.invalid} + keys_missing_set: Set[Path] = set([p.prover.get_filename() for p in self.keys_missing]) + duplicates_set: Set[str] = set([p.prover.get_filename() for p in self.duplicates]) + + # Inject invalid plots into `PlotManager` of the harvester so that the callback calls below can use them + # to sync them to the farmer. + self.harvester.plot_manager.failed_to_open_filenames = invalid_dict + # Inject key missing plots into `PlotManager` of the harvester so that the callback calls below can use them + # to sync them to the farmer. + self.harvester.plot_manager.no_key_filenames = keys_missing_set + # Inject duplicated plots into `PlotManager` of the harvester so that the callback calls below can use them + # to sync them to the farmer. + for plot_info in loaded: + plot_path = Path(plot_info.prover.get_filename()) + self.harvester.plot_manager.plot_filename_paths[plot_path.name] = (str(plot_path.parent), set()) + for duplicate in duplicates_set: + plot_path = Path(duplicate) + assert plot_path.name in self.harvester.plot_manager.plot_filename_paths + self.harvester.plot_manager.plot_filename_paths[plot_path.name][1].add(str(plot_path.parent)) + + batch_size = self.harvester.plot_manager.refresh_parameter.batch_size + + # Used to capture the sync id in `run_internal` + sync_id: Optional[uint64] = None + + def run_internal() -> None: + nonlocal sync_id + # Simulate one plot manager refresh cycle by calling the methods directly. + self.harvester.plot_sync_sender.sync_start(len(loaded), initial) + sync_id = self.plot_sync_sender._sync_id + if len(loaded) == 0: + self.harvester.plot_sync_sender.process_batch([], 0) + for remaining, batch in list_to_batches(loaded, batch_size): + self.harvester.plot_sync_sender.process_batch(batch, remaining) + self.harvester.plot_sync_sender.sync_done(removed_paths, 0) + + await self.event_loop.run_in_executor(None, run_internal) + + async def sync_done() -> bool: + assert sync_id is not None + return self.plot_sync_receiver.last_sync_id() == self.plot_sync_sender._last_sync_id == sync_id + + await time_out_assert(60, sync_done) + + for plot_info in loaded: + self.plots[plot_info.prover.get_filename()] = plot_info + for plot_info in removed: + del self.plots[plot_info.prover.get_filename()] + + def validate_plot_sync(self) -> None: + assert len(self.plots) == len(self.plot_sync_receiver.plots()) + assert len(self.invalid) == len(self.plot_sync_receiver.invalid()) + assert len(self.keys_missing) == len(self.plot_sync_receiver.keys_missing()) + for _, plot_info in self.plots.items(): + assert plot_info.prover.get_filename() not in self.plot_sync_receiver.invalid() + assert plot_info.prover.get_filename() not in self.plot_sync_receiver.keys_missing() + assert plot_info.prover.get_filename() in self.plot_sync_receiver.plots() + synced_plot = self.plot_sync_receiver.plots()[plot_info.prover.get_filename()] + assert plot_info.prover.get_filename() == synced_plot.filename + assert plot_info.pool_public_key == synced_plot.pool_public_key + assert plot_info.pool_contract_puzzle_hash == synced_plot.pool_contract_puzzle_hash + assert plot_info.plot_public_key == synced_plot.plot_public_key + assert plot_info.file_size == synced_plot.file_size + assert uint64(int(plot_info.time_modified)) == synced_plot.time_modified + for plot_info in self.invalid: + assert plot_info.prover.get_filename() not in self.plot_sync_receiver.plots() + assert plot_info.prover.get_filename() in self.plot_sync_receiver.invalid() + assert plot_info.prover.get_filename() not in self.plot_sync_receiver.keys_missing() + assert plot_info.prover.get_filename() not in self.plot_sync_receiver.duplicates() + for plot_info in self.keys_missing: + assert plot_info.prover.get_filename() not in self.plot_sync_receiver.plots() + assert plot_info.prover.get_filename() not in self.plot_sync_receiver.invalid() + assert plot_info.prover.get_filename() in self.plot_sync_receiver.keys_missing() + assert plot_info.prover.get_filename() not in self.plot_sync_receiver.duplicates() + for plot_info in self.duplicates: + assert plot_info.prover.get_filename() not in self.plot_sync_receiver.invalid() + assert plot_info.prover.get_filename() not in self.plot_sync_receiver.keys_missing() + assert plot_info.prover.get_filename() in self.plot_sync_receiver.duplicates() + + +@dataclass +class TestRunner: + test_data: List[TestData] + + def __init__( + self, harvesters: List[Harvester], farmer: Farmer, event_loop: asyncio.events.AbstractEventLoop + ) -> None: + self.test_data = [] + for harvester in harvesters: + assert harvester.server is not None + self.test_data.append( + TestData( + harvester, + harvester.plot_sync_sender, + farmer.plot_sync_receivers[harvester.server.node_id], + event_loop, + ) + ) + + async def run( + self, + index: int, + *, + loaded: List[PlotInfo], + removed: List[PlotInfo], + invalid: List[PlotInfo], + keys_missing: List[PlotInfo], + duplicates: List[PlotInfo], + initial: bool, + ) -> None: + await self.test_data[index].run( + loaded=loaded, + removed=removed, + invalid=invalid, + keys_missing=keys_missing, + duplicates=duplicates, + initial=initial, + ) + for data in self.test_data: + data.validate_plot_sync() + + +async def skip_processing(self: Any, _: WSChiaConnection, message_type: ProtocolMessageTypes, message: Any) -> bool: + self.message_counter += 1 + if self.simulate_error == ErrorSimulation.DropEveryFourthMessage: + if self.message_counter % 4 == 0: + return True + if self.simulate_error == ErrorSimulation.DropThreeMessages: + if 2 < self.message_counter < 6: + return True + if self.simulate_error == ErrorSimulation.RespondTooLateEveryFourthMessage: + if self.message_counter % 4 == 0: + await asyncio.sleep(Constants.message_timeout + 1) + return False + if self.simulate_error == ErrorSimulation.RespondTwice: + await self.connection().send_message( + make_msg( + ProtocolMessageTypes.plot_sync_response, + PlotSyncResponse(message.identifier, int16(message_type.value), None), + ) + ) + if self.simulate_error == ErrorSimulation.NonRecoverableError and self.message_counter > 1: + await self.connection().send_message( + make_msg( + ProtocolMessageTypes.plot_sync_response, + PlotSyncResponse( + message.identifier, int16(message_type.value), PlotSyncError(int16(0), "non recoverable", None) + ), + ) + ) + self.simulate_error = 0 + return True + return False + + +async def _testable_process( + self: Any, peer: WSChiaConnection, message_type: ProtocolMessageTypes, message: Any +) -> None: + if await skip_processing(self, peer, message_type, message): + return + await self.original_process(peer, message_type, message) + + +async def create_test_runner( + harvester_services: List[Service], farmer: Farmer, event_loop: asyncio.events.AbstractEventLoop +) -> TestRunner: + assert len(farmer.plot_sync_receivers) == 0 + harvesters: List[Harvester] = [await start_harvester_service(service) for service in harvester_services] + for receiver in farmer.plot_sync_receivers.values(): + receiver.simulate_error = 0 # type: ignore[attr-defined] + receiver.message_counter = 0 # type: ignore[attr-defined] + receiver.original_process = receiver._process # type: ignore[attr-defined] + receiver._process = functools.partial(_testable_process, receiver) # type: ignore[assignment] + return TestRunner(harvesters, farmer, event_loop) + + +def create_example_plots(count: int) -> List[PlotInfo]: + @dataclass + class DiskProver: + file_name: str + plot_id: bytes32 + size: int + + def get_filename(self) -> str: + return self.file_name + + def get_id(self) -> bytes32: + return self.plot_id + + def get_size(self) -> int: + return self.size + + return [ + PlotInfo( + prover=DiskProver(f"{x}", bytes32(token_bytes(32)), x % 255), + pool_public_key=None, + pool_contract_puzzle_hash=None, + plot_public_key=G1Element(), + file_size=uint64(0), + time_modified=time.time(), + ) + for x in range(0, count) + ] + + +@pytest.mark.asyncio +async def test_sync_simulated( + farmer_three_harvester: Tuple[List[Service], Service], event_loop: asyncio.events.AbstractEventLoop +) -> None: + harvester_services: List[Service] + farmer_service: Service + harvester_services, farmer_service = farmer_three_harvester + farmer: Farmer = farmer_service._node + test_runner: TestRunner = await create_test_runner(harvester_services, farmer, event_loop) + plots = create_example_plots(31000) + + await test_runner.run( + 0, loaded=plots[0:10000], removed=[], invalid=[], keys_missing=[], duplicates=plots[0:1000], initial=True + ) + await test_runner.run( + 1, + loaded=plots[10000:20000], + removed=[], + invalid=plots[30000:30100], + keys_missing=[], + duplicates=[], + initial=True, + ) + await test_runner.run( + 2, + loaded=plots[20000:30000], + removed=[], + invalid=[], + keys_missing=plots[30100:30200], + duplicates=[], + initial=True, + ) + await test_runner.run( + 0, + loaded=[], + removed=[], + invalid=plots[30300:30400], + keys_missing=plots[30400:30453], + duplicates=[], + initial=False, + ) + await test_runner.run(0, loaded=[], removed=[], invalid=[], keys_missing=[], duplicates=[], initial=False) + await test_runner.run( + 0, loaded=[], removed=plots[5000:10000], invalid=[], keys_missing=[], duplicates=[], initial=False + ) + await test_runner.run( + 1, loaded=[], removed=plots[10000:20000], invalid=[], keys_missing=[], duplicates=[], initial=False + ) + await test_runner.run( + 2, loaded=[], removed=plots[20000:29000], invalid=[], keys_missing=[], duplicates=[], initial=False + ) + await test_runner.run( + 0, loaded=[], removed=plots[0:5000], invalid=[], keys_missing=[], duplicates=[], initial=False + ) + await test_runner.run( + 2, + loaded=plots[5000:10000], + removed=plots[29000:30000], + invalid=plots[30000:30500], + keys_missing=plots[30500:31000], + duplicates=plots[5000:6000], + initial=False, + ) + await test_runner.run( + 2, loaded=[], removed=plots[5000:10000], invalid=[], keys_missing=[], duplicates=[], initial=False + ) + assert len(farmer.plot_sync_receivers) == 3 + for plot_sync in farmer.plot_sync_receivers.values(): + assert len(plot_sync.plots()) == 0 + + +@pytest.mark.parametrize( + "simulate_error", + [ + ErrorSimulation.DropEveryFourthMessage, + ErrorSimulation.DropThreeMessages, + ErrorSimulation.RespondTooLateEveryFourthMessage, + ErrorSimulation.RespondTwice, + ], +) +@pytest.mark.asyncio +async def test_farmer_error_simulation( + farmer_one_harvester: Tuple[List[Service], Service], + event_loop: asyncio.events.AbstractEventLoop, + simulate_error: ErrorSimulation, +) -> None: + Constants.message_timeout = 5 + harvester_services: List[Service] + farmer_service: Service + harvester_services, farmer_service = farmer_one_harvester + test_runner: TestRunner = await create_test_runner(harvester_services, farmer_service._node, event_loop) + batch_size = test_runner.test_data[0].harvester.plot_manager.refresh_parameter.batch_size + plots = create_example_plots(batch_size + 3) + receiver = test_runner.test_data[0].plot_sync_receiver + receiver.simulate_error = simulate_error # type: ignore[attr-defined] + await test_runner.run( + 0, + loaded=plots[0 : batch_size + 1], + removed=[], + invalid=[plots[batch_size + 1]], + keys_missing=[plots[batch_size + 2]], + duplicates=[], + initial=True, + ) + + +@pytest.mark.parametrize("simulate_error", [ErrorSimulation.NonRecoverableError, ErrorSimulation.NotConnected]) +@pytest.mark.asyncio +async def test_sync_reset_cases( + farmer_one_harvester: Tuple[List[Service], Service], + event_loop: asyncio.events.AbstractEventLoop, + simulate_error: ErrorSimulation, +) -> None: + harvester_services: List[Service] + farmer_service: Service + harvester_services, farmer_service = farmer_one_harvester + test_runner: TestRunner = await create_test_runner(harvester_services, farmer_service._node, event_loop) + test_data: TestData = test_runner.test_data[0] + plot_manager: PlotManager = test_data.harvester.plot_manager + plots = create_example_plots(30) + # Inject some data into `PlotManager` of the harvester so that we can validate the reset worked and triggered a + # fresh sync of all available data of the plot manager + for plot_info in plots[0:10]: + test_data.plots[plot_info.prover.get_filename()] = plot_info + plot_manager.plots = test_data.plots + test_data.invalid = plots[10:20] + test_data.keys_missing = plots[20:30] + test_data.plot_sync_receiver.simulate_error = simulate_error # type: ignore[attr-defined] + sender: Sender = test_runner.test_data[0].plot_sync_sender + started_sync_id: uint64 = uint64(0) + + plot_manager.failed_to_open_filenames = {p.prover.get_filename(): 0 for p in test_data.invalid} + plot_manager.no_key_filenames = set([p.prover.get_filename() for p in test_data.keys_missing]) + + async def wait_for_reset() -> bool: + assert started_sync_id != 0 + return sender._sync_id != started_sync_id != 0 + + async def sync_done() -> bool: + assert started_sync_id != 0 + return test_data.plot_sync_receiver.last_sync_id() == sender._last_sync_id == started_sync_id + + # Send start and capture the sync_id + sender.sync_start(len(plots), True) + started_sync_id = sender._sync_id + # Sleep 2 seconds to make sure we have a different sync_id after the reset which gets triggered + await asyncio.sleep(2) + saved_connection = sender._connection + if simulate_error == ErrorSimulation.NotConnected: + sender._connection = None + sender.process_batch(plots, 0) + await time_out_assert(60, wait_for_reset) + started_sync_id = sender._sync_id + sender._connection = saved_connection + await time_out_assert(60, sync_done) + test_runner.test_data[0].validate_plot_sync() diff --git a/tests/plot_sync/util.py b/tests/plot_sync/util.py new file mode 100644 index 000000000000..823616f50b40 --- /dev/null +++ b/tests/plot_sync/util.py @@ -0,0 +1,53 @@ +import time +from dataclasses import dataclass +from secrets import token_bytes +from typing import Optional + +from chia.harvester.harvester_api import Harvester +from chia.plot_sync.sender import Sender +from chia.protocols.harvester_protocol import PlotSyncIdentifier +from chia.server.start_service import Service +from chia.server.ws_connection import Message, NodeType +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.util.ints import uint64 +from tests.time_out_assert import time_out_assert + + +@dataclass +class WSChiaConnectionDummy: + connection_type: NodeType + peer_node_id: bytes32 + peer_host: str = "localhost" + peer_port: int = 0 + last_sent_message: Optional[Message] = None + + async def send_message(self, message: Message) -> None: + self.last_sent_message = message + + +def get_dummy_connection(node_type: NodeType, peer_id: Optional[bytes32] = None) -> WSChiaConnectionDummy: + return WSChiaConnectionDummy(node_type, bytes32(token_bytes(32)) if peer_id is None else peer_id) + + +def plot_sync_identifier(current_sync_id: uint64, message_id: uint64) -> PlotSyncIdentifier: + return PlotSyncIdentifier(uint64(int(time.time())), current_sync_id, message_id) + + +async def start_harvester_service(harvester_service: Service) -> Harvester: + # Set the `last_refresh_time` of the plot manager to avoid initial plot loading + harvester: Harvester = harvester_service._node + harvester.plot_manager.last_refresh_time = time.time() + await harvester_service.start() + harvester.plot_manager.stop_refreshing() # type: ignore[no-untyped-call] # TODO, Add typing in PlotManager + + assert harvester.plot_sync_sender._sync_id == 0 + assert harvester.plot_sync_sender._next_message_id == 0 + assert harvester.plot_sync_sender._last_sync_id == 0 + assert harvester.plot_sync_sender._messages == [] + + def wait_for_farmer_connection(plot_sync_sender: Sender) -> bool: + return plot_sync_sender._connection is not None + + await time_out_assert(10, wait_for_farmer_connection, True, harvester.plot_sync_sender) + + return harvester diff --git a/tests/plotting/test_plot_manager.py b/tests/plotting/test_plot_manager.py index f2a0e843c255..fea50cc4e171 100644 --- a/tests/plotting/test_plot_manager.py +++ b/tests/plotting/test_plot_manager.py @@ -236,7 +236,7 @@ async def run_test_case( trigger=trigger_remove_plot, test_path=drop_path, expect_loaded=[], - expect_removed=[drop_path], + expect_removed=[], expect_processed=len(env.dir_1) + len(env.dir_2) + len(dir_duplicates), expect_duplicates=len(dir_duplicates), expected_directories=3, @@ -262,7 +262,7 @@ async def run_test_case( trigger=remove_plot_directory, test_path=dir_duplicates.path, expect_loaded=[], - expect_removed=dir_duplicates.path_list(), + expect_removed=[], expect_processed=len(env.dir_1) + len(env.dir_2), expect_duplicates=0, expected_directories=2, @@ -316,7 +316,7 @@ async def run_test_case( trigger=trigger_remove_plot, test_path=drop_path, expect_loaded=[], - expect_removed=[drop_path], + expect_removed=[], expect_processed=len(env.dir_1) + len(env.dir_2) + len(dir_duplicates), expect_duplicates=len(env.dir_1), expected_directories=3, @@ -357,6 +357,17 @@ async def run_test_case( ) +@pytest.mark.asyncio +async def test_initial_refresh_flag(test_plot_environment: TestEnvironment) -> None: + env: TestEnvironment = test_plot_environment + assert env.refresh_tester.plot_manager.initial_refresh() + for _ in range(2): + await env.refresh_tester.run(PlotRefreshResult()) + assert not env.refresh_tester.plot_manager.initial_refresh() + env.refresh_tester.plot_manager.reset() + assert env.refresh_tester.plot_manager.initial_refresh() + + @pytest.mark.asyncio async def test_invalid_plots(test_plot_environment): env: TestEnvironment = test_plot_environment diff --git a/tests/setup_nodes.py b/tests/setup_nodes.py index 4cfb701d9c14..03a97c386d67 100644 --- a/tests/setup_nodes.py +++ b/tests/setup_nodes.py @@ -1,12 +1,15 @@ import asyncio import logging from secrets import token_bytes -from typing import Dict, List +from typing import AsyncIterator, Dict, List, Tuple +from pathlib import Path from chia.consensus.constants import ConsensusConstants +from chia.cmds.init_funcs import init from chia.full_node.full_node_api import FullNodeAPI from chia.server.start_service import Service from chia.server.start_wallet import service_kwargs_for_wallet +from chia.util.config import load_config, save_config from chia.util.hash import std_hash from chia.util.ints import uint16, uint32 from chia.util.keychain import bytes_to_mnemonic @@ -294,7 +297,7 @@ async def setup_harvester_farmer(bt: BlockTools, consensus_constants: ConsensusC harvester_rpc_port = find_available_listen_port("harvester rpc") node_iters = [ setup_harvester( - bt, + bt.root_path, bt.config["self_hostname"], harvester_port, harvester_rpc_port, @@ -320,6 +323,62 @@ async def setup_harvester_farmer(bt: BlockTools, consensus_constants: ConsensusC await _teardown_nodes(node_iters) +async def setup_farmer_multi_harvester( + block_tools: BlockTools, + harvester_count: int, + temp_dir: Path, + consensus_constants: ConsensusConstants, +) -> AsyncIterator[Tuple[List[Service], Service]]: + farmer_port = find_available_listen_port("farmer") + farmer_rpc_port = find_available_listen_port("farmer rpc") + + node_iterators = [ + setup_farmer( + block_tools, block_tools.config["self_hostname"], farmer_port, farmer_rpc_port, consensus_constants + ) + ] + + for i in range(0, harvester_count): + root_path: Path = temp_dir / str(i) + init(None, root_path) + init(block_tools.root_path / "config" / "ssl" / "ca", root_path) + config = load_config(root_path, "config.yaml") + config["logging"]["log_stdout"] = True + config["selected_network"] = "testnet0" + config["harvester"]["selected_network"] = "testnet0" + harvester_port = find_available_listen_port("harvester") + harvester_rpc_port = find_available_listen_port("harvester rpc") + save_config(root_path, "config.yaml", config) + node_iterators.append( + setup_harvester( + root_path, + block_tools.config["self_hostname"], + harvester_port, + harvester_rpc_port, + farmer_port, + consensus_constants, + False, + ) + ) + + farmer_service = await node_iterators[0].__anext__() + harvester_services = [] + for node in node_iterators[1:]: + harvester_service = await node.__anext__() + harvester_services.append(harvester_service) + + yield harvester_services, farmer_service + + for harvester_service in harvester_services: + harvester_service.stop() + await harvester_service.wait_closed() + + farmer_service.stop() + await farmer_service.wait_closed() + + await _teardown_nodes(node_iterators) + + async def setup_full_system( consensus_constants: ConsensusConstants, shared_b_tools: BlockTools, @@ -353,7 +412,7 @@ async def setup_full_system( node_iters = [ setup_introducer(shared_b_tools, introducer_port), setup_harvester( - shared_b_tools, + shared_b_tools.root_path, shared_b_tools.config["self_hostname"], harvester_port, harvester_rpc_port, diff --git a/tests/setup_services.py b/tests/setup_services.py index 848506fe7fed..27b6b42410dd 100644 --- a/tests/setup_services.py +++ b/tests/setup_services.py @@ -2,6 +2,7 @@ import logging import signal import sqlite3 +from pathlib import Path from secrets import token_bytes from typing import AsyncGenerator, Optional @@ -16,8 +17,8 @@ from chia.server.start_wallet import service_kwargs_for_wallet from chia.simulator.start_simulator import service_kwargs_for_full_node_simulator from chia.timelord.timelord_launcher import kill_processes, spawn_process -from chia.types.peer_info import PeerInfo from chia.util.bech32m import encode_puzzle_hash +from chia.util.config import load_config, save_config from chia.util.ints import uint16 from chia.util.keychain import bytes_to_mnemonic from tests.block_tools import BlockTools @@ -184,7 +185,7 @@ async def setup_wallet_node( async def setup_harvester( - b_tools: BlockTools, + root_path: Path, self_hostname: str, port, rpc_port, @@ -192,15 +193,14 @@ async def setup_harvester( consensus_constants: ConsensusConstants, start_service: bool = True, ): - - config = b_tools.config["harvester"] - config["port"] = port - config["rpc_port"] = rpc_port - kwargs = service_kwargs_for_harvester(b_tools.root_path, config, consensus_constants) + config = load_config(root_path, "config.yaml") + config["harvester"]["port"] = port + config["harvester"]["rpc_port"] = rpc_port + config["harvester"]["farmer_peer"]["host"] = self_hostname + config["harvester"]["farmer_peer"]["port"] = farmer_port + save_config(root_path, "config.yaml", config) + kwargs = service_kwargs_for_harvester(root_path, config["harvester"], consensus_constants) kwargs.update( - server_listen_ports=[port], - advertised_port=port, - connect_peers=[PeerInfo(self_hostname, farmer_port)], parse_cli_args=False, connect_to_daemon=False, service_name_prefix="test_", From c7a89fc425f42f610a206160239279d96b935602 Mon Sep 17 00:00:00 2001 From: Chris Marslender Date: Thu, 7 Apr 2022 19:43:14 -0500 Subject: [PATCH 343/378] Add wallentx as additional assignee on mozilla CA update PRs (#11089) --- .github/workflows/mozilla-ca-cert.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/mozilla-ca-cert.yml b/.github/workflows/mozilla-ca-cert.yml index a637f430e1bd..c616df40f21f 100644 --- a/.github/workflows/mozilla-ca-cert.yml +++ b/.github/workflows/mozilla-ca-cert.yml @@ -28,5 +28,6 @@ jobs: commit-message: "adding ca updates" delete-branch: true reviewers: "wjblanke,emlowe" + assignees: "wallentx" title: "CA Cert updates" token: "${{ secrets.GITHUB_TOKEN }}" From 02a880fbac6f0c304b84b6f485d10af2251f7dbc Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 7 Apr 2022 23:43:27 -0400 Subject: [PATCH 344/378] rebuild workflows (#11092) --- .../workflows/build-test-macos-plot_sync.yml | 24 ++++++++++++++----- .../workflows/build-test-ubuntu-plot_sync.yml | 24 ++++++++++++++----- 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build-test-macos-plot_sync.yml b/.github/workflows/build-test-macos-plot_sync.yml index d18e76cf7d20..606b7d71a7d3 100644 --- a/.github/workflows/build-test-macos-plot_sync.yml +++ b/.github/workflows/build-test-macos-plot_sync.yml @@ -65,13 +65,25 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 + - name: Daily (POC) cache key invalidation for test blocks and plots + id: today-date + run: date +%F > today.txt + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: ${{ hashFiles('today.txt') }} + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-plot_sync.yml b/.github/workflows/build-test-ubuntu-plot_sync.yml index 886573b5743d..54fa8b19e6e5 100644 --- a/.github/workflows/build-test-ubuntu-plot_sync.yml +++ b/.github/workflows/build-test-ubuntu-plot_sync.yml @@ -65,13 +65,25 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Checkout test blocks and plots - uses: actions/checkout@v3 + - name: Daily (POC) cache key invalidation for test blocks and plots + id: today-date + run: date +%F > today.txt + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots with: - repository: 'Chia-Network/test-cache' - path: '.chia' - ref: '0.28.0' - fetch-depth: 1 + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: ${{ hashFiles('today.txt') }} + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia - name: Run install script env: From 21fb6f260e59763295eb1eda747a17dc48744e2d Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Fri, 8 Apr 2022 18:37:10 +0200 Subject: [PATCH 345/378] transition to using chia_rs module (#11094) --- chia/full_node/mempool_check_conditions.py | 6 +++--- chia/types/blockchain_format/program.py | 6 +++--- chia/types/spend_bundle_conditions.py | 2 +- chia/util/full_block_utils.py | 2 +- setup.py | 2 +- tests/wallet/test_singleton.py | 4 ++-- tools/analyze-chain.py | 6 +++--- tools/run_block.py | 2 +- 8 files changed, 15 insertions(+), 15 deletions(-) diff --git a/chia/full_node/mempool_check_conditions.py b/chia/full_node/mempool_check_conditions.py index 10c44cd1e329..1457a29f4560 100644 --- a/chia/full_node/mempool_check_conditions.py +++ b/chia/full_node/mempool_check_conditions.py @@ -1,7 +1,6 @@ import logging - from typing import Dict, Optional -from clvm_rs import MEMPOOL_MODE, COND_CANON_INTS, NO_NEG_DIV +from chia_rs import MEMPOOL_MODE, COND_CANON_INTS, NO_NEG_DIV, STRICT_ARGS_COUNT from chia.consensus.default_constants import DEFAULT_CONSTANTS from chia.consensus.cost_calculator import NPCResult @@ -43,7 +42,8 @@ def get_name_puzzle_conditions( assert (MEMPOOL_MODE & NO_NEG_DIV) != 0 if mempool_mode: - flags = MEMPOOL_MODE + # Don't apply the strict args count rule yet + flags = MEMPOOL_MODE & (~STRICT_ARGS_COUNT) elif unwrap(height) >= DEFAULT_CONSTANTS.SOFT_FORK_HEIGHT: # conditions must use integers in canonical encoding (i.e. no redundant # leading zeros) diff --git a/chia/types/blockchain_format/program.py b/chia/types/blockchain_format/program.py index 36278ef7ec4f..096f72d25bdf 100644 --- a/chia/types/blockchain_format/program.py +++ b/chia/types/blockchain_format/program.py @@ -5,7 +5,7 @@ from clvm.casts import int_from_bytes from clvm.EvalError import EvalError from clvm.serialize import sexp_from_stream, sexp_to_stream -from clvm_rs import MEMPOOL_MODE, run_chia_program, serialized_length, run_generator2 +from chia_rs import MEMPOOL_MODE, run_chia_program, serialized_length, run_generator from clvm_tools.curry import curry, uncurry from chia.types.blockchain_format.sized_bytes import bytes32 @@ -222,7 +222,7 @@ def run_mempool_with_cost(self, max_cost: int, *args) -> Tuple[int, Program]: def run_with_cost(self, max_cost: int, *args) -> Tuple[int, Program]: return self._run(max_cost, 0, *args) - # returns an optional error code and an optional SpendBundleConditions + # returns an optional error code and an optional SpendBundleConditions (from chia_rs) # exactly one of those will hold a value def run_as_generator( self, max_cost: int, flags: int, *args @@ -238,7 +238,7 @@ def run_as_generator( else: serialized_args += _serialize(args[0]) - err, conds = run_generator2( + err, conds = run_generator( self._buf, serialized_args, max_cost, diff --git a/chia/types/spend_bundle_conditions.py b/chia/types/spend_bundle_conditions.py index 0c59fb732eaf..3bae9b34d239 100644 --- a/chia/types/spend_bundle_conditions.py +++ b/chia/types/spend_bundle_conditions.py @@ -7,7 +7,7 @@ # the Spend and SpendBundleConditions classes are mirrors of native types, returned by -# run_generator2 +# run_generator @dataclass(frozen=True) @streamable class Spend(Streamable): diff --git a/chia/util/full_block_utils.py b/chia/util/full_block_utils.py index bc41e03f8f2a..a1f076b9ce93 100644 --- a/chia/util/full_block_utils.py +++ b/chia/util/full_block_utils.py @@ -1,7 +1,7 @@ from typing import Callable, Optional from blspy import G1Element, G2Element -from clvm_rs import serialized_length +from chia_rs import serialized_length from chia.types.blockchain_format.program import SerializedProgram diff --git a/setup.py b/setup.py index 9c6e98238dc1..2ac24927f492 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ "chiapos==1.0.9", # proof of space "clvm==0.9.7", "clvm_tools==0.4.4", # Currying, Program.to, other conveniences - "clvm_rs==0.1.19", + "chia_rs==0.1.1", "clvm-tools-rs==0.1.7", # Rust implementation of clvm_tools "aiohttp==3.7.4", # HTTP server for full node rpc "aiosqlite==0.17.0", # asyncio wrapper for sqlite, to store blocks diff --git a/tests/wallet/test_singleton.py b/tests/wallet/test_singleton.py index bf3eba6f71a1..8f806e71c7a6 100644 --- a/tests/wallet/test_singleton.py +++ b/tests/wallet/test_singleton.py @@ -51,7 +51,7 @@ def test_only_odd_coins(): try: cost, result = SINGLETON_MOD.run_with_cost(INFINITE_COST, solution) except Exception as e: - assert e.args == ("clvm raise",) + assert e.args == ("clvm raise", "80") else: assert False @@ -84,7 +84,7 @@ def test_only_one_odd_coin_created(): try: cost, result = SINGLETON_MOD.run_with_cost(INFINITE_COST, solution) except Exception as e: - assert e.args == ("clvm raise",) + assert e.args == ("clvm raise", "80") else: assert False solution = Program.to( diff --git a/tools/analyze-chain.py b/tools/analyze-chain.py index 6d8484d51e0f..ccf2de828779 100755 --- a/tools/analyze-chain.py +++ b/tools/analyze-chain.py @@ -10,7 +10,7 @@ from time import time -from clvm_rs import run_generator2, MEMPOOL_MODE +from chia_rs import run_generator, MEMPOOL_MODE from chia.types.full_block import FullBlock from chia.types.blockchain_format.program import Program @@ -21,7 +21,7 @@ GENERATOR_ROM = bytes(get_generator()) -# returns an optional error code and an optional PySpendBundleConditions (from clvm_rs) +# returns an optional error code and an optional PySpendBundleConditions (from chia_rs) # exactly one of those will hold a value and the number of seconds it took to # run def run_gen(env_data: bytes, block_program_args: bytes, flags: uint32): @@ -36,7 +36,7 @@ def run_gen(env_data: bytes, block_program_args: bytes, flags: uint32): try: start_time = time() - err, result = run_generator2( + err, result = run_generator( GENERATOR_ROM, env_data, max_cost, diff --git a/tools/run_block.py b/tools/run_block.py index c8fe416adf5c..6588b26e2c40 100644 --- a/tools/run_block.py +++ b/tools/run_block.py @@ -42,7 +42,7 @@ import click -from clvm_rs import COND_CANON_INTS, NO_NEG_DIV +from chia_rs import COND_CANON_INTS, NO_NEG_DIV from chia.consensus.constants import ConsensusConstants from chia.consensus.default_constants import DEFAULT_CONSTANTS From 7cd23b007738999a33d68086288c4ed0b5020654 Mon Sep 17 00:00:00 2001 From: Adam Kelly <338792+aqk@users.noreply.github.com> Date: Fri, 8 Apr 2022 09:57:08 -0700 Subject: [PATCH 346/378] Fix the case of claiming a large number of coins (#11038) * Fix the case of claiming a large number of coins with a fee from a pool wallet * Revert change to unrelated test * Set PoolWallet.DEFAULT_MAX_CLAIM_SPENDS to 300 * A few review improvements --- chia/pools/pool_wallet.py | 31 ++++++-- chia/rpc/wallet_rpc_api.py | 3 +- chia/rpc/wallet_rpc_client.py | 8 +- chia/wallet/wallet_state_manager.py | 4 +- tests/pools/test_pool_rpc.py | 109 +++++++++++++++++++++++++--- 5 files changed, 133 insertions(+), 22 deletions(-) diff --git a/chia/pools/pool_wallet.py b/chia/pools/pool_wallet.py index 341cb758d9b1..b41a2585cb06 100644 --- a/chia/pools/pool_wallet.py +++ b/chia/pools/pool_wallet.py @@ -61,6 +61,7 @@ class PoolWallet: MINIMUM_INITIAL_BALANCE = 1 MINIMUM_RELATIVE_LOCK_HEIGHT = 5 MAXIMUM_RELATIVE_LOCK_HEIGHT = 1000 + DEFAULT_MAX_CLAIM_SPENDS = 100 wallet_state_manager: Any log: logging.Logger @@ -326,6 +327,7 @@ async def create( block_spends: List[CoinSpend], block_height: uint32, in_transaction: bool, + *, name: str = None, ): """ @@ -778,13 +780,21 @@ async def self_pool(self, fee: uint64) -> Tuple[uint64, TransactionRecord, Optio travel_tx, fee_tx = await self.generate_travel_transactions(fee) return total_fee, travel_tx, fee_tx - async def claim_pool_rewards(self, fee: uint64) -> Tuple[TransactionRecord, Optional[TransactionRecord]]: + async def claim_pool_rewards( + self, fee: uint64, max_spends_in_tx: Optional[int] + ) -> Tuple[TransactionRecord, Optional[TransactionRecord]]: # Search for p2_puzzle_hash coins, and spend them with the singleton if await self.have_unconfirmed_transaction(): raise ValueError( "Cannot claim due to unconfirmed transaction. If this is stuck, delete the unconfirmed transaction." ) + if max_spends_in_tx is None: + max_spends_in_tx = self.DEFAULT_MAX_CLAIM_SPENDS + elif max_spends_in_tx <= 0: + self.log.info(f"Bad max_spends_in_tx value of {max_spends_in_tx}. Set to {self.DEFAULT_MAX_CLAIM_SPENDS}.") + max_spends_in_tx = self.DEFAULT_MAX_CLAIM_SPENDS + unspent_coin_records: List[CoinRecord] = list( await self.wallet_state_manager.coin_store.get_unspent_coins_for_wallet(self.wallet_id) ) @@ -807,13 +817,20 @@ async def claim_pool_rewards(self, fee: uint64) -> Tuple[TransactionRecord, Opti all_spends: List[CoinSpend] = [] total_amount = 0 - current_coin_record = None + # The coins being claimed are gathered into the `SpendBundle`, :absorb_spend: + # We use an announcement in the fee spend to ensure that the claim spend is spent in the same block as the fee + # We only need to do this for one of the coins, because each `SpendBundle` can only be spent as a unit + + first_coin_record = None for coin_record in unspent_coin_records: if coin_record.coin not in coin_to_height_farmed: continue - current_coin_record = coin_record - if len(all_spends) >= 100: - # Limit the total number of spends, so it fits into the block + if first_coin_record is None: + first_coin_record = coin_record + if len(all_spends) >= max_spends_in_tx: + # Limit the total number of spends, so the SpendBundle fits into the block + self.log.info(f"pool wallet truncating absorb to {max_spends_in_tx} spends to fit into block") + print(f"pool wallet truncating absorb to {max_spends_in_tx} spends to fit into block") break absorb_spend: List[CoinSpend] = create_absorb_spend( last_solution, @@ -830,7 +847,7 @@ async def claim_pool_rewards(self, fee: uint64) -> Tuple[TransactionRecord, Opti self.log.info( f"Farmer coin: {coin_record.coin} {coin_record.coin.name()} {coin_to_height_farmed[coin_record.coin]}" ) - if len(all_spends) == 0 or current_coin_record is None: + if len(all_spends) == 0 or first_coin_record is None: raise ValueError("Nothing to claim, no unspent coinbase rewards") claim_spend: SpendBundle = SpendBundle(all_spends, G2Element()) @@ -840,7 +857,7 @@ async def claim_pool_rewards(self, fee: uint64) -> Tuple[TransactionRecord, Opti fee_tx = None if fee > 0: - absorb_announce = Announcement(current_coin_record.coin.name(), b"$") + absorb_announce = Announcement(first_coin_record.coin.name(), b"$") fee_tx = await self.generate_fee_transaction(fee, coin_announcements=[absorb_announce]) full_spend = SpendBundle.aggregate([fee_tx.spend_bundle, claim_spend]) diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 3f43af809564..d4e7820411c3 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -1408,13 +1408,14 @@ async def pw_absorb_rewards(self, request) -> Dict: if await self.service.wallet_state_manager.synced() is False: raise ValueError("Wallet needs to be fully synced before collecting rewards") fee = uint64(request.get("fee", 0)) + max_spends_in_tx = request.get("max_spends_in_tx", None) wallet_id = uint32(request["wallet_id"]) wallet: PoolWallet = self.service.wallet_state_manager.wallets[wallet_id] if wallet.type() != uint8(WalletType.POOLING_WALLET): raise ValueError(f"Wallet with wallet id: {wallet_id} is not a plotNFT wallet.") async with self.service.wallet_state_manager.lock: - transaction, fee_tx = await wallet.claim_pool_rewards(fee) + transaction, fee_tx = await wallet.claim_pool_rewards(fee, max_spends_in_tx) state: PoolWalletInfo = await wallet.get_current_state() return {"state": state.to_json_dict(), "transaction": transaction, "fee_transaction": fee_tx} diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index 82058176ba95..135fb9f37ef0 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -327,8 +327,12 @@ async def pw_join_pool( reply = parse_result_transactions(reply) return reply - async def pw_absorb_rewards(self, wallet_id: str, fee: uint64 = uint64(0)) -> Dict: - reply = await self.fetch("pw_absorb_rewards", {"wallet_id": wallet_id, "fee": fee}) + async def pw_absorb_rewards( + self, wallet_id: str, fee: uint64 = uint64(0), max_spends_in_tx: Optional[int] = None + ) -> Dict: + reply = await self.fetch( + "pw_absorb_rewards", {"wallet_id": wallet_id, "fee": fee, "max_spends_in_tx": max_spends_in_tx} + ) reply["state"] = PoolWalletInfo.from_json_dict(reply["state"]) reply = parse_result_transactions(reply) return reply diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index e7044beec904..01282e32932c 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -860,8 +860,8 @@ async def new_coin_state( child.coin.name(), [launcher_spend], child.spent_height, - True, - "pool_wallet", + in_transaction=True, + name="pool_wallet", ) launcher_spend_additions = launcher_spend.additions() assert len(launcher_spend_additions) == 1 diff --git a/tests/pools/test_pool_rpc.py b/tests/pools/test_pool_rpc.py index 504431f5e979..de66f42df212 100644 --- a/tests/pools/test_pool_rpc.py +++ b/tests/pools/test_pool_rpc.py @@ -4,13 +4,14 @@ from dataclasses import dataclass from pathlib import Path from shutil import rmtree -from typing import Any, Optional, List, Dict +from typing import Any, Optional, List, Dict, Tuple, AsyncGenerator import pytest import pytest_asyncio from blspy import G1Element from chia.consensus.block_rewards import calculate_base_farmer_reward, calculate_pool_reward +from chia.full_node.full_node_api import FullNodeAPI from chia.pools.pool_puzzles import SINGLETON_LAUNCHER_HASH from chia.pools.pool_wallet_info import PoolWalletInfo, PoolSingletonState from chia.protocols import full_node_protocol @@ -38,6 +39,7 @@ # TODO: Compare deducted fees in all tests against reported total_fee log = logging.getLogger(__name__) FEE_AMOUNT = 2000000000000 +MAX_WAIT_SECS = 20 # A high value for WAIT_SECS is useful when paused in the debugger def get_pool_plot_dir(): @@ -90,7 +92,7 @@ async def wallet_is_synced(wallet_node: WalletNode, full_node_api): @pytest_asyncio.fixture(scope="function") -async def one_wallet_node_and_rpc(bt, self_hostname): +async def one_wallet_node_and_rpc(bt, self_hostname) -> AsyncGenerator[Tuple[WalletRpcClient, Any, FullNodeAPI], None]: rmtree(get_pool_plot_dir(), ignore_errors=True) async for nodes in setup_simulators_and_wallets(1, 1, {}): full_nodes, wallets = nodes @@ -104,7 +106,7 @@ async def one_wallet_node_and_rpc(bt, self_hostname): api_user = WalletRpcApi(wallet_node_0) config = bt.config daemon_port = config["daemon_port"] - test_rpc_port = find_available_listen_port("rpc_port") + test_rpc_port = uint16(find_available_listen_port("rpc_port")) rpc_cleanup = await start_rpc_server( api_user, @@ -510,7 +512,7 @@ async def test_absorb_self(self, one_wallet_node_and_rpc, trusted_and_fee, bt, s absorb_tx1.spend_bundle.debug() await time_out_assert( - 10, + MAX_WAIT_SECS, full_node_api.full_node.mempool_manager.get_spendbundle, absorb_tx1.spend_bundle, absorb_tx1.name, @@ -545,6 +547,95 @@ async def test_absorb_self(self, one_wallet_node_and_rpc, trusted_and_fee, bt, s assert (250000000000 + fee) in [tx.additions[0].amount for tx in tx1] # await time_out_assert(10, wallet_0.get_confirmed_balance, total_block_rewards) + @pytest.mark.asyncio + @pytest.mark.parametrize("trusted_and_fee", [(True, FEE_AMOUNT * 2)]) + async def test_absorb_self_multiple_coins(self, one_wallet_node_and_rpc, trusted_and_fee, bt, self_hostname): + trusted, fee = trusted_and_fee + client, wallet_node_0, full_node_api = one_wallet_node_and_rpc + if trusted: + wallet_node_0.config["trusted_peers"] = { + full_node_api.full_node.server.node_id.hex(): full_node_api.full_node.server.node_id.hex() + } + else: + wallet_node_0.config["trusted_peers"] = {} + + await wallet_node_0.server.start_client( + PeerInfo(self_hostname, uint16(full_node_api.full_node.server._port)), None + ) + wallet_0 = wallet_node_0.wallet_state_manager.main_wallet + total_block_rewards = await get_total_block_rewards(PREFARMED_BLOCKS) + await time_out_assert(10, wallet_0.get_confirmed_balance, total_block_rewards) + await time_out_assert(10, wallet_node_0.wallet_state_manager.blockchain.get_peak_height, PREFARMED_BLOCKS) + + our_ph = await wallet_0.get_new_puzzlehash() + assert len(await client.get_wallets(WalletType.POOLING_WALLET)) == 0 + + await time_out_assert(10, wallet_is_synced, True, wallet_node_0, full_node_api) + creation_tx: TransactionRecord = await client.create_new_pool_wallet( + our_ph, "", 0, f"{self_hostname}:5000", "new", "SELF_POOLING", fee + ) + + await time_out_assert( + 10, + full_node_api.full_node.mempool_manager.get_spendbundle, + creation_tx.spend_bundle, + creation_tx.name, + ) + await farm_blocks(full_node_api, our_ph, 1) + # await asyncio.sleep(5) + + async def pool_wallet_created(): + try: + status: PoolWalletInfo = (await client.pw_status(2))[0] + return status.current.state == PoolSingletonState.SELF_POOLING.value + except ValueError: + return False + + await time_out_assert(10, pool_wallet_created) + + status: PoolWalletInfo = (await client.pw_status(2))[0] + async with TemporaryPoolPlot(bt, status.p2_singleton_puzzle_hash) as pool_plot: + all_blocks = await full_node_api.get_all_full_blocks() + blocks = bt.get_consecutive_blocks( + 3, + block_list_input=all_blocks, + force_plot_id=pool_plot.plot_id, + farmer_reward_puzzle_hash=our_ph, + guarantee_transaction_block=True, + ) + + await full_node_api.full_node.respond_block(full_node_protocol.RespondBlock(blocks[-3])) + await full_node_api.full_node.respond_block(full_node_protocol.RespondBlock(blocks[-2])) + await full_node_api.full_node.respond_block(full_node_protocol.RespondBlock(blocks[-1])) + await asyncio.sleep(2) + + bal = await client.get_wallet_balance(2) + assert bal["confirmed_wallet_balance"] == 2 * 1750000000000 + + await farm_blocks(full_node_api, our_ph, 6) + await asyncio.sleep(6) + + # Claim + absorb_tx: TransactionRecord = (await client.pw_absorb_rewards(2, fee, 1))["transaction"] + await time_out_assert( + 5, + full_node_api.full_node.mempool_manager.get_spendbundle, + absorb_tx.spend_bundle, + absorb_tx.name, + ) + await farm_blocks(full_node_api, our_ph, 2) + await asyncio.sleep(2) + new_status: PoolWalletInfo = (await client.pw_status(2))[0] + assert status.current == new_status.current + assert status.tip_singleton_coin_id != new_status.tip_singleton_coin_id + main_bal = await client.get_wallet_balance(1) + pool_bal = await client.get_wallet_balance(2) + assert pool_bal["confirmed_wallet_balance"] == 2 * 1750000000000 + assert main_bal["confirmed_wallet_balance"] == 26499999999999 + print(pool_bal) + print("---") + print(main_bal) + @pytest.mark.asyncio @pytest.mark.parametrize("trusted_and_fee", [(True, FEE_AMOUNT), (False, 0)]) async def test_absorb_pooling(self, one_wallet_node_and_rpc, trusted_and_fee, bt, self_hostname): @@ -843,8 +934,6 @@ async def test_leave_pool(self, setup, trusted_and_fee, self_hostname): PeerInfo(self_hostname, uint16(full_node_api.full_node.server._port)), None ) - WAIT_SECS = 200 - try: assert len(await client.get_wallets(WalletType.POOLING_WALLET)) == 0 @@ -852,7 +941,7 @@ async def have_chia(): await farm_blocks(full_node_api, our_ph, 1) return (await wallets[0].get_confirmed_balance()) > 0 - await time_out_assert(timeout=WAIT_SECS, function=have_chia) + await time_out_assert(timeout=MAX_WAIT_SECS, function=have_chia) await time_out_assert(10, wallet_is_synced, True, wallet_nodes[0], full_node_api) creation_tx: TransactionRecord = await client.create_new_pool_wallet( @@ -909,7 +998,7 @@ async def status_is_farming_to_pool(): pw_status: PoolWalletInfo = (await client.pw_status(wallet_id))[0] return pw_status.current.state == PoolSingletonState.FARMING_TO_POOL.value - await time_out_assert(timeout=WAIT_SECS, function=status_is_farming_to_pool) + await time_out_assert(timeout=MAX_WAIT_SECS, function=status_is_farming_to_pool) await time_out_assert(10, wallet_is_synced, True, wallet_nodes[0], full_node_api) @@ -924,7 +1013,7 @@ async def status_is_leaving(): pw_status: PoolWalletInfo = (await client.pw_status(wallet_id))[0] return pw_status.current.state == PoolSingletonState.LEAVING_POOL.value - await time_out_assert(timeout=WAIT_SECS, function=status_is_leaving) + await time_out_assert(timeout=MAX_WAIT_SECS, function=status_is_leaving) async def status_is_self_pooling(): # Farm enough blocks to wait for relative_lock_height @@ -932,7 +1021,7 @@ async def status_is_self_pooling(): pw_status: PoolWalletInfo = (await client.pw_status(wallet_id))[0] return pw_status.current.state == PoolSingletonState.SELF_POOLING.value - await time_out_assert(timeout=WAIT_SECS, function=status_is_self_pooling) + await time_out_assert(timeout=MAX_WAIT_SECS, function=status_is_self_pooling) assert len(await wallets[0].wallet_state_manager.tx_store.get_unconfirmed_for_wallet(2)) == 0 finally: From 2f9e718073d88758dcd7a5d9d15f97469045fdf5 Mon Sep 17 00:00:00 2001 From: Mariano Sorgente <3069354+mariano54@users.noreply.github.com> Date: Fri, 8 Apr 2022 12:57:59 -0400 Subject: [PATCH 347/378] Ms.fast test blockchain (#11051) * more work on test blockchain * Optimize test_blockchain.py * Fix weight proof bug * Rename variable * first rc_sub_slot hash bug * New plots * try with a new ID * Run without cache * Address test blocks and plots preparation. * Update constant in test_compact_protocol(). * Update this constant too. * Revert accidental altering of the gui submodule in ae7e3295f280a591e76c4dffdea75fb74ea5de6f. * Fix benchmark test * Revert mozilla-ca change * Rebase on main Co-authored-by: almog Co-authored-by: Amine Khaldi --- .github/workflows/benchmarks.yml | 2 +- .../workflows/build-test-macos-blockchain.yml | 10 +-- .../build-test-macos-core-daemon.yml | 10 +-- ...ld-test-macos-core-full_node-full_sync.yml | 10 +-- ...build-test-macos-core-full_node-stores.yml | 10 +-- .../build-test-macos-core-full_node.yml | 10 +-- .../build-test-macos-core-server.yml | 10 +-- .../workflows/build-test-macos-core-ssl.yml | 10 +-- .../workflows/build-test-macos-core-util.yml | 10 +-- .github/workflows/build-test-macos-core.yml | 10 +-- .../build-test-macos-farmer_harvester.yml | 10 +-- .../workflows/build-test-macos-plot_sync.yml | 10 +-- .../workflows/build-test-macos-plotting.yml | 10 +-- .github/workflows/build-test-macos-pools.yml | 10 +-- .../workflows/build-test-macos-simulation.yml | 10 +-- .../build-test-macos-wallet-cat_wallet.yml | 10 +-- .../workflows/build-test-macos-wallet-rpc.yml | 10 +-- .../build-test-macos-wallet-simple_sync.yml | 10 +-- .../build-test-macos-wallet-sync.yml | 10 +-- .github/workflows/build-test-macos-wallet.yml | 10 +-- .../build-test-macos-weight_proof.yml | 10 +-- .../build-test-ubuntu-blockchain.yml | 10 +-- .../build-test-ubuntu-core-daemon.yml | 10 +-- ...d-test-ubuntu-core-full_node-full_sync.yml | 10 +-- ...uild-test-ubuntu-core-full_node-stores.yml | 10 +-- .../build-test-ubuntu-core-full_node.yml | 10 +-- .../build-test-ubuntu-core-server.yml | 10 +-- .../workflows/build-test-ubuntu-core-ssl.yml | 10 +-- .../workflows/build-test-ubuntu-core-util.yml | 10 +-- .github/workflows/build-test-ubuntu-core.yml | 10 +-- .../build-test-ubuntu-farmer_harvester.yml | 10 +-- .../workflows/build-test-ubuntu-plot_sync.yml | 10 +-- .../workflows/build-test-ubuntu-plotting.yml | 10 +-- .github/workflows/build-test-ubuntu-pools.yml | 10 +-- .../build-test-ubuntu-simulation.yml | 10 +-- .../build-test-ubuntu-wallet-cat_wallet.yml | 10 +-- .../build-test-ubuntu-wallet-rpc.yml | 10 +-- .../build-test-ubuntu-wallet-simple_sync.yml | 10 +-- .../build-test-ubuntu-wallet-sync.yml | 10 +-- .../workflows/build-test-ubuntu-wallet.yml | 10 +-- .../build-test-ubuntu-weight_proof.yml | 10 +-- chia/full_node/weight_proof.py | 6 +- chia/plotting/create_plots.py | 6 +- tests/block_tools.py | 4 +- tests/blockchain/test_blockchain.py | 72 +++++++++++++------ tests/conftest.py | 56 +++++++++++++-- .../full_node/full_sync/test_full_sync.py | 6 +- tests/core/full_node/test_full_node.py | 4 +- tests/core/full_node/test_performance.py | 7 +- .../checkout-test-plots.include.yml | 10 +-- tests/util/blockchain.py | 14 +++- 51 files changed, 255 insertions(+), 332 deletions(-) diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index b7589f89c468..ac025730c125 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -60,7 +60,7 @@ jobs: with: repository: 'Chia-Network/test-cache' path: '.chia' - ref: '0.28.0' + ref: '0.29.0' fetch-depth: 1 - name: Run install script diff --git a/.github/workflows/build-test-macos-blockchain.yml b/.github/workflows/build-test-macos-blockchain.yml index 8fcb0b1c1003..a816459ff7b7 100644 --- a/.github/workflows/build-test-macos-blockchain.yml +++ b/.github/workflows/build-test-macos-blockchain.yml @@ -65,10 +65,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Daily (POC) cache key invalidation for test blocks and plots - id: today-date - run: date +%F > today.txt - - name: Cache test blocks and plots uses: actions/cache@v2 id: test-blocks-plots @@ -76,14 +72,14 @@ jobs: path: | ${{ github.workspace }}/.chia/blocks ${{ github.workspace }}/.chia/test-plots - key: ${{ hashFiles('today.txt') }} + key: 0.29.0 - name: Checkout test blocks and plots if: steps.test-blocks-plots.outputs.cache-hit != 'true' run: | - wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - mkdir ${{ github.workspace }}/.chia - mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-macos-core-daemon.yml b/.github/workflows/build-test-macos-core-daemon.yml index d017da0e3ee8..950f08481d80 100644 --- a/.github/workflows/build-test-macos-core-daemon.yml +++ b/.github/workflows/build-test-macos-core-daemon.yml @@ -65,10 +65,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Daily (POC) cache key invalidation for test blocks and plots - id: today-date - run: date +%F > today.txt - - name: Cache test blocks and plots uses: actions/cache@v2 id: test-blocks-plots @@ -76,14 +72,14 @@ jobs: path: | ${{ github.workspace }}/.chia/blocks ${{ github.workspace }}/.chia/test-plots - key: ${{ hashFiles('today.txt') }} + key: 0.29.0 - name: Checkout test blocks and plots if: steps.test-blocks-plots.outputs.cache-hit != 'true' run: | - wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - mkdir ${{ github.workspace }}/.chia - mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-macos-core-full_node-full_sync.yml b/.github/workflows/build-test-macos-core-full_node-full_sync.yml index 61825197ea0b..fd8e5da8d28d 100644 --- a/.github/workflows/build-test-macos-core-full_node-full_sync.yml +++ b/.github/workflows/build-test-macos-core-full_node-full_sync.yml @@ -65,10 +65,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Daily (POC) cache key invalidation for test blocks and plots - id: today-date - run: date +%F > today.txt - - name: Cache test blocks and plots uses: actions/cache@v2 id: test-blocks-plots @@ -76,14 +72,14 @@ jobs: path: | ${{ github.workspace }}/.chia/blocks ${{ github.workspace }}/.chia/test-plots - key: ${{ hashFiles('today.txt') }} + key: 0.29.0 - name: Checkout test blocks and plots if: steps.test-blocks-plots.outputs.cache-hit != 'true' run: | - wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - mkdir ${{ github.workspace }}/.chia - mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-macos-core-full_node-stores.yml b/.github/workflows/build-test-macos-core-full_node-stores.yml index b25f81ea349f..72a06b6ca0b7 100644 --- a/.github/workflows/build-test-macos-core-full_node-stores.yml +++ b/.github/workflows/build-test-macos-core-full_node-stores.yml @@ -65,10 +65,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Daily (POC) cache key invalidation for test blocks and plots - id: today-date - run: date +%F > today.txt - - name: Cache test blocks and plots uses: actions/cache@v2 id: test-blocks-plots @@ -76,14 +72,14 @@ jobs: path: | ${{ github.workspace }}/.chia/blocks ${{ github.workspace }}/.chia/test-plots - key: ${{ hashFiles('today.txt') }} + key: 0.29.0 - name: Checkout test blocks and plots if: steps.test-blocks-plots.outputs.cache-hit != 'true' run: | - wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - mkdir ${{ github.workspace }}/.chia - mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-macos-core-full_node.yml b/.github/workflows/build-test-macos-core-full_node.yml index 03b9cb834e1c..83d6cb01568c 100644 --- a/.github/workflows/build-test-macos-core-full_node.yml +++ b/.github/workflows/build-test-macos-core-full_node.yml @@ -65,10 +65,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Daily (POC) cache key invalidation for test blocks and plots - id: today-date - run: date +%F > today.txt - - name: Cache test blocks and plots uses: actions/cache@v2 id: test-blocks-plots @@ -76,14 +72,14 @@ jobs: path: | ${{ github.workspace }}/.chia/blocks ${{ github.workspace }}/.chia/test-plots - key: ${{ hashFiles('today.txt') }} + key: 0.29.0 - name: Checkout test blocks and plots if: steps.test-blocks-plots.outputs.cache-hit != 'true' run: | - wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - mkdir ${{ github.workspace }}/.chia - mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-macos-core-server.yml b/.github/workflows/build-test-macos-core-server.yml index 03a771e6b60e..e93f23b2f8b9 100644 --- a/.github/workflows/build-test-macos-core-server.yml +++ b/.github/workflows/build-test-macos-core-server.yml @@ -65,10 +65,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Daily (POC) cache key invalidation for test blocks and plots - id: today-date - run: date +%F > today.txt - - name: Cache test blocks and plots uses: actions/cache@v2 id: test-blocks-plots @@ -76,14 +72,14 @@ jobs: path: | ${{ github.workspace }}/.chia/blocks ${{ github.workspace }}/.chia/test-plots - key: ${{ hashFiles('today.txt') }} + key: 0.29.0 - name: Checkout test blocks and plots if: steps.test-blocks-plots.outputs.cache-hit != 'true' run: | - wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - mkdir ${{ github.workspace }}/.chia - mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-macos-core-ssl.yml b/.github/workflows/build-test-macos-core-ssl.yml index 51c348f8fe30..db9903634ac1 100644 --- a/.github/workflows/build-test-macos-core-ssl.yml +++ b/.github/workflows/build-test-macos-core-ssl.yml @@ -65,10 +65,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Daily (POC) cache key invalidation for test blocks and plots - id: today-date - run: date +%F > today.txt - - name: Cache test blocks and plots uses: actions/cache@v2 id: test-blocks-plots @@ -76,14 +72,14 @@ jobs: path: | ${{ github.workspace }}/.chia/blocks ${{ github.workspace }}/.chia/test-plots - key: ${{ hashFiles('today.txt') }} + key: 0.29.0 - name: Checkout test blocks and plots if: steps.test-blocks-plots.outputs.cache-hit != 'true' run: | - wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - mkdir ${{ github.workspace }}/.chia - mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-macos-core-util.yml b/.github/workflows/build-test-macos-core-util.yml index 95c8603deb17..0935d2d546b4 100644 --- a/.github/workflows/build-test-macos-core-util.yml +++ b/.github/workflows/build-test-macos-core-util.yml @@ -65,10 +65,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Daily (POC) cache key invalidation for test blocks and plots - id: today-date - run: date +%F > today.txt - - name: Cache test blocks and plots uses: actions/cache@v2 id: test-blocks-plots @@ -76,14 +72,14 @@ jobs: path: | ${{ github.workspace }}/.chia/blocks ${{ github.workspace }}/.chia/test-plots - key: ${{ hashFiles('today.txt') }} + key: 0.29.0 - name: Checkout test blocks and plots if: steps.test-blocks-plots.outputs.cache-hit != 'true' run: | - wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - mkdir ${{ github.workspace }}/.chia - mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-macos-core.yml b/.github/workflows/build-test-macos-core.yml index df94b8d09dd2..4fe7cab2494f 100644 --- a/.github/workflows/build-test-macos-core.yml +++ b/.github/workflows/build-test-macos-core.yml @@ -65,10 +65,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Daily (POC) cache key invalidation for test blocks and plots - id: today-date - run: date +%F > today.txt - - name: Cache test blocks and plots uses: actions/cache@v2 id: test-blocks-plots @@ -76,14 +72,14 @@ jobs: path: | ${{ github.workspace }}/.chia/blocks ${{ github.workspace }}/.chia/test-plots - key: ${{ hashFiles('today.txt') }} + key: 0.29.0 - name: Checkout test blocks and plots if: steps.test-blocks-plots.outputs.cache-hit != 'true' run: | - wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - mkdir ${{ github.workspace }}/.chia - mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-macos-farmer_harvester.yml b/.github/workflows/build-test-macos-farmer_harvester.yml index 3017a3579026..9c9f2b4736a2 100644 --- a/.github/workflows/build-test-macos-farmer_harvester.yml +++ b/.github/workflows/build-test-macos-farmer_harvester.yml @@ -65,10 +65,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Daily (POC) cache key invalidation for test blocks and plots - id: today-date - run: date +%F > today.txt - - name: Cache test blocks and plots uses: actions/cache@v2 id: test-blocks-plots @@ -76,14 +72,14 @@ jobs: path: | ${{ github.workspace }}/.chia/blocks ${{ github.workspace }}/.chia/test-plots - key: ${{ hashFiles('today.txt') }} + key: 0.29.0 - name: Checkout test blocks and plots if: steps.test-blocks-plots.outputs.cache-hit != 'true' run: | - wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - mkdir ${{ github.workspace }}/.chia - mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-macos-plot_sync.yml b/.github/workflows/build-test-macos-plot_sync.yml index 606b7d71a7d3..315c909bb476 100644 --- a/.github/workflows/build-test-macos-plot_sync.yml +++ b/.github/workflows/build-test-macos-plot_sync.yml @@ -65,10 +65,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Daily (POC) cache key invalidation for test blocks and plots - id: today-date - run: date +%F > today.txt - - name: Cache test blocks and plots uses: actions/cache@v2 id: test-blocks-plots @@ -76,14 +72,14 @@ jobs: path: | ${{ github.workspace }}/.chia/blocks ${{ github.workspace }}/.chia/test-plots - key: ${{ hashFiles('today.txt') }} + key: 0.29.0 - name: Checkout test blocks and plots if: steps.test-blocks-plots.outputs.cache-hit != 'true' run: | - wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - mkdir ${{ github.workspace }}/.chia - mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-macos-plotting.yml b/.github/workflows/build-test-macos-plotting.yml index 80a5c9ceba81..30d44782fd4b 100644 --- a/.github/workflows/build-test-macos-plotting.yml +++ b/.github/workflows/build-test-macos-plotting.yml @@ -65,10 +65,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Daily (POC) cache key invalidation for test blocks and plots - id: today-date - run: date +%F > today.txt - - name: Cache test blocks and plots uses: actions/cache@v2 id: test-blocks-plots @@ -76,14 +72,14 @@ jobs: path: | ${{ github.workspace }}/.chia/blocks ${{ github.workspace }}/.chia/test-plots - key: ${{ hashFiles('today.txt') }} + key: 0.29.0 - name: Checkout test blocks and plots if: steps.test-blocks-plots.outputs.cache-hit != 'true' run: | - wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - mkdir ${{ github.workspace }}/.chia - mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-macos-pools.yml b/.github/workflows/build-test-macos-pools.yml index 39969bddaf7c..9279455daaf5 100644 --- a/.github/workflows/build-test-macos-pools.yml +++ b/.github/workflows/build-test-macos-pools.yml @@ -65,10 +65,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Daily (POC) cache key invalidation for test blocks and plots - id: today-date - run: date +%F > today.txt - - name: Cache test blocks and plots uses: actions/cache@v2 id: test-blocks-plots @@ -76,14 +72,14 @@ jobs: path: | ${{ github.workspace }}/.chia/blocks ${{ github.workspace }}/.chia/test-plots - key: ${{ hashFiles('today.txt') }} + key: 0.29.0 - name: Checkout test blocks and plots if: steps.test-blocks-plots.outputs.cache-hit != 'true' run: | - wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - mkdir ${{ github.workspace }}/.chia - mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-macos-simulation.yml b/.github/workflows/build-test-macos-simulation.yml index 44a920ac3160..a7bd5c669573 100644 --- a/.github/workflows/build-test-macos-simulation.yml +++ b/.github/workflows/build-test-macos-simulation.yml @@ -65,10 +65,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Daily (POC) cache key invalidation for test blocks and plots - id: today-date - run: date +%F > today.txt - - name: Cache test blocks and plots uses: actions/cache@v2 id: test-blocks-plots @@ -76,14 +72,14 @@ jobs: path: | ${{ github.workspace }}/.chia/blocks ${{ github.workspace }}/.chia/test-plots - key: ${{ hashFiles('today.txt') }} + key: 0.29.0 - name: Checkout test blocks and plots if: steps.test-blocks-plots.outputs.cache-hit != 'true' run: | - wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - mkdir ${{ github.workspace }}/.chia - mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-macos-wallet-cat_wallet.yml b/.github/workflows/build-test-macos-wallet-cat_wallet.yml index d38bb5a5a532..70858e93b721 100644 --- a/.github/workflows/build-test-macos-wallet-cat_wallet.yml +++ b/.github/workflows/build-test-macos-wallet-cat_wallet.yml @@ -65,10 +65,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Daily (POC) cache key invalidation for test blocks and plots - id: today-date - run: date +%F > today.txt - - name: Cache test blocks and plots uses: actions/cache@v2 id: test-blocks-plots @@ -76,14 +72,14 @@ jobs: path: | ${{ github.workspace }}/.chia/blocks ${{ github.workspace }}/.chia/test-plots - key: ${{ hashFiles('today.txt') }} + key: 0.29.0 - name: Checkout test blocks and plots if: steps.test-blocks-plots.outputs.cache-hit != 'true' run: | - wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - mkdir ${{ github.workspace }}/.chia - mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-macos-wallet-rpc.yml b/.github/workflows/build-test-macos-wallet-rpc.yml index 674e246e6e37..68a95ea7a772 100644 --- a/.github/workflows/build-test-macos-wallet-rpc.yml +++ b/.github/workflows/build-test-macos-wallet-rpc.yml @@ -65,10 +65,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Daily (POC) cache key invalidation for test blocks and plots - id: today-date - run: date +%F > today.txt - - name: Cache test blocks and plots uses: actions/cache@v2 id: test-blocks-plots @@ -76,14 +72,14 @@ jobs: path: | ${{ github.workspace }}/.chia/blocks ${{ github.workspace }}/.chia/test-plots - key: ${{ hashFiles('today.txt') }} + key: 0.29.0 - name: Checkout test blocks and plots if: steps.test-blocks-plots.outputs.cache-hit != 'true' run: | - wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - mkdir ${{ github.workspace }}/.chia - mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-macos-wallet-simple_sync.yml b/.github/workflows/build-test-macos-wallet-simple_sync.yml index c80bd4d72bee..0a1a359b3621 100644 --- a/.github/workflows/build-test-macos-wallet-simple_sync.yml +++ b/.github/workflows/build-test-macos-wallet-simple_sync.yml @@ -65,10 +65,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Daily (POC) cache key invalidation for test blocks and plots - id: today-date - run: date +%F > today.txt - - name: Cache test blocks and plots uses: actions/cache@v2 id: test-blocks-plots @@ -76,14 +72,14 @@ jobs: path: | ${{ github.workspace }}/.chia/blocks ${{ github.workspace }}/.chia/test-plots - key: ${{ hashFiles('today.txt') }} + key: 0.29.0 - name: Checkout test blocks and plots if: steps.test-blocks-plots.outputs.cache-hit != 'true' run: | - wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - mkdir ${{ github.workspace }}/.chia - mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-macos-wallet-sync.yml b/.github/workflows/build-test-macos-wallet-sync.yml index bedeaa601659..c92ca8e21efe 100644 --- a/.github/workflows/build-test-macos-wallet-sync.yml +++ b/.github/workflows/build-test-macos-wallet-sync.yml @@ -65,10 +65,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Daily (POC) cache key invalidation for test blocks and plots - id: today-date - run: date +%F > today.txt - - name: Cache test blocks and plots uses: actions/cache@v2 id: test-blocks-plots @@ -76,14 +72,14 @@ jobs: path: | ${{ github.workspace }}/.chia/blocks ${{ github.workspace }}/.chia/test-plots - key: ${{ hashFiles('today.txt') }} + key: 0.29.0 - name: Checkout test blocks and plots if: steps.test-blocks-plots.outputs.cache-hit != 'true' run: | - wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - mkdir ${{ github.workspace }}/.chia - mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-macos-wallet.yml b/.github/workflows/build-test-macos-wallet.yml index 100585f786be..0a71248808d7 100644 --- a/.github/workflows/build-test-macos-wallet.yml +++ b/.github/workflows/build-test-macos-wallet.yml @@ -65,10 +65,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Daily (POC) cache key invalidation for test blocks and plots - id: today-date - run: date +%F > today.txt - - name: Cache test blocks and plots uses: actions/cache@v2 id: test-blocks-plots @@ -76,14 +72,14 @@ jobs: path: | ${{ github.workspace }}/.chia/blocks ${{ github.workspace }}/.chia/test-plots - key: ${{ hashFiles('today.txt') }} + key: 0.29.0 - name: Checkout test blocks and plots if: steps.test-blocks-plots.outputs.cache-hit != 'true' run: | - wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - mkdir ${{ github.workspace }}/.chia - mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-macos-weight_proof.yml b/.github/workflows/build-test-macos-weight_proof.yml index 0870d1f95492..9b737c31e4c3 100644 --- a/.github/workflows/build-test-macos-weight_proof.yml +++ b/.github/workflows/build-test-macos-weight_proof.yml @@ -65,10 +65,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Daily (POC) cache key invalidation for test blocks and plots - id: today-date - run: date +%F > today.txt - - name: Cache test blocks and plots uses: actions/cache@v2 id: test-blocks-plots @@ -76,14 +72,14 @@ jobs: path: | ${{ github.workspace }}/.chia/blocks ${{ github.workspace }}/.chia/test-plots - key: ${{ hashFiles('today.txt') }} + key: 0.29.0 - name: Checkout test blocks and plots if: steps.test-blocks-plots.outputs.cache-hit != 'true' run: | - wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - mkdir ${{ github.workspace }}/.chia - mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-blockchain.yml b/.github/workflows/build-test-ubuntu-blockchain.yml index e6a5aa872365..60dbb451694e 100644 --- a/.github/workflows/build-test-ubuntu-blockchain.yml +++ b/.github/workflows/build-test-ubuntu-blockchain.yml @@ -65,10 +65,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Daily (POC) cache key invalidation for test blocks and plots - id: today-date - run: date +%F > today.txt - - name: Cache test blocks and plots uses: actions/cache@v2 id: test-blocks-plots @@ -76,14 +72,14 @@ jobs: path: | ${{ github.workspace }}/.chia/blocks ${{ github.workspace }}/.chia/test-plots - key: ${{ hashFiles('today.txt') }} + key: 0.29.0 - name: Checkout test blocks and plots if: steps.test-blocks-plots.outputs.cache-hit != 'true' run: | - wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - mkdir ${{ github.workspace }}/.chia - mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-core-daemon.yml b/.github/workflows/build-test-ubuntu-core-daemon.yml index 19f60ca1e797..3611af06d244 100644 --- a/.github/workflows/build-test-ubuntu-core-daemon.yml +++ b/.github/workflows/build-test-ubuntu-core-daemon.yml @@ -65,10 +65,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Daily (POC) cache key invalidation for test blocks and plots - id: today-date - run: date +%F > today.txt - - name: Cache test blocks and plots uses: actions/cache@v2 id: test-blocks-plots @@ -76,14 +72,14 @@ jobs: path: | ${{ github.workspace }}/.chia/blocks ${{ github.workspace }}/.chia/test-plots - key: ${{ hashFiles('today.txt') }} + key: 0.29.0 - name: Checkout test blocks and plots if: steps.test-blocks-plots.outputs.cache-hit != 'true' run: | - wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - mkdir ${{ github.workspace }}/.chia - mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml b/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml index 546c3d21bd7a..a2295de7ebb7 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml @@ -65,10 +65,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Daily (POC) cache key invalidation for test blocks and plots - id: today-date - run: date +%F > today.txt - - name: Cache test blocks and plots uses: actions/cache@v2 id: test-blocks-plots @@ -76,14 +72,14 @@ jobs: path: | ${{ github.workspace }}/.chia/blocks ${{ github.workspace }}/.chia/test-plots - key: ${{ hashFiles('today.txt') }} + key: 0.29.0 - name: Checkout test blocks and plots if: steps.test-blocks-plots.outputs.cache-hit != 'true' run: | - wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - mkdir ${{ github.workspace }}/.chia - mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-core-full_node-stores.yml b/.github/workflows/build-test-ubuntu-core-full_node-stores.yml index be9cef49ade0..fbcf34703727 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node-stores.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node-stores.yml @@ -65,10 +65,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Daily (POC) cache key invalidation for test blocks and plots - id: today-date - run: date +%F > today.txt - - name: Cache test blocks and plots uses: actions/cache@v2 id: test-blocks-plots @@ -76,14 +72,14 @@ jobs: path: | ${{ github.workspace }}/.chia/blocks ${{ github.workspace }}/.chia/test-plots - key: ${{ hashFiles('today.txt') }} + key: 0.29.0 - name: Checkout test blocks and plots if: steps.test-blocks-plots.outputs.cache-hit != 'true' run: | - wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - mkdir ${{ github.workspace }}/.chia - mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-core-full_node.yml b/.github/workflows/build-test-ubuntu-core-full_node.yml index e59db07dedb7..97208996a1fa 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node.yml @@ -65,10 +65,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Daily (POC) cache key invalidation for test blocks and plots - id: today-date - run: date +%F > today.txt - - name: Cache test blocks and plots uses: actions/cache@v2 id: test-blocks-plots @@ -76,14 +72,14 @@ jobs: path: | ${{ github.workspace }}/.chia/blocks ${{ github.workspace }}/.chia/test-plots - key: ${{ hashFiles('today.txt') }} + key: 0.29.0 - name: Checkout test blocks and plots if: steps.test-blocks-plots.outputs.cache-hit != 'true' run: | - wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - mkdir ${{ github.workspace }}/.chia - mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-core-server.yml b/.github/workflows/build-test-ubuntu-core-server.yml index a770ae9a40df..c96f2e13f107 100644 --- a/.github/workflows/build-test-ubuntu-core-server.yml +++ b/.github/workflows/build-test-ubuntu-core-server.yml @@ -65,10 +65,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Daily (POC) cache key invalidation for test blocks and plots - id: today-date - run: date +%F > today.txt - - name: Cache test blocks and plots uses: actions/cache@v2 id: test-blocks-plots @@ -76,14 +72,14 @@ jobs: path: | ${{ github.workspace }}/.chia/blocks ${{ github.workspace }}/.chia/test-plots - key: ${{ hashFiles('today.txt') }} + key: 0.29.0 - name: Checkout test blocks and plots if: steps.test-blocks-plots.outputs.cache-hit != 'true' run: | - wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - mkdir ${{ github.workspace }}/.chia - mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-core-ssl.yml b/.github/workflows/build-test-ubuntu-core-ssl.yml index 050996b89047..0edc22b104e5 100644 --- a/.github/workflows/build-test-ubuntu-core-ssl.yml +++ b/.github/workflows/build-test-ubuntu-core-ssl.yml @@ -65,10 +65,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Daily (POC) cache key invalidation for test blocks and plots - id: today-date - run: date +%F > today.txt - - name: Cache test blocks and plots uses: actions/cache@v2 id: test-blocks-plots @@ -76,14 +72,14 @@ jobs: path: | ${{ github.workspace }}/.chia/blocks ${{ github.workspace }}/.chia/test-plots - key: ${{ hashFiles('today.txt') }} + key: 0.29.0 - name: Checkout test blocks and plots if: steps.test-blocks-plots.outputs.cache-hit != 'true' run: | - wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - mkdir ${{ github.workspace }}/.chia - mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-core-util.yml b/.github/workflows/build-test-ubuntu-core-util.yml index 68b23bd9c99f..2e5c699cb98d 100644 --- a/.github/workflows/build-test-ubuntu-core-util.yml +++ b/.github/workflows/build-test-ubuntu-core-util.yml @@ -65,10 +65,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Daily (POC) cache key invalidation for test blocks and plots - id: today-date - run: date +%F > today.txt - - name: Cache test blocks and plots uses: actions/cache@v2 id: test-blocks-plots @@ -76,14 +72,14 @@ jobs: path: | ${{ github.workspace }}/.chia/blocks ${{ github.workspace }}/.chia/test-plots - key: ${{ hashFiles('today.txt') }} + key: 0.29.0 - name: Checkout test blocks and plots if: steps.test-blocks-plots.outputs.cache-hit != 'true' run: | - wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - mkdir ${{ github.workspace }}/.chia - mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-core.yml b/.github/workflows/build-test-ubuntu-core.yml index bd32713578c1..ed717b529b72 100644 --- a/.github/workflows/build-test-ubuntu-core.yml +++ b/.github/workflows/build-test-ubuntu-core.yml @@ -65,10 +65,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Daily (POC) cache key invalidation for test blocks and plots - id: today-date - run: date +%F > today.txt - - name: Cache test blocks and plots uses: actions/cache@v2 id: test-blocks-plots @@ -76,14 +72,14 @@ jobs: path: | ${{ github.workspace }}/.chia/blocks ${{ github.workspace }}/.chia/test-plots - key: ${{ hashFiles('today.txt') }} + key: 0.29.0 - name: Checkout test blocks and plots if: steps.test-blocks-plots.outputs.cache-hit != 'true' run: | - wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - mkdir ${{ github.workspace }}/.chia - mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-farmer_harvester.yml b/.github/workflows/build-test-ubuntu-farmer_harvester.yml index 127ead45c06f..03a3050232a7 100644 --- a/.github/workflows/build-test-ubuntu-farmer_harvester.yml +++ b/.github/workflows/build-test-ubuntu-farmer_harvester.yml @@ -65,10 +65,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Daily (POC) cache key invalidation for test blocks and plots - id: today-date - run: date +%F > today.txt - - name: Cache test blocks and plots uses: actions/cache@v2 id: test-blocks-plots @@ -76,14 +72,14 @@ jobs: path: | ${{ github.workspace }}/.chia/blocks ${{ github.workspace }}/.chia/test-plots - key: ${{ hashFiles('today.txt') }} + key: 0.29.0 - name: Checkout test blocks and plots if: steps.test-blocks-plots.outputs.cache-hit != 'true' run: | - wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - mkdir ${{ github.workspace }}/.chia - mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-plot_sync.yml b/.github/workflows/build-test-ubuntu-plot_sync.yml index 54fa8b19e6e5..fddf0cd9c2ab 100644 --- a/.github/workflows/build-test-ubuntu-plot_sync.yml +++ b/.github/workflows/build-test-ubuntu-plot_sync.yml @@ -65,10 +65,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Daily (POC) cache key invalidation for test blocks and plots - id: today-date - run: date +%F > today.txt - - name: Cache test blocks and plots uses: actions/cache@v2 id: test-blocks-plots @@ -76,14 +72,14 @@ jobs: path: | ${{ github.workspace }}/.chia/blocks ${{ github.workspace }}/.chia/test-plots - key: ${{ hashFiles('today.txt') }} + key: 0.29.0 - name: Checkout test blocks and plots if: steps.test-blocks-plots.outputs.cache-hit != 'true' run: | - wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - mkdir ${{ github.workspace }}/.chia - mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-plotting.yml b/.github/workflows/build-test-ubuntu-plotting.yml index 099f50f6d7c3..882d0b0c113a 100644 --- a/.github/workflows/build-test-ubuntu-plotting.yml +++ b/.github/workflows/build-test-ubuntu-plotting.yml @@ -65,10 +65,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Daily (POC) cache key invalidation for test blocks and plots - id: today-date - run: date +%F > today.txt - - name: Cache test blocks and plots uses: actions/cache@v2 id: test-blocks-plots @@ -76,14 +72,14 @@ jobs: path: | ${{ github.workspace }}/.chia/blocks ${{ github.workspace }}/.chia/test-plots - key: ${{ hashFiles('today.txt') }} + key: 0.29.0 - name: Checkout test blocks and plots if: steps.test-blocks-plots.outputs.cache-hit != 'true' run: | - wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - mkdir ${{ github.workspace }}/.chia - mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-pools.yml b/.github/workflows/build-test-ubuntu-pools.yml index a4284b00e168..d2671e3a18de 100644 --- a/.github/workflows/build-test-ubuntu-pools.yml +++ b/.github/workflows/build-test-ubuntu-pools.yml @@ -65,10 +65,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Daily (POC) cache key invalidation for test blocks and plots - id: today-date - run: date +%F > today.txt - - name: Cache test blocks and plots uses: actions/cache@v2 id: test-blocks-plots @@ -76,14 +72,14 @@ jobs: path: | ${{ github.workspace }}/.chia/blocks ${{ github.workspace }}/.chia/test-plots - key: ${{ hashFiles('today.txt') }} + key: 0.29.0 - name: Checkout test blocks and plots if: steps.test-blocks-plots.outputs.cache-hit != 'true' run: | - wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - mkdir ${{ github.workspace }}/.chia - mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-simulation.yml b/.github/workflows/build-test-ubuntu-simulation.yml index 805f646b14e5..d6fbec1fc102 100644 --- a/.github/workflows/build-test-ubuntu-simulation.yml +++ b/.github/workflows/build-test-ubuntu-simulation.yml @@ -65,10 +65,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Daily (POC) cache key invalidation for test blocks and plots - id: today-date - run: date +%F > today.txt - - name: Cache test blocks and plots uses: actions/cache@v2 id: test-blocks-plots @@ -76,14 +72,14 @@ jobs: path: | ${{ github.workspace }}/.chia/blocks ${{ github.workspace }}/.chia/test-plots - key: ${{ hashFiles('today.txt') }} + key: 0.29.0 - name: Checkout test blocks and plots if: steps.test-blocks-plots.outputs.cache-hit != 'true' run: | - wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - mkdir ${{ github.workspace }}/.chia - mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml index 25835db6d9a0..af979fc0fd9c 100644 --- a/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-cat_wallet.yml @@ -65,10 +65,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Daily (POC) cache key invalidation for test blocks and plots - id: today-date - run: date +%F > today.txt - - name: Cache test blocks and plots uses: actions/cache@v2 id: test-blocks-plots @@ -76,14 +72,14 @@ jobs: path: | ${{ github.workspace }}/.chia/blocks ${{ github.workspace }}/.chia/test-plots - key: ${{ hashFiles('today.txt') }} + key: 0.29.0 - name: Checkout test blocks and plots if: steps.test-blocks-plots.outputs.cache-hit != 'true' run: | - wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - mkdir ${{ github.workspace }}/.chia - mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-wallet-rpc.yml b/.github/workflows/build-test-ubuntu-wallet-rpc.yml index 4ceb70b32bb4..bdeb794df439 100644 --- a/.github/workflows/build-test-ubuntu-wallet-rpc.yml +++ b/.github/workflows/build-test-ubuntu-wallet-rpc.yml @@ -65,10 +65,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Daily (POC) cache key invalidation for test blocks and plots - id: today-date - run: date +%F > today.txt - - name: Cache test blocks and plots uses: actions/cache@v2 id: test-blocks-plots @@ -76,14 +72,14 @@ jobs: path: | ${{ github.workspace }}/.chia/blocks ${{ github.workspace }}/.chia/test-plots - key: ${{ hashFiles('today.txt') }} + key: 0.29.0 - name: Checkout test blocks and plots if: steps.test-blocks-plots.outputs.cache-hit != 'true' run: | - wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - mkdir ${{ github.workspace }}/.chia - mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml b/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml index 899501734bf7..7a9585c713fd 100644 --- a/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml +++ b/.github/workflows/build-test-ubuntu-wallet-simple_sync.yml @@ -65,10 +65,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Daily (POC) cache key invalidation for test blocks and plots - id: today-date - run: date +%F > today.txt - - name: Cache test blocks and plots uses: actions/cache@v2 id: test-blocks-plots @@ -76,14 +72,14 @@ jobs: path: | ${{ github.workspace }}/.chia/blocks ${{ github.workspace }}/.chia/test-plots - key: ${{ hashFiles('today.txt') }} + key: 0.29.0 - name: Checkout test blocks and plots if: steps.test-blocks-plots.outputs.cache-hit != 'true' run: | - wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - mkdir ${{ github.workspace }}/.chia - mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-wallet-sync.yml b/.github/workflows/build-test-ubuntu-wallet-sync.yml index 5da886be8867..ceb86d8b4234 100644 --- a/.github/workflows/build-test-ubuntu-wallet-sync.yml +++ b/.github/workflows/build-test-ubuntu-wallet-sync.yml @@ -65,10 +65,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Daily (POC) cache key invalidation for test blocks and plots - id: today-date - run: date +%F > today.txt - - name: Cache test blocks and plots uses: actions/cache@v2 id: test-blocks-plots @@ -76,14 +72,14 @@ jobs: path: | ${{ github.workspace }}/.chia/blocks ${{ github.workspace }}/.chia/test-plots - key: ${{ hashFiles('today.txt') }} + key: 0.29.0 - name: Checkout test blocks and plots if: steps.test-blocks-plots.outputs.cache-hit != 'true' run: | - wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - mkdir ${{ github.workspace }}/.chia - mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-wallet.yml b/.github/workflows/build-test-ubuntu-wallet.yml index a6f734c9ae39..0e88ee66d539 100644 --- a/.github/workflows/build-test-ubuntu-wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet.yml @@ -65,10 +65,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Daily (POC) cache key invalidation for test blocks and plots - id: today-date - run: date +%F > today.txt - - name: Cache test blocks and plots uses: actions/cache@v2 id: test-blocks-plots @@ -76,14 +72,14 @@ jobs: path: | ${{ github.workspace }}/.chia/blocks ${{ github.workspace }}/.chia/test-plots - key: ${{ hashFiles('today.txt') }} + key: 0.29.0 - name: Checkout test blocks and plots if: steps.test-blocks-plots.outputs.cache-hit != 'true' run: | - wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - mkdir ${{ github.workspace }}/.chia - mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/.github/workflows/build-test-ubuntu-weight_proof.yml b/.github/workflows/build-test-ubuntu-weight_proof.yml index abccbf822928..041aaef86bfc 100644 --- a/.github/workflows/build-test-ubuntu-weight_proof.yml +++ b/.github/workflows/build-test-ubuntu-weight_proof.yml @@ -65,10 +65,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - name: Daily (POC) cache key invalidation for test blocks and plots - id: today-date - run: date +%F > today.txt - - name: Cache test blocks and plots uses: actions/cache@v2 id: test-blocks-plots @@ -76,14 +72,14 @@ jobs: path: | ${{ github.workspace }}/.chia/blocks ${{ github.workspace }}/.chia/test-plots - key: ${{ hashFiles('today.txt') }} + key: 0.29.0 - name: Checkout test blocks and plots if: steps.test-blocks-plots.outputs.cache-hit != 'true' run: | - wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - mkdir ${{ github.workspace }}/.chia - mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: diff --git a/chia/full_node/weight_proof.py b/chia/full_node/weight_proof.py index 1fac85317571..7756b8e65a80 100644 --- a/chia/full_node/weight_proof.py +++ b/chia/full_node/weight_proof.py @@ -1278,7 +1278,7 @@ def validate_recent_blocks( ses = False height = block.height for sub_slot in block.finished_sub_slots: - prev_challenge = challenge + prev_challenge = sub_slot.challenge_chain.challenge_chain_end_of_slot_vdf.challenge challenge = sub_slot.challenge_chain.get_hash() deficit = sub_slot.reward_chain.deficit if sub_slot.challenge_chain.subepoch_summary_hash is not None: @@ -1504,6 +1504,10 @@ def __get_rc_sub_slot( assert segment.rc_slot_end_info is not None if idx != 0: + # this is not the first slot, ses details should not be included + ses_hash = None + new_ssi = None + new_diff = None cc_vdf_info = VDFInfo(sub_slot.cc_slot_end_info.challenge, curr_ssi, sub_slot.cc_slot_end_info.output) if sub_slot.icc_slot_end_info is not None: icc_slot_end_info = VDFInfo( diff --git a/chia/plotting/create_plots.py b/chia/plotting/create_plots.py index c72c686a6dac..aef0ac773da7 100644 --- a/chia/plotting/create_plots.py +++ b/chia/plotting/create_plots.py @@ -156,9 +156,9 @@ async def create_plots( if args.size < config["min_mainnet_k_size"] and test_private_keys is None: log.warning(f"Creating plots with size k={args.size}, which is less than the minimum required for mainnet") - if args.size < 22: - log.warning("k under 22 is not supported. Increasing k to 22") - args.size = 22 + if args.size < 20: + log.warning("k under 22 is not supported. Increasing k to 21") + args.size = 20 if keys.pool_public_key is not None: log.info( diff --git a/tests/block_tools.py b/tests/block_tools.py index 6e480fdd256c..a000f494d4fc 100644 --- a/tests/block_tools.py +++ b/tests/block_tools.py @@ -98,7 +98,7 @@ **{ "MIN_PLOT_SIZE": 18, "MIN_BLOCKS_PER_CHALLENGE_BLOCK": 12, - "DIFFICULTY_STARTING": 2 ** 12, + "DIFFICULTY_STARTING": 2 ** 10, "DISCRIMINANT_SIZE_BITS": 16, "SUB_EPOCH_BLOCKS": 170, "WEIGHT_PROOF_THRESHOLD": 2, @@ -277,7 +277,7 @@ async def new_plot( tmp_dir = self.temp_dir args = Namespace() # Can't go much lower than 20, since plots start having no solutions and more buggy - args.size = 22 + args.size = 20 # Uses many plots for testing, in order to guarantee proofs of space at every height args.num = 1 args.buffer = 100 diff --git a/tests/blockchain/test_blockchain.py b/tests/blockchain/test_blockchain.py index 346822d44eb7..f6d9852bc85f 100644 --- a/tests/blockchain/test_blockchain.py +++ b/tests/blockchain/test_blockchain.py @@ -187,6 +187,7 @@ async def test_long_chain(self, empty_blockchain, default_1000_blocks): "reward_chain.challenge_chain_sub_slot_hash", new_finished_ss_3.challenge_chain.get_hash(), ) + log.warning(f"Number of slots: {len(block.finished_sub_slots)}") block_bad_3 = recursive_replace( block, "finished_sub_slots", [new_finished_ss_3] + block.finished_sub_slots[1:] ) @@ -741,11 +742,12 @@ async def test_empty_slot_no_ses(self, empty_blockchain, bt): await _validate_and_add_block(blockchain, block_bad, expected_result=ReceiveBlockResult.INVALID_BLOCK) @pytest.mark.asyncio - async def test_empty_sub_slots_epoch(self, empty_blockchain, bt): + async def test_empty_sub_slots_epoch(self, empty_blockchain, default_400_blocks, bt): # 2m # Tests adding an empty sub slot after the sub-epoch / epoch. # Also tests overflow block in epoch - blocks_base = bt.get_consecutive_blocks(test_constants.EPOCH_BLOCKS) + blocks_base = default_400_blocks[: test_constants.EPOCH_BLOCKS] + assert len(blocks_base) == test_constants.EPOCH_BLOCKS blocks_1 = bt.get_consecutive_blocks(1, block_list_input=blocks_base, force_overflow=True) blocks_2 = bt.get_consecutive_blocks(1, skip_slots=3, block_list_input=blocks_base, force_overflow=True) for block in blocks_base: @@ -779,10 +781,19 @@ async def test_wrong_cc_hash_rc(self, empty_blockchain, bt): @pytest.mark.asyncio async def test_invalid_cc_sub_slot_vdf(self, empty_blockchain, bt): # 2q - blocks = bt.get_consecutive_blocks(10) + blocks: List[FullBlock] = [] + found_overflow_slot: bool = False - for block in blocks: - if len(block.finished_sub_slots): + while not found_overflow_slot: + blocks = bt.get_consecutive_blocks(1, blocks) + block = blocks[-1] + if ( + len(block.finished_sub_slots) + and is_overflow_block(test_constants, block.reward_chain_block.signage_point_index) + and block.finished_sub_slots[-1].challenge_chain.challenge_chain_end_of_slot_vdf.output + != ClassgroupElement.get_default_element() + ): + found_overflow_slot = True # Bad iters new_finished_ss = recursive_replace( block.finished_sub_slots[-1], @@ -798,9 +809,11 @@ async def test_invalid_cc_sub_slot_vdf(self, empty_blockchain, bt): "reward_chain.challenge_chain_sub_slot_hash", new_finished_ss.challenge_chain.get_hash(), ) + log.warning(f"Num slots: {len(block.finished_sub_slots)}") block_bad = recursive_replace( block, "finished_sub_slots", block.finished_sub_slots[:-1] + [new_finished_ss] ) + log.warning(f"Signage point index: {block_bad.reward_chain_block.signage_point_index}") await _validate_and_add_block(empty_blockchain, block_bad, expected_error=Err.INVALID_CC_EOS_VDF) # Bad output @@ -845,7 +858,9 @@ async def test_invalid_cc_sub_slot_vdf(self, empty_blockchain, bt): ) await _validate_and_add_block_multi_error( - empty_blockchain, block_bad_3, [Err.INVALID_CC_EOS_VDF, Err.INVALID_PREV_CHALLENGE_SLOT_HASH] + empty_blockchain, + block_bad_3, + [Err.INVALID_CC_EOS_VDF, Err.INVALID_PREV_CHALLENGE_SLOT_HASH, Err.INVALID_POSPACE], ) # Bad proof @@ -864,9 +879,18 @@ async def test_invalid_cc_sub_slot_vdf(self, empty_blockchain, bt): @pytest.mark.asyncio async def test_invalid_rc_sub_slot_vdf(self, empty_blockchain, bt): # 2p - blocks = bt.get_consecutive_blocks(10) - for block in blocks: - if len(block.finished_sub_slots): + blocks: List[FullBlock] = [] + found_block: bool = False + + while not found_block: + blocks = bt.get_consecutive_blocks(1, blocks) + block = blocks[-1] + if ( + len(block.finished_sub_slots) + and block.finished_sub_slots[-1].reward_chain.end_of_slot_vdf.output + != ClassgroupElement.get_default_element() + ): + found_block = True # Bad iters new_finished_ss = recursive_replace( block.finished_sub_slots[-1], @@ -1011,7 +1035,9 @@ async def test_no_ses_if_no_se(self, empty_blockchain, bt): while True: blocks = bt.get_consecutive_blocks(1, block_list_input=blocks) - if len(blocks[-1].finished_sub_slots) > 0: + if len(blocks[-1].finished_sub_slots) > 0 and is_overflow_block( + test_constants, blocks[-1].reward_chain_block.signage_point_index + ): new_finished_ss: EndOfSubSlotBundle = recursive_replace( blocks[-1].finished_sub_slots[0], "challenge_chain", @@ -2860,7 +2886,7 @@ async def test_basic_reorg(self, empty_blockchain, bt): assert b.get_peak().height == 16 @pytest.mark.asyncio - async def test_long_reorg(self, empty_blockchain, default_10000_blocks, bt): + async def test_long_reorg(self, empty_blockchain, default_1500_blocks, test_long_reorg_blocks, bt): # Reorg longer than a difficulty adjustment # Also tests higher weight chain but lower height b = empty_blockchain @@ -2869,7 +2895,7 @@ async def test_long_reorg(self, empty_blockchain, default_10000_blocks, bt): num_blocks_chain_2 = 3 * test_constants.EPOCH_BLOCKS + test_constants.MAX_SUB_SLOT_BLOCKS + 8 assert num_blocks_chain_1 < 10000 - blocks = default_10000_blocks[:num_blocks_chain_1] + blocks = default_1500_blocks[:num_blocks_chain_1] for block in blocks: await _validate_and_add_block(b, block, skip_prevalidation=True) @@ -2877,15 +2903,15 @@ async def test_long_reorg(self, empty_blockchain, default_10000_blocks, bt): chain_1_weight = b.get_peak().weight assert chain_1_height == (num_blocks_chain_1 - 1) - # These blocks will have less time between them (timestamp) and therefore will make difficulty go up + # The reorg blocks will have less time between them (timestamp) and therefore will make difficulty go up # This means that the weight will grow faster, and we can get a heavier chain with lower height - blocks_reorg_chain = bt.get_consecutive_blocks( - num_blocks_chain_2 - num_blocks_chain_2_start, - blocks[:num_blocks_chain_2_start], - seed=b"2", - time_per_block=8, - ) - for reorg_block in blocks_reorg_chain: + + # If these assert fail, you probably need to change the fixture in test_long_reorg_blocks to create the + # right amount of blocks at the right time + assert test_long_reorg_blocks[num_blocks_chain_2_start - 1] == default_1500_blocks[num_blocks_chain_2_start - 1] + assert test_long_reorg_blocks[num_blocks_chain_2_start] != default_1500_blocks[num_blocks_chain_2_start] + + for reorg_block in test_long_reorg_blocks: if reorg_block.height < num_blocks_chain_2_start: await _validate_and_add_block( b, reorg_block, expected_result=ReceiveBlockResult.ALREADY_HAVE_BLOCK, skip_prevalidation=True @@ -2905,11 +2931,11 @@ async def test_long_reorg(self, empty_blockchain, default_10000_blocks, bt): assert b.get_peak().height < chain_1_height @pytest.mark.asyncio - async def test_long_compact_blockchain(self, empty_blockchain, default_10000_blocks_compact): + async def test_long_compact_blockchain(self, empty_blockchain, default_2000_blocks_compact): b = empty_blockchain - for block in default_10000_blocks_compact: + for block in default_2000_blocks_compact: await _validate_and_add_block(b, block, skip_prevalidation=True) - assert b.get_peak().height == len(default_10000_blocks_compact) - 1 + assert b.get_peak().height == len(default_2000_blocks_compact) - 1 @pytest.mark.asyncio async def test_reorg_from_genesis(self, empty_blockchain, bt): diff --git a/tests/conftest.py b/tests/conftest.py index f5377b57618e..cad1186f7e48 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -101,21 +101,21 @@ def softfork_height(request): return request.param -block_format_version = "rc4" +saved_blocks_version = "rc5" @pytest.fixture(scope="session") def default_400_blocks(bt): from tests.util.blockchain import persistent_blocks - return persistent_blocks(400, f"test_blocks_400_{block_format_version}.db", bt, seed=b"alternate2") + return persistent_blocks(400, f"test_blocks_400_{saved_blocks_version}.db", bt, seed=b"400") @pytest.fixture(scope="session") def default_1000_blocks(bt): from tests.util.blockchain import persistent_blocks - return persistent_blocks(1000, f"test_blocks_1000_{block_format_version}.db", bt) + return persistent_blocks(1000, f"test_blocks_1000_{saved_blocks_version}.db", bt, seed=b"1000") @pytest.fixture(scope="session") @@ -123,22 +123,63 @@ def pre_genesis_empty_slots_1000_blocks(bt): from tests.util.blockchain import persistent_blocks return persistent_blocks( - 1000, f"pre_genesis_empty_slots_1000_blocks{block_format_version}.db", bt, seed=b"alternate2", empty_sub_slots=1 + 1000, + f"pre_genesis_empty_slots_1000_blocks{saved_blocks_version}.db", + bt, + seed=b"empty_slots", + empty_sub_slots=1, ) +@pytest.fixture(scope="session") +def default_1500_blocks(bt): + from tests.util.blockchain import persistent_blocks + + return persistent_blocks(1500, f"test_blocks_1500_{saved_blocks_version}.db", bt, seed=b"1500") + + @pytest.fixture(scope="session") def default_10000_blocks(bt): from tests.util.blockchain import persistent_blocks - return persistent_blocks(10000, f"test_blocks_10000_{block_format_version}.db", bt) + return persistent_blocks(10000, f"test_blocks_10000_{saved_blocks_version}.db", bt, seed=b"10000") @pytest.fixture(scope="session") def default_20000_blocks(bt): from tests.util.blockchain import persistent_blocks - return persistent_blocks(20000, f"test_blocks_20000_{block_format_version}.db", bt) + return persistent_blocks(20000, f"test_blocks_20000_{saved_blocks_version}.db", bt, seed=b"20000") + + +@pytest.fixture(scope="session") +def test_long_reorg_blocks(bt, default_1500_blocks): + from tests.util.blockchain import persistent_blocks + + return persistent_blocks( + 758, + f"test_blocks_long_reorg_{saved_blocks_version}.db", + bt, + block_list_input=default_1500_blocks[:320], + seed=b"reorg_blocks", + time_per_block=8, + ) + + +@pytest.fixture(scope="session") +def default_2000_blocks_compact(bt): + from tests.util.blockchain import persistent_blocks + + return persistent_blocks( + 2000, + f"test_blocks_2000_compact_{saved_blocks_version}.db", + bt, + normalized_to_identity_cc_eos=True, + normalized_to_identity_icc_eos=True, + normalized_to_identity_cc_ip=True, + normalized_to_identity_cc_sp=True, + seed=b"2000_compact", + ) @pytest.fixture(scope="session") @@ -147,12 +188,13 @@ def default_10000_blocks_compact(bt): return persistent_blocks( 10000, - f"test_blocks_10000_compact_{block_format_version}.db", + f"test_blocks_10000_compact_{saved_blocks_version}.db", bt, normalized_to_identity_cc_eos=True, normalized_to_identity_icc_eos=True, normalized_to_identity_cc_ip=True, normalized_to_identity_cc_sp=True, + seed=b"1000_compact", ) diff --git a/tests/core/full_node/full_sync/test_full_sync.py b/tests/core/full_node/full_sync/test_full_sync.py index df244e7bbe85..5513f7bdf5f7 100644 --- a/tests/core/full_node/full_sync/test_full_sync.py +++ b/tests/core/full_node/full_sync/test_full_sync.py @@ -278,7 +278,7 @@ async def test_close_height_but_big_reorg(self, three_nodes, bt, self_hostname): @pytest.mark.asyncio async def test_sync_bad_peak_while_synced( - self, three_nodes, default_1000_blocks, default_10000_blocks, self_hostname + self, three_nodes, default_1000_blocks, default_1500_blocks, self_hostname ): # Must be larger than "sync_block_behind_threshold" in the config num_blocks_initial = len(default_1000_blocks) - 250 @@ -292,7 +292,7 @@ async def test_sync_bad_peak_while_synced( await full_node_1.full_node.respond_block(full_node_protocol.RespondBlock(block)) # Node 3 syncs from a different blockchain - for block in default_10000_blocks[:1100]: + for block in default_1500_blocks[:1100]: await full_node_3.full_node.respond_block(full_node_protocol.RespondBlock(block)) await server_2.start_client(PeerInfo(self_hostname, uint16(server_1._port)), full_node_2.full_node.on_connect) @@ -304,7 +304,7 @@ async def test_sync_bad_peak_while_synced( # node 2 should keep being synced and receive blocks await server_3.start_client(PeerInfo(self_hostname, uint16(server_3._port)), full_node_3.full_node.on_connect) # trigger long sync in full node 2 - peak_block = default_10000_blocks[1050] + peak_block = default_1500_blocks[1050] await server_2.start_client(PeerInfo(self_hostname, uint16(server_3._port)), full_node_2.full_node.on_connect) con = server_2.all_connections[full_node_3.full_node.server.node_id] peak = full_node_protocol.NewPeak( diff --git a/tests/core/full_node/test_full_node.py b/tests/core/full_node/test_full_node.py index c71e8b0758aa..bb9c0e0596dc 100644 --- a/tests/core/full_node/test_full_node.py +++ b/tests/core/full_node/test_full_node.py @@ -1541,7 +1541,7 @@ async def test_compact_protocol(self, setup_two_nodes_fixture, bt): ) # Note: the below numbers depend on the block cache, so might need to be updated - assert cc_eos_count == 4 and icc_eos_count == 3 + assert cc_eos_count == 3 and icc_eos_count == 3 for compact_proof in timelord_protocol_finished: await full_node_1.full_node.respond_compact_proof_of_time(compact_proof) stored_blocks = await full_node_1.get_all_full_blocks() @@ -1563,7 +1563,7 @@ async def test_compact_protocol(self, setup_two_nodes_fixture, bt): if block.challenge_chain_ip_proof.normalized_to_identity: has_compact_cc_ip_vdf = True # Note: the below numbers depend on the block cache, so might need to be updated - assert cc_eos_compact_count == 4 + assert cc_eos_compact_count == 3 assert icc_eos_compact_count == 3 assert has_compact_cc_sp_vdf assert has_compact_cc_ip_vdf diff --git a/tests/core/full_node/test_performance.py b/tests/core/full_node/test_performance.py index 58e1edeac358..fa57244f6588 100644 --- a/tests/core/full_node/test_performance.py +++ b/tests/core/full_node/test_performance.py @@ -10,6 +10,7 @@ from clvm.casts import int_to_bytes from chia.consensus.block_record import BlockRecord +from chia.consensus.pot_iterations import is_overflow_block from chia.full_node.full_node_api import FullNodeAPI from chia.protocols import full_node_protocol as fnp from chia.types.condition_opcodes import ConditionOpcode @@ -155,8 +156,12 @@ async def test_full_block_performance(self, bt, wallet_nodes_perf, self_hostname guarantee_transaction_block=True, ) block = blocks[-1] + if is_overflow_block(bt.constants, block.reward_chain_block.signage_point_index): + sub_slots = block.finished_sub_slots[:-1] + else: + sub_slots = block.finished_sub_slots unfinished = UnfinishedBlock( - block.finished_sub_slots, + sub_slots, block.reward_chain_block.get_unfinished(), block.challenge_chain_sp_proof, block.reward_chain_sp_proof, diff --git a/tests/runner_templates/checkout-test-plots.include.yml b/tests/runner_templates/checkout-test-plots.include.yml index 6d3239bbe662..af48e1a2206b 100644 --- a/tests/runner_templates/checkout-test-plots.include.yml +++ b/tests/runner_templates/checkout-test-plots.include.yml @@ -1,7 +1,3 @@ - - name: Daily (POC) cache key invalidation for test blocks and plots - id: today-date - run: date +%F > today.txt - - name: Cache test blocks and plots uses: actions/cache@v2 id: test-blocks-plots @@ -9,11 +5,11 @@ path: | ${{ github.workspace }}/.chia/blocks ${{ github.workspace }}/.chia/test-plots - key: ${{ hashFiles('today.txt') }} + key: 0.29.0 - name: Checkout test blocks and plots if: steps.test-blocks-plots.outputs.cache-hit != 'true' run: | - wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.28.0.tar.gz | tar xzf - + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - mkdir ${{ github.workspace }}/.chia - mv ${{ github.workspace }}/test-cache-0.28.0/* ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia diff --git a/tests/util/blockchain.py b/tests/util/blockchain.py index 95d6908b0603..ed1a628aba1a 100644 --- a/tests/util/blockchain.py +++ b/tests/util/blockchain.py @@ -1,7 +1,7 @@ import os import pickle from pathlib import Path -from typing import List +from typing import List, Optional import aiosqlite import tempfile @@ -45,9 +45,13 @@ def persistent_blocks( normalized_to_identity_icc_eos: bool = False, normalized_to_identity_cc_sp: bool = False, normalized_to_identity_cc_ip: bool = False, + block_list_input: List[FullBlock] = None, + time_per_block: Optional[float] = None, ): # try loading from disc, if not create new blocks.db file # TODO hash fixtures.py and blocktool.py, add to path, delete if the files changed + if block_list_input is None: + block_list_input = [] block_path_dir = DEFAULT_ROOT_PATH.parent.joinpath("blocks") file_path = block_path_dir.joinpath(db_name) @@ -65,7 +69,7 @@ def persistent_blocks( blocks: List[FullBlock] = [] for block_bytes in block_bytes_list: blocks.append(FullBlock.from_bytes(block_bytes)) - if len(blocks) == num_of_blocks: + if len(blocks) == num_of_blocks + len(block_list_input): print(f"\n loaded {file_path} with {len(blocks)} blocks") return blocks except EOFError: @@ -80,6 +84,8 @@ def persistent_blocks( seed, empty_sub_slots, bt, + block_list_input, + time_per_block, normalized_to_identity_cc_eos, normalized_to_identity_icc_eos, normalized_to_identity_cc_sp, @@ -93,6 +99,8 @@ def new_test_db( seed: bytes, empty_sub_slots: int, bt: BlockTools, + block_list_input: List[FullBlock], + time_per_block: Optional[float], normalized_to_identity_cc_eos: bool = False, # CC_EOS, normalized_to_identity_icc_eos: bool = False, # ICC_EOS normalized_to_identity_cc_sp: bool = False, # CC_SP, @@ -101,6 +109,8 @@ def new_test_db( print(f"create {path} with {num_of_blocks} blocks with ") blocks: List[FullBlock] = bt.get_consecutive_blocks( num_of_blocks, + block_list_input=block_list_input, + time_per_block=time_per_block, seed=seed, skip_slots=empty_sub_slots, normalized_to_identity_cc_eos=normalized_to_identity_cc_eos, From 415236bf679b9f88e5047d22e1df6e09010a034a Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 8 Apr 2022 12:58:46 -0400 Subject: [PATCH 348/378] can we get by without dead snakes? (#11070) * can we get by without dead snakes? * Update install-timelord.sh * Revert "Update install-timelord.sh" This reverts commit cba3250b095efbac5459c430102a7914243bfa96. * do not install python dev package for timelords build in ci it is already there... * more quotes for sh --- .../build-test-macos-core-daemon.yml | 10 +---- .../workflows/build-test-macos-simulation.yml | 10 +---- .../build-test-ubuntu-core-daemon.yml | 10 +---- .../build-test-ubuntu-simulation.yml | 10 +---- install-timelord.sh | 37 +++++++++++++++++-- .../install-timelord.include.yml | 10 +---- 6 files changed, 38 insertions(+), 49 deletions(-) diff --git a/.github/workflows/build-test-macos-core-daemon.yml b/.github/workflows/build-test-macos-core-daemon.yml index 950f08481d80..7ab9dc765a25 100644 --- a/.github/workflows/build-test-macos-core-daemon.yml +++ b/.github/workflows/build-test-macos-core-daemon.yml @@ -88,18 +88,10 @@ jobs: brew install boost sh install.sh -d - - name: Install Ubuntu dependencies - if: startsWith(matrix.os, 'ubuntu') - run: | - sudo apt-get install software-properties-common - sudo add-apt-repository ppa:deadsnakes/ppa - sudo apt-get update - sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y - - name: Install timelord run: | . ./activate - sh install-timelord.sh + sh install-timelord.sh -n ./vdf_bench square_asm 400000 - name: Test core-daemon code with pytest diff --git a/.github/workflows/build-test-macos-simulation.yml b/.github/workflows/build-test-macos-simulation.yml index a7bd5c669573..9d8e1f57eb7c 100644 --- a/.github/workflows/build-test-macos-simulation.yml +++ b/.github/workflows/build-test-macos-simulation.yml @@ -88,18 +88,10 @@ jobs: brew install boost sh install.sh -d - - name: Install Ubuntu dependencies - if: startsWith(matrix.os, 'ubuntu') - run: | - sudo apt-get install software-properties-common - sudo add-apt-repository ppa:deadsnakes/ppa - sudo apt-get update - sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y - - name: Install timelord run: | . ./activate - sh install-timelord.sh + sh install-timelord.sh -n ./vdf_bench square_asm 400000 - name: Test simulation code with pytest diff --git a/.github/workflows/build-test-ubuntu-core-daemon.yml b/.github/workflows/build-test-ubuntu-core-daemon.yml index 3611af06d244..1992c859f152 100644 --- a/.github/workflows/build-test-ubuntu-core-daemon.yml +++ b/.github/workflows/build-test-ubuntu-core-daemon.yml @@ -87,18 +87,10 @@ jobs: run: | sh install.sh -d - - name: Install Ubuntu dependencies - if: startsWith(matrix.os, 'ubuntu') - run: | - sudo apt-get install software-properties-common - sudo add-apt-repository ppa:deadsnakes/ppa - sudo apt-get update - sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y - - name: Install timelord run: | . ./activate - sh install-timelord.sh + sh install-timelord.sh -n ./vdf_bench square_asm 400000 - name: Test core-daemon code with pytest diff --git a/.github/workflows/build-test-ubuntu-simulation.yml b/.github/workflows/build-test-ubuntu-simulation.yml index d6fbec1fc102..06435f947ea9 100644 --- a/.github/workflows/build-test-ubuntu-simulation.yml +++ b/.github/workflows/build-test-ubuntu-simulation.yml @@ -87,18 +87,10 @@ jobs: run: | sh install.sh -d - - name: Install Ubuntu dependencies - if: startsWith(matrix.os, 'ubuntu') - run: | - sudo apt-get install software-properties-common - sudo add-apt-repository ppa:deadsnakes/ppa - sudo apt-get update - sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y - - name: Install timelord run: | . ./activate - sh install-timelord.sh + sh install-timelord.sh -n ./vdf_bench square_asm 400000 - name: Test simulation code with pytest diff --git a/install-timelord.sh b/install-timelord.sh index 3c99263768a6..7368887492b6 100644 --- a/install-timelord.sh +++ b/install-timelord.sh @@ -2,6 +2,29 @@ set -o errexit +USAGE_TEXT="\ +Usage: $0 [-d] + + -n do not install Python development package, Python.h etc + -h display this help and exit +" + +usage() { + echo "${USAGE_TEXT}" +} + +INSTALL_PYTHON_DEV=1 + +while getopts nh flag +do + case "${flag}" in + # development + n) INSTALL_PYTHON_DEV=;; + h) usage; exit 0;; + *) echo; usage; exit 1;; + esac +done + if [ -z "$VIRTUAL_ENV" ]; then echo "This requires the chia python virtual environment." echo "Execute '. ./activate' before running." @@ -13,6 +36,12 @@ echo "Timelord requires CMake 3.14+ to compile vdf_client." PYTHON_VERSION=$(python -c 'import sys; print(f"python{sys.version_info.major}.{sys.version_info.minor}")') echo "Python version: $PYTHON_VERSION" +if [ "$INSTALL_PYTHON_DEV" ]; then + PYTHON_DEV_DEPENDENCY=lib"$PYTHON_VERSION"-dev +else + PYTHON_DEV_DEPENDENCY= +fi + export BUILD_VDF_BENCH=Y # Installs the useful vdf_bench test of CPU squaring speed THE_PATH=$(python -c 'import pkg_resources; print( pkg_resources.get_distribution("chiavdf").location)' 2>/dev/null)/vdf_client CHIAVDF_VERSION=$(python -c 'from setup import dependencies; t = [_ for _ in dependencies if _.startswith("chiavdf")][0]; print(t)') @@ -63,16 +92,16 @@ else # If Ubuntu version is older than 20.04LTS then upgrade CMake ubuntu_cmake_install # Install remaining needed development tools - assumes venv and prior run of install.sh - echo apt-get install libgmp-dev libboost-python-dev lib"$PYTHON_VERSION"-dev libboost-system-dev build-essential -y - sudo apt-get install libgmp-dev libboost-python-dev lib"$PYTHON_VERSION"-dev libboost-system-dev build-essential -y + echo apt-get install libgmp-dev libboost-python-dev "$PYTHON_DEV_DEPENDENCY" libboost-system-dev build-essential -y + sudo apt-get install libgmp-dev libboost-python-dev "$PYTHON_DEV_DEPENDENCY" libboost-system-dev build-essential -y echo venv/bin/python -m pip install --force --no-binary chiavdf "$CHIAVDF_VERSION" venv/bin/python -m pip install --force --no-binary chiavdf "$CHIAVDF_VERSION" symlink_vdf_bench "$PYTHON_VERSION" elif [ -e venv/bin/python ] && test $RHEL_BASED; then echo "Installing chiavdf from source on RedHat/CentOS/Fedora" # Install remaining needed development tools - assumes venv and prior run of install.sh - echo yum install gcc gcc-c++ gmp-devel python3-devel libtool make autoconf automake openssl-devel libevent-devel boost-devel python3 -y - sudo yum install gcc gcc-c++ gmp-devel python3-devel libtool make autoconf automake openssl-devel libevent-devel boost-devel python3 -y + echo yum install gcc gcc-c++ gmp-devel "$PYTHON_DEV_DEPENDENCY" libtool make autoconf automake openssl-devel libevent-devel boost-devel python3 -y + sudo yum install gcc gcc-c++ gmp-devel "$PYTHON_DEV_DEPENDENCY" libtool make autoconf automake openssl-devel libevent-devel boost-devel python3 -y echo venv/bin/python -m pip install --force --no-binary chiavdf "$CHIAVDF_VERSION" venv/bin/python -m pip install --force --no-binary chiavdf "$CHIAVDF_VERSION" symlink_vdf_bench "$PYTHON_VERSION" diff --git a/tests/runner_templates/install-timelord.include.yml b/tests/runner_templates/install-timelord.include.yml index 0093f48f6947..0fbdcf20e8c7 100644 --- a/tests/runner_templates/install-timelord.include.yml +++ b/tests/runner_templates/install-timelord.include.yml @@ -1,13 +1,5 @@ - - name: Install Ubuntu dependencies - if: startsWith(matrix.os, 'ubuntu') - run: | - sudo apt-get install software-properties-common - sudo add-apt-repository ppa:deadsnakes/ppa - sudo apt-get update - sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y - - name: Install timelord run: | . ./activate - sh install-timelord.sh + sh install-timelord.sh -n ./vdf_bench square_asm 400000 From 34d48a108b57cea93309c2a4adff606ebd1e7ca3 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 8 Apr 2022 12:59:10 -0400 Subject: [PATCH 349/378] Changelog from 1.3.3 (#11081) * Updating changelog * Update appdmg to 0.6.4 to work with macos 12.3 (#10886) * restrict click to < 8.1 for black https://github.com/pallets/click/issues/2225 Doing this instead of updating since updating black will change several files due to some formatting change. I would like to take that on separately from unbreaking CI. * Check for vulnerable openssl (#10988) * Check for vulnerable openssl * Update OpenSSL on MacOS * First attempt - openssl Ubuntu 18.04 and 20.04 * place local/bin ahead in PATH * specify install openssl * correct path * run ldconfig * stop building and check for patched openssl * spell sudo right by removing it * Remove openssl building - 1st attempt RHs * Test Windows OpenSSL version HT @AmineKhaldi * Get updated openssl version (#10991) * Get updated openssl version * Update pyinstaller * Fix typo * lets try this * Let's try this * Try this Co-authored-by: Earle Lowe * Gh 1.3.3v2 (#11011) * Non Hobo patch the winstaller for CVE-2022-0778 (#10995) * install.sh is not upgrading OpenSSL on MacOS (#11003) * MacOS isn't updating OpenSSL in install.sh * Exit if no brew on MacOS * Code the if tree like a pro instead. Co-authored-by: Kyle Altendorf Co-authored-by: Kyle Altendorf * Remove hobo patch * apt show not needed (#10997) * install/upgrade openssl on Arch Linux also * Update CHANGELOG * revert Arch change backport Co-authored-by: Kyle Altendorf Co-authored-by: wallentx Co-authored-by: Chris Marslender Co-authored-by: Gene Hoffman <30377676+hoffmang9@users.noreply.github.com> Co-authored-by: William Allen Co-authored-by: Earle Lowe --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35248b96e168..343909b93ca3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,19 @@ for setuptools_scm/PEP 440 reasons. ## [Unreleased] +## 1.3.3 Chia blockchain 2022-4-02 + +### Fixed + +- In version 1.3.2 our patch for the OpenSSL vulnerability was not complete for the Windows installer. Thank you @xsmolasses of Core-Pool. +- MacOS would not update openssl when installing via `install.sh` +- Some debugging information remained in `install.sh` + +## 1.3.2 Chia blockchain 2022-4-01 + +### Fixed + +- Fixed OpenSSL vulnerability CVE-2022-0778 ## 1.3.1 Chia blockchain 2022-3-16 From 13512be2b28e91dfcb9ebff1cbef8bab97127307 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 8 Apr 2022 12:59:36 -0400 Subject: [PATCH 350/378] consistently name installer github actions artifact zips (#11096) --- .github/workflows/build-linux-arm64-installer.yml | 2 +- .github/workflows/build-linux-installer-deb.yml | 2 +- .github/workflows/build-linux-installer-rpm.yml | 2 +- .github/workflows/build-macos-installer.yml | 2 +- .github/workflows/build-macos-m1-installer.yml | 2 +- .github/workflows/build-windows-installer.yml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-linux-arm64-installer.yml b/.github/workflows/build-linux-arm64-installer.yml index 6b05a2560e40..6c4a1f5ac5d3 100644 --- a/.github/workflows/build-linux-arm64-installer.yml +++ b/.github/workflows/build-linux-arm64-installer.yml @@ -120,7 +120,7 @@ jobs: - name: Upload Linux artifacts uses: actions/upload-artifact@v2 with: - name: Linux-ARM-64-Installer + name: chia-installers-linux-deb-arm64 path: ${{ github.workspace }}/build_scripts/final_installer/ - name: Configure AWS Credentials diff --git a/.github/workflows/build-linux-installer-deb.yml b/.github/workflows/build-linux-installer-deb.yml index e9690e8bfadf..55380909624c 100644 --- a/.github/workflows/build-linux-installer-deb.yml +++ b/.github/workflows/build-linux-installer-deb.yml @@ -163,7 +163,7 @@ jobs: - name: Upload Linux artifacts uses: actions/upload-artifact@v2 with: - name: Linux-Installers + name: chia-installers-linux-deb-intel path: ${{ github.workspace }}/build_scripts/final_installer/ - name: Configure AWS Credentials diff --git a/.github/workflows/build-linux-installer-rpm.yml b/.github/workflows/build-linux-installer-rpm.yml index aa9c85ed835c..4d3f6ff45c88 100644 --- a/.github/workflows/build-linux-installer-rpm.yml +++ b/.github/workflows/build-linux-installer-rpm.yml @@ -123,7 +123,7 @@ jobs: - name: Upload Linux artifacts uses: actions/upload-artifact@v2 with: - name: Linux-Installers + name: chia-installers-linux-rpm-intel path: ${{ github.workspace }}/build_scripts/final_installer/ - name: Configure AWS Credentials diff --git a/.github/workflows/build-macos-installer.yml b/.github/workflows/build-macos-installer.yml index da60f6f20ab4..cb568c9518e1 100644 --- a/.github/workflows/build-macos-installer.yml +++ b/.github/workflows/build-macos-installer.yml @@ -150,7 +150,7 @@ jobs: - name: Upload MacOS artifacts uses: actions/upload-artifact@v2 with: - name: Chia-Installer-MacOS-intel-dmg + name: chia-installers-macos-dmg-intel path: ${{ github.workspace }}/build_scripts/final_installer/ - name: Create Checksums diff --git a/.github/workflows/build-macos-m1-installer.yml b/.github/workflows/build-macos-m1-installer.yml index 18d08db16d89..67082a946d85 100644 --- a/.github/workflows/build-macos-m1-installer.yml +++ b/.github/workflows/build-macos-m1-installer.yml @@ -124,7 +124,7 @@ jobs: - name: Upload MacOS artifacts uses: actions/upload-artifact@v2 with: - name: Chia-Installer-MacOS-arm64-dmg + name: chia-installers-macos-dmg-arm64 path: ${{ github.workspace }}/build_scripts/final_installer/ - name: Install AWS CLI diff --git a/.github/workflows/build-windows-installer.yml b/.github/workflows/build-windows-installer.yml index 67ee617b5e52..4830cfe91355 100644 --- a/.github/workflows/build-windows-installer.yml +++ b/.github/workflows/build-windows-installer.yml @@ -155,7 +155,7 @@ jobs: - name: Upload Windows exe's to artifacts uses: actions/upload-artifact@v2.2.2 with: - name: Windows-Exe + name: chia-installers-windows-exe-intel path: ${{ github.workspace }}\chia-blockchain-gui\Chia-win32-x64\ - name: Upload Installer to artifacts From e6f60c572ebda42d99899128cbc41dd270e5f293 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 8 Apr 2022 12:59:59 -0400 Subject: [PATCH 351/378] git -C and consistent activation in installer builds (#11098) --- .github/workflows/build-linux-arm64-installer.yml | 7 +++---- .github/workflows/build-linux-installer-deb.yml | 7 +++---- .github/workflows/build-linux-installer-rpm.yml | 7 +++---- .github/workflows/build-macos-installer.yml | 5 ++--- .github/workflows/build-macos-m1-installer.yml | 5 ++--- .github/workflows/build-windows-installer.yml | 1 + 6 files changed, 14 insertions(+), 18 deletions(-) diff --git a/.github/workflows/build-linux-arm64-installer.yml b/.github/workflows/build-linux-arm64-installer.yml index 6c4a1f5ac5d3..f2a65b1b5ba9 100644 --- a/.github/workflows/build-linux-arm64-installer.yml +++ b/.github/workflows/build-linux-arm64-installer.yml @@ -110,11 +110,10 @@ jobs: - name: Build arm64 .deb package run: | - . ./activate ldd --version - cd ./chia-blockchain-gui - git status - cd ../build_scripts + git -C ./chia-blockchain-gui status + . ./activate + cd ./build_scripts sh build_linux_deb.sh arm64 - name: Upload Linux artifacts diff --git a/.github/workflows/build-linux-installer-deb.yml b/.github/workflows/build-linux-installer-deb.yml index 55380909624c..372448210811 100644 --- a/.github/workflows/build-linux-installer-deb.yml +++ b/.github/workflows/build-linux-installer-deb.yml @@ -153,11 +153,10 @@ jobs: - name: Build .deb package run: | - . ./activate ldd --version - cd ./chia-blockchain-gui - git status - cd ../build_scripts + git -C ./chia-blockchain-gui status + . ./activate + cd ./build_scripts sh build_linux_deb.sh amd64 - name: Upload Linux artifacts diff --git a/.github/workflows/build-linux-installer-rpm.yml b/.github/workflows/build-linux-installer-rpm.yml index 4d3f6ff45c88..43ef0ccdf876 100644 --- a/.github/workflows/build-linux-installer-rpm.yml +++ b/.github/workflows/build-linux-installer-rpm.yml @@ -113,11 +113,10 @@ jobs: - name: Build .rpm package run: | - . ./activate ldd --version - cd ./chia-blockchain-gui - git status - cd ../build_scripts + git -C ./chia-blockchain-gui status + . ./activate + cd ./build_scripts sh build_linux_rpm.sh amd64 - name: Upload Linux artifacts diff --git a/.github/workflows/build-macos-installer.yml b/.github/workflows/build-macos-installer.yml index cb568c9518e1..2bf43d1f5b25 100644 --- a/.github/workflows/build-macos-installer.yml +++ b/.github/workflows/build-macos-installer.yml @@ -141,10 +141,9 @@ jobs: APPLE_NOTARIZE_USERNAME: "${{ secrets.APPLE_NOTARIZE_USERNAME }}" APPLE_NOTARIZE_PASSWORD: "${{ secrets.APPLE_NOTARIZE_PASSWORD }}" run: | + git -C ./chia-blockchain-gui status . ./activate - cd ./chia-blockchain-gui - git status - cd ../build_scripts + cd ./build_scripts sh build_macos.sh - name: Upload MacOS artifacts diff --git a/.github/workflows/build-macos-m1-installer.yml b/.github/workflows/build-macos-m1-installer.yml index 67082a946d85..9c728c395c2d 100644 --- a/.github/workflows/build-macos-m1-installer.yml +++ b/.github/workflows/build-macos-m1-installer.yml @@ -115,10 +115,9 @@ jobs: APPLE_NOTARIZE_PASSWORD: "${{ secrets.APPLE_NOTARIZE_PASSWORD }}" run: | export PATH=$(brew --prefix node@16)/bin:$PATH + git -C ./chia-blockchain-gui status . ./activate - cd ./chia-blockchain-gui - arch -arm64 git status - cd ../build_scripts + cd ./build_scripts arch -arm64 sh build_macos_m1.sh - name: Upload MacOS artifacts diff --git a/.github/workflows/build-windows-installer.yml b/.github/workflows/build-windows-installer.yml index 4830cfe91355..7fbd981c03d5 100644 --- a/.github/workflows/build-windows-installer.yml +++ b/.github/workflows/build-windows-installer.yml @@ -150,6 +150,7 @@ jobs: run: | $env:path="C:\Program` Files` (x86)\Microsoft` Visual` Studio\2019\Enterprise\SDK\ScopeCppSDK\vc15\VC\bin\;$env:path" $env:path="C:\Program` Files` (x86)\Windows` Kits\10\App` Certification` Kit;$env:path" + git -C .\chia-blockchain-gui status .\build_scripts\build_windows.ps1 - name: Upload Windows exe's to artifacts From f928283b3d0871f7e5e08000791f03121328e90d Mon Sep 17 00:00:00 2001 From: William Blanke Date: Fri, 8 Apr 2022 10:17:32 -0700 Subject: [PATCH 352/378] updated gui to d714c21b4ee3ebbc7d18b5f819772cd9868d0bf5 --- chia-blockchain-gui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia-blockchain-gui b/chia-blockchain-gui index 054d7b342e7c..d714c21b4ee3 160000 --- a/chia-blockchain-gui +++ b/chia-blockchain-gui @@ -1 +1 @@ -Subproject commit 054d7b342e7c8284c9b58a775f87d393a1008bfe +Subproject commit d714c21b4ee3ebbc7d18b5f819772cd9868d0bf5 From 2d4045b9d222091bdd79881a81fb2c0795f46c18 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 8 Apr 2022 16:49:21 -0400 Subject: [PATCH 353/378] only check the version once in installer build workflows (#11099) --- .github/workflows/build-linux-arm64-installer.yml | 2 ++ .github/workflows/build-linux-installer-deb.yml | 2 ++ .github/workflows/build-linux-installer-rpm.yml | 2 ++ .github/workflows/build-macos-installer.yml | 1 + .github/workflows/build-macos-m1-installer.yml | 1 + .github/workflows/build-windows-installer.yml | 1 + build_scripts/build_linux_deb.sh | 3 --- build_scripts/build_linux_rpm.sh | 3 --- build_scripts/build_macos.sh | 3 --- build_scripts/build_macos_m1.sh | 3 --- build_scripts/build_windows.ps1 | 3 --- 11 files changed, 9 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build-linux-arm64-installer.yml b/.github/workflows/build-linux-arm64-installer.yml index f2a65b1b5ba9..61ad71d55030 100644 --- a/.github/workflows/build-linux-arm64-installer.yml +++ b/.github/workflows/build-linux-arm64-installer.yml @@ -109,6 +109,8 @@ jobs: sh install.sh - name: Build arm64 .deb package + env: + CHIA_INSTALLER_VERSION: ${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }} run: | ldd --version git -C ./chia-blockchain-gui status diff --git a/.github/workflows/build-linux-installer-deb.yml b/.github/workflows/build-linux-installer-deb.yml index 372448210811..a9feddfe1441 100644 --- a/.github/workflows/build-linux-installer-deb.yml +++ b/.github/workflows/build-linux-installer-deb.yml @@ -152,6 +152,8 @@ jobs: sudo apt-get install -y jq - name: Build .deb package + env: + CHIA_INSTALLER_VERSION: ${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }} run: | ldd --version git -C ./chia-blockchain-gui status diff --git a/.github/workflows/build-linux-installer-rpm.yml b/.github/workflows/build-linux-installer-rpm.yml index 43ef0ccdf876..c97c85526079 100644 --- a/.github/workflows/build-linux-installer-rpm.yml +++ b/.github/workflows/build-linux-installer-rpm.yml @@ -112,6 +112,8 @@ jobs: sh install.sh - name: Build .rpm package + env: + CHIA_INSTALLER_VERSION: ${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }} run: | ldd --version git -C ./chia-blockchain-gui status diff --git a/.github/workflows/build-macos-installer.yml b/.github/workflows/build-macos-installer.yml index 2bf43d1f5b25..fa9f7d12dc98 100644 --- a/.github/workflows/build-macos-installer.yml +++ b/.github/workflows/build-macos-installer.yml @@ -137,6 +137,7 @@ jobs: - name: Build MacOS DMG env: + CHIA_INSTALLER_VERSION: ${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }} NOTARIZE: ${{ steps.check_secrets.outputs.HAS_APPLE_SECRET }} APPLE_NOTARIZE_USERNAME: "${{ secrets.APPLE_NOTARIZE_USERNAME }}" APPLE_NOTARIZE_PASSWORD: "${{ secrets.APPLE_NOTARIZE_PASSWORD }}" diff --git a/.github/workflows/build-macos-m1-installer.yml b/.github/workflows/build-macos-m1-installer.yml index 9c728c395c2d..323e636311cf 100644 --- a/.github/workflows/build-macos-m1-installer.yml +++ b/.github/workflows/build-macos-m1-installer.yml @@ -110,6 +110,7 @@ jobs: - name: Build MacOS DMG env: + CHIA_INSTALLER_VERSION: ${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }} NOTARIZE: ${{ steps.check_secrets.outputs.HAS_APPLE_SECRET }} APPLE_NOTARIZE_USERNAME: "${{ secrets.APPLE_NOTARIZE_USERNAME }}" APPLE_NOTARIZE_PASSWORD: "${{ secrets.APPLE_NOTARIZE_PASSWORD }}" diff --git a/.github/workflows/build-windows-installer.yml b/.github/workflows/build-windows-installer.yml index 7fbd981c03d5..ae2161f0b3b3 100644 --- a/.github/workflows/build-windows-installer.yml +++ b/.github/workflows/build-windows-installer.yml @@ -145,6 +145,7 @@ jobs: - name: Build Windows installer with build_scripts\build_windows.ps1 env: + CHIA_INSTALLER_VERSION: ${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }} WIN_CODE_SIGN_PASS: ${{ secrets.WIN_CODE_SIGN_PASS }} HAS_SECRET: ${{ steps.check_secrets.outputs.HAS_SIGNING_SECRET }} run: | diff --git a/build_scripts/build_linux_deb.sh b/build_scripts/build_linux_deb.sh index b60b6744ac46..ea493bafe525 100644 --- a/build_scripts/build_linux_deb.sh +++ b/build_scripts/build_linux_deb.sh @@ -13,11 +13,8 @@ else DIR_NAME="chia-blockchain-linux-arm64" fi -pip install setuptools_scm -# The environment variable CHIA_INSTALLER_VERSION needs to be defined # If the env variable NOTARIZE and the username and password variables are # set, this will attempt to Notarize the signed DMG -CHIA_INSTALLER_VERSION=$(python installer-version.py) if [ ! "$CHIA_INSTALLER_VERSION" ]; then echo "WARNING: No environment variable CHIA_INSTALLER_VERSION set. Using 0.0.0." diff --git a/build_scripts/build_linux_rpm.sh b/build_scripts/build_linux_rpm.sh index e8fa595fc032..7ec656eeef8e 100644 --- a/build_scripts/build_linux_rpm.sh +++ b/build_scripts/build_linux_rpm.sh @@ -14,11 +14,8 @@ else DIR_NAME="chia-blockchain-linux-arm64" fi -pip install setuptools_scm -# The environment variable CHIA_INSTALLER_VERSION needs to be defined # If the env variable NOTARIZE and the username and password variables are # set, this will attempt to Notarize the signed DMG -CHIA_INSTALLER_VERSION=$(python installer-version.py) if [ ! "$CHIA_INSTALLER_VERSION" ]; then echo "WARNING: No environment variable CHIA_INSTALLER_VERSION set. Using 0.0.0." diff --git a/build_scripts/build_macos.sh b/build_scripts/build_macos.sh index d1831e285f5f..2cacc4d9fb3c 100644 --- a/build_scripts/build_macos.sh +++ b/build_scripts/build_macos.sh @@ -2,11 +2,8 @@ set -o errexit -o nounset -pip install setuptools_scm -# The environment variable CHIA_INSTALLER_VERSION needs to be defined. # If the env variable NOTARIZE and the username and password variables are # set, this will attempt to Notarize the signed DMG. -CHIA_INSTALLER_VERSION=$(python installer-version.py) if [ ! "$CHIA_INSTALLER_VERSION" ]; then echo "WARNING: No environment variable CHIA_INSTALLER_VERSION set. Using 0.0.0." diff --git a/build_scripts/build_macos_m1.sh b/build_scripts/build_macos_m1.sh index 8cb006e7b4e1..45f063373ba9 100644 --- a/build_scripts/build_macos_m1.sh +++ b/build_scripts/build_macos_m1.sh @@ -2,11 +2,8 @@ set -o errexit -o nounset -pip install setuptools_scm -# The environment variable CHIA_INSTALLER_VERSION needs to be defined. # If the env variable NOTARIZE and the username and password variables are # set, this will attempt to Notarize the signed DMG. -CHIA_INSTALLER_VERSION=$(python installer-version.py) if [ ! "$CHIA_INSTALLER_VERSION" ]; then echo "WARNING: No environment variable CHIA_INSTALLER_VERSION set. Using 0.0.0." diff --git a/build_scripts/build_windows.ps1 b/build_scripts/build_windows.ps1 index 52a2a14b63be..38498d42b543 100644 --- a/build_scripts/build_windows.ps1 +++ b/build_scripts/build_windows.ps1 @@ -32,12 +32,9 @@ python -m pip install --upgrade pip pip install wheel pep517 pip install pywin32 pip install pyinstaller==4.9 -pip install setuptools_scm Write-Output " ---" -Write-Output "Get CHIA_INSTALLER_VERSION" # The environment variable CHIA_INSTALLER_VERSION needs to be defined -$env:CHIA_INSTALLER_VERSION = python .\build_scripts\installer-version.py -win if (-not (Test-Path env:CHIA_INSTALLER_VERSION)) { $env:CHIA_INSTALLER_VERSION = '0.0.0' From 5dea4a238e1912f4ea298de54257611f089bf546 Mon Sep 17 00:00:00 2001 From: William Blanke Date: Fri, 8 Apr 2022 14:11:04 -0700 Subject: [PATCH 354/378] updated gui to 5f8b23fc7deb0b07f665c075ed491059f4d8b95c --- chia-blockchain-gui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia-blockchain-gui b/chia-blockchain-gui index d714c21b4ee3..5f8b23fc7deb 160000 --- a/chia-blockchain-gui +++ b/chia-blockchain-gui @@ -1 +1 @@ -Subproject commit d714c21b4ee3ebbc7d18b5f819772cd9868d0bf5 +Subproject commit 5f8b23fc7deb0b07f665c075ed491059f4d8b95c From 930a4b682558e934a2ed41b48b515b8a5a08087b Mon Sep 17 00:00:00 2001 From: William Blanke Date: Fri, 8 Apr 2022 15:03:13 -0700 Subject: [PATCH 355/378] updated gui to fccbd3e10d27673e39c01f0f89e47b5455b8331a --- chia-blockchain-gui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia-blockchain-gui b/chia-blockchain-gui index 5f8b23fc7deb..fccbd3e10d27 160000 --- a/chia-blockchain-gui +++ b/chia-blockchain-gui @@ -1 +1 @@ -Subproject commit 5f8b23fc7deb0b07f665c075ed491059f4d8b95c +Subproject commit fccbd3e10d27673e39c01f0f89e47b5455b8331a From a48fd431009ff0cd4874e4da99bc3071f45564da Mon Sep 17 00:00:00 2001 From: dustinface <35775977+xdustinface@users.noreply.github.com> Date: Sat, 9 Apr 2022 03:29:32 +0200 Subject: [PATCH 356/378] streamable: Simplify and force correct usage (#10509) * streamable: Merge `strictdataclass` into `Streamable` class * tests: Test not supported streamable types * streamable: Reorder decorators * streamable: Simplify streamable decorator and force correct usage/syntax * streamable: Just move some stuff around in the file * streamable: Improve syntax error messages * mypy: Drop `type_checking.py` and `test_type_checking.py` from exclusion * streamable: Use cached fields instead of `__annotations__` This is now possible after merging `__post_init__` into `Streamable` * Introduce `DefinitionError` as `StreamableError` * `/t` -> ` ` --- benchmarks/streamable.py | 6 +- chia/clvm/spend_sim.py | 6 +- chia/consensus/block_record.py | 2 +- chia/consensus/cost_calculator.py | 2 +- chia/consensus/multiprocess_validation.py | 2 +- chia/full_node/block_height_map.py | 2 +- chia/full_node/full_node_store.py | 2 +- chia/full_node/signage_point.py | 2 +- chia/plotting/manager.py | 4 +- chia/pools/pool_config.py | 2 +- chia/pools/pool_wallet_info.py | 4 +- chia/protocols/farmer_protocol.py | 10 +- chia/protocols/full_node_protocol.py | 50 ++-- chia/protocols/harvester_protocol.py | 32 +-- chia/protocols/introducer_protocol.py | 4 +- chia/protocols/pool_protocol.py | 26 +- chia/protocols/shared_protocol.py | 2 +- chia/protocols/timelord_protocol.py | 14 +- chia/protocols/wallet_protocol.py | 58 ++-- chia/seeder/peer_record.py | 2 +- chia/server/address_manager_store.py | 2 +- chia/server/outbound_message.py | 2 +- chia/simulator/simulator_protocol.py | 4 +- chia/timelord/timelord.py | 2 +- chia/types/blockchain_format/classgroup.py | 2 +- chia/types/blockchain_format/coin.py | 2 +- chia/types/blockchain_format/foliage.py | 8 +- chia/types/blockchain_format/pool_target.py | 2 +- .../types/blockchain_format/proof_of_space.py | 2 +- .../blockchain_format/reward_chain_block.py | 4 +- chia/types/blockchain_format/slots.py | 10 +- .../blockchain_format/sub_epoch_summary.py | 2 +- chia/types/blockchain_format/vdf.py | 4 +- chia/types/coin_record.py | 2 +- chia/types/coin_spend.py | 2 +- chia/types/condition_with_args.py | 2 +- chia/types/end_of_slot_bundle.py | 2 +- chia/types/full_block.py | 2 +- chia/types/generator_types.py | 2 +- chia/types/header_block.py | 2 +- chia/types/mempool_item.py | 2 +- chia/types/peer_info.py | 4 +- chia/types/spend_bundle.py | 2 +- chia/types/spend_bundle_conditions.py | 4 +- chia/types/unfinished_block.py | 2 +- chia/types/unfinished_header_block.py | 2 +- chia/types/weight_proof.py | 14 +- chia/util/streamable.py | 264 ++++++++++++------ chia/util/type_checking.py | 103 ------- chia/wallet/block_record.py | 2 +- chia/wallet/cat_wallet/cat_info.py | 4 +- chia/wallet/did_wallet/did_info.py | 2 +- chia/wallet/lineage_proof.py | 2 +- chia/wallet/rl_wallet/rl_wallet.py | 2 +- chia/wallet/settings/settings_objects.py | 2 +- chia/wallet/trade_record.py | 2 +- chia/wallet/transaction_record.py | 2 +- chia/wallet/wallet_info.py | 4 +- mypy.ini | 2 +- tests/core/util/test_streamable.py | 206 ++++++++++++-- tests/core/util/test_type_checking.py | 91 ------ 61 files changed, 549 insertions(+), 461 deletions(-) delete mode 100644 chia/util/type_checking.py delete mode 100644 tests/core/util/test_type_checking.py diff --git a/benchmarks/streamable.py b/benchmarks/streamable.py index 47e77d224a06..d940a421a659 100644 --- a/benchmarks/streamable.py +++ b/benchmarks/streamable.py @@ -17,14 +17,14 @@ _version = 1 -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class BenchmarkInner(Streamable): a: str -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class BenchmarkMiddle(Streamable): a: uint64 b: List[bytes32] @@ -33,8 +33,8 @@ class BenchmarkMiddle(Streamable): e: BenchmarkInner -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class BenchmarkClass(Streamable): a: Optional[BenchmarkMiddle] b: Optional[BenchmarkMiddle] diff --git a/chia/clvm/spend_sim.py b/chia/clvm/spend_sim.py index b448966e4ace..d143b6ed4f8a 100644 --- a/chia/clvm/spend_sim.py +++ b/chia/clvm/spend_sim.py @@ -38,15 +38,15 @@ """ -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class SimFullBlock(Streamable): transactions_generator: Optional[BlockGenerator] height: uint32 # Note that height is not on a regular FullBlock -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class SimBlockRecord(Streamable): reward_claims_incorporated: List[Coin] height: uint32 @@ -69,8 +69,8 @@ def create(cls, rci: List[Coin], height: uint32, timestamp: uint64): ) -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class SimStore(Streamable): timestamp: uint64 block_height: uint32 diff --git a/chia/consensus/block_record.py b/chia/consensus/block_record.py index 520a9497b1e9..a6ccfc4f7cf3 100644 --- a/chia/consensus/block_record.py +++ b/chia/consensus/block_record.py @@ -11,8 +11,8 @@ from chia.util.streamable import Streamable, streamable -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class BlockRecord(Streamable): """ This class is not included or hashed into the blockchain, but it is kept in memory as a more diff --git a/chia/consensus/cost_calculator.py b/chia/consensus/cost_calculator.py index c207ccf56123..34577a82cb8f 100644 --- a/chia/consensus/cost_calculator.py +++ b/chia/consensus/cost_calculator.py @@ -6,8 +6,8 @@ from chia.util.streamable import Streamable, streamable -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class NPCResult(Streamable): error: Optional[uint16] conds: Optional[SpendBundleConditions] diff --git a/chia/consensus/multiprocess_validation.py b/chia/consensus/multiprocess_validation.py index d908ab5808a7..6cf3d302815b 100644 --- a/chia/consensus/multiprocess_validation.py +++ b/chia/consensus/multiprocess_validation.py @@ -35,8 +35,8 @@ log = logging.getLogger(__name__) -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class PreValidationResult(Streamable): error: Optional[uint16] required_iters: Optional[uint64] # Iff error is None diff --git a/chia/full_node/block_height_map.py b/chia/full_node/block_height_map.py index 49e60df16461..321225d7a4e3 100644 --- a/chia/full_node/block_height_map.py +++ b/chia/full_node/block_height_map.py @@ -13,8 +13,8 @@ log = logging.getLogger(__name__) -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class SesCache(Streamable): content: List[Tuple[uint32, bytes]] diff --git a/chia/full_node/full_node_store.py b/chia/full_node/full_node_store.py index 728266998790..0626e8e1a1e5 100644 --- a/chia/full_node/full_node_store.py +++ b/chia/full_node/full_node_store.py @@ -29,8 +29,8 @@ log = logging.getLogger(__name__) -@dataclasses.dataclass(frozen=True) @streamable +@dataclasses.dataclass(frozen=True) class FullNodeStorePeakResult(Streamable): added_eos: Optional[EndOfSubSlotBundle] new_signage_points: List[Tuple[uint8, SignagePoint]] diff --git a/chia/full_node/signage_point.py b/chia/full_node/signage_point.py index be79026fd5c6..9230d6f3f3a1 100644 --- a/chia/full_node/signage_point.py +++ b/chia/full_node/signage_point.py @@ -5,8 +5,8 @@ from chia.util.streamable import Streamable, streamable -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class SignagePoint(Streamable): cc_vdf: Optional[VDFInfo] cc_proof: Optional[VDFProof] diff --git a/chia/plotting/manager.py b/chia/plotting/manager.py index 7f48cbfc304e..24ec531e9286 100644 --- a/chia/plotting/manager.py +++ b/chia/plotting/manager.py @@ -32,16 +32,16 @@ CURRENT_VERSION: uint16 = uint16(0) -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class CacheEntry(Streamable): pool_public_key: Optional[G1Element] pool_contract_puzzle_hash: Optional[bytes32] plot_public_key: G1Element -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class DiskCache(Streamable): version: uint16 data: List[Tuple[bytes32, CacheEntry]] diff --git a/chia/pools/pool_config.py b/chia/pools/pool_config.py index ffa07e962e17..cc4519169ae0 100644 --- a/chia/pools/pool_config.py +++ b/chia/pools/pool_config.py @@ -25,8 +25,8 @@ log = logging.getLogger(__name__) -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class PoolWalletConfig(Streamable): launcher_id: bytes32 pool_url: str diff --git a/chia/pools/pool_wallet_info.py b/chia/pools/pool_wallet_info.py index 42c5e4aebc9c..3fef250952e2 100644 --- a/chia/pools/pool_wallet_info.py +++ b/chia/pools/pool_wallet_info.py @@ -38,8 +38,8 @@ class PoolSingletonState(IntEnum): FARMING_TO_POOL = PoolSingletonState.FARMING_TO_POOL -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class PoolState(Streamable): """ `PoolState` is a type that is serialized to the blockchain to track the state of the user's pool singleton @@ -97,8 +97,8 @@ def create_pool_state( return ps -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class PoolWalletInfo(Streamable): """ Internal Pool Wallet state, not destined for the blockchain. This can be completely derived with diff --git a/chia/protocols/farmer_protocol.py b/chia/protocols/farmer_protocol.py index 1d2c4f062e78..e60a421d16d3 100644 --- a/chia/protocols/farmer_protocol.py +++ b/chia/protocols/farmer_protocol.py @@ -15,8 +15,8 @@ """ -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class NewSignagePoint(Streamable): challenge_hash: bytes32 challenge_chain_sp: bytes32 @@ -26,8 +26,8 @@ class NewSignagePoint(Streamable): signage_point_index: uint8 -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class DeclareProofOfSpace(Streamable): challenge_hash: bytes32 challenge_chain_sp: bytes32 @@ -41,16 +41,16 @@ class DeclareProofOfSpace(Streamable): pool_signature: Optional[G2Element] -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RequestSignedValues(Streamable): quality_string: bytes32 foliage_block_data_hash: bytes32 foliage_transaction_block_hash: bytes32 -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class FarmingInfo(Streamable): challenge_hash: bytes32 sp_hash: bytes32 @@ -60,8 +60,8 @@ class FarmingInfo(Streamable): total_plots: uint32 -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class SignedValues(Streamable): quality_string: bytes32 foliage_block_data_signature: G2Element diff --git a/chia/protocols/full_node_protocol.py b/chia/protocols/full_node_protocol.py index cbafdad00cc8..793dabbe9415 100644 --- a/chia/protocols/full_node_protocol.py +++ b/chia/protocols/full_node_protocol.py @@ -18,8 +18,8 @@ """ -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class NewPeak(Streamable): header_hash: bytes32 height: uint32 @@ -28,102 +28,102 @@ class NewPeak(Streamable): unfinished_reward_block_hash: bytes32 -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class NewTransaction(Streamable): transaction_id: bytes32 cost: uint64 fees: uint64 -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RequestTransaction(Streamable): transaction_id: bytes32 -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RespondTransaction(Streamable): transaction: SpendBundle -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RequestProofOfWeight(Streamable): total_number_of_blocks: uint32 tip: bytes32 -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RespondProofOfWeight(Streamable): wp: WeightProof tip: bytes32 -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RequestBlock(Streamable): height: uint32 include_transaction_block: bool -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RejectBlock(Streamable): height: uint32 -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RequestBlocks(Streamable): start_height: uint32 end_height: uint32 include_transaction_block: bool -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RespondBlocks(Streamable): start_height: uint32 end_height: uint32 blocks: List[FullBlock] -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RejectBlocks(Streamable): start_height: uint32 end_height: uint32 -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RespondBlock(Streamable): block: FullBlock -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class NewUnfinishedBlock(Streamable): unfinished_reward_hash: bytes32 -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RequestUnfinishedBlock(Streamable): unfinished_reward_hash: bytes32 -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RespondUnfinishedBlock(Streamable): unfinished_block: UnfinishedBlock -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class NewSignagePointOrEndOfSubSlot(Streamable): prev_challenge_hash: Optional[bytes32] challenge_hash: bytes32 @@ -131,16 +131,16 @@ class NewSignagePointOrEndOfSubSlot(Streamable): last_rc_infusion: bytes32 -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RequestSignagePointOrEndOfSubSlot(Streamable): challenge_hash: bytes32 index_from_challenge: uint8 last_rc_infusion: bytes32 -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RespondSignagePoint(Streamable): index_from_challenge: uint8 challenge_chain_vdf: VDFInfo @@ -149,20 +149,20 @@ class RespondSignagePoint(Streamable): reward_chain_proof: VDFProof -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RespondEndOfSubSlot(Streamable): end_of_slot_bundle: EndOfSubSlotBundle -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RequestMempoolTransactions(Streamable): filter: bytes -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class NewCompactVDF(Streamable): height: uint32 header_hash: bytes32 @@ -170,8 +170,8 @@ class NewCompactVDF(Streamable): vdf_info: VDFInfo -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RequestCompactVDF(Streamable): height: uint32 header_hash: bytes32 @@ -179,8 +179,8 @@ class RequestCompactVDF(Streamable): vdf_info: VDFInfo -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RespondCompactVDF(Streamable): height: uint32 header_hash: bytes32 @@ -189,15 +189,15 @@ class RespondCompactVDF(Streamable): vdf_proof: VDFProof -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RequestPeers(Streamable): """ Return full list of peers """ -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RespondPeers(Streamable): peer_list: List[TimestampedPeerInfo] diff --git a/chia/protocols/harvester_protocol.py b/chia/protocols/harvester_protocol.py index 4c5cddc1459c..d3323a48a0a9 100644 --- a/chia/protocols/harvester_protocol.py +++ b/chia/protocols/harvester_protocol.py @@ -14,23 +14,23 @@ """ -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class PoolDifficulty(Streamable): difficulty: uint64 sub_slot_iters: uint64 pool_contract_puzzle_hash: bytes32 -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class HarvesterHandshake(Streamable): farmer_public_keys: List[G1Element] pool_public_keys: List[G1Element] -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class NewSignagePointHarvester(Streamable): challenge_hash: bytes32 difficulty: uint64 @@ -40,8 +40,8 @@ class NewSignagePointHarvester(Streamable): pool_difficulties: List[PoolDifficulty] -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class NewProofOfSpace(Streamable): challenge_hash: bytes32 sp_hash: bytes32 @@ -50,8 +50,8 @@ class NewProofOfSpace(Streamable): signage_point_index: uint8 -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RequestSignatures(Streamable): plot_identifier: str challenge_hash: bytes32 @@ -59,8 +59,8 @@ class RequestSignatures(Streamable): messages: List[bytes32] -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RespondSignatures(Streamable): plot_identifier: str challenge_hash: bytes32 @@ -70,8 +70,8 @@ class RespondSignatures(Streamable): message_signatures: List[Tuple[bytes32, G2Element]] -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class Plot(Streamable): filename: str size: uint8 @@ -83,30 +83,30 @@ class Plot(Streamable): time_modified: uint64 -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RequestPlots(Streamable): pass -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RespondPlots(Streamable): plots: List[Plot] failed_to_open_filenames: List[str] no_key_filenames: List[str] -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class PlotSyncIdentifier(Streamable): timestamp: uint64 sync_id: uint64 message_id: uint64 -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class PlotSyncStart(Streamable): identifier: PlotSyncIdentifier initial: bool @@ -120,8 +120,8 @@ def __str__(self) -> str: ) -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class PlotSyncPathList(Streamable): identifier: PlotSyncIdentifier data: List[str] @@ -131,8 +131,8 @@ def __str__(self) -> str: return f"PlotSyncPathList: identifier {self.identifier}, count {len(self.data)}, final {self.final}" -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class PlotSyncPlotList(Streamable): identifier: PlotSyncIdentifier data: List[Plot] @@ -142,8 +142,8 @@ def __str__(self) -> str: return f"PlotSyncPlotList: identifier {self.identifier}, count {len(self.data)}, final {self.final}" -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class PlotSyncDone(Streamable): identifier: PlotSyncIdentifier duration: uint64 @@ -152,8 +152,8 @@ def __str__(self) -> str: return f"PlotSyncDone: identifier {self.identifier}, duration {self.duration}" -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class PlotSyncError(Streamable): code: int16 message: str @@ -163,8 +163,8 @@ def __str__(self) -> str: return f"PlotSyncError: code {self.code}, count {self.message}, expected_identifier {self.expected_identifier}" -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class PlotSyncResponse(Streamable): identifier: PlotSyncIdentifier message_type: int16 diff --git a/chia/protocols/introducer_protocol.py b/chia/protocols/introducer_protocol.py index 7eadacb21462..e9eeab997833 100644 --- a/chia/protocols/introducer_protocol.py +++ b/chia/protocols/introducer_protocol.py @@ -10,15 +10,15 @@ """ -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RequestPeersIntroducer(Streamable): """ Return full list of peers """ -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RespondPeersIntroducer(Streamable): peer_list: List[TimestampedPeerInfo] diff --git a/chia/protocols/pool_protocol.py b/chia/protocols/pool_protocol.py index e6a1f7f8ff15..3b8678ec90a1 100644 --- a/chia/protocols/pool_protocol.py +++ b/chia/protocols/pool_protocol.py @@ -33,8 +33,8 @@ class PoolErrorCode(Enum): # Used to verify GET /farmer and GET /login -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class AuthenticationPayload(Streamable): method_name: str launcher_id: bytes32 @@ -43,8 +43,8 @@ class AuthenticationPayload(Streamable): # GET /pool_info -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class GetPoolInfoResponse(Streamable): name: str logo_url: str @@ -60,8 +60,8 @@ class GetPoolInfoResponse(Streamable): # POST /partial -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class PostPartialPayload(Streamable): launcher_id: bytes32 authentication_token: uint64 @@ -71,16 +71,16 @@ class PostPartialPayload(Streamable): harvester_id: bytes32 -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class PostPartialRequest(Streamable): payload: PostPartialPayload aggregate_signature: G2Element # Response in success case -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class PostPartialResponse(Streamable): new_difficulty: uint64 @@ -89,8 +89,8 @@ class PostPartialResponse(Streamable): # Response in success case -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class GetFarmerResponse(Streamable): authentication_public_key: G1Element payout_instructions: str @@ -101,8 +101,8 @@ class GetFarmerResponse(Streamable): # POST /farmer -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class PostFarmerPayload(Streamable): launcher_id: bytes32 authentication_token: uint64 @@ -111,16 +111,16 @@ class PostFarmerPayload(Streamable): suggested_difficulty: Optional[uint64] -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class PostFarmerRequest(Streamable): payload: PostFarmerPayload signature: G2Element # Response in success case -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class PostFarmerResponse(Streamable): welcome_message: str @@ -128,8 +128,8 @@ class PostFarmerResponse(Streamable): # PUT /farmer -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class PutFarmerPayload(Streamable): launcher_id: bytes32 authentication_token: uint64 @@ -138,16 +138,16 @@ class PutFarmerPayload(Streamable): suggested_difficulty: Optional[uint64] -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class PutFarmerRequest(Streamable): payload: PutFarmerPayload signature: G2Element # Response in success case -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class PutFarmerResponse(Streamable): authentication_public_key: Optional[bool] payout_instructions: Optional[bool] @@ -158,8 +158,8 @@ class PutFarmerResponse(Streamable): # Response in error case for all endpoints of the pool protocol -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class ErrorResponse(Streamable): error_code: uint16 error_message: Optional[str] diff --git a/chia/protocols/shared_protocol.py b/chia/protocols/shared_protocol.py index ed1cc9e7d680..5b0f608f54af 100644 --- a/chia/protocols/shared_protocol.py +++ b/chia/protocols/shared_protocol.py @@ -19,8 +19,8 @@ class Capability(IntEnum): BASE = 1 # Base capability just means it supports the chia protocol at mainnet -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class Handshake(Streamable): network_id: str protocol_version: str diff --git a/chia/protocols/timelord_protocol.py b/chia/protocols/timelord_protocol.py index 282bb09bfbd6..6db1c32e8ddc 100644 --- a/chia/protocols/timelord_protocol.py +++ b/chia/protocols/timelord_protocol.py @@ -16,8 +16,8 @@ """ -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class NewPeakTimelord(Streamable): reward_chain_block: RewardChainBlock difficulty: uint64 @@ -31,8 +31,8 @@ class NewPeakTimelord(Streamable): passes_ses_height_but_not_yet_included: bool -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class NewUnfinishedBlockTimelord(Streamable): reward_chain_block: RewardChainBlockUnfinished # Reward chain trunk data difficulty: uint64 @@ -44,8 +44,8 @@ class NewUnfinishedBlockTimelord(Streamable): rc_prev: bytes32 -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class NewInfusionPointVDF(Streamable): unfinished_reward_hash: bytes32 challenge_chain_ip_vdf: VDFInfo @@ -56,8 +56,8 @@ class NewInfusionPointVDF(Streamable): infused_challenge_chain_ip_proof: Optional[VDFProof] -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class NewSignagePointVDF(Streamable): index_from_challenge: uint8 challenge_chain_sp_vdf: VDFInfo @@ -66,14 +66,14 @@ class NewSignagePointVDF(Streamable): reward_chain_sp_proof: VDFProof -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class NewEndOfSubSlotVDF(Streamable): end_of_sub_slot_bundle: EndOfSubSlotBundle -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RequestCompactProofOfTime(Streamable): new_proof_of_time: VDFInfo header_hash: bytes32 @@ -81,8 +81,8 @@ class RequestCompactProofOfTime(Streamable): field_vdf: uint8 -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RespondCompactProofOfTime(Streamable): vdf_info: VDFInfo vdf_proof: VDFProof diff --git a/chia/protocols/wallet_protocol.py b/chia/protocols/wallet_protocol.py index efb0650aa774..a96edca4be47 100644 --- a/chia/protocols/wallet_protocol.py +++ b/chia/protocols/wallet_protocol.py @@ -15,15 +15,15 @@ """ -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RequestPuzzleSolution(Streamable): coin_name: bytes32 height: uint32 -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class PuzzleSolutionResponse(Streamable): coin_name: bytes32 height: uint32 @@ -31,35 +31,35 @@ class PuzzleSolutionResponse(Streamable): solution: Program -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RespondPuzzleSolution(Streamable): response: PuzzleSolutionResponse -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RejectPuzzleSolution(Streamable): coin_name: bytes32 height: uint32 -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class SendTransaction(Streamable): transaction: SpendBundle -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class TransactionAck(Streamable): txid: bytes32 status: uint8 # MempoolInclusionStatus error: Optional[str] -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class NewPeakWallet(Streamable): header_hash: bytes32 height: uint32 @@ -67,34 +67,34 @@ class NewPeakWallet(Streamable): fork_point_with_previous_peak: uint32 -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RequestBlockHeader(Streamable): height: uint32 -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RespondBlockHeader(Streamable): header_block: HeaderBlock -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RejectHeaderRequest(Streamable): height: uint32 -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RequestRemovals(Streamable): height: uint32 header_hash: bytes32 coin_names: Optional[List[bytes32]] -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RespondRemovals(Streamable): height: uint32 header_hash: bytes32 @@ -102,23 +102,23 @@ class RespondRemovals(Streamable): proofs: Optional[List[Tuple[bytes32, bytes]]] -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RejectRemovalsRequest(Streamable): height: uint32 header_hash: bytes32 -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RequestAdditions(Streamable): height: uint32 header_hash: Optional[bytes32] puzzle_hashes: Optional[List[bytes32]] -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RespondAdditions(Streamable): height: uint32 header_hash: bytes32 @@ -126,75 +126,75 @@ class RespondAdditions(Streamable): proofs: Optional[List[Tuple[bytes32, bytes, Optional[bytes]]]] -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RejectAdditionsRequest(Streamable): height: uint32 header_hash: bytes32 -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RequestHeaderBlocks(Streamable): start_height: uint32 end_height: uint32 -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RejectHeaderBlocks(Streamable): start_height: uint32 end_height: uint32 -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RespondHeaderBlocks(Streamable): start_height: uint32 end_height: uint32 header_blocks: List[HeaderBlock] -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class CoinState(Streamable): coin: Coin spent_height: Optional[uint32] created_height: Optional[uint32] -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RegisterForPhUpdates(Streamable): puzzle_hashes: List[bytes32] min_height: uint32 -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RespondToPhUpdates(Streamable): puzzle_hashes: List[bytes32] min_height: uint32 coin_states: List[CoinState] -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RegisterForCoinUpdates(Streamable): coin_ids: List[bytes32] min_height: uint32 -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RespondToCoinUpdates(Streamable): coin_ids: List[bytes32] min_height: uint32 coin_states: List[CoinState] -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class CoinStateUpdate(Streamable): height: uint32 fork_height: uint32 @@ -202,27 +202,27 @@ class CoinStateUpdate(Streamable): items: List[CoinState] -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RequestChildren(Streamable): coin_name: bytes32 -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RespondChildren(Streamable): coin_states: List[CoinState] -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RequestSESInfo(Streamable): start_height: uint32 end_height: uint32 -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RespondSESInfo(Streamable): reward_chain_hash: List[bytes32] heights: List[List[uint32]] diff --git a/chia/seeder/peer_record.py b/chia/seeder/peer_record.py index d307c4065a88..2a6f8dde481c 100644 --- a/chia/seeder/peer_record.py +++ b/chia/seeder/peer_record.py @@ -6,8 +6,8 @@ from chia.util.streamable import Streamable, streamable -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class PeerRecord(Streamable): peer_id: str ip_address: str diff --git a/chia/server/address_manager_store.py b/chia/server/address_manager_store.py index 3f725cb3dd04..25069133c31b 100644 --- a/chia/server/address_manager_store.py +++ b/chia/server/address_manager_store.py @@ -21,8 +21,8 @@ log = logging.getLogger(__name__) -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class PeerDataSerialization(Streamable): """ Serializable property bag for the peer data that was previously stored in sqlite. diff --git a/chia/server/outbound_message.py b/chia/server/outbound_message.py index 66861b413afa..ba55ca7b8b29 100644 --- a/chia/server/outbound_message.py +++ b/chia/server/outbound_message.py @@ -31,8 +31,8 @@ class Delivery(IntEnum): SPECIFIC = 6 -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class Message(Streamable): type: uint8 # one of ProtocolMessageTypes # message id diff --git a/chia/simulator/simulator_protocol.py b/chia/simulator/simulator_protocol.py index f512ab04c3a2..29d8492d17a5 100644 --- a/chia/simulator/simulator_protocol.py +++ b/chia/simulator/simulator_protocol.py @@ -5,14 +5,14 @@ from chia.util.streamable import Streamable, streamable -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class FarmNewBlockProtocol(Streamable): puzzle_hash: bytes32 -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class ReorgProtocol(Streamable): old_index: uint32 new_index: uint32 diff --git a/chia/timelord/timelord.py b/chia/timelord/timelord.py index e3fd5ae58a1b..456b6641d15a 100644 --- a/chia/timelord/timelord.py +++ b/chia/timelord/timelord.py @@ -41,8 +41,8 @@ log = logging.getLogger(__name__) -@dataclasses.dataclass(frozen=True) @streamable +@dataclasses.dataclass(frozen=True) class BlueboxProcessData(Streamable): challenge: bytes32 size_bits: uint16 diff --git a/chia/types/blockchain_format/classgroup.py b/chia/types/blockchain_format/classgroup.py index a3bf22ad9301..10a83e93e934 100644 --- a/chia/types/blockchain_format/classgroup.py +++ b/chia/types/blockchain_format/classgroup.py @@ -5,8 +5,8 @@ from chia.util.streamable import Streamable, streamable -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class ClassgroupElement(Streamable): """ Represents a classgroup element (a,b,c) where a, b, and c are 512 bit signed integers. However this is using diff --git a/chia/types/blockchain_format/coin.py b/chia/types/blockchain_format/coin.py index 1ba4e0a04b48..0709f62c621b 100644 --- a/chia/types/blockchain_format/coin.py +++ b/chia/types/blockchain_format/coin.py @@ -9,8 +9,8 @@ from chia.util.streamable import Streamable, streamable -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class Coin(Streamable): """ This structure is used in the body for the reward and fees genesis coins. diff --git a/chia/types/blockchain_format/foliage.py b/chia/types/blockchain_format/foliage.py index 043f361b35d4..412e40ba3956 100644 --- a/chia/types/blockchain_format/foliage.py +++ b/chia/types/blockchain_format/foliage.py @@ -10,8 +10,8 @@ from chia.util.streamable import Streamable, streamable -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class TransactionsInfo(Streamable): # Information that goes along with each transaction block generator_root: bytes32 # sha256 of the block generator in this block @@ -22,8 +22,8 @@ class TransactionsInfo(Streamable): reward_claims_incorporated: List[Coin] # These can be in any order -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class FoliageTransactionBlock(Streamable): # Information that goes along with each transaction block that is relevant for light clients prev_transaction_block_hash: bytes32 @@ -34,8 +34,8 @@ class FoliageTransactionBlock(Streamable): transactions_info_hash: bytes32 -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class FoliageBlockData(Streamable): # Part of the block that is signed by the plot key unfinished_reward_block_hash: bytes32 @@ -45,8 +45,8 @@ class FoliageBlockData(Streamable): extension_data: bytes32 # Used for future updates. Can be any 32 byte value initially -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class Foliage(Streamable): # The entire foliage block, containing signature and the unsigned back pointer # The hash of this is the "header hash". Note that for unfinished blocks, the prev_block_hash diff --git a/chia/types/blockchain_format/pool_target.py b/chia/types/blockchain_format/pool_target.py index 57659c9f634e..6d9b126d76dd 100644 --- a/chia/types/blockchain_format/pool_target.py +++ b/chia/types/blockchain_format/pool_target.py @@ -5,8 +5,8 @@ from chia.util.streamable import Streamable, streamable -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class PoolTarget(Streamable): puzzle_hash: bytes32 max_height: uint32 # A max height of 0 means it is valid forever diff --git a/chia/types/blockchain_format/proof_of_space.py b/chia/types/blockchain_format/proof_of_space.py index 72d90a7a9f6b..f9d1fc8bdbba 100644 --- a/chia/types/blockchain_format/proof_of_space.py +++ b/chia/types/blockchain_format/proof_of_space.py @@ -15,8 +15,8 @@ log = logging.getLogger(__name__) -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class ProofOfSpace(Streamable): challenge: bytes32 pool_public_key: Optional[G1Element] # Only one of these two should be present diff --git a/chia/types/blockchain_format/reward_chain_block.py b/chia/types/blockchain_format/reward_chain_block.py index 9bcb4bf59707..515d032c09aa 100644 --- a/chia/types/blockchain_format/reward_chain_block.py +++ b/chia/types/blockchain_format/reward_chain_block.py @@ -10,8 +10,8 @@ from chia.util.streamable import Streamable, streamable -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RewardChainBlockUnfinished(Streamable): total_iters: uint128 signage_point_index: uint8 @@ -23,8 +23,8 @@ class RewardChainBlockUnfinished(Streamable): reward_chain_sp_signature: G2Element -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RewardChainBlock(Streamable): weight: uint128 height: uint32 diff --git a/chia/types/blockchain_format/slots.py b/chia/types/blockchain_format/slots.py index 0f55073f5d47..a230dcf0432e 100644 --- a/chia/types/blockchain_format/slots.py +++ b/chia/types/blockchain_format/slots.py @@ -10,8 +10,8 @@ from chia.util.streamable import Streamable, streamable -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class ChallengeBlockInfo(Streamable): # The hash of this is used as the challenge_hash for the ICC VDF proof_of_space: ProofOfSpace challenge_chain_sp_vdf: Optional[VDFInfo] # Only present if not the first sp @@ -19,8 +19,8 @@ class ChallengeBlockInfo(Streamable): # The hash of this is used as the challen challenge_chain_ip_vdf: VDFInfo -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class ChallengeChainSubSlot(Streamable): challenge_chain_end_of_slot_vdf: VDFInfo infused_challenge_chain_sub_slot_hash: Optional[bytes32] # Only at the end of a slot @@ -29,14 +29,14 @@ class ChallengeChainSubSlot(Streamable): new_difficulty: Optional[uint64] # Only at the end of epoch, sub-epoch, and slot -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class InfusedChallengeChainSubSlot(Streamable): infused_challenge_chain_end_of_slot_vdf: VDFInfo -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RewardChainSubSlot(Streamable): end_of_slot_vdf: VDFInfo challenge_chain_sub_slot_hash: bytes32 @@ -44,8 +44,8 @@ class RewardChainSubSlot(Streamable): deficit: uint8 # 16 or less. usually zero -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class SubSlotProofs(Streamable): challenge_chain_slot_proof: VDFProof infused_challenge_chain_slot_proof: Optional[VDFProof] diff --git a/chia/types/blockchain_format/sub_epoch_summary.py b/chia/types/blockchain_format/sub_epoch_summary.py index 419182374349..6a1fc89fa697 100644 --- a/chia/types/blockchain_format/sub_epoch_summary.py +++ b/chia/types/blockchain_format/sub_epoch_summary.py @@ -6,8 +6,8 @@ from chia.util.streamable import Streamable, streamable -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class SubEpochSummary(Streamable): prev_subepoch_summary_hash: bytes32 reward_chain_hash: bytes32 # hash of reward chain at end of last segment diff --git a/chia/types/blockchain_format/vdf.py b/chia/types/blockchain_format/vdf.py index 4d417502cc70..55b47595b978 100644 --- a/chia/types/blockchain_format/vdf.py +++ b/chia/types/blockchain_format/vdf.py @@ -44,16 +44,16 @@ def verify_vdf( ) -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class VDFInfo(Streamable): challenge: bytes32 # Used to generate the discriminant (VDF group) number_of_iterations: uint64 output: ClassgroupElement -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class VDFProof(Streamable): witness_type: uint8 witness: bytes diff --git a/chia/types/coin_record.py b/chia/types/coin_record.py index 85579e687d26..b195ad3de085 100644 --- a/chia/types/coin_record.py +++ b/chia/types/coin_record.py @@ -8,8 +8,8 @@ from chia.util.streamable import Streamable, streamable -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class CoinRecord(Streamable): """ These are values that correspond to a CoinName that are used diff --git a/chia/types/coin_spend.py b/chia/types/coin_spend.py index b37a61ac0889..895fd761c3dc 100644 --- a/chia/types/coin_spend.py +++ b/chia/types/coin_spend.py @@ -7,8 +7,8 @@ from chia.util.streamable import Streamable, streamable -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class CoinSpend(Streamable): """ This is a rather disparate data structure that validates coin transfers. It's generally populated diff --git a/chia/types/condition_with_args.py b/chia/types/condition_with_args.py index 2222baa3e3d5..5f0dbce11fd9 100644 --- a/chia/types/condition_with_args.py +++ b/chia/types/condition_with_args.py @@ -5,8 +5,8 @@ from chia.util.streamable import Streamable, streamable -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class ConditionWithArgs(Streamable): """ This structure is used to store parsed CLVM conditions diff --git a/chia/types/end_of_slot_bundle.py b/chia/types/end_of_slot_bundle.py index 0e7292b61b67..72d708f5e7a4 100644 --- a/chia/types/end_of_slot_bundle.py +++ b/chia/types/end_of_slot_bundle.py @@ -10,8 +10,8 @@ from chia.util.streamable import Streamable, streamable -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class EndOfSubSlotBundle(Streamable): challenge_chain: ChallengeChainSubSlot infused_challenge_chain: Optional[InfusedChallengeChainSubSlot] diff --git a/chia/types/full_block.py b/chia/types/full_block.py index 25a0772d6290..086fb6ea7628 100644 --- a/chia/types/full_block.py +++ b/chia/types/full_block.py @@ -11,8 +11,8 @@ from chia.util.streamable import Streamable, streamable -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class FullBlock(Streamable): # All the information required to validate a block finished_sub_slots: List[EndOfSubSlotBundle] # If first sb diff --git a/chia/types/generator_types.py b/chia/types/generator_types.py index 060f3d85d987..7b86520bf3f3 100644 --- a/chia/types/generator_types.py +++ b/chia/types/generator_types.py @@ -21,8 +21,8 @@ class CompressorArg: end: int -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class BlockGenerator(Streamable): program: SerializedProgram generator_refs: List[SerializedProgram] diff --git a/chia/types/header_block.py b/chia/types/header_block.py index afc5c5a50450..e0b44abd2442 100644 --- a/chia/types/header_block.py +++ b/chia/types/header_block.py @@ -8,8 +8,8 @@ from chia.util.streamable import Streamable, streamable -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class HeaderBlock(Streamable): # Same as a FullBlock but without TransactionInfo and Generator (but with filter), used by light clients finished_sub_slots: List[EndOfSubSlotBundle] # If first sb diff --git a/chia/types/mempool_item.py b/chia/types/mempool_item.py index 9e89b86cc2e2..dfd588a7ed69 100644 --- a/chia/types/mempool_item.py +++ b/chia/types/mempool_item.py @@ -10,8 +10,8 @@ from chia.util.streamable import Streamable, streamable -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class MempoolItem(Streamable): spend_bundle: SpendBundle fee: uint64 diff --git a/chia/types/peer_info.py b/chia/types/peer_info.py index 4404b1fc8561..7435cb180951 100644 --- a/chia/types/peer_info.py +++ b/chia/types/peer_info.py @@ -6,8 +6,8 @@ from chia.util.streamable import Streamable, streamable -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class PeerInfo(Streamable): host: str port: uint16 @@ -59,8 +59,8 @@ def get_group(self): return group -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class TimestampedPeerInfo(Streamable): host: str port: uint16 diff --git a/chia/types/spend_bundle.py b/chia/types/spend_bundle.py index f8e9977cce2f..5c003838a0d0 100644 --- a/chia/types/spend_bundle.py +++ b/chia/types/spend_bundle.py @@ -15,8 +15,8 @@ from .coin_spend import CoinSpend -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class SpendBundle(Streamable): """ This is a list of coins being spent along with their solution programs, and a single diff --git a/chia/types/spend_bundle_conditions.py b/chia/types/spend_bundle_conditions.py index 3bae9b34d239..01758503053a 100644 --- a/chia/types/spend_bundle_conditions.py +++ b/chia/types/spend_bundle_conditions.py @@ -8,8 +8,8 @@ # the Spend and SpendBundleConditions classes are mirrors of native types, returned by # run_generator -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class Spend(Streamable): coin_id: bytes32 puzzle_hash: bytes32 @@ -19,8 +19,8 @@ class Spend(Streamable): agg_sig_me: List[Tuple[bytes48, bytes]] -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class SpendBundleConditions(Streamable): spends: List[Spend] reserve_fee: uint64 diff --git a/chia/types/unfinished_block.py b/chia/types/unfinished_block.py index 491ab364f2c9..da110a66c116 100644 --- a/chia/types/unfinished_block.py +++ b/chia/types/unfinished_block.py @@ -10,8 +10,8 @@ from chia.util.streamable import Streamable, streamable -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class UnfinishedBlock(Streamable): # Full block, without the final VDFs finished_sub_slots: List[EndOfSubSlotBundle] # If first sb diff --git a/chia/types/unfinished_header_block.py b/chia/types/unfinished_header_block.py index 39db047c1eb4..da6f3b436adc 100644 --- a/chia/types/unfinished_header_block.py +++ b/chia/types/unfinished_header_block.py @@ -8,8 +8,8 @@ from chia.util.streamable import Streamable, streamable -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class UnfinishedHeaderBlock(Streamable): # Same as a FullBlock but without TransactionInfo and Generator, used by light clients finished_sub_slots: List[EndOfSubSlotBundle] # If first sb diff --git a/chia/types/weight_proof.py b/chia/types/weight_proof.py index bb958040eaaa..47f5b9118777 100644 --- a/chia/types/weight_proof.py +++ b/chia/types/weight_proof.py @@ -11,8 +11,8 @@ from chia.util.streamable import Streamable, streamable -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class SubEpochData(Streamable): reward_chain_hash: bytes32 num_blocks_overflow: uint8 @@ -31,8 +31,8 @@ class SubEpochData(Streamable): # total number of challenge blocks == total number of reward chain blocks -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class SubSlotData(Streamable): # if infused proof_of_space: Optional[ProofOfSpace] @@ -65,37 +65,37 @@ def is_end_of_slot(self) -> bool: return False -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class SubEpochChallengeSegment(Streamable): sub_epoch_n: uint32 sub_slots: List[SubSlotData] rc_slot_end_info: Optional[VDFInfo] # in first segment of each sub_epoch -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) # this is used only for serialization to database class SubEpochSegments(Streamable): challenge_segments: List[SubEpochChallengeSegment] -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) # this is used only for serialization to database class RecentChainData(Streamable): recent_chain_data: List[HeaderBlock] -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class ProofBlockHeader(Streamable): finished_sub_slots: List[EndOfSubSlotBundle] reward_chain_block: RewardChainBlock -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class WeightProof(Streamable): sub_epochs: List[SubEpochData] sub_epoch_segments: List[SubEpochChallengeSegment] # sampled sub epoch diff --git a/chia/util/streamable.py b/chia/util/streamable.py index 2602709600be..cf545fd4833d 100644 --- a/chia/util/streamable.py +++ b/chia/util/streamable.py @@ -5,7 +5,7 @@ import pprint import sys from enum import Enum -from typing import Any, BinaryIO, Dict, get_type_hints, List, Tuple, Type, TypeVar, Callable, Optional, Iterator +from typing import Any, BinaryIO, Dict, get_type_hints, List, Tuple, Type, TypeVar, Union, Callable, Optional, Iterator from blspy import G1Element, G2Element, PrivateKey from typing_extensions import Literal @@ -14,20 +14,31 @@ from chia.util.byte_types import hexstr_to_bytes from chia.util.hash import std_hash from chia.util.ints import int64, int512, uint32, uint64, uint128 -from chia.util.type_checking import is_type_List, is_type_SpecificOptional, is_type_Tuple, strictdataclass if sys.version_info < (3, 8): def get_args(t: Type[Any]) -> Tuple[Any, ...]: return getattr(t, "__args__", ()) + def get_origin(t: Type[Any]) -> Optional[Type[Any]]: + return getattr(t, "__origin__", None) + else: - from typing import get_args + from typing import get_args, get_origin pp = pprint.PrettyPrinter(indent=1, width=120, compact=True) + +class StreamableError(Exception): + pass + + +class DefinitionError(StreamableError): + pass + + # TODO: Remove hack, this allows streaming these objects from binary size_hints = { "PrivateKey": PrivateKey.PRIVATE_KEY_SIZE, @@ -48,6 +59,27 @@ def get_args(t: Type[Any]) -> Tuple[Any, ...]: _T_Streamable = TypeVar("_T_Streamable", bound="Streamable") +# Caches to store the fields and (de)serialization methods for all available streamable classes. +FIELDS_FOR_STREAMABLE_CLASS = {} +STREAM_FUNCTIONS_FOR_STREAMABLE_CLASS = {} +PARSE_FUNCTIONS_FOR_STREAMABLE_CLASS = {} + + +def is_type_List(f_type: Type) -> bool: + return get_origin(f_type) == list or f_type == list + + +def is_type_SpecificOptional(f_type) -> bool: + """ + Returns true for types such as Optional[T], but not Optional, or T. + """ + return get_origin(f_type) == Union and get_args(f_type)[1]() is None + + +def is_type_Tuple(f_type: Type) -> bool: + return get_origin(f_type) == tuple or f_type == tuple + + def dataclass_from_dict(klass, d): """ Converts a dictionary based on a dataclass, into an instance of that dataclass. @@ -124,81 +156,6 @@ def recurse_jsonify(d): return d -STREAM_FUNCTIONS_FOR_STREAMABLE_CLASS = {} -PARSE_FUNCTIONS_FOR_STREAMABLE_CLASS = {} -FIELDS_FOR_STREAMABLE_CLASS = {} - - -def streamable(cls: Any): - """ - This is a decorator for class definitions. It applies the strictdataclass decorator, - which checks all types at construction. It also defines a simple serialization format, - and adds parse, from bytes, stream, and __bytes__ methods. - - The primitives are: - * Sized ints serialized in big endian format, e.g. uint64 - * Sized bytes serialized in big endian format, e.g. bytes32 - * BLS public keys serialized in bls format (48 bytes) - * BLS signatures serialized in bls format (96 bytes) - * bool serialized into 1 byte (0x01 or 0x00) - * bytes serialized as a 4 byte size prefix and then the bytes. - * ConditionOpcode is serialized as a 1 byte value. - * str serialized as a 4 byte size prefix and then the utf-8 representation in bytes. - - An item is one of: - * primitive - * Tuple[item1, .. itemx] - * List[item1, .. itemx] - * Optional[item] - * Custom item - - A streamable must be a Tuple at the root level (although a dataclass is used here instead). - Iters are serialized in the following way: - - 1. A tuple of x items is serialized by appending the serialization of each item. - 2. A List is serialized into a 4 byte size prefix (number of items) and the serialization of each item. - 3. An Optional is serialized into a 1 byte prefix of 0x00 or 0x01, and if it's one, it's followed by the - serialization of the item. - 4. A Custom item is serialized by calling the .parse method, passing in the stream of bytes into it. An example is - a CLVM program. - - All of the constituents must have parse/from_bytes, and stream/__bytes__ and therefore - be of fixed size. For example, int cannot be a constituent since it is not a fixed size, - whereas uint32 can be. - - Furthermore, a get_hash() member is added, which performs a serialization and a sha256. - - This class is used for deterministic serialization and hashing, for consensus critical - objects such as the block header. - - Make sure to use the Streamable class as a parent class when using the streamable decorator, - as it will allow linters to recognize the methods that are added by the decorator. Also, - use the @dataclass(frozen=True) decorator as well, for linters to recognize constructor - arguments. - """ - - cls1 = strictdataclass(cls) - t = type(cls.__name__, (cls1, Streamable), {}) - - stream_functions = [] - parse_functions = [] - try: - hints = get_type_hints(t) - fields = {field.name: hints.get(field.name, field.type) for field in dataclasses.fields(t)} - except Exception: - fields = {} - - FIELDS_FOR_STREAMABLE_CLASS[t] = fields - - for _, f_type in fields.items(): - stream_functions.append(cls.function_to_stream_one_item(f_type)) - parse_functions.append(cls.function_to_parse_one_item(f_type)) - - STREAM_FUNCTIONS_FOR_STREAMABLE_CLASS[t] = stream_functions - PARSE_FUNCTIONS_FOR_STREAMABLE_CLASS[t] = parse_functions - return t - - def parse_bool(f: BinaryIO) -> bool: bool_byte = f.read(1) assert bool_byte is not None and len(bool_byte) == 1 # Checks for EOF @@ -298,7 +255,158 @@ def stream_str(item: Any, f: BinaryIO) -> None: f.write(str_bytes) +def streamable(cls: Any): + """ + This decorator forces correct streamable protocol syntax/usage and populates the caches for types hints and + (de)serialization methods for all members of the class. The correct usage is: + + @streamable + @dataclass(frozen=True) + class Example(Streamable): + ... + + The order how the decorator are applied and the inheritance from Streamable are forced. The explicit inheritance is + required because mypy doesn't analyse the type returned by decorators, so we can't just inherit from inside the + decorator. The dataclass decorator is required to fetch type hints, let mypy validate constructor calls and restrict + direct modification of objects by `frozen=True`. + """ + + correct_usage_string: str = ( + "Correct usage is:\n\n@streamable\n@dataclass(frozen=True)\nclass Example(Streamable):\n ..." + ) + + if not dataclasses.is_dataclass(cls): + raise DefinitionError(f"@dataclass(frozen=True) required first. {correct_usage_string}") + + try: + object.__new__(cls)._streamable_test_if_dataclass_frozen_ = None + except dataclasses.FrozenInstanceError: + pass + else: + raise DefinitionError(f"dataclass needs to be frozen. {correct_usage_string}") + + if not issubclass(cls, Streamable): + raise DefinitionError(f"Streamable inheritance required. {correct_usage_string}") + + stream_functions = [] + parse_functions = [] + try: + hints = get_type_hints(cls) + fields = {field.name: hints.get(field.name, field.type) for field in dataclasses.fields(cls)} + except Exception: + fields = {} + + FIELDS_FOR_STREAMABLE_CLASS[cls] = fields + + for _, f_type in fields.items(): + stream_functions.append(cls.function_to_stream_one_item(f_type)) + parse_functions.append(cls.function_to_parse_one_item(f_type)) + + STREAM_FUNCTIONS_FOR_STREAMABLE_CLASS[cls] = stream_functions + PARSE_FUNCTIONS_FOR_STREAMABLE_CLASS[cls] = parse_functions + return cls + + class Streamable: + """ + This class defines a simple serialization format, and adds methods to parse from/to bytes and json. It also + validates and parses all fields at construction in ´__post_init__` to make sure all fields have the correct type + and can be streamed/parsed properly. + + The available primitives are: + * Sized ints serialized in big endian format, e.g. uint64 + * Sized bytes serialized in big endian format, e.g. bytes32 + * BLS public keys serialized in bls format (48 bytes) + * BLS signatures serialized in bls format (96 bytes) + * bool serialized into 1 byte (0x01 or 0x00) + * bytes serialized as a 4 byte size prefix and then the bytes. + * ConditionOpcode is serialized as a 1 byte value. + * str serialized as a 4 byte size prefix and then the utf-8 representation in bytes. + + An item is one of: + * primitive + * Tuple[item1, .. itemx] + * List[item1, .. itemx] + * Optional[item] + * Custom item + + A streamable must be a Tuple at the root level (although a dataclass is used here instead). + Iters are serialized in the following way: + + 1. A tuple of x items is serialized by appending the serialization of each item. + 2. A List is serialized into a 4 byte size prefix (number of items) and the serialization of each item. + 3. An Optional is serialized into a 1 byte prefix of 0x00 or 0x01, and if it's one, it's followed by the + serialization of the item. + 4. A Custom item is serialized by calling the .parse method, passing in the stream of bytes into it. An example is + a CLVM program. + + All of the constituents must have parse/from_bytes, and stream/__bytes__ and therefore + be of fixed size. For example, int cannot be a constituent since it is not a fixed size, + whereas uint32 can be. + + Furthermore, a get_hash() member is added, which performs a serialization and a sha256. + + This class is used for deterministic serialization and hashing, for consensus critical + objects such as the block header. + + Make sure to use the streamable decorator when inheriting from the Streamable class to prepare the streaming caches. + """ + + def post_init_parse(self, item: Any, f_name: str, f_type: Type) -> Any: + if is_type_List(f_type): + collected_list: List = [] + inner_type: Type = get_args(f_type)[0] + # wjb assert inner_type != get_args(List)[0] # type: ignore + if not is_type_List(type(item)): + raise ValueError(f"Wrong type for {f_name}, need a list.") + for el in item: + collected_list.append(self.post_init_parse(el, f_name, inner_type)) + return collected_list + if is_type_SpecificOptional(f_type): + if item is None: + return None + else: + inner_type: Type = get_args(f_type)[0] # type: ignore + return self.post_init_parse(item, f_name, inner_type) + if is_type_Tuple(f_type): + collected_list = [] + if not is_type_Tuple(type(item)) and not is_type_List(type(item)): + raise ValueError(f"Wrong type for {f_name}, need a tuple.") + if len(item) != len(get_args(f_type)): + raise ValueError(f"Wrong number of elements in tuple {f_name}.") + for i in range(len(item)): + inner_type = get_args(f_type)[i] + tuple_item = item[i] + collected_list.append(self.post_init_parse(tuple_item, f_name, inner_type)) + return tuple(collected_list) + if not isinstance(item, f_type): + try: + item = f_type(item) + except (TypeError, AttributeError, ValueError): + try: + item = f_type.from_bytes(item) + except Exception: + item = f_type.from_bytes(bytes(item)) + if not isinstance(item, f_type): + raise ValueError(f"Wrong type for {f_name}") + return item + + def __post_init__(self): + try: + fields = FIELDS_FOR_STREAMABLE_CLASS[type(self)] + except Exception: + fields = {} + data = self.__dict__ + for (f_name, f_type) in fields.items(): + if f_name not in data: + raise ValueError(f"Field {f_name} not present") + try: + if not isinstance(data[f_name], f_type): + object.__setattr__(self, f_name, self.post_init_parse(data[f_name], f_name, f_type)) + except TypeError: + # Throws a TypeError because we cannot call isinstance for subscripted generics like Optional[int] + object.__setattr__(self, f_name, self.post_init_parse(data[f_name], f_name, f_type)) + @classmethod def function_to_parse_one_item(cls, f_type: Type) -> Callable[[BinaryIO], Any]: """ diff --git a/chia/util/type_checking.py b/chia/util/type_checking.py deleted file mode 100644 index 8f9adf38e50d..000000000000 --- a/chia/util/type_checking.py +++ /dev/null @@ -1,103 +0,0 @@ -import dataclasses -import sys -from typing import Any, List, Optional, Tuple, Type, Union - -if sys.version_info < (3, 8): - - def get_args(t: Type[Any]) -> Tuple[Any, ...]: - return getattr(t, "__args__", ()) - - def get_origin(t: Type[Any]) -> Optional[Type[Any]]: - return getattr(t, "__origin__", None) - -else: - - from typing import get_args, get_origin - - -def is_type_List(f_type: Type) -> bool: - return get_origin(f_type) == list or f_type == list - - -def is_type_SpecificOptional(f_type) -> bool: - """ - Returns true for types such as Optional[T], but not Optional, or T. - """ - return get_origin(f_type) == Union and get_args(f_type)[1]() is None - - -def is_type_Tuple(f_type: Type) -> bool: - return get_origin(f_type) == tuple or f_type == tuple - - -def strictdataclass(cls: Any): - class _Local: - """ - Dataclass where all fields must be type annotated, and type checking is performed - at initialization, even recursively through Lists. Non-annotated fields are ignored. - Also, for any fields which have a type with .from_bytes(bytes) or constructor(bytes), - bytes can be passed in and the type can be constructed. - """ - - def parse_item(self, item: Any, f_name: str, f_type: Type) -> Any: - if is_type_List(f_type): - collected_list: List = [] - inner_type: Type = get_args(f_type)[0] - # wjb assert inner_type != get_args(List)[0] # type: ignore - if not is_type_List(type(item)): - raise ValueError(f"Wrong type for {f_name}, need a list.") - for el in item: - collected_list.append(self.parse_item(el, f_name, inner_type)) - return collected_list - if is_type_SpecificOptional(f_type): - if item is None: - return None - else: - inner_type: Type = get_args(f_type)[0] # type: ignore - return self.parse_item(item, f_name, inner_type) - if is_type_Tuple(f_type): - collected_list = [] - if not is_type_Tuple(type(item)) and not is_type_List(type(item)): - raise ValueError(f"Wrong type for {f_name}, need a tuple.") - if len(item) != len(get_args(f_type)): - raise ValueError(f"Wrong number of elements in tuple {f_name}.") - for i in range(len(item)): - inner_type = get_args(f_type)[i] - tuple_item = item[i] - collected_list.append(self.parse_item(tuple_item, f_name, inner_type)) - return tuple(collected_list) - if not isinstance(item, f_type): - try: - item = f_type(item) - except (TypeError, AttributeError, ValueError): - try: - item = f_type.from_bytes(item) - except Exception: - item = f_type.from_bytes(bytes(item)) - if not isinstance(item, f_type): - raise ValueError(f"Wrong type for {f_name}") - return item - - def __post_init__(self): - try: - fields = self.__annotations__ # pylint: disable=no-member - except Exception: - fields = {} - data = self.__dict__ - for (f_name, f_type) in fields.items(): - if f_name not in data: - raise ValueError(f"Field {f_name} not present") - try: - if not isinstance(data[f_name], f_type): - object.__setattr__(self, f_name, self.parse_item(data[f_name], f_name, f_type)) - except TypeError: - # Throws a TypeError because we cannot call isinstance for subscripted generics like Optional[int] - object.__setattr__(self, f_name, self.parse_item(data[f_name], f_name, f_type)) - - class NoTypeChecking: - __no_type_check__ = True - - cls1 = dataclasses.dataclass(cls, init=False, frozen=True) # type: ignore - if dataclasses.fields(cls1) == (): - return type(cls.__name__, (cls1, _Local, NoTypeChecking), {}) - return type(cls.__name__, (cls1, _Local), {}) diff --git a/chia/wallet/block_record.py b/chia/wallet/block_record.py index 81cc7c57c00f..8324592523db 100644 --- a/chia/wallet/block_record.py +++ b/chia/wallet/block_record.py @@ -6,8 +6,8 @@ from chia.util.streamable import Streamable, streamable -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class HeaderBlockRecord(Streamable): """ These are values that are stored in the wallet database, corresponding to information diff --git a/chia/wallet/cat_wallet/cat_info.py b/chia/wallet/cat_wallet/cat_info.py index 9e2d6eadf5cf..78c9fec5ed1a 100644 --- a/chia/wallet/cat_wallet/cat_info.py +++ b/chia/wallet/cat_wallet/cat_info.py @@ -7,8 +7,8 @@ from chia.util.streamable import Streamable, streamable -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class CATInfo(Streamable): limitations_program_hash: bytes32 my_tail: Optional[Program] # this is the program @@ -16,8 +16,8 @@ class CATInfo(Streamable): # We used to store all of the lineage proofs here but it was very slow to serialize for a lot of transactions # so we moved it to CATLineageStore. We keep this around for migration purposes. -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class LegacyCATInfo(Streamable): limitations_program_hash: bytes32 my_tail: Optional[Program] # this is the program diff --git a/chia/wallet/did_wallet/did_info.py b/chia/wallet/did_wallet/did_info.py index 5b57402afa6b..d4a83a77e42c 100644 --- a/chia/wallet/did_wallet/did_info.py +++ b/chia/wallet/did_wallet/did_info.py @@ -9,8 +9,8 @@ from chia.types.blockchain_format.coin import Coin -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class DIDInfo(Streamable): origin_coin: Optional[Coin] # Coin ID of this coin is our DID backup_ids: List[bytes] diff --git a/chia/wallet/lineage_proof.py b/chia/wallet/lineage_proof.py index 177f2d127434..ec5d3119d378 100644 --- a/chia/wallet/lineage_proof.py +++ b/chia/wallet/lineage_proof.py @@ -7,8 +7,8 @@ from chia.util.streamable import Streamable, streamable -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class LineageProof(Streamable): parent_name: Optional[bytes32] = None inner_puzzle_hash: Optional[bytes32] = None diff --git a/chia/wallet/rl_wallet/rl_wallet.py b/chia/wallet/rl_wallet/rl_wallet.py index e51fe841c7e8..3db219e1b15f 100644 --- a/chia/wallet/rl_wallet/rl_wallet.py +++ b/chia/wallet/rl_wallet/rl_wallet.py @@ -34,8 +34,8 @@ from chia.wallet.wallet_info import WalletInfo -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class RLInfo(Streamable): type: str admin_pubkey: Optional[bytes] diff --git a/chia/wallet/settings/settings_objects.py b/chia/wallet/settings/settings_objects.py index 9878a2c4eee1..27f6f92c6070 100644 --- a/chia/wallet/settings/settings_objects.py +++ b/chia/wallet/settings/settings_objects.py @@ -3,8 +3,8 @@ from chia.util.streamable import Streamable, streamable -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class BackupInitialized(Streamable): """ Stores user decision regarding import of backup info diff --git a/chia/wallet/trade_record.py b/chia/wallet/trade_record.py index 278181b81d85..08c56bb4bb60 100644 --- a/chia/wallet/trade_record.py +++ b/chia/wallet/trade_record.py @@ -9,8 +9,8 @@ from chia.wallet.trading.trade_status import TradeStatus -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class TradeRecord(Streamable): """ Used for storing transaction data and status in wallets. diff --git a/chia/wallet/transaction_record.py b/chia/wallet/transaction_record.py index f43907f1ba25..b13b6d366010 100644 --- a/chia/wallet/transaction_record.py +++ b/chia/wallet/transaction_record.py @@ -12,8 +12,8 @@ from chia.wallet.util.transaction_type import TransactionType -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class TransactionRecord(Streamable): """ Used for storing transaction data and status in wallets. diff --git a/chia/wallet/wallet_info.py b/chia/wallet/wallet_info.py index 4567540f6df2..1d02bb6e7276 100644 --- a/chia/wallet/wallet_info.py +++ b/chia/wallet/wallet_info.py @@ -5,8 +5,8 @@ from chia.util.streamable import Streamable, streamable -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class WalletInfo(Streamable): """ This object represents the wallet data as it is stored in DB. @@ -24,8 +24,8 @@ class WalletInfo(Streamable): data: str -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class WalletInfoBackup(Streamable): """ Used for transforming list of WalletInfo objects into bytes. diff --git a/mypy.ini b/mypy.ini index 4c3283cc77d9..4c3f96cbfd0f 100644 --- a/mypy.ini +++ b/mypy.ini @@ -17,7 +17,7 @@ no_implicit_reexport = True strict_equality = True # list created by: venv/bin/mypy | sed -n 's/.py:.*//p' | sort | uniq | tr '/' '.' | tr '\n' ',' -[mypy-benchmarks.block_ref,benchmarks.block_store,benchmarks.coin_store,benchmarks.utils,build_scripts.installer-version,chia.clvm.spend_sim,chia.cmds.configure,chia.cmds.db,chia.cmds.db_upgrade_func,chia.cmds.farm_funcs,chia.cmds.init,chia.cmds.init_funcs,chia.cmds.keys,chia.cmds.keys_funcs,chia.cmds.passphrase,chia.cmds.passphrase_funcs,chia.cmds.plotnft,chia.cmds.plotnft_funcs,chia.cmds.plots,chia.cmds.plotters,chia.cmds.show,chia.cmds.start_funcs,chia.cmds.wallet,chia.cmds.wallet_funcs,chia.consensus.block_body_validation,chia.consensus.blockchain,chia.consensus.blockchain_interface,chia.consensus.block_creation,chia.consensus.block_header_validation,chia.consensus.block_record,chia.consensus.block_root_validation,chia.consensus.coinbase,chia.consensus.constants,chia.consensus.difficulty_adjustment,chia.consensus.get_block_challenge,chia.consensus.multiprocess_validation,chia.consensus.pos_quality,chia.consensus.vdf_info_computation,chia.daemon.client,chia.daemon.keychain_proxy,chia.daemon.keychain_server,chia.daemon.server,chia.farmer.farmer,chia.farmer.farmer_api,chia.full_node.block_height_map,chia.full_node.block_store,chia.full_node.bundle_tools,chia.full_node.coin_store,chia.full_node.full_node,chia.full_node.full_node_api,chia.full_node.full_node_store,chia.full_node.generator,chia.full_node.hint_store,chia.full_node.lock_queue,chia.full_node.mempool,chia.full_node.mempool_check_conditions,chia.full_node.mempool_manager,chia.full_node.pending_tx_cache,chia.full_node.sync_store,chia.full_node.weight_proof,chia.harvester.harvester,chia.harvester.harvester_api,chia.introducer.introducer,chia.introducer.introducer_api,chia.plotters.bladebit,chia.plotters.chiapos,chia.plotters.install_plotter,chia.plotters.madmax,chia.plotters.plotters,chia.plotters.plotters_util,chia.plotting.check_plots,chia.plotting.create_plots,chia.plotting.manager,chia.plotting.util,chia.pools.pool_config,chia.pools.pool_puzzles,chia.pools.pool_wallet,chia.pools.pool_wallet_info,chia.protocols.pool_protocol,chia.rpc.crawler_rpc_api,chia.rpc.farmer_rpc_api,chia.rpc.farmer_rpc_client,chia.rpc.full_node_rpc_api,chia.rpc.full_node_rpc_client,chia.rpc.harvester_rpc_api,chia.rpc.harvester_rpc_client,chia.rpc.rpc_client,chia.rpc.rpc_server,chia.rpc.timelord_rpc_api,chia.rpc.util,chia.rpc.wallet_rpc_api,chia.rpc.wallet_rpc_client,chia.seeder.crawler,chia.seeder.crawler_api,chia.seeder.crawl_store,chia.seeder.dns_server,chia.seeder.peer_record,chia.seeder.start_crawler,chia.server.address_manager,chia.server.address_manager_store,chia.server.connection_utils,chia.server.introducer_peers,chia.server.node_discovery,chia.server.peer_store_resolver,chia.server.rate_limits,chia.server.reconnect_task,chia.server.server,chia.server.ssl_context,chia.server.start_farmer,chia.server.start_full_node,chia.server.start_harvester,chia.server.start_introducer,chia.server.start_service,chia.server.start_timelord,chia.server.start_wallet,chia.server.upnp,chia.server.ws_connection,chia.simulator.full_node_simulator,chia.simulator.start_simulator,chia.ssl.create_ssl,chia.timelord.iters_from_block,chia.timelord.timelord,chia.timelord.timelord_api,chia.timelord.timelord_launcher,chia.timelord.timelord_state,chia.types.announcement,chia.types.blockchain_format.classgroup,chia.types.blockchain_format.coin,chia.types.blockchain_format.program,chia.types.blockchain_format.proof_of_space,chia.types.blockchain_format.tree_hash,chia.types.blockchain_format.vdf,chia.types.full_block,chia.types.header_block,chia.types.mempool_item,chia.types.name_puzzle_condition,chia.types.peer_info,chia.types.spend_bundle,chia.types.transaction_queue_entry,chia.types.unfinished_block,chia.types.unfinished_header_block,chia.util.api_decorators,chia.util.block_cache,chia.util.byte_types,chia.util.cached_bls,chia.util.check_fork_next_block,chia.util.chia_logging,chia.util.config,chia.util.db_wrapper,chia.util.dump_keyring,chia.util.file_keyring,chia.util.files,chia.util.hash,chia.util.ints,chia.util.json_util,chia.util.keychain,chia.util.keyring_wrapper,chia.util.log_exceptions,chia.util.lru_cache,chia.util.make_test_constants,chia.util.merkle_set,chia.util.network,chia.util.partial_func,chia.util.pip_import,chia.util.profiler,chia.util.safe_cancel_task,chia.util.service_groups,chia.util.ssl_check,chia.util.streamable,chia.util.struct_stream,chia.util.type_checking,chia.util.validate_alert,chia.wallet.block_record,chia.wallet.cat_wallet.cat_utils,chia.wallet.cat_wallet.cat_wallet,chia.wallet.cat_wallet.lineage_store,chia.wallet.chialisp,chia.wallet.did_wallet.did_wallet,chia.wallet.did_wallet.did_wallet_puzzles,chia.wallet.key_val_store,chia.wallet.lineage_proof,chia.wallet.payment,chia.wallet.puzzles.load_clvm,chia.wallet.puzzles.p2_conditions,chia.wallet.puzzles.p2_delegated_conditions,chia.wallet.puzzles.p2_delegated_puzzle,chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle,chia.wallet.puzzles.p2_m_of_n_delegate_direct,chia.wallet.puzzles.p2_puzzle_hash,chia.wallet.puzzles.prefarm.spend_prefarm,chia.wallet.puzzles.puzzle_utils,chia.wallet.puzzles.rom_bootstrap_generator,chia.wallet.puzzles.singleton_top_layer,chia.wallet.puzzles.tails,chia.wallet.rl_wallet.rl_wallet,chia.wallet.rl_wallet.rl_wallet_puzzles,chia.wallet.secret_key_store,chia.wallet.settings.user_settings,chia.wallet.trade_manager,chia.wallet.trade_record,chia.wallet.trading.offer,chia.wallet.trading.trade_store,chia.wallet.transaction_record,chia.wallet.util.debug_spend_bundle,chia.wallet.util.new_peak_queue,chia.wallet.util.peer_request_cache,chia.wallet.util.wallet_sync_utils,chia.wallet.wallet,chia.wallet.wallet_action_store,chia.wallet.wallet_blockchain,chia.wallet.wallet_coin_store,chia.wallet.wallet_interested_store,chia.wallet.wallet_node,chia.wallet.wallet_node_api,chia.wallet.wallet_pool_store,chia.wallet.wallet_puzzle_store,chia.wallet.wallet_state_manager,chia.wallet.wallet_sync_store,chia.wallet.wallet_transaction_store,chia.wallet.wallet_user_store,chia.wallet.wallet_weight_proof_handler,installhelper,tests.blockchain.blockchain_test_utils,tests.blockchain.test_blockchain,tests.blockchain.test_blockchain_transactions,tests.block_tools,tests.build-init-files,tests.build-workflows,tests.clvm.coin_store,tests.clvm.test_chialisp_deserialization,tests.clvm.test_clvm_compilation,tests.clvm.test_program,tests.clvm.test_puzzle_compression,tests.clvm.test_puzzles,tests.clvm.test_serialized_program,tests.clvm.test_singletons,tests.clvm.test_spend_sim,tests.conftest,tests.connection_utils,tests.core.cmds.test_keys,tests.core.consensus.test_pot_iterations,tests.core.custom_types.test_coin,tests.core.custom_types.test_proof_of_space,tests.core.custom_types.test_spend_bundle,tests.core.daemon.test_daemon,tests.core.full_node.full_sync.test_full_sync,tests.core.full_node.stores.test_block_store,tests.core.full_node.stores.test_coin_store,tests.core.full_node.stores.test_full_node_store,tests.core.full_node.stores.test_hint_store,tests.core.full_node.stores.test_sync_store,tests.core.full_node.test_address_manager,tests.core.full_node.test_block_height_map,tests.core.full_node.test_conditions,tests.core.full_node.test_full_node,tests.core.full_node.test_mempool,tests.core.full_node.test_mempool_performance,tests.core.full_node.test_node_load,tests.core.full_node.test_peer_store_resolver,tests.core.full_node.test_performance,tests.core.full_node.test_transactions,tests.core.make_block_generator,tests.core.node_height,tests.core.server.test_dos,tests.core.server.test_rate_limits,tests.core.ssl.test_ssl,tests.core.test_cost_calculation,tests.core.test_crawler_rpc,tests.core.test_daemon_rpc,tests.core.test_db_conversion,tests.core.test_farmer_harvester_rpc,tests.core.test_filter,tests.core.test_full_node_rpc,tests.core.test_merkle_set,tests.core.test_setproctitle,tests.core.util.test_cached_bls,tests.core.util.test_config,tests.core.util.test_file_keyring_synchronization,tests.core.util.test_files,tests.core.util.test_keychain,tests.core.util.test_keyring_wrapper,tests.core.util.test_lru_cache,tests.core.util.test_significant_bits,tests.core.util.test_streamable,tests.core.util.test_type_checking,tests.farmer_harvester.test_farmer_harvester,tests.generator.test_compression,tests.generator.test_generator_types,tests.generator.test_list_to_batches,tests.generator.test_rom,tests.generator.test_scan,tests.plotting.test_plot_manager,tests.pools.test_pool_cmdline,tests.pools.test_pool_config,tests.pools.test_pool_puzzles_lifecycle,tests.pools.test_pool_rpc,tests.pools.test_wallet_pool_store,tests.setup_nodes,tests.setup_services,tests.simulation.test_simulation,tests.time_out_assert,tests.tools.test_full_sync,tests.tools.test_run_block,tests.util.alert_server,tests.util.benchmark_cost,tests.util.blockchain,tests.util.build_network_protocol_files,tests.util.db_connection,tests.util.generator_tools_testing,tests.util.keyring,tests.util.key_tool,tests.util.misc,tests.util.network,tests.util.rpc,tests.util.test_full_block_utils,tests.util.test_lock_queue,tests.util.test_network_protocol_files,tests.util.test_struct_stream,tests.wallet.cat_wallet.test_cat_lifecycle,tests.wallet.cat_wallet.test_cat_wallet,tests.wallet.cat_wallet.test_offer_lifecycle,tests.wallet.cat_wallet.test_trades,tests.wallet.did_wallet.test_did,tests.wallet.did_wallet.test_did_rpc,tests.wallet.rl_wallet.test_rl_rpc,tests.wallet.rl_wallet.test_rl_wallet,tests.wallet.rpc.test_wallet_rpc,tests.wallet.simple_sync.test_simple_sync_protocol,tests.wallet.sync.test_wallet_sync,tests.wallet.test_bech32m,tests.wallet.test_chialisp,tests.wallet.test_puzzle_store,tests.wallet.test_singleton,tests.wallet.test_singleton_lifecycle,tests.wallet.test_singleton_lifecycle_fast,tests.wallet.test_taproot,tests.wallet.test_wallet,tests.wallet.test_wallet_blockchain,tests.wallet.test_wallet_interested_store,tests.wallet.test_wallet_key_val_store,tests.wallet.test_wallet_user_store,tests.wallet_tools,tests.weight_proof.test_weight_proof,tools.analyze-chain,tools.run_block,tools.test_full_sync] +[mypy-benchmarks.block_ref,benchmarks.block_store,benchmarks.coin_store,benchmarks.utils,build_scripts.installer-version,chia.clvm.spend_sim,chia.cmds.configure,chia.cmds.db,chia.cmds.db_upgrade_func,chia.cmds.farm_funcs,chia.cmds.init,chia.cmds.init_funcs,chia.cmds.keys,chia.cmds.keys_funcs,chia.cmds.passphrase,chia.cmds.passphrase_funcs,chia.cmds.plotnft,chia.cmds.plotnft_funcs,chia.cmds.plots,chia.cmds.plotters,chia.cmds.show,chia.cmds.start_funcs,chia.cmds.wallet,chia.cmds.wallet_funcs,chia.consensus.block_body_validation,chia.consensus.blockchain,chia.consensus.blockchain_interface,chia.consensus.block_creation,chia.consensus.block_header_validation,chia.consensus.block_record,chia.consensus.block_root_validation,chia.consensus.coinbase,chia.consensus.constants,chia.consensus.difficulty_adjustment,chia.consensus.get_block_challenge,chia.consensus.multiprocess_validation,chia.consensus.pos_quality,chia.consensus.vdf_info_computation,chia.daemon.client,chia.daemon.keychain_proxy,chia.daemon.keychain_server,chia.daemon.server,chia.farmer.farmer,chia.farmer.farmer_api,chia.full_node.block_height_map,chia.full_node.block_store,chia.full_node.bundle_tools,chia.full_node.coin_store,chia.full_node.full_node,chia.full_node.full_node_api,chia.full_node.full_node_store,chia.full_node.generator,chia.full_node.hint_store,chia.full_node.lock_queue,chia.full_node.mempool,chia.full_node.mempool_check_conditions,chia.full_node.mempool_manager,chia.full_node.pending_tx_cache,chia.full_node.sync_store,chia.full_node.weight_proof,chia.harvester.harvester,chia.harvester.harvester_api,chia.introducer.introducer,chia.introducer.introducer_api,chia.plotters.bladebit,chia.plotters.chiapos,chia.plotters.install_plotter,chia.plotters.madmax,chia.plotters.plotters,chia.plotters.plotters_util,chia.plotting.check_plots,chia.plotting.create_plots,chia.plotting.manager,chia.plotting.util,chia.pools.pool_config,chia.pools.pool_puzzles,chia.pools.pool_wallet,chia.pools.pool_wallet_info,chia.protocols.pool_protocol,chia.rpc.crawler_rpc_api,chia.rpc.farmer_rpc_api,chia.rpc.farmer_rpc_client,chia.rpc.full_node_rpc_api,chia.rpc.full_node_rpc_client,chia.rpc.harvester_rpc_api,chia.rpc.harvester_rpc_client,chia.rpc.rpc_client,chia.rpc.rpc_server,chia.rpc.timelord_rpc_api,chia.rpc.util,chia.rpc.wallet_rpc_api,chia.rpc.wallet_rpc_client,chia.seeder.crawler,chia.seeder.crawler_api,chia.seeder.crawl_store,chia.seeder.dns_server,chia.seeder.peer_record,chia.seeder.start_crawler,chia.server.address_manager,chia.server.address_manager_store,chia.server.connection_utils,chia.server.introducer_peers,chia.server.node_discovery,chia.server.peer_store_resolver,chia.server.rate_limits,chia.server.reconnect_task,chia.server.server,chia.server.ssl_context,chia.server.start_farmer,chia.server.start_full_node,chia.server.start_harvester,chia.server.start_introducer,chia.server.start_service,chia.server.start_timelord,chia.server.start_wallet,chia.server.upnp,chia.server.ws_connection,chia.simulator.full_node_simulator,chia.simulator.start_simulator,chia.ssl.create_ssl,chia.timelord.iters_from_block,chia.timelord.timelord,chia.timelord.timelord_api,chia.timelord.timelord_launcher,chia.timelord.timelord_state,chia.types.announcement,chia.types.blockchain_format.classgroup,chia.types.blockchain_format.coin,chia.types.blockchain_format.program,chia.types.blockchain_format.proof_of_space,chia.types.blockchain_format.tree_hash,chia.types.blockchain_format.vdf,chia.types.full_block,chia.types.header_block,chia.types.mempool_item,chia.types.name_puzzle_condition,chia.types.peer_info,chia.types.spend_bundle,chia.types.transaction_queue_entry,chia.types.unfinished_block,chia.types.unfinished_header_block,chia.util.api_decorators,chia.util.block_cache,chia.util.byte_types,chia.util.cached_bls,chia.util.check_fork_next_block,chia.util.chia_logging,chia.util.config,chia.util.db_wrapper,chia.util.dump_keyring,chia.util.file_keyring,chia.util.files,chia.util.hash,chia.util.ints,chia.util.json_util,chia.util.keychain,chia.util.keyring_wrapper,chia.util.log_exceptions,chia.util.lru_cache,chia.util.make_test_constants,chia.util.merkle_set,chia.util.network,chia.util.partial_func,chia.util.pip_import,chia.util.profiler,chia.util.safe_cancel_task,chia.util.service_groups,chia.util.ssl_check,chia.util.streamable,chia.util.struct_stream,chia.util.validate_alert,chia.wallet.block_record,chia.wallet.cat_wallet.cat_utils,chia.wallet.cat_wallet.cat_wallet,chia.wallet.cat_wallet.lineage_store,chia.wallet.chialisp,chia.wallet.did_wallet.did_wallet,chia.wallet.did_wallet.did_wallet_puzzles,chia.wallet.key_val_store,chia.wallet.lineage_proof,chia.wallet.payment,chia.wallet.puzzles.load_clvm,chia.wallet.puzzles.p2_conditions,chia.wallet.puzzles.p2_delegated_conditions,chia.wallet.puzzles.p2_delegated_puzzle,chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle,chia.wallet.puzzles.p2_m_of_n_delegate_direct,chia.wallet.puzzles.p2_puzzle_hash,chia.wallet.puzzles.prefarm.spend_prefarm,chia.wallet.puzzles.puzzle_utils,chia.wallet.puzzles.rom_bootstrap_generator,chia.wallet.puzzles.singleton_top_layer,chia.wallet.puzzles.tails,chia.wallet.rl_wallet.rl_wallet,chia.wallet.rl_wallet.rl_wallet_puzzles,chia.wallet.secret_key_store,chia.wallet.settings.user_settings,chia.wallet.trade_manager,chia.wallet.trade_record,chia.wallet.trading.offer,chia.wallet.trading.trade_store,chia.wallet.transaction_record,chia.wallet.util.debug_spend_bundle,chia.wallet.util.new_peak_queue,chia.wallet.util.peer_request_cache,chia.wallet.util.wallet_sync_utils,chia.wallet.wallet,chia.wallet.wallet_action_store,chia.wallet.wallet_blockchain,chia.wallet.wallet_coin_store,chia.wallet.wallet_interested_store,chia.wallet.wallet_node,chia.wallet.wallet_node_api,chia.wallet.wallet_pool_store,chia.wallet.wallet_puzzle_store,chia.wallet.wallet_state_manager,chia.wallet.wallet_sync_store,chia.wallet.wallet_transaction_store,chia.wallet.wallet_user_store,chia.wallet.wallet_weight_proof_handler,installhelper,tests.blockchain.blockchain_test_utils,tests.blockchain.test_blockchain,tests.blockchain.test_blockchain_transactions,tests.block_tools,tests.build-init-files,tests.build-workflows,tests.clvm.coin_store,tests.clvm.test_chialisp_deserialization,tests.clvm.test_clvm_compilation,tests.clvm.test_program,tests.clvm.test_puzzle_compression,tests.clvm.test_puzzles,tests.clvm.test_serialized_program,tests.clvm.test_singletons,tests.clvm.test_spend_sim,tests.conftest,tests.connection_utils,tests.core.cmds.test_keys,tests.core.consensus.test_pot_iterations,tests.core.custom_types.test_coin,tests.core.custom_types.test_proof_of_space,tests.core.custom_types.test_spend_bundle,tests.core.daemon.test_daemon,tests.core.full_node.full_sync.test_full_sync,tests.core.full_node.stores.test_block_store,tests.core.full_node.stores.test_coin_store,tests.core.full_node.stores.test_full_node_store,tests.core.full_node.stores.test_hint_store,tests.core.full_node.stores.test_sync_store,tests.core.full_node.test_address_manager,tests.core.full_node.test_block_height_map,tests.core.full_node.test_conditions,tests.core.full_node.test_full_node,tests.core.full_node.test_mempool,tests.core.full_node.test_mempool_performance,tests.core.full_node.test_node_load,tests.core.full_node.test_peer_store_resolver,tests.core.full_node.test_performance,tests.core.full_node.test_transactions,tests.core.make_block_generator,tests.core.node_height,tests.core.server.test_dos,tests.core.server.test_rate_limits,tests.core.ssl.test_ssl,tests.core.test_cost_calculation,tests.core.test_crawler_rpc,tests.core.test_daemon_rpc,tests.core.test_db_conversion,tests.core.test_farmer_harvester_rpc,tests.core.test_filter,tests.core.test_full_node_rpc,tests.core.test_merkle_set,tests.core.test_setproctitle,tests.core.util.test_cached_bls,tests.core.util.test_config,tests.core.util.test_file_keyring_synchronization,tests.core.util.test_files,tests.core.util.test_keychain,tests.core.util.test_keyring_wrapper,tests.core.util.test_lru_cache,tests.core.util.test_significant_bits,tests.core.util.test_streamable,tests.farmer_harvester.test_farmer_harvester,tests.generator.test_compression,tests.generator.test_generator_types,tests.generator.test_list_to_batches,tests.generator.test_rom,tests.generator.test_scan,tests.plotting.test_plot_manager,tests.pools.test_pool_cmdline,tests.pools.test_pool_config,tests.pools.test_pool_puzzles_lifecycle,tests.pools.test_pool_rpc,tests.pools.test_wallet_pool_store,tests.setup_nodes,tests.setup_services,tests.simulation.test_simulation,tests.time_out_assert,tests.tools.test_full_sync,tests.tools.test_run_block,tests.util.alert_server,tests.util.benchmark_cost,tests.util.blockchain,tests.util.build_network_protocol_files,tests.util.db_connection,tests.util.generator_tools_testing,tests.util.keyring,tests.util.key_tool,tests.util.misc,tests.util.network,tests.util.rpc,tests.util.test_full_block_utils,tests.util.test_lock_queue,tests.util.test_network_protocol_files,tests.util.test_struct_stream,tests.wallet.cat_wallet.test_cat_lifecycle,tests.wallet.cat_wallet.test_cat_wallet,tests.wallet.cat_wallet.test_offer_lifecycle,tests.wallet.cat_wallet.test_trades,tests.wallet.did_wallet.test_did,tests.wallet.did_wallet.test_did_rpc,tests.wallet.rl_wallet.test_rl_rpc,tests.wallet.rl_wallet.test_rl_wallet,tests.wallet.rpc.test_wallet_rpc,tests.wallet.simple_sync.test_simple_sync_protocol,tests.wallet.sync.test_wallet_sync,tests.wallet.test_bech32m,tests.wallet.test_chialisp,tests.wallet.test_puzzle_store,tests.wallet.test_singleton,tests.wallet.test_singleton_lifecycle,tests.wallet.test_singleton_lifecycle_fast,tests.wallet.test_taproot,tests.wallet.test_wallet,tests.wallet.test_wallet_blockchain,tests.wallet.test_wallet_interested_store,tests.wallet.test_wallet_key_val_store,tests.wallet.test_wallet_user_store,tests.wallet_tools,tests.weight_proof.test_weight_proof,tools.analyze-chain,tools.run_block,tools.test_full_sync] disallow_any_generics = False disallow_subclassing_any = False disallow_untyped_calls = False diff --git a/tests/core/util/test_streamable.py b/tests/core/util/test_streamable.py index 5562d03d15b3..65b3255212ee 100644 --- a/tests/core/util/test_streamable.py +++ b/tests/core/util/test_streamable.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import List, Optional, Tuple +from typing import Dict, List, Optional, Tuple import io import pytest @@ -14,6 +14,7 @@ from chia.types.weight_proof import SubEpochChallengeSegment from chia.util.ints import uint8, uint32, uint64 from chia.util.streamable import ( + DefinitionError, Streamable, streamable, parse_bool, @@ -25,13 +26,147 @@ parse_tuple, parse_size_hints, parse_str, + is_type_List, + is_type_SpecificOptional, ) from tests.setup_nodes import test_constants -def test_basic(): +def test_int_not_supported() -> None: + with raises(NotImplementedError): + + @streamable + @dataclass(frozen=True) + class TestClassInt(Streamable): + a: int + + +def test_float_not_supported() -> None: + with raises(NotImplementedError): + + @streamable + @dataclass(frozen=True) + class TestClassFloat(Streamable): + a: float + + +def test_dict_not_suppported() -> None: + with raises(NotImplementedError): + + @streamable + @dataclass(frozen=True) + class TestClassDict(Streamable): + a: Dict[str, str] + + +def test_pure_dataclass_not_supported() -> None: + @dataclass(frozen=True) + class DataClassOnly: + a: uint8 + + with raises(NotImplementedError): + + @streamable + @dataclass(frozen=True) + class TestClassDataclass(Streamable): + a: DataClassOnly + + +def test_plain_class_not_supported() -> None: + class PlainClass: + a: uint8 + + with raises(NotImplementedError): + + @streamable + @dataclass(frozen=True) + class TestClassPlain(Streamable): + a: PlainClass + + +def test_basic_list(): + a = [1, 2, 3] + assert is_type_List(type(a)) + assert is_type_List(List) + assert is_type_List(List[int]) + assert is_type_List(List[uint8]) + assert is_type_List(list) + assert not is_type_List(Tuple) + assert not is_type_List(tuple) + assert not is_type_List(dict) + + +def test_not_lists(): + assert not is_type_List(Dict) + + +def test_basic_optional(): + assert is_type_SpecificOptional(Optional[int]) + assert is_type_SpecificOptional(Optional[Optional[int]]) + assert not is_type_SpecificOptional(List[int]) + + +def test_StrictDataClass(): + @streamable @dataclass(frozen=True) + class TestClass1(Streamable): + a: uint8 + b: str + + good: TestClass1 = TestClass1(24, "!@12") + assert TestClass1.__name__ == "TestClass1" + assert good + assert good.a == 24 + assert good.b == "!@12" + good2 = TestClass1(52, bytes([1, 2, 3])) + assert good2.b == str(bytes([1, 2, 3])) + + +def test_StrictDataClassBad(): @streamable + @dataclass(frozen=True) + class TestClass2(Streamable): + a: uint8 + b = 0 + + assert TestClass2(25) + + with raises(TypeError): + TestClass2(1, 2) # pylint: disable=too-many-function-args + + +def test_StrictDataClassLists(): + @streamable + @dataclass(frozen=True) + class TestClass(Streamable): + a: List[uint8] + b: List[List[uint8]] + + assert TestClass([1, 2, 3], [[uint8(200), uint8(25)], [uint8(25)]]) + + with raises(ValueError): + TestClass({"1": 1}, [[uint8(200), uint8(25)], [uint8(25)]]) + + with raises(ValueError): + TestClass([1, 2, 3], [uint8(200), uint8(25)]) + + +def test_StrictDataClassOptional(): + @streamable + @dataclass(frozen=True) + class TestClass(Streamable): + a: Optional[uint8] + b: Optional[uint8] + c: Optional[Optional[uint8]] + d: Optional[Optional[uint8]] + + good = TestClass(12, None, 13, None) + assert good + + +def test_basic(): + @streamable + @dataclass(frozen=True) class TestClass(Streamable): a: uint32 b: uint32 @@ -48,8 +183,8 @@ class TestClass(Streamable): def test_variable_size(): - @dataclass(frozen=True) @streamable + @dataclass(frozen=True) class TestClass2(Streamable): a: uint32 b: uint32 @@ -60,8 +195,8 @@ class TestClass2(Streamable): with raises(NotImplementedError): - @dataclass(frozen=True) @streamable + @dataclass(frozen=True) class TestClass3(Streamable): a: int @@ -72,8 +207,8 @@ def test_json(bt): assert FullBlock.from_json_dict(dict_block) == block -@dataclass(frozen=True) @streamable +@dataclass(frozen=True) class OptionalTestClass(Streamable): a: Optional[str] b: Optional[bool] @@ -99,13 +234,13 @@ def test_optional_json(a: Optional[str], b: Optional[bool], c: Optional[List[Opt def test_recursive_json(): - @dataclass(frozen=True) @streamable + @dataclass(frozen=True) class TestClass1(Streamable): a: List[uint32] - @dataclass(frozen=True) @streamable + @dataclass(frozen=True) class TestClass2(Streamable): a: uint32 b: List[Optional[List[TestClass1]]] @@ -130,8 +265,8 @@ def test_ambiguous_deserialization_optionals(): with raises(AssertionError): SubEpochChallengeSegment.from_bytes(b"\x00\x00\x00\x03\xff\xff\xff\xff") - @dataclass(frozen=True) @streamable + @dataclass(frozen=True) class TestClassOptional(Streamable): a: Optional[uint8] @@ -144,8 +279,8 @@ class TestClassOptional(Streamable): def test_ambiguous_deserialization_int(): - @dataclass(frozen=True) @streamable + @dataclass(frozen=True) class TestClassUint(Streamable): a: uint32 @@ -155,8 +290,8 @@ class TestClassUint(Streamable): def test_ambiguous_deserialization_list(): - @dataclass(frozen=True) @streamable + @dataclass(frozen=True) class TestClassList(Streamable): a: List[uint8] @@ -166,8 +301,8 @@ class TestClassList(Streamable): def test_ambiguous_deserialization_tuple(): - @dataclass(frozen=True) @streamable + @dataclass(frozen=True) class TestClassTuple(Streamable): a: Tuple[uint8, str] @@ -177,8 +312,8 @@ class TestClassTuple(Streamable): def test_ambiguous_deserialization_str(): - @dataclass(frozen=True) @streamable + @dataclass(frozen=True) class TestClassStr(Streamable): a: str @@ -188,8 +323,8 @@ class TestClassStr(Streamable): def test_ambiguous_deserialization_bytes(): - @dataclass(frozen=True) @streamable + @dataclass(frozen=True) class TestClassBytes(Streamable): a: bytes @@ -205,8 +340,8 @@ class TestClassBytes(Streamable): def test_ambiguous_deserialization_bool(): - @dataclass(frozen=True) @streamable + @dataclass(frozen=True) class TestClassBool(Streamable): a: bool @@ -219,8 +354,8 @@ class TestClassBool(Streamable): def test_ambiguous_deserialization_program(): - @dataclass(frozen=True) @streamable + @dataclass(frozen=True) class TestClassProgram(Streamable): a: Program @@ -233,8 +368,8 @@ class TestClassProgram(Streamable): def test_streamable_empty(): - @dataclass(frozen=True) @streamable + @dataclass(frozen=True) class A(Streamable): pass @@ -414,3 +549,42 @@ def test_parse_str(): # EOF off by one with raises(AssertionError): parse_str(io.BytesIO(b"\x00\x00\x02\x01" + b"a" * 512)) + + +def test_wrong_decorator_order(): + + with raises(DefinitionError): + + @dataclass(frozen=True) + @streamable + class WrongDecoratorOrder(Streamable): + pass + + +def test_dataclass_not_frozen(): + + with raises(DefinitionError): + + @streamable + @dataclass(frozen=False) + class DataclassNotFrozen(Streamable): + pass + + +def test_dataclass_missing(): + + with raises(DefinitionError): + + @streamable + class DataclassMissing(Streamable): + pass + + +def test_streamable_inheritance_missing(): + + with raises(DefinitionError): + + @streamable + @dataclass(frozen=True) + class StreamableInheritanceMissing: + pass diff --git a/tests/core/util/test_type_checking.py b/tests/core/util/test_type_checking.py deleted file mode 100644 index 8e90f8ad4c34..000000000000 --- a/tests/core/util/test_type_checking.py +++ /dev/null @@ -1,91 +0,0 @@ -import unittest -from dataclasses import dataclass -from typing import Dict, List, Optional, Tuple - -from pytest import raises - -from chia.util.ints import uint8 -from chia.util.type_checking import is_type_List, is_type_SpecificOptional, strictdataclass - - -class TestIsTypeList(unittest.TestCase): - def test_basic_list(self): - a = [1, 2, 3] - assert is_type_List(type(a)) - assert is_type_List(List) - assert is_type_List(List[int]) - assert is_type_List(List[uint8]) - assert is_type_List(list) - assert not is_type_List(Tuple) - assert not is_type_List(tuple) - assert not is_type_List(dict) - - def test_not_lists(self): - assert not is_type_List(Dict) - - -class TestIsTypeSpecificOptional(unittest.TestCase): - def test_basic_optional(self): - assert is_type_SpecificOptional(Optional[int]) - assert is_type_SpecificOptional(Optional[Optional[int]]) - assert not is_type_SpecificOptional(List[int]) - - -class TestStrictClass(unittest.TestCase): - def test_StrictDataClass(self): - @dataclass(frozen=True) - @strictdataclass - class TestClass1: - a: int - b: str - - good: TestClass1 = TestClass1(24, "!@12") - assert TestClass1.__name__ == "TestClass1" - assert good - assert good.a == 24 - assert good.b == "!@12" - good2 = TestClass1(52, bytes([1, 2, 3])) - assert good2.b == str(bytes([1, 2, 3])) - - def test_StrictDataClassBad(self): - @dataclass(frozen=True) - @strictdataclass - class TestClass2: - a: int - b = 0 - - assert TestClass2(25) - - with raises(TypeError): - TestClass2(1, 2) # pylint: disable=too-many-function-args - - def test_StrictDataClassLists(self): - @dataclass(frozen=True) - @strictdataclass - class TestClass: - a: List[int] - b: List[List[uint8]] - - assert TestClass([1, 2, 3], [[uint8(200), uint8(25)], [uint8(25)]]) - - with raises(ValueError): - TestClass({"1": 1}, [[uint8(200), uint8(25)], [uint8(25)]]) - - with raises(ValueError): - TestClass([1, 2, 3], [uint8(200), uint8(25)]) - - def test_StrictDataClassOptional(self): - @dataclass(frozen=True) - @strictdataclass - class TestClass: - a: Optional[int] - b: Optional[int] - c: Optional[Optional[int]] - d: Optional[Optional[int]] - - good = TestClass(12, None, 13, None) - assert good - - -if __name__ == "__main__": - unittest.main() From 0e9c91711945cedbd04362f72439c46360e208a1 Mon Sep 17 00:00:00 2001 From: Yostra Date: Mon, 11 Apr 2022 00:18:18 -0400 Subject: [PATCH 357/378] Expose farm_block RPC for simulator (#10830) * expose farm block api to RPC for simulator * lint * pre-commit lint --- chia/simulator/SimulatorFullNodeRpcApi.py | 19 +++++++++++++++++++ chia/simulator/start_simulator.py | 6 +++--- 2 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 chia/simulator/SimulatorFullNodeRpcApi.py diff --git a/chia/simulator/SimulatorFullNodeRpcApi.py b/chia/simulator/SimulatorFullNodeRpcApi.py new file mode 100644 index 000000000000..ff5700a013d5 --- /dev/null +++ b/chia/simulator/SimulatorFullNodeRpcApi.py @@ -0,0 +1,19 @@ +from typing import Any, Dict, Optional + +from chia.rpc.full_node_rpc_api import FullNodeRpcApi +from chia.simulator.simulator_protocol import FarmNewBlockProtocol +from chia.util.bech32m import decode_puzzle_hash + + +class SimulatorFullNodeRpcApi(FullNodeRpcApi): + def get_routes(self) -> Dict[str, Any]: + routes = super().get_routes() + routes["/farm_tx_block"] = self.farm_tx_block + return routes + + async def farm_tx_block(self, _request: Dict[str, str]) -> Optional[Dict[str, str]]: + request_address = _request["address"] + ph = decode_puzzle_hash(request_address) + req = FarmNewBlockProtocol(ph) + await self.service.server.api.farm_new_transaction_block(req) + return None diff --git a/chia/simulator/start_simulator.py b/chia/simulator/start_simulator.py index 83c555e3d498..6c1a2e76d251 100644 --- a/chia/simulator/start_simulator.py +++ b/chia/simulator/start_simulator.py @@ -4,16 +4,16 @@ from typing import Dict from chia.full_node.full_node import FullNode -from chia.rpc.full_node_rpc_api import FullNodeRpcApi from chia.server.outbound_message import NodeType from chia.server.start_service import run_service +from chia.simulator.SimulatorFullNodeRpcApi import SimulatorFullNodeRpcApi from chia.util.config import load_config_cli from chia.util.default_root import DEFAULT_ROOT_PATH from chia.util.path import mkdir, path_from_root from tests.block_tools import BlockTools, create_block_tools, test_constants from tests.util.keyring import TempKeyring -from .full_node_simulator import FullNodeSimulator +from chia.simulator.full_node_simulator import FullNodeSimulator # See: https://bugs.python.org/issue29288 "".encode("idna") @@ -43,7 +43,7 @@ def service_kwargs_for_full_node_simulator(root_path: Path, config: Dict, bt: Bl service_name=SERVICE_NAME, server_listen_ports=[config["port"]], on_connect_callback=node.on_connect, - rpc_info=(FullNodeRpcApi, config["rpc_port"]), + rpc_info=(SimulatorFullNodeRpcApi, config["rpc_port"]), network_id=network_id, ) return kwargs From 9ff3fc993f69b5c59ecf14470284a5b9e799e6cf Mon Sep 17 00:00:00 2001 From: Mariano Sorgente <3069354+mariano54@users.noreply.github.com> Date: Mon, 11 Apr 2022 12:16:01 -0400 Subject: [PATCH 358/378] Ms.plot load perf2 (#10978) * 2.7 seconds -> 0.45 seconds * Merge * Work on create_plots refactor * Try to fix tests * Try to fix tests * Use new functions * Fix block_tools by adding dir * Extra argument * Try to fix cyclic import * isort * Drop warning * Some cleanups around `exclude_final_dir` and directory adding * Cleanup `min_mainnet_k_size` checks * Drop unrelated changes * Fixes after rebase * Fix cyclic import * Update tests/block_tools.py Co-authored-by: dustinface <35775977+xdustinface@users.noreply.github.com> * Update tests/block_tools.py Co-authored-by: dustinface <35775977+xdustinface@users.noreply.github.com> Co-authored-by: xdustinface Co-authored-by: dustinface <35775977+xdustinface@users.noreply.github.com> --- chia/cmds/plots.py | 20 +++++++++++--------- chia/daemon/server.py | 17 ++++------------- chia/plotters/chiapos.py | 17 +++++++++-------- chia/plotting/create_plots.py | 31 ++++--------------------------- chia/plotting/manager.py | 18 +++++++++--------- chia/plotting/util.py | 12 ++++++++++++ tests/block_tools.py | 24 +++++++++++------------- tests/pools/test_pool_rpc.py | 4 +++- 8 files changed, 63 insertions(+), 80 deletions(-) diff --git a/chia/cmds/plots.py b/chia/cmds/plots.py index 247f603edc1d..8286842c2158 100644 --- a/chia/cmds/plots.py +++ b/chia/cmds/plots.py @@ -5,6 +5,8 @@ import click +from chia.plotting.util import add_plot_directory, validate_plot_size + DEFAULT_STRIPE_SIZE = 65536 log = logging.getLogger(__name__) @@ -128,14 +130,12 @@ def __init__(self): self.plotid = plotid self.memo = memo self.nobitfield = nobitfield - self.exclude_final_dir = exclude_final_dir - if size < 32 and not override_k: - print("k=32 is the minimum size for farming.") - print("If you are testing and you want to use smaller size please add the --override-k flag.") - sys.exit(1) - elif size < 25 and override_k: - print("Error: The minimum k size allowed from the cli is k=25.") + root_path: Path = ctx.obj["root_path"] + try: + validate_plot_size(root_path, size, override_k) + except ValueError as e: + print(e) sys.exit(1) plot_keys = asyncio.run( @@ -144,13 +144,15 @@ def __init__(self): alt_fingerprint, pool_public_key, pool_contract_address, - ctx.obj["root_path"], + root_path, log, connect_to_daemon, ) ) - asyncio.run(create_plots(Params(), plot_keys, ctx.obj["root_path"])) + asyncio.run(create_plots(Params(), plot_keys)) + if not exclude_final_dir: + add_plot_directory(root_path, final_dir) @plots_cmd.command("check", short_help="Checks plots") diff --git a/chia/daemon/server.py b/chia/daemon/server.py index 5281279dbe2d..014bb2a78259 100644 --- a/chia/daemon/server.py +++ b/chia/daemon/server.py @@ -864,20 +864,11 @@ def _run_next_serial_plotting(self, loop: asyncio.AbstractEventLoop, queue: str def _post_process_plotting_job(self, job: Dict[str, Any]): id: str = job["id"] - final_dir: str = job.get("final_dir", "") - exclude_final_dir: bool = job.get("exclude_final_dir", False) - + final_dir: str = job["final_dir"] + exclude_final_dir: bool = job["exclude_final_dir"] log.info(f"Post-processing plotter job with ID {id}") # lgtm [py/clear-text-logging-sensitive-data] - - if exclude_final_dir is False and len(final_dir) > 0: - resolved_final_dir: str = str(Path(final_dir).resolve()) - config = load_config(self.root_path, "config.yaml") - plot_directories_list: str = config["harvester"]["plot_directories"] - - if resolved_final_dir not in plot_directories_list: - # Adds the directory to the plot directories if it is not present - log.info(f"Adding directory {resolved_final_dir} to harvester for farming") - add_plot_directory(self.root_path, resolved_final_dir) + if not exclude_final_dir: + add_plot_directory(self.root_path, final_dir) async def _start_plotting(self, id: str, loop: asyncio.AbstractEventLoop, queue: str = "default"): current_process = None diff --git a/chia/plotters/chiapos.py b/chia/plotters/chiapos.py index fc24acbdf199..0613077c7ce5 100644 --- a/chia/plotters/chiapos.py +++ b/chia/plotters/chiapos.py @@ -9,6 +9,8 @@ from pathlib import Path from typing import Any, Dict, Optional +from chia.plotting.util import add_plot_directory, validate_plot_size + log = logging.getLogger(__name__) @@ -31,16 +33,13 @@ def __init__(self, args): self.plotid = args.id self.memo = args.memo self.nobitfield = args.nobitfield - self.exclude_final_dir = args.exclude_final_dir def plot_chia(args, root_path): - if args.size < 32 and not args.override: - print("k=32 is the minimum size for farming.") - print("If you are testing and you want to use smaller size please add the --override flag.") - return - elif args.size < 25 and args.override: - print("Error: The minimum k size allowed from the cli is k=25.") + try: + validate_plot_size(root_path, args.size, args.override) + except ValueError as e: + print(e) return plot_keys = asyncio.run( @@ -54,4 +53,6 @@ def plot_chia(args, root_path): args.connect_to_daemon, ) ) - asyncio.run(create_plots(Params(args), plot_keys, root_path)) + asyncio.run(create_plots(Params(args), plot_keys)) + if not args.exclude_final_dir: + add_plot_directory(root_path, args.finaldir) diff --git a/chia/plotting/create_plots.py b/chia/plotting/create_plots.py index aef0ac773da7..5de568794d8e 100644 --- a/chia/plotting/create_plots.py +++ b/chia/plotting/create_plots.py @@ -8,12 +8,10 @@ from chiapos import DiskPlotter from chia.daemon.keychain_proxy import KeychainProxy, connect_to_keychain_and_validate, wrap_local_keychain -from chia.plotting.util import add_plot_directory from chia.plotting.util import stream_plot_info_ph, stream_plot_info_pk from chia.types.blockchain_format.proof_of_space import ProofOfSpace from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.bech32m import decode_puzzle_hash -from chia.util.config import config_path_for_filename, load_config from chia.util.keychain import Keychain from chia.util.path import mkdir from chia.wallet.derive_keys import master_sk_to_farmer_sk, master_sk_to_local_sk, master_sk_to_pool_sk @@ -142,24 +140,16 @@ async def resolve_plot_keys( async def create_plots( - args, keys: PlotKeys, root_path, use_datetime=True, test_private_keys: Optional[List] = None + args, + keys: PlotKeys, + use_datetime: bool = True, + test_private_keys: Optional[List] = None, ) -> Tuple[Dict[bytes32, Path], Dict[bytes32, Path]]: - - config_filename = config_path_for_filename(root_path, "config.yaml") - config = load_config(root_path, config_filename) - if args.tmp2_dir is None: args.tmp2_dir = args.tmp_dir - assert (keys.pool_public_key is None) != (keys.pool_contract_puzzle_hash is None) num = args.num - if args.size < config["min_mainnet_k_size"] and test_private_keys is None: - log.warning(f"Creating plots with size k={args.size}, which is less than the minimum required for mainnet") - if args.size < 20: - log.warning("k under 22 is not supported. Increasing k to 21") - args.size = 20 - if keys.pool_public_key is not None: log.info( f"Creating {num} plots of size {args.size}, pool public key: " @@ -230,19 +220,6 @@ async def create_plots( filename = f"plot-k{args.size}-{plot_id}.plot" full_path: Path = args.final_dir / filename - resolved_final_dir: str = str(Path(args.final_dir).resolve()) - plot_directories_list: str = config["harvester"]["plot_directories"] - - if args.exclude_final_dir: - log.info(f"NOT adding directory {resolved_final_dir} to harvester for farming") - if resolved_final_dir in plot_directories_list: - log.warning(f"Directory {resolved_final_dir} already exists for harvester, please remove it manually") - else: - if resolved_final_dir not in plot_directories_list: - # Adds the directory to the plot directories if it is not present - log.info(f"Adding directory {resolved_final_dir} to harvester for farming") - config = add_plot_directory(root_path, resolved_final_dir) - if not full_path.exists(): log.info(f"Starting plot {i + 1}/{num}") # Creates the plot. This will take a long time for larger plots. diff --git a/chia/plotting/manager.py b/chia/plotting/manager.py index 24ec531e9286..aa309795e95a 100644 --- a/chia/plotting/manager.py +++ b/chia/plotting/manager.py @@ -204,11 +204,11 @@ def get_duplicates(self): def needs_refresh(self) -> bool: return time.time() - self.last_refresh_time > float(self.refresh_parameter.interval_seconds) - def start_refreshing(self): + def start_refreshing(self, sleep_interval_ms: int = 1000): self._refreshing_enabled = True if self._refresh_thread is None or not self._refresh_thread.is_alive(): self.cache.load() - self._refresh_thread = threading.Thread(target=self._refresh_task) + self._refresh_thread = threading.Thread(target=self._refresh_task, args=(sleep_interval_ms,)) self._refresh_thread.start() def stop_refreshing(self): @@ -221,11 +221,11 @@ def trigger_refresh(self): log.debug("trigger_refresh") self.last_refresh_time = 0 - def _refresh_task(self): + def _refresh_task(self, sleep_interval_ms: int): while self._refreshing_enabled: try: while not self.needs_refresh() and self._refreshing_enabled: - time.sleep(1) + time.sleep(sleep_interval_ms / 1000.0) if not self._refreshing_enabled: return @@ -264,12 +264,12 @@ def _refresh_task(self): continue paths_to_remove: List[str] = [] - for path in duplicated_paths: - loaded_plot = Path(path) / Path(plot_filename) + for path_str in duplicated_paths: + loaded_plot = Path(path_str) / Path(plot_filename) if loaded_plot not in plot_paths: - paths_to_remove.append(path) - for path in paths_to_remove: - duplicated_paths.remove(path) + paths_to_remove.append(path_str) + for path_str in paths_to_remove: + duplicated_paths.remove(path_str) for filename in filenames_to_remove: del self.plot_filename_paths[filename] diff --git a/chia/plotting/util.py b/chia/plotting/util.py index 132f0d67ee3a..ae542437c76c 100644 --- a/chia/plotting/util.py +++ b/chia/plotting/util.py @@ -205,3 +205,15 @@ def find_duplicate_plot_IDs(all_filenames=None) -> None: for filename_str in duplicate_filenames: log_message += "\t" + filename_str + "\n" log.warning(f"{log_message}") + + +def validate_plot_size(root_path: Path, k: int, override_k: bool) -> None: + config = load_config(root_path, "config.yaml") + min_k = config["min_mainnet_k_size"] + if k < min_k and not override_k: + raise ValueError( + f"k={min_k} is the minimum size for farming.\n" + "If you are testing and you want to use smaller size please add the --override-k flag." + ) + elif k < 25 and override_k: + raise ValueError("Error: The minimum k size allowed from the cli is k=25.") diff --git a/tests/block_tools.py b/tests/block_tools.py index a000f494d4fc..079ddde28f9e 100644 --- a/tests/block_tools.py +++ b/tests/block_tools.py @@ -27,6 +27,7 @@ from chia.full_node.generator import setup_generator_args from chia.full_node.mempool_check_conditions import GENERATOR_MOD from chia.plotting.create_plots import create_plots, PlotKeys +from chia.plotting.util import add_plot_directory from chia.consensus.block_creation import unfinished_block_to_full_block from chia.consensus.block_record import BlockRecord from chia.consensus.block_rewards import calculate_base_farmer_reward, calculate_pool_reward @@ -83,7 +84,7 @@ from chia.util.prev_transaction_block import get_prev_transaction_block from chia.util.path import mkdir from chia.util.vdf_prover import get_vdf_info_and_proof -from tests.time_out_assert import time_out_assert +from tests.time_out_assert import time_out_assert_custom_interval from tests.wallet_tools import WalletTool from tests.util.socket import find_available_listen_port from tests.util.ssl_certs import get_next_nodes_certs_and_keys, get_next_private_ca_cert_and_key @@ -243,7 +244,11 @@ def change_config(self, new_config: Dict): with lock_config(self.root_path, "config.yaml"): save_config(self.root_path, "config.yaml", self._config) + def add_plot_directory(self, path: Path) -> None: + self._config = add_plot_directory(self.root_path, str(path)) + async def setup_plots(self): + self.add_plot_directory(self.plot_dir) assert self.created_plots == 0 # OG Plots for i in range(15): @@ -256,7 +261,7 @@ async def setup_plots(self): await self.new_plot( path=self.plot_dir / "not_in_keychain", plot_keys=PlotKeys(G1Element(), G1Element(), None), - exclude_final_dir=True, + exclude_plots=True, ) await self.refresh_plots() @@ -267,7 +272,7 @@ async def new_plot( path: Path = None, tmp_dir: Path = None, plot_keys: Optional[PlotKeys] = None, - exclude_final_dir: bool = False, + exclude_plots: bool = False, ) -> Optional[bytes32]: final_dir = self.plot_dir if path is not None: @@ -292,7 +297,6 @@ async def new_plot( args.nobitfield = False args.exclude_final_dir = False args.list_duplicates = False - args.exclude_final_dir = exclude_final_dir try: if plot_keys is None: pool_pk: Optional[G1Element] = None @@ -307,7 +311,6 @@ async def new_plot( created, existed = await create_plots( args, plot_keys, - self.root_path, use_datetime=False, test_private_keys=[AugSchemeMPL.key_gen(std_hash(self.created_plots.to_bytes(2, "big")))], ) @@ -326,14 +329,9 @@ async def new_plot( assert plot_id_new is not None assert path_new is not None - if not exclude_final_dir: + if not exclude_plots: self.expected_plots[plot_id_new] = path_new - # create_plots() updates plot_directories. Ensure we refresh our config to reflect the updated value - self._config["harvester"]["plot_directories"] = load_config(self.root_path, "config.yaml", "harvester")[ - "plot_directories" - ] - return plot_id_new except KeyboardInterrupt: @@ -346,8 +344,8 @@ async def refresh_plots(self): ) # Make sure we have at least some batches + a remainder self.plot_manager.trigger_refresh() assert self.plot_manager.needs_refresh() - self.plot_manager.start_refreshing() - await time_out_assert(10, self.plot_manager.needs_refresh, value=False) + self.plot_manager.start_refreshing(sleep_interval_ms=1) + await time_out_assert_custom_interval(10, 0.001, self.plot_manager.needs_refresh, value=False) self.plot_manager.stop_refreshing() assert not self.plot_manager.needs_refresh() diff --git a/tests/pools/test_pool_rpc.py b/tests/pools/test_pool_rpc.py index de66f42df212..77d6da1933bc 100644 --- a/tests/pools/test_pool_rpc.py +++ b/tests/pools/test_pool_rpc.py @@ -69,7 +69,9 @@ class TemporaryPoolPlot: async def __aenter__(self): self._tmpdir = tempfile.TemporaryDirectory() dirname = self._tmpdir.__enter__() - plot_id: bytes32 = await self.bt.new_plot(self.p2_singleton_puzzle_hash, Path(dirname), tmp_dir=Path(dirname)) + tmp_path: Path = Path(dirname) + self.bt.add_plot_directory(tmp_path) + plot_id: bytes32 = await self.bt.new_plot(self.p2_singleton_puzzle_hash, Path(dirname), tmp_dir=tmp_path) assert plot_id is not None await self.bt.refresh_plots() self.plot_id = plot_id From 151fd6476a0220f87fc4dca653a33b80508df773 Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Tue, 12 Apr 2022 10:15:33 -0700 Subject: [PATCH 359/378] rebase and more fixes (#10885) --- chia/clvm/spend_sim.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/chia/clvm/spend_sim.py b/chia/clvm/spend_sim.py index d143b6ed4f8a..a0b6ed98f65d 100644 --- a/chia/clvm/spend_sim.py +++ b/chia/clvm/spend_sim.py @@ -89,9 +89,12 @@ class SpendSim: defaults: ConsensusConstants @classmethod - async def create(cls, db_path=":memory:", defaults=DEFAULT_CONSTANTS): + async def create(cls, db_path=None, defaults=DEFAULT_CONSTANTS): self = cls() - uri = f"file:db_{random.randint(0, 99999999)}?mode=memory&cache=shared" + if db_path is None: + uri = f"file:db_{random.randint(0, 99999999)}?mode=memory&cache=shared" + else: + uri = f"file:{db_path}" connection = await aiosqlite.connect(uri, uri=True) self.db_wrapper = DBWrapper2(connection) await self.db_wrapper.add_connection(await aiosqlite.connect(uri, uri=True)) @@ -111,6 +114,7 @@ async def create(cls, db_path=":memory:", defaults=DEFAULT_CONSTANTS): self.block_height = store_data.block_height self.block_records = store_data.block_records self.blocks = store_data.blocks + self.mempool_manager.peak = self.block_records[-1] else: self.timestamp = 1 self.block_height = 0 @@ -269,6 +273,20 @@ async def push_tx(self, spend_bundle: SpendBundle) -> Tuple[MempoolInclusionStat async def get_coin_record_by_name(self, name: bytes32) -> CoinRecord: return await self.service.mempool_manager.coin_store.get_coin_record(name) + async def get_coin_records_by_names( + self, + names: List[bytes32], + start_height: Optional[int] = None, + end_height: Optional[int] = None, + include_spent_coins: bool = False, + ) -> List[CoinRecord]: + kwargs: Dict[str, Any] = {"include_spent_coins": include_spent_coins, "names": names} + if start_height is not None: + kwargs["start_height"] = start_height + if end_height is not None: + kwargs["end_height"] = end_height + return await self.service.mempool_manager.coin_store.get_coin_records_by_names(**kwargs) + async def get_coin_records_by_parent_ids( self, parent_ids: List[bytes32], From db536c615a0c7226ea8319fb98ec545eded23670 Mon Sep 17 00:00:00 2001 From: Yostra Date: Tue, 12 Apr 2022 13:29:36 -0400 Subject: [PATCH 360/378] derivation from just a master public key (#11140) --- chia/cmds/keys_funcs.py | 7 +++++++ chia/wallet/derive_keys.py | 15 +++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/chia/cmds/keys_funcs.py b/chia/cmds/keys_funcs.py index 1d9af0580756..b153ea95daff 100644 --- a/chia/cmds/keys_funcs.py +++ b/chia/cmds/keys_funcs.py @@ -17,6 +17,7 @@ master_sk_to_pool_sk, master_sk_to_wallet_sk, master_sk_to_wallet_sk_unhardened, + master_pk_to_wallet_pk_unhardened, ) @@ -97,7 +98,13 @@ def show_all_keys(show_mnemonic: bool, non_observer_derivation: bool): if non_observer_derivation else master_sk_to_wallet_sk_unhardened(sk, uint32(0)) ) + # Test pk derivation + wallet_root_unhardened = master_pk_to_wallet_pk_unhardened(sk.get_g1(), uint32(0)) wallet_address: str = encode_puzzle_hash(create_puzzlehash_for_pk(first_wallet_sk.get_g1()), prefix) + wallet_address_from_unhard_root: str = encode_puzzle_hash( + create_puzzlehash_for_pk(wallet_root_unhardened), prefix + ) + assert wallet_address == wallet_address_from_unhard_root print(f"First wallet address{' (non-observer)' if non_observer_derivation else ''}: {wallet_address}") assert seed is not None if show_mnemonic: diff --git a/chia/wallet/derive_keys.py b/chia/wallet/derive_keys.py index 9f3443dca5f2..573870a3baf3 100644 --- a/chia/wallet/derive_keys.py +++ b/chia/wallet/derive_keys.py @@ -20,6 +20,12 @@ def _derive_path(sk: PrivateKey, path: List[int]) -> PrivateKey: return sk +def _derive_path_pub(pk: G1Element, path: List[int]) -> G1Element: + for index in path: + pk = AugSchemeMPL.derive_child_pk_unhardened(pk, index) + return pk + + def _derive_path_unhardened(sk: PrivateKey, path: List[int]) -> PrivateKey: for index in path: sk = AugSchemeMPL.derive_child_sk_unhardened(sk, index) @@ -38,11 +44,20 @@ def master_sk_to_wallet_sk_intermediate(master: PrivateKey) -> PrivateKey: return _derive_path(master, [12381, 8444, 2]) +def master_pk_to_wallet_pk_intermediate(master: G1Element) -> G1Element: + return _derive_path_pub(master, [12381, 8444, 2]) + + def master_sk_to_wallet_sk(master: PrivateKey, index: uint32) -> PrivateKey: intermediate = master_sk_to_wallet_sk_intermediate(master) return _derive_path(intermediate, [index]) +def master_pk_to_wallet_pk_unhardened(master: G1Element, index: uint32) -> G1Element: + intermediate = master_pk_to_wallet_pk_intermediate(master) + return _derive_path_pub(intermediate, [index]) + + def master_sk_to_wallet_sk_unhardened_intermediate(master: PrivateKey) -> PrivateKey: return _derive_path_unhardened(master, [12381, 8444, 2]) From de5ed625bf93dfc52bed9d1fc160b376ace18fa2 Mon Sep 17 00:00:00 2001 From: Patrick Maslana <79757486+pmaslana@users.noreply.github.com> Date: Tue, 12 Apr 2022 10:51:29 -0700 Subject: [PATCH 361/378] Add dependencies macos rhel chiavdf (#11142) * Added steps, when building the chiavdf wheel for macos and rhel-based systems, to install cmake and/or gmp. --- install-timelord.sh | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/install-timelord.sh b/install-timelord.sh index 7368887492b6..0f976131ab6d 100644 --- a/install-timelord.sh +++ b/install-timelord.sh @@ -88,26 +88,28 @@ if [ -e "$THE_PATH" ]; then echo "vdf_client already exists, no action taken" else if [ -e venv/bin/python ] && test $UBUNTU_DEBIAN; then - echo "Installing chiavdf from source on Ubuntu/Debian" + echo "Installing chiavdf dependencies on Ubuntu/Debian" # If Ubuntu version is older than 20.04LTS then upgrade CMake ubuntu_cmake_install # Install remaining needed development tools - assumes venv and prior run of install.sh - echo apt-get install libgmp-dev libboost-python-dev "$PYTHON_DEV_DEPENDENCY" libboost-system-dev build-essential -y + echo "apt-get install libgmp-dev libboost-python-dev $PYTHON_DEV_DEPENDENCY libboost-system-dev build-essential -y" sudo apt-get install libgmp-dev libboost-python-dev "$PYTHON_DEV_DEPENDENCY" libboost-system-dev build-essential -y + echo "Installing chiavdf from source on Ubuntu/Debian" echo venv/bin/python -m pip install --force --no-binary chiavdf "$CHIAVDF_VERSION" venv/bin/python -m pip install --force --no-binary chiavdf "$CHIAVDF_VERSION" symlink_vdf_bench "$PYTHON_VERSION" elif [ -e venv/bin/python ] && test $RHEL_BASED; then - echo "Installing chiavdf from source on RedHat/CentOS/Fedora" + echo "Installing chiavdf dependencies on RedHat/CentOS/Fedora" # Install remaining needed development tools - assumes venv and prior run of install.sh - echo yum install gcc gcc-c++ gmp-devel "$PYTHON_DEV_DEPENDENCY" libtool make autoconf automake openssl-devel libevent-devel boost-devel python3 -y - sudo yum install gcc gcc-c++ gmp-devel "$PYTHON_DEV_DEPENDENCY" libtool make autoconf automake openssl-devel libevent-devel boost-devel python3 -y + echo "yum install gcc gcc-c++ gmp-devel $PYTHON_DEV_DEPENDENCY libtool make autoconf automake openssl-devel libevent-devel boost-devel python3 cmake -y" + sudo yum install gcc gcc-c++ gmp-devel "$PYTHON_DEV_DEPENDENCY" libtool make autoconf automake openssl-devel libevent-devel boost-devel python3 cmake -y + echo "Installing chiavdf from source on RedHat/CentOS/Fedora" echo venv/bin/python -m pip install --force --no-binary chiavdf "$CHIAVDF_VERSION" venv/bin/python -m pip install --force --no-binary chiavdf "$CHIAVDF_VERSION" symlink_vdf_bench "$PYTHON_VERSION" - elif [ -e venv/bin/python ] && test $MACOS && [ "$(brew info boost | grep -c 'Not installed')" -eq 1 ]; then - echo "Installing chiavdf requirement boost for MacOS." - brew install boost + elif [ -e venv/bin/python ] && test $MACOS; then + echo "Installing chiavdf dependencies for MacOS." + brew install boost cmake gmp echo "Installing chiavdf from source." # User needs to provide required packages echo venv/bin/python -m pip install --force --no-binary chiavdf "$CHIAVDF_VERSION" From 26466d54d914d7dfcb8c30355f70a5467cbb3435 Mon Sep 17 00:00:00 2001 From: William Blanke Date: Tue, 12 Apr 2022 16:15:10 -0700 Subject: [PATCH 362/378] updated gui to d5b75bcf7a0fe1ef76775e7f1d5e12d169069676 --- chia-blockchain-gui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia-blockchain-gui b/chia-blockchain-gui index fccbd3e10d27..d5b75bcf7a0f 160000 --- a/chia-blockchain-gui +++ b/chia-blockchain-gui @@ -1 +1 @@ -Subproject commit fccbd3e10d27673e39c01f0f89e47b5455b8331a +Subproject commit d5b75bcf7a0fe1ef76775e7f1d5e12d169069676 From a99c3be36bbcbae5c48a98e8c417ebaf179be01f Mon Sep 17 00:00:00 2001 From: wjblanke Date: Wed, 13 Apr 2022 07:21:46 -0700 Subject: [PATCH 363/378] Revert "derivation from just a master public key (#11140)" (#11143) This reverts commit db536c615a0c7226ea8319fb98ec545eded23670. --- chia/cmds/keys_funcs.py | 7 ------- chia/wallet/derive_keys.py | 15 --------------- 2 files changed, 22 deletions(-) diff --git a/chia/cmds/keys_funcs.py b/chia/cmds/keys_funcs.py index b153ea95daff..1d9af0580756 100644 --- a/chia/cmds/keys_funcs.py +++ b/chia/cmds/keys_funcs.py @@ -17,7 +17,6 @@ master_sk_to_pool_sk, master_sk_to_wallet_sk, master_sk_to_wallet_sk_unhardened, - master_pk_to_wallet_pk_unhardened, ) @@ -98,13 +97,7 @@ def show_all_keys(show_mnemonic: bool, non_observer_derivation: bool): if non_observer_derivation else master_sk_to_wallet_sk_unhardened(sk, uint32(0)) ) - # Test pk derivation - wallet_root_unhardened = master_pk_to_wallet_pk_unhardened(sk.get_g1(), uint32(0)) wallet_address: str = encode_puzzle_hash(create_puzzlehash_for_pk(first_wallet_sk.get_g1()), prefix) - wallet_address_from_unhard_root: str = encode_puzzle_hash( - create_puzzlehash_for_pk(wallet_root_unhardened), prefix - ) - assert wallet_address == wallet_address_from_unhard_root print(f"First wallet address{' (non-observer)' if non_observer_derivation else ''}: {wallet_address}") assert seed is not None if show_mnemonic: diff --git a/chia/wallet/derive_keys.py b/chia/wallet/derive_keys.py index 573870a3baf3..9f3443dca5f2 100644 --- a/chia/wallet/derive_keys.py +++ b/chia/wallet/derive_keys.py @@ -20,12 +20,6 @@ def _derive_path(sk: PrivateKey, path: List[int]) -> PrivateKey: return sk -def _derive_path_pub(pk: G1Element, path: List[int]) -> G1Element: - for index in path: - pk = AugSchemeMPL.derive_child_pk_unhardened(pk, index) - return pk - - def _derive_path_unhardened(sk: PrivateKey, path: List[int]) -> PrivateKey: for index in path: sk = AugSchemeMPL.derive_child_sk_unhardened(sk, index) @@ -44,20 +38,11 @@ def master_sk_to_wallet_sk_intermediate(master: PrivateKey) -> PrivateKey: return _derive_path(master, [12381, 8444, 2]) -def master_pk_to_wallet_pk_intermediate(master: G1Element) -> G1Element: - return _derive_path_pub(master, [12381, 8444, 2]) - - def master_sk_to_wallet_sk(master: PrivateKey, index: uint32) -> PrivateKey: intermediate = master_sk_to_wallet_sk_intermediate(master) return _derive_path(intermediate, [index]) -def master_pk_to_wallet_pk_unhardened(master: G1Element, index: uint32) -> G1Element: - intermediate = master_pk_to_wallet_pk_intermediate(master) - return _derive_path_pub(intermediate, [index]) - - def master_sk_to_wallet_sk_unhardened_intermediate(master: PrivateKey) -> PrivateKey: return _derive_path_unhardened(master, [12381, 8444, 2]) From b3a9459fea76650bacf0a98bdcb1af16c4143213 Mon Sep 17 00:00:00 2001 From: Chris Marslender Date: Wed, 13 Apr 2022 10:35:42 -0500 Subject: [PATCH 364/378] Mark the github workspace as safe (#11159) * Mark the github workspace as safe * Move the git config step after git is installed in the test containers --- .github/workflows/test-install-scripts.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test-install-scripts.yml b/.github/workflows/test-install-scripts.yml index 9b2a7e859673..0a79447b35a9 100644 --- a/.github/workflows/test-install-scripts.yml +++ b/.github/workflows/test-install-scripts.yml @@ -178,6 +178,9 @@ jobs: apt-get --yes update apt-get install --yes git lsb-release sudo + - name: Add safe git directory + run: git config --global --add safe.directory $GITHUB_WORKSPACE + # after installing git so we use that copy - name: Checkout Code uses: actions/checkout@v3 From 9a8556acdeb899e39724898e67b77bf1ffdb6dc8 Mon Sep 17 00:00:00 2001 From: William Blanke Date: Wed, 13 Apr 2022 12:10:05 -0700 Subject: [PATCH 365/378] updated gui to 81303fb962f4a627a2e1c55098e187a9057745da --- chia-blockchain-gui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia-blockchain-gui b/chia-blockchain-gui index d5b75bcf7a0f..81303fb962f4 160000 --- a/chia-blockchain-gui +++ b/chia-blockchain-gui @@ -1 +1 @@ -Subproject commit d5b75bcf7a0fe1ef76775e7f1d5e12d169069676 +Subproject commit 81303fb962f4a627a2e1c55098e187a9057745da From 56804a0b4c5e06bdc5ac918aa319fdbfbe689f2b Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Wed, 13 Apr 2022 21:13:25 +0200 Subject: [PATCH 366/378] optimize wallet tool by not caching the puzzle_hash -> derivation index, but caching puzzle_hash -> secret key (which is the lookup we're actually interested in). This avoids duplicating the actual derivation (#11154) --- tests/wallet_tools.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/tests/wallet_tools.py b/tests/wallet_tools.py index 6600dee0e36d..aff5ab4cbeca 100644 --- a/tests/wallet_tools.py +++ b/tests/wallet_tools.py @@ -30,6 +30,7 @@ class WalletTool: next_address = 0 pubkey_num_lookup: Dict[bytes, uint32] = {} + puzzle_pk_cache: Dict[bytes32, PrivateKey] = {} def __init__(self, constants: ConsensusConstants, sk: Optional[PrivateKey] = None): self.constants = constants @@ -48,16 +49,13 @@ def get_next_address_index(self) -> uint32: return self.next_address def get_private_key_for_puzzle_hash(self, puzzle_hash: bytes32) -> PrivateKey: - if puzzle_hash in self.puzzle_pk_cache: - child = self.puzzle_pk_cache[puzzle_hash] - private = master_sk_to_wallet_sk(self.private_key, uint32(child)) - # pubkey = private.get_g1() - return private - else: - for child in range(self.next_address): - pubkey = master_sk_to_wallet_sk(self.private_key, uint32(child)).get_g1() - if puzzle_hash == puzzle_for_pk(bytes(pubkey)).get_tree_hash(): - return master_sk_to_wallet_sk(self.private_key, uint32(child)) + sk = self.puzzle_pk_cache.get(puzzle_hash) + if sk: + return sk + for child in range(self.next_address): + pubkey = master_sk_to_wallet_sk(self.private_key, uint32(child)).get_g1() + if puzzle_hash == puzzle_for_pk(bytes(pubkey)).get_tree_hash(): + return master_sk_to_wallet_sk(self.private_key, uint32(child)) raise ValueError(f"Do not have the keys for puzzle hash {puzzle_hash}") def puzzle_for_pk(self, pubkey: bytes) -> Program: @@ -65,12 +63,13 @@ def puzzle_for_pk(self, pubkey: bytes) -> Program: def get_new_puzzle(self) -> Program: next_address_index: uint32 = self.get_next_address_index() - pubkey: G1Element = master_sk_to_wallet_sk(self.private_key, next_address_index).get_g1() + sk: PrivateKey = master_sk_to_wallet_sk(self.private_key, next_address_index) + pubkey: G1Element = sk.get_g1() self.pubkey_num_lookup[bytes(pubkey)] = next_address_index puzzle: Program = puzzle_for_pk(pubkey) - self.puzzle_pk_cache[puzzle.get_tree_hash()] = next_address_index + self.puzzle_pk_cache[puzzle.get_tree_hash()] = sk return puzzle def get_new_puzzlehash(self) -> bytes32: From 6259643c367f84f699bc7781c4c567f153e7a0a6 Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Wed, 13 Apr 2022 23:47:14 +0200 Subject: [PATCH 367/378] make listen port colissions in CI less likely (#11164) --- tests/util/socket.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/util/socket.py b/tests/util/socket.py index 84630c6cad91..74526a1dde20 100644 --- a/tests/util/socket.py +++ b/tests/util/socket.py @@ -9,7 +9,7 @@ def find_available_listen_port(name: str = "free") -> int: global recent_ports while True: - port = secrets.randbits(15) + 2000 + port = secrets.randbelow(0xFFFF - 1024) + 1024 if port in recent_ports: continue From 3ea3a4dc0c0ea82ff575c002fb6f06d9a89b6878 Mon Sep 17 00:00:00 2001 From: Chris Marslender Date: Wed, 13 Apr 2022 18:06:50 -0500 Subject: [PATCH 368/378] Use get latest release endpoint for plotters, so that we ignore any pre-releases that could be returned by listReleases (#11165) --- .github/workflows/build-linux-arm64-installer.yml | 8 ++++---- .github/workflows/build-linux-installer-deb.yml | 8 ++++---- .github/workflows/build-linux-installer-rpm.yml | 8 ++++---- .github/workflows/build-macos-installer.yml | 4 ++-- .github/workflows/build-macos-m1-installer.yml | 4 ++-- .github/workflows/build-windows-installer.yml | 8 ++++---- .gitignore | 4 ++++ 7 files changed, 24 insertions(+), 20 deletions(-) diff --git a/.github/workflows/build-linux-arm64-installer.yml b/.github/workflows/build-linux-arm64-installer.yml index 61ad71d55030..be1d7c9cea38 100644 --- a/.github/workflows/build-linux-arm64-installer.yml +++ b/.github/workflows/build-linux-arm64-installer.yml @@ -68,11 +68,11 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} result-encoding: string script: | - const releases = await github.rest.repos.listReleases({ + const release = await github.rest.repos.getLatestRelease({ owner: 'Chia-Network', repo: 'chia-plotter-madmax', }); - return releases.data[0].tag_name; + return release.data.tag_name; - name: Get latest madmax plotter run: | @@ -89,11 +89,11 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} result-encoding: string script: | - const releases = await github.rest.repos.listReleases({ + const release = await github.rest.repos.getLatestRelease({ owner: 'Chia-Network', repo: 'bladebit', }); - return releases.data[0].tag_name; + return release.data.tag_name; - name: Get latest bladebit plotter run: | diff --git a/.github/workflows/build-linux-installer-deb.yml b/.github/workflows/build-linux-installer-deb.yml index a9feddfe1441..0937822c51f0 100644 --- a/.github/workflows/build-linux-installer-deb.yml +++ b/.github/workflows/build-linux-installer-deb.yml @@ -102,11 +102,11 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} result-encoding: string script: | - const releases = await github.rest.repos.listReleases({ + const release = await github.rest.repos.getLatestRelease({ owner: 'Chia-Network', repo: 'chia-plotter-madmax', }); - return releases.data[0].tag_name; + return release.data.tag_name; - name: Get latest madmax plotter run: | @@ -123,11 +123,11 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} result-encoding: string script: | - const releases = await github.rest.repos.listReleases({ + const release = await github.rest.repos.getLatestRelease({ owner: 'Chia-Network', repo: 'bladebit', }); - return releases.data[0].tag_name; + return release.data.tag_name; - name: Get latest bladebit plotter run: | diff --git a/.github/workflows/build-linux-installer-rpm.yml b/.github/workflows/build-linux-installer-rpm.yml index c97c85526079..16d8378fb6f3 100644 --- a/.github/workflows/build-linux-installer-rpm.yml +++ b/.github/workflows/build-linux-installer-rpm.yml @@ -71,11 +71,11 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} result-encoding: string script: | - const releases = await github.rest.repos.listReleases({ + const release = await github.rest.repos.getLatestRelease({ owner: 'Chia-Network', repo: 'chia-plotter-madmax', }); - return releases.data[0].tag_name; + return release.data.tag_name; - name: Get latest madmax plotter run: | @@ -92,11 +92,11 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} result-encoding: string script: | - const releases = await github.rest.repos.listReleases({ + const release = await github.rest.repos.getLatestRelease({ owner: 'Chia-Network', repo: 'bladebit', }); - return releases.data[0].tag_name; + return release.data.tag_name; - name: Get latest bladebit plotter run: | diff --git a/.github/workflows/build-macos-installer.yml b/.github/workflows/build-macos-installer.yml index fa9f7d12dc98..04c7bff453b2 100644 --- a/.github/workflows/build-macos-installer.yml +++ b/.github/workflows/build-macos-installer.yml @@ -110,11 +110,11 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} result-encoding: string script: | - const releases = await github.rest.repos.listReleases({ + const release = await github.rest.repos.getLatestRelease({ owner: 'Chia-Network', repo: 'chia-plotter-madmax', }); - return releases.data[0].tag_name; + return release.data.tag_name; - name: Get latest madmax plotter run: | diff --git a/.github/workflows/build-macos-m1-installer.yml b/.github/workflows/build-macos-m1-installer.yml index 323e636311cf..92c118120e4f 100644 --- a/.github/workflows/build-macos-m1-installer.yml +++ b/.github/workflows/build-macos-m1-installer.yml @@ -84,11 +84,11 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} result-encoding: string script: | - const releases = await github.rest.repos.listReleases({ + const release = await github.rest.repos.getLatestRelease({ owner: 'Chia-Network', repo: 'chia-plotter-madmax', }); - return releases.data[0].tag_name; + return release.data.tag_name; - name: Get latest madmax plotter run: | diff --git a/.github/workflows/build-windows-installer.yml b/.github/workflows/build-windows-installer.yml index ae2161f0b3b3..22abcf919476 100644 --- a/.github/workflows/build-windows-installer.yml +++ b/.github/workflows/build-windows-installer.yml @@ -111,11 +111,11 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} result-encoding: string script: | - const releases = await github.rest.repos.listReleases({ + const release = await github.rest.repos.getLatestRelease({ owner: 'Chia-Network', repo: 'chia-plotter-madmax', }); - return releases.data[0].tag_name; + return release.data.tag_name; - name: Get latest madmax plotter run: | @@ -130,11 +130,11 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} result-encoding: string script: | - const releases = await github.rest.repos.listReleases({ + const release = await github.rest.repos.getLatestRelease({ owner: 'Chia-Network', repo: 'bladebit', }); - return releases.data[0].tag_name; + return release.data.tag_name; - name: Get latest bladebit plotter run: | diff --git a/.gitignore b/.gitignore index c5082e8b5f64..f70408d1fec5 100644 --- a/.gitignore +++ b/.gitignore @@ -298,3 +298,7 @@ tags [._]*.un~ # End of https://www.toptal.com/developers/gitignore/api/python,git,vim + +# Ignore the binaries that are pulled for the installer +/bladebit/ +/madmax/ From 2fbe062e785f783bac3e8ca07efb12862713ffe7 Mon Sep 17 00:00:00 2001 From: Chris Marslender Date: Thu, 14 Apr 2022 19:32:26 -0500 Subject: [PATCH 369/378] Build cli only version of debs (#11166) * Build cli only version of debs * Export the vars needed by j2 * Fix paths * Add symlink to chia in /usr/local/bin/ * Upload the cli only debs to s3 * Add init.py * Ensure SHA is on the dev build for amd64 --- .../workflows/build-linux-arm64-installer.yml | 24 +++++++++----- .../workflows/build-linux-installer-deb.yml | 32 ++++++++++++------- build_scripts/assets/deb/__init__.py | 0 build_scripts/assets/deb/control.j2 | 6 ++++ build_scripts/assets/deb/postinst | 5 +++ build_scripts/assets/deb/prerm | 5 +++ build_scripts/build_linux_deb.sh | 19 +++++++++++ 7 files changed, 71 insertions(+), 20 deletions(-) create mode 100644 build_scripts/assets/deb/__init__.py create mode 100644 build_scripts/assets/deb/control.j2 create mode 100644 build_scripts/assets/deb/postinst create mode 100644 build_scripts/assets/deb/prerm diff --git a/.github/workflows/build-linux-arm64-installer.yml b/.github/workflows/build-linux-arm64-installer.yml index be1d7c9cea38..57d3275c309a 100644 --- a/.github/workflows/build-linux-arm64-installer.yml +++ b/.github/workflows/build-linux-arm64-installer.yml @@ -137,10 +137,11 @@ jobs: CHIA_INSTALLER_VERSION: ${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }} if: steps.check_secrets.outputs.HAS_SECRET run: | - GIT_SHORT_HASH=$(echo "${GITHUB_SHA}" | cut -c1-8) - CHIA_DEV_BUILD=${CHIA_INSTALLER_VERSION}-$GIT_SHORT_HASH - echo "CHIA_DEV_BUILD=$CHIA_DEV_BUILD" >>$GITHUB_ENV - aws s3 cp "$GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_arm64.deb" "s3://download.chia.net/dev/chia-blockchain_${CHIA_DEV_BUILD}_arm64.deb" + GIT_SHORT_HASH=$(echo "${GITHUB_SHA}" | cut -c1-8) + CHIA_DEV_BUILD=${CHIA_INSTALLER_VERSION}-$GIT_SHORT_HASH + echo "CHIA_DEV_BUILD=$CHIA_DEV_BUILD" >>$GITHUB_ENV + aws s3 cp "$GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_arm64.deb" "s3://download.chia.net/dev/chia-blockchain_${CHIA_DEV_BUILD}_arm64.deb" + aws s3 cp "$GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain-cli_${CHIA_INSTALLER_VERSION}-1_arm64.deb" "s3://download.chia.net/dev/chia-blockchain-cli_${CHIA_DEV_BUILD}-1_arm64.deb" - name: Create Checksums if: startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main' @@ -149,6 +150,7 @@ jobs: run: | ls $GITHUB_WORKSPACE/build_scripts/final_installer/ sha256sum $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_arm64.deb > $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_arm64.deb.sha256 + sha256sum $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain-cli_${CHIA_INSTALLER_VERSION}-1_arm64.deb > $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain-cli_${CHIA_INSTALLER_VERSION}-1_arm64.deb.sha256 ls $GITHUB_WORKSPACE/build_scripts/final_installer/ - name: Install py3createtorrent @@ -162,6 +164,7 @@ jobs: CHIA_INSTALLER_VERSION: ${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }} run: | py3createtorrent -f -t udp://tracker.opentrackr.org:1337/announce $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_arm64.deb -o $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_arm64.deb.torrent --webseed https://download.chia.net/install/chia-blockchain_${CHIA_INSTALLER_VERSION}_arm64.deb + py3createtorrent -f -t udp://tracker.opentrackr.org:1337/announce $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain-cli_${CHIA_INSTALLER_VERSION}-1_arm64.deb -o $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain-cli_${CHIA_INSTALLER_VERSION}-1_arm64.deb.torrent --webseed https://download.chia.net/install/chia-blockchain-cli_${CHIA_INSTALLER_VERSION}-1_arm64.deb ls $GITHUB_WORKSPACE/build_scripts/final_installer/ - name: Upload Beta Installer @@ -171,16 +174,21 @@ jobs: run: | aws s3 cp $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_arm64.deb s3://download.chia.net/beta/chia-blockchain_arm64_latest_beta.deb aws s3 cp $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_arm64.deb.sha256 s3://download.chia.net/beta/chia-blockchain_arm64_latest_beta.deb.sha256 + aws s3 cp $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain-cli_${CHIA_INSTALLER_VERSION}-1_arm64.deb s3://download.chia.net/beta/chia-blockchain-cli_arm64_latest_beta.deb + aws s3 cp $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain-cli_${CHIA_INSTALLER_VERSION}-1_arm64.deb.sha256 s3://download.chia.net/beta/chia-blockchain-cli_arm64_latest_beta.deb.sha256 - name: Upload Release Files if: steps.check_secrets.outputs.HAS_SECRET && startsWith(github.ref, 'refs/tags/') env: CHIA_INSTALLER_VERSION: ${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }} run: | - ls $GITHUB_WORKSPACE/build_scripts/final_installer/ - aws s3 cp $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_arm64.deb s3://download.chia.net/install/ - aws s3 cp $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_arm64.deb.sha256 s3://download.chia.net/install/ - aws s3 cp $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_arm64.deb.torrent s3://download.chia.net/torrents/ + ls $GITHUB_WORKSPACE/build_scripts/final_installer/ + aws s3 cp $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_arm64.deb s3://download.chia.net/install/ + aws s3 cp $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_arm64.deb.sha256 s3://download.chia.net/install/ + aws s3 cp $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_arm64.deb.torrent s3://download.chia.net/torrents/ + aws s3 cp $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain-cli_${CHIA_INSTALLER_VERSION}-1_arm64.deb s3://download.chia.net/install/ + aws s3 cp $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain-cli_${CHIA_INSTALLER_VERSION}-1_arm64.deb.sha256 s3://download.chia.net/install/ + aws s3 cp $GITHUB_WORKSPACE/build_scripts/final_installer/chia-blockchain-cli_${CHIA_INSTALLER_VERSION}-1_arm64.deb.torrent s3://download.chia.net/torrents/ - name: Get tag name if: startsWith(github.ref, 'refs/tags/') diff --git a/.github/workflows/build-linux-installer-deb.yml b/.github/workflows/build-linux-installer-deb.yml index 0937822c51f0..2efa7e5176ac 100644 --- a/.github/workflows/build-linux-installer-deb.yml +++ b/.github/workflows/build-linux-installer-deb.yml @@ -178,22 +178,24 @@ jobs: - name: Upload to s3 if: steps.check_secrets.outputs.HAS_SECRET env: - CHIA_INSTALLER_VERSION: ${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }} + CHIA_INSTALLER_VERSION: ${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }} run: | - GIT_SHORT_HASH=$(echo "${GITHUB_SHA}" | cut -c1-8) - CHIA_DEV_BUILD=${CHIA_INSTALLER_VERSION}-$GIT_SHORT_HASH - echo "CHIA_DEV_BUILD=$CHIA_DEV_BUILD" >>$GITHUB_ENV - ls ${{ github.workspace }}/build_scripts/final_installer/ - aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_amd64.deb s3://download.chia.net/dev/chia-blockchain_${CHIA_DEV_BUILD}_amd64.deb + GIT_SHORT_HASH=$(echo "${GITHUB_SHA}" | cut -c1-8) + CHIA_DEV_BUILD=${CHIA_INSTALLER_VERSION}-$GIT_SHORT_HASH + echo "CHIA_DEV_BUILD=$CHIA_DEV_BUILD" >>$GITHUB_ENV + ls ${{ github.workspace }}/build_scripts/final_installer/ + aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_amd64.deb s3://download.chia.net/dev/chia-blockchain_${CHIA_DEV_BUILD}_amd64.deb + aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain-cli_${CHIA_INSTALLER_VERSION}-1_amd64.deb s3://download.chia.net/dev/chia-blockchain-cli_${CHIA_DEV_BUILD}-1_amd64.deb - name: Create Checksums if: startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main' env: CHIA_INSTALLER_VERSION: ${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }} run: | - ls ${{ github.workspace }}/build_scripts/final_installer/ - sha256sum ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_amd64.deb > ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_amd64.deb.sha256 - ls ${{ github.workspace }}/build_scripts/final_installer/ + ls ${{ github.workspace }}/build_scripts/final_installer/ + sha256sum ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_amd64.deb > ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_amd64.deb.sha256 + sha256sum ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain-cli_${CHIA_INSTALLER_VERSION}-1_amd64.deb > ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain-cli_${CHIA_INSTALLER_VERSION}-1_amd64.deb.sha256 + ls ${{ github.workspace }}/build_scripts/final_installer/ - name: Install py3createtorrent if: startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main' @@ -206,6 +208,7 @@ jobs: if: startsWith(github.ref, 'refs/tags/') run: | py3createtorrent -f -t udp://tracker.opentrackr.org:1337/announce ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_amd64.deb -o ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_amd64.deb.torrent --webseed https://download.chia.net/install/chia-blockchain_${CHIA_INSTALLER_VERSION}_amd64.deb + py3createtorrent -f -t udp://tracker.opentrackr.org:1337/announce ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain-cli_${CHIA_INSTALLER_VERSION}-1_amd64.deb -o ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain-cli_${CHIA_INSTALLER_VERSION}-1_amd64.deb.torrent --webseed https://download.chia.net/install/chia-blockchain-cli_${CHIA_INSTALLER_VERSION}-1_amd64.deb ls - name: Upload Beta Installer @@ -215,15 +218,20 @@ jobs: run: | aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_amd64.deb s3://download.chia.net/beta/chia-blockchain_amd64_latest_beta.deb aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_amd64.deb.sha256 s3://download.chia.net/beta/chia-blockchain_amd64_latest_beta.deb.sha256 + aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain-cli_${CHIA_INSTALLER_VERSION}-1_amd64.deb s3://download.chia.net/beta/chia-blockchain-cli_amd64_latest_beta.deb + aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain-cli_${CHIA_INSTALLER_VERSION}-1_amd64.deb.sha256 s3://download.chia.net/beta/chia-blockchain-cli_amd64_latest_beta.deb.sha256 - name: Upload Release Files env: CHIA_INSTALLER_VERSION: ${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }} if: steps.check_secrets.outputs.HAS_SECRET && startsWith(github.ref, 'refs/tags/') run: | - aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_amd64.deb s3://download.chia.net/install/ - aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_amd64.deb.sha256 s3://download.chia.net/install/ - aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_amd64.deb.torrent s3://download.chia.net/torrents/ + aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_amd64.deb s3://download.chia.net/install/ + aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_amd64.deb.sha256 s3://download.chia.net/install/ + aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain_${CHIA_INSTALLER_VERSION}_amd64.deb.torrent s3://download.chia.net/torrents/ + aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain-cli_${CHIA_INSTALLER_VERSION}-1_amd64.deb s3://download.chia.net/install/ + aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain-cli_${CHIA_INSTALLER_VERSION}-1_amd64.deb.sha256 s3://download.chia.net/install/ + aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/chia-blockchain-cli_${CHIA_INSTALLER_VERSION}-1_amd64.deb.torrent s3://download.chia.net/torrents/ - name: Get tag name if: startsWith(github.ref, 'refs/tags/') diff --git a/build_scripts/assets/deb/__init__.py b/build_scripts/assets/deb/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/build_scripts/assets/deb/control.j2 b/build_scripts/assets/deb/control.j2 new file mode 100644 index 000000000000..29e9d2678059 --- /dev/null +++ b/build_scripts/assets/deb/control.j2 @@ -0,0 +1,6 @@ +Package: chia-blockchain-cli +Version: {{ CHIA_INSTALLER_VERSION }} +Architecture: {{ PLATFORM }} +Maintainer: Chia Network Inc +Description: Chia Blockchain + Chia is a modern cryptocurrency built from scratch, designed to be efficient, decentralized, and secure. diff --git a/build_scripts/assets/deb/postinst b/build_scripts/assets/deb/postinst new file mode 100644 index 000000000000..ecd01ea753b2 --- /dev/null +++ b/build_scripts/assets/deb/postinst @@ -0,0 +1,5 @@ +#!/bin/sh + +set -e + +ln -s /opt/chia/chia /usr/local/bin/chia diff --git a/build_scripts/assets/deb/prerm b/build_scripts/assets/deb/prerm new file mode 100644 index 000000000000..30fb724db228 --- /dev/null +++ b/build_scripts/assets/deb/prerm @@ -0,0 +1,5 @@ +#!/bin/sh + +set -e + +unlink /usr/local/bin/chia diff --git a/build_scripts/build_linux_deb.sh b/build_scripts/build_linux_deb.sh index ea493bafe525..bce0ea330111 100644 --- a/build_scripts/build_linux_deb.sh +++ b/build_scripts/build_linux_deb.sh @@ -12,6 +12,7 @@ else PLATFORM="$1" DIR_NAME="chia-blockchain-linux-arm64" fi +export PLATFORM # If the env variable NOTARIZE and the username and password variables are # set, this will attempt to Notarize the signed DMG @@ -21,6 +22,7 @@ if [ ! "$CHIA_INSTALLER_VERSION" ]; then CHIA_INSTALLER_VERSION="0.0.0" fi echo "Chia Installer Version is: $CHIA_INSTALLER_VERSION" +export CHIA_INSTALLER_VERSION echo "Installing npm and electron packagers" cd npm_linux_deb || exit @@ -42,6 +44,20 @@ if [ "$LAST_EXIT_CODE" -ne 0 ]; then exit $LAST_EXIT_CODE fi +# Builds CLI only .deb +# need j2 for templating the control file +pip install j2cli +CLI_DEB_BASE="chia-blockchain-cli_$CHIA_INSTALLER_VERSION-1_$PLATFORM" +mkdir -p "dist/$CLI_DEB_BASE/opt/chia" +mkdir -p "dist/$CLI_DEB_BASE/DEBIAN" +j2 -o "dist/$CLI_DEB_BASE/DEBIAN/control" assets/deb/control.j2 +cp assets/deb/postinst "dist/$CLI_DEB_BASE/DEBIAN/postinst" +cp assets/deb/prerm "dist/$CLI_DEB_BASE/DEBIAN/prerm" +chmod 0755 "dist/$CLI_DEB_BASE/DEBIAN/postinst" "dist/$CLI_DEB_BASE/DEBIAN/prerm" +cp -r dist/daemon/* "dist/$CLI_DEB_BASE/opt/chia/" +dpkg-deb --build --root-owner-group "dist/$CLI_DEB_BASE" +# CLI only .deb done + cp -r dist/daemon ../chia-blockchain-gui/packages/gui cd .. || exit cd chia-blockchain-gui || exit @@ -92,4 +108,7 @@ if [ "$LAST_EXIT_CODE" -ne 0 ]; then exit $LAST_EXIT_CODE fi +# Move the cli only deb into final installers as well, so it gets uploaded as an artifact +mv "dist/$CLI_DEB_BASE.deb" final_installer/ + ls final_installer/ From 511c13e632dad3f208d07fd7c15acf5b18adc6f7 Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Fri, 15 Apr 2022 19:11:42 +0200 Subject: [PATCH 370/378] add tool to generate a blockchain with full blocks, as a benchmark (#11146) --- tests/tools/test_full_sync.py | 2 +- tools/generate_chain.py | 142 ++++++++++++++++++++++++++++++++++ tools/test_constants.py | 18 +++++ tools/test_full_sync.py | 23 ++++-- 4 files changed, 178 insertions(+), 7 deletions(-) create mode 100644 tools/generate_chain.py create mode 100644 tools/test_constants.py diff --git a/tests/tools/test_full_sync.py b/tests/tools/test_full_sync.py index 5b6dcbfa8e44..ad64138ecdae 100644 --- a/tests/tools/test_full_sync.py +++ b/tests/tools/test_full_sync.py @@ -10,4 +10,4 @@ def test_full_sync_test(): file_path = os.path.realpath(__file__) db_file = Path(file_path).parent / "test-blockchain-db.sqlite" - asyncio.run(run_sync_test(db_file, db_version=2, profile=False, single_thread=False)) + asyncio.run(run_sync_test(db_file, db_version=2, profile=False, single_thread=False, test_constants=False)) diff --git a/tools/generate_chain.py b/tools/generate_chain.py new file mode 100644 index 000000000000..4e7a8e942944 --- /dev/null +++ b/tools/generate_chain.py @@ -0,0 +1,142 @@ +import cProfile +import random +import sqlite3 +import time +from contextlib import closing, contextmanager +from pathlib import Path +from typing import Iterator, List + +import zstd + +from chia.types.blockchain_format.coin import Coin +from chia.types.spend_bundle import SpendBundle +from chia.util.chia_logging import initialize_logging +from chia.util.ints import uint64 +from chia.util.path import mkdir +from tests.block_tools import create_block_tools +from tests.util.keyring import TempKeyring +from tools.test_constants import test_constants + + +@contextmanager +def enable_profiler(profile: bool) -> Iterator[None]: + if not profile: + yield + return + + with cProfile.Profile() as pr: + yield + + pr.create_stats() + pr.dump_stats("generate-chain.profile") + + +root_path = Path("./test-chain").resolve() +mkdir(root_path) +with TempKeyring() as keychain: + + bt = create_block_tools(constants=test_constants, root_path=root_path, keychain=keychain) + initialize_logging( + "generate_chain", {"log_level": "DEBUG", "log_stdout": False, "log_syslog": False}, root_path=root_path + ) + + with closing(sqlite3.connect("stress-test-blockchain.sqlite")) as db: + + print("initializing v2 block store") + db.execute( + "CREATE TABLE full_blocks(" + "header_hash blob PRIMARY KEY," + "prev_hash blob," + "height bigint," + "in_main_chain tinyint," + "block blob)" + ) + + wallet = bt.get_farmer_wallet_tool() + coinbase_puzzlehash = wallet.get_new_puzzlehash() + + blocks = bt.get_consecutive_blocks( + 3, + farmer_reward_puzzle_hash=coinbase_puzzlehash, + pool_reward_puzzle_hash=coinbase_puzzlehash, + guarantee_transaction_block=True, + genesis_timestamp=uint64(1234567890), + time_per_block=30, + ) + + unspent_coins: List[Coin] = [] + + for b in blocks: + for coin in b.get_included_reward_coins(): + if coin.puzzle_hash == coinbase_puzzlehash: + unspent_coins.append(coin) + db.execute( + "INSERT INTO full_blocks VALUES(?, ?, ?, ?, ?)", + ( + b.header_hash, + b.prev_header_hash, + b.height, + 1, # in_main_chain + zstd.compress(bytes(b)), + ), + ) + db.commit() + + # build 2000 transaction blocks + with enable_profiler(False): + for k in range(2000): + + start_time = time.monotonic() + + print(f"block: {len(blocks)} unspent: {len(unspent_coins)}") + new_coins: List[Coin] = [] + spend_bundles: List[SpendBundle] = [] + for i in range(1010): + if unspent_coins == []: + break + c = unspent_coins.pop(random.randrange(len(unspent_coins))) + receiver = wallet.get_new_puzzlehash() + bundle = wallet.generate_signed_transaction(uint64(c.amount // 2), receiver, c) + new_coins.extend(bundle.additions()) + spend_bundles.append(bundle) + + coinbase_puzzlehash = wallet.get_new_puzzlehash() + blocks = bt.get_consecutive_blocks( + 1, + blocks, + farmer_reward_puzzle_hash=coinbase_puzzlehash, + pool_reward_puzzle_hash=coinbase_puzzlehash, + guarantee_transaction_block=True, + transaction_data=SpendBundle.aggregate(spend_bundles), + time_per_block=30, + ) + + b = blocks[-1] + for coin in b.get_included_reward_coins(): + if coin.puzzle_hash == coinbase_puzzlehash: + unspent_coins.append(coin) + unspent_coins.extend(new_coins) + + if b.transactions_info: + fill_rate = b.transactions_info.cost / test_constants.MAX_BLOCK_COST_CLVM + else: + fill_rate = 0 + + end_time = time.monotonic() + + print( + f"included {i} spend bundles. fill_rate: {fill_rate*100:.1f}% " + f"new coins: {len(new_coins)} time: {end_time - start_time:0.2f}s" + ) + + db.execute( + "INSERT INTO full_blocks VALUES(?, ?, ?, ?, ?)", + ( + b.header_hash, + b.prev_header_hash, + b.height, + 1, # in_main_chain + zstd.compress(bytes(b)), + ), + ) + db.commit() diff --git a/tools/test_constants.py b/tools/test_constants.py new file mode 100644 index 000000000000..a2c2be4e8c1a --- /dev/null +++ b/tools/test_constants.py @@ -0,0 +1,18 @@ +from chia.consensus.default_constants import DEFAULT_CONSTANTS + +test_constants = DEFAULT_CONSTANTS.replace( + **{ + "MIN_PLOT_SIZE": 20, + "MIN_BLOCKS_PER_CHALLENGE_BLOCK": 12, + "DISCRIMINANT_SIZE_BITS": 16, + "SUB_EPOCH_BLOCKS": 170, + "WEIGHT_PROOF_THRESHOLD": 2, + "WEIGHT_PROOF_RECENT_BLOCKS": 380, + "DIFFICULTY_CONSTANT_FACTOR": 33554432, + "NUM_SPS_SUB_SLOT": 16, # Must be a power of 2 + "MAX_SUB_SLOT_BLOCKS": 50, + "EPOCH_BLOCKS": 340, + "SUB_SLOT_ITERS_STARTING": 2 ** 10, # Must be a multiple of 64 + "NUMBER_ZERO_BITS_PLOT_FILTER": 1, # H(plot signature of the challenge) must start with these many zeroes + } +) diff --git a/tools/test_full_sync.py b/tools/test_full_sync.py index d5e1a0741350..d4088b9bf521 100755 --- a/tools/test_full_sync.py +++ b/tools/test_full_sync.py @@ -18,6 +18,7 @@ from chia.full_node.full_node import FullNode from chia.types.full_block import FullBlock from chia.util.config import load_config +from tools.test_constants import test_constants as TEST_CONSTANTS class ExitOnError(logging.Handler): @@ -46,7 +47,7 @@ def enable_profiler(profile: bool, counter: int) -> Iterator[None]: pr.dump_stats(f"slow-batch-{counter:05d}.profile") -async def run_sync_test(file: Path, db_version, profile: bool, single_thread: bool) -> None: +async def run_sync_test(file: Path, db_version, profile: bool, single_thread: bool, test_constants: bool) -> None: logger = logging.getLogger() logger.setLevel(logging.WARNING) @@ -67,8 +68,11 @@ async def run_sync_test(file: Path, db_version, profile: bool, single_thread: bo chia_init(root_path, should_check_keys=False, v1_db=(db_version == 1)) config = load_config(root_path, "config.yaml") - overrides = config["network_overrides"]["constants"][config["selected_network"]] - constants = DEFAULT_CONSTANTS.replace_str_to_bytes(**overrides) + if test_constants: + constants = TEST_CONSTANTS + else: + overrides = config["network_overrides"]["constants"][config["selected_network"]] + constants = DEFAULT_CONSTANTS.replace_str_to_bytes(**overrides) if single_thread: config["full_node"]["single_threaded"] = True config["full_node"]["db_sync"] = "off" @@ -85,7 +89,7 @@ async def run_sync_test(file: Path, db_version, profile: bool, single_thread: bo counter = 0 height = 0 async with aiosqlite.connect(file) as in_db: - + await in_db.execute("pragma query_only") rows = await in_db.execute( "SELECT header_hash, height, block FROM full_blocks WHERE in_main_chain=1 ORDER BY height" ) @@ -135,6 +139,13 @@ def main() -> None: @click.argument("file", type=click.Path(), required=True) @click.option("--db-version", type=int, required=False, default=2, help="the DB version to use in simulated node") @click.option("--profile", is_flag=True, required=False, default=False, help="dump CPU profiles for slow batches") +@click.option( + "--test-constants", + is_flag=True, + required=False, + default=False, + help="expect the blockchain database to be blocks using the test constants", +) @click.option( "--single-thread", is_flag=True, @@ -142,11 +153,11 @@ def main() -> None: default=False, help="run node in a single process, to include validation in profiles", ) -def run(file: Path, db_version: int, profile: bool, single_thread: bool) -> None: +def run(file: Path, db_version: int, profile: bool, single_thread: bool, test_constants: bool) -> None: """ The FILE parameter should point to an existing blockchain database file (in v2 format) """ - asyncio.run(run_sync_test(Path(file), db_version, profile, single_thread)) + asyncio.run(run_sync_test(Path(file), db_version, profile, single_thread, test_constants)) @main.command("analyze", short_help="generate call stacks for all profiles dumped to current directory") From 82c01ff2bc1b0825e324f012c2ee40a97946136b Mon Sep 17 00:00:00 2001 From: Chris Marslender Date: Fri, 15 Apr 2022 14:13:00 -0500 Subject: [PATCH 371/378] Simplify how the chia symlink is created in the CLI .deb (#11188) --- build_scripts/assets/deb/postinst | 5 ----- build_scripts/assets/deb/prerm | 5 ----- build_scripts/build_linux_deb.sh | 5 ++--- 3 files changed, 2 insertions(+), 13 deletions(-) delete mode 100644 build_scripts/assets/deb/postinst delete mode 100644 build_scripts/assets/deb/prerm diff --git a/build_scripts/assets/deb/postinst b/build_scripts/assets/deb/postinst deleted file mode 100644 index ecd01ea753b2..000000000000 --- a/build_scripts/assets/deb/postinst +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh - -set -e - -ln -s /opt/chia/chia /usr/local/bin/chia diff --git a/build_scripts/assets/deb/prerm b/build_scripts/assets/deb/prerm deleted file mode 100644 index 30fb724db228..000000000000 --- a/build_scripts/assets/deb/prerm +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh - -set -e - -unlink /usr/local/bin/chia diff --git a/build_scripts/build_linux_deb.sh b/build_scripts/build_linux_deb.sh index bce0ea330111..62834020104d 100644 --- a/build_scripts/build_linux_deb.sh +++ b/build_scripts/build_linux_deb.sh @@ -49,12 +49,11 @@ fi pip install j2cli CLI_DEB_BASE="chia-blockchain-cli_$CHIA_INSTALLER_VERSION-1_$PLATFORM" mkdir -p "dist/$CLI_DEB_BASE/opt/chia" +mkdir -p "dist/$CLI_DEB_BASE/usr/bin" mkdir -p "dist/$CLI_DEB_BASE/DEBIAN" j2 -o "dist/$CLI_DEB_BASE/DEBIAN/control" assets/deb/control.j2 -cp assets/deb/postinst "dist/$CLI_DEB_BASE/DEBIAN/postinst" -cp assets/deb/prerm "dist/$CLI_DEB_BASE/DEBIAN/prerm" -chmod 0755 "dist/$CLI_DEB_BASE/DEBIAN/postinst" "dist/$CLI_DEB_BASE/DEBIAN/prerm" cp -r dist/daemon/* "dist/$CLI_DEB_BASE/opt/chia/" +ln -s ../../opt/chia/chia "dist/$CLI_DEB_BASE/usr/bin/chia" dpkg-deb --build --root-owner-group "dist/$CLI_DEB_BASE" # CLI only .deb done From 36a610f038f1ca70cd436e0061432b97364146a8 Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Sat, 16 Apr 2022 01:23:37 +0200 Subject: [PATCH 372/378] fix block_tools feature when specifying a list of block references. Also add feature keep_going_until_tx_block. (#11185) --- tests/block_tools.py | 20 +++++++++++++++----- tests/blockchain/test_blockchain.py | 8 +++++++- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/tests/block_tools.py b/tests/block_tools.py index 079ddde28f9e..9a7317297394 100644 --- a/tests/block_tools.py +++ b/tests/block_tools.py @@ -419,6 +419,7 @@ def get_consecutive_blocks( self, num_blocks: int, block_list_input: List[FullBlock] = None, + *, farmer_reward_puzzle_hash: Optional[bytes32] = None, pool_reward_puzzle_hash: Optional[bytes32] = None, transaction_data: Optional[SpendBundle] = None, @@ -427,6 +428,7 @@ def get_consecutive_blocks( force_overflow: bool = False, skip_slots: int = 0, # Force at least this number of empty slots before the first SB guarantee_transaction_block: bool = False, # Force that this block must be a tx block + keep_going_until_tx_block: bool = False, # keep making new blocks until we find a tx block normalized_to_identity_cc_eos: bool = False, normalized_to_identity_icc_eos: bool = False, normalized_to_identity_cc_sp: bool = False, @@ -566,7 +568,8 @@ def get_consecutive_blocks( removals = None if transaction_data_included: transaction_data = None - if transaction_data is not None and not transaction_data_included: + previous_generator = None + if transaction_data is not None: additions = transaction_data.additions() removals = transaction_data.removals() assert start_timestamp is not None @@ -582,9 +585,10 @@ def get_consecutive_blocks( else: pool_target = PoolTarget(self.pool_ph, uint32(0)) + block_generator: Optional[BlockGenerator] if transaction_data is not None: if type(previous_generator) is CompressorArg: - block_generator: Optional[BlockGenerator] = best_solution_generator_from_template( + block_generator = best_solution_generator_from_template( previous_generator, transaction_data ) else: @@ -629,6 +633,8 @@ def get_consecutive_blocks( ) if block_record.is_transaction_block: transaction_data_included = True + previous_generator = None + keep_going_until_tx_block = False else: if guarantee_transaction_block: continue @@ -650,7 +656,7 @@ def get_consecutive_blocks( latest_block = blocks[full_block.header_hash] finished_sub_slots_at_ip = [] num_blocks -= 1 - if num_blocks == 0: + if num_blocks <= 0 and not keep_going_until_tx_block: return block_list # Finish the end of sub-slot and try again next sub-slot @@ -789,7 +795,7 @@ def get_consecutive_blocks( removals = None if transaction_data_included: transaction_data = None - if transaction_data is not None and not transaction_data_included: + if transaction_data is not None: additions = transaction_data.additions() removals = transaction_data.removals() sub_slots_finished += 1 @@ -856,6 +862,8 @@ def get_consecutive_blocks( ) else: block_generator = simple_solution_generator(transaction_data) + if type(previous_generator) is list: + block_generator = BlockGenerator(block_generator.program, [], previous_generator) aggregate_signature = transaction_data.aggregated_signature else: block_generator = None @@ -895,6 +903,8 @@ def get_consecutive_blocks( if block_record.is_transaction_block: transaction_data_included = True + previous_generator = None + keep_going_until_tx_block = False elif guarantee_transaction_block: continue if pending_ses: @@ -911,7 +921,7 @@ def get_consecutive_blocks( blocks_added_this_sub_slot += 1 log.info(f"Created block {block_record.height } ov=True, iters " f"{block_record.total_iters}") num_blocks -= 1 - if num_blocks == 0: + if num_blocks <= 0 and not keep_going_until_tx_block: return block_list blocks[full_block.header_hash] = block_record diff --git a/tests/blockchain/test_blockchain.py b/tests/blockchain/test_blockchain.py index f6d9852bc85f..c413b6440329 100644 --- a/tests/blockchain/test_blockchain.py +++ b/tests/blockchain/test_blockchain.py @@ -3280,7 +3280,13 @@ async def test_reorg_flip_flop(empty_blockchain, bt): ) spend_bundle = wallet_a.generate_signed_transaction(1000, receiver_puzzlehash, all_coins.pop()) - chain_a = bt.get_consecutive_blocks(5, chain_a, previous_generator=[uint32(10)], transaction_data=spend_bundle) + chain_a = bt.get_consecutive_blocks( + 5, + chain_a, + previous_generator=[uint32(10)], + transaction_data=spend_bundle, + guarantee_transaction_block=True, + ) spend_bundle = wallet_a.generate_signed_transaction(1000, receiver_puzzlehash, all_coins.pop()) chain_a = bt.get_consecutive_blocks( From 7567b4ea9c17124041e58ba69ccfc47652da4307 Mon Sep 17 00:00:00 2001 From: Chris Marslender Date: Mon, 18 Apr 2022 10:18:59 -0500 Subject: [PATCH 373/378] Fix filename of latest intel dev installer (#11203) --- .github/workflows/build-macos-installer.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-macos-installer.yml b/.github/workflows/build-macos-installer.yml index 04c7bff453b2..09597f3ef29d 100644 --- a/.github/workflows/build-macos-installer.yml +++ b/.github/workflows/build-macos-installer.yml @@ -190,8 +190,8 @@ jobs: AWS_SECRET_ACCESS_KEY: ${{ secrets.INSTALLER_UPLOAD_SECRET }} AWS_REGION: us-west-2 run: | - aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg s3://download.chia.net/beta/Chia_latest_beta.dmg - aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg.sha256 s3://download.chia.net/beta/Chia_latest_beta.dmg.sha256 + aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg s3://download.chia.net/beta/Chia-intel_latest_beta.dmg + aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg.sha256 s3://download.chia.net/beta/Chia-intel_latest_beta.dmg.sha256 - name: Upload Release Files if: steps.check_secrets.outputs.HAS_AWS_SECRET && startsWith(github.ref, 'refs/tags/') From 10f7bcde5c6e2a180328e239bcbbaa20121e3a12 Mon Sep 17 00:00:00 2001 From: Chris Marslender Date: Mon, 18 Apr 2022 15:58:44 -0500 Subject: [PATCH 374/378] Add start_crawler and start_seeder to pyinstaller config (#11205) --- chia/pyinstaller.spec | 3 +++ 1 file changed, 3 insertions(+) diff --git a/chia/pyinstaller.spec b/chia/pyinstaller.spec index 08769306086e..6d81c943a30d 100644 --- a/chia/pyinstaller.spec +++ b/chia/pyinstaller.spec @@ -189,6 +189,9 @@ add_binary("daemon", f"{ROOT}/chia/daemon/server.py", COLLECT_ARGS) for server in SERVERS: add_binary(f"start_{server}", f"{ROOT}/chia/server/start_{server}.py", COLLECT_ARGS) +add_binary("start_crawler", f"{ROOT}/chia/seeder/start_crawler.py", COLLECT_ARGS) +add_binary("start_seeder", f"{ROOT}/chia/seeder/dns_server.py", COLLECT_ARGS) + COLLECT_KWARGS = dict( strip=False, upx_exclude=[], From 93a61eece1ab537f40cf3daf76be69dfce405762 Mon Sep 17 00:00:00 2001 From: Chris Marslender Date: Mon, 18 Apr 2022 16:34:27 -0500 Subject: [PATCH 375/378] Pin mac intel installer to 10.15 (#11209) --- .github/workflows/build-macos-installer.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-macos-installer.yml b/.github/workflows/build-macos-installer.yml index 09597f3ef29d..e7cd7e6399f7 100644 --- a/.github/workflows/build-macos-installer.yml +++ b/.github/workflows/build-macos-installer.yml @@ -25,7 +25,7 @@ jobs: max-parallel: 4 matrix: python-version: [3.9] - os: [macOS-latest] + os: [macos-10.15] steps: - name: Checkout Code From d3e73a75ab44c799f8b6f9a76fab550ad6d7824a Mon Sep 17 00:00:00 2001 From: Chris Marslender Date: Mon, 18 Apr 2022 16:42:09 -0500 Subject: [PATCH 376/378] Revert "Pin mac intel installer to 10.15 (#11209)" (#11210) This reverts commit 93a61eece1ab537f40cf3daf76be69dfce405762. --- .github/workflows/build-macos-installer.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-macos-installer.yml b/.github/workflows/build-macos-installer.yml index e7cd7e6399f7..09597f3ef29d 100644 --- a/.github/workflows/build-macos-installer.yml +++ b/.github/workflows/build-macos-installer.yml @@ -25,7 +25,7 @@ jobs: max-parallel: 4 matrix: python-version: [3.9] - os: [macos-10.15] + os: [macOS-latest] steps: - name: Checkout Code From 4a6c1903b780dd9ab83727834af3cbc019bbdeee Mon Sep 17 00:00:00 2001 From: William Allen Date: Tue, 19 Apr 2022 12:38:44 -0500 Subject: [PATCH 377/378] Adding changelog (#11223) --- CHANGELOG.md | 73 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 343909b93ca3..e8a9a7844f94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,76 @@ for setuptools_scm/PEP 440 reasons. ## [Unreleased] +## 1.3.4 Chia blockchain 2022-4-19 + +## What's Changed + +### Added + +- Creating an offer now allows you to edit the exchange between two tokens that will auto calculate either the sending token amount or the receiving token amount +- When making an offer, makers can now create an offer including a fee to help get the transaction into the mempool when an offer is accepted +- Implemented `chia rpc` command +- New RPC `get_coin_records_by_hint` - Get coins for a given hint (Thanks @freddiecoleman) +- Add maker fee to remaining offer RPCs +- Add healthcheck endpoint to rpc services +- Optional wallet type parameter for `get_wallets` and `wallet show` +- Add `select_coins` RPC method by (Thanks @ftruzzi) +- Added `-n`/`--new-address` option to `chia wallet get_address` +- New DBWrapper supporting concurrent readers +- Added `config.yaml` option to run the `full_node` in single-threaded mode +- Build cli only version of debs +- Add `/get_stray_cats` API for accessing unknown CATs + +### Changed + +- Left navigation bar in the GUI has been reorganized and icons have been updated +- Settings has been moved to the new left hand nav bar +- Token selection has been changed to a permanent column in the GUI instead of the drop down list along +- Manage token option has been added at the bottom of the Token column to all users to show/hide token wallets +- Users can show/hide token wallets. If you have auto-discover cats in config.yaml turned off, new tokens will still show up there, but those wallets won’t get created until the token has been toggled on for the first time +- CATs now have a link to Taildatabase.com to look up the Asset ID +- Ongoing improvements to the internal test framework for speed and reliability. +- Significant harvester protocol update: You will need to update your farmer and all your harvesters as this is a breaking change in the harvester protocol. The new protocol solves many scaling issues. In particular, the protocol supports sending delta changes to the farmer - so for example, adding plots to a farm results in only the new plots being reported. We recommend you update your farmer first. +- Updated clvm_tools to 0.4.4 +- Updated clvm_tools_rs to 0.1.7 +- Changed code to use by default the Rust implementation of clvm_tools (clvm_tools_rs) +- Consolidated socket library to aiohttp and removed websockets dependency +- During node startup, missing blocks in the DB will throw an exception +- Updated cryptography to 36.0.2 +- The rust implementation of CLVM is now called `chia_rs` instead of `clvm_rs`. +- Updated code to use improved rust interface `run_generator2` +- Code improvements to prefer connecting to a local trusted node over untrusted nodes + +### Fixed + +- Fixed issues with claiming self-pool rewards with and without a fee +- Fixed wallet creation in edge cases around chain reorgs +- Harvester: Reuse legacy refresh interval if new params aren't available +- Fixed typos `lastest` > `latest` (Thanks @daverof) +- Fixed typo in command line argument parsing for `chia db validate` +- Improved backwards compatibility for node RPC calls `get_blockchain_state` and `get_additions_and_removals` +- Fixed issue where `--root_path` option was not honored by `chia configure` CLI command +- Fixed cases where node DB was not created initially using v2 format +- Improved error messages from `chia db upgrade` +- Capitalized display of `Rpc` -> `RPC` in `chia show -s` by (Thanks @hugepants) +- Improved handling of chain reorgs with atomic rollback for the wallet +- Handled cases where one node doesn't have the coin we are looking for +- Fixed timelord installation for Debian +- Checked for requesting items when creating an offer +- Minor output formatting/enhancements for `chia wallet show` +- Fixed typo and index issues in wallet database +- Used the rust clvm version instead of python in more places +- Fixed trailing bytes shown in CAT asset ID row when using `chia wallet show` +- Maintain all chain state during reorg until the new fork has been fully validated +- Improved performance of `get_coin_records_by_names` by using proper index (Thanks @roseiliend) +- Improved handling of unknown pending balances +- Improved plot load times + +### Known Issues + +- You cannot install and run chia blockchain using the macOS packaged DMG on macOS Mojave (10.14). +- Pending transactions are not retried correctly and so can be stuck in the pending state unless manually removed and re-submitted + ## 1.3.3 Chia blockchain 2022-4-02 ### Fixed @@ -49,7 +119,6 @@ for setuptools_scm/PEP 440 reasons. - Update the database queries for the `block_count_metrics` RPC endpoint to utilize indexes effectively for V2 DBs. - Several improvements to tests. - ## 1.3.0 Chia blockchain 2022-3-07 ### Added: @@ -139,7 +208,6 @@ for setuptools_scm/PEP 440 reasons. - Workaround: Restart the GUI, or clear unconfirmed TX. - Claiming rewards when self-pooling using CLI will show an error message, but it will actually create the transaction. - ## 1.2.11 Chia blockchain 2021-11-4 Farmers rejoice: today's release integrates two plotters in broad use in the Chia community: Bladebit, created by @harold-b, and Madmax, created by @madMAx43v3r. Both of these plotters bring significant improvements in plotting time. More plotting info [here](https://github.com/Chia-Network/chia-blockchain/wiki/Alternative--Plotters). @@ -174,7 +242,6 @@ This release also includes several important performance improvements as a resul - PlotNFT transactions via CLI (e.g. `chia plotnft join`) now accept a fee parameter, but it is not yet operable. - ## 1.2.10 Chia blockchain 2021-10-25 We have some great improvements in this release: We launched our migration of keys to a common encrypted keyring.yaml file, and we secure this with an optional passphrase in both GUI and CLI. We've added a passphrase hint in case you forget your passphrase. More info on our [wiki](https://github.com/Chia-Network/chia-blockchain/wiki/Passphrase-Protected-Chia-Keys-and-Key-Storage-Migration). We also launched a new Chialisp compiler in clvm_tools_rs which substantially improves compile time for Chialisp developers. We also addressed a widely reported issue in which a system failure, such as a power outage, would require some farmers to sync their full node from zero. This release also includes several other improvements and fixes. From d154105a6b35f94649f15bca4e3fb8a11a39e70e Mon Sep 17 00:00:00 2001 From: William Allen Date: Tue, 19 Apr 2022 12:38:55 -0500 Subject: [PATCH 378/378] Adding changelog (#11223)

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%0AH@Nhb6tXi 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%0D5&ZpwP`@jmZg^2_5dg9DVAO%Wu}88L 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 Rax@`;T%gp#x`XJB@n0kn(&PXD delta 385338 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 z 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%0D>itu{2xK9tda3Qq>< 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%0D)^x1v#Ou{tiDixpqF-0Aai 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 z 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