From eb44a9ea31df4e0ca37c07cb3151552268c504fc Mon Sep 17 00:00:00 2001 From: Amine Khaldi Date: Thu, 20 Aug 2026 14:28:13 +0100 Subject: [PATCH] Add request_transaction and respond_transaction to rate limits v3. --- chia/_tests/core/full_node/test_full_node.py | 10 +++++----- chia/_tests/core/full_node/test_performance.py | 2 +- chia/_tests/core/server/test_rate_limits_v3.py | 1 + chia/apis/full_node_stub.py | 6 ++++-- chia/full_node/full_node.py | 2 +- chia/full_node/full_node_api.py | 13 +++++++++++-- chia/server/rate_limits_v3.py | 2 ++ 7 files changed, 25 insertions(+), 11 deletions(-) diff --git a/chia/_tests/core/full_node/test_full_node.py b/chia/_tests/core/full_node/test_full_node.py index f666f18a9d..8f356395d8 100644 --- a/chia/_tests/core/full_node/test_full_node.py +++ b/chia/_tests/core/full_node/test_full_node.py @@ -1213,7 +1213,7 @@ async def test_new_transaction_and_mempool( await full_node_1.send_transaction(respond_transaction, fake_peer) request = fnp.RequestTransaction(spend_bundle.get_hash()) - req = await full_node_1.request_transaction(request) + req = await full_node_1.request_transaction(request, fake_peer) fee_rate_for_med = full_node_1.full_node.mempool_manager.mempool.get_min_fee_rate(5000000) fee_rate_for_large = full_node_1.full_node.mempool_manager.mempool.get_min_fee_rate(50000000) @@ -1329,7 +1329,7 @@ async def test_request_respond_transaction( tx_id = bytes32.random(seeded_random) request_transaction = fnp.RequestTransaction(tx_id) - msg = await full_node_1.request_transaction(request_transaction) + msg = await full_node_1.request_transaction(request_transaction, peer) assert msg is None receiver_puzzlehash = wallet_receiver.get_new_puzzlehash() @@ -1346,7 +1346,7 @@ async def test_request_respond_transaction( await time_out_assert(10, time_out_messages(incoming_queue, "new_transaction")) request_transaction = fnp.RequestTransaction(spend_bundle.get_hash()) - msg = await full_node_1.request_transaction(request_transaction) + msg = await full_node_1.request_transaction(request_transaction, peer) assert msg is not None assert msg.data == bytes(fnp.RespondTransaction(spend_bundle)) @@ -1368,7 +1368,7 @@ async def test_respond_transaction_fail( tx_id = bytes32.random(seeded_random) request_transaction = fnp.RequestTransaction(tx_id) - msg = await full_node_1.request_transaction(request_transaction) + msg = await full_node_1.request_transaction(request_transaction, peer) assert msg is None receiver_puzzlehash = wallet_receiver.get_new_puzzlehash() @@ -1617,7 +1617,7 @@ async def test_malformed_peer_version_on_connect( await full_node_1.full_node.on_connect(peer) # Unparseable version should be treated as old, so the counter is incremented - assert peer.expected_mempool_responses == 100 + assert peer.expected_mempool_responses == 200 finally: full_node_1.full_node.config["selected_network"] = original_network diff --git a/chia/_tests/core/full_node/test_performance.py b/chia/_tests/core/full_node/test_performance.py index bb4f38dc3c..aadf7882c6 100644 --- a/chia/_tests/core/full_node/test_performance.py +++ b/chia/_tests/core/full_node/test_performance.py @@ -113,7 +113,7 @@ class TestPerformance: await full_node_1.respond_transaction(respond_transaction, fake_peer) request_transaction = fnp.RequestTransaction(spend_bundle_id) - req = await full_node_1.request_transaction(request_transaction) + req = await full_node_1.request_transaction(request_transaction, fake_peer) if req is None: break diff --git a/chia/_tests/core/server/test_rate_limits_v3.py b/chia/_tests/core/server/test_rate_limits_v3.py index 97e9ac603f..bb716d50fd 100644 --- a/chia/_tests/core/server/test_rate_limits_v3.py +++ b/chia/_tests/core/server/test_rate_limits_v3.py @@ -271,6 +271,7 @@ async def test_unsolicited_unlimited_v3_messages( ProtocolMessageTypes.respond_proof_of_weight: network_protocol_data.respond_proof_of_weight, ProtocolMessageTypes.respond_puzzle_solution: network_protocol_data.respond_puzzle_solution, ProtocolMessageTypes.reject_puzzle_solution: network_protocol_data.reject_puzzle_solution, + ProtocolMessageTypes.respond_transaction: network_protocol_data.respond_transaction, } expected_unlimited = {msg_type for msg_type, settings in rate_limits_v3.items() if settings.window_size is None} current_unlimited = set(unsolicited_messages) diff --git a/chia/apis/full_node_stub.py b/chia/apis/full_node_stub.py index 53155a8614..f1c7fa8916 100644 --- a/chia/apis/full_node_stub.py +++ b/chia/apis/full_node_stub.py @@ -63,8 +63,10 @@ class FullNodeApiStub(ApiProtocol, Protocol): """Handle new transaction from peer.""" ... - @metadata.request(reply_types=[ProtocolMessageTypes.respond_transaction]) - async def request_transaction(self, request: full_node_protocol.RequestTransaction) -> Message | None: + @metadata.request(reply_types=[ProtocolMessageTypes.respond_transaction], peer_required=True) + async def request_transaction( + self, request: full_node_protocol.RequestTransaction, peer: WSChiaConnection + ) -> Message | None: """Handle transaction request.""" ... diff --git a/chia/full_node/full_node.py b/chia/full_node/full_node.py index dea0844a13..4baf81ff5f 100644 --- a/chia/full_node/full_node.py +++ b/chia/full_node/full_node.py @@ -1050,7 +1050,7 @@ class FullNode: except Exception: old_peer = True if old_peer: - connection.expected_mempool_responses = 100 + connection.expected_mempool_responses = 200 peak_full: FullBlock | None = await self.blockchain.get_full_peak() diff --git a/chia/full_node/full_node_api.py b/chia/full_node/full_node_api.py index d04dd4a2a4..97fe0e5a4c 100644 --- a/chia/full_node/full_node_api.py +++ b/chia/full_node/full_node_api.py @@ -343,8 +343,10 @@ class FullNodeAPI: return None return None - @metadata.request(reply_types=[ProtocolMessageTypes.respond_transaction]) - async def request_transaction(self, request: full_node_protocol.RequestTransaction) -> Message | None: + @metadata.request(reply_types=[ProtocolMessageTypes.respond_transaction], peer_required=True) + async def request_transaction( + self, request: full_node_protocol.RequestTransaction, peer: WSChiaConnection + ) -> Message | None: """Peer has requested a full transaction from us.""" # Ignore if syncing if self.full_node.sync_store.get_sync_mode(): @@ -353,6 +355,12 @@ class FullNodeAPI: if spend_bundle is None: return None + if not is_localhost(peer.peer_info.host) and not is_in_network( + peer.peer_info.host, self.full_node.server.exempt_peer_networks + ): + # Pace successful replies. Coupled with a rate limits v3 receive + # window of 3, 0.5s per fetch is ~360 replies per minute. + await asyncio.sleep(0.5) transaction = full_node_protocol.RespondTransaction(spend_bundle) msg = make_msg(ProtocolMessageTypes.respond_transaction, transaction) @@ -381,6 +389,7 @@ class FullNodeAPI: f"Received unsolicited transaction {spend_name} from peer " f"{peer.peer_node_id} / {peer.peer_info.host} version {peer.version}" ) + await peer.close(RATE_LIMITER_BAN_SECONDS) return None peers_with_tx = {} if spend_name in self.full_node.full_node_store.peers_with_tx: diff --git a/chia/server/rate_limits_v3.py b/chia/server/rate_limits_v3.py index eb1dae3f45..2c0857d6f9 100644 --- a/chia/server/rate_limits_v3.py +++ b/chia/server/rate_limits_v3.py @@ -68,6 +68,8 @@ rate_limits_v3: dict[ProtocolMessageTypes, RLSettingsV3] = { ProtocolMessageTypes.request_puzzle_solution: RLSettingsV3(window_size=2), ProtocolMessageTypes.respond_puzzle_solution: RLSettingsV3(window_size=None), ProtocolMessageTypes.reject_puzzle_solution: RLSettingsV3(window_size=None), + ProtocolMessageTypes.request_transaction: RLSettingsV3(window_size=3), + ProtocolMessageTypes.respond_transaction: RLSettingsV3(window_size=None), } # Maximum number of window sizes we allow to be set by the