Skip to content

fix(retryProvider): Don't mask quorum failures with a ReferenceError - #1483

Merged
pxrl merged 1 commit into
masterfrom
droplet/C03GHT4RV42-1784127850-691789
Jul 15, 2026
Merged

fix(retryProvider): Don't mask quorum failures with a ReferenceError#1483
pxrl merged 1 commit into
masterfrom
droplet/C03GHT4RV42-1784127850-691789

Conversation

@droplet-rl

Copy link
Copy Markdown
Contributor

Motivation

When RetryProvider.send fails to reach quorum and no fallback providers remain, the early-exit throwQuorumError() call crashes with:

ReferenceError: Cannot access 'quorumResult' before initialization
    at getMismatchedProviders (retryProvider.js:77)
    at throwQuorumError (retryProvider.js:95)
    at RetryProvider.send (retryProvider.js:104)

getMismatchedProviders closes over quorumResult, a const that is only declared ~45 lines later (after fallback results are counted). The early-exit path invokes it inside the temporal dead zone, so instead of the intended "Not enough providers agreed to meet quorum" error the process dies with a ReferenceError — and since the crash happens before logQuorumMismatchOrFailureDetails, the diagnostic warn (which providers mismatched, on which method) is lost too.

The pattern was introduced in #728. In production it has been crashing the Across dataworker/proposer and other bots whenever a provider failure exhausts the fallback list and the surviving providers disagree (most recently zion-across-proposer, 5 consecutive runs on 2026-07-15).

Changes

  • getMismatchedProviders now takes the quorum result as an explicit parameter instead of closing over the not-yet-declared quorumResult.
  • throwQuorumError takes an optional { quorumResult, fallbackValues }: the post-count call site passes it; the early-exit call site has no quorum result, so mismatch computation is skipped there (there is nothing to compare against — the thrown error already lists the disagreeing providers).
  • Added test/providers/retryProvider.test.ts covering: the no-fallback disagreement path (throws the quorum error, not a ReferenceError), the fallback-consumed-by-failure path, and the quorum-reached-via-fallback happy path.
  • Bumped patch version 4.4.11 → 4.4.12.

🤖 Generated with Claude Code

When quorum is not reached and no fallback providers remain, the
early-exit throwQuorumError() called getMismatchedProviders(), which
closed over quorumResult - a const declared only after the fallback
results are counted. That access lands in the temporal dead zone, so
instead of the intended 'Not enough providers agreed to meet quorum'
error the call site crashes with a ReferenceError, and the diagnostic
warn log is skipped, hiding which providers disagreed.

Pass the quorum result into getMismatchedProviders explicitly and skip
the mismatch computation on the early-exit path, where no quorum result
exists to compare against.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@droplet-rl

Copy link
Copy Markdown
Contributor Author

Thanks for the review! No inline comments to address, so no further changes from my side. CI (Builds/Lint/Test) is still running — I'll leave merging to you once it's green.

@droplet-rl

Copy link
Copy Markdown
Contributor Author

🔎 View trace

@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: b24805fde1

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/providers/retryProvider.ts
droplet-rl added a commit that referenced this pull request Jul 15, 2026
…stack

RetryProvider.send and the Solana QuorumFallback factory accumulate
per-provider failures via `err.stack || err.toString()`. For ethers
transport errors (SERVER_ERROR, etc.), `err.stack` and the stringified
form embed the failed RPC URL with its API key in cleartext, plus a
multi-line ethers stack trace. That accumulator then gets concatenated
into the aggregate "Not enough providers succeeded" error message and
into the `logQuorumMismatchOrFailureDetails` warn log — both reach
Slack/Datadog and leak the key.

