|
| 1 | +# rpcproxy |
| 2 | + |
| 3 | +High-performance JSON-RPC reverse proxy written in Rust. Designed as a drop-in replacement for [etherproxy](https://github.com/MetaProvide/etherproxy) with smart caching, priority-based failover, and active health checking. |
| 4 | + |
| 5 | +## Features |
| 6 | + |
| 7 | +- **Priority-based failover** — routes requests through ordered upstream RPCs, skipping unhealthy backends |
| 8 | +- **Smart caching** — method-aware TTLs (immutable data cached for 1 hour, chain-tip data for configurable TTL) |
| 9 | +- **Request coalescing** — deduplicates identical in-flight requests to reduce upstream load |
| 10 | +- **Active health checking** — periodic `eth_blockNumber` probes detect stale or down backends |
| 11 | +- **URL path token auth** — optional token as URL path (like GetBlock) so clients that can only provide a URL (e.g. Bee) can authenticate |
| 12 | +- **Batch JSON-RPC** — full support for batched requests |
| 13 | +- **Verbose mode** — toggle between detailed debug logs and quiet production logging |
| 14 | +- **Docker ready** — built-in `HEALTHCHECK` that validates at least one backend returns real block data |
| 15 | + |
| 16 | +## Quick Start |
| 17 | + |
| 18 | +### Docker (recommended) |
| 19 | + |
| 20 | +```bash |
| 21 | +docker pull ghcr.io/metaprovide/rpcproxy:latest |
| 22 | + |
| 23 | +docker run -p 9000:9000 ghcr.io/metaprovide/rpcproxy:latest \ |
| 24 | + --targets https://rpc.gnosis.gateway.fm,https://rpc.ankr.com/gnosis |
| 25 | +``` |
| 26 | + |
| 27 | +### From source |
| 28 | + |
| 29 | +```bash |
| 30 | +cargo build --release |
| 31 | +./target/release/rpcproxy --targets https://rpc.gnosis.gateway.fm --verbose |
| 32 | +``` |
| 33 | + |
| 34 | +## Configuration |
| 35 | + |
| 36 | +All options can be set via CLI flags or environment variables. |
| 37 | + |
| 38 | +| Flag | Env Var | Default | Description | |
| 39 | +|------|---------|---------|-------------| |
| 40 | +| `--port` | `RPCPROXY_PORT` | `9000` | Port to listen on | |
| 41 | +| `--targets` | `RPCPROXY_TARGETS` | `http://localhost:8545` | Comma-separated upstream RPC URLs (priority order) | |
| 42 | +| `--cache-ttl` | `RPCPROXY_CACHE_TTL` | `2000` | Default cache TTL in milliseconds | |
| 43 | +| `--health-interval` | `RPCPROXY_HEALTH_INTERVAL` | `10` | Health check interval in seconds | |
| 44 | +| `--request-timeout` | `RPCPROXY_REQUEST_TIMEOUT` | `10` | Upstream request timeout in seconds | |
| 45 | +| `--cache-max-size` | `RPCPROXY_CACHE_MAX_SIZE` | `10000` | Maximum number of cached entries | |
| 46 | +| `--token` | `RPCPROXY_TOKEN` | _(none)_ | Token for URL-path authentication (see below) | |
| 47 | +| `-v, --verbose` | `RPCPROXY_VERBOSE` | `false` | Enable detailed debug logging | |
| 48 | + |
| 49 | +### Example with Docker Compose |
| 50 | + |
| 51 | +```yaml |
| 52 | +services: |
| 53 | + rpcproxy: |
| 54 | + image: ghcr.io/metaprovide/rpcproxy:latest |
| 55 | + environment: |
| 56 | + RPCPROXY_TARGETS: https://rpc.gnosis.gateway.fm,https://rpc.ankr.com/gnosis |
| 57 | + RPCPROXY_CACHE_TTL: 2000 |
| 58 | + RPCPROXY_TOKEN: my-secret-token |
| 59 | + ports: |
| 60 | + - "9000:9000" |
| 61 | +``` |
| 62 | +
|
| 63 | +### Example with Bee (dockerbee) |
| 64 | +
|
| 65 | +Since Bee can only provide a URL for its RPC endpoint (no custom headers), the token |
| 66 | +becomes part of the URL path: |
| 67 | +
|
| 68 | +```yaml |
| 69 | +services: |
| 70 | + bee: |
| 71 | + environment: |
| 72 | + BEE_BLOCKCHAIN_RPC_ENDPOINT: http://rpcproxy:9000/my-secret-token |
| 73 | + |
| 74 | + rpcproxy: |
| 75 | + image: ghcr.io/metaprovide/rpcproxy:latest |
| 76 | + environment: |
| 77 | + RPCPROXY_TARGETS: https://rpc.gnosis.gateway.fm |
| 78 | + RPCPROXY_TOKEN: my-secret-token |
| 79 | +``` |
| 80 | +
|
| 81 | +## Endpoints |
| 82 | +
|
| 83 | +| Endpoint | Method | Auth | Description | |
| 84 | +|----------|--------|------|-------------| |
| 85 | +| `POST /` | POST | No | JSON-RPC proxy when no token is set | |
| 86 | +| `POST /<token>` | POST | Yes | JSON-RPC proxy when `--token` is set | |
| 87 | +| `/health` | GET | No | Returns `200 ok` if ≥1 backend has real block data, else `503` | |
| 88 | +| `/readiness` | GET | No | JSON response with backend details and overall status | |
| 89 | +| `/status` | GET | No | Detailed JSON: all backends, states, request counts, cache stats | |
| 90 | + |
| 91 | +### Authentication |
| 92 | + |
| 93 | +When `--token` is set, the token becomes a **URL path segment**. This design lets |
| 94 | +clients that can only provide a URL (like Bee nodes) authenticate without custom headers, |
| 95 | +similar to how [GetBlock](https://getblock.io) works: |
| 96 | + |
| 97 | +```text |
| 98 | +# No token set → open access |
| 99 | +curl -X POST http://localhost:9000 -d '{...}' |
| 100 | +
|
| 101 | +# Token set → must use path |
| 102 | +curl -X POST http://localhost:9000/my-secret-token -d '{...}' |
| 103 | +
|
| 104 | +# Without token path → 401 Unauthorized |
| 105 | +curl -X POST http://localhost:9000 -d '{...}' |
| 106 | +``` |
| 107 | + |
| 108 | +Health, readiness, and status endpoints are **not** protected. |
| 109 | + |
| 110 | +### Status response example |
| 111 | + |
| 112 | +```json |
| 113 | +{ |
| 114 | + "healthy_backends": 2, |
| 115 | + "total_backends": 3, |
| 116 | + "cache_entries": 42, |
| 117 | + "backends": [ |
| 118 | + { |
| 119 | + "url": "https://rpc.gnosis.gateway.fm", |
| 120 | + "priority": 0, |
| 121 | + "state": "Healthy", |
| 122 | + "latency_ms": 120.5, |
| 123 | + "latest_block": 44662374, |
| 124 | + "total_requests": 1500, |
| 125 | + "total_errors": 3, |
| 126 | + "uptime_secs": 86400 |
| 127 | + } |
| 128 | + ] |
| 129 | +} |
| 130 | +``` |
| 131 | + |
| 132 | +## Caching Strategy |
| 133 | + |
| 134 | +| Category | TTL | Examples | |
| 135 | +|----------|-----|---------| |
| 136 | +| **Immutable** | 1 hour | `eth_getTransactionReceipt`, `eth_getBlockByHash`, `eth_chainId`, `net_version` | |
| 137 | +| **Immutable (conditional)** | 1 hour | `eth_getBlockByNumber` with hex block, `eth_getLogs` with `blockHash` | |
| 138 | +| **Chain-tip** | `--cache-ttl` | `eth_blockNumber`, `eth_gasPrice`, `eth_getBalance` | |
| 139 | +| **Never cached** | — | `eth_sendRawTransaction`, `personal_sign`, `debug_*` | |
| 140 | + |
| 141 | +## Verbose Mode |
| 142 | + |
| 143 | +- **Off (default)**: logs startup info, backend state changes, errors, and warnings only |
| 144 | +- **On (`-v`)**: adds per-request debug logs — cache hits/misses, upstream selection, latencies, health check probes |
| 145 | + |
| 146 | +`RUST_LOG` env var always takes precedence if set. |
| 147 | + |
| 148 | +## Building the Docker Image |
| 149 | + |
| 150 | +```bash |
| 151 | +docker build -t rpcproxy:latest . |
| 152 | +``` |
| 153 | + |
| 154 | +The Dockerfile uses a multi-stage build with [cargo-chef](https://github.com/LukeMathWalker/cargo-chef) for dependency layer caching. |
| 155 | +Subsequent builds after dependency changes are fast because only your source code is recompiled. |
| 156 | + |
| 157 | +## License |
| 158 | + |
| 159 | +MIT |
0 commit comments