Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
7fe4a67
feat(rpc-client): make a deadline give-up say where the budget went
flyq Aug 9, 2026
160d31e
fix(debug-trace-server): R2-first witness fetches and a per-hop budge…
flyq Aug 10, 2026
5a09203
fix(rpc-client): derive the witness hop cap from the live budget, not…
flyq Aug 10, 2026
79bb33a
fix(rpc-client): hold the inter-round backoff to half the remaining d…
flyq Aug 10, 2026
7c77d52
test(debug-trace-server): pin the deadline-bounded permit wait
flyq Aug 10, 2026
3d321c1
refactor(rpc-client): type the give-up phase; single attempt-recordin…
flyq Aug 10, 2026
f9f3452
Merge branch 'liquan/feat/witness-attempt-forensics' into liquan/fix/…
flyq Aug 10, 2026
de13344
refactor(debug-trace-server): /simplify follow-ups on the R2-first delta
flyq Aug 10, 2026
7678b0f
fix(rpc-client): clamp the permit acquire to the caller deadline
flyq Aug 12, 2026
1c855e8
refactor(rpc-client): collapse give-up WARN arms; tighten comments an…
flyq Aug 12, 2026
e12683a
Merge branch 'liquan/feat/witness-attempt-forensics' (post-review) in…
flyq Aug 12, 2026
63c8fac
fix(debug-trace-server): narrow the R2 frontier band to uploader-lag …
flyq Aug 12, 2026
8092029
refactor(debug-trace-server): /simplify pass over the PR delta
flyq Aug 12, 2026
36b4bac
fix(rpc-client): close three budget leaks around the witness reserve
flyq Aug 12, 2026
4642efb
Merge remote-tracking branch 'origin/main' into liquan/feat/witness-a…
flyq Aug 12, 2026
350962c
Merge branch 'liquan/feat/witness-attempt-forensics' into liquan/fix/…
flyq Aug 12, 2026
29cb1b5
fix(rpc-client): quiet best-effort give-ups; future-proof RpcAttemptO…
flyq Aug 12, 2026
69e725e
Merge branch 'liquan/feat/witness-attempt-forensics' into liquan/fix/…
flyq Aug 12, 2026
6085bf3
fix(rpc-client): reserve the half-window only while rotation remains;…
flyq Aug 12, 2026
a385013
fix(debug-trace-server): above-tip R2 probes get the speculative budg…
flyq Aug 12, 2026
4541564
Merge remote-tracking branch 'origin/main' into liquan/fix/witness-ho…
flyq Aug 13, 2026
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
7 changes: 5 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,10 @@ Tag requests (`latest`/`finalized`/`safe`) bind number → hash in their single
Below the response cache, a bounded in-memory `BlockData` cache keyed by block hash (`--block-data-cache-max-size`, default 1GB, 0 disables) fronts the DB and RPC tiers; block-number lookups resolve number → hash before touching it, so canonicality is never cached and it needs no reorg invalidation.
The cache pins 4 shards (largest cacheable entry = `max_bytes / 4` on any host), counts non-retained inserts, and drops an entry when a trace fails for a data-attributable reason (`TraceError::Data` — bad witness) while request-attributable failures (invalid tracer configs) never evict.
In local cache mode with a `--witness-generator-endpoint` plus at least one fallback `--witness-endpoint`, request-serving witness fetches route by block age: blocks at least `--witness-local-window` blocks below the local tip skip the generator (which prunes beyond its `BACKUP` window) and fetch from the fallbacks; without the generator flag, witness endpoints are plain failover and routing is disabled.
With the `--r2-*` flag group (endpoint, bucket, access key id, secret), historical witness fetches try a direct SigV4-signed R2 GET (light decode, capped at half the remaining witness budget) before the RPC chain, falling back on any failure; frontier blocks keep the generator path, and `--r2-max-concurrent-requests` caps R2 GETs separately from the RPC witness semaphore.
With the `--r2-*` flag group (endpoint, bucket, access key id, secret), every request-serving witness fetch tries a direct SigV4-signed R2 GET (light decode, capped at half the remaining witness budget) before the RPC chain, falling back on any failure; `--r2-max-concurrent-requests` caps R2 GETs separately from the RPC witness semaphore.
The bucket is the same store the public gateway reads and can lead the generator at the frontier (uploader and generator RPC server publish from different files), so frontier hits are real; the frontier band is a small near-tip window (`R2_FRONTIER_WINDOW`, 32 blocks of uploader-lag grace on either side of the local tip — deliberately far narrower than the 4096-block routing window, so a stale catching-up tip cannot silence holes above it), hits there are labeled `witness_r2_frontier` (vs `witness_r2` past the band), the speculative frontier probe runs on an eighth of the remaining stage (vs half for blocks R2 must hold, so degraded R2 cannot burn half of every near-tip request's budget), and a `missing` classifies by band: in-band is the expected probe-ahead outcome (excluded from the alarm), below-band feeds `debug_trace_r2_witness_errors_total{kind="missing"}` (the bucket-integrity alarm, still covering recent-but-below-tip holes), and above-band — only reachable behind a stale catching-up tip — lands on its own `kind="missing_above_tip"` series, visible without flooding the alarm on every catch-up.
Any witness-chain RPC attempt under a deadline is capped at the tightest of three bounds — half the full witness stage (`RpcClientConfig::witness_per_attempt_timeout`, derived from `--witness-timeout`), the global `--rpc-per-attempt-timeout-ms` (an explicitly stricter operator setting is honored, never loosened), and — only while the round still has an untried provider to rotate to — half of what the call still has as the attempt starts (recomputed after any concurrency-permit wait, so neither an old-block-clamped stage, a post-R2 remainder, nor a long permit queue defeats the reserve).
The round's last hop, and every hop of a single-provider chain, takes the remainder whole under the ceiling instead: rotation stays protected without structurally condemning a slow-but-honest transfer, and the witness decode runs outside the attempt window (bounded by the deadline alone), so CPU-bound decode neither burns the reserve nor reads as a provider stall while a corrupt payload still rotates as the provider's error; deadline-less chain-sync fetches keep the general 20s cap so a slower-than-cap transfer still completes.
When a logical upstream call gives up on its deadline it logs one WARN naming the `phase` it died in (`before_attempt` / `permit_wait_clamped` / `attempt_clamped` / `before_backoff`) with `provider` / `round` / `permit_wait_ms` / `attempt_ms`, and the abandoned attempt is recorded as `outcome="deadline_clamped"` rather than dropped; best-effort internal probes (the throttled upstream tip seed) demote that give-up log to debug while the deadline metric still fires, so a probe whose failure is already degraded cannot page as a user-visible incident.
Permit wait is timed separately (`debug_trace_upstream_permit_wait_seconds{method}`) and the acquire is clamped to the deadline (phase `permit_wait_clamped`, cut-short wait still sampled), so queueing behind our own `--witness-max-concurrent-requests` stays distinguishable from endpoint slowness and a saturated queue cannot block a call past its budget unobserved.
The background chain-sync prefetch routes by freshness against the last observed remote head: frontier-fresh blocks give the generator a short exclusive grace (its "witness not found" means "not generated yet" — fallbacks are fed by the same pipeline and cannot be ahead) before falling back to the full endpoint chain, while deep catch-up blocks — and any block classified against a stale head observation (older than the grace, as during a long catch-up stretch when the tip is not re-polled) — use the full chain from the first attempt.
Expand All @@ -147,7 +150,7 @@ The background chain-sync prefetch routes by freshness against the last observed
| `bin/debug-trace-server/src/rpc_middleware.rs` | Concurrent execution of inbound JSON-RPC batch entries |
| `bin/debug-trace-server/src/data_provider.rs` | Block data fetching with single-flight coalescing |
| `bin/debug-trace-server/src/block_data_cache.rs` | Bounded in-memory `BlockData` cache keyed by block hash |
| `bin/debug-trace-server/src/r2_witness.rs` | Direct-from-R2 historical witness source (light decode, deadline-aware) |
| `bin/debug-trace-server/src/r2_witness.rs` | Direct-from-R2 witness source (light decode, deadline-aware) |
| `bin/debug-trace-server/src/server_db.rs` | Defines + implements the bin-local `BlockStore` trait (backed by `stateless-db`) |

## Test Organization
Expand Down
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ The workspace contains two binaries and five library crates:
| `stateless-db` | `crates/stateless-db` | redb-backed persistence: table definitions, read/write helpers, bounded `ContractCache` |
| `stateless-common` | `crates/stateless-common` | Shared utilities: RPC client, logging, metrics |
| `stateless-test-utils` | `crates/stateless-test-utils` | Test fixtures (blocks, witnesses, contracts) and env-var lock for integration tests |
| `stateless-r2` | `crates/stateless-r2` | Shared R2 (S3) witness primitives: SigV4 signer, object-key layout, endpoint parsing, signed PUT/GET with retry; consumed by mega-reth's witness uploaders (write), this repo's validator, and the trace server's historical witness source (read) |
| `stateless-r2` | `crates/stateless-r2` | Shared R2 (S3) witness primitives: SigV4 signer, object-key layout, endpoint parsing, signed PUT/GET with retry; consumed by mega-reth's witness uploaders (write), this repo's validator, and the trace server's witness source (read) |
| `stateless-validator` | `bin/stateless-validator` | Main binary: chain sync, parallel validation workers |
| `debug-trace-server` | `bin/debug-trace-server` | Standalone RPC server for debug/trace methods |

Expand Down Expand Up @@ -151,10 +151,14 @@ The head observation is trusted as a freshness anchor only while itself recent (
Deep catch-up blocks (far below the observed head, where the generator may have pruned the witness) keep full failover from the first attempt.
Without `--witness-generator-endpoint`, historical routing is disabled and the endpoints are plain failover.

**Direct-from-R2 historical witnesses:**
With `--r2-endpoint`, `--r2-bucket`, `--r2-access-key-id`, and `--r2-secret-access-key` (all four together), request-serving witness fetches for historical blocks try a SigV4-signed GET against the bucket before the RPC witness chain.
**Direct-from-R2 witnesses:**
With `--r2-endpoint`, `--r2-bucket`, `--r2-access-key-id`, and `--r2-secret-access-key` (all four together), every request-serving witness fetch tries a SigV4-signed GET against the bucket before the RPC witness chain.
Object storage tolerates far higher parallelism than a shared RPC gateway and the bucket holds full history, so bulk backfill traffic stops competing with everything else on the public endpoint; any R2 failure (missing object, throttle, transport, corrupt payload — counted in `debug_trace_r2_witness_errors_total{kind}`) falls back to the RPC chain on the remaining witness budget, and the R2 attempt is capped at half that budget so a hung endpoint can never starve the fallback.
Frontier blocks keep the generator path (the bucket receives objects only after the uploader PUTs them), and the route needs a local DB (`--data-dir`) to anchor block age.
Frontier probes usually miss — the uploader typically lags the generator — and cost one fast 404; the frontier band is a small near-tip window (32 blocks of uploader-lag grace on either side of the local tip — far narrower than the 4096-block routing window, and a stale, catching-up tip cannot silence holes above it), hits there are labeled `witness_r2_frontier` (vs `witness_r2` past the band) so their hit rate stays separable, and the speculative probe runs on an eighth of the remaining stage (vs half for blocks R2 must hold), so degraded R2 cannot burn half of every near-tip request's budget before the RPC chain runs.
A `missing` classifies by band: in-band is expected probe-ahead (excluded from `debug_trace_r2_witness_errors_total{kind="missing"}`), below-band feeds that bucket-integrity alarm (the object must exist there), and above-band — only reachable behind a stale catching-up tip — lands on its own `kind="missing_above_tip"` series so catch-up windows stay visible without flooding the alarm.
The bucket is the same store the public gateway serves witnesses from, so at the frontier it can lead the generator (whose RPC server publishes from a different file than the uploader reads); the route needs a local DB (`--data-dir`) to anchor block age.
Any single witness-chain RPC attempt under a deadline is additionally capped at the tightest of: half the witness stage budget, the global per-attempt timeout, and — only while the round still has an untried provider to rotate to — half of what the call still has as the attempt starts (recomputed after any permit wait).
The round's last hop, and every hop of a single-provider chain, takes the remainder whole under the ceiling instead, so a stalled endpoint (or a saturated concurrency permit — waits are deadline-bounded too) can never consume the stage while a rotation is still worth reserving for, and a slow-but-honest transfer is never structurally condemned; the witness decode runs outside the attempt window, bounded by the request deadline alone.
`--r2-max-concurrent-requests` caps in-flight GETs separately from `--witness-max-concurrent-requests` — the RPC cap sizes a shared gateway, R2 tolerates far more.

**Witness routing and sync knobs** (each also settable via its `DEBUG_TRACE_SERVER_*` env var):
Expand Down
Loading
Loading