Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
104 commits
Select commit Hold shift + click to select a range
cf025ce
feat: add ipfs-retriever worker
bajtos Sep 29, 2025
f73c566
feat: implement IPFS-style retrievals
bajtos Sep 29, 2025
dd025cc
fixup! package-lock maintenance
bajtos Oct 2, 2025
c26393b
feat: add BigInt<>Base32 converters
bajtos Oct 2, 2025
b2e4c81
feat: add `getSlugForWalletAndCid` helper
bajtos Oct 2, 2025
56b4cee
feat: redirect /wallet/cid/pathname to slug.ipfs.filbeam.io/pathname
bajtos Oct 2, 2025
d7e4692
feat: https://1-{dataset}-{piece}.ipfs.filbeam.io
bajtos Oct 2, 2025
4b7b1c6
fix: drop multibase `b` prefix from the slugs
bajtos Oct 2, 2025
f8de5b0
fix: CF worker name
bajtos Oct 2, 2025
6faf291
feat: special-case handling for our Frisbii instance
bajtos Oct 2, 2025
290b38d
fix: improve error message about the slug format
bajtos Oct 2, 2025
4425cc3
fix: use 0x00dead address for the special case
bajtos Oct 2, 2025
aed1018
fix: serve redirects also at link.ipfs.calibration.filbeam.io
bajtos Oct 2, 2025
c47071f
Merge branch 'main' into serve-ipfs-retrievals
bajtos Oct 20, 2025
78d175c
feat: convert CAR to RAW
bajtos Oct 20, 2025
4d1541e
disable bad-bits lookup for IPFS retrievals
bajtos Oct 24, 2025
9baaea0
Merge branch 'main' into serve-ipfs-retrievals
juliangruber Oct 27, 2025
eee955b
re-enable bad bits
juliangruber Oct 27, 2025
4723465
fix bad bits method signature
juliangruber Oct 27, 2025
6b28eb1
Merge branch 'main' into serve-ipfs-retrievals
juliangruber Oct 27, 2025
0e0451d
fix missing `await`
juliangruber Oct 27, 2025
992e284
add CD
juliangruber Oct 27, 2025
2105982
fix type
juliangruber Oct 27, 2025
07c4dec
add support mixed case wallet addresses
juliangruber Oct 27, 2025
9c0c2a4
fix recreate node module tree
juliangruber Oct 27, 2025
8d9fdec
tests wip
juliangruber Oct 27, 2025
2e0f385
fix test
juliangruber Oct 27, 2025
7ccfd61
refactor
juliangruber Oct 27, 2025
78bb34d
tests wip
juliangruber Oct 27, 2025
784006d
fix test
juliangruber Oct 27, 2025
044a105
fix test
juliangruber Oct 27, 2025
1690c01
SP retrievals wip
juliangruber Oct 29, 2025
fa1011f
real SP test passes
juliangruber Oct 29, 2025
4900845
fmt
juliangruber Oct 29, 2025
d40e592
no mainnet for now
juliangruber Oct 29, 2025
89e8fad
remove frisbii special case
juliangruber Nov 2, 2025
1f3c471
fix: enable content-type sniffing for RAW responses
bajtos Oct 21, 2025
ff768d2
fix: handle 404 responses and empty response body
bajtos Oct 21, 2025
9f2272c
Merge branch 'main' into serve-ipfs-retrievals
bajtos Nov 4, 2025
e223831
fix support entry type `raw`
juliangruber Nov 5, 2025
0aac84d
Merge branch 'main' into serve-ipfs-retrievals
bajtos Nov 6, 2025
8e04420
refactor: `@filbeam/retrieval` in ipfs-retriever
bajtos Nov 6, 2025
78a7d32
Merge branch 'main' into serve-ipfs-retrievals
juliangruber Nov 13, 2025
def2c30
Merge branch 'main' into serve-ipfs-retrievals
juliangruber Nov 14, 2025
ef6cecf
use shared `getDataSetStats()`
juliangruber Nov 14, 2025
4320137
clean up
juliangruber Nov 14, 2025
ceed20c
re-enable mainnet
juliangruber Nov 14, 2025
78778c3
clean up
juliangruber Nov 14, 2025
5be53ac
Merge branch 'main' into serve-ipfs-retrievals
juliangruber Nov 14, 2025
9ba5aa5
refactor `logRetrievalResult()`
juliangruber Nov 14, 2025
48db1e4
Merge branch 'main' into serve-ipfs-retrievals
Jun 15, 2026
393fc4c
Merge branch 'main' into serve-ipfs-retrievals
juliangruber Jun 22, 2026
5faaa9e
ipfs-retriever: add top-level vars and regenerate wrangler types
juliangruber Jun 22, 2026
a17c30f
ipfs-retriever: fix and skip failing retriever tests
juliangruber Jun 22, 2026
1306170
Track cache-miss egress separately from client egress
juliangruber Jun 22, 2026
1df103d
ipfs-retriever: measure origin CAR egress separately from served raw …
juliangruber Jun 22, 2026
278407b
Bill and report cache-miss egress from the origin CAR size
juliangruber Jun 22, 2026
50ffcc2
Extract getErrorHttpStatusMessage into @filbeam/retrieval
juliangruber Jun 22, 2026
bafeddb
Extract handleError into @filbeam/retrieval
juliangruber Jun 22, 2026
247f5ad
Extract checkBotAuthorization into @filbeam/retrieval
juliangruber Jun 22, 2026
a203600
Extract isCidDenied bad-bits lookup into @filbeam/retrieval
juliangruber Jun 22, 2026
fa11ff7
Extract setRetrievalResponseHeaders into @filbeam/retrieval
juliangruber Jun 22, 2026
d69138e
Extract originCacheOptions into @filbeam/retrieval
juliangruber Jun 22, 2026
003ceb1
ipfs-retriever: move CAR-to-raw header adjustments into processIpfsRe…
juliangruber Jun 22, 2026
80f2422
ipfs-retriever: give each processIpfsResponse return key its own bullet
juliangruber Jun 22, 2026
f91ba6a
ipfs-retriever: compare block multihashes instead of full CIDs
juliangruber Jun 22, 2026
0fc4f6f
ipfs-retriever: retry across service providers on retrieval failure
juliangruber Jun 22, 2026
85f5c8c
Extract shared request-handling flow into @filbeam/retrieval (#674)
juliangruber Jun 23, 2026
694a9b9
DRY: share the retrieval authorization cascade (Tier 3) (#675)
juliangruber Jun 23, 2026
d2cf934
ipfs-retriever: enforce egress quota on retrieval
juliangruber Jun 23, 2026
852a8d7
Fold the egress quota check into filterAuthorizedRetrievalCandidates
juliangruber Jun 23, 2026
80aece2
Share the retrieval worker fetch wrapper via handleFetchRequest
juliangruber Jun 23, 2026
e115c23
Move the method check into handleFetchRequest
juliangruber Jun 23, 2026
c6aaaf3
Move the legacy domain redirect into handleFetchRequest
juliangruber Jun 23, 2026
146dc9e
Move payer address validation into piece-retriever parseRequest
juliangruber Jun 23, 2026
61a256b
Extract retrieval candidate selection into @filbeam/retrieval (#677)
juliangruber Jun 23, 2026
dff6a73
Fold the no-service-provider 502 response into selectRetrievalCandida…
juliangruber Jun 23, 2026
3ce7d83
Extract the empty-body response handling into @filbeam/retrieval (#679)
juliangruber Jun 23, 2026
9fda451
Log a 900 retrieval result when the ipfs response stream errors (#684)
juliangruber Jun 23, 2026
4c70b22
Charge the ipfs cache-miss egress quota on every cache miss (#683)
juliangruber Jun 23, 2026
a7c7132
Build retrieval candidate queries from a shared helper (#685)
juliangruber Jun 23, 2026
9855fb0
Combine the Bad Bits check into assertCidNotDenied (#686)
juliangruber Jun 23, 2026
18f1d9f
Handle egress measurement and logging in handleFetchRequest (#687)
juliangruber Jun 23, 2026
36f06d5
Move retrieval error logging into handleFetchRequest (#688)
juliangruber Jun 23, 2026
c565da5
Move handleEmptyBodyResponse into fetch-handler
juliangruber Jun 24, 2026
df91f10
Move redirectLegacyDomain and setRetrievalResponseHeaders into fetch-…
juliangruber Jun 24, 2026
4174e93
Move logRetrievalError into fetch-handler
juliangruber Jun 24, 2026
cf8a236
Enable live calibration retrieval tests against newly uploaded content
juliangruber Jun 24, 2026
04dbafe
Align ipfs-retriever wrangler config with piece-retriever
juliangruber Jun 25, 2026
da457d9
Use wrangler-action@v4 for the IPFS Retriever deploy step
juliangruber Jun 25, 2026
9170899
Index pieces.ipfs_root_cid for ipfs-retriever candidate lookups
juliangruber Jun 25, 2026
d0c3fe7
Gate the ipfs-retriever deploy to calibration only
juliangruber Jun 25, 2026
b2d5f0f
Fix ipfs-retriever package.json main entrypoint and drop deploy:mainnet
juliangruber Jun 25, 2026
55a0839
Add pieceCid to the CONTENT_STORED_ON_CALIBRATION type
juliangruber Jun 25, 2026
63253e9
Trim trailing space from the calibration ENVIRONMENT var
juliangruber Jun 25, 2026
5724d36
Harden checkBotAuthorization against an empty BOT_TOKENS
juliangruber Jun 25, 2026
24d6691
Stream the CAR with CarBlockIterator instead of buffering it in memory
juliangruber Jun 25, 2026
325b693
Fix cacheMissResponseValid JSDoc type in usage-reporter test helper
juliangruber Jun 25, 2026
9adcf9c
Merge remote-tracking branch 'origin/main' into serve-ipfs-retrievals
juliangruber Jun 25, 2026
e5bf02e
fix: cancel SP response body on early exit in processIpfsResponse
BravoNatalie Jul 2, 2026
99c7912
docs: add ipfs-retriever architecture and design decisions
BravoNatalie Jul 2, 2026
94a6b34
chore: update ipfs-retriever doc
BravoNatalie Jul 9, 2026
251ba12
docs: add data layer reference (tables, ER diagram, queries)
BravoNatalie Jul 9, 2026
9bcf009
fix: linter errors
BravoNatalie Jul 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,15 @@ jobs:
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
preCommands: ../db/deploy-${{ matrix.environment }}.sh
environment: ${{ matrix.environment }}
- name: Deploy IPFS Retriever and Migrate Database
if: matrix.environment == 'calibration'
uses: cloudflare/wrangler-action@v4
with:
workingDirectory: ipfs-retriever
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
preCommands: ../db/deploy-${{ matrix.environment }}.sh
environment: ${{ matrix.environment }}
- name: Deploy Indexer
uses: cloudflare/wrangler-action@v4
with:
Expand Down
236 changes: 236 additions & 0 deletions docs/data-layer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
# FilBeam Data Layer

All workers share a **single Cloudflare D1 database** (SQLite). Migrations live in `db/migrations/`.

- **Tests**: migrations are applied automatically via `wrangler d1 migrations apply test-db --local` (see root `package.json` test script).
- **Deploy**: worker deploys (`npm run deploy:calibration/mainnet`) do **not** apply migrations automatically. Run `db/deploy-calibration.sh` / `db/deploy-mainnet.sh` separately before deploying workers when migrations are pending.

---

## Tables

| Table | Description |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------- |
| `service_providers` | SP registry: service URL and deletion status, keyed by on-chain provider ID |
| `data_sets` | CDN deals: links an SP to a payer, tracks CDN/IPFS flags, egress usage, usage reporting watermarks, and termination state |
| `pieces` | Pieces per data set: piece CID, IPFS root CID (from chain metadata), and deletion flag |
| `data_set_egress_quotas` | Remaining byte budgets for CDN delivery and cache-miss charges; only exists for data sets that have been topped up |
| `wallet_details` | Payer addresses with sanction status and last Chainalysis screen timestamp |
| `retrieval_logs` | Per-request audit log: egress bytes, cache hit/miss, performance timings, country code, and bot flag |

---

## ER Diagram

```mermaid
erDiagram
service_providers {
TEXT id PK
TEXT service_url
INTEGER block_number
BOOLEAN is_deleted
}
data_sets {
TEXT id PK
TEXT service_provider_id
TEXT payer_address
BOOLEAN with_cdn
BOOLEAN with_ipfs_indexing
INTEGER total_egress_bytes_used
TIMESTAMP usage_reported_until
TEXT pending_usage_report_tx_hash
TIMESTAMP cdn_payments_settled_until
TEXT terminate_service_tx_hash
TIMESTAMP lockup_unlocks_at
}
pieces {
TEXT id PK
TEXT data_set_id PK
TEXT cid
TEXT ipfs_root_cid
TEXT x402_price
BOOLEAN is_deleted
}
data_set_egress_quotas {
TEXT data_set_id PK
INTEGER cdn_egress_quota
INTEGER cache_miss_egress_quota
}
wallet_details {
TEXT address PK
BOOLEAN is_sanctioned
TIMESTAMP last_screened_at
}
retrieval_logs {
INTEGER id PK
DATETIME timestamp
TEXT data_set_id
INTEGER response_status
INTEGER egress_bytes
INTEGER cache_miss_egress_bytes
BOOLEAN cache_miss
BOOLEAN cache_miss_response_valid
INTEGER fetch_ttfb
INTEGER fetch_ttlb
INTEGER worker_ttfb
TEXT request_country_code
TEXT bot_name
}

service_providers ||--o{ data_sets : "hosts"
data_sets ||--o{ pieces : "contains"
data_sets ||--o| data_set_egress_quotas : "has quota"
wallet_details ||--o{ data_sets : "pays for"
data_sets ||--o{ retrieval_logs : "logs"
```

> Relationships are logical — no foreign-key constraints are declared in the schema.

---

## How Tables Are Populated

### `service_providers`

Written by **indexer** in response to `ServiceProviderRegistry` on-chain events:

| Event | Handler | Effect |
| ------------------------------------ | -------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `ProductAdded` / `ProductUpdated` | `handleProductAdded/Updated` | Upserts `id`, `service_url`, `block_number`; skips if stored `block_number` is newer (out-of-order guard) |
| `ProductRemoved` / `ProviderRemoved` | `handleProductRemoved/ProviderRemoved` | Sets `is_deleted = true` |

### `data_sets`

Written by **indexer** in response to `FWSS` and `FilBeamOperator` events:

| Event | Handler | Effect |
| ------------------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DataSetCreated` | `handleFWSSDataSetCreated` | Upserts `id`, `service_provider_id`, `payer_address`, `with_cdn`, `with_ipfs_indexing`; also creates/updates `wallet_details` with sanction screen result **only when `withCDN` is set** |
| `ServiceTerminated` | `handleFWSSServiceTerminated` | Sets `with_cdn = false`, calculates and sets `lockup_unlocks_at` |
| `CDNPaymentRailsToppedUp` | `handleFWSSCDNPaymentRailsToppedUp` | Increments `data_set_egress_quotas` (idempotent via KV event dedup) |
| `CDNPaymentSettled` | `handleCdnPaymentSettled` | Advances `cdn_payments_settled_until` to the block timestamp |

Written by **usage-reporter** after confirmed on-chain usage report:

- Sets `usage_reported_until` watermark and clears `pending_usage_report_tx_hash`

Written by **terminator** after confirmed termination transaction:

- Sets `terminate_service_tx_hash`

### `pieces`

Written by **indexer** in response to `PDPVerifier` on-chain events:

| Event | Handler | Effect |
| ------------------------- | --------------------- | --------------------------------------------------------------------------------------------------------------- |
| `PieceAdded` / `addPiece` | `insertDataSetPiece` | Upserts `id`, `data_set_id`, `cid`, `ipfs_root_cid`, `x402_price`; `ipfs_root_cid` comes from on-chain metadata |
| `PieceRemoved` | `removeDataSetPieces` | Sets `is_deleted = true` (batch, up to 50 per D1 statement) |

### `data_set_egress_quotas`

Written by **indexer** (`handleFWSSCDNPaymentRailsToppedUp`): converts top-up amounts to byte quotas using configured rates and increments both `cdn_egress_quota` and `cache_miss_egress_quota`.

Decremented by **piece-retriever** / **ipfs-retriever** after each successful retrieval (only when `ENFORCE_EGRESS_QUOTA` is enabled): `cdn_egress_quota` is charged for all egress bytes served to the client; `cache_miss_egress_quota` is charged only on valid cache misses.

### `wallet_details`

Created/updated by **indexer** on `DataSetCreated`, but **only when `withCDN` is true** (Chainalysis API call per new payer).

Re-screened periodically by **indexer** scheduled task (`screenWallets`): re-screens wallets not checked within the configured stale threshold, ordered oldest-first.

### `retrieval_logs`

Written by **piece-retriever** and **ipfs-retriever** after every request via `recordRetrieval` in `retrieval/lib/stats.js`. Written inside `ctx.waitUntil` so it does not block the response.

---

## Key Queries by Worker

### Retrieval candidate lookup (piece-retriever, ipfs-retriever)

The shared query in `retrieval/lib/access.js` (`buildRetrievalCandidateQuery`) JOINs five tables in one shot:

```sql
SELECT pieces.data_set_id, data_sets.service_provider_id, data_sets.payer_address,
data_sets.with_cdn, data_set_egress_quotas.cdn_egress_quota,
data_set_egress_quotas.cache_miss_egress_quota,
service_providers.service_url, service_providers.is_deleted AS service_provider_is_deleted,
wallet_details.is_sanctioned
FROM pieces
LEFT OUTER JOIN data_sets ON pieces.data_set_id = data_sets.id
LEFT OUTER JOIN data_set_egress_quotas ON pieces.data_set_id = data_set_egress_quotas.data_set_id
LEFT OUTER JOIN service_providers ON data_sets.service_provider_id = service_providers.id
LEFT OUTER JOIN wallet_details ON data_sets.payer_address = wallet_details.address
WHERE pieces.cid = ? -- piece-retriever
-- or: pieces.ipfs_root_cid = ? -- ipfs-retriever
AND pieces.is_deleted IS FALSE
```

The result rows are then filtered by `filterAuthorizedRetrievalCandidates` (authorization cascade) — see `retrieval/lib/access.js`.

### Slug resolution (ipfs-retriever, slug flow)

Two sequential queries in `ipfs-retriever/lib/store.js`:

1. Resolve `(pieceId, dataSetId)` → `ipfs_root_cid` + `payer_address`:

```sql
SELECT pieces.ipfs_root_cid, data_sets.payer_address
FROM pieces LEFT OUTER JOIN data_sets ON pieces.data_set_id = data_sets.id
WHERE pieces.id = ? AND pieces.data_set_id = ?
```

2. Then the retrieval candidate query above, keyed by `ipfs_root_cid`.

### Usage aggregation (usage-reporter)

Aggregates `retrieval_logs` per data set between the `usage_reported_until` watermark and a target timestamp, excluding bot traffic and data sets with a pending transaction:

```sql
SELECT rl.data_set_id,
SUM(rl.egress_bytes) AS cdn_bytes,
SUM(CASE WHEN rl.cache_miss = 1 AND rl.cache_miss_response_valid = 1
THEN COALESCE(rl.cache_miss_egress_bytes, rl.egress_bytes) ELSE 0 END) AS cache_miss_bytes
FROM retrieval_logs rl
INNER JOIN data_sets ds ON rl.data_set_id = ds.id
WHERE rl.timestamp > datetime(ds.usage_reported_until)
AND rl.timestamp <= datetime(?)
AND rl.egress_bytes IS NOT NULL
AND rl.bot_name IS NULL
AND ds.pending_usage_report_tx_hash IS NULL
GROUP BY rl.data_set_id
HAVING (cdn_bytes > 0 OR cache_miss_bytes > 0)
```

### Terminator

Finds active CDN data sets whose payer is sanctioned and have no pending termination:

```sql
SELECT DISTINCT data_sets.id
FROM data_sets
LEFT JOIN wallet_details ON data_sets.payer_address = wallet_details.address
WHERE data_sets.with_cdn = 1
AND wallet_details.is_sanctioned = 1
AND data_sets.terminate_service_tx_hash IS NULL
```

### Payment settler

Finds data sets that need CDN payment rail settlement (active, recently reporting, not sanctioned):

```sql
SELECT data_sets.id
FROM data_sets
LEFT JOIN wallet_details ON data_sets.payer_address = wallet_details.address
WHERE (data_sets.with_cdn = 1 OR data_sets.lockup_unlocks_at >= datetime('now'))
AND data_sets.terminate_service_tx_hash IS NULL
AND data_sets.usage_reported_until >= datetime('now', '-30 days')
AND (wallet_details.is_sanctioned IS NULL OR wallet_details.is_sanctioned = 0)
```

### Stats API

- **Per data set** (`stats-api`): reads `data_set_egress_quotas` by `data_set_id`
- **Per payer** (`stats-api`): aggregates `data_set_egress_quotas` and `retrieval_logs` joined through `data_sets`, grouped by `payer_address`
Loading