Filecoin
Sampled Retrieval Check
Dealbot Methodology
This document is the source of truth for how dealbot's Sampled Retrieval check works.
Source code links throughout this document point to the current implementation.
For event and metric definitions to be used by the dashboard, see Dealbot Events & Metrics.
Rendered from the dealbot documentation · cached for 24 hours
Overview
The Sampled Retrieval check (sometimes referred to internally as retrieval++) tests publicly discoverable pieces on a storage provider (pieces that were not uploaded by dealbot). The intent is to measure SP retrievability against real-world tenant data, not just dealbot's own corpus.
This is distinct from the Retrieval check, which exercises pieces dealbot itself uploaded as part of a Data Storage check. The Sampled Retrieval check answers a different question: does the SP serve arbitrary pieces from its broader public corpus, with the same correctness and performance properties as dealbot's controlled pieces?
Definition of Successful Retrieval
A successful sampled retrieval requires:
- Piece fetch —
GET {spBaseUrl}/piece/{pieceCid}returns HTTP 2xx and the response bytes hash to the declared CommP (piece CID).
If the piece advertises IPFS indexing (withIPFSIndexing = true and a non-null ipfsRootCid), three additional dimensions are validated independently. Importantly, they do not gate the overall piece_fetch_status, and each is recorded as its own outcome column / metric:
- CAR parseable: the fetched bytes parse as a CAR file.
- IPNI: the SP is advertised as a provider for the root CID and a sample of child CIDs via filecoinpin.contact.
- Block fetch: a sample of CIDs from the parsed CAR is re-fetched via
{spBaseUrl}/ipfs/{cid}?format=rawand each response is hash-verified against its declared CID.
A piece without IPFS indexing is exercised only at step (1).
Operational timeouts exist to prevent jobs from running indefinitely. If the job exceeds SAMPLED_RETRIEVAL_JOB_TIMEOUT_SECONDS, it is aborted; a row is still emitted so that partial metrics (TTFB, bytes, response code) are not lost.
Piece Selection
Unlike the Retrieval check, dealbot does not retrieve from its own deals. Pieces are sampled from the on-chain subgraph of all FWSS-served pieces for the SP under test 1.
Selection strategy (per scheduled job, per SP):
- Pick a size bucket by weighted random:
small(1–10 MiB) — 20%medium(10–50 MiB) — 50%large(50–100 MiB) — 30%
Note: the bucket sizes were chosen such that the whole file will still fit into memory. In the future we may implement a streaming verification and parsing.
2. Pick a pool:
- indexed (IPFS-indexed pieces) — 80%
- any (all FWSS pieces) — 20%
Note: this split exists so that SPs cannot optimize only their CAR corpus and still appear healthy on this check.
3. Generate a uniform-random sampleKey and query the subgraph for the smallest Root.sampleKey ≥ $sampleKey matching the SP, payer, size range, and pool filters. If no such row exists (the random key fell above every matching sampleKey), samplePiece retries in the reverse direction (largest sampleKey < $sampleKey) so the highest keys are not a dead zone.
4. Drop the candidate if pdpPaymentEndEpoch has passed.
5. Fall back through: (same bucket, opposite pool) → (any bucket, indexed) → (any bucket, any).
Source: sampled-piece-selector.service.ts
What Happens Each Cycle
flowchart TD
Select["Sample sampled piece for SP from subgraph"] --> Fetch["GET /piece/{pieceCid}"]
Fetch --> CommP["Hash bytes → verify CommP"]
CommP --> HasIpfs{"piece.withIPFSIndexing<br/>and ipfsRootCid?"}
HasIpfs -- "no" --> Record["Persist ClickHouse row + emit Prometheus metrics"]
HasIpfs -- "yes" --> ParseCar["Parse bytes as CAR"]
ParseCar --> SampleBlocks["Pick N random CIDs<br/>(SAMPLED_RETRIEVAL_BLOCK_SAMPLE_COUNT)"]
SampleBlocks --> Ipni["IPNI: verify SP advertises root + sampled CIDs"]
SampleBlocks --> BlockFetch["GET /ipfs/{cid}?format=raw for each sampled CID"]
BlockFetch --> HashCheck["Hash-verify each response against its CID"]
Ipni --> Record
HashCheck --> Record
Piece Fetch
- URL:
{spBaseUrl}/piece/{pieceCid}(HTTP/2) - Buffered in memory — piece sizes are capped at 100 MiB by selection (the upper bound of the
largebucket). - Download ceiling enforced — the download is aborted once received bytes exceed
SAMPLED_MAX_PIECE_DOWNLOAD_BYTES; the partial buffer is discarded and the result is recorded asfailure.too_large. The advertised size is the subgraph's claim, not an enforced limit, so this is the actual on-the-wire ceiling. - Validates CommP — the CommP of the response bytes must match
pieceCid.
Source: piece-retrieval.service.ts
CAR / IPNI / block-fetch validation (only when piece advertises IPFS indexing)
When the selected piece has withIPFSIndexing = true and a non-null ipfsRootCid, three dimensions are exercised by PieceValidationService. Each dimension has an independent outcome; a failure or skip in one never bleeds into another's status.
- CAR parse:
@ipld/carparses the response bytes; a random sample ofSAMPLED_RETRIEVAL_BLOCK_SAMPLE_COUNTCIDs is selected for the next two steps. - IPNI check:
IpniVerificationService.verify(rootCid, sampledCids, sp)polls filecoinpin.contact until each CID resolves to the SP under test, the timeout fires, orIPNI_VERIFICATION_TIMEOUT_MSis reached. - Block fetch check: for each sampled CID, fetch
{spBaseUrl}/ipfs/{cid}?format=rawand hash-verify the response against the CID. Non-2xx, hash mismatch, unsupported codec, a response exceedingSAMPLED_MAX_BLOCK_DOWNLOAD_BYTES, or transport errors all count as a single failed block.
CAR parse failure (failure.not_parseable) is attributed to the client (bad upload), not the SP. When the CAR is unparseable, IPNI and block fetch are skipped because there are no sampleable CIDs to verify or fetch.
Source: piece-validation.service.ts
What Gets Asserted
| # | Assertion | How It's Checked | Retries | Relevant Metric |
|---|---|---|---|---|
| 1 | SP serves the piece | GET /piece/{pieceCid} returns HTTP 2xx |
0 | sampledPieceRetrievalLastByteMs |
| 2 | Bytes match the declared CommP | Hash of response bytes equals pieceCid |
0 | sampledPieceRetrievalStatus |
| 3 | Bytes parse as a CAR (IPFS-indexed pieces only) | @ipld/car parses the response |
0 | sampledCarParseStatus |
| 4 | SP is advertised on IPNI for root + sampled CIDs | filecoinpin.contact returns provider records | polling until timeout | sampledIpniStatus |
| 5 | Sampled blocks fetch + hash-verify | /ipfs/{cid}?format=raw for each sample |
0 | sampledBlockFetchStatus |
Sub-status meanings
Unlike the Data Storage check, sampled retrieval does not have a rolled-up status (e.g., sampledRetrievalStatus). Piece retrieval, CAR parsing, IPNI verification, block-fetch outcomes are recorded independently. Each status metric below is emitted exactly once per check, except when sampledPieceRetrievalStatus=skipped because selection itself failed and no HTTP/CAR/IPNI/block-fetch work ran.
| sampledPieceRetrievalStatus | Meaning |
|---|---|
success |
GET /piece/{pieceCid} returned HTTP 2xx and the response bytes hashed to the declared CommP. |
skipped |
The subgraph returned no candidate piece for the SP after all selection fallbacks. No HTTP request was attempted. |
failure.http |
Piece fetch did not return HTTP 2xx, or the request failed at the transport layer (DNS, TLS, connection reset, etc.). |
failure.commp |
Piece fetch returned HTTP 2xx, but the response bytes hashed to a different CID than pieceCid. The bytes are discarded — downstream CAR / IPNI / block-fetch validation is skipped to avoid amplifying a misbehaving SP. |
failure.too_large |
The SP streamed more than SAMPLED_MAX_PIECE_DOWNLOAD_BYTES. The download was aborted mid-stream and the partial buffer discarded so a misbehaving SP cannot OOM the worker. Downstream CAR / IPNI / block-fetch validation is skipped. |
failure.timedout |
The job-level AbortSignal fired (most often SAMPLED_RETRIEVAL_JOB_TIMEOUT_SECONDS). Partial timing/byte evidence is still persisted. |
failure.other |
Catch-all for retrieval failures that do not match any of the categories above. |
| sampledCarParseStatus | Meaning |
|---|---|
success |
The fetched piece bytes were successfully parsed as a CAR by @ipld/car. |
skipped |
CAR parsing was not attempted — piece fetch failed, the piece does not advertise IPFS indexing, or the job aborted before parsing. |
failure.not_parseable |
The fetched piece bytes could not be parsed as a CAR (malformed header, truncated content, unexpected encoding, or parser threw an error). Attributed to the client (bad upload), not the SP. |
| sampledIpniStatus | Meaning |
|---|---|
success |
filecoinpin.contact returned the SP as a provider for the root CID and every sampled child CID within IPNI_VERIFICATION_TIMEOUT_MS. |
skipped |
IPNI verification was not attempted — piece fetch failed, the piece does not advertise IPFS indexing, CAR parsing returned failure.not_parseable, the root CID itself failed to parse, or the job aborted. |
failure.timedout |
IPNI was queried but at least one CID never resolved to the SP under test before IPNI_VERIFICATION_TIMEOUT_MS (the poll loop exhausted its timeout with unresolved CIDs). |
failure.other |
IPNI verification was attempted and IpniVerificationService.verify threw unexpectedly (transport error, service down, etc.). |
| sampledBlockFetchStatus | Meaning |
|---|---|
success |
Every sampled CID was fetched via GET {spBaseUrl}/ipfs/{cid}?format=raw and the response bytes hash-verified against the declared CID. |
skipped |
Block-fetch sampling was not attempted — piece fetch failed, the piece does not advertise IPFS indexing, CAR parsing returned failure.not_parseable, or the job aborted. |
failure.other |
At least one sampled block fetch failed (non-2xx HTTP, hash mismatch, unsupported codec, unsupported hash, a response exceeding SAMPLED_MAX_BLOCK_DOWNLOAD_BYTES, or transport error — each failed sample counts as one failed block), or the sampling loop threw unexpectedly outside the per-block try/catch. Per-block granularity lives in block_fetch_failed_count. |
Sources:
- sampled-retrieval.service.ts — orchestrates the dimensions and emits the four status metrics
- piece-retrieval.service.ts — classifies piece-fetch outcomes
- piece-validation.service.ts — produces CAR / IPNI / block-fetch outcomes independently
- types.ts (CarParseStatus, IpniCheckStatus) — enum source of truth
Result Recording
Each sampled retrieval attempt writes one row to the sampled_retrieval_checks ClickHouse table — including a skipped row (piece_fetch_status='skipped') when piece selection found no candidate for the SP after all fallbacks, so the empty-pool case is recorded as check data rather than counted as a job failure. The row is emitted even on abort or unexpected error so that the partial evidence (TTFB, bytes, response code) is preserved. On a skipped row there is no piece to test, so the piece-identity columns (piece_cid, data_set_id, piece_id, raw_size) carry sentinel values ('' / 0).
The DDL and column-level comments in clickhouse.schema.ts are authoritative. The summary below is for orientation.
| Column | Meaning |
|---|---|
timestamp |
When the check started (ms UTC) |
probe_location |
Dealbot probe location (DEALBOT_PROBE_LOCATION) - currently unused and set to unknown until https://github.com/FilOzone/dealbot/issues/246 lands |
sp_address, sp_id, sp_name |
SP identity |
retrieval_id |
Per-event UUID; correlates row to logs and Prometheus |
piece_cid, data_set_id, piece_id, raw_size |
Sampled piece identity |
with_ipfs_indexing, ipfs_root_cid |
Whether the piece advertises IPNI metadata |
service_type |
Always direct_sp today |
retrieval_endpoint |
URL probed for piece fetch |
piece_fetch_status |
success, failed, or skipped — outcome of /piece/{cid} (HTTP 2xx and CommP match). skipped = no candidate piece was found, so no request was made. CAR/IPNI/block-fetch outcomes live in their own columns and do not flip this status. |
http_response_code |
Raw HTTP status; null on transport failure |
first_byte_ms, last_byte_ms, bytes_retrieved |
Piece-fetch performance |
commp_valid |
Null when retrieval failed before CommP could be hashed |
car_status |
success | skipped | failure.not_parseable — mirrors sampledCarParseStatus |
car_block_count |
Total CAR block count; null unless car_status='success' |
block_fetch_endpoint |
Gateway base URL probed; null when skipped or SP info missing |
block_fetch_status |
success | skipped | failure.other — mirrors sampledBlockFetchStatus |
block_fetch_sampled_count, block_fetch_failed_count |
Sampled / failed block counts; null when skipped |
ipni_status |
success | skipped | failure.timedout | failure.other — mirrors sampledIpniStatus |
ipni_verify_ms |
IPNI verification duration; null when skipped |
error_message |
Failure reason; null on success |
Source: sampled-retrieval.service.ts
Metrics Recorded
Sampled-retrieval Prometheus metric definitions live in Dealbot Events & Metrics. All sampled-retrieval metrics carry checkType=sampledRetrieval.
Configuration
Key environment variables that control sampled retrieval testing:
| Variable | Description |
|---|---|
SAMPLED_RETRIEVALS_PER_SP_PER_HOUR |
Sampled retrieval rate per SP. Falls back to RETRIEVALS_PER_SP_PER_HOUR when unset. |
SAMPLED_RETRIEVAL_JOB_TIMEOUT_SECONDS |
Max end-to-end sampled retrieval job runtime before forced abort (default 360s). |
SAMPLED_RETRIEVAL_BLOCK_SAMPLE_COUNT |
Number of CIDs sampled from the parsed CAR for IPNI + block-fetch verification (default 5, max 50). |
IPNI_VERIFICATION_TIMEOUT_MS |
Max time to wait for IPNI provider verification (shared with the Retrieval check). |
IPNI_VERIFICATION_POLLING_MS |
Poll interval between IPNI verification attempts (shared). |
CONNECT_TIMEOUT_MS |
Connection/header timeout for HTTP requests. |
HTTP2_REQUEST_TIMEOUT_MS |
Total timeout for HTTP/2 retrieval requests. |
The in-memory download ceilings (SAMPLED_MAX_PIECE_DOWNLOAD_BYTES, SAMPLED_MAX_BLOCK_DOWNLOAD_BYTES) are compile-time constants in sampled-piece-selector.service.ts, not environment variables. The piece ceiling is derived from SIZE_BUCKETS.large.max so it stays in sync with the selection buckets.
See also: docs/environment-variables.md for the full configuration reference.