Skip to content

refactor: split the validator's R2 witness cap from the RPC one - #191

Merged
flyq merged 5 commits into
mainfrom
liquan/refactor/split-r2-witness-concurrency
Aug 27, 2026
Merged

refactor: split the validator's R2 witness cap from the RPC one#191
flyq merged 5 commits into
mainfrom
liquan/refactor/split-r2-witness-concurrency

Conversation

@flyq

@flyq flyq commented Aug 25, 2026

Copy link
Copy Markdown
Member

Summary

--witness-max-concurrent-requests sized 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.

  • New --r2-max-concurrent-requests (env STATELESS_VALIDATOR_R2_MAX_CONCURRENT_REQUESTS) caps R2 GETs.
  • --witness-max-concurrent-requests now sizes the RPC witness path only.
  • The --r2-connections ceiling check moves to the new flag, so the per-connection share is computed against the budget that actually feeds those connections.
  • The three docstrings that still named the old flag as the R2 cap — the --r2-connections help, R2WitnessClient::new, and the r2_connections gauge doc — now name the new one (review finding, both bots).
  • The configured cap is retained on 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 r2 the 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:

--witness-max-concurrent-requests no longer caps R2 GETs under --witness-source r2
(it now sizes only the RPC witness path): set --r2-max-concurrent-requests
(env STATELESS_VALIDATOR_R2_MAX_CONCURRENT_REQUESTS) instead

Same treatment a leftover S3 credential gets, and for the same reason: silently ignoring configuration hides what is actually in effect. Under rpc the 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, beside validate_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 drive build_r2_client itself, 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-core remains the workspace's one user.

Testing

cargo test --workspace: 477 passed, 0 failed (3 new). fmt / clippy --all-targets --all-features / cargo sort clean.

Verified on a release binary against mainnet:

Case Result
r2 mode + old spelling only refused, error names both flags
r2 mode + new spelling starts
rpc mode + old spelling rule inert
--r2-connections 8 with r2 cap 4 refused
--r2-connections 8 with r2 cap 64 starts

Behavioural 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

`--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>
@mega-maxwell

mega-maxwell Bot commented Aug 25, 2026

Copy link
Copy Markdown

Claude review status

Living comment — rewritten in place. The review workflow keeps this single comment up to date instead of posting a new one each round, so it always describes the latest reviewed commit and the earlier text is intentionally gone. No reply is needed here; reply to a finding in its own review thread, and answer an open question in a reply on this PR. The next review round reconciles your answer.

✅ Review clean

Last reviewed: 0472616e..38d5f239 · updated 2026-08-27T01:41:08+00:00

New this round: 0 finding(s), 0 question(s) · Resolved this round: 1 · Open questions: 0

@claude

claude Bot commented Aug 25, 2026

Copy link
Copy Markdown

Label check: this PR currently has no labels applied.

Based on the diff (adds a new --r2-max-concurrent-requests CLI flag/env var, splits R2 GET concurrency from the RPC witness path, updates docs and tests), this looks like a fit for enhancement (new configurable behavior), since none of the other available labels (bug, documentation, duplicate, good first issue, help wanted, invalid, question, wontfix, compat:seamless, agent) match as well.

Suggest adding enhancement.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread bin/stateless-validator/src/app.rs

@mega-maxwell mega-maxwell Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Review needs attention — 1 finding(s)

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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread bin/stateless-validator/src/app.rs
…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>
@flyq flyq added the enhancement New feature or request label Aug 26, 2026
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>
@flyq flyq changed the title refactor(validator): split the R2 witness cap from the RPC one refactor: split the validator's R2 witness cap from the RPC one Aug 26, 2026
@vincent-k2026

Copy link
Copy Markdown
Collaborator

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 build_r2_client keeps it gated by the same call site as every other R2 rule.

Should fix: the one behavioural change in this PR has no test that discriminates

What actually changes behaviour is bin/stateless-validator/src/app.rs:503 and :524args.witness_max_concurrent_requestsargs.r2_max_concurrent_requests. Revert just those two lines and r2_mode_refuses_the_pre_split_concurrency_spelling still passes: it asserts the guard fires, that each spelling lands on its own field, and that build_r2_client returns Ok. None of that observes which value reaches the semaphore.

R2WitnessClient only exposes origin() / connections() / target_label(), so the assertion can't be written today. Adding max_concurrent_requests() -> Option<usize> next to connections() (same kind of read-only transport property) would let the test assert that --witness-max-concurrent-requests 16 --r2-max-concurrent-requests 48 builds a client capped at 48, not 16 — a positive control that also pins the "both set" semantics.

Second: parse_r2() only builds the custom-domain target, so the S3 arm at app.rs:524 — the same rewiring, second occurrence — is untested. A variant with --r2-endpoint + credentials is cheap.

Non-blocking

  • The refusal message names only the CLI flags. Given the env-file deploy shape documented in AGENTS.md, an operator in r2 mode most likely set STATELESS_VALIDATOR_WITNESS_MAX_CONCURRENT_REQUESTS; naming STATELESS_VALIDATOR_R2_MAX_CONCURRENT_REQUESTS in the message saves a "that flag doesn't exist" round trip.
  • Setting both is accepted, and in r2 mode the RPC one is then silently ignored — the exact thing the guard exists to prevent. I think the trade-off is right (setting both proves the operator knows about the split), but it's a deliberate asymmetry and deserves a why line beside the guard, or the next reader files it as an oversight.
  • This breaks r2-mode startup for anyone who doesn't rewrite their config. enhancement undersells it — a compat: label plus a release note fits better.

…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>
@flyq
flyq merged commit f88a5c1 into main Aug 27, 2026
21 checks passed
@flyq
flyq deleted the liquan/refactor/split-r2-witness-concurrency branch August 27, 2026 09:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants