Repository navigation
fix(storage): bound 0G upload with a wall-clock timeout - #8
Closed
alkautsarf wants to merge 1 commit into
Closed
alkautsarf wants to merge 1 commit into
alkautsarf wants to merge 1 commit into
Conversation
The 0G SDK's Uploader.waitForLogEntry is an unbounded while(true) poll that hangs forever on a reachable-but-stuck storage node (it returns null, never throws, so the SDK's own axios timeout never arms). OGStorage.putBlob drove that loop with no timeout, so a wedged mainnet node could pin `anima init` (keystore upload), local `anima sync`, and the daemon POST /sync handler indefinitely. The download path already had this guard; the upload path did not. Wrap putBlob in a Promise.race against a wall-clock deadline (SDK_UPLOAD_TIMEOUT_MS=120s, configurable via uploadTimeoutMs), mirroring downloadBlobByRoot. Swallow the abandoned upload's late rejection so it can't crash the daemon as an unhandledRejection, and add AbortSignal.timeout to the indexer node-list fetch. The detached SDK loop is uncancellable (no AbortSignal upstream); documented as an accepted, bounded trade-off vs an unbounded hang. Adds og.test.ts: both upload paths time out, success passes through, real errors propagate (not masked as timeouts), late rejection is swallowed. Verified live against 0G testnet (real error in ~15s, no false timeout). Claude-Session: https://claude.ai/code/session_01JciX2J7DXGGBTDKuvk7q1K
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Member
Author
|
Subsumed by the v0.26.0 release: this work was squash-merged into |
This branch was successfully deployed
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.
The bug
The 0G SDK's
Uploader.waitForLogEntry(@0gfoundation/0g-ts-sdk@1.2.6) is an unboundedwhile (true)poll that only exits on success. A storage node "stuck at a block" (logSyncHeight stalled) is reachable and answerszgs_getFileInfowith a fast HTTP-200{result:null}— so it never throws, the SDK's own axios timeout never arms, and the loop spins forever.OGStorage.putBlobdrove that loop (via bothindexer.uploadand the discovered-nodessplitableUpload) with no timeout. So a wedged mainnet node could pin:anima init(keystore upload) — first-time setup hangs foreveranima syncPOST /synchandlerThe download path in the same file was already hardened against exactly this ("a hung indexer or wedged storage node"); the upload path never got the same guard. This was flagged by a friend who hit it building a sibling 0G app, then independently validated (4-investigator + 3-skeptic adversarial pass) against the SDK source.
The fix
Mirror the download-side guard onto the write path:
putBlobinPromise.raceagainst a wall-clock deadline (SDK_UPLOAD_TIMEOUT_MS = 120s, configurable viaOGStorageOpts.uploadTimeoutMs). A wedged node now throws instead of hanging.inner.catch) so it can't crash the long-lived daemon as anunhandledRejection. The race still observes a rejection that lands first, so real errors propagate normally (not masked as timeouts).AbortSignal.timeout(NODE_LIST_TIMEOUT_MS)to theindexer_getShardedNodesfetch, with a clear error translation.New
og.test.ts(5 tests): both upload paths time out, success passes through unchanged, real errors propagate (not masked), and the late rejection is swallowed without anunhandledRejection.Verification
maintree)Known trade-offs (documented in code) + deferred follow-ups
The SDK loop is uncancellable (no upstream
AbortSignal), so a timed-out upload is abandoned, not stopped. This is bounded, rare (true wedge only), and strictly better than an unbounded hang, but it has follow-ups worth tracking separately:doFlushatomicity — it advanceslastPlaintextHashper-slot before the batched chain anchor, so a mid-flushputBlobthrow (now reachable via timeout) can leave earlier slots marked-uploaded-but-unanchored until daemon restart. Pre-existing for anyputBlobthrow (e.g. insufficient gas); fix = advance the hash only after the anchor tx.skipIfFinalized: falseon the discovered-nodes path means a timeout-then-retry re-submits storage gas even if the first tx finalized. Flip totrueto let the SDK dedupe.These are intentionally out of scope for this minimal, well-tested guard.
https://claude.ai/code/session_01JciX2J7DXGGBTDKuvk7q1K