From 6711f095b3adb8673a0f51e0484b85ee879172db Mon Sep 17 00:00:00 2001 From: Izumi Hoshino Date: Sat, 30 Aug 2025 01:43:20 +0900 Subject: [PATCH 01/16] Updated GUI pin (#20008) --- chia-blockchain-gui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia-blockchain-gui b/chia-blockchain-gui index 4969c3b9186c..fa06cf12453f 160000 --- a/chia-blockchain-gui +++ b/chia-blockchain-gui @@ -1 +1 @@ -Subproject commit 4969c3b9186cf1db87a79ad0486b51327f8375dc +Subproject commit fa06cf12453f4ac0f60d65fca0b62a1d85272b29 From 5f151e53c956eca67a031cf2677e826add4c467f Mon Sep 17 00:00:00 2001 From: Amine Khaldi Date: Fri, 5 Sep 2025 12:05:17 +0100 Subject: [PATCH 02/16] Update the GUI anchor. --- chia-blockchain-gui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia-blockchain-gui b/chia-blockchain-gui index fa06cf12453f..a80ef593caae 160000 --- a/chia-blockchain-gui +++ b/chia-blockchain-gui @@ -1 +1 @@ -Subproject commit fa06cf12453f4ac0f60d65fca0b62a1d85272b29 +Subproject commit a80ef593caae2cc112762e535d184dfa95f099c2 From 653944328e835eebafd8b8b699b94ac443d63fb2 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Mon, 8 Sep 2025 13:39:43 -0400 Subject: [PATCH 03/16] move workaround arm64 ruby install to the build image (#20037) * Remove arm64 ruby installation workaround Removed temporary fix for arm64 platform related to ruby installation. depends on https://github.com/Chia-Network/build-images/pull/105 * drop fpm, update comment --- build_scripts/build_linux_deb-2-installer.sh | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/build_scripts/build_linux_deb-2-installer.sh b/build_scripts/build_linux_deb-2-installer.sh index 1d70340f4f1d..e059f5412cf8 100644 --- a/build_scripts/build_linux_deb-2-installer.sh +++ b/build_scripts/build_linux_deb-2-installer.sh @@ -86,12 +86,9 @@ jq --arg VER "$CHIA_INSTALLER_VERSION" '.version=$VER' package.json >temp.json & echo "Building Linux(deb) Electron app" PRODUCT_NAME="chia" if [ "$PLATFORM" = "arm64" ]; then - # electron-builder does not work for arm64 as of Aug 16, 2022. - # This is a temporary fix. # https://github.com/jordansissel/fpm/issues/1801#issuecomment-919877499 - # @TODO Consolidates the process to amd64 if the issue of electron-builder is resolved - sudo apt-get -y install ruby ruby-dev - sudo gem install fpm + # workaround for above now implemented in the image build at + # https://github.com/Chia-Network/build-images/blob/7c74d2f20739543c486c2522032cf09d96396d24/ubuntu-22.04/Dockerfile#L48-L61 echo USE_SYSTEM_FPM=true "${NPM_PATH}/electron-builder" build --linux deb --arm64 \ --config.extraMetadata.name=chia-blockchain \ --config.productName="$PRODUCT_NAME" --config.linux.desktop.Name="Chia Blockchain" \ From b872728304794c646047227adb0393a7da6244e9 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Tue, 9 Sep 2025 12:05:26 -0400 Subject: [PATCH 04/16] add another case to `test_get_rate_limits_to_use()` (#20004) --- chia/_tests/core/server/test_rate_limits.py | 1 + 1 file changed, 1 insertion(+) diff --git a/chia/_tests/core/server/test_rate_limits.py b/chia/_tests/core/server/test_rate_limits.py index bf4cef41c768..ea768dace048 100644 --- a/chia/_tests/core/server/test_rate_limits.py +++ b/chia/_tests/core/server/test_rate_limits.py @@ -42,6 +42,7 @@ def advance(self, duration: float) -> None: @pytest.mark.anyio async def test_get_rate_limits_to_use() -> None: assert get_rate_limits_to_use(rl_v2, rl_v2) != get_rate_limits_to_use(rl_v2, rl_v1) + assert get_rate_limits_to_use(rl_v2, rl_v2) != get_rate_limits_to_use(rl_v1, rl_v2) assert get_rate_limits_to_use(rl_v1, rl_v1) == get_rate_limits_to_use(rl_v2, rl_v1) assert get_rate_limits_to_use(rl_v1, rl_v1) == get_rate_limits_to_use(rl_v1, rl_v2) From d62a61da375e343a9efcce1c34af432583e7a6df Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Tue, 9 Sep 2025 18:06:02 +0200 Subject: [PATCH 05/16] [CHIA-3701] Check plots v2 (#19982) * support v2 plots in check-plots * review comments --- chia/plotting/check_plots.py | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/chia/plotting/check_plots.py b/chia/plotting/check_plots.py index 443510e5f734..6308cfcc61f1 100644 --- a/chia/plotting/check_plots.py +++ b/chia/plotting/check_plots.py @@ -3,16 +3,20 @@ import concurrent.futures import logging from collections import Counter +from collections.abc import Sequence from pathlib import Path from threading import Lock from time import monotonic, sleep -from typing import Optional +from typing import Optional, Union from chia_rs import G1Element +from chia_rs.sized_bytes import bytes32 from chia_rs.sized_ints import uint8, uint32 from chiapos import Verifier +from chia.consensus.default_constants import DEFAULT_CONSTANTS from chia.plotting.manager import PlotManager +from chia.plotting.prover import PlotVersion from chia.plotting.util import ( PlotInfo, PlotRefreshEvents, @@ -22,6 +26,10 @@ get_plot_filenames, parse_plot_info, ) +from chia.types.blockchain_format.proof_of_space import ( + quality_for_partial_proof, + solve_proof, +) from chia.util.bech32m import encode_puzzle_hash from chia.util.config import load_config from chia.util.cpu import available_logical_cores @@ -168,12 +176,20 @@ def process_plot(plot_path: Path, plot_info: PlotInfo, num_start: int, num_end: total_proofs = 0 caught_exception: bool = False + version = pr.get_version() for i in range(num_start, num_end): challenge = std_hash(i.to_bytes(32, "big")) + # these are either qualities (v1) or partial proofs (v2) + proofs: Sequence[Union[bytes32, bytes]] # Some plot errors cause get_qualities_for_challenge to throw a RuntimeError try: quality_start_time = round(monotonic() * 1000) - qualities = pr.get_qualities_for_challenge(challenge) + if version == PlotVersion.V1: + proofs = pr.get_qualities_for_challenge(challenge) + else: + proofs = pr.get_partial_proofs_for_challenge( + challenge, DEFAULT_CONSTANTS.PLOT_DIFFICULTY_INITIAL + ) quality_spent_time = round(monotonic() * 1000) - quality_start_time if quality_spent_time > 8000: log.warning( @@ -201,13 +217,19 @@ def process_plot(plot_path: Path, plot_info: PlotInfo, num_start: int, num_end: caught_exception = True break - for index, quality_str in enumerate(qualities): + for index, proof in enumerate(proofs): # Other plot errors cause get_full_proof or validate_proof to throw an AssertionError try: proof_start_time = round(monotonic() * 1000) - # TODO : todo_v2_plots handle v2 plots - proof = pr.get_full_proof(challenge, index, parallel_read) - proof_spent_time = round(monotonic() * 1000) - proof_start_time + if version == PlotVersion.V1: + quality_str = bytes32(proof) + full_proof = pr.get_full_proof(challenge, index, parallel_read) + proof_spent_time = round(monotonic() * 1000) - proof_start_time + else: + quality_str = quality_for_partial_proof(proof, challenge) + proof_spent_time = round(monotonic() * 1000) - proof_start_time + full_proof = solve_proof(proof) + if proof_spent_time > 15000: log.warning( f"\tFinding proof took: {proof_spent_time} ms. This should be below 15 seconds " @@ -216,7 +238,7 @@ def process_plot(plot_path: Path, plot_info: PlotInfo, num_start: int, num_end: else: log.info(f"\tFinding proof took: {proof_spent_time} ms. Filepath: {plot_path}") - ver_quality_str = v.validate_proof(pr.get_id(), pr.get_size().size_v1, challenge, proof) + ver_quality_str = v.validate_proof(pr.get_id(), pr.get_size().size_v1, challenge, full_proof) if quality_str == ver_quality_str: total_proofs += 1 else: From 3acce7b555ce644bd0d69042c4e0876d0d7331fa Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Tue, 9 Sep 2025 15:49:26 -0400 Subject: [PATCH 06/16] test on linux arm :penguin: :muscle: :boom: (#20010) * test on linux arm * Update Linux test parameters in workflow Renamed and added parameters for Linux test runs. * more * fixup --- .github/workflows/test.yml | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9302e1c19b26..829eac8879f7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,8 +40,13 @@ on: default: 1 required: false type: number - run-linux: - description: "run Linux tests" + run-linux-intel: + description: "run Linux-intel tests" + default: true + required: false + type: boolean + run-linux-arm: + description: "run Linux-arm tests" default: true required: false type: boolean @@ -151,22 +156,38 @@ jobs: arch: arm arch-emoji: 💪 collect-junit: false - ubuntu: - if: github.event_name != 'workflow_dispatch' || inputs.run-linux + ubuntu-intel: + if: github.event_name != 'workflow_dispatch' || inputs.run-linux-intel uses: ./.github/workflows/test-single.yml needs: configure with: os-emoji: 🐧 matrix: ubuntu name: Ubuntu - file_name: ubuntu - concurrency-name: ubuntu + file_name: ubuntu-intel + concurrency-name: ubuntu-intel configuration: ${{ needs.configure.outputs.configuration }} matrix_mode: ${{ needs.configure.outputs.matrix_mode }} runs-on: ubuntu-latest arch: intel arch-emoji: 🌀 collect-junit: false + ubuntu-arm: + if: github.event_name != 'workflow_dispatch' || inputs.run-linux-arm + uses: ./.github/workflows/test-single.yml + needs: configure + with: + os-emoji: 🐧 + matrix: ubuntu + name: Ubuntu + file_name: ubuntu-arm + concurrency-name: ubuntu-arm + configuration: ${{ needs.configure.outputs.configuration }} + matrix_mode: ${{ needs.configure.outputs.matrix_mode }} + runs-on: ubuntu-24.04-arm + arch: arm + arch-emoji: 💪 + collect-junit: false windows: if: github.event_name != 'workflow_dispatch' || inputs.run-windows uses: ./.github/workflows/test-single.yml @@ -192,7 +213,8 @@ jobs: - configure - macos-intel - macos-arm - - ubuntu + - ubuntu-intel + - ubuntu-arm - windows strategy: fail-fast: false From 0396922f57bed965d8c858e9b29db98854c3784b Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Tue, 9 Sep 2025 15:50:16 -0400 Subject: [PATCH 07/16] difficulty -> strength (#20045) --- chia/plotting/check_plots.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/chia/plotting/check_plots.py b/chia/plotting/check_plots.py index 6308cfcc61f1..848c0300874b 100644 --- a/chia/plotting/check_plots.py +++ b/chia/plotting/check_plots.py @@ -187,9 +187,7 @@ def process_plot(plot_path: Path, plot_info: PlotInfo, num_start: int, num_end: if version == PlotVersion.V1: proofs = pr.get_qualities_for_challenge(challenge) else: - proofs = pr.get_partial_proofs_for_challenge( - challenge, DEFAULT_CONSTANTS.PLOT_DIFFICULTY_INITIAL - ) + proofs = pr.get_partial_proofs_for_challenge(challenge, DEFAULT_CONSTANTS.PLOT_STRENGTH_INITIAL) quality_spent_time = round(monotonic() * 1000) - quality_start_time if quality_spent_time > 8000: log.warning( From 81a0a957d96efc30b2159a14a32de9095424bc56 Mon Sep 17 00:00:00 2001 From: Amine Khaldi Date: Wed, 10 Sep 2025 20:30:42 +0100 Subject: [PATCH 08/16] CHIA-3731 Cover peak post processing w.r.t. added transactions as a result of retrying potential transactions (#20044) Cover with a test peak processing w.r.t. added transactions as a result of retrying PendingTXCache items. --- chia/_tests/core/full_node/test_full_node.py | 59 ++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/chia/_tests/core/full_node/test_full_node.py b/chia/_tests/core/full_node/test_full_node.py index 95f37f722609..9b70e3c23ee5 100644 --- a/chia/_tests/core/full_node/test_full_node.py +++ b/chia/_tests/core/full_node/test_full_node.py @@ -3294,3 +3294,62 @@ def compare_unfinished_blocks(block1: UnfinishedBlock, block2: UnfinishedBlock) # Final assertion to check the entire block assert block1 == block2, "The entire block objects are not identical" return True + + +@pytest.mark.anyio +@pytest.mark.parametrize( + "condition, error", + [ + (ConditionOpcode.ASSERT_HEIGHT_RELATIVE, "ASSERT_HEIGHT_RELATIVE_FAILED"), + (ConditionOpcode.ASSERT_HEIGHT_ABSOLUTE, "ASSERT_HEIGHT_ABSOLUTE_FAILED"), + ], +) +async def test_pending_tx_cache_retry_on_new_peak( + condition: ConditionOpcode, error: str, blockchain_constants: ConsensusConstants, caplog: pytest.LogCaptureFixture +) -> None: + """ + Covers PendingTXCache items that are placed there due to unmet relative or + absolute height conditions, to make sure those items get retried at peak + post processing when those conditions are met. + """ + async with setup_simulators_and_wallets(1, 0, blockchain_constants) as new: + full_node_api = new.simulators[0].peer_api + bt = new.bt + wallet = WalletTool(test_constants) + ph = wallet.get_new_puzzlehash() + blocks = bt.get_consecutive_blocks( + 3, guarantee_transaction_block=True, farmer_reward_puzzle_hash=ph, pool_reward_puzzle_hash=ph + ) + for block in blocks: + await full_node_api.full_node.add_block(block) + peak = full_node_api.full_node.blockchain.get_peak() + assert peak is not None + current_height = peak.height + # Create a transaction with a height condition that makes it pending + coin = blocks[-1].get_included_reward_coins()[0] + if condition == ConditionOpcode.ASSERT_HEIGHT_RELATIVE: + condition_height = 1 + else: + condition_height = current_height + 1 + condition_dic = {condition: [ConditionWithArgs(condition, [int_to_bytes(condition_height)])]} + sb = wallet.generate_signed_transaction(uint64(42), ph, coin, condition_dic) + sb_name = sb.name() + # Send the transaction + res = await full_node_api.send_transaction(SendTransaction(sb)) + assert res is not None + assert ProtocolMessageTypes(res.type) == ProtocolMessageTypes.transaction_ack + transaction_ack = TransactionAck.from_bytes(res.data) + assert transaction_ack.status == MempoolInclusionStatus.PENDING.value + assert transaction_ack.error == error + # Make sure it ends up in the pending cache, not the mempool + assert full_node_api.full_node.mempool_manager.get_mempool_item(sb_name, include_pending=False) is None + assert full_node_api.full_node.mempool_manager.get_mempool_item(sb_name, include_pending=True) is not None + # Advance peak to meet the asserted height condition + with caplog.at_level(logging.DEBUG): + blocks = bt.get_consecutive_blocks(2, block_list_input=blocks, guarantee_transaction_block=True) + for block in blocks: + await full_node_api.full_node.add_block(block) + # This should trigger peak post processing with the added transaction + assert f"Added transaction to mempool: {sb_name}\n" in caplog.text + # Make sure the transaction was retried and got added to the mempool + assert full_node_api.full_node.mempool_manager.get_mempool_item(sb_name, include_pending=False) is not None From 326b1084ce483cadcda7a8641f43e7e829dfb3cf Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Wed, 10 Sep 2025 12:31:00 -0700 Subject: [PATCH 09/16] [CHIA-3729] Fix clvm streamable type analysis (#20031) * Use fully recursive JSON logic for compound clvm streamable types * Accomodate rust types * Accomodate dicts * Broaden function to handle type[object] instead of type[Streamable] (with a little mypy workaround) * Whoops, delete that * Small bug * Okay a complete rework * Delete some now unused stuff * Some edge cases --- chia/util/streamable.py | 8 ++-- chia/wallet/util/clvm_streamable.py | 69 ++++++++++++----------------- 2 files changed, 33 insertions(+), 44 deletions(-) diff --git a/chia/util/streamable.py b/chia/util/streamable.py index ccca8ecb5d48..eda72959aceb 100644 --- a/chia/util/streamable.py +++ b/chia/util/streamable.py @@ -3,6 +3,7 @@ from __future__ import annotations import dataclasses +import functools import io import os import pprint @@ -210,7 +211,7 @@ def streamable_from_dict(klass: type[_T_Streamable], item: Any) -> _T_Streamable def function_to_convert_one_item( - f_type: type[Any], json_parser: Optional[Callable[[object], Streamable]] = None + f_type: type[Any], json_parser: Optional[Callable[[object, type[object]], Streamable]] = None ) -> ConvertFunctionType: if is_type_SpecificOptional(f_type): convert_inner_func = function_to_convert_one_item(get_args(f_type)[0], json_parser) @@ -234,8 +235,9 @@ def function_to_convert_one_item( return lambda mapping: convert_dict(key_converter, value_converter, mapping) # type: ignore[arg-type] elif hasattr(f_type, "from_json_dict"): if json_parser is None: - json_parser = f_type.from_json_dict - return json_parser + return f_type.from_json_dict # type: ignore[no-any-return] + else: + return functools.partial(json_parser, streamable_type=f_type) # type: ignore[call-arg] elif issubclass(f_type, bytes): # Type is bytes, data is a hex string or bytes return lambda item: convert_byte_type(f_type, item) diff --git a/chia/wallet/util/clvm_streamable.py b/chia/wallet/util/clvm_streamable.py index 7849970783bb..7abc51612487 100644 --- a/chia/wallet/util/clvm_streamable.py +++ b/chia/wallet/util/clvm_streamable.py @@ -3,17 +3,16 @@ import dataclasses import functools from types import MappingProxyType -from typing import Any, Callable, Generic, Optional, TypeVar, Union, get_args, get_type_hints +from typing import Any, Callable, Generic, Optional, TypeVar, Union, get_type_hints from hsms.clvm_serde import from_program_for_type, to_program_for_type from typing_extensions import TypeGuard from chia.types.blockchain_format.program import Program +from chia.util.byte_types import hexstr_to_bytes from chia.util.streamable import ( Streamable, function_to_convert_one_item, - is_type_List, - is_type_SpecificOptional, is_type_Tuple, recurse_jsonify, streamable, @@ -94,14 +93,10 @@ def byte_deserialize_clvm_streamable( ) -def is_compound_type(typ: Any) -> bool: - return is_type_SpecificOptional(typ) or is_type_Tuple(typ) or is_type_List(typ) - - # TODO: this is more than _just_ a Streamable, but it is also a Streamable and that's # useful for now -def is_clvm_streamable_type(v: type[object]) -> TypeGuard[type[Streamable]]: - return issubclass(v, Streamable) and hasattr(v, "_clvm_streamable") +def is_clvm_streamable_type(v: type[object]) -> bool: + return isinstance(v, type) and issubclass(v, Streamable) and hasattr(v, "_clvm_streamable") # TODO: this is more than _just_ a Streamable, but it is also a Streamable and that's @@ -115,48 +110,40 @@ def json_deserialize_with_clvm_streamable( streamable_type: type[_T_Streamable], translation_layer: Optional[TranslationLayer] = None, ) -> _T_Streamable: - if isinstance(json_dict, str): + # This function is flawed for compound types because it's highjacking the function_to_convert_one_item func + # which does not call back to it. More examination is needed. + if is_clvm_streamable_type(streamable_type) and isinstance(json_dict, str): return byte_deserialize_clvm_streamable( - bytes.fromhex(json_dict), streamable_type, translation_layer=translation_layer + hexstr_to_bytes(json_dict), streamable_type, translation_layer=translation_layer ) - else: + elif hasattr(streamable_type, "streamable_fields"): old_streamable_fields = streamable_type.streamable_fields() new_streamable_fields = [] for old_field in old_streamable_fields: - if is_compound_type(old_field.type): - inner_type = get_args(old_field.type)[0] - if is_clvm_streamable_type(inner_type): - new_streamable_fields.append( - dataclasses.replace( - old_field, - convert_function=function_to_convert_one_item( - old_field.type, - functools.partial( - json_deserialize_with_clvm_streamable, - streamable_type=inner_type, - translation_layer=translation_layer, - ), - ), - ) - ) - else: - new_streamable_fields.append(old_field) - elif is_clvm_streamable_type(old_field.type): - new_streamable_fields.append( - dataclasses.replace( - old_field, - convert_function=functools.partial( + new_streamable_fields.append( + dataclasses.replace( + old_field, + convert_function=function_to_convert_one_item( + old_field.type, + functools.partial( json_deserialize_with_clvm_streamable, - streamable_type=old_field.type, translation_layer=translation_layer, ), - ) + ), ) - else: - new_streamable_fields.append(old_field) - + ) setattr(streamable_type, "_streamable_fields", tuple(new_streamable_fields)) - return streamable_type.from_json_dict(json_dict) + return streamable_type.from_json_dict(json_dict) # type: ignore[arg-type] + elif hasattr(streamable_type, "from_json_dict"): + return streamable_type.from_json_dict(json_dict) # type: ignore[arg-type] + else: + return function_to_convert_one_item( # type: ignore[return-value] + streamable_type, + functools.partial( + json_deserialize_with_clvm_streamable, + translation_layer=translation_layer, + ), + )(json_dict) _T_ClvmStreamable = TypeVar("_T_ClvmStreamable", bound="Streamable") From 698b2dd7515bfaaff9183b6cf69fe1b6216d81b0 Mon Sep 17 00:00:00 2001 From: Amine Khaldi Date: Thu, 11 Sep 2025 16:53:31 +0100 Subject: [PATCH 10/16] CHIA-3704 Optimize peak post processing by using transaction IDs instead of NewPeakItem elements (#19985) Optimize peak post processing by using transaction IDs instead of NewPeakItem elements. --- chia/_tests/core/mempool/test_mempool_manager.py | 10 ++++------ chia/full_node/full_node.py | 16 +++++++++------- chia/full_node/mempool_manager.py | 11 ++--------- 3 files changed, 15 insertions(+), 22 deletions(-) diff --git a/chia/_tests/core/mempool/test_mempool_manager.py b/chia/_tests/core/mempool/test_mempool_manager.py index a443f8bb2cf8..24d92fe0efcb 100644 --- a/chia/_tests/core/mempool/test_mempool_manager.py +++ b/chia/_tests/core/mempool/test_mempool_manager.py @@ -41,7 +41,6 @@ QUOTE_BYTES, QUOTE_EXECUTION_COST, MempoolManager, - NewPeakItem, TimelockConditions, can_replace, check_removals, @@ -3288,7 +3287,7 @@ async def test_new_peak_txs_added(condition_and_error: tuple[ConditionOpcode, Er assert mempool_manager.peak is not None condition_height = mempool_manager.peak.height + 1 condition, expected_error = condition_and_error - sb, sb_name, result = await generate_and_add_spendbundle(mempool_manager, [[condition, condition_height]]) + _, sb_name, result = await generate_and_add_spendbundle(mempool_manager, [[condition, condition_height]]) _, status, error = result assert status == MempoolInclusionStatus.PENDING assert error == expected_error @@ -3299,14 +3298,13 @@ async def test_new_peak_txs_added(condition_and_error: tuple[ConditionOpcode, Er create_test_block_record(height=uint32(condition_height)), spent_coins ) # We're not there yet (needs to be higher, not equal) + assert new_peak_info.spend_bundle_ids == [] assert mempool_manager.get_mempool_item(sb_name, include_pending=False) is None - assert new_peak_info.items == [] else: spent_coins = None new_peak_info = await mempool_manager.new_peak( create_test_block_record(height=uint32(condition_height + 1)), spent_coins ) # The item gets retried successfully now - mi = mempool_manager.get_mempool_item(sb_name, include_pending=False) - assert mi is not None - assert new_peak_info.items == [NewPeakItem(sb_name, sb, mi.conds)] + assert new_peak_info.spend_bundle_ids == [sb_name] + assert mempool_manager.get_mempool_item(sb_name, include_pending=False) is not None diff --git a/chia/full_node/full_node.py b/chia/full_node/full_node.py index 28a8c9c397d2..ff589394622c 100644 --- a/chia/full_node/full_node.py +++ b/chia/full_node/full_node.py @@ -58,7 +58,7 @@ from chia.full_node.hint_management import get_hints_and_subscription_coin_ids from chia.full_node.hint_store import HintStore from chia.full_node.mempool import MempoolRemoveInfo -from chia.full_node.mempool_manager import MempoolManager, NewPeakItem +from chia.full_node.mempool_manager import MempoolManager from chia.full_node.subscriptions import PeerSubscriptions, peers_for_spend_bundle from chia.full_node.sync_store import Peak, SyncStore from chia.full_node.tx_processing_queue import TransactionQueue, TransactionQueueEntry @@ -99,7 +99,8 @@ # This is the result of calling peak_post_processing, which is then fed into peak_post_processing_2 @dataclasses.dataclass class PeakPostProcessingResult: - mempool_peak_result: list[NewPeakItem] # The new items from calling MempoolManager.new_peak + # The added transactions IDs from calling MempoolManager.new_peak + mempool_peak_added_tx_ids: list[bytes32] mempool_removals: list[MempoolRemoveInfo] # The removed mempool items from calling MempoolManager.new_peak fns_peak_result: FullNodeStorePeakResult # The result of calling FullNodeStore.new_peak hints: list[tuple[bytes32, bytes]] # The hints added to the DB @@ -321,7 +322,8 @@ async def manage(self) -> AsyncIterator[None]: ) async with self.blockchain.priority_mutex.acquire(priority=BlockchainMutexPriority.high): pending_tx = await self.mempool_manager.new_peak(self.blockchain.get_tx_peak(), None) - assert len(pending_tx.items) == 0 # no pending transactions when starting up + # No pending transactions when starting up + assert len(pending_tx.spend_bundle_ids) == 0 full_peak: Optional[FullBlock] = await self.blockchain.get_full_peak() assert full_peak is not None @@ -1939,7 +1941,7 @@ async def peak_post_processing( mempool_new_peak_result = await self.mempool_manager.new_peak(self.blockchain.get_tx_peak(), spent_coins) return PeakPostProcessingResult( - mempool_new_peak_result.items, + mempool_new_peak_result.spend_bundle_ids, mempool_new_peak_result.removals, fns_peak_result, hints_to_add, @@ -1961,9 +1963,9 @@ async def peak_post_processing_2( record = state_change_summary.peak for signage_point in ppp_result.signage_points: await self.signage_point_post_processing(*signage_point) - for new_peak_item in ppp_result.mempool_peak_result: - self.log.debug(f"Added transaction to mempool: {new_peak_item.transaction_id}") - mempool_item = self.mempool_manager.get_mempool_item(new_peak_item.transaction_id) + for transaction_id in ppp_result.mempool_peak_added_tx_ids: + self.log.debug(f"Added transaction to mempool: {transaction_id}") + mempool_item = self.mempool_manager.get_mempool_item(transaction_id) assert mempool_item is not None await self.broadcast_added_tx(mempool_item) diff --git a/chia/full_node/mempool_manager.py b/chia/full_node/mempool_manager.py index ac70ddfce5e8..034afe4b0a13 100644 --- a/chia/full_node/mempool_manager.py +++ b/chia/full_node/mempool_manager.py @@ -133,17 +133,10 @@ class SpendBundleAddInfo: @dataclass class NewPeakInfo: - items: list[NewPeakItem] + spend_bundle_ids: list[bytes32] removals: list[MempoolRemoveInfo] -@dataclass -class NewPeakItem: - transaction_id: bytes32 - spend_bundle: SpendBundle - conds: SpendBundleConditions - - # For block overhead cost calculation QUOTE_BYTES = 2 QUOTE_EXECUTION_COST = 20 @@ -992,7 +985,7 @@ async def local_get_coin_records(names: Collection[bytes32]) -> list[CoinRecord] lineage_cache.get_unspent_lineage_info, ) if info.status == MempoolInclusionStatus.SUCCESS: - txs_added.append(NewPeakItem(item.spend_bundle_name, item.spend_bundle, item.conds)) + txs_added.append(item.spend_bundle_name) mempool_item_removals.extend(info.removals) log.info( f"Size of mempool: {self.mempool.size()} spends, " From c1d15dfda3caff270149ff9826ff0cdc570555e5 Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Thu, 11 Sep 2025 17:54:17 +0200 Subject: [PATCH 11/16] Simplify rate limit numbers (#19991) * remove unused default_settings in rate limits * simplify rate_limit_numbers by flattening the structure. just map message type -> rate limits * review comments --- chia/_tests/core/server/test_rate_limits.py | 84 +---- .../_tests/util/test_network_protocol_test.py | 18 + chia/server/rate_limit_numbers.py | 323 +++++++++--------- chia/server/rate_limits.py | 59 ++-- 4 files changed, 217 insertions(+), 267 deletions(-) diff --git a/chia/_tests/core/server/test_rate_limits.py b/chia/_tests/core/server/test_rate_limits.py index ea768dace048..c915b2ca05dd 100644 --- a/chia/_tests/core/server/test_rate_limits.py +++ b/chia/_tests/core/server/test_rate_limits.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any, cast +from typing import Union import pytest from chia_rs.sized_ints import uint32 @@ -13,8 +13,7 @@ from chia.protocols.outbound_message import make_msg from chia.protocols.protocol_message_types import ProtocolMessageTypes from chia.protocols.shared_protocol import Capability -from chia.server.rate_limit_numbers import RLSettings, compose_rate_limits, get_rate_limits_to_use -from chia.server.rate_limit_numbers import rate_limits as rl_numbers +from chia.server.rate_limit_numbers import RLSettings, Unlimited, get_rate_limits_to_use from chia.server.rate_limits import RateLimiter from chia.server.server import ChiaServer from chia.server.ws_connection import WSChiaConnection @@ -67,39 +66,22 @@ async def test_limits_v2(incoming: bool, tx_msg: bool, limit_size: bool, monkeyp message_data = b"\0" * 1024 msg_type = ProtocolMessageTypes.new_transaction - limits: dict[str, Any] = {} + limits: dict[ProtocolMessageTypes, Union[RLSettings, Unlimited]] if limit_size: - limits.update( - { - # this is the rate limit across all (non-tx) messages - "non_tx_freq": count * 2, - # this is the byte size limit across all (non-tx) messages - "non_tx_max_total_size": count * len(message_data), - } - ) + agg_limit = RLSettings(False, count * 2, len(message_data), count * len(message_data)) else: - limits.update( - { - # this is the rate limit across all (non-tx) messages - "non_tx_freq": count, - # this is the byte size limit across all (non-tx) messages - "non_tx_max_total_size": count * 2 * len(message_data), - } - ) + agg_limit = RLSettings(False, count, len(message_data), count * 2 * len(message_data)) if limit_size: - rate_limit = {msg_type: RLSettings(count * 2, 1024, count * len(message_data))} + limits = {msg_type: RLSettings(not tx_msg, count * 2, len(message_data), count * len(message_data))} else: - rate_limit = {msg_type: RLSettings(count, 1024, count * 2 * len(message_data))} + limits = {msg_type: RLSettings(not tx_msg, count, len(message_data), count * 2 * len(message_data))} - if tx_msg: - limits.update({"rate_limits_tx": rate_limit, "rate_limits_other": {}}) - else: - limits.update({"rate_limits_other": rate_limit, "rate_limits_tx": {}}) - - def mock_get_limits(our_capabilities: list[Capability], peer_capabilities: list[Capability]) -> dict[str, Any]: - return limits + def mock_get_limits( + our_capabilities: list[Capability], peer_capabilities: list[Capability] + ) -> tuple[dict[ProtocolMessageTypes, Union[RLSettings, Unlimited]], RLSettings]: + return limits, agg_limit import chia.server.rate_limits @@ -327,18 +309,18 @@ async def test_too_many_outgoing_messages() -> None: # Too many messages r = RateLimiter(incoming=False, get_time=lambda: 0) new_peers_message = make_msg(ProtocolMessageTypes.respond_peers, bytes([1])) - non_tx_freq = get_rate_limits_to_use(rl_v2, rl_v2)["non_tx_freq"] + _, agg_limit = get_rate_limits_to_use(rl_v2, rl_v2) passed = 0 blocked = 0 - for i in range(non_tx_freq): + for i in range(agg_limit.frequency): if r.process_msg_and_check(new_peers_message, rl_v2, rl_v2) is None: passed += 1 else: blocked += 1 assert passed == 10 - assert blocked == non_tx_freq - passed + assert blocked == agg_limit.frequency - passed # ensure that *another* message type is not blocked because of this @@ -351,18 +333,18 @@ async def test_too_many_incoming_messages() -> None: # Too many messages r = RateLimiter(incoming=True, get_time=lambda: 0) new_peers_message = make_msg(ProtocolMessageTypes.respond_peers, bytes([1])) - non_tx_freq = get_rate_limits_to_use(rl_v2, rl_v2)["non_tx_freq"] + _, agg_limit = get_rate_limits_to_use(rl_v2, rl_v2) passed = 0 blocked = 0 - for i in range(non_tx_freq): + for i in range(agg_limit.frequency): if r.process_msg_and_check(new_peers_message, rl_v2, rl_v2) is None: passed += 1 else: blocked += 1 assert passed == 10 - assert blocked == non_tx_freq - passed + assert blocked == agg_limit.frequency - passed # ensure that other message types *are* blocked because of this @@ -436,43 +418,13 @@ async def test_different_versions( # The following code checks whether all of the runs resulted in the same number of items in "rate_limits_tx", # which would mean the same rate limits are always used. This should not happen, since two nodes with V2 # will use V2. - total_tx_msg_count = len( - get_rate_limits_to_use(a_con.local_capabilities, a_con.peer_capabilities)["rate_limits_tx"] - ) + total_tx_msg_count = len(get_rate_limits_to_use(a_con.local_capabilities, a_con.peer_capabilities)) test_different_versions_results.append(total_tx_msg_count) if len(test_different_versions_results) >= 4: assert len(set(test_different_versions_results)) >= 2 -@pytest.mark.anyio -async def test_compose() -> None: - rl_1 = rl_numbers[1] - rl_2 = rl_numbers[2] - rl_1_rate_limits_other = cast(dict[ProtocolMessageTypes, RLSettings], rl_1["rate_limits_other"]) - rl_2_rate_limits_other = cast(dict[ProtocolMessageTypes, RLSettings], rl_2["rate_limits_other"]) - rl_1_rate_limits_tx = cast(dict[ProtocolMessageTypes, RLSettings], rl_1["rate_limits_tx"]) - rl_2_rate_limits_tx = cast(dict[ProtocolMessageTypes, RLSettings], rl_2["rate_limits_tx"]) - assert ProtocolMessageTypes.respond_children in rl_1_rate_limits_other - assert ProtocolMessageTypes.respond_children not in rl_1_rate_limits_tx - assert ProtocolMessageTypes.respond_children not in rl_2_rate_limits_other - assert ProtocolMessageTypes.respond_children in rl_2_rate_limits_tx - - assert ProtocolMessageTypes.request_block in rl_1_rate_limits_other - assert ProtocolMessageTypes.request_block not in rl_1_rate_limits_tx - assert ProtocolMessageTypes.request_block not in rl_2_rate_limits_other - assert ProtocolMessageTypes.request_block not in rl_2_rate_limits_tx - - comps = compose_rate_limits(rl_1, rl_2) - # v2 limits are used if present - assert ProtocolMessageTypes.respond_children not in comps["rate_limits_other"] - assert ProtocolMessageTypes.respond_children in comps["rate_limits_tx"] - - # Otherwise, fall back to v1 - assert ProtocolMessageTypes.request_block in rl_1_rate_limits_other - assert ProtocolMessageTypes.request_block not in rl_1_rate_limits_tx - - @pytest.mark.anyio @pytest.mark.parametrize( "msg_type, size", diff --git a/chia/_tests/util/test_network_protocol_test.py b/chia/_tests/util/test_network_protocol_test.py index 0adb46afa5e6..289f2383caf3 100644 --- a/chia/_tests/util/test_network_protocol_test.py +++ b/chia/_tests/util/test_network_protocol_test.py @@ -4,6 +4,8 @@ import inspect from typing import Any, cast +import pytest + from chia.protocols import ( farmer_protocol, full_node_protocol, @@ -264,3 +266,19 @@ def test_missing_messages() -> None: assert types_in_module(shared_protocol) == shared_msgs, ( f"message types were added or removed from shared_protocol. {STANDARD_ADVICE}" ) + + +@pytest.mark.parametrize("version", [1, 2]) +def test_rate_limits_complete(version: int) -> None: + from chia.protocols.protocol_message_types import ProtocolMessageTypes + from chia.server.rate_limit_numbers import rate_limits + + if version == 1: + composed = rate_limits[1] + elif version == 2: + composed = { + **rate_limits[1], + **rate_limits[2], + } + + assert set(composed.keys()) == set(ProtocolMessageTypes) diff --git a/chia/server/rate_limit_numbers.py b/chia/server/rate_limit_numbers.py index b7eea59ae8c2..16b7c4265f60 100644 --- a/chia/server/rate_limit_numbers.py +++ b/chia/server/rate_limit_numbers.py @@ -1,21 +1,22 @@ # All of these rate limits scale with the number of transactions so the aggregate amounts are higher from __future__ import annotations -import copy import dataclasses -import functools -from typing import Any, Optional +from typing import Optional, Union from chia.protocols.protocol_message_types import ProtocolMessageTypes from chia.protocols.shared_protocol import Capability -compose_rate_limits_cache: dict[int, dict[str, Any]] = {} +compose_rate_limits_cache: dict[int, dict[ProtocolMessageTypes, Union[RLSettings, Unlimited]]] = {} # this class is used to configure the *rate* limit for a message type. The # limits are counts and size per 60 seconds. @dataclasses.dataclass(frozen=True) class RLSettings: + # if true, messages affect and are limited by the per connection aggregate limiter, + # which affects messages across message types + aggregate_limit: bool frequency: int # Max request per time period (ie 1 min) max_size: int # Max size of each request max_total_size: Optional[int] = None # Max cumulative size of all requests in that period @@ -31,186 +32,172 @@ class Unlimited: max_size: int # Max size of each request -def get_rate_limits_to_use(our_capabilities: list[Capability], peer_capabilities: list[Capability]) -> dict[str, Any]: +# for the aggregate limit, not all fields of RLSettings are used. Only "frequency" and "max_total_size" +aggregate_limit = RLSettings( + aggregate_limit=False, + frequency=1000, + max_size=0, + max_total_size=100 * 1024 * 1024, +) + + +def get_rate_limits_to_use( + our_capabilities: list[Capability], peer_capabilities: list[Capability] +) -> tuple[dict[ProtocolMessageTypes, Union[RLSettings, Unlimited]], RLSettings]: # This will use the newest possible rate limits that both peers support. At this time there are only two # options, v1 and v2. if Capability.RATE_LIMITS_V2 in our_capabilities and Capability.RATE_LIMITS_V2 in peer_capabilities: # Use V2 rate limits if 2 in compose_rate_limits_cache: - return compose_rate_limits_cache[2] - composed = compose_rate_limits(rate_limits[1], rate_limits[2]) + return compose_rate_limits_cache[2], aggregate_limit + composed = { + **rate_limits[1], + **rate_limits[2], + } compose_rate_limits_cache[2] = composed - return composed + return composed, aggregate_limit else: # Use V1 rate limits - return rate_limits[1] - - -def compose_rate_limits(old_rate_limits: dict[str, Any], new_rate_limits: dict[str, Any]) -> dict[str, Any]: - # Composes two rate limits dicts, so that the newer values override the older values - final_rate_limits: dict[str, Any] = copy.deepcopy(new_rate_limits) - categories: list[str] = ["rate_limits_tx", "rate_limits_other"] - all_new_msgs_lists: list[list[ProtocolMessageTypes]] = [ - list(new_rate_limits[category].keys()) for category in categories - ] - all_new_msgs: list[ProtocolMessageTypes] = functools.reduce(lambda a, b: a + b, all_new_msgs_lists) - for old_cat, mapping in old_rate_limits.items(): - if old_cat in categories: - for old_protocol_msg, old_rate_limit_value in mapping.items(): - if old_protocol_msg not in all_new_msgs: - if old_cat not in final_rate_limits: - final_rate_limits[old_cat] = {} - final_rate_limits[old_cat][old_protocol_msg] = old_rate_limit_value - return final_rate_limits + return rate_limits[1], aggregate_limit # Each number in this dict corresponds to a specific version of rate limits (1, 2, etc). # Version 1 includes the original limits for chia software from versions 1.0 to 1.4. -rate_limits = { +rate_limits: dict[int, dict[ProtocolMessageTypes, Union[RLSettings, Unlimited]]] = { 1: { - "default_settings": RLSettings(100, 1024 * 1024, 100 * 1024 * 1024), - "non_tx_freq": 1000, # There is also a freq limit for many requests - "non_tx_max_total_size": 100 * 1024 * 1024, # There is also a size limit for many requests - # All transaction related apis also have an aggregate limit - "rate_limits_tx": { - ProtocolMessageTypes.new_transaction: RLSettings(5000, 100, 5000 * 100), - ProtocolMessageTypes.request_transaction: RLSettings(5000, 100, 5000 * 100), - ProtocolMessageTypes.respond_transaction: RLSettings( - 5000, 1 * 1024 * 1024, 20 * 1024 * 1024 - ), # TODO: check this - ProtocolMessageTypes.send_transaction: RLSettings(5000, 1024 * 1024), - ProtocolMessageTypes.transaction_ack: RLSettings(5000, 2048), - }, + ProtocolMessageTypes.new_transaction: RLSettings(False, 5000, 100, 5000 * 100), + ProtocolMessageTypes.request_transaction: RLSettings(False, 5000, 100, 5000 * 100), + # TODO: check this + ProtocolMessageTypes.respond_transaction: RLSettings(False, 5000, 1 * 1024 * 1024, 20 * 1024 * 1024), + ProtocolMessageTypes.send_transaction: RLSettings(False, 5000, 1024 * 1024), + ProtocolMessageTypes.transaction_ack: RLSettings(False, 5000, 2048), # All non-transaction apis also have an aggregate limit - "rate_limits_other": { - ProtocolMessageTypes.handshake: RLSettings(5, 10 * 1024, 5 * 10 * 1024), - ProtocolMessageTypes.harvester_handshake: RLSettings(5, 1024 * 1024), - ProtocolMessageTypes.new_signage_point_harvester: RLSettings(100, 4886), # Size with 100 pool list - ProtocolMessageTypes.new_proof_of_space: RLSettings(100, 2048), - ProtocolMessageTypes.request_signatures: RLSettings(100, 2048), - ProtocolMessageTypes.respond_signatures: RLSettings(100, 2048), - ProtocolMessageTypes.new_signage_point: RLSettings(200, 2048), - ProtocolMessageTypes.declare_proof_of_space: RLSettings(100, 10 * 1024), - ProtocolMessageTypes.request_signed_values: RLSettings(100, 10 * 1024), - ProtocolMessageTypes.farming_info: RLSettings(100, 1024), - ProtocolMessageTypes.signed_values: RLSettings(100, 1024), - ProtocolMessageTypes.new_peak_timelord: RLSettings(100, 20 * 1024), - ProtocolMessageTypes.new_unfinished_block_timelord: RLSettings(100, 10 * 1024), - ProtocolMessageTypes.new_signage_point_vdf: RLSettings(100, 100 * 1024), - ProtocolMessageTypes.new_infusion_point_vdf: RLSettings(100, 100 * 1024), - ProtocolMessageTypes.new_end_of_sub_slot_vdf: RLSettings(100, 100 * 1024), - ProtocolMessageTypes.request_compact_proof_of_time: RLSettings(100, 10 * 1024), - ProtocolMessageTypes.respond_compact_proof_of_time: RLSettings(100, 100 * 1024), - ProtocolMessageTypes.new_peak: RLSettings(200, 512), - ProtocolMessageTypes.request_proof_of_weight: RLSettings(5, 100), - ProtocolMessageTypes.respond_proof_of_weight: RLSettings(5, 50 * 1024 * 1024, 100 * 1024 * 1024), - ProtocolMessageTypes.request_block: RLSettings(200, 100), - ProtocolMessageTypes.reject_block: Unlimited(100), - ProtocolMessageTypes.request_blocks: RLSettings(500, 100), - ProtocolMessageTypes.respond_blocks: Unlimited(50 * 1024 * 1024), - ProtocolMessageTypes.reject_blocks: Unlimited(100), - ProtocolMessageTypes.respond_block: Unlimited(2 * 1024 * 1024), - ProtocolMessageTypes.new_unfinished_block: RLSettings(200, 100), - ProtocolMessageTypes.request_unfinished_block: RLSettings(200, 100), - ProtocolMessageTypes.new_unfinished_block2: RLSettings(200, 100), - ProtocolMessageTypes.request_unfinished_block2: RLSettings(200, 100), - ProtocolMessageTypes.respond_unfinished_block: RLSettings(200, 2 * 1024 * 1024, 10 * 2 * 1024 * 1024), - ProtocolMessageTypes.new_signage_point_or_end_of_sub_slot: RLSettings(200, 200), - ProtocolMessageTypes.request_signage_point_or_end_of_sub_slot: RLSettings(200, 200), - ProtocolMessageTypes.respond_signage_point: RLSettings(200, 50 * 1024), - ProtocolMessageTypes.respond_end_of_sub_slot: RLSettings(100, 50 * 1024), - ProtocolMessageTypes.request_mempool_transactions: RLSettings(5, 1024 * 1024), - ProtocolMessageTypes.request_compact_vdf: RLSettings(200, 1024), - ProtocolMessageTypes.respond_compact_vdf: RLSettings(200, 100 * 1024), - ProtocolMessageTypes.new_compact_vdf: RLSettings(100, 1024), - ProtocolMessageTypes.request_peers: RLSettings(10, 100), - ProtocolMessageTypes.respond_peers: RLSettings(10, 1 * 1024 * 1024), - ProtocolMessageTypes.request_puzzle_solution: RLSettings(1000, 100), - ProtocolMessageTypes.respond_puzzle_solution: RLSettings(1000, 1024 * 1024), - ProtocolMessageTypes.reject_puzzle_solution: RLSettings(1000, 100), - ProtocolMessageTypes.new_peak_wallet: RLSettings(200, 300), - ProtocolMessageTypes.request_block_header: RLSettings(500, 100), - ProtocolMessageTypes.respond_block_header: RLSettings(500, 500 * 1024), - ProtocolMessageTypes.reject_header_request: RLSettings(500, 100), - ProtocolMessageTypes.request_removals: RLSettings(500, 50 * 1024, 10 * 1024 * 1024), - ProtocolMessageTypes.respond_removals: RLSettings(500, 1024 * 1024, 10 * 1024 * 1024), - ProtocolMessageTypes.reject_removals_request: RLSettings(500, 100), - ProtocolMessageTypes.request_additions: RLSettings(500, 1024 * 1024, 10 * 1024 * 1024), - ProtocolMessageTypes.respond_additions: RLSettings(500, 1024 * 1024, 10 * 1024 * 1024), - ProtocolMessageTypes.reject_additions_request: RLSettings(500, 100), - ProtocolMessageTypes.request_header_blocks: RLSettings(500, 100), - ProtocolMessageTypes.reject_header_blocks: RLSettings(100, 100), - ProtocolMessageTypes.respond_header_blocks: RLSettings(500, 2 * 1024 * 1024, 100 * 1024 * 1024), - ProtocolMessageTypes.request_peers_introducer: RLSettings(100, 100), - ProtocolMessageTypes.respond_peers_introducer: RLSettings(100, 1024 * 1024), - 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_for_ph_updates: RLSettings(1000, 100 * 1024 * 1024), - ProtocolMessageTypes.respond_to_ph_updates: RLSettings(1000, 100 * 1024 * 1024), - ProtocolMessageTypes.register_for_coin_updates: RLSettings(1000, 100 * 1024 * 1024), - ProtocolMessageTypes.respond_to_coin_updates: RLSettings(1000, 100 * 1024 * 1024), - ProtocolMessageTypes.request_remove_puzzle_subscriptions: RLSettings(1000, 100 * 1024 * 1024), - ProtocolMessageTypes.respond_remove_puzzle_subscriptions: RLSettings(1000, 100 * 1024 * 1024), - ProtocolMessageTypes.request_remove_coin_subscriptions: RLSettings(1000, 100 * 1024 * 1024), - ProtocolMessageTypes.respond_remove_coin_subscriptions: RLSettings(1000, 100 * 1024 * 1024), - ProtocolMessageTypes.request_puzzle_state: RLSettings(1000, 100 * 1024 * 1024), - ProtocolMessageTypes.respond_puzzle_state: RLSettings(1000, 100 * 1024 * 1024), - ProtocolMessageTypes.reject_puzzle_state: RLSettings(200, 100), - ProtocolMessageTypes.request_coin_state: RLSettings(1000, 100 * 1024 * 1024), - ProtocolMessageTypes.respond_coin_state: RLSettings(1000, 100 * 1024 * 1024), - ProtocolMessageTypes.reject_coin_state: RLSettings(200, 100), - ProtocolMessageTypes.mempool_items_added: RLSettings(1000, 100 * 1024 * 1024), - ProtocolMessageTypes.mempool_items_removed: RLSettings(1000, 100 * 1024 * 1024), - ProtocolMessageTypes.request_cost_info: RLSettings(1000, 100), - ProtocolMessageTypes.respond_cost_info: RLSettings(1000, 1024), - ProtocolMessageTypes.request_ses_hashes: RLSettings(2000, 1 * 1024 * 1024), - ProtocolMessageTypes.respond_ses_hashes: RLSettings(2000, 1 * 1024 * 1024), - ProtocolMessageTypes.request_children: RLSettings(2000, 1024 * 1024), - ProtocolMessageTypes.respond_children: RLSettings(2000, 1 * 1024 * 1024), - }, + ProtocolMessageTypes.handshake: RLSettings(True, 5, 10 * 1024, 5 * 10 * 1024), + ProtocolMessageTypes.harvester_handshake: RLSettings(True, 5, 1024 * 1024), + ProtocolMessageTypes.new_signage_point_harvester: RLSettings(True, 100, 4886), # Size with 100 pool list + ProtocolMessageTypes.new_proof_of_space: RLSettings(True, 100, 2048), + ProtocolMessageTypes.request_signatures: RLSettings(True, 100, 2048), + ProtocolMessageTypes.respond_signatures: RLSettings(True, 100, 2048), + ProtocolMessageTypes.new_signage_point: RLSettings(True, 200, 2048), + ProtocolMessageTypes.declare_proof_of_space: RLSettings(True, 100, 10 * 1024), + ProtocolMessageTypes.request_signed_values: RLSettings(True, 100, 10 * 1024), + ProtocolMessageTypes.farming_info: RLSettings(True, 100, 1024), + ProtocolMessageTypes.signed_values: RLSettings(True, 100, 1024), + ProtocolMessageTypes.new_peak_timelord: RLSettings(True, 100, 20 * 1024), + ProtocolMessageTypes.new_unfinished_block_timelord: RLSettings(True, 100, 10 * 1024), + ProtocolMessageTypes.new_signage_point_vdf: RLSettings(True, 100, 100 * 1024), + ProtocolMessageTypes.new_infusion_point_vdf: RLSettings(True, 100, 100 * 1024), + ProtocolMessageTypes.new_end_of_sub_slot_vdf: RLSettings(True, 100, 100 * 1024), + ProtocolMessageTypes.request_compact_proof_of_time: RLSettings(True, 100, 10 * 1024), + ProtocolMessageTypes.respond_compact_proof_of_time: RLSettings(True, 100, 100 * 1024), + ProtocolMessageTypes.new_peak: RLSettings(True, 200, 512), + ProtocolMessageTypes.request_proof_of_weight: RLSettings(True, 5, 100), + ProtocolMessageTypes.respond_proof_of_weight: RLSettings(True, 5, 50 * 1024 * 1024, 100 * 1024 * 1024), + ProtocolMessageTypes.request_block: RLSettings(True, 200, 100), + ProtocolMessageTypes.reject_block: Unlimited(100), + ProtocolMessageTypes.request_blocks: RLSettings(True, 500, 100), + ProtocolMessageTypes.respond_blocks: Unlimited(50 * 1024 * 1024), + ProtocolMessageTypes.reject_blocks: Unlimited(100), + ProtocolMessageTypes.respond_block: Unlimited(2 * 1024 * 1024), + ProtocolMessageTypes.new_unfinished_block: RLSettings(True, 200, 100), + ProtocolMessageTypes.request_unfinished_block: RLSettings(True, 200, 100), + ProtocolMessageTypes.new_unfinished_block2: RLSettings(True, 200, 100), + ProtocolMessageTypes.request_unfinished_block2: RLSettings(True, 200, 100), + ProtocolMessageTypes.respond_unfinished_block: RLSettings(True, 200, 2 * 1024 * 1024, 10 * 2 * 1024 * 1024), + ProtocolMessageTypes.new_signage_point_or_end_of_sub_slot: RLSettings(True, 200, 200), + ProtocolMessageTypes.request_signage_point_or_end_of_sub_slot: RLSettings(True, 200, 200), + ProtocolMessageTypes.respond_signage_point: RLSettings(True, 200, 50 * 1024), + ProtocolMessageTypes.respond_end_of_sub_slot: RLSettings(True, 100, 50 * 1024), + ProtocolMessageTypes.request_mempool_transactions: RLSettings(True, 5, 1024 * 1024), + ProtocolMessageTypes.request_compact_vdf: RLSettings(True, 200, 1024), + ProtocolMessageTypes.respond_compact_vdf: RLSettings(True, 200, 100 * 1024), + ProtocolMessageTypes.new_compact_vdf: RLSettings(True, 100, 1024), + ProtocolMessageTypes.request_peers: RLSettings(True, 10, 100), + ProtocolMessageTypes.respond_peers: RLSettings(True, 10, 1 * 1024 * 1024), + ProtocolMessageTypes.request_puzzle_solution: RLSettings(True, 1000, 100), + ProtocolMessageTypes.respond_puzzle_solution: RLSettings(True, 1000, 1024 * 1024), + ProtocolMessageTypes.reject_puzzle_solution: RLSettings(True, 1000, 100), + ProtocolMessageTypes.none_response: RLSettings(False, 500, 100), + ProtocolMessageTypes.new_peak_wallet: RLSettings(True, 200, 300), + ProtocolMessageTypes.request_block_header: RLSettings(True, 500, 100), + ProtocolMessageTypes.respond_block_header: RLSettings(True, 500, 500 * 1024), + ProtocolMessageTypes.reject_header_request: RLSettings(True, 500, 100), + ProtocolMessageTypes.request_block_headers: RLSettings(False, 5000, 100), + ProtocolMessageTypes.reject_block_headers: RLSettings(False, 1000, 100), + ProtocolMessageTypes.respond_block_headers: RLSettings(False, 5000, 2 * 1024 * 1024), + ProtocolMessageTypes.request_removals: RLSettings(True, 500, 50 * 1024, 10 * 1024 * 1024), + ProtocolMessageTypes.respond_removals: RLSettings(True, 500, 1024 * 1024, 10 * 1024 * 1024), + ProtocolMessageTypes.reject_removals_request: RLSettings(True, 500, 100), + ProtocolMessageTypes.request_additions: RLSettings(True, 500, 1024 * 1024, 10 * 1024 * 1024), + ProtocolMessageTypes.respond_additions: RLSettings(True, 500, 1024 * 1024, 10 * 1024 * 1024), + ProtocolMessageTypes.reject_additions_request: RLSettings(True, 500, 100), + ProtocolMessageTypes.request_header_blocks: RLSettings(True, 500, 100), + ProtocolMessageTypes.reject_header_blocks: RLSettings(True, 100, 100), + ProtocolMessageTypes.respond_header_blocks: RLSettings(True, 500, 2 * 1024 * 1024, 100 * 1024 * 1024), + ProtocolMessageTypes.request_peers_introducer: RLSettings(True, 100, 100), + ProtocolMessageTypes.respond_peers_introducer: RLSettings(True, 100, 1024 * 1024), + ProtocolMessageTypes.farm_new_block: RLSettings(True, 200, 200), + ProtocolMessageTypes.request_plots: RLSettings(True, 10, 10 * 1024 * 1024), + ProtocolMessageTypes.respond_plots: RLSettings(True, 10, 100 * 1024 * 1024), + ProtocolMessageTypes.plot_sync_start: RLSettings(True, 1000, 100 * 1024 * 1024), + ProtocolMessageTypes.plot_sync_loaded: RLSettings(True, 1000, 100 * 1024 * 1024), + ProtocolMessageTypes.plot_sync_removed: RLSettings(True, 1000, 100 * 1024 * 1024), + ProtocolMessageTypes.plot_sync_invalid: RLSettings(True, 1000, 100 * 1024 * 1024), + ProtocolMessageTypes.plot_sync_keys_missing: RLSettings(True, 1000, 100 * 1024 * 1024), + ProtocolMessageTypes.plot_sync_duplicates: RLSettings(True, 1000, 100 * 1024 * 1024), + ProtocolMessageTypes.plot_sync_done: RLSettings(True, 1000, 100 * 1024 * 1024), + ProtocolMessageTypes.plot_sync_response: RLSettings(True, 3000, 100 * 1024 * 1024), + ProtocolMessageTypes.coin_state_update: RLSettings(True, 1000, 100 * 1024 * 1024), + ProtocolMessageTypes.register_for_ph_updates: RLSettings(True, 1000, 100 * 1024 * 1024), + ProtocolMessageTypes.respond_to_ph_updates: RLSettings(True, 1000, 100 * 1024 * 1024), + ProtocolMessageTypes.register_for_coin_updates: RLSettings(True, 1000, 100 * 1024 * 1024), + ProtocolMessageTypes.respond_to_coin_updates: RLSettings(True, 1000, 100 * 1024 * 1024), + ProtocolMessageTypes.request_remove_puzzle_subscriptions: RLSettings(True, 1000, 100 * 1024 * 1024), + ProtocolMessageTypes.respond_remove_puzzle_subscriptions: RLSettings(True, 1000, 100 * 1024 * 1024), + ProtocolMessageTypes.request_remove_coin_subscriptions: RLSettings(True, 1000, 100 * 1024 * 1024), + ProtocolMessageTypes.respond_remove_coin_subscriptions: RLSettings(True, 1000, 100 * 1024 * 1024), + ProtocolMessageTypes.request_puzzle_state: RLSettings(True, 1000, 100 * 1024 * 1024), + ProtocolMessageTypes.respond_puzzle_state: RLSettings(True, 1000, 100 * 1024 * 1024), + ProtocolMessageTypes.reject_puzzle_state: RLSettings(True, 200, 100), + ProtocolMessageTypes.request_coin_state: RLSettings(True, 1000, 100 * 1024 * 1024), + ProtocolMessageTypes.respond_coin_state: RLSettings(True, 1000, 100 * 1024 * 1024), + ProtocolMessageTypes.reject_coin_state: RLSettings(True, 200, 100), + ProtocolMessageTypes.mempool_items_added: RLSettings(True, 1000, 100 * 1024 * 1024), + ProtocolMessageTypes.mempool_items_removed: RLSettings(True, 1000, 100 * 1024 * 1024), + ProtocolMessageTypes.request_cost_info: RLSettings(True, 1000, 100), + ProtocolMessageTypes.respond_cost_info: RLSettings(True, 1000, 1024), + ProtocolMessageTypes.request_ses_hashes: RLSettings(True, 2000, 1 * 1024 * 1024), + ProtocolMessageTypes.respond_ses_hashes: RLSettings(True, 2000, 1 * 1024 * 1024), + ProtocolMessageTypes.request_children: RLSettings(True, 2000, 1024 * 1024), + ProtocolMessageTypes.respond_children: RLSettings(True, 2000, 1 * 1024 * 1024), + ProtocolMessageTypes.error: RLSettings(False, 50000, 100), + ProtocolMessageTypes.request_fee_estimates: RLSettings(True, 10, 100), + ProtocolMessageTypes.respond_fee_estimates: RLSettings(True, 10, 100), + ProtocolMessageTypes.solve: RLSettings(False, 120, 1024), + ProtocolMessageTypes.solution_response: RLSettings(False, 120, 1024), + ProtocolMessageTypes.partial_proofs: RLSettings(False, 120, 3 * 1024), }, 2: { - "default_settings": RLSettings(100, 1024 * 1024, 100 * 1024 * 1024), - "non_tx_freq": 1000, # There is also a freq limit for many requests - "non_tx_max_total_size": 100 * 1024 * 1024, # There is also a size limit for many requests - "rate_limits_tx": { - ProtocolMessageTypes.request_block_header: RLSettings(500, 100), - ProtocolMessageTypes.respond_block_header: RLSettings(500, 500 * 1024), - ProtocolMessageTypes.reject_header_request: RLSettings(500, 100), - ProtocolMessageTypes.request_removals: RLSettings(5000, 50 * 1024, 10 * 1024 * 1024), - ProtocolMessageTypes.respond_removals: RLSettings(5000, 1024 * 1024, 10 * 1024 * 1024), - ProtocolMessageTypes.reject_removals_request: RLSettings(500, 100), - ProtocolMessageTypes.request_additions: RLSettings(50000, 100 * 1024 * 1024), - ProtocolMessageTypes.respond_additions: RLSettings(50000, 100 * 1024 * 1024), - ProtocolMessageTypes.reject_additions_request: RLSettings(500, 100), - ProtocolMessageTypes.reject_header_blocks: RLSettings(1000, 100), - ProtocolMessageTypes.respond_header_blocks: RLSettings(5000, 2 * 1024 * 1024), - ProtocolMessageTypes.request_block_headers: RLSettings(5000, 100), - ProtocolMessageTypes.reject_block_headers: RLSettings(1000, 100), - ProtocolMessageTypes.respond_block_headers: RLSettings(5000, 2 * 1024 * 1024), - ProtocolMessageTypes.request_ses_hashes: RLSettings(2000, 1 * 1024 * 1024), - ProtocolMessageTypes.respond_ses_hashes: RLSettings(2000, 1 * 1024 * 1024), - ProtocolMessageTypes.request_children: RLSettings(2000, 1024 * 1024), - ProtocolMessageTypes.respond_children: RLSettings(2000, 1 * 1024 * 1024), - ProtocolMessageTypes.request_puzzle_solution: RLSettings(5000, 100), - ProtocolMessageTypes.respond_puzzle_solution: RLSettings(5000, 1024 * 1024), - ProtocolMessageTypes.reject_puzzle_solution: RLSettings(5000, 100), - ProtocolMessageTypes.none_response: RLSettings(500, 100), - ProtocolMessageTypes.error: RLSettings(50000, 100), - }, - "rate_limits_other": { # These will have a lower cap since they don't scale with high TPS (NON_TX_FREQ) - ProtocolMessageTypes.request_header_blocks: RLSettings(5000, 100), - }, + ProtocolMessageTypes.request_block_header: RLSettings(False, 500, 100), + ProtocolMessageTypes.respond_block_header: RLSettings(False, 500, 500 * 1024), + ProtocolMessageTypes.reject_header_request: RLSettings(False, 500, 100), + ProtocolMessageTypes.request_removals: RLSettings(False, 5000, 50 * 1024, 10 * 1024 * 1024), + ProtocolMessageTypes.respond_removals: RLSettings(False, 5000, 1024 * 1024, 10 * 1024 * 1024), + ProtocolMessageTypes.reject_removals_request: RLSettings(False, 500, 100), + ProtocolMessageTypes.request_additions: RLSettings(False, 50000, 100 * 1024 * 1024), + ProtocolMessageTypes.respond_additions: RLSettings(False, 50000, 100 * 1024 * 1024), + ProtocolMessageTypes.reject_additions_request: RLSettings(False, 500, 100), + ProtocolMessageTypes.reject_header_blocks: RLSettings(False, 1000, 100), + ProtocolMessageTypes.respond_header_blocks: RLSettings(False, 5000, 2 * 1024 * 1024), + ProtocolMessageTypes.request_ses_hashes: RLSettings(False, 2000, 1 * 1024 * 1024), + ProtocolMessageTypes.respond_ses_hashes: RLSettings(False, 2000, 1 * 1024 * 1024), + ProtocolMessageTypes.request_children: RLSettings(False, 2000, 1024 * 1024), + ProtocolMessageTypes.respond_children: RLSettings(False, 2000, 1 * 1024 * 1024), + ProtocolMessageTypes.request_puzzle_solution: RLSettings(False, 5000, 100), + ProtocolMessageTypes.respond_puzzle_solution: RLSettings(False, 5000, 1024 * 1024), + ProtocolMessageTypes.reject_puzzle_solution: RLSettings(False, 5000, 100), + # These will have a lower cap since they don't scale with high TPS (NON_TX_FREQ) + ProtocolMessageTypes.request_header_blocks: RLSettings(True, 5000, 100), }, } diff --git a/chia/server/rate_limits.py b/chia/server/rate_limits.py index 685ed142517b..ac5aa2210277 100644 --- a/chia/server/rate_limits.py +++ b/chia/server/rate_limits.py @@ -4,7 +4,7 @@ import logging import time from collections import Counter -from typing import Callable, Optional +from typing import Callable, Optional, Union from chia.protocols.outbound_message import Message from chia.protocols.protocol_message_types import ProtocolMessageTypes @@ -80,40 +80,33 @@ def process_msg_and_check( proportion_of_limit: float = self.percentage_of_limit / 100 ret: bool = False - rate_limits = get_rate_limits_to_use(our_capabilities, peer_capabilities) + rate_limits: dict[ProtocolMessageTypes, Union[RLSettings, Unlimited]] + rate_limits, agg_limit = get_rate_limits_to_use(our_capabilities, peer_capabilities) try: - limits: RLSettings - if message_type in rate_limits["rate_limits_tx"]: - limits = rate_limits["rate_limits_tx"][message_type] - elif message_type in rate_limits["rate_limits_other"]: - limits = rate_limits["rate_limits_other"][message_type] - if isinstance(limits, RLSettings): - non_tx_freq = rate_limits["non_tx_freq"] - non_tx_max_total_size = rate_limits["non_tx_max_total_size"] - new_non_tx_count = self.non_tx_message_counts + 1 - new_non_tx_size = self.non_tx_cumulative_size + len(message.data) - if new_non_tx_count > non_tx_freq * proportion_of_limit: - return " ".join( - [ - f"non-tx count: {new_non_tx_count}", - f"> {non_tx_freq * proportion_of_limit}", - f"(scale factor: {proportion_of_limit})", - ] - ) - if new_non_tx_size > non_tx_max_total_size * proportion_of_limit: - return " ".join( - [ - f"non-tx size: {new_non_tx_size}", - f"> {non_tx_max_total_size * proportion_of_limit}", - f"(scale factor: {proportion_of_limit})", - ] - ) - else: # pragma: no cover - log.warning( - f"Message type {message_type} not found in rate limits (scale factor: {proportion_of_limit})", - ) - limits = rate_limits["default_settings"] + limits: Union[RLSettings, Unlimited] = rate_limits[message_type] + if isinstance(limits, RLSettings) and limits.aggregate_limit: + non_tx_freq = agg_limit.frequency + assert agg_limit.max_total_size is not None + non_tx_max_total_size = agg_limit.max_total_size + new_non_tx_count = self.non_tx_message_counts + 1 + new_non_tx_size = self.non_tx_cumulative_size + len(message.data) + if new_non_tx_count > non_tx_freq * proportion_of_limit: + return " ".join( + [ + f"non-tx count: {new_non_tx_count}", + f"> {non_tx_freq * proportion_of_limit}", + f"(scale factor: {proportion_of_limit})", + ] + ) + if new_non_tx_size > non_tx_max_total_size * proportion_of_limit: + return " ".join( + [ + f"non-tx size: {new_non_tx_size}", + f"> {non_tx_max_total_size * proportion_of_limit}", + f"(scale factor: {proportion_of_limit})", + ] + ) if isinstance(limits, Unlimited): # this message type is not rate limited. This is used for From 23ad9619e317b32f161137d8019b7f82a687b673 Mon Sep 17 00:00:00 2001 From: Amine Khaldi Date: Thu, 11 Sep 2025 16:54:51 +0100 Subject: [PATCH 12/16] CHIA-3736 Avoid recomputing skipped transaction ID in create_bundle_from_mempool_items (#20055) Avoid recomputing skipped transaction ID in create_bundle_from_mempool_items. --- chia/full_node/mempool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia/full_node/mempool.py b/chia/full_node/mempool.py index 25c256b44814..ade1ee17ca49 100644 --- a/chia/full_node/mempool.py +++ b/chia/full_node/mempool.py @@ -613,7 +613,7 @@ def create_bundle_from_mempool_items( if any( sd.eligible_for_dedup or sd.eligible_for_fast_forward for sd in item.bundle_coin_spends.values() ): - log.info(f"Skipping transaction with dedup or FF spends {item.spend_bundle.name()}") + log.info(f"Skipping transaction with dedup or FF spends {name}") continue unique_coin_spends = [] From 61034b5646c9b52205263e211d95af0250191c92 Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Sat, 13 Sep 2025 11:11:03 -0700 Subject: [PATCH 13/16] [CHIA-3602] Port wallet coin endpoints to `@marshal` (#19966) * Port `spend_clawback_coins` to `@marshal` * fix tests * fix test again * how do I keep missing these? * Port `select_coins` to `@marshal` * fix tests * fix test * Port `get_spendable_coins` * Port `get_coin_records_by_names` * bad port of `include_spent_coins` * Less bad replace logic * Actually autofill the CoinSelectionConfigLoader in `get_spendable_coins` * Comments by @altendky --- chia/_tests/cmds/cmd_test_utils.py | 43 +--- .../wallet/nft_wallet/test_nft_bulk_mint.py | 27 ++- chia/_tests/wallet/rpc/test_wallet_rpc.py | 221 +++++++++++------- chia/cmds/coin_funcs.py | 49 ++-- chia/wallet/util/tx_config.py | 9 +- chia/wallet/wallet_request_types.py | 76 +++++- chia/wallet/wallet_rpc_api.py | 123 +++++----- chia/wallet/wallet_rpc_client.py | 51 ++-- 8 files changed, 333 insertions(+), 266 deletions(-) diff --git a/chia/_tests/cmds/cmd_test_utils.py b/chia/_tests/cmds/cmd_test_utils.py index 8234fb838f03..1829790dc1be 100644 --- a/chia/_tests/cmds/cmd_test_utils.py +++ b/chia/_tests/cmds/cmd_test_utils.py @@ -22,7 +22,6 @@ from chia.full_node.full_node_rpc_client import FullNodeRpcClient from chia.rpc.rpc_client import RpcClient from chia.simulator.simulator_full_node_rpc_client import SimulatorFullNodeRpcClient -from chia.types.coin_record import CoinRecord from chia.types.signing_mode import SigningMode from chia.util.bech32m import encode_puzzle_hash from chia.util.config import load_config @@ -31,7 +30,7 @@ from chia.wallet.nft_wallet.nft_wallet import NFTWallet from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.transaction_type import TransactionType -from chia.wallet.util.tx_config import CoinSelectionConfig, TXConfig +from chia.wallet.util.tx_config import TXConfig from chia.wallet.util.wallet_types import WalletType from chia.wallet.wallet_request_types import ( GetSyncStatusResponse, @@ -232,46 +231,6 @@ async def nft_calculate_royalties( ) ) - async def get_spendable_coins( - self, - wallet_id: int, - coin_selection_config: CoinSelectionConfig, - ) -> tuple[list[CoinRecord], list[CoinRecord], list[Coin]]: - """ - We return a tuple containing: (confirmed records, unconfirmed removals, unconfirmed additions) - """ - self.add_to_log( - "get_spendable_coins", - (wallet_id, coin_selection_config), - ) - confirmed_records = [ - CoinRecord( - Coin(bytes32([1] * 32), bytes32([2] * 32), uint64(1234560000)), - uint32(123456), - uint32(0), - False, - uint64(0), - ), - CoinRecord( - Coin(bytes32([3] * 32), bytes32([4] * 32), uint64(1234560000)), - uint32(123456), - uint32(0), - False, - uint64(0), - ), - ] - unconfirmed_removals = [ - CoinRecord( - Coin(bytes32([5] * 32), bytes32([6] * 32), uint64(1234570000)), - uint32(123457), - uint32(0), - True, - uint64(0), - ) - ] - unconfirmed_additions = [Coin(bytes32([7] * 32), bytes32([8] * 32), uint64(1234580000))] - return confirmed_records, unconfirmed_removals, unconfirmed_additions - async def send_transaction_multi( self, wallet_id: int, diff --git a/chia/_tests/wallet/nft_wallet/test_nft_bulk_mint.py b/chia/_tests/wallet/nft_wallet/test_nft_bulk_mint.py index 26ae27020071..f6b1cff0361d 100644 --- a/chia/_tests/wallet/nft_wallet/test_nft_bulk_mint.py +++ b/chia/_tests/wallet/nft_wallet/test_nft_bulk_mint.py @@ -15,7 +15,7 @@ from chia.wallet.nft_wallet.uncurry_nft import UncurriedNFT from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.address_type import AddressType -from chia.wallet.wallet_request_types import NFTGetNFTs, NFTMintBulk, NFTMintMetadata, PushTransactions +from chia.wallet.wallet_request_types import NFTGetNFTs, NFTMintBulk, NFTMintMetadata, PushTransactions, SelectCoins async def nft_count(wallet: NFTWallet) -> int: @@ -291,22 +291,25 @@ async def test_nft_mint_rpc(wallet_environments: WalletTestFramework, zero_royal fee = 100 num_chunks = int(n / chunk) + (1 if n % chunk > 0 else 0) required_amount = n + (fee * num_chunks) - xch_coins = await env_0.rpc_client.select_coins( - amount=required_amount, - coin_selection_config=wallet_environments.tx_config.coin_selection_config, - wallet_id=wallet_0.id(), + select_coins_response = await env_0.rpc_client.select_coins( + SelectCoins.from_coin_selection_config( + amount=uint64(required_amount), + coin_selection_config=wallet_environments.tx_config.coin_selection_config, + wallet_id=wallet_0.id(), + ) ) - funding_coin = xch_coins[0] + funding_coin = select_coins_response.coins[0] assert funding_coin.amount >= required_amount - funding_coin_dict = xch_coins[0].to_json_dict() next_coin = funding_coin did_coin = ( await env_0.rpc_client.select_coins( - amount=1, - coin_selection_config=wallet_environments.tx_config.coin_selection_config, - wallet_id=env_0.wallet_aliases["did"], + SelectCoins.from_coin_selection_config( + amount=uint64(1), + coin_selection_config=wallet_environments.tx_config.coin_selection_config, + wallet_id=uint32(env_0.wallet_aliases["did"]), + ) ) - )[0] + ).coins[0] did_lineage_parent: Optional[bytes32] = None txs: list[TransactionRecord] = [] nft_ids = set() @@ -321,7 +324,7 @@ async def test_nft_mint_rpc(wallet_environments: WalletTestFramework, zero_royal mint_number_start=uint16(i + 1), mint_total=uint16(n), xch_coins=[next_coin], - xch_change_target=funding_coin_dict["puzzle_hash"], + xch_change_target=funding_coin.puzzle_hash.hex(), did_coin=did_coin if with_did else None, did_lineage_parent=did_lineage_parent if with_did else None, mint_from_did=with_did, diff --git a/chia/_tests/wallet/rpc/test_wallet_rpc.py b/chia/_tests/wallet/rpc/test_wallet_rpc.py index 1c60c5ffcc8e..8c5082d3b5df 100644 --- a/chia/_tests/wallet/rpc/test_wallet_rpc.py +++ b/chia/_tests/wallet/rpc/test_wallet_rpc.py @@ -121,9 +121,11 @@ DIDTransferDID, DIDUpdateMetadata, FungibleAsset, + GetCoinRecordsByNames, GetNextAddress, GetNotifications, GetPrivateKey, + GetSpendableCoins, GetSyncStatusResponse, GetTimestampForHeight, GetTransaction, @@ -141,6 +143,7 @@ PushTransactions, PushTX, RoyaltyAsset, + SelectCoins, SendTransaction, SetWalletResyncOnStartup, SpendClawbackCoins, @@ -645,25 +648,28 @@ async def test_create_signed_transaction( selected_coin = None if select_coin: - selected_coin = await wallet_1_rpc.select_coins( - amount=amount_total, wallet_id=wallet_id, coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG + select_coins_response = await wallet_1_rpc.select_coins( + SelectCoins.from_coin_selection_config( + amount=amount_total, wallet_id=uint32(wallet_id), coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG + ) ) - assert len(selected_coin) == 1 + assert len(select_coins_response.coins) == 1 + selected_coin = select_coins_response.coins[0] txs = ( await wallet_1_rpc.create_signed_transactions( outputs, - coins=selected_coin, + coins=[selected_coin] if selected_coin is not None else [], fee=amount_fee, wallet_id=wallet_id, # shouldn't actually block it tx_config=DEFAULT_TX_CONFIG.override( - excluded_coin_amounts=[uint64(selected_coin[0].amount)] if selected_coin is not None else [], + excluded_coin_amounts=[uint64(selected_coin.amount)] if selected_coin is not None else [], ), push=True, ) ).transactions - change_expected = not selected_coin or selected_coin[0].amount - amount_total > 0 + change_expected = not selected_coin or selected_coin.amount - amount_total > 0 assert_tx_amounts(txs[-1], outputs, amount_fee=amount_fee, change_expected=change_expected, is_cat=is_cat) # Farm the transaction and make sure the wallet balance reflects it correct @@ -784,38 +790,42 @@ async def test_create_signed_transaction_with_excluded_coins(wallet_rpc_environm await generate_funds(full_node_api, env.wallet_1) async def it_does_not_include_the_excluded_coins() -> None: - selected_coins = await wallet_1_rpc.select_coins( - amount=250000000000, wallet_id=1, coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG + select_coins_response = await wallet_1_rpc.select_coins( + SelectCoins.from_coin_selection_config( + amount=uint64(250000000000), wallet_id=uint32(1), coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG + ) ) - assert len(selected_coins) == 1 + assert len(select_coins_response.coins) == 1 outputs = await create_tx_outputs(wallet_1, [(uint64(250000000000), None)]) tx = ( await wallet_1_rpc.create_signed_transactions( outputs, DEFAULT_TX_CONFIG.override( - excluded_coin_ids=[c.name() for c in selected_coins], + excluded_coin_ids=[c.name() for c in select_coins_response.coins], ), ) ).signed_tx assert len(tx.removals) == 1 - assert tx.removals[0] != selected_coins[0] + assert tx.removals[0] != select_coins_response.coins[0] assert tx.removals[0].amount == uint64(1750000000000) await assert_push_tx_error(full_node_rpc, tx) async def it_throws_an_error_when_all_spendable_coins_are_excluded() -> None: - selected_coins = await wallet_1_rpc.select_coins( - amount=1750000000000, wallet_id=1, coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG + select_coins_response = await wallet_1_rpc.select_coins( + SelectCoins.from_coin_selection_config( + amount=uint64(1750000000000), wallet_id=uint32(1), coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG + ) ) - assert len(selected_coins) == 1 + assert len(select_coins_response.coins) == 1 outputs = await create_tx_outputs(wallet_1, [(uint64(1750000000000), None)]) with pytest.raises(ValueError): await wallet_1_rpc.create_signed_transactions( outputs, DEFAULT_TX_CONFIG.override( - excluded_coin_ids=[c.name() for c in selected_coins], + excluded_coin_ids=[c.name() for c in select_coins_response.coins], ), ) @@ -960,8 +970,10 @@ async def test_send_transaction_multi(wallet_rpc_environment: WalletRpcTestEnvir generated_funds = await generate_funds(full_node_api, env.wallet_1) - removals = await client.select_coins( - 1750000000000, wallet_id=1, coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG + select_coins_response = await client.select_coins( + SelectCoins.from_coin_selection_config( + amount=uint64(1750000000000), wallet_id=uint32(1), coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG + ) ) # we want a coin that won't be selected by default outputs = await create_tx_outputs(wallet_2, [(uint64(1), ["memo_1"]), (uint64(2), ["memo_2"])]) amount_outputs = sum(output["amount"] for output in outputs) @@ -972,7 +984,7 @@ async def test_send_transaction_multi(wallet_rpc_environment: WalletRpcTestEnvir 1, outputs, DEFAULT_TX_CONFIG, - coins=removals, + coins=select_coins_response.coins, fee=amount_fee, ) ).transaction @@ -981,7 +993,7 @@ async def test_send_transaction_multi(wallet_rpc_environment: WalletRpcTestEnvir assert send_tx_res is not None assert_tx_amounts(send_tx_res, outputs, amount_fee=amount_fee, change_expected=True) - assert send_tx_res.removals == removals + assert send_tx_res.removals == select_coins_response.coins await farm_transaction(full_node_api, wallet_node, spend_bundle) @@ -1355,8 +1367,12 @@ async def test_cat_endpoints(wallet_environments: WalletTestFramework, wallet_ty await wallet_environments.process_pending_states(cat_spend_changes) # Test CAT spend with a fee and pre-specified removals / coins - removals = await env_0.rpc_client.select_coins( - amount=uint64(2), wallet_id=cat_0_id, coin_selection_config=wallet_environments.tx_config.coin_selection_config + select_coins_response = await env_0.rpc_client.select_coins( + SelectCoins.from_coin_selection_config( + amount=uint64(2), + wallet_id=cat_0_id, + coin_selection_config=wallet_environments.tx_config.coin_selection_config, + ) ) tx_res = await env_0.rpc_client.cat_spend( cat_0_id, @@ -1365,12 +1381,12 @@ async def test_cat_endpoints(wallet_environments: WalletTestFramework, wallet_ty addr_1, uint64(5_000_000), ["the cat memo"], - removals=removals, + removals=select_coins_response.coins, ) spend_bundle = tx_res.transaction.spend_bundle assert spend_bundle is not None - assert removals[0] in {removal for tx in tx_res.transactions for removal in tx.removals} + assert select_coins_response.coins[0] in {removal for tx in tx_res.transactions for removal in tx.removals} await wallet_environments.process_pending_states(cat_spend_changes) @@ -1382,10 +1398,14 @@ async def test_cat_endpoints(wallet_environments: WalletTestFramework, wallet_ty assert len(cats) == 1 # Test CAT coin selection - selected_coins = await env_0.rpc_client.select_coins( - amount=1, wallet_id=cat_0_id, coin_selection_config=wallet_environments.tx_config.coin_selection_config + select_coins_response = await env_0.rpc_client.select_coins( + SelectCoins.from_coin_selection_config( + amount=uint64(1), + wallet_id=cat_0_id, + coin_selection_config=wallet_environments.tx_config.coin_selection_config, + ) ) - assert len(selected_coins) > 0 + assert len(select_coins_response.coins) > 0 # Test get_cat_list cat_list = (await env_0.rpc_client.get_cat_list()).cat_list @@ -1485,13 +1505,17 @@ async def test_offer_endpoints(wallet_environments: WalletTestFramework, wallet_ ] ) - test_crs: list[CoinRecord] = await env_1.rpc_client.get_coin_records_by_names( - [a.name() for a in spend_bundle.additions() if a.amount != 4] - ) + test_crs: list[CoinRecord] = ( + await env_1.rpc_client.get_coin_records_by_names( + GetCoinRecordsByNames([a.name() for a in spend_bundle.additions() if a.amount != 4]) + ) + ).coin_records for cr in test_crs: assert cr.coin in spend_bundle.additions() with pytest.raises(ValueError): - await env_1.rpc_client.get_coin_records_by_names([a.name() for a in spend_bundle.additions() if a.amount == 4]) + await env_1.rpc_client.get_coin_records_by_names( + GetCoinRecordsByNames([a.name() for a in spend_bundle.additions() if a.amount == 4]) + ) # Create an offer of 5 chia for one CAT await env_1.rpc_client.create_offer_for_ids( {uint32(1): -5, cat_asset_id.hex(): 1}, wallet_environments.tx_config, validate_only=True @@ -1836,16 +1860,18 @@ async def test_get_coin_records_by_names(wallet_rpc_environment: WalletRpcTestEn assert len(coin_ids_unspent) > 0 # Do some queries to trigger all parameters # 1. Empty coin_ids - assert await client.get_coin_records_by_names([]) == [] + assert (await client.get_coin_records_by_names(GetCoinRecordsByNames([]))).coin_records == [] # 2. All coins - rpc_result = await client.get_coin_records_by_names(coin_ids + coin_ids_unspent) - assert {record.coin for record in rpc_result} == {*coins, *coins_unspent} + rpc_result = await client.get_coin_records_by_names(GetCoinRecordsByNames(coin_ids + coin_ids_unspent)) + assert {record.coin for record in rpc_result.coin_records} == {*coins, *coins_unspent} # 3. All spent coins - rpc_result = await client.get_coin_records_by_names(coin_ids, include_spent_coins=True) - assert {record.coin for record in rpc_result} == coins + rpc_result = await client.get_coin_records_by_names(GetCoinRecordsByNames(coin_ids, include_spent_coins=True)) + assert {record.coin for record in rpc_result.coin_records} == coins # 4. All unspent coins - rpc_result = await client.get_coin_records_by_names(coin_ids_unspent, include_spent_coins=False) - assert {record.coin for record in rpc_result} == coins_unspent + rpc_result = await client.get_coin_records_by_names( + GetCoinRecordsByNames(coin_ids_unspent, include_spent_coins=False) + ) + assert {record.coin for record in rpc_result.coin_records} == coins_unspent # 5. Filter start/end height filter_records = result.records[:10] assert len(filter_records) == 10 @@ -1854,11 +1880,13 @@ async def test_get_coin_records_by_names(wallet_rpc_environment: WalletRpcTestEn min_height = min(record.confirmed_block_height for record in filter_records) max_height = max(record.confirmed_block_height for record in filter_records) assert min_height != max_height - rpc_result = await client.get_coin_records_by_names(filter_coin_ids, start_height=min_height, end_height=max_height) - assert {record.coin for record in rpc_result} == filter_coins + rpc_result = await client.get_coin_records_by_names( + GetCoinRecordsByNames(filter_coin_ids, start_height=min_height, end_height=max_height) + ) + assert {record.coin for record in rpc_result.coin_records} == filter_coins # 8. Test the failure case with pytest.raises(ValueError, match="not found"): - await client.get_coin_records_by_names(coin_ids, include_spent_coins=False) + await client.get_coin_records_by_names(GetCoinRecordsByNames(coin_ids, include_spent_coins=False)) @pytest.mark.anyio @@ -2299,51 +2327,63 @@ async def test_select_coins_rpc(wallet_rpc_environment: WalletRpcTestEnvironment await time_out_assert(20, get_confirmed_balance, funds, client, 1) # test min coin amount - min_coins: list[Coin] = await client_2.select_coins( - amount=1000, - wallet_id=1, - coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG.override(min_coin_amount=uint64(1001)), + min_coins_response = await client_2.select_coins( + SelectCoins.from_coin_selection_config( + amount=uint64(1000), + wallet_id=uint32(1), + coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG.override(min_coin_amount=uint64(1001)), + ) ) - assert len(min_coins) == 1 - assert min_coins[0].amount == uint64(10_000) + assert len(min_coins_response.coins) == 1 + assert min_coins_response.coins[0].amount == uint64(10_000) # test max coin amount - max_coins: list[Coin] = await client_2.select_coins( - amount=2000, - wallet_id=1, - coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG.override( - min_coin_amount=uint64(999), max_coin_amount=uint64(9999) - ), + max_coins_reponse = await client_2.select_coins( + SelectCoins.from_coin_selection_config( + amount=uint64(2000), + wallet_id=uint32(1), + coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG.override( + min_coin_amount=uint64(999), max_coin_amount=uint64(9999) + ), + ) ) - assert len(max_coins) == 2 - assert max_coins[0].amount == uint64(1000) + assert len(max_coins_reponse.coins) == 2 + assert max_coins_reponse.coins[0].amount == uint64(1000) # test excluded coin amounts non_1000_amt: int = sum(a for a in tx_amounts if a != 1000) - excluded_amt_coins: list[Coin] = await client_2.select_coins( - amount=non_1000_amt, - wallet_id=1, - coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG.override(excluded_coin_amounts=[uint64(1000)]), + excluded_amt_coins_response = await client_2.select_coins( + SelectCoins.from_coin_selection_config( + amount=uint64(non_1000_amt), + wallet_id=uint32(1), + coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG.override(excluded_coin_amounts=[uint64(1000)]), + ) ) - assert len(excluded_amt_coins) == len([a for a in tx_amounts if a != 1000]) - assert sum(c.amount for c in excluded_amt_coins) == non_1000_amt + assert len(excluded_amt_coins_response.coins) == len([a for a in tx_amounts if a != 1000]) + assert sum(c.amount for c in excluded_amt_coins_response.coins) == non_1000_amt # test excluded coins with pytest.raises(ValueError): await client_2.select_coins( - amount=5000, - wallet_id=1, + SelectCoins.from_coin_selection_config( + amount=uint64(5000), + wallet_id=uint32(1), + coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG.override( + excluded_coin_ids=[c.name() for c in min_coins_response.coins] + ), + ) + ) + excluded_test_response = await client_2.select_coins( + SelectCoins.from_coin_selection_config( + amount=uint64(1300), + wallet_id=uint32(1), coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG.override( - excluded_coin_ids=[c.name() for c in min_coins] + excluded_coin_ids=[c.name() for c in coin_300] ), ) - excluded_test = await client_2.select_coins( - amount=1300, - wallet_id=1, - coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG.override(excluded_coin_ids=[c.name() for c in coin_300]), ) - assert len(excluded_test) == 2 - for coin in excluded_test: + assert len(excluded_test_response.coins) == 2 + for coin in excluded_test_response.coins: assert coin != coin_300[0] # test backwards compatibility in the RPC @@ -2362,27 +2402,40 @@ async def test_select_coins_rpc(wallet_rpc_environment: WalletRpcTestEnvironment assert coin != coin_300[0] # test get coins - all_coins, _, _ = await client_2.get_spendable_coins( - wallet_id=1, - coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG.override( - excluded_coin_ids=[c.name() for c in excluded_amt_coins] + spendable_coins_response = await client_2.get_spendable_coins( + GetSpendableCoins.from_coin_selection_config( + wallet_id=uint32(1), + coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG.override( + excluded_coin_ids=[c.name() for c in excluded_amt_coins_response.coins] + ), ), ) - assert set(excluded_amt_coins).intersection({rec.coin for rec in all_coins}) == set() - all_coins, _, _ = await client_2.get_spendable_coins( - wallet_id=1, - coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG.override(excluded_coin_amounts=[uint64(1000)]), + assert ( + set(excluded_amt_coins_response.coins).intersection( + {rec.coin for rec in spendable_coins_response.confirmed_records} + ) + == set() ) - assert len([rec for rec in all_coins if rec.coin.amount == 1000]) == 0 - all_coins_2, _, _ = await client_2.get_spendable_coins( - wallet_id=1, - coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG.override(max_coin_amount=uint64(999)), + spendable_coins_response = await client_2.get_spendable_coins( + GetSpendableCoins.from_coin_selection_config( + wallet_id=uint32(1), + coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG.override(excluded_coin_amounts=[uint64(1000)]), + ) ) - assert all_coins_2[0].coin == coin_300[0] + assert len([rec for rec in spendable_coins_response.confirmed_records if rec.coin.amount == 1000]) == 0 + spendable_coins_response = await client_2.get_spendable_coins( + GetSpendableCoins.from_coin_selection_config( + wallet_id=uint32(1), + coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG.override(max_coin_amount=uint64(999)), + ) + ) + assert spendable_coins_response.confirmed_records[0].coin == coin_300[0] with pytest.raises(ValueError): # validate fail on invalid coin id. await client_2.get_spendable_coins( - wallet_id=1, - coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG.override(excluded_coin_ids=[b"a"]), + GetSpendableCoins.from_coin_selection_config( + wallet_id=uint32(1), + coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG.override(excluded_coin_ids=[b"a"]), + ) ) diff --git a/chia/cmds/coin_funcs.py b/chia/cmds/coin_funcs.py index 1a9bc9c1dea0..f6b205d38f44 100644 --- a/chia/cmds/coin_funcs.py +++ b/chia/cmds/coin_funcs.py @@ -18,7 +18,7 @@ from chia.wallet.conditions import ConditionValidTimes from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.wallet_types import WalletType -from chia.wallet.wallet_request_types import CombineCoins, SplitCoins +from chia.wallet.wallet_request_types import CombineCoins, GetCoinRecordsByNames, GetSpendableCoins, SplitCoins async def async_list( @@ -44,23 +44,28 @@ async def async_list( if not (await client_info.client.get_sync_status()).synced: print("Wallet not synced. Please wait.") return - conf_coins, unconfirmed_removals, unconfirmed_additions = await client_info.client.get_spendable_coins( - wallet_id=wallet_id, - coin_selection_config=CMDCoinSelectionConfigLoader( - max_coin_amount=max_coin_amount, - min_coin_amount=min_coin_amount, - excluded_coin_amounts=list(excluded_amounts), - excluded_coin_ids=list(excluded_coin_ids), - ).to_coin_selection_config(mojo_per_unit), + response = await client_info.client.get_spendable_coins( + GetSpendableCoins.from_coin_selection_config( + wallet_id=uint32(wallet_id), + coin_selection_config=CMDCoinSelectionConfigLoader( + max_coin_amount=max_coin_amount, + min_coin_amount=min_coin_amount, + excluded_coin_amounts=list(excluded_amounts), + excluded_coin_ids=list(excluded_coin_ids), + ).to_coin_selection_config(mojo_per_unit), + ) + ) + print( + f"There are a total of {len(response.confirmed_records) + len(response.unconfirmed_additions)}" + f" coins in wallet {wallet_id}." ) - print(f"There are a total of {len(conf_coins) + len(unconfirmed_additions)} coins in wallet {wallet_id}.") - print(f"{len(conf_coins)} confirmed coins.") - print(f"{len(unconfirmed_additions)} unconfirmed additions.") - print(f"{len(unconfirmed_removals)} unconfirmed removals.") + print(f"{len(response.confirmed_records)} confirmed coins.") + print(f"{len(response.unconfirmed_additions)} unconfirmed additions.") + print(f"{len(response.unconfirmed_removals)} unconfirmed removals.") print("Confirmed coins:") print_coins( "\tAddress: {} Amount: {}, Confirmed in block: {}\n", - [(cr.coin, str(cr.confirmed_block_index)) for cr in conf_coins], + [(cr.coin, str(cr.confirmed_block_index)) for cr in response.confirmed_records], mojo_per_unit, addr_prefix, paginate, @@ -69,7 +74,7 @@ async def async_list( print("\nUnconfirmed Removals:") print_coins( "\tPrevious Address: {} Amount: {}, Confirmed in block: {}\n", - [(cr.coin, str(cr.confirmed_block_index)) for cr in unconfirmed_removals], + [(cr.coin, str(cr.confirmed_block_index)) for cr in response.unconfirmed_removals], mojo_per_unit, addr_prefix, paginate, @@ -77,7 +82,7 @@ async def async_list( print("\nUnconfirmed Additions:") print_coins( "\tNew Address: {} Amount: {}, Not yet confirmed in a block.{}\n", - [(coin, "") for coin in unconfirmed_additions], + [(coin, "") for coin in response.unconfirmed_additions], mojo_per_unit, addr_prefix, paginate, @@ -217,19 +222,19 @@ async def async_split( return [] if number_of_coins is None: - coins = await client_info.client.get_coin_records_by_names([target_coin_id]) - if len(coins) == 0: + response = await client_info.client.get_coin_records_by_names(GetCoinRecordsByNames([target_coin_id])) + if len(response.coin_records) == 0: print("Could not find target coin.") return [] assert amount_per_coin is not None - number_of_coins = int(coins[0].coin.amount // amount_per_coin.convert_amount(mojo_per_unit)) + number_of_coins = int(response.coin_records[0].coin.amount // amount_per_coin.convert_amount(mojo_per_unit)) elif amount_per_coin is None: - coins = await client_info.client.get_coin_records_by_names([target_coin_id]) - if len(coins) == 0: + response = await client_info.client.get_coin_records_by_names(GetCoinRecordsByNames([target_coin_id])) + if len(response.coin_records) == 0: print("Could not find target coin.") return [] assert number_of_coins is not None - amount_per_coin = CliAmount(True, uint64(coins[0].coin.amount // number_of_coins)) + amount_per_coin = CliAmount(True, uint64(response.coin_records[0].coin.amount // number_of_coins)) final_amount_per_coin = amount_per_coin.convert_amount(mojo_per_unit) diff --git a/chia/wallet/util/tx_config.py b/chia/wallet/util/tx_config.py index ddca5c89280a..2f4b05f07414 100644 --- a/chia/wallet/util/tx_config.py +++ b/chia/wallet/util/tx_config.py @@ -88,7 +88,7 @@ def autofill( @classmethod def from_json_dict(cls, json_dict: dict[str, Any]) -> Self: - if "excluded_coins" in json_dict: + if json_dict.get("excluded_coins") is not None: excluded_coins: list[Coin] = [Coin.from_json_dict(c) for c in json_dict["excluded_coins"]] excluded_coin_ids: list[str] = [c.name().hex() for c in excluded_coins] if "excluded_coin_ids" in json_dict: @@ -98,7 +98,8 @@ def from_json_dict(cls, json_dict: dict[str, Any]) -> Self: return super().from_json_dict(json_dict) # This function is purely for ergonomics - def override(self, **kwargs: Any) -> CoinSelectionConfigLoader: + # But creates a small linting complication + def override(self, **kwargs: Any) -> Self: return dataclasses.replace(self, **kwargs) @@ -138,10 +139,6 @@ def autofill( reuse_puzhash, ) - # This function is purely for ergonomics - def override(self, **kwargs: Any) -> TXConfigLoader: - return dataclasses.replace(self, **kwargs) - DEFAULT_COIN_SELECTION_CONFIG = CoinSelectionConfig(uint64(0), uint64(DEFAULT_CONSTANTS.MAX_COIN_AMOUNT), [], []) DEFAULT_TX_CONFIG = TXConfig( diff --git a/chia/wallet/wallet_request_types.py b/chia/wallet/wallet_request_types.py index dc76faa16e4e..d40fcfc8df75 100644 --- a/chia/wallet/wallet_request_types.py +++ b/chia/wallet/wallet_request_types.py @@ -13,6 +13,7 @@ from chia.data_layer.singleton_record import SingletonRecord from chia.pools.pool_wallet_info import PoolWalletInfo from chia.types.blockchain_format.program import Program +from chia.types.coin_record import CoinRecord from chia.util.byte_types import hexstr_to_bytes from chia.util.streamable import Streamable, streamable from chia.wallet.conditions import Condition, ConditionValidTimes, conditions_to_json_dicts @@ -32,7 +33,7 @@ from chia.wallet.util.clvm_streamable import json_deserialize_with_clvm_streamable from chia.wallet.util.puzzle_decorator_type import PuzzleDecoratorType from chia.wallet.util.query_filter import TransactionTypeFilter -from chia.wallet.util.tx_config import TXConfig +from chia.wallet.util.tx_config import CoinSelectionConfig, CoinSelectionConfigLoader, TXConfig from chia.wallet.vc_wallet.vc_store import VCProofs, VCRecord from chia.wallet.wallet_info import WalletInfo from chia.wallet.wallet_node import Balance @@ -480,6 +481,79 @@ class DeleteUnconfirmedTransactions(Streamable): wallet_id: uint32 +@streamable +@dataclass(frozen=True) +class SelectCoins(CoinSelectionConfigLoader): + wallet_id: uint32 = field(default_factory=default_raise) + amount: uint64 = field(default_factory=default_raise) + exclude_coins: Optional[list[Coin]] = None # for backwards compatibility + + def __post_init__(self) -> None: + if self.excluded_coin_ids is not None and self.exclude_coins is not None: + raise ValueError( + "Cannot specify both excluded_coin_ids/excluded_coins and exclude_coins (the latter is deprecated)" + ) + super().__post_init__() + + @classmethod + def from_coin_selection_config( + cls, wallet_id: uint32, amount: uint64, coin_selection_config: CoinSelectionConfig + ) -> Self: + return cls( + wallet_id=wallet_id, + amount=amount, + min_coin_amount=coin_selection_config.min_coin_amount, + max_coin_amount=coin_selection_config.max_coin_amount, + excluded_coin_amounts=coin_selection_config.excluded_coin_amounts, + excluded_coin_ids=coin_selection_config.excluded_coin_ids, + ) + + +@streamable +@dataclass(frozen=True) +class SelectCoinsResponse(Streamable): + coins: list[Coin] + + +@streamable +@dataclass(frozen=True) +class GetSpendableCoins(CoinSelectionConfigLoader): + wallet_id: uint32 = field(default_factory=default_raise) + + @classmethod + def from_coin_selection_config(cls, wallet_id: uint32, coin_selection_config: CoinSelectionConfig) -> Self: + return cls( + wallet_id=wallet_id, + min_coin_amount=coin_selection_config.min_coin_amount, + max_coin_amount=coin_selection_config.max_coin_amount, + excluded_coin_amounts=coin_selection_config.excluded_coin_amounts, + excluded_coin_ids=coin_selection_config.excluded_coin_ids, + ) + + +@streamable +@dataclass(frozen=True) +class GetSpendableCoinsResponse(Streamable): + confirmed_records: list[CoinRecord] + unconfirmed_removals: list[CoinRecord] + unconfirmed_additions: list[Coin] + + +@streamable +@dataclass(frozen=True) +class GetCoinRecordsByNames(Streamable): + names: list[bytes32] + start_height: Optional[uint32] = None + end_height: Optional[uint32] = None + include_spent_coins: bool = True + + +@streamable +@dataclass(frozen=True) +class GetCoinRecordsByNamesResponse(Streamable): + coin_records: list[CoinRecord] + + @streamable @dataclass(frozen=True) class GetCurrentDerivationIndexResponse(Streamable): diff --git a/chia/wallet/wallet_rpc_api.py b/chia/wallet/wallet_rpc_api.py index db33a641a332..0d4a97610ff2 100644 --- a/chia/wallet/wallet_rpc_api.py +++ b/chia/wallet/wallet_rpc_api.py @@ -27,7 +27,7 @@ from chia.types.signing_mode import CHIP_0002_SIGN_MESSAGE_PREFIX, SigningMode 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, str2bool +from chia.util.config import load_config from chia.util.errors import KeychainIsLocked from chia.util.hash import std_hash from chia.util.keychain import bytes_to_mnemonic, generate_mnemonic @@ -172,6 +172,8 @@ GatherSigningInfo, GatherSigningInfoResponse, GenerateMnemonicResponse, + GetCoinRecordsByNames, + GetCoinRecordsByNamesResponse, GetCurrentDerivationIndexResponse, GetHeightInfoResponse, GetLoggedInFingerprintResponse, @@ -183,6 +185,8 @@ GetPrivateKeyFormat, GetPrivateKeyResponse, GetPublicKeysResponse, + GetSpendableCoins, + GetSpendableCoinsResponse, GetSyncStatusResponse, GetTimestampForHeight, GetTimestampForHeightResponse, @@ -242,6 +246,8 @@ PWSelfPoolResponse, PWStatus, PWStatusResponse, + SelectCoins, + SelectCoinsResponse, SendTransaction, SendTransactionResponse, SetWalletResyncOnStartup, @@ -1728,74 +1734,63 @@ async def delete_unconfirmed_transactions(self, request: DeleteUnconfirmedTransa wallet.target_state = None return Empty() + @marshal async def select_coins( self, - request: dict[str, Any], - ) -> EndpointResult: + request: SelectCoins, + ) -> SelectCoinsResponse: assert self.service.logged_in_fingerprint is not None - tx_config_loader: TXConfigLoader = TXConfigLoader.from_json_dict(request) # Some backwards compat fill-ins - if tx_config_loader.excluded_coin_ids is None: - excluded_coins: Optional[list[dict[str, Any]]] = request.get("excluded_coins", request.get("exclude_coins")) - if excluded_coins is not None: - tx_config_loader = tx_config_loader.override( - excluded_coin_ids=[Coin.from_json_dict(c).name() for c in excluded_coins], + if request.excluded_coin_ids is None: + if request.exclude_coins is not None: + request = request.override( + excluded_coin_ids=[c.name() for c in request.exclude_coins], + exclude_coins=None, ) - tx_config: TXConfig = tx_config_loader.autofill( + # don't love this snippet of code + # but I think action scopes need to accept CoinSelectionConfigs + # instead of solely TXConfigs in order for this to be less ugly + autofilled_cs_config = request.autofill( constants=self.service.wallet_state_manager.constants, ) + tx_config = DEFAULT_TX_CONFIG.override( + **{ + field.name: getattr(autofilled_cs_config, field.name) + for field in dataclasses.fields(autofilled_cs_config) + } + ) 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] + wallet = self.service.wallet_state_manager.wallets[request.wallet_id] async with self.service.wallet_state_manager.new_action_scope(tx_config, push=False) as action_scope: - selected_coins = await wallet.select_coins(amount, action_scope) + selected_coins = await wallet.select_coins(request.amount, action_scope) - return {"coins": [coin.to_json_dict() for coin in selected_coins]} + return SelectCoinsResponse(coins=list(selected_coins)) - async def get_spendable_coins(self, request: dict[str, Any]) -> EndpointResult: + @marshal + async def get_spendable_coins(self, request: GetSpendableCoins) -> GetSpendableCoinsResponse: if await self.service.wallet_state_manager.synced() is False: raise ValueError("Wallet needs to be fully synced before getting all coins") - wallet_id = uint32(request["wallet_id"]) - min_coin_amount = uint64(request.get("min_coin_amount", 0)) - max_coin_amount: uint64 = uint64(request.get("max_coin_amount", 0)) - if max_coin_amount == 0: - max_coin_amount = uint64(self.service.wallet_state_manager.constants.MAX_COIN_AMOUNT) - excluded_coin_amounts: Optional[list[uint64]] = request.get("excluded_coin_amounts") - if excluded_coin_amounts is not None: - excluded_coin_amounts = [uint64(a) for a in excluded_coin_amounts] - else: - excluded_coin_amounts = [] - excluded_coins_input: Optional[dict[str, dict[str, Any]]] = request.get("excluded_coins") - if excluded_coins_input is not None: - excluded_coins = [Coin.from_json_dict(json_coin) for json_coin in excluded_coins_input.values()] - else: - excluded_coins = [] - excluded_coin_ids_input: Optional[list[str]] = request.get("excluded_coin_ids") - if excluded_coin_ids_input is not None: - excluded_coin_ids = [bytes32.from_hexstr(hex_id) for hex_id in excluded_coin_ids_input] - else: - excluded_coin_ids = [] state_mgr = self.service.wallet_state_manager - wallet = state_mgr.wallets[wallet_id] + wallet = state_mgr.wallets[request.wallet_id] async with state_mgr.lock: - all_coin_records = await state_mgr.coin_store.get_unspent_coins_for_wallet(wallet_id) + all_coin_records = await state_mgr.coin_store.get_unspent_coins_for_wallet(request.wallet_id) if wallet.type() in {WalletType.CAT, WalletType.CRCAT, WalletType.RCAT}: assert isinstance(wallet, CATWallet) spendable_coins: list[WalletCoinRecord] = await wallet.get_cat_spendable_coins(all_coin_records) else: - spendable_coins = list(await state_mgr.get_spendable_coins_for_wallet(wallet_id, all_coin_records)) + spendable_coins = list( + await state_mgr.get_spendable_coins_for_wallet(request.wallet_id, all_coin_records) + ) # Now we get the unconfirmed transactions and manually derive the additions and removals. unconfirmed_transactions: list[TransactionRecord] = await state_mgr.tx_store.get_unconfirmed_for_wallet( - wallet_id + request.wallet_id ) unconfirmed_removal_ids: dict[bytes32, uint64] = { coin.name(): transaction.created_at_time @@ -1806,54 +1801,54 @@ async def get_spendable_coins(self, request: dict[str, Any]) -> EndpointResult: coin for transaction in unconfirmed_transactions for coin in transaction.additions - if await state_mgr.does_coin_belong_to_wallet(coin, wallet_id) + if await state_mgr.does_coin_belong_to_wallet(coin, request.wallet_id) ] valid_spendable_cr: list[CoinRecord] = [] unconfirmed_removals: list[CoinRecord] = [] for coin_record in all_coin_records: if coin_record.name() in unconfirmed_removal_ids: unconfirmed_removals.append(coin_record.to_coin_record(unconfirmed_removal_ids[coin_record.name()])) + + cs_config = request.autofill(constants=self.service.wallet_state_manager.constants) for coin_record in spendable_coins: # remove all the unconfirmed coins, exclude coins and dust. if coin_record.name() in unconfirmed_removal_ids: continue - if coin_record.coin in excluded_coins: - continue - if coin_record.name() in excluded_coin_ids: + if coin_record.coin.name() in cs_config.excluded_coin_ids: continue - if coin_record.coin.amount < min_coin_amount or coin_record.coin.amount > max_coin_amount: + if (coin_record.coin.amount < cs_config.min_coin_amount) or ( + coin_record.coin.amount > cs_config.max_coin_amount + ): continue - if coin_record.coin.amount in excluded_coin_amounts: + if coin_record.coin.amount in cs_config.excluded_coin_amounts: continue c_r = await state_mgr.get_coin_record_by_wallet_record(coin_record) assert c_r is not None and c_r.coin == coin_record.coin # this should never happen valid_spendable_cr.append(c_r) - return { - "confirmed_records": [cr.to_json_dict() for cr in valid_spendable_cr], - "unconfirmed_removals": [cr.to_json_dict() for cr in unconfirmed_removals], - "unconfirmed_additions": [coin.to_json_dict() for coin in unconfirmed_additions], - } + return GetSpendableCoinsResponse( + confirmed_records=valid_spendable_cr, + unconfirmed_removals=unconfirmed_removals, + unconfirmed_additions=unconfirmed_additions, + ) - async def get_coin_records_by_names(self, request: dict[str, Any]) -> EndpointResult: + @marshal + async def get_coin_records_by_names(self, request: GetCoinRecordsByNames) -> GetCoinRecordsByNamesResponse: if await self.service.wallet_state_manager.synced() is False: raise ValueError("Wallet needs to be fully synced before finding coin information") - if "names" not in request: - raise ValueError("Names not in request") - coin_ids = [bytes32.from_hexstr(name) for name in request["names"]] kwargs: dict[str, Any] = { - "coin_id_filter": HashFilter.include(coin_ids), + "coin_id_filter": HashFilter.include(request.names), } confirmed_range = UInt32Range() - if "start_height" in request: - confirmed_range = dataclasses.replace(confirmed_range, start=uint32(request["start_height"])) - if "end_height" in request: - confirmed_range = dataclasses.replace(confirmed_range, stop=uint32(request["end_height"])) + if request.start_height is not None: + confirmed_range = dataclasses.replace(confirmed_range, start=request.start_height) + if request.end_height is not None: + confirmed_range = dataclasses.replace(confirmed_range, stop=request.end_height) if confirmed_range != UInt32Range(): kwargs["confirmed_range"] = confirmed_range - if "include_spent_coins" in request and not str2bool(request["include_spent_coins"]): + if not request.include_spent_coins: kwargs["spent_range"] = unspent_range async with self.service.wallet_state_manager.lock: @@ -1861,12 +1856,12 @@ async def get_coin_records_by_names(self, request: dict[str, Any]) -> EndpointRe **kwargs ) missed_coins: list[str] = [ - "0x" + c_id.hex() for c_id in coin_ids if c_id not in [cr.name for cr in coin_records] + "0x" + c_id.hex() for c_id in request.names if c_id not in [cr.name for cr in coin_records] ] if missed_coins: raise ValueError(f"Coin ID's: {missed_coins} not found.") - return {"coin_records": [cr.to_json_dict() for cr in coin_records]} + return GetCoinRecordsByNamesResponse(coin_records) @marshal async def get_current_derivation_index(self, request: Empty) -> GetCurrentDerivationIndexResponse: diff --git a/chia/wallet/wallet_rpc_client.py b/chia/wallet/wallet_rpc_client.py index 16ecf7d584e0..14d2a2fcd515 100644 --- a/chia/wallet/wallet_rpc_client.py +++ b/chia/wallet/wallet_rpc_client.py @@ -9,14 +9,13 @@ from chia.rpc.rpc_client import RpcClient from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.program import Program -from chia.types.coin_record import CoinRecord from chia.wallet.conditions import Condition, ConditionValidTimes, conditions_to_json_dicts from chia.wallet.puzzles.clawback.metadata import AutoClaimSettings from chia.wallet.trade_record import TradeRecord from chia.wallet.trading.offer import Offer from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.clvm_streamable import json_deserialize_with_clvm_streamable -from chia.wallet.util.tx_config import CoinSelectionConfig, TXConfig +from chia.wallet.util.tx_config import TXConfig from chia.wallet.wallet_coin_store import GetCoinRecords from chia.wallet.wallet_request_types import ( AddKey, @@ -88,6 +87,8 @@ GatherSigningInfoResponse, GenerateMnemonicResponse, GetCATListResponse, + GetCoinRecordsByNames, + GetCoinRecordsByNamesResponse, GetCurrentDerivationIndexResponse, GetHeightInfoResponse, GetLoggedInFingerprintResponse, @@ -99,6 +100,8 @@ GetPrivateKey, GetPrivateKeyResponse, GetPublicKeysResponse, + GetSpendableCoins, + GetSpendableCoinsResponse, GetSyncStatusResponse, GetTimestampForHeight, GetTimestampForHeightResponse, @@ -157,6 +160,8 @@ PWSelfPoolResponse, PWStatus, PWStatusResponse, + SelectCoins, + SelectCoinsResponse, SendTransaction, SendTransactionMultiResponse, SendTransactionResponse, @@ -405,43 +410,19 @@ async def create_signed_transactions( response = await self.fetch("create_signed_transaction", request) return json_deserialize_with_clvm_streamable(response, CreateSignedTransactionsResponse) - async def select_coins(self, amount: int, wallet_id: int, coin_selection_config: CoinSelectionConfig) -> list[Coin]: - request = {"amount": amount, "wallet_id": wallet_id, **coin_selection_config.to_json_dict()} - response = await self.fetch("select_coins", request) - return [Coin.from_json_dict(coin) for coin in response["coins"]] + async def select_coins(self, request: SelectCoins) -> SelectCoinsResponse: + return SelectCoinsResponse.from_json_dict(await self.fetch("select_coins", request.to_json_dict())) async def get_coin_records(self, request: GetCoinRecords) -> dict[str, Any]: return await self.fetch("get_coin_records", request.to_json_dict()) - async def get_spendable_coins( - self, wallet_id: int, coin_selection_config: CoinSelectionConfig - ) -> tuple[list[CoinRecord], list[CoinRecord], list[Coin]]: - """ - We return a tuple containing: (confirmed records, unconfirmed removals, unconfirmed additions) - """ - request = {"wallet_id": wallet_id, **coin_selection_config.to_json_dict()} - response = await self.fetch("get_spendable_coins", request) - confirmed_wrs = [CoinRecord.from_json_dict(coin) for coin in response["confirmed_records"]] - unconfirmed_removals = [CoinRecord.from_json_dict(coin) for coin in response["unconfirmed_removals"]] - unconfirmed_additions = [Coin.from_json_dict(coin) for coin in response["unconfirmed_additions"]] - return confirmed_wrs, unconfirmed_removals, unconfirmed_additions - - async def get_coin_records_by_names( - self, - names: list[bytes32], - include_spent_coins: bool = True, - start_height: Optional[int] = None, - end_height: Optional[int] = None, - ) -> list[CoinRecord]: - names_hex = [name.hex() for name in names] - request = {"names": names_hex, "include_spent_coins": include_spent_coins} - if start_height is not None: - request["start_height"] = start_height - if end_height is not None: - request["end_height"] = end_height - - response = await self.fetch("get_coin_records_by_names", request) - return [CoinRecord.from_json_dict(cr) for cr in response["coin_records"]] + async def get_spendable_coins(self, request: GetSpendableCoins) -> GetSpendableCoinsResponse: + return GetSpendableCoinsResponse.from_json_dict(await self.fetch("get_spendable_coins", request.to_json_dict())) + + async def get_coin_records_by_names(self, request: GetCoinRecordsByNames) -> GetCoinRecordsByNamesResponse: + return GetCoinRecordsByNamesResponse.from_json_dict( + await self.fetch("get_coin_records_by_names", request.to_json_dict()) + ) # DID wallet async def create_new_did_wallet( From 83e164d3caec312e3d1349ae09c7d3635bbc6024 Mon Sep 17 00:00:00 2001 From: Chris Marslender Date: Tue, 16 Sep 2025 09:36:34 -0500 Subject: [PATCH 14/16] Use setup-node action and .nvmrc specified version for all installers (#20060) * Update all installer builds to use the version of node specified in chia-blockchain-gui .nvmrc * Update the workflows that didn't previously use the setup-node action to use it, to make future updates easier (this action now works with all the currently supported platforms we build on) --- .github/workflows/build-linux-installer-deb.yml | 5 +++++ .github/workflows/build-linux-installer-rpm.yml | 5 +++++ .github/workflows/build-macos-installers.yml | 6 +++--- .github/workflows/build-windows-installer.yml | 6 +++--- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-linux-installer-deb.yml b/.github/workflows/build-linux-installer-deb.yml index d6df3e8fe6bd..df20dc18a52f 100644 --- a/.github/workflows/build-linux-installer-deb.yml +++ b/.github/workflows/build-linux-installer-deb.yml @@ -92,6 +92,11 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Setup Node per .nvmrc in GUI + uses: actions/setup-node@v5 + with: + node-version-file: chia-blockchain-gui/.nvmrc + - name: Get latest madmax plotter env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/build-linux-installer-rpm.yml b/.github/workflows/build-linux-installer-rpm.yml index 54d8f6f6dca9..7b11db217d0b 100644 --- a/.github/workflows/build-linux-installer-rpm.yml +++ b/.github/workflows/build-linux-installer-rpm.yml @@ -82,6 +82,11 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Setup Node per .nvmrc in GUI + uses: actions/setup-node@v5 + with: + node-version-file: chia-blockchain-gui/.nvmrc + - name: Get latest madmax plotter env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/build-macos-installers.yml b/.github/workflows/build-macos-installers.yml index 0e4a7068a398..0ec86131e18b 100644 --- a/.github/workflows/build-macos-installers.yml +++ b/.github/workflows/build-macos-installers.yml @@ -176,10 +176,10 @@ jobs: - uses: chia-network/actions/activate-venv@main - - name: Setup Node 20.x - uses: actions/setup-node@v4 + - name: Setup Node per .nvmrc in GUI + uses: actions/setup-node@v5 with: - node-version: "20.x" + node-version-file: chia-blockchain-gui/.nvmrc - name: Prepare GUI cache id: gui-ref diff --git a/.github/workflows/build-windows-installer.yml b/.github/workflows/build-windows-installer.yml index f15707979a01..ebe9a8c13ca3 100644 --- a/.github/workflows/build-windows-installer.yml +++ b/.github/workflows/build-windows-installer.yml @@ -95,10 +95,10 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: Setup Node 20.x - uses: actions/setup-node@v4 + - name: Setup Node per .nvmrc in GUI + uses: actions/setup-node@v5 with: - node-version: "20.x" + node-version-file: chia-blockchain-gui/.nvmrc - name: Test for secrets access id: check_secrets From dbd67001f7f18026388dfa83473908849a4984c7 Mon Sep 17 00:00:00 2001 From: Almog De Paz Date: Tue, 16 Sep 2025 18:10:47 +0300 Subject: [PATCH 15/16] fix sp lookup at genesis (#20057) * fix genesis sp lookup * fix test * lint --- chia/_tests/core/full_node/test_full_node.py | 8 +++----- chia/full_node/full_node_store.py | 8 +++++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/chia/_tests/core/full_node/test_full_node.py b/chia/_tests/core/full_node/test_full_node.py index 9b70e3c23ee5..b1969788e56e 100644 --- a/chia/_tests/core/full_node/test_full_node.py +++ b/chia/_tests/core/full_node/test_full_node.py @@ -1882,7 +1882,9 @@ async def test_new_signage_point_caching( ) -> None: full_node_1, _full_node_2, server_1, server_2, _wallet_a, _wallet_receiver, bt = wallet_nodes blocks = await full_node_1.get_all_full_blocks() - + assert full_node_1.full_node.full_node_store.get_signage_point_by_index_and_cc_output( + bytes32.zeros, full_node_1.full_node.constants.GENESIS_CHALLENGE, uint8(0) + ) == SignagePoint(None, None, None, None) 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.add_block(blocks[-3]) @@ -1951,10 +1953,6 @@ async def test_new_signage_point_caching( is not None ) - assert full_node_1.full_node.full_node_store.get_signage_point_by_index_and_cc_output( - full_node_1.full_node.constants.GENESIS_CHALLENGE, bytes32.zeros, uint8(0) - ) == SignagePoint(None, None, None, None) - @pytest.mark.anyio async def test_slot_catch_up_genesis( diff --git a/chia/full_node/full_node_store.py b/chia/full_node/full_node_store.py index b3cc6dea483d..b21abc6cd36e 100644 --- a/chia/full_node/full_node_store.py +++ b/chia/full_node/full_node_store.py @@ -833,10 +833,12 @@ def get_signage_point_by_index_and_cc_output( self, cc_signage_point: bytes32, challenge: bytes32, index: uint8 ) -> Optional[SignagePoint]: assert len(self.finished_sub_slots) >= 1 - if cc_signage_point == self.constants.GENESIS_CHALLENGE: - return SignagePoint(None, None, None, None) for sub_slot, sps, _ in self.finished_sub_slots: - if sub_slot is not None and sub_slot.challenge_chain.get_hash() == challenge: + if sub_slot is not None: + cc_hash = sub_slot.challenge_chain.get_hash() + else: + cc_hash = self.constants.GENESIS_CHALLENGE + if cc_hash == challenge: if index == 0: # first SP in the sub slot return SignagePoint(None, None, None, None) From 73cb54789be134e0e373960f73e1a6ba70d3a90f Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Tue, 16 Sep 2025 19:15:37 +0200 Subject: [PATCH 16/16] [CHIA-3712] simplify add_transaction() (#20063) * simpliy add_transaction() and replace literals with the CONSENSUS_ERROR_BAN_SECONDS constant * fix tests and review comments * review comments --- chia/_tests/core/full_node/test_full_node.py | 14 ++- chia/_tests/core/mempool/test_mempool.py | 12 +- chia/full_node/full_node.py | 114 +++++++++---------- chia/server/server.py | 12 +- 4 files changed, 82 insertions(+), 70 deletions(-) diff --git a/chia/_tests/core/full_node/test_full_node.py b/chia/_tests/core/full_node/test_full_node.py index b1969788e56e..a6a0418d4eac 100644 --- a/chia/_tests/core/full_node/test_full_node.py +++ b/chia/_tests/core/full_node/test_full_node.py @@ -919,6 +919,10 @@ async def suppress_value_error(coro: Coroutine[Any, Any, None]) -> None: @pytest.mark.anyio +@pytest.mark.limit_consensus_modes( + allowed=[ConsensusMode.HARD_FORK_2_0, ConsensusMode.HARD_FORK_3_0], + reason="We can no longer (reliably) farm blocks from before the hard fork", +) async def test_new_transaction_and_mempool( wallet_nodes: tuple[ FullNodeSimulator, FullNodeSimulator, ChiaServer, ChiaServer, WalletTool, WalletTool, BlockTools @@ -946,8 +950,10 @@ async def test_new_transaction_and_mempool( # Makes a bunch of coins conditions_dict: dict[ConditionOpcode, list[ConditionWithArgs]] = {ConditionOpcode.CREATE_COIN: []} - # This should fit in one transaction - for _ in range(100): + # This should fit in one transaction. The test constants have a max block cost of 400,000,000 + # and the default max *transaction* cost is half that, so 200,000,000. CREATE_COIN has a cost of + # 1,800,000, we create 80 coins + for _ in range(80): receiver_puzzlehash = wallet_receiver.get_new_puzzlehash() puzzle_hashes.append(receiver_puzzlehash) output = ConditionWithArgs(ConditionOpcode.CREATE_COIN, [receiver_puzzlehash, int_to_bytes(10000000000)]) @@ -1046,8 +1052,8 @@ async def test_new_transaction_and_mempool( # these numbers reflect the capacity of the mempool. In these # tests MEMPOOL_BLOCK_BUFFER is 1. The other factors are COST_PER_BYTE # and MAX_BLOCK_COST_CLVM - assert included_tx == 23 - assert not_included_tx == 10 + assert included_tx == 20 + assert not_included_tx == 7 assert seen_bigger_transaction_has_high_fee # Mempool is full diff --git a/chia/_tests/core/mempool/test_mempool.py b/chia/_tests/core/mempool/test_mempool.py index 1527a5abd057..134b12a655fe 100644 --- a/chia/_tests/core/mempool/test_mempool.py +++ b/chia/_tests/core/mempool/test_mempool.py @@ -72,7 +72,7 @@ from chia.types.mempool_inclusion_status import MempoolInclusionStatus from chia.types.mempool_item import MempoolItem, UnspentLineageInfo from chia.util.casts import int_to_bytes -from chia.util.errors import Err +from chia.util.errors import Err, ValidationError from chia.util.hash import std_hash from chia.util.recursive_replace import recursive_replace from chia.wallet.conditions import AssertCoinAnnouncement, AssertPuzzleAnnouncement @@ -361,7 +361,10 @@ async def respond_transaction( self.full_node.full_node_store.pending_tx_request.pop(spend_name) if spend_name in self.full_node.full_node_store.peers_with_tx: self.full_node.full_node_store.peers_with_tx.pop(spend_name) - ret = await self.full_node.add_transaction(tx.transaction, spend_name, peer, test) + try: + ret = await self.full_node.add_transaction(tx.transaction, spend_name, peer, test) + except ValidationError as e: + ret = (MempoolInclusionStatus.FAILED, e.code) invariant_check_mempool(self.full_node.mempool_manager.mempool) return ret @@ -2865,8 +2868,9 @@ async def test_invalid_coin_spend_coin( coin_spend_0 = make_spend(coin_0, cs.puzzle_reveal, cs.solution) 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.add_transaction(new_bundle, new_bundle.name(), test=True) - assert res == (MempoolInclusionStatus.FAILED, Err.WRONG_PUZZLE_HASH) + with pytest.raises(ValidationError) as e: + await full_node_1.full_node.add_transaction(new_bundle, new_bundle.name(), test=True) + assert e.value.code == Err.WRONG_PUZZLE_HASH coins = make_test_coins() diff --git a/chia/full_node/full_node.py b/chia/full_node/full_node.py index ff589394622c..163ccf77a020 100644 --- a/chia/full_node/full_node.py +++ b/chia/full_node/full_node.py @@ -68,6 +68,7 @@ from chia.protocols.full_node_protocol import RequestBlocks, RespondBlock, RespondBlocks, RespondSignagePoint from chia.protocols.outbound_message import Message, NodeType, make_msg from chia.protocols.protocol_message_types import ProtocolMessageTypes +from chia.protocols.protocol_timing import CONSENSUS_ERROR_BAN_SECONDS from chia.protocols.shared_protocol import Capability from chia.protocols.wallet_protocol import CoinStateUpdate, RemovedMempoolItem from chia.rpc.rpc_server import StateChangedProtocol @@ -502,11 +503,16 @@ async def _handle_one_transaction(self, entry: TransactionQueueEntry) -> None: except asyncio.CancelledError: error_stack = traceback.format_exc() self.log.debug(f"Cancelling _handle_one_transaction, closing: {error_stack}") + except ValidationError as e: + self.log.exception("ValidationError in _handle_one_transaction, closing") + if peer is not None: + await peer.close(CONSENSUS_ERROR_BAN_SECONDS) + entry.done.set((MempoolInclusionStatus.FAILED, e.code)) except Exception: - error_stack = traceback.format_exc() - self.log.error(f"Error in _handle_one_transaction, closing: {error_stack}") + self.log.exception("Error in _handle_one_transaction, closing") if peer is not None: - await peer.close() + await peer.close(CONSENSUS_ERROR_BAN_SECONDS) + entry.done.set((MempoolInclusionStatus.FAILED, Err.UNKNOWN)) finally: self.add_transaction_semaphore.release() @@ -1092,13 +1098,13 @@ async def request_validate_wp( response = await weight_proof_peer.call_api(FullNodeAPI.request_proof_of_weight, request, timeout=wp_timeout) # Disconnect from this peer, because they have not behaved properly if response is None or not isinstance(response, full_node_protocol.RespondProofOfWeight): - await weight_proof_peer.close(600) + await weight_proof_peer.close(CONSENSUS_ERROR_BAN_SECONDS) raise RuntimeError(f"Weight proof did not arrive in time from peer: {weight_proof_peer.peer_info.host}") if response.wp.recent_chain_data[-1].reward_chain_block.height != peak_height: - await weight_proof_peer.close(600) + await weight_proof_peer.close(CONSENSUS_ERROR_BAN_SECONDS) raise RuntimeError(f"Weight proof had the wrong height: {weight_proof_peer.peer_info.host}") if response.wp.recent_chain_data[-1].reward_chain_block.weight != peak_weight: - await weight_proof_peer.close(600) + await weight_proof_peer.close(CONSENSUS_ERROR_BAN_SECONDS) raise RuntimeError(f"Weight proof had the wrong weight: {weight_proof_peer.peer_info.host}") if self.in_bad_peak_cache(response.wp): raise ValueError("Weight proof failed bad peak cache validation") @@ -1113,10 +1119,10 @@ async def request_validate_wp( try: validated, fork_point, summaries = await self.weight_proof_handler.validate_weight_proof(response.wp) except Exception as e: - await weight_proof_peer.close(600) + await weight_proof_peer.close(CONSENSUS_ERROR_BAN_SECONDS) raise ValueError(f"Weight proof validation threw an error {e}") if not validated: - await weight_proof_peer.close(600) + await weight_proof_peer.close(CONSENSUS_ERROR_BAN_SECONDS) raise ValueError("Weight proof validation failed") self.log.info(f"Re-checked peers: total of {len(peers_with_peak)} peers with peak {peak_height}") self.sync_store.set_sync_mode(True) @@ -1378,7 +1384,7 @@ async def ingest_blocks( vs, ) if err is not None: - await peer.close(600) + await peer.close(CONSENSUS_ERROR_BAN_SECONDS) raise ValueError(f"Failed to validate block batch {start_height} to {end_height}: {err}") if end_height - block_rate_height > 100: now = time.monotonic() @@ -2767,66 +2773,56 @@ async def add_transaction( return MempoolInclusionStatus.SUCCESS, None if self.mempool_manager.seen(spend_name): return MempoolInclusionStatus.FAILED, Err.ALREADY_INCLUDING_TRANSACTION - self.mempool_manager.add_and_maybe_pop_seen(spend_name) self.log.debug(f"Processing transaction: {spend_name}") # Ignore if syncing or if we have not yet received a block # the mempool must have a peak to validate transactions if self.sync_store.get_sync_mode() or self.mempool_manager.peak is None: - status = MempoolInclusionStatus.FAILED - error: Optional[Err] = Err.NO_TRANSACTIONS_WHILE_SYNCING - self.mempool_manager.remove_seen(spend_name) - else: + return MempoolInclusionStatus.FAILED, Err.NO_TRANSACTIONS_WHILE_SYNCING + + cost_result = await self.mempool_manager.pre_validate_spendbundle(transaction, spend_name, self._bls_cache) + + self.mempool_manager.add_and_maybe_pop_seen(spend_name) + + if self.config.get("log_mempool", False): # pragma: no cover try: - cost_result = await self.mempool_manager.pre_validate_spendbundle( - transaction, spend_name, self._bls_cache - ) - except ValidationError as e: - self.mempool_manager.remove_seen(spend_name) - return MempoolInclusionStatus.FAILED, e.code + mempool_dir = path_from_root(self.root_path, "mempool-log") / f"{self.blockchain.get_peak_height()}" + mempool_dir.mkdir(parents=True, exist_ok=True) + with open(mempool_dir / f"{spend_name}.bundle", "wb+") as f: + f.write(bytes(transaction)) except Exception: - self.mempool_manager.remove_seen(spend_name) - raise + self.log.exception(f"Failed to log mempool item: {spend_name}") - if self.config.get("log_mempool", False): # pragma: no cover - try: - mempool_dir = path_from_root(self.root_path, "mempool-log") / f"{self.blockchain.get_peak_height()}" - mempool_dir.mkdir(parents=True, exist_ok=True) - with open(mempool_dir / f"{spend_name}.bundle", "wb+") as f: - f.write(bytes(transaction)) - except Exception: - self.log.exception(f"Failed to log mempool item: {spend_name}") - - async with self.blockchain.priority_mutex.acquire(priority=BlockchainMutexPriority.low): - if self.mempool_manager.get_spendbundle(spend_name) is not None: - self.mempool_manager.remove_seen(spend_name) - return MempoolInclusionStatus.SUCCESS, None - if self.mempool_manager.peak is None: - return MempoolInclusionStatus.FAILED, Err.MEMPOOL_NOT_INITIALIZED - info = await self.mempool_manager.add_spend_bundle( - transaction, cost_result, spend_name, self.mempool_manager.peak.height - ) - status = info.status - error = info.error - if status == MempoolInclusionStatus.SUCCESS: - self.log.debug( - f"Added transaction to mempool: {spend_name} mempool size: " - f"{self.mempool_manager.mempool.total_mempool_cost()} normalized " - f"{self.mempool_manager.mempool.total_mempool_cost() / 5000000}" - ) + async with self.blockchain.priority_mutex.acquire(priority=BlockchainMutexPriority.low): + if self.mempool_manager.get_spendbundle(spend_name) is not None: + self.mempool_manager.remove_seen(spend_name) + return MempoolInclusionStatus.SUCCESS, None + if self.mempool_manager.peak is None: + return MempoolInclusionStatus.FAILED, Err.MEMPOOL_NOT_INITIALIZED + info = await self.mempool_manager.add_spend_bundle( + transaction, cost_result, spend_name, self.mempool_manager.peak.height + ) + status = info.status + error = info.error + if status == MempoolInclusionStatus.SUCCESS: + self.log.debug( + f"Added transaction to mempool: {spend_name} mempool size: " + f"{self.mempool_manager.mempool.total_mempool_cost()} normalized " + f"{self.mempool_manager.mempool.total_mempool_cost() / 5000000}" + ) - # Only broadcast successful transactions, not pending ones. Otherwise it's a DOS - # vector. - mempool_item = self.mempool_manager.get_mempool_item(spend_name) - assert mempool_item is not None - await self.broadcast_removed_tx(info.removals) - await self.broadcast_added_tx(mempool_item, current_peer=peer) + # Only broadcast successful transactions, not pending ones. Otherwise it's a DOS + # vector. + mempool_item = self.mempool_manager.get_mempool_item(spend_name) + assert mempool_item is not None + await self.broadcast_removed_tx(info.removals) + await self.broadcast_added_tx(mempool_item, current_peer=peer) - if self.simulator_transaction_callback is not None: # callback - await self.simulator_transaction_callback(spend_name) + if self.simulator_transaction_callback is not None: # callback + await self.simulator_transaction_callback(spend_name) - else: - self.mempool_manager.remove_seen(spend_name) - self.log.debug(f"Wasn't able to add transaction with id {spend_name}, status {status} error: {error}") + else: + self.mempool_manager.remove_seen(spend_name) + self.log.debug(f"Wasn't able to add transaction with id {spend_name}, status {status} error: {error}") return status, error async def broadcast_added_tx( diff --git a/chia/server/server.py b/chia/server/server.py index f6f97b50791c..b036a258f7ba 100644 --- a/chia/server/server.py +++ b/chia/server/server.py @@ -553,9 +553,15 @@ async def connection_closed( # in this case we still want to do the banning logic and remove the connection from the list # but the other cleanup should already have been done so we skip that - if is_localhost(connection.peer_info.host) and ban_time != 0: - self.log.warning(f"Trying to ban localhost for {ban_time}, but will not ban") - ban_time = 0 + if ban_time > 0: + if is_localhost(connection.peer_info.host): + self.log.warning(f"Trying to ban localhost for {ban_time}, but will not ban") + ban_time = 0 + elif self.is_trusted_peer(connection, self.config.get("trusted_peers", {})): + self.log.warning( + f"Trying to ban trusted peer {connection.peer_info.host} for {ban_time}, but will not ban" + ) + ban_time = 0 if ban_time > 0: ban_until: float = time.time() + ban_time self.log.warning(f"Banning {connection.peer_info.host} for {ban_time} seconds")