-
Notifications
You must be signed in to change notification settings - Fork 44
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add support for null-padded carv1 payloads #140
Comments
cc @magik6k since you originally authored the go-car commit to skip the padding. |
your proposal of a lenient |
This is the behavior that Filecoin relies on, and it seems like we all find it reasonable (at least from the people I've talked to, including the IPLD sync call). Plus, it would be weird for the CARv1 to allow interleaving null bytes in between valid block frames. I'm making the implementation treat the first null byte as EOF. We can change the CARv1 spec shortly after, unless @rvagg disagrees. |
ooof, I thought Filecoin was keeping track of payload length so it could just reverse fr32, make a limitreader and read the car to its extent. I think the main hangup to making this "OK" is that we lose any determinism that might be afforded by packing into a CARv1, i.e. you can have an arbitrary number of (potentially random) bytes trailing it when at rest. I don't think this is a very serious concern as a system can always work work just the bytes it cares about (like Filecoin does). It may also open up some interesting avenues for low-grade attacks (DoS or similar) when transferring CARs between untrusted parties. But, what we could do is simply make it an optional mode in the spec, something like:
|
LimitReader was also my first response, but I was told they don't keep the padding offset. Do you have any suggestion in mind in terms of having them detect the limit/offset efficieny without a full migration? Even when reading from the end of the file, it seems relatively slow to me. You're right that the null bytes are effectively part of the file so they affect determinism. And you're right that it opens up infinite ways to represent almost duplicate CARv1s. I'm not sure I agree with this behavior being on by default is problematic. Certainly safer to make it opt-in, but that also makes the API bigger - note that right now we don't have any index.Generate options. |
I'm suggesting the opt-in for CARv1. CARv2 already has padding all over the place, very little determinism to be found in the format outside of the CARv1 payload, and it's not as useful as a transport anyway so I reckon CARv2 could use CARv1 with the "null byte == EOF" option enabled, no need to add a way to turn it off. The only question is then of extending CARv1 |
Gotcha, I had missed that nuance. I'm happy to merge the PR as soon as someone does a final review, perhaps from @masih or @aarshkshah1992.
We won't have a very nice way per se, because retrofitting options into a frozen API is pretty constrained. But luckily we can retrofit an option by adding a method to the reader, such as:
Then the opt-in would look like:
|
cr.StopAtNull() not awesome but workable, let's do that |
OK this makes sense, though I would have loved the spec to be as strict as possible. I think the ship has sailed on this and we have to flex the spec to to not break existing usage. |
Fixed by #141, but let's keep this open as a reminder to update the carv1 spec to mention null padding. |
❤️ sadly there's lots of regrets in CARv1! but we shall cope, and imagine a beautiful CARv3 ... |
Filed #142, and since the PR here is merged and the spec PR is open, I think we can close this in favor of the v1 issue and the spec PR. |
Late to the party in this issue, but I would advise against including this mode in the spec as an allowed mode of operation. I would just put this under an "implementers' note", merely targeted at dissipating doubts on why this hack exists, for folks reading go-car (as the reference implementation). Rationale:
The right way to fix this is to record the real payload size in Lotus/go-fil-markets, but unfortunately that's a very costly thing to backfill now, because it would require us to unseal every deal to find the 0x00... boundaries. With the current PoRep, that's unfeasible. Another option is wrapping the read stream Lotus-side in a utility that would interrupt the stream with an EOF when the boundary was detected. This could work by using "lookahead buffers", but unfortunately there's no way of making it reliable without leaking implementation details. I think our best bet to get rid of this workaround is to wait for fast unsealing. |
It sounds like we're mostly all leaning in the direction of this being an unfortunate, but internal concern for Lotus rather than something we want to be able to live with long term, which is great because we can leave the spec unmolested and keep the hack .. somewhat hidden. 👍 |
* Implement an option for read-write blockstore, that if enabled the write can resume from where the writer left off. For resumption to work the `WithResumption` option needs to be set explicitly. Otherwise, if path to an existing file is passed, the blockstore construction will return an error. The resumption requires the roots passed to constructor as well as padding options to be identical with roots in file. Resumption only works on paths where at least V2 pragma and CAR v1 header was successfully written onto the file. Otherwise an error is returned. * Implement resumption test that verifies files resumed from match expected header, data and index. * Implement a CAR v1 equals function to check if two given headers are identical. This implementation requires exact ordering of root elements. A TODO is left to relax the exact ordering requirement. * Implement Seeker in internal offset writer in order to forward offset of CAR v1 writer within a resumed read-write blockstore after resumption. The offset of the writer needs to be set to the latest written frame in order for consecutive writes to be at the right offset. * Fix bug in offset read seeker in internal IO, where seek and returned position was not normalized by the base. Reflect the fix in read-only blockstore AllKeysChan where reader was twisted to work. We now read the header to get its size, then seek past it to then iterate over blocks to populate the channel. * Add TODOs in places to make treating zero-length frames as EOF optional; See #140 for context. * Run `gofumpt -l -w .` on everything to maintain consistent formatting.
* Implement an option for read-write blockstore, that if enabled the write can resume from where the writer left off. For resumption to work the `WithResumption` option needs to be set explicitly. Otherwise, if path to an existing file is passed, the blockstore construction will return an error. The resumption requires the roots passed to constructor as well as padding options to be identical with roots in file. Resumption only works on paths where at least V2 pragma and CAR v1 header was successfully written onto the file. Otherwise an error is returned. * Implement resumption test that verifies files resumed from match expected header, data and index. * Implement a CAR v1 equals function to check if two given headers are identical. This implementation requires exact ordering of root elements. A TODO is left to relax the exact ordering requirement. * Implement Seeker in internal offset writer in order to forward offset of CAR v1 writer within a resumed read-write blockstore after resumption. The offset of the writer needs to be set to the latest written frame in order for consecutive writes to be at the right offset. * Fix bug in offset read seeker in internal IO, where seek and returned position was not normalized by the base. Reflect the fix in read-only blockstore AllKeysChan where reader was twisted to work. We now read the header to get its size, then seek past it to then iterate over blocks to populate the channel. * Add TODOs in places to make treating zero-length frames as EOF optional; See #140 for context. * Run `gofumpt -l -w .` on everything to maintain consistent formatting. Address review comments * Implement equality check for CAR v1 headers where roots in different order are considered to be equal. * Improve resumption docs to clarify what matching roots mean.
Filecoin has been using a fork of the carv1 library with commit 11b6074, which allows decoding carv1 files that end with null padding. They use this because their crypto proofs require power-of-two payload lengths.
Spec-wise, this seems valid. A CARv1 is a series of "frames", in the form of:
So, if such a frame begins with the zero byte, then we should simply skip to the next byte, because we have a varint of value 0 for the cid+block length, and thus we have nothing else to do. It's reasonable to simply ignore the frame entirely, because there's no CID and no block data.
There's only one place in the carv2 library where we decode CARv1 frames:
index.Generate
. Right now this padding makes it error, because it unconditionally reads a CID, even if the frame size is zero. In that way, it incorrectly implements the spec; it shouldn't attempt to read CID bytes when it knows a frame is of zero length. So we definitely have something to fix in carv2.The CARv1 spec doesn't explicitly allow this kind of empty CID plus empty block, but it's not explicitly disallowed either. It does contain a section on padding, however:
I think the ship has sailed in terms of forbidding this kind of padding in the CARv1 spec, given that Filecoin has been using this for some time. We could tell them to "migrate" all of those null-padded CARv1 files, so that they also store them alongside a "padding offset" and cut the payload when using the carv2 library, effectively producing an EOF error when the library reaches the padding.
However, this would come at a relatively high cost to them (migrations are work), whereas on our side it's fairly trivial to amend one line to handle zero-length frames in a better way. What I propose is to skip the zero-length frames, effectively meaning that
index.Generate
would skip the entire null padding one byte at a time.That would be pretty slow, but it would work. Later on, we have two options:
A) In the CARv1 spec, specify that a decoder is allowed to treat a frame beginning with a null byte as an EOF, discarding the rest of the input. This would allow us to quickly exit and stop reading bytes.
B) In the carv2 implementation, when we reach a null-byte frame, we read the rest of the input in big chunks, which would allow us to quickly reach EOF as long as there are no non-zero bytes. If there are any non-zero bytes, we would go back to the slow path of decoding each frame starting with a varint. Purely an optimization for the common case.
I lean towards making the fix above today, and later on amending the CARv1 spec as per option A.
The text was updated successfully, but these errors were encountered: