fix: stop transient RPC errors from wedging the tx verifier - #338
Merged
Conversation
A node that has saved a block but not yet persisted its ABCI responses answers blockResults with "could not find results for height #N". That call sat outside verifyTransaction's try/catch, so the error escaped as a thrown activity failure instead of the `unavailable` the tri-state contract already defines; the sweep workflow then threw WorkflowError, which extends plain Error rather than TemporalFailure and therefore fails the workflow TASK, retried forever, keeping runningActions non-empty so ScheduleOverlapPolicy.SKIP skipped every later fire. Mainnet went three days without verifying a transaction this way on 2026-08-10.
Review follow-ups: split 'result-unreadable' from 'result-missing' so the logs name the actual cause instead of claiming a missing result row for what was a failed read, cap the ApplicationFailure details payload so a large post-stall backlog cannot fail the very workflow task that reports the failure, and make the new test's blockResults rejection one-shot so it cannot poison a confirmed-path test added after it.
Merged
jorgecuesta
pushed a commit
that referenced
this pull request
Aug 13, 2026
### Reliability - Transient RPC errors no longer wedge the transaction verifier. A node at a block's commit boundary answers blockResults with "could not find results for height #N"; that error escaped verifyTransaction's tri-state contract and the sweep then threw WorkflowError, which is a plain Error rather than a TemporalFailure and therefore fails the workflow task forever. Under ScheduleOverlapPolicy.SKIP that wedged the schedule: mainnet went three days without verifying a transaction on 2026-08-10. (#338) - Corrupt schedules self-heal, with manual pause/resume/recreate from the UI. (#323) - Provider Keys pending-state polling is gated on an actually-pending state. Observability - Structured logging foundation: LogTape replaces pino, full console migration, repo is console-free and guarded by scripts/no-console-guard.sh. (#322) - Transaction tables show friendly on-chain failure reasons instead of raw codes. (#328) ### Supplier endpoints - Endpoint override compatibility is preserved across legacy and numeric forms, supplier override readers are aligned, and the behaviour is pinned by regression tests. - New CometBFT RPC type for service endpoints. (#328) ### UI - Revamp: sticky headers, tabs, failure reasons, notification filters. (#325) ### CI - Fork-PR staging deploys are fixed and the dead mainnet overlay is dropped. (#330) ### Database - Two additive migrations run on deploy: 0017 (notification channels, events and preferences) and 0018 (watchdog heal state). New tables and enums only — no ALTER on existing tables, no data movement.
This was referenced Aug 13, 2026
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.
fix: stop transient RPC errors from wedging the tx verifier
What happened on mainnet
On 2026-08-10 at 13:34:30 UTC the scheduled
VerifyPendingTransactionsrun stopped making progress and stayedRunningfor three days. WithScheduleOverlapPolicy: Skip, every subsequent fire (one every 30s) was skipped —SkippedOverlapclimbed to 9533 — so no pending transaction was verified for three days. Five stake transactions sat atstatus = pending, and the suppliers behind them never got theirnodesrow, so users saw their supplier as "pending" in the UI while it was staked and healthy on chain.The chain of causation, taken from the workflow history:
code 0— successful).blockResults(872893)on the same height. That call hit a node which had the block but had not yet persisted its ABCI responses, which CometBFT reports ascould not find results for height #N. Per-nodecometbft_consensus_heightfrom Prometheus confirms all three backends were sitting exactly at 872893 at that moment — the failure landed on the block's commit boundary.blockResultssat outsideverifyTransaction'stry/catch(onlycomet.block(h)was wrapped), so the error escaped instead of degrading to theunavailablestate the tri-state contract already defines for "the tx is on chain but we cannot read its outcome yet".results.every(rejected)held and the workflow threwWorkflowError.WorkflowErrorextends plainError, notTemporalFailure. The SDK fails the workflow only forTemporalFailuresubclasses; anything else fails the workflow task, which then retries forever. The run stayedRunning, andSkipdid the rest.Note that
WorkflowErroris exported from@temporalio/workflowand does construct — verified at runtime,instanceof Errortrue,instanceof TemporalFailurefalse. Two comments inprovider-workflowsclaimed it was unexported and threw aTypeError; the conclusion they drew was right but the stated mechanism was wrong, and they are corrected here.What this PR changes
packages/pocket—blockResultsis wrapped so a failed read of a matched tx's result row degrades toresult-missing→unavailable, which makes the verifier retry rather than fail. Covered by a new regression test that reproduces the exact production error string.middleman-workflows— all fivethrow new WorkflowError(...)sites become non-retryableApplicationFailure, matching the patternprovider-workflowsalready used. A failed execution now ends the run, so the schedule's next fire proceeds normally.Retry policy for the verification sweep (both apps) — from 3 attempts in ~4s to 5 attempts with a 5s base, 2x backoff, capped at 30s. That spans more than one block, so a commit-boundary hiccup resolves itself without any human involvement.
noWorkflowError.test.ts— a structural guard over both apps' workflow directories that fails ifnew WorkflowError(reappears. It greps source rather than exercising the runtime, because@temporalio/testingis not a dependency of this repo and adding it was out of scope here. Verified to go red by injecting the pattern.Verification
packages/pocket: 7/7 (new commit-boundary test red before the fix, green after)middleman-workflows: 91/91 ·provider-workflows: 33/33turbo lint,turbo build,no-console-guard.sh: passReview follow-ups already folded in
blockResultsfailing is nowresult-unreadable, kept apart fromresult-missing, because the old shared path logged "matched tx has no block result entry" for a read that never happened. Both still map tounavailable.ApplicationFailuredetails payload is capped at 20 reasons in both apps. An oversized payload would fail the workflow task that reports the failure — the same wedge this PR removes.blockResultsrejection is one-shot.clearAllMocks()resets calls but not implementations, and this jest config sets neitherresetMocksnorrestoreMocks, so a persistent rejection would have silently poisoned any confirmed-path test appended after it.Known gaps, deliberately not addressed here
evaluateLivenessreturnshealthywheneverrunningActions.length > 0(packages/temporal/src/scheduleWatchdog.ts:166), so a schedule blocked by a wedged run reads green in the Workflows UI. This PR removes one cause, not the blind spot: a nondeterminism error after a workflow-code deploy, an unregistered activity name, or an oversized payload all reproduce the same silent stall, and the new grep-based guard catches none of them. The concrete proposal for a follow-up is to haveevaluateLivenesstreat a running action older than K × the schedule interval asstaleinstead of trusting its mere presence.startToCloseTimeout+ ~3s backoff) to ~665s (5 × 120s + 65s backoff), andcreateOptionssets noworkflowRunTimeout. This is accepted here because the run now ends — under the old behaviour it never did — so starvation is bounded by the failure rather than unbounded. Two follow-ups worth doing: a run timeout on the sweep, andnonRetryableErrorTypesfor the deterministic activity throws (tx missing hash,tx not found), which today burn all 5 attempts plus 65s of backoff on permanent conditions.ExecuteTransactionhas a false-failure path on the broadcast leg. Theif (!result)guard is unreachable —sendTransactionalways returns an object, returning{ transactionHash: '', success: false }from its own catch — so a socket timeout onbroadcastTxSyncafter the node accepted the tx into mempool takes the!result.transactionHashbranch, marks the txFailureand emails the user. Pre-existing and untouched here, but it is the same false-negative class this PR removes from the verification leg, and it is worth its own fix.provider-workflows/src/activities/index.tshas three pre-existingrawLogtype errors (TS2339), present onstagingbefore this branch and confirmed by re-running the check with this branch's provider changes stashed.check-typesiscontinue-on-error: truein CI.apps/middleman-workflows/src/lib/blockchain/index.tsis dead code — nothing imports it — and carries its own copy of the sameblockResultscall. Left untouched; it should probably be deleted.