refactor: split the validator's R2 witness cap from the RPC one - #191
Conversation
`--witness-max-concurrent-requests` sized two unrelated things: the RPC witness path and, under `--witness-source r2`, the R2 GETs. They are budgets against different services with different tolerances -- the RPC gateway is shared with the data endpoints and sizes what we ask of someone else's node, while R2 tolerates far higher parallelism -- so one number could not be right for both. The trace server has kept them apart as `--witness-max-concurrent-requests` and `--r2-max-concurrent-requests` since R2 landed there; this brings the validator to the same shape. `--r2-max-concurrent-requests` (env `STATELESS_VALIDATOR_R2_MAX_CONCURRENT_REQUESTS`) now caps R2 GETs, and `--witness-max-concurrent-requests` sizes the RPC witness path only. The `--r2-connections` ceiling check moves with it, so the per-connection share is computed against the budget that actually feeds those connections. The split cannot be silent. An operator running R2 mode today writes the cap as `--witness-max-concurrent-requests`, and carrying that spelling forward would leave R2 uncapped -- in a mode with no RPC fallback, so the fetcher would aim its whole in-flight window at the bucket. Under `--witness-source r2`, the old spelling without the new one is therefore refused at startup by name, the way a leftover S3 credential is; the error says which flag to set instead. Under `rpc` the rule stays inert, where the old spelling still means exactly what it says. The check lives in `check_r2_concurrency_migration` rather than inline in `run` so a test can drive it: the existing tests note that `validate_r2_flags` is unreachable from the parse layer, and this rule would have inherited that blind spot. Verified on a real binary against mainnet: the guard fires in r2 mode and stays inert in rpc mode; `--r2-connections` is rejected above the new cap and accepted below it; and a 2000-block equivalence run over the S3 endpoint with the new flag produces a chain identical, block hash for block hash, to the same range validated over the RPC witness path and to the pre-split run. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude review status
✅ Review clean Last reviewed: New this round: 0 finding(s), 0 question(s) · Resolved this round: 1 · Open questions: 0 |
|
Label check: this PR currently has no labels applied. Based on the diff (adds a new Suggest adding |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d820f197ad
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
0 blocking · 0 should-fix · 1 suggestion(s) · 0 open question(s)
Reviewed head d820f197.
Findings without inline anchors:
bin/stateless-validator/src/app.rs:172— [Minor] Stale docs still name --witness-max-concurrent-requests as the R2 cap An operator following --help under --witness-source r2 sets --witness-max-concurrent-requests believing it caps R2; check_r2_concurrency_migration then refuses startup and points at the right flag, so it fails loudly rather than silently, but the misdirection wastes a deploy cycle and makes the new rule feel arbitrary. The metrics.rs docstring also misleads whoever writes the R2 dashboard divisor. Suggested fix: In all three docstrings, replace --witness-max-concurrent-requests with --r2-max-concurrent-requests (bin/stateless-validator/src/app.rs:172, bin/stateless-validator/src/r2_witness.rs:139, bin/stateless-validator/src/metrics.rs:277). The trace-server r2_connections doc at bin/debug-trace-server/src/main.rs:509 already has the target phrasing.
…ient The pre-split-spelling refusal now sits beside validate_r2_flags, where every other R2-mode rule lives, gated by call site instead of an internal mode guard that was dead at its only production call site. Drops the test-only pub export, moves the tests in-file to drive build_r2_client itself, deletes the independence test that clap guarantees by construction, and restyles the error to the shared flag-named convention. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ded81c1e8c
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
…till name the old flag The --r2-connections help, the R2WitnessClient::new doc, and the r2_connections gauge doc all still pointed at --witness-max-concurrent-requests as the R2 cap; following the help under --witness-source r2 would walk an operator straight into the migration refusal. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dead since the single-home block-execution refactor (#171) removed the last use, OpAlloyReceiptBuilder; stateless-core remains the workspace's one user. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The split and the fail-closed migration are both right — refusing the pre-split spelling instead of silently dropping the cap is the correct call for a mode with no RPC fallback, and putting the rule at the top of Should fix: the one behavioural change in this PR has no test that discriminatesWhat actually changes behaviour is
Second: Non-blocking
|
…arms The migration guard fires on the flags alone, so its test passed even with the constructors still wired to the old field. Retain the configured cap on R2WitnessClient (the fetcher decomposes it into per-connection permits and cannot report it back), expose it, and assert that with both spellings set the client is capped by --r2-max-concurrent-requests, not the RPC cap - driven through build_r2_client on the custom-domain and S3 arms alike, with the uncapped default pinned too. Reverting either arm now fails by value. Also from the review round: the configured cap joins the two R2 startup log lines, the refusal names the env spelling (the deployments the guard exists for configure through env files), and the guard comment records why setting both caps stays legal (one env template can feed rpc- and r2-mode roles). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Summary
--witness-max-concurrent-requestssized two unrelated things on the validator: the RPC witness path and, under--witness-source r2, the R2 GETs. The trace server has kept those apart since R2 landed there; this brings the validator to the same shape.--r2-max-concurrent-requests(envSTATELESS_VALIDATOR_R2_MAX_CONCURRENT_REQUESTS) caps R2 GETs.--witness-max-concurrent-requestsnow sizes the RPC witness path only.--r2-connectionsceiling check moves to the new flag, so the per-connection share is computed against the budget that actually feeds those connections.--r2-connectionshelp,R2WitnessClient::new, and ther2_connectionsgauge doc — now name the new one (review finding, both bots).R2WitnessClient(max_concurrent_requests()) and logged in the two R2 startup lines, and a new test asserts which cap actually reaches the client on both target arms (review finding, human round: the guard test alone survived a revert of the wiring).Why one number could not serve both
They are budgets against different services with different tolerances. The RPC cap sizes what we ask of someone else's node and is shared with the data endpoints; R2 is a different service that tolerates far higher parallelism. And under
--witness-source r2the RPC witness path is not used at all, so the flag was sizing a path that does not run.The split is refused rather than made silently
An operator running R2 mode today writes the cap as
--witness-max-concurrent-requests. Carrying that spelling forward would leave R2 uncapped — in a mode with no RPC fallback, so the fetcher would aim its whole in-flight window at the bucket. Under--witness-source r2, the old spelling without the new one is therefore refused at startup by name:Same treatment a leftover S3 credential gets, and for the same reason: silently ignoring configuration hides what is actually in effect. Under
rpcthe rule stays inert — there the old spelling still means exactly what it says. The message names the env spelling because r2-mode deployments configure through env files, and the guard comment records why both spellings together stay legal: one env template can feed rpc-mode and r2-mode roles alike, each mode reading only its own cap.The refusal sits at the top of
build_r2_client, besidevalidate_r2_flags, so it is mode-gated by its call site like every other R2 rule and a future R2-flavored witness source cannot skip it; the unit tests drivebuild_r2_clientitself, so the wiring is covered, not just the predicate — including which cap reaches the client: with both spellings set it must be the R2 one, not the RPC one, pinned on the custom-domain and S3 arms alike (reverting either arm's constructor argument to the old field fails the test by value).Also in this PR
chore(debug-trace-server): drop the unused alloy-op-evm dependency— dead since the single-home block-execution refactor (#171) removed its last use (OpAlloyReceiptBuilder);stateless-coreremains the workspace's one user.Testing
cargo test --workspace: 477 passed, 0 failed (3 new).fmt/clippy --all-targets --all-features/cargo sortclean.Verified on a release binary against mainnet:
r2mode + old spelling onlyr2mode + new spellingrpcmode + old spelling--r2-connections 8with r2 cap 4--r2-connections 8with r2 cap 64Behavioural equivalence: a 2000-block run over the S3 endpoint with the new flag produces a chain identical, block hash for block hash, to the same range validated over the RPC witness path and to the pre-split run over the same endpoint (2001 blocks, 0 mismatches).
🤖 Generated with Claude Code