- Go 94.1%
- Solidity 4.9%
- Dockerfile 0.5%
- Makefile 0.5%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
CI / Go (push) Successful in 4m22s
Otterscan showed "It does not seem to be an ETH node" at random: its startup probe fails when eth_blockNumber does, and a head read was returning -32603 "no healthy endpoint" roughly 1% of the time. Selection admitted an endpoint for a head read only while its head observation was under 3 probe intervals old, and a round refreshed no observation until every endpoint had been probed AND every due retention verification had run, one after another. Four endpoints re-verifying together every ten minutes, or one upstream sitting on the probe timeout, pushed the whole round past nine seconds, so every endpoint aged out at once and nothing was left to serve. A head read is answered from the endpoint's own head, so how recently the prober polled it is not evidence about its ability to answer. When staleness alone empties the candidate list, selection now retries against endpoints that were in step when last seen, up to one full round's budget, and reports the decision as stale_head; beyond that the prober is broken and the read still fails. Rounds also apply their outcomes concurrently and stamp each head with the time the endpoint answered rather than the time the round started. |
||
| .forgejo/workflows | ||
| cmd/icu-rpc-switch | ||
| config | ||
| contracts | ||
| internal | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| docker-compose.yml | ||
| Dockerfile | ||
| go.mod | ||
| go.sum | ||
| LICENSE | ||
| Makefile | ||
| README.md | ||
icu-rpc-switch
One read-only JSON-RPC URL in front of a pruned node and one or more archive
endpoints. Every call is routed by the oldest block it needs: recent reads race
the local node against the archives, older reads go only to endpoints whose
verified retention covers them, and a pruned node's null is never returned
until an archive has confirmed it.
Interfaces
POST /rpc/{chainId}— JSON-RPC 2.0, single or batch;POST /rpcaliasesdefaultChainId.GET /beacon/{chainId}/eth/...— read-only proxy to the chain's beacon API;GET /beacon/eth/...aliases the default chain.GET /healthz,GET /readyz,GET /metrics(Prometheus).
With requireOrigin set, /rpc and /beacon answer only requests that a
browser issued from one of allowedOrigins (Origin, Sec-Fetch-Site: same-origin or Referer); bare clients get 403.
pagePolicies pins requests by the referring site path: with
{"pathPrefix": "/address/", "tier": "archive"} every call issued from an
address page is tried on verified archives first, falling back to the other
endpoints once every archive has failed.
Transaction submission, signing, filters, subscriptions and the admin_,
personal_, engine_, miner_, debug_, txpool_, clique_ and les_
namespaces are refused with -32601. Unknown methods are forwarded to verified
archives only.
Configuration
One strict JSON file — copy config/rpc-switch.example.json to
config/rpc-switch.json and fill in your endpoints (the real file stays out
of git); unknown fields, duplicate
names or chain ids, invalid URLs and out-of-range values are rejected. A
positive blockLimit is an endpoint's guaranteed retained depth; null marks
a claimed archive, which is verified at archiveProbeBlock before use.
The containing directory is watched: a valid edit (in place or by atomic
rename) replaces the active configuration for new requests after a 250 ms
debounce; an invalid one is logged and counted while the previous
configuration stays active. A broken file at startup exits non-zero without
opening the listener. routing.mode (parallel, hedged, best) and every
other routing value are hot-reloadable.
Running
make check # fmt, vet, test, race, build, example config
./icu-rpc-switch check-config config/rpc-switch.json
./icu-rpc-switch run -config config/rpc-switch.json -listen :8080
docker-compose.yml runs the switch behind Traefik (copy .env.example to
.env for the host name and resolver); the container joins the
reverse-proxy network and the node's internal network. An explorer such as
Otterscan is deployed separately and pointed at https://<host>/rpc and
https://<host>/beacon.
contracts/ holds PulseXNativeUsdOracle, an AggregatorV3-shaped native/USD
price feed computed from PulseX stable pools, for explorers that need one.
Verification
curl -s https://<host>/rpc -H 'Content-Type: application/json' -H 'Origin: https://<host>' \
-d '{"jsonrpc":"2.0","id":1,"method":"ots_getBlockDetails","params":[1000000]}'
curl -s https://<host>/beacon/eth/v1/beacon/headers/finalized -H 'Referer: https://<host>/'
retention_routing_total{decision="archive_only"} and
endpoint_wins_total{endpoint="..."} in /metrics show where each class of
request was served.
License
MIT — see LICENSE.