Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions rips/rustchain-core/api/rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,22 @@
import threading


RPC_ALLOWED_METHODS = frozenset({
"getStats",
"getBlock",
"getBlockByHash",
"getWallet",
"getBalance",
"getMiningStatus",
"getAntiquityScore",
"getProposals",
"getProposal",
"getNodeInfo",
"getPeers",
"getEntropyProfile",
})


# =============================================================================
# API Response
# =============================================================================
Expand Down Expand Up @@ -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}")
Expand Down
78 changes: 78 additions & 0 deletions tests/test_json_rpc_method_whitelist.py
Original file line number Diff line number Diff line change
@@ -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"