Skip to content

Make Solana re-sign retries verification-safe - #54

Merged
VickyXAI merged 2 commits into
BlockRunAI:mainfrom
KillerQueen-Z:fix/solana-safe-resign-retry
Aug 26, 2026
Merged

Make Solana re-sign retries verification-safe#54
VickyXAI merged 2 commits into
BlockRunAI:mainfrom
KillerQueen-Z:fix/solana-safe-resign-retry

Conversation

@KillerQueen-Z

Copy link
Copy Markdown
Contributor

Summary

  • re-sign only explicit verification-phase stale-blockhash rejections
  • cap fresh-signature retries at two with bounded backoff
  • never replay settlement-phase or phase-ambiguous payment failures
  • apply the same policy to sync/async chat, streaming, raw POST, and raw GET paths
  • preserve bounded gateway code/reason fields needed for safe classification

Safety

  • prevents a lost settlement acknowledgement from authorizing a second payment
  • bare transaction_simulation_failed and insufficient-funds errors remain terminal
  • streaming retries stop once any chunk has been yielded

Validation

  • 636 passed, 15 skipped
  • Black and Ruff passed on changed files
  • compileall passed
  • wheel build passed
  • Mypy still reports the repository existing baseline errors; introduced missing-return findings were eliminated

KillerQueen-Z and others added 2 commits August 26, 2026 00:56
The previous commit closed a real double-charge hole — a lost settlement
acknowledgement could authorize a second payment — but keyed the allowlist on
"is this a stale blockhash" instead of "did this reach settlement". Staleness
is not the safety property; phase is. Verify never broadcasts, so every
verify-phase rejection is free to re-sign, and four safe cases were swept up:

  PAYMENT_UNDERPAID          pre-verify amount binding. Its own message says
                             "Re-fetch the 402 quote and sign the amount it
                             specifies" — the facilitator's
                             invalid_exact_svm_payload_amount_mismatch, one of
                             the two failures 5448b1c was benchmarked against.
  PAYMENT_REPLAY             nonce claim rejected after verify, before the
                             result is served. The other one.
  verification_unavailable   gateway docs: "Retry the request; the signed
                             payment was not rejected." It even ships a
                             jittered Retry-After.
  verification_failed        the verify-phase catch-all, which carries
                             facilitator_timeout.

Together with _MAX_PAYMENT_RETRIES dropping 4 -> 2, that reverted the
concurrent single-wallet fix (measured 3-10% failures -> 100% at concurrency
10) without saying so. _should_fallback_solana refuses every PaymentError, so
these surfaced to the caller with no second model tried.

Replace the staleness allowlist with a phase gate:

  - settlement is terminal, checked first and on all three of code / reason /
    message so no single missing field can turn a broadcast into a re-sign
  - insufficient_funds and the unrecoverable invalidMessage causes stay
    terminal; _is_unrecoverable_payment_error is live again, restoring the BlockRunAI#23
    fail-fast for a payer with no USDC token account
  - pre-broadcast requires positive proof; silence stays terminal
  - restore _MAX_PAYMENT_RETRIES = 4. The bound that prevents paying twice is
    that settlement is never replayed at all, not the attempt count: every
    retried rejection is one the gateway refused before broadcasting.

Match the gateway's two 402 body families, not one. /v1/chat/completions sends
{error, message, code, reason}; the other ~16 paid routes send {error, reason}
with no code and no message — exactly the routes the raw POST/GET wrappers
serve. Titles are matched by prefix, never substring: _normalize_reason strips
separators, so a substring test straddles word boundaries, the same over-match
blockrun-sol/src/lib/payment-rejection.ts documents avoiding.

Drop the PAYMENT_BLOCKHASH_STALE and invalidMessage branches: neither exists in
the gateway. An exhaustive sweep of blockrun-sol yields five payment codes —
PAYMENT_INVALID, PAYMENT_REPLAY, PAYMENT_UNDERPAID, SETTLEMENT_FAILED, and
PAYMENT_VERIFICATION_UNAVAILABLE (503 only).

Tests: 15 -> 47, built through build_payment_rejected_error from the literal
gateway bodies so a wording change fails here instead of silently disabling the
retry. Covers all 8 retry call sites (both raw wrappers and both streams were
unexecuted), the streaming yielded>0 guard, and the retry bound. Verified by
mutation: removing the bound fails 7, removing the stream guard fails 2,
removing the phase gate fails 2, substring-matching the title fails 1.

Also: two stream docstrings still described the old permissive policy; drop the
unreachable AssertionError after the two generator loops (falling off the end
of a generator is legal and mypy does not require a return there) and make the
remaining six raise PaymentError, so a bound regression surfaces as the domain
error rather than as a bug check.

668 passed, black and ruff clean, mypy unchanged at the repo baseline.
@VickyXAI
VickyXAI merged commit a1cc169 into BlockRunAI:main Aug 26, 2026
4 checks passed
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