diff --git a/rips/rustchain-core/api/rpc.py b/rips/rustchain-core/api/rpc.py index 08b7c8563..220e9aa49 100644 --- a/rips/rustchain-core/api/rpc.py +++ b/rips/rustchain-core/api/rpc.py @@ -21,6 +21,22 @@ import threading +RPC_ALLOWED_METHODS = frozenset({ + "getStats", + "getBlock", + "getBlockByHash", + "getWallet", + "getBalance", + "getMiningStatus", + "getAntiquityScore", + "getProposals", + "getProposal", + "getNodeInfo", + "getPeers", + "getEntropyProfile", +}) + + # ============================================================================= # API Response # ============================================================================= @@ -325,7 +341,13 @@ def _route_request(self, path: str, params: Dict[str, Any]) -> ApiResponse: # JSON-RPC endpoint if path == "/rpc": method = params.get("method", "") + if method not in RPC_ALLOWED_METHODS: + return ApiResponse(success=False, error=f"Method not allowed: {method}") + rpc_params = params.get("params", {}) + if not isinstance(rpc_params, dict): + return ApiResponse(success=False, error="RPC params must be an object") + return self.api.rpc.call(method, rpc_params) return ApiResponse(success=False, error=f"Unknown endpoint: {path}") diff --git a/tests/test_json_rpc_method_whitelist.py b/tests/test_json_rpc_method_whitelist.py new file mode 100644 index 000000000..1fcfb563b --- /dev/null +++ b/tests/test_json_rpc_method_whitelist.py @@ -0,0 +1,78 @@ +import importlib +import sys +from pathlib import Path + + +RUSTCHAIN_CORE = Path(__file__).resolve().parents[1] / "rips" / "rustchain-core" +sys.path.insert(0, str(RUSTCHAIN_CORE)) + +rpc = importlib.import_module("api.rpc") + + +class SpyNode(rpc.MockNode): + def __init__(self): + super().__init__() + self.created_proposals = 0 + self.submitted_proofs = 0 + self.votes = 0 + + def create_proposal(self, **kwargs): + self.created_proposals += 1 + return super().create_proposal(**kwargs) + + def submit_mining_proof(self, **kwargs): + self.submitted_proofs += 1 + return super().submit_mining_proof(**kwargs) + + def vote_proposal(self, **kwargs): + self.votes += 1 + return super().vote_proposal(**kwargs) + + +def make_handler(node): + handler = object.__new__(rpc.ApiRequestHandler) + handler.api = rpc.RustChainApi(node) + return handler + + +def test_json_rpc_blocks_state_changing_methods(): + node = SpyNode() + handler = make_handler(node) + + for method in ("createProposal", "submitProof", "vote"): + response = handler._route_request("/rpc", {"method": method, "params": {}}) + assert response.success is False + assert response.error == f"Method not allowed: {method}" + + assert node.created_proposals == 0 + assert node.submitted_proofs == 0 + assert node.votes == 0 + + +def test_json_rpc_allows_read_only_methods(): + node = SpyNode() + handler = make_handler(node) + + stats = handler._route_request("/rpc", {"method": "getStats", "params": {}}) + wallet = handler._route_request( + "/rpc", + {"method": "getWallet", "params": {"address": "RTC1Test"}}, + ) + + assert stats.success is True + assert stats.data["chain_id"] == 2718 + assert wallet.success is True + assert wallet.data["address"] == "RTC1Test" + + +def test_json_rpc_rejects_non_object_params(): + node = SpyNode() + handler = make_handler(node) + + response = handler._route_request( + "/rpc", + {"method": "getWallet", "params": ["not", "an", "object"]}, + ) + + assert response.success is False + assert response.error == "RPC params must be an object"