Serve IPFS retrievals - #312
Conversation
This comment was marked as outdated.
This comment was marked as outdated.
Clone `retriever` to `ipfs-retriever`. Signed-off-by: Miroslav Bajtoš <oss@bajtos.net>
Signed-off-by: Miroslav Bajtoš <oss@bajtos.net>
8c9649a to
f73c566
Compare
Signed-off-by: Miroslav Bajtoš <oss@bajtos.net>
Signed-off-by: Miroslav Bajtoš <oss@bajtos.net>
Signed-off-by: Miroslav Bajtoš <oss@bajtos.net>
Signed-off-by: Miroslav Bajtoš <oss@bajtos.net>
Signed-off-by: Miroslav Bajtoš <oss@bajtos.net>
Signed-off-by: Miroslav Bajtoš <oss@bajtos.net>
Signed-off-by: Miroslav Bajtoš <oss@bajtos.net>
Signed-off-by: Miroslav Bajtoš <oss@bajtos.net>
Signed-off-by: Miroslav Bajtoš <oss@bajtos.net>
Signed-off-by: Miroslav Bajtoš <oss@bajtos.net>
Signed-off-by: Miroslav Bajtoš <oss@bajtos.net>
Signed-off-by: Miroslav Bajtoš <oss@bajtos.net>
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
Signed-off-by: Miroslav Bajtoš <oss@bajtos.net>
|
Manual deployment of IPFS retrievals on FilBeam still works: |
|
|
||
| ### Query parameters | ||
|
|
||
| - `?format=car` — skip CAR-to-raw conversion and serve the raw CAR archive to the client |
There was a problem hiding this comment.
but we also skip validation of the CAR? so we pass it on to the client and it becomes their problem to validate? might be fine, but if this is part of the service we're advertising then we should be sure that they follow standard trustless-http protocol for validation of the contents of the CAR.
There was a problem hiding this comment.
Yes, we don't validate it.
Isn't that the expected behavior according to the Trustless Gateway protocol?
I've made that explicit in the documentation.
https://github.com/filbeam/worker/pull/312/changes#diff-cfd04945276d0a793adc966c6641b4a4c93606dc159a46e3ad3195ec757a096aR58
|
|
||
| `CarBlockIterator.fromIterable` reads only the CAR header (roots + version) upfront. Block data is pulled lazily as the iterator is consumed. | ||
|
|
||
| ### CAR-to-raw conversion |
There was a problem hiding this comment.
you have directory entries below, but what about the two cases for non-directories, where you hit file contents but may not know what to do with it; you kind of alude to it in this section but let's be more clear:
- The root CID itself resolves to some raw content, you don't actually know what it is, you don't even have a filename (we did away with directory wrapping for filecoin-pin in feat(unixfs)!: adopt IPIP-499 unixfs-v1-2025 profile filecoin-project/filecoin-pin#448 so it'll be common even for our content). I think this section says that we just shove it out to the browser and make it their problem to figure out the content type by magic bytes sniffing, I guess that works with modern browsers now.
- We have a filename, so at least have an extension for the file and could give a reasonable content-type for it like a standard web server. Do we not even do that here and just send it out and make it the browser's problem? Do you know if this is fine and good for modern browsers to render a whole website, JS, CSS, HTML, images, videos, etc.? Is there any reason this might be a risky strategy?
Either way, let's just note that there are two paths here -- we retrieve content that we just know are raw bytes and know nothing else about it, and the case where we at least have the terminal path that tells us the filename which may have an extension.
|
|
||
| ### Directory entries | ||
|
|
||
| If the resolved path is a UnixFS directory, the worker returns **404 Not Found** (`retrieval.js:195`). Directory listing is not implemented. Since the SP returns a path-scoped CAR with `dag-scope=all`, the directory block and immediate child blocks are present in the CAR. The 404 is a choice, not an architectural limit. This is tracked in [issue #696](https://github.com/filbeam/worker/issues/696). |
There was a problem hiding this comment.
see my note there about dag-scope=entity, I think it'd be a better choice in general for everything we're doing here than all.
|
|
||
| 2. **DFS-ordered blocks** — blocks are written to the CAR in the same order the DAG traversal engine requests them (depth-first). This is enforced in frisbii via `carPipe`, which hooks into the IPLD link system and writes each block to the CAR immediately as it is loaded during traversal. This is what makes the sequential `blocksReader.next()` blockstore safe. | ||
|
|
||
| 3. **Complete or fail loudly** — if any block is missing from SP storage, the traversal fails before any bytes are written to the CAR. The SP returns an HTTP error, not a partial CAR. The worker either gets a complete, valid CAR or an error response, never a silently truncated one. |
There was a problem hiding this comment.
Well ... not quite this clean. we made a choice in the frisbii version of trustless http that we'd prioritise streaming, so while traversing, it's possible to find that we don't have a block that we need, perhaps the client didn't upload their full DAG, and we have no useful way of signalling an error in that case, trailers are not a useful error signalling mechanism and we decided not to build in some kind of error signal into the CAR we returned, so we must rely on the fact that the receiver is doing the identical traversal to also discover that the next block isn't present and the CAR terminated prematurely. So it is possible start getting data, be happily traversing down the DAG with unixfs-exporter and then just halt. In which case we need to return some kind of meaningful error. It could be an SP problem, maybe their system is busted in some way, maybe network got disconnected and we don't get a clean disconnect error, but it could also be that the user didn't actually store the entire DAG with that SP.
The nice thing about IPFS retrievals and how they work on the SP side is that the entire DAG doesn't need to be in the same piece. You could have one tiny piece that just has the root CID and then a hundred more pieces that contain CARs with all the rest of the blocks for your content, and frisbii retrieval will just fetch the blocks in the right order out of all of those pieces to serve back to you (so the 1Gb piece limit we currently have in place doesn't stop you from storing and serving a 100Gb movie file using IPFS retrievals). BUT unlike Rainbow and other IPFS gateway software that taps into a broader network, using the DHT or IPNI to discover providers for blocks, the DAG must be all on the same SP for any of this to work. You can't shard your DAG across multiple SPs to have the filbeam retrieval work; but you can shard it and have generic IPFS gateway retrievals work because it does block-by-block lookups as it traverses and can skip from SP to SP to find what it needs. Important distinction that we should probably note somewhere.
|
|
||
| An alternative design (used by IPFS Shipyard tooling like Boxo and verified-fetch) drives the exporter with a blockstore whose `get(cid)` makes individual HTTP requests: `GET /ipfs/{blockCid}?format=raw`. This eliminates the ordering dependency and enables per-block Cloudflare caching. | ||
|
|
||
| The trade-off: a single file goes from 1 SP fetch to potentially 20–80 individual fetches. For FilBeam, which controls both ends and knows all blocks are at the same SP, the CAR approach is currently a good solution, one round-trip, any file size, fully streaming. The block-by-block design exists to solve distributed-network problems (blocks scattered across unknown peers) that FilBeam does not have. |
There was a problem hiding this comment.
On the other side of the equation here is that there's caching potential for block-by-block, that should be noted, maybe it's in the slack thread linked; /path/to/some/deep/nested/content.html where that html includes 100 jpgs has to re-fetch the DAG for /path/to/some/deep/nested/ for each jpg. Typically these are quite small and it's not really a big deal. There are some cases where it might matter but on balance I'd say that for our narrow case where the client is choosing to opt in to this service by storing the entire DAG on one of the SPs in this service rahter than scattering it around the internet and crossing their fingers, CAR retrieval makes more sense. But it's not a clear-cut case I think.
|
|
||
| **Cache key:** `{spBaseUrl}/ipfs/{ipfsRootCid}{ipfsSubpath}?format=car` | ||
|
|
||
| Since CIDs are content-addressed and immutable, the same key always resolves to the same bytes. `ORIGIN_CACHE_TTL` could theoretically be much longer, but a very large TTL risks filling PoP cache storage with large CARs. The current 1-day value is a reasonable starting point and should be revisited once cache hit rates and storage pressure are measurable. |
There was a problem hiding this comment.
also reinforced by my point above that perhaps the SP is having a bad day and their database had problems resolving all the blocks in the CAR, or one of their disks holding a piece that contained part of the DAG was down for a moment and you got an incomplete CAR; you don't want to associated strictly a forever-cache with a potential blip in serving.
It's true that CIDs point to immutable content, but you're dealing with a whole string of CIDs that must be validated together. In theory CID+subpath is unique, but there a ways for it to go bad and not know until you run the traversal.
| Set on every successful response to the client (`retrieval/lib/fetch-handler.js`): | ||
|
|
||
| ``` | ||
| Cache-Control: public, max-age=31536000 | ||
| ``` |
There was a problem hiding this comment.
You do have the same problem as I outlined above I think if we're streaming the content to the user unless you hard-abort the connection or something (IIRC there's no good, reliable signal for terminating an HTTP stream that would invalidate a cache? I think I remember we wrestled with this for a while in the trustless HTTP design). Might be worth a bit of research to make sure that this isn't going to cause problems -- tell the client that their 100Mb movie file is correct for a year, only able to fetch and serve 50Mb of it, what then? is the movie now 50Mb for that client for a year? Can we terminate in a way that will reliably tell the client that the max-age shouldn't be respected?
There was a problem hiding this comment.
if in doubt, we should try and pull in @lidel to get some answers since this kind of nuance is right up his alley
cc409cd to
94a6b34
Compare
|
@BravoNatalie : I'm going to put this back to TODO since I know this work is currently on pause. If there are still "TODOs after merge" that are listed in the issue description, lets capture those in the project issue #297 which will stay open after the merge so we don't lose track of them. |
URL schema:
https://1-{base32(dataSetId)}-{base32(pieceId)}.ipfs.filbeam.io/{favicon.ico}https://link.ipfs.calibration.filbeam.io/{WalletAddres}/{IpfsRootCid}/{favicon.ico}(redirects to the above)This is a proof of concept that I don't expect to land. Instead, I'll extract smaller pull requests and land them independently.
I will be updating
filbeam-ipfs-retriever-calibrationworker on CF with the code from this pull request.I have a special-case path sending the retrieval requests to our Frisbii instance as a workaround until Curio implements IPFS retrievals for PDP deals:
Raw image:
CAR format:
Does not work yet - we need to figure out Cloudflare config:
https://ipfs.calibration.filbeam.io/0x000000000000000000000000000000000000dead/bafybeiagrjpf2rwth5oylc64czsrz2jm7a4fgo67b2luygqjrivjbswuku/rusty-lassie.png
TODO:
This is blocked until there is at least one calibnet SP running the Curio version that scans PDP pieces for IPFS CAR blocks.
Support all(later)?format=rawSupport for other IPFS Trustless GW options, like dag-scope, etc.(later)Bot to peridically check IPFS retrievals(later)Preserve(later)?format=carand other query-string params when redirecting from/wallet/cidto1-{dataset}-{piece}.Links:
TODO after merge