Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
e8f352e
feat: add `erpc dump` command (#765)
kasrakhosravi Apr 9, 2026
daff277
ci: add Logic Diagram Action for PR architecture diagrams (#823)
kasrakhosravi Apr 9, 2026
ee7cc66
fix: prevent ErrUpstreamsExhausted from being misclassified in consen…
kasrakhosravi Apr 10, 2026
d5f8537
fix: return HTTP 200 for JSON-RPC request-too-large errors (#830)
aramalipoor Apr 10, 2026
6c17f15
chore: replace logic-diagram with xray (#828)
kasrakhosravi Apr 10, 2026
f39402e
feat: add x402 nanopayment auth strategy (#831)
kasrakhosravi Apr 15, 2026
55b7f78
fix: prevent directive defaults from overwriting HTTP query params (#…
kasrakhosravi Apr 16, 2026
a7755aa
fix: normalize Alchemy/DRPC/Infura 'eth_getLogs too large' errors (#835)
ThallesP Apr 16, 2026
9662009
feat: add query methods shim and gRPC query server and execution flow…
aramalipoor Apr 18, 2026
88be8fc
ci: fork-safe xray with GitHub App token (#837)
kasrakhosravi Apr 18, 2026
c6142d9
feat: config-driven label drop for histograms (#841)
kasrakhosravi Apr 21, 2026
d0e9a1b
fix: bound continueAsyncRefresh goroutine lifetime (#838)
kasrakhosravi Apr 21, 2026
8490474
fix: stop retrying sendRawTransaction balance errors (#834)
kasrakhosravi Apr 21, 2026
affb416
fix(metrics): prevent double-registration panic with label filter (#842)
kasrakhosravi Apr 21, 2026
4a3dcc4
fix: stop discarding valid consensus results on context cancellation …
kasrakhosravi Apr 21, 2026
a795cac
feat: trace_filter and arbtrace_filter auto-splitting (#839)
rslowinski Apr 22, 2026
a9ba3f6
fix(failsafe): honor WithRetryableTowardNetwork(false) at network sco…
kasrakhosravi Apr 23, 2026
9da7e25
feat: per-network static responses for RPC methods (#844)
paymog Apr 23, 2026
20e672d
fix: normalize emptyish trace_filter/arbtrace_filter upstream respons…
rslowinski Apr 23, 2026
eac201e
fix: wrap permanent init failures with NewTaskFatal (#847)
kasrakhosravi Apr 23, 2026
d75aabe
chore: release 0.0.64
github-actions[bot] Apr 23, 2026
a75325d
fix(networks): rewrite response ID for all JSON-RPC architectures (#846)
kasrakhosravi Apr 23, 2026
f6cef4a
auth: merge PaymentRequired Accepts across x402 strategies (#854)
kasrakhosravi Apr 29, 2026
eae6de7
feat: dynamic quantile-based timeout policy (#811)
kasrakhosravi Apr 30, 2026
00b883c
fix: defer head-of-chain bound check to network smart path (#853)
kasrakhosravi Apr 30, 2026
5d70053
fix: preserve verbatim request id bytes through response (#851)
kasrakhosravi Apr 30, 2026
20402ad
docs: add DVN-ready config preset (#856)
kasrakhosravi Apr 30, 2026
4dcdec4
fix: sync Alchemy default chain map with live API (#862)
andreclaro May 4, 2026
e14ed7a
chore: bump manifesto schema version to latest (#868)
sfsf9797 May 6, 2026
5040c56
fix(defaults): exclude eth_getBlockReceipts from MarkEmptyAsErrorMeth…
kasrakhosravi May 6, 2026
02fb4e9
chore: bump manifesto schema version to latest (#874)
sfsf9797 May 7, 2026
0379e11
feat: migrate dRPC networks discovery to REST JSON API with cold-star…
andreclaro May 7, 2026
fb482a1
feat: add Blockdaemon third-party provider (#861)
andreclaro May 7, 2026
c7202b2
fix: fall back to built-in subdomain map on cold-start API failure (#…
andreclaro May 7, 2026
23c1307
fix: redis go routine leak during fail-open thunder + update sonic js…
aramalipoor May 8, 2026
292b086
docs: clarify blockHeadLag/finalizationLag are block-number deltas, n…
kasrakhosravi May 12, 2026
699dd7e
feat: cache connector parallel fan-out + transport-only retries (#876)
kasrakhosravi May 12, 2026
7f070bb
fix: thread-safe vendor remote fetcher (#881)
aramalipoor May 13, 2026
05a7dc1
fix: fail-open auth logic for postgresql connections (#884)
aramalipoor May 13, 2026
662a4be
test: selection-policy rewrite safety net (#885)
aramalipoor May 13, 2026
5a44831
fix: exclude hedge attempts from per-upstream score counters (#886)
aramalipoor May 13, 2026
10513c8
feat: in-house failsafe + per-attempt observability (#889)
aramalipoor May 15, 2026
173d294
chore: pin xray-pr/xray-pr action to commit SHA (#890)
andreclaro May 16, 2026
7dde245
docs: full revamp with systematic 4-pass coverage audit (#891)
aramalipoor May 16, 2026
fd97789
feat(directives): add skip-consensus to bypass consensus per-request …
kasrakhosravi May 19, 2026
8dba159
fix: accept empty results for state-reads + trace filters by default …
aramalipoor May 20, 2026
2f94c1b
fix(hedge): reject emptyish results as winners for non-accept methods…
kasrakhosravi May 20, 2026
10c96e7
fix(grpc-bds): preserve null wildcard at non-terminal topic positions…
kasrakhosravi May 23, 2026
ce6fdfc
fix(eth_sendRawTransaction): verify on-chain before returning 'all up…
kasrakhosravi May 26, 2026
70a1f17
feat: unified selection policy and scoring mechanism (#888)
aramalipoor May 28, 2026
beb319b
fix(clients/bds): bound worst-case for wedged H2 streams (#899)
aramalipoor May 28, 2026
de13477
chore: add Maestro config with public-repo security gate (#901)
kasrakhosravi May 29, 2026
3135a1b
fix(docs): escape p<q> so MDX doesn't parse it as a JSX tag (#903)
kasrakhosravi May 29, 2026
5627db1
feat: WebSocket support and transport-agnostic event indexer
jleeh Apr 16, 2026
94e412e
fix(sharedState): keep-latest pub/sub delivery + ctx-deadline-aware r…
jleeh Apr 18, 2026
cddeb01
fix(upstream): plug four upstream-instance leak paths
jleeh Apr 29, 2026
bf9f50b
fix(util): pooledGzipReadCloser closes its source on Close
jleeh Apr 30, 2026
51faede
fix(networks): clamp regressions in EvmHighestBlockNumber monotonic g…
jleeh May 8, 2026
5a46e14
fix(failover): close tracker-signal gap and add per-request fallback …
May 22, 2026
6fb6b51
fix(cache): preserve "latest"/"finalized" tags for state-read methods
May 28, 2026
9372a10
fix(eth_sendRawTransaction): treat replay-attack rejections as idempo…
jleeh Jun 1, 2026
454c54d
test: adapt failover/selection tests to rebased upstream API
jleeh Jun 1, 2026
3bbc290
fix(websocket): self-heal wedged upstream WS connections and surface …
snowkide Jun 12, 2026
faf6648
test(websocket): end-to-end regression for ungraceful upstream death
snowkide Jun 12, 2026
1886390
refactor(websocket): slim to core fixes per review; fix races
snowkide Jun 12, 2026
ccfbfda
fix(failsafe): release half-open permit on ignored outcome (breaker w…
snowkide Jun 24, 2026
a88e68a
Merge pull request #3 from linkpoolio/fix/breaker-halfopen-permit-leak
snowkide Jun 30, 2026
cbbb538
fix(initializer): keep retrying recoverable tasks when a sibling task…
shpookas Jul 20, 2026
95719d0
fix(initializer): bound each auto-retry round by TaskTimeout (hung ta…
shpookas Jul 20, 2026
42f6842
fix(initializer): stop the retry loop before taking tasksMu in Stop (…
shpookas Jul 20, 2026
1be9510
fix(ws): advance network latest tip before newHeads fan-out
jleeh Jul 22, 2026
0d8003e
docs: drop consumer-specific wording from WS tip comments
jleeh Jul 22, 2026
c1c3404
fix(initializer): reap hung tasks, fix State/Wait tip races
jleeh Jul 22, 2026
defe2e4
Merge pull request #5 from linkpoolio/fix/initializer-retry-wedge
snowkide Jul 22, 2026
845ccb8
Merge pull request #10 from linkpoolio/fix/ws-tip-advances-network-la…
snowkide Jul 22, 2026
8f4a725
fix(ws): serve cached newHeads header when HTTP latest lags tip
jleeh Jul 22, 2026
eb99874
fix(ws): route tip reads to upstreams that already have the block
jleeh Jul 22, 2026
961d3f9
fix(ws): drop parallel tip-source id; use poller partition for latest
jleeh Jul 22, 2026
d142444
fix(ws): pin tip re-fetch to EvmLeaderUpstream
jleeh Jul 22, 2026
0b81d3e
fix(ws): bypass poller debounce when tip gate needs a fresh head
jleeh Jul 23, 2026
8d21290
fix(ws): refuse stale latest when tip re-fetch misses TipHW
jleeh Jul 23, 2026
0387ea2
fix(ws): sync TipHW publish and refresh on HTTP false-negative
jleeh Jul 23, 2026
28cd7f9
fix(ws): serve cached newHeads header when tip re-fetch misses TipHW
jleeh Jul 23, 2026
53456da
fix(ws): stop TipHW inflation from fallback WS; escape on emptyish miss
jleeh Jul 23, 2026
9ba72db
fix(ws): drop cached newHeads header serve for HTTP latest
jleeh Jul 23, 2026
f78796d
fix(ws): always advance TipHW for fan-out heads, keep emptyish escape
jleeh Jul 23, 2026
08dd03b
Merge pull request #11 from linkpoolio/fix/ws-tip-http-floor-from-cac…
snowkide Jul 24, 2026
4af71ff
fix(ws): pin near-tip eth_getBlockByNumber to EvmLeaderUpstream
shpookas Jul 31, 2026
80aad9e
fix(ws): slim near-tip getBlock pin to inline Forward path
shpookas Jul 31, 2026
009958d
fix(ws): restore near-tip pin helper and laggingHits test
shpookas Jul 31, 2026
9e39885
Merge pull request #14 from linkpoolio/fix/pin-near-tip-getblock-to-e…
shpookas Jul 31, 2026
88c9bd5
fix(ws): skip gzip ResponseWriter wrap on WebSocket upgrades
Aug 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
5 changes: 5 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
# Version control
.git
.gitignore
.claude/
.cursor/
.github/
.vscode/

# Build artifacts
bin/
dist/
build/
temp/
*.exe
*.dll
*.so
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ permissions:
jobs:
units:
runs-on: "${{ github.repository_owner == 'erpc' && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-24.04' }}"
timeout-minutes: 20
timeout-minutes: 30
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
Expand Down
58 changes: 58 additions & 0 deletions .github/workflows/xray.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: xray

on:
pull_request:
types: [opened, synchronize, ready_for_review]
issue_comment:
types: [created]

permissions:
contents: read
pull-requests: write

jobs:
xray-on-pr:
if: github.event_name == 'pull_request' && github.event.pull_request.draft == false && github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4
with:
fetch-depth: 0
- uses: xray-pr/xray-pr@489e56199b92f696dbc3757964dffd2503ec68e2 # v0.2.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
openrouter_api_key: ${{ secrets.OPENROUTER_API_KEY }}
languages: go

xray-on-command:
if: |
github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
contains(fromJSON('["OWNER", "MEMBER"]'), github.event.comment.author_association) &&
contains(github.event.comment.body, '/xray')
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
- name: Get PR head SHA
id: pr-info
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
SHA=$(gh api repos/${{ github.repository }}/pulls/${{ github.event.issue.number }} --jq '.head.sha')
echo "sha=$SHA" >> $GITHUB_OUTPUT
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4
with:
ref: ${{ steps.pr-info.outputs.sha }}
fetch-depth: 0
- uses: xray-pr/xray-pr@489e56199b92f696dbc3757964dffd2503ec68e2 # v0.2.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
openrouter_api_key: ${{ secrets.OPENROUTER_API_KEY }}
languages: go
23 changes: 23 additions & 0 deletions .maestrorc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"$schema": "https://maestro.goldsky.com/maestrorc/schema",
"agent": {
"model": "opus",
"effort": "high"
},
"planning": {
"enabled": true
},
"security": {
"trusted_author_associations": ["OWNER", "MEMBER", "COLLABORATOR"]
},
"sandbox": {
"setup_commands": {
"post_clone": [
"GO_VERSION=$(grep -m1 '^toolchain go' go.mod | sed 's/.*go//'); GO_VERSION=${GO_VERSION:-$(grep -m1 '^go [0-9]' go.mod | awk '{print $2}')}; echo \"Installing Go ${GO_VERSION} from go.mod\" && curl -fsSL \"https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz\" | tar -C /usr/local -xz && ln -sf /usr/local/go/bin/go /usr/local/bin/go && ln -sf /usr/local/go/bin/gofmt /usr/local/bin/gofmt",
"PNPM_VERSION=$(jq -r .packageManager package.json | cut -d@ -f2); echo \"Activating pnpm ${PNPM_VERSION} from package.json packageManager\" && corepack enable && corepack prepare \"pnpm@${PNPM_VERSION}\" --activate",
"go mod download",
"pnpm install --frozen-lockfile --ignore-scripts"
]
}
}
}
49 changes: 49 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# AGENTS.md — eRPC

Pointer file for cross-tool AI coding agents (Maestro, Codex, Cursor,
Claude Code). The canonical project rules live in
[`.cursor/rules/erpc.md`](.cursor/rules/erpc.md) — read that file first;
it applies to all agents, not just Cursor.

This file only repeats the bare minimum needed to bootstrap.

## Project

- **eRPC** — fault-tolerant EVM RPC proxy with re-org-aware permanent caching.
- Go server (`cmd/erpc`) + TypeScript packages under `typescript/` (`@erpc-cloud/config`, `@erpc-cloud/cli`).
- Module: `github.com/erpc/erpc`. Docs: <https://docs.erpc.cloud/>.

## Bootstrap commands

```bash
make setup # go mod tidy
pnpm install --ignore-scripts # JS workspace (skip postinstall, matches CI)

make fmt # format Go (also Biome for TS via pnpm)
make build # build the eRPC server
make test-fast # parallel-sharded unit tests (daily-driver)
make test # full suite with -race (slow, run before merge / CI runs this)
make test-race # race detector on a slimmer scope
```

Single test: `LOG_LEVEL=trace go test -run <pattern> ./...`

## PR expectations

- One logical change per PR.
- `make fmt` clean.
- `make test-fast` green locally; `make test` (race) green in CI.
- Brief test plan in the description.

## Hard rules from `.cursor/rules/erpc.md`

These bite hard if missed; read the cursor rules for the full version:

- **Test logger init:** every test file that logs must include
`func init() { util.ConfigureTestLogger() }`.
- **Gock setup order:** set up *all* gock mocks **before** initializing any
network components. Always `util.ResetGock()` + `defer util.ResetGock()`.
- **Logging:** zerolog only (`log.Logger` from `github.com/rs/zerolog/log`).
- **Errors:** wrap with context (`fmt.Errorf("...: %w", err)`).
- **Don't** change the public `erpc.yaml` schema without version markers + docs.
- **Don't** disable failing tests — fix the root cause or surface it in the PR.
8 changes: 7 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ help:
@echo "Usage: make [command]"
@echo
@echo "Commands:"
@echo " build Build the eRPC server"
@echo " build Build the eRPC server + simulator"
@echo " fmt Format source code"
@echo " test Run unit tests"
@echo
@echo " run-k6 Run k6 tests"
@echo " run-pprof Run the eRPC server with pprof"
@echo " run-fake-rpcs Run fake RPCs"
@echo " run-simulator Run the eRPC traffic simulator (http://127.0.0.1:8080)"
@echo " up Up docker services"
@echo " down Down docker services"
@echo " fmt Format source code"
Expand Down Expand Up @@ -45,6 +46,11 @@ run-k6-evm-historical-randomized:
build:
@CGO_ENABLED=0 go build -ldflags="-w -s" -o ./bin/erpc-server ./cmd/erpc/main.go
@CGO_ENABLED=0 go build -ldflags="-w -s" -tags pprof -o ./bin/erpc-server-pprof ./cmd/erpc/*.go
@CGO_ENABLED=0 go build -ldflags="-w -s" -o ./bin/erpc-simulator ./cmd/erpc-simulator

.PHONY: run-simulator
run-simulator:
@go run ./cmd/erpc-simulator

.PHONY: test
test:
Expand Down
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,36 @@ This setup is ideal for development and testing purposes. For production environ

---

### CLI Commands

eRPC provides several CLI commands beyond the default server start:

#### `erpc validate <config-file>`

Validate a configuration file (TS, JS, or YAML) and report any errors, warnings, or notices. Useful in CI pipelines to catch misconfigurations before deployment.

```bash
erpc validate erpc.yaml
erpc validate erpc.ts
```

#### `erpc dump <config-file>`

Parse a configuration file and output the fully resolved configuration with all eRPC defaults applied. Supports YAML and JSON output. This is useful for inspecting what your final config looks like after eRPC fills in all default values (retry policies, timeouts, selection policies, etc.).

```bash
# Output as YAML (default)
erpc dump erpc.yaml

# Output as JSON
erpc dump --format json erpc.ts

# Compare two configs (e.g. before/after a migration)
diff <(erpc dump old-config.yaml) <(erpc dump new-config.yaml)
```

---

### Local Development

1. **Clone this repository:**
Expand Down
95 changes: 91 additions & 4 deletions architecture/evm/block_ref.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,10 @@ func ExtractBlockReferenceFromRequest(ctx context.Context, r *common.NormalizedR
// In case of "*" since it means any block, we can still augment it from response ref, because during cache.Get()
// we'll be using reverse index (i.e. ignoring ref), but after reorg invalidation is added a specific block ref is useful.
//
// TODO An ideal version stores the data for all eth_getBlockByNumber(latest) and eth_getBlockByNumber(blockNumber),
// and eth_getBlockByNumber(blockHash) where blockNumber/blockHash are the actual values returned in the response.
// So that if user gets the latest block, then cache is populated for when they provide that specific block as well.
// When implementing that feature remember that CacheHash() must be calculated separately for each number/hash combo.
// For moving-tag requests ("latest", "finalized", "safe"), the
// cache layer calls ResolveCacheBlockRef instead, which resolves
// the tag to a concrete block number so each tip advance gets
// its own cache key.
blockRef = br
}
if bn > 0 {
Expand Down Expand Up @@ -114,6 +114,93 @@ func ExtractBlockReferenceFromRequest(ctx context.Context, r *common.NormalizedR
return blockRef, blockNumber, nil
}

// ResolveCacheBlockRef returns the block reference the cache layer should use
// when keying an eth_getBlockByNumber("latest") response (and other moving
// tags). Regular ExtractBlockReferenceFromRequest preserves the literal tag
// string ("latest") as blockRef so the cache hits on repeat tag queries —
// but that makes every request within the TTL window return the same pinned
// response regardless of chain progression (see the bug fixed alongside this
// helper: stale "latest" responses served from cache until TTL expiry, with
// enforceHighestBlock explicitly skipping cached responses).
//
// This helper substitutes the tag with a concrete block number so each tip
// advance is a distinct cache key: on WRITE we use the response's own block
// number (definitive answer for what the cached payload represents); on READ
// we consult the network's tip tracker (EvmHighestLatestBlockNumber, which
// aggregates max over upstream pollers and the cross-pod shared counter) to
// decide which block we'd be asking for *right now*. Within a single tip
// the key is stable and concurrent "latest" queries coalesce onto one cached
// entry; across tip advances the key changes and the next request forwards
// upstream.
//
// The function does NOT mutate the request's EvmBlockRef — the original
// "latest" tag is preserved on the request so downstream finality computation
// and other tag-aware logic keeps working.
//
// Fallback: if the tag can't be resolved to a concrete block number (no
// response, no network attached to the request, or the tracker hasn't seen
// a block yet), the original tag is returned and the cache key stays
// tag-literal — same as prior behaviour. That path should be rare in
// production since every normal HTTP request has a Network and an upstream
// response by the SET stage.
func ResolveCacheBlockRef(ctx context.Context, req *common.NormalizedRequest, resp *common.NormalizedResponse) (string, int64, error) {
blockRef, blockNumber, err := ExtractBlockReferenceFromRequest(ctx, req)
if err != nil {
return blockRef, blockNumber, err
}

// Only rewrite moving tip-bound tags. Numeric refs, block-hash refs, "*",
// and slower-moving tags like "earliest" are already correct.
if blockRef != "latest" && blockRef != "finalized" && blockRef != "safe" {
return blockRef, blockNumber, nil
}

// WRITE path: prefer the response's own block number, which is the
// definitive answer for what payload we're about to cache.
if resp != nil {
if _, respBN, rerr := ExtractBlockReferenceFromResponse(ctx, resp); rerr == nil && respBN > 0 {
hex, herr := common.NormalizeHex(respBN)
if herr == nil {
return hex, respBN, nil
}
}
}

// READ path (and WRITE fallback): consult the network's aggregated view
// of the tag's current value. Guarded against panics because this helper
// is purely an optimization — if the network state isn't reachable for
// any reason (partially-constructed Network in a test, nil upstream
// registry, transient initialization race), we fall back to the tag-
// literal blockRef and retain the previous behaviour rather than
// aborting a live cache operation.
net := req.Network()
if net == nil {
return blockRef, blockNumber, nil
}
var num int64
func() {
defer func() {
if r := recover(); r != nil {
num = 0
}
}()
switch blockRef {
case "latest":
num = net.EvmHighestLatestBlockNumber(ctx)
case "finalized", "safe":
num = net.EvmHighestFinalizedBlockNumber(ctx)
}
}()
if num > 0 {
hex, herr := common.NormalizeHex(num)
if herr == nil {
return hex, num, nil
}
}

return blockRef, blockNumber, nil
}

func ExtractBlockReferenceFromResponse(ctx context.Context, r *common.NormalizedResponse) (string, int64, error) {
ctx, span := common.StartDetailSpan(ctx, "Evm.ExtractBlockReferenceFromResponse")
defer span.End()
Expand Down
Loading
Loading