Skip to content

Feat/s3 gateway additions - #138

Merged
0g-peterzhb merged 8 commits into
mainfrom
feat/s3-gateway-additions
May 25, 2026
Merged

Feat/s3 gateway additions#138
0g-peterzhb merged 8 commits into
mainfrom
feat/s3-gateway-additions

Conversation

@0g-peterzhb

@0g-peterzhb 0g-peterzhb commented May 23, 2026

Copy link
Copy Markdown
Contributor

Summary

SDK changes needed by the 0G S3 gateway: stream-to-io.Writer downloads, range downloads, and cheap fee estimation. Plus a string of refactor / bug fixes the gateway work surfaced.

Commits

Commit What
e0aa330 DownloadToWriter / DownloadRangeToWriter / indexer EstimateFee — initial gateway-facing additions
9febd5d drop the unused EstimateFee pricePerSector cache (over-engineered)
1d22769 extract fetchSegmentFromNode shared helper
80ff6e9 route download_parallel.go through the shared helper
7f52f76 group fetchSegmentFromNode params into a segmentFetchRequest struct
a1843ae use core.NumSplits for ceil-divide in downloader_writer.go
707eeee route both downloaders through core.SegmentRange
b63e6b6 indexer: don't geo-lookup loopback / private IPs, bound the HTTP call

Closes

Test plan

  • go build ./... clean
  • go test ./transfer/... ./indexer/... green
  • All gateway e2e tests (26) pass with the rebuilt indexer binary; previously-recurring "indexer port not available within 60s" flake no longer reproduces.

🤖 Generated with Claude Code

0g-peterzhb and others added 8 commits May 7, 2026 08:36
…ateway

Three additions for the 0G S3 gateway, kept self-contained in new files
so the existing surface is untouched:

  Downloader streaming (transfer/downloader_writer.go):
    DownloadToWriter(ctx, root, w, withProof) — full file
    DownloadRangeToWriter(ctx, root, offset, length, w) — byte range
  Avoids the temp-file disk hop in (*Downloader).Download. Sequential
  segment fetch via DownloadSegmentByTxSeq; per-segment slicing for
  range reads. Encryption is rejected at the entry point — encrypted
  files require buffering for header parsing, defeating streaming;
  callers should use Download(filename) for those.

  Indexer wrappers (indexer/client_writer.go):
    Same two methods on (*indexer.Client), discovering nodes via
    NewDownloaderFromIndexerNodes and forwarding.

  Cheap fee preflight (indexer/client_estimate.go):
    EstimateFee(ctx, w3, marketAddr, data, tags) returns the on-chain
    protocol fee a submission would charge — pricePerSector * numSectors.
    Caches pricePerSector for ~30s (configurable via SetEstimateCacheTTL)
    so a hot-path preflight on a high-QPS gateway doesn't pin the chain
    RPC. Cheaper than Uploader.EstimateFee, which requires
    NewUploaderFromIndexerNodes (storage-node selection round-trip).

The 5 chunk-math unit tests for the writer methods carry over from the
v1.2.2 fork. No upstream callers changed.
pricePerSector is governance-gated (FixedPrice.setPricePerSector in
0g-storage-contracts/contracts/market/FixedPrice.sol) and in practice
never changes — a 30s TTL re-reads it ~120×/hour for no reason. The
actual hot-path consumer (0g-storage-s3-server) already wraps its own
cache around Market.PricePerSector, so the SDK cache was redundant
double-caching.

Strip the four cache fields (estimateMu, estimateTTL, estimatePrice,
estimateLoaded) and SetEstimateCacheTTL / cachedPricePerSector. Every
EstimateFee now does one Market.PricePerSector eth_call — cheap and
not on any hot path. Callers that need caching can layer their own.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Step 1 of 2 toward eliminating the tryFetchSegment dup in
downloader_writer.go (#139). This change:

  - Adds transfer/segment_fetch.go with fetchSegmentFromNode(...) —
    the single source of truth for the shard-skip + fetch +
    proof-validation per-node download path.
  - Replaces tryFetchSegment in downloader_writer.go with a call to
    the new helper.

Behavior tightened: the helper returns an error if the client has no
ShardConfig (download_parallel.go was already this strict; the
writer path was loose). Loose nil-shard behavior was a latent bug —
we'd silently try to fetch from a node that may not have the segment.

Step 2 will refactor download_parallel.go's downloadSegment to also
use this helper, deleting its inline shard-check + downloadWithProof
method.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Step 2 of 2 closing #139. download_parallel.go now calls the shared
fetchSegmentFromNode helper for shard-skip + RPC + proof-validation,
deleting its own segmentDownloader.downloadWithProof method. Net
effect: one implementation of the per-node fetch path serves both
file-based parallel download and io.Writer streaming download.

Preserved invariants:
  - Per-attempt logging in download_parallel.go (with the same
    fields: node index, segment range, chunk range)
  - The (endChunk-startChunk)*ChunkSize length-check from the old
    downloadWithProof — moved into the helper so both callers get it
  - Padding trim for the last segment stays at the call site (only
    download_parallel.go needs it; downloader_writer.go has its own
    range-trim logic)

Closes #139.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The helper was a 10-arg function — readable enough at the def site,
hostile at the call site. Group the parameters into a request struct:

  - file-level:  TxSeq, Root, FileSize
  - segment-level: SegIdx, GlobalSegIdx, StartChunk, EndChunk
  - mode: WithProof

No behavior change; both call sites now construct the struct literally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…r_writer

(endByte + chunkSize - 1) / chunkSize is exactly what core.NumSplits
already does. Call the SDK helper instead of rolling our own.

Closes #140.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two pre-existing inline reinventions of core.SegmentRange now call it:

  - download_parallel.go:36-37 was computing
      startSegmentIndex := info.Tx.StartEntryIndex / DefaultSegmentMaxChunks
      endSegmentIndex   := (info.Tx.StartEntryIndex + NumSplits(...) - 1) / DefaultSegmentMaxChunks
    which is exactly what SegmentRange returns. Replaced with a
    single call.
  - downloader_writer.go:70 was computing globalStartSeg = startEntryIndex
    / segChunks inline. Now reads SegmentRange's startSegmentIndex
    (endSegmentIndex discarded — the byte-range case computes its own
    file-relative endSeg from the requested offset/length).

The uploader (uploader.go:853, uploader_segments.go:104) already uses
SegmentRange; downloaders were stragglers.

Closes #141.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root cause of the e2e "indexer port not available within 60s" flake.

cmd/indexer.go:59 → InitDefaultNodeManager → AddTrustedNodes calls
defaultIPLocationManager.Query(parseIP(url)) for every trusted node
at startup. For loopback URLs ("http://127.0.0.1:11260") this fires
http.Get("http://ipinfo.io/127.0.0.1/json") using the default
http.Client — which has no timeout. On a machine where ipinfo.io
is slow/blocked (WARP/VPN, restricted DNS, captive portal, etc.)
the indexer can't bind its RPC port until that GET returns.

Fix:
  - isNonRoutableIP — short-circuit Query for loopback / RFC1918 /
    link-local / unspecified addresses. ipinfo has nothing useful
    to say about them anyway.
  - ipLocationHTTPClient with 5s Timeout — for real public IPs that
    DO go through, bound the call so a sick upstream can't wedge
    indexer startup. The Query result is already non-fatal at the
    call site (node_manager.go:158 logs Warn and continues), so
    failing fast is strictly better.
  - unit test for isNonRoutableIP covering loopback, RFC1918, link-
    local, public, and unparseable cases.

Closes #143.

Co-Authored-By: Claude Opus 4.7 (1M context) <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

1 participant