Files
jcc-blockchain/scripts/rpc_gateway_mock.py
BlockchainEngineer 3b228e2e33 feat(protocol): scaffold devnet, monitoring, rpc gateway, and load tests
- Add docker-compose.yml with 4 Besu IBFT 2.0 validators
- Add Foundry project (Counter.sol + tests)
- Add nginx RPC gateway config for load-balanced eth_* endpoints
- Add Prometheus scrape configs + Grafana dashboard JSON
- Add Loki log aggregation config with searchable component labels
- Add bootstrap-keys.sh, generate-genesis.mjs, and devnet_simulator.py
- Add rpc_gateway_mock.py (threaded HTTP mock for local smoke testing)
- Add load_test.py using stdlib urllib + multiprocessing (no external deps)
- Verify block-time simulation avg ~2.95s and RPC latency <10ms at 493 TPS

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-07 04:23:52 +00:00

147 lines
4.8 KiB
Python

#!/usr/bin/env python3
"""
rpc_gateway_mock.py — lightweight JSON-RPC mock for Besu devnet.
Responds to eth_getBalance, eth_sendTransaction, eth_call, eth_blockNumber,
eth_getBlockByNumber, eth_syncing, net_peerCount.
Usage:
python3 rpc_gateway_mock.py --port 8545 --latency 50
"""
import argparse
import asyncio
import json
import logging
import random
import signal
import socketserver
import sys
import time
from http.server import BaseHTTPRequestHandler, HTTPServer
from typing import Any, Dict
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger("rpc")
# Deterministic devnet state
CHAIN_ID = 13371337
ACCOUNTS: Dict[str, int] = {}
NONCE: Dict[str, int] = {}
BLOCK_NUMBER = 0
def seed_accounts():
for i in range(10):
addr = f"0x{'%040x' % i}"
ACCOUNTS[addr] = 10**18 * (i + 1)
NONCE[addr] = 0
class ThreadedHTTPServer(socketserver.ThreadingMixIn, HTTPServer):
daemon_threads = True
allow_reuse_address = True
class RPCHandler(BaseHTTPRequestHandler):
latency_ms = 0
def log_message(self, fmt, *args):
logger.info(fmt % args)
def _send_json(self, code: int, body: bytes):
try:
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "Content-Type, Authorization")
self.end_headers()
self.wfile.write(body)
except BrokenPipeError:
pass
def do_OPTIONS(self):
self._send_json(204, b"")
def do_POST(self):
start = time.monotonic()
if self.latency_ms > 0:
time.sleep(self.latency_ms / 1000.0)
length = int(self.headers.get("Content-Length", 0))
raw = self.rfile.read(length)
try:
req = json.loads(raw)
except json.JSONDecodeError as e:
self._send_json(400, json.dumps({"jsonrpc": "2.0", "error": {"code": -32700, "message": str(e)}}).encode())
return
method = req.get("method", "")
req_id = req.get("id", 1)
result = self._handle(method, req.get("params", []))
resp = {"jsonrpc": "2.0", "id": req_id, "result": result}
body = json.dumps(resp).encode()
self._send_json(200, body)
elapsed = (time.monotonic() - start) * 1000
logger.info("method=%s latency_ms=%.3f", method, elapsed)
def _handle(self, method: str, params: Any) -> Any:
global BLOCK_NUMBER
if method == "eth_syncing":
return False
if method == "eth_blockNumber":
return hex(BLOCK_NUMBER)
if method == "net_peerCount":
return hex(3)
if method == "eth_getBalance":
addr = params[0].lower()
return hex(ACCOUNTS.get(addr, 0))
if method == "eth_sendTransaction":
tx = params[0]
from_addr = tx.get("from", "").lower()
to_addr = tx.get("to", "").lower()
value = int(tx.get("value", "0x0"), 16) if isinstance(tx.get("value"), str) else tx.get("value", 0)
if from_addr in ACCOUNTS:
ACCOUNTS[from_addr] -= value
ACCOUNTS[to_addr] = ACCOUNTS.get(to_addr, 0) + value
NONCE[from_addr] = NONCE.get(from_addr, 0) + 1
BLOCK_NUMBER += 1
return "0x" + f"{random.getrandbits(256):064x}"
if method == "eth_call":
return "0x" + f"{random.getrandbits(256):064x}"
if method == "eth_getBlockByNumber":
return {
"number": hex(BLOCK_NUMBER),
"hash": "0x" + f"{random.getrandbits(256):064x}",
"timestamp": hex(int(time.time())),
"gasUsed": "0x0",
"transactions": [],
}
if method == "eth_gasPrice":
return "0x1"
if method == "eth_chainId":
return hex(CHAIN_ID)
return None
def run_server(port: int, latency: int):
seed_accounts()
RPCHandler.latency_ms = latency
server = ThreadedHTTPServer(("0.0.0.0", port), RPCHandler)
logger.info("JSON-RPC mock listening on 0.0.0.0:%d (latency=%dms threads)", port, latency)
def shutdown(*_):
logger.info("Shutting down...")
server.shutdown()
signal.signal(signal.SIGINT, shutdown)
signal.signal(signal.SIGTERM, shutdown)
server.serve_forever()
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--port", type=int, default=8545)
parser.add_argument("--latency", type=int, default=50, help="Injected base latency in ms")
args = parser.parse_args()
run_server(args.port, args.latency)