Make Solana re-sign retries verification-safe - #54
Merged
VickyXAI merged 2 commits intoAug 26, 2026
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Safety
Validation