Files
chia-blockchain/tools/validate_rpcs.py
T
Arvid NorbergandGitHub fe15e9ee6f [CHIA-4326] enable redundant expressions check in mypy (#21143)
* fix type annotation in configure.py and init.py

* enable mypy redundant-expr and remove redundant expressions
2026-07-29 09:37:24 -05:00

280 lines
8.7 KiB
Python
Executable File

#!/usr/bin/env python3
from __future__ import annotations
import asyncio
import logging
import time
from itertools import chain
from pathlib import Path
from typing import Any
import aiofiles
import click
from chia_rs.sized_bytes import bytes32
from chia.cmds.cmds_util import get_any_service_client
from chia.full_node.full_node_rpc_client import FullNodeRpcClient
from chia.util.default_root import resolve_root_path
from chia.util.path import path_from_root
from chia.util.task_referencer import create_referenced_task
DEFAULT_PIPELINE_DEPTH: int = 10
log = logging.getLogger("validate_rpcs")
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-8s %(message)s")
def get_height_to_hash_filename(root_path: Path, config: dict[str, Any]) -> Path:
"""
Utility function to get the path to the height-to-hash database file.
"""
db_path_replaced: Path = root_path / config["full_node"]["database_path"]
db_directory: Path = path_from_root(root_path, db_path_replaced).parent
selected_network: str = config["full_node"]["selected_network"]
suffix = "" if selected_network == "mainnet" else f"-{selected_network}"
return db_directory / f"height-to-hash{suffix}"
async def get_height_to_hash_bytes(root_path: Path, config: dict[str, Any]) -> bytes:
"""
Load the height-to-hash database file into a bytearray.
"""
height_to_hash_filename: Path = get_height_to_hash_filename(root_path, config)
async with aiofiles.open(height_to_hash_filename, "rb") as f:
return await f.read()
def get_block_hash_for_height(height: int, height_to_hash: bytes) -> bytes32:
"""
Get the block header hash from the height-to-hash database.
"""
idx = height * 32
assert idx + 32 <= len(height_to_hash)
return bytes32(height_to_hash[idx : idx + 32])
@click.command(help="Test RPC endpoints using chain", no_args_is_help=True)
@click.option(
"--root-path",
default=resolve_root_path(override=None),
help="Config file root",
type=click.Path(),
show_default=True,
)
@click.option(
"-p",
"--rpc-port",
help=(
"Set the port where the Full Node is hosting the RPC interface. See the rpc_port under full_node in config.yaml"
),
type=int,
default=None,
)
@click.option(
"-sc",
"--spends_with_conditions",
help="Test get_block_spends_with_conditions",
is_flag=True,
type=bool,
default=False,
)
@click.option(
"-sp",
"--block_spends",
help="Test get_block_spends",
is_flag=True,
type=bool,
default=False,
)
@click.option(
"-ar",
"--additions_and_removals",
help="Test get_additions_and_removals",
is_flag=True,
type=bool,
default=False,
)
@click.option(
"-s",
"--start-height",
help="Start height for the RPC calls",
type=int,
default=None,
)
@click.option(
"-e",
"--end-height",
help="End height for the RPC calls",
type=int,
default=None,
)
@click.option(
"-r",
"--pipeline-depth",
help="Set the number of concurrent RPC calls to make.",
type=int,
default=DEFAULT_PIPELINE_DEPTH,
)
def cli(
root_path: str,
rpc_port: int | None,
spends_with_conditions: bool,
block_spends: bool,
additions_and_removals: bool,
pipeline_depth: int,
start_height: int | None = None,
end_height: int | None = None,
) -> None:
root_path_path = Path(root_path)
requests_per_batch = 0
if spends_with_conditions:
requests_per_batch += 1
if block_spends:
requests_per_batch += 1
if additions_and_removals:
requests_per_batch += 1
if requests_per_batch == 0:
print("No RPC calls selected. Exiting.")
return
if start_height is None:
start_height = 0
asyncio.run(
cli_async(
root_path=root_path_path,
rpc_port=rpc_port,
spends_with_conditions=spends_with_conditions,
block_spends=block_spends,
additions_and_removals=additions_and_removals,
start_height=start_height,
end_height=end_height,
pipeline_depth=pipeline_depth,
)
)
async def node_spends_with_conditions(
node_client: FullNodeRpcClient,
block_hash: bytes32,
height: int,
) -> None:
try:
res = await node_client.get_block_spends_with_conditions(block_hash)
for c in res:
c.coin_spend.get_hash() # Ensure CoinSpend is valid
except Exception as e:
log.error(f"ERROR: [{height}] get_block_spends_with_conditions returned invalid result")
raise e
async def node_block_spends(
node_client: FullNodeRpcClient,
block_hash: bytes32,
height: int,
) -> None:
try:
res = await node_client.get_block_spends(block_hash)
for c in res:
c.get_hash() # Ensure CoinSpend is valid
except Exception as e:
log.error(f"ERROR: [{height}] get_block_spends returned invalid result")
raise e
async def node_additions_removals(
node_client: FullNodeRpcClient,
block_hash: bytes32,
height: int,
) -> None:
try:
add, rem = await node_client.get_additions_and_removals(block_hash)
for coin in chain(add, rem):
coin.get_hash()
except Exception as e:
log.error(f"ERROR: [{height}] get_additions_and_removals returned invalid result")
raise e
async def cli_async(
root_path: Path,
rpc_port: int | None,
spends_with_conditions: bool,
block_spends: bool,
additions_and_removals: bool,
pipeline_depth: int,
start_height: int,
end_height: int | None = None,
) -> None:
async with get_any_service_client(FullNodeRpcClient, root_path, rpc_port) as (
node_client,
config,
):
blockchain_state: dict[str, Any] = await node_client.get_blockchain_state()
if blockchain_state["peak"] is None:
# Peak height is required for the cache.
print("No blockchain found. Exiting.")
return
peak_height = blockchain_state["peak"].height
assert peak_height is not None, "Blockchain peak height is None"
if end_height is None:
end_height = peak_height
print("Connected to Full Node")
height_to_hash_bytes: bytes = await get_height_to_hash_bytes(root_path=root_path, config=config)
log.info("block header hashes loaded from height-to-hash file.")
# Set initial values for the loop
pipeline: set[asyncio.Task[None]] = set()
completed_requests: int = 0
# measure time for performance measurement per segment
cycle_start: float = time.monotonic()
# also measure time for the whole process
start_time: float = cycle_start
def add_tasks_for_height(height: int) -> None:
block_header_hash = get_block_hash_for_height(height, height_to_hash_bytes)
# Create tasks for each RPC call based on the flags
if spends_with_conditions:
pipeline.add(
create_referenced_task(node_spends_with_conditions(node_client, block_header_hash, height))
)
if block_spends:
pipeline.add(create_referenced_task(node_block_spends(node_client, block_header_hash, height)))
if additions_and_removals:
pipeline.add(create_referenced_task(node_additions_removals(node_client, block_header_hash, height)))
for i in range(start_height, end_height + 1):
add_tasks_for_height(height=i)
# Make Status Updates.
while len(pipeline) >= pipeline_depth:
done, pipeline = await asyncio.wait(pipeline, return_when=asyncio.FIRST_COMPLETED)
completed_requests += len(done)
now = time.monotonic()
if cycle_start + 5 < now:
time_taken = now - cycle_start
log.info(
f"Processed {completed_requests} RPCs in {time_taken:.2f}s, "
f"{time_taken / completed_requests:.4f}s per RPC "
f"({i - start_height} Blocks completed out of {end_height - start_height})"
)
completed_requests = 0
cycle_start = now
# Wait for any remaining tasks to complete
log.info(f"Waiting for {len(pipeline)} remaining tasks to complete...")
if pipeline:
await asyncio.gather(*pipeline)
print(f"Finished processing blocks from {start_height} to {end_height} (peak: {peak_height})")
print(
f"Time per block for the whole process: "
f"{(time.monotonic() - start_time) / (end_height - start_height):.4f} seconds"
)
if __name__ == "__main__":
cli()