Introduce `summarizeProviderError(err)` which produces a single-line,
log-safe summary. Preference order:

  1. Parsed JSON-RPC error message (e.g. "execution reverted: ERC20:
     burn amount exceeds balance"), drilling into `err.error` if
     necessary — this is what callers actually want.
  2. `err.reason` — ethers' short, URL-free reason string.
  3. `err.message` for non-ethers errors (no `err.code` present).
  4. `err.code` (e.g. "SERVER_ERROR", "NETWORK_ERROR") as a last resort.

`err.message` is deliberately skipped for ethers-shaped errors because
that's exactly where transport-layer errors splice in the failed URL.
The original error remains reachable via `error.cause` on the aggregate
thrown by `createSendErrorWithMessage`, so forensic detail isn't lost.

Wire it into both error-collection sites in `retryProvider.ts` and both
in `solana/quorumFallbackRpcFactory.ts`. No behavior change beyond the
shape of the recorded error string.

Rebase notes (onto #1483's branch): master's Solana path meanwhile
introduced formatRpcError(), which preserves the SolanaError JSON-RPC
code in the wrap message. Keep formatRpcError() at the Solana call
sites and replace only its non-Solana fallback (err.stack ||
err.toString()) with summarizeProviderError(), so both intents compose:
code preservation for SolanaErrors, no URL/API-key leakage for
everything else.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pxrl
pxrl merged commit 883e543 into master Jul 15, 2026
4 checks passed
@pxrl
pxrl deleted the droplet/C03GHT4RV42-1784127850-691789 branch July 15, 2026 16:02
@droplet-rl

Copy link
Copy Markdown
Contributor Author

Addressed the Codex P2 finding in 77d1105: the no-fallback quorum-failure path now computes the most frequent result from the successful providers' responses (via a new findQuorumCandidate helper, extracted from the existing fallback-path counting logic) and measures mismatches against it, so the diagnostic warn log identifies which providers diverged instead of logging mismatchedProviders: []. Added a test that asserts the log flags the diverging provider on this path. Tests, lint, and build all pass locally.

@codex review

@droplet-rl

Copy link
Copy Markdown
Contributor Author

🔎 View trace

@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: 883e5439ec

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +166 to +168
const mismatchedProviders = quorum
? getMismatchedProviders(quorum.quorumResult, [...values, ...quorum.fallbackValues])
: [];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Compute mismatch diagnostics without fallbacks

When quorum requires all configured providers and those providers return different valid results, this early-exit path passes no quorum candidate and hard-codes mismatchedProviders to [], so the warn log cannot identify any diverging RPC even though the call is failing because the successful providers disagreed. Compute the most frequent successful response on this no-fallback path and compare the successful providers against it before logging, matching the later fallback path’s diagnostics.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Valid — this PR was squash-merged just before the follow-up commit landed on the branch, so master still has the empty-mismatchedProviders early-exit path. Re-opened the fix as #1484: the no-fallback path now computes the most frequent successful response via a findQuorumCandidate helper (shared with the fallback path) and measures mismatches against it, with a test asserting the diverging provider is flagged.

@droplet-rl

Copy link
Copy Markdown
Contributor Author

Codex's re-flag is valid, but for a timing reason: #1483 was squash-merged at 16:02 UTC with only the original commit — the diagnostics follow-up (77d1105) reached the branch right after the merge, so it never landed on master, and Codex reviewed the merge commit.

I've re-opened the fix as #1484 (#1484), cherry-picked cleanly onto master: the no-fallback quorum-failure path now computes the most frequent successful response via a shared findQuorumCandidate helper and logs which providers diverged, with a test asserting mismatchedProviders is populated. Tests pass locally; CI (Builds/Lint/Test) is running. @pxrl requested as reviewer — no changes needed on this merged PR.

@droplet-rl

Copy link
Copy Markdown
Contributor Author

🔎 View trace

droplet-rl added a commit to across-protocol/relayer that referenced this pull request Jul 15, 2026
Picks up across-protocol/sdk#1483 (quorum failures throw the intended
error instead of a ReferenceError, fixing the recurring
zion-across-proposer crash) and across-protocol/sdk#1441 (log-safe
per-provider error summaries, no RPC URL/API-key leakage).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pxrl pushed a commit to across-protocol/relayer that referenced this pull request Jul 15, 2026
Picks up two RetryProvider fixes shipping together in 4.4.12:

- across-protocol/sdk#1483 — quorum failures with no fallback providers
left now throw the intended "Not enough providers agreed to meet quorum"
error instead of crashing with `ReferenceError: Cannot access
'quorumResult' before initialization`. This is the error that has been
killing `zion-across-proposer` runs (5 consecutive runs on 2026-07-15,
previously 2026-06-22) and other dataworker/monitor bots since April.
- across-protocol/sdk#1441 — per-provider failures are summarized into
single-line, log-safe messages instead of `err.stack`, keeping RPC URLs
(and embedded API keys) out of aggregate error messages and logs.

Co-authored-by: droplet-rl <284132418+droplet-rl@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants