-
Notifications
You must be signed in to change notification settings - Fork 38
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
fix!: keep ProtoNode deserialised state stable until explicit mutation #87
Conversation
4d7f788
to
8a73123
Compare
ipld/ipld#233 has spec updates that address this, and refer to a go-merkledag@v0.6.0 which should result from a release with this commit. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would also bubble this change up using a commit hash into kubo. There are a bunch of tests and history there such that if a test breaks it should link to what the original concern/issue was.
code archaeology tip: If you're curious why something is how it is you can use git blame and when you land on a commit for something that seems pretty old copy-paste the commit title (or hash) and search for it in kubo to find the related PR.
node.go
Outdated
// Links returns a copy of the node's links. | ||
func (n *ProtoNode) Links() []*format.Link { | ||
return n.links | ||
links := make([]*format.Link, len(n.links)) | ||
copy(links, n.links) | ||
return links | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems more reasonable. Hopefully no one is using mutation here, did you check for usage of this function or mostly assuming folks haven't been doing anything nuts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, I haven't checked very broadly and I haven't seen anyone doing anything weird but I don't want to expose this if we're taking so much care with statefulness of sorting internally. But, I had to do this here filecoin-project/boost#675 to deal with the inverse - that go-merkledag was messing with it.
node.go
Outdated
func (n *ProtoNode) AddRawLink(name string, l *format.Link) error { | ||
n.encoded = nil | ||
n.links = append(n.links, &format.Link{ | ||
Name: name, | ||
Size: l.Size, | ||
Cid: l.Cid, | ||
}) | ||
|
||
sort.Stable(LinkSlice(n.links)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we concerned about performance hits here during node creation since we're sorting after every link addition?
Would it make more sense to basically mark the node as dirty and then sort + clear the dirty bit the first time anyone encodes or tries to inspect the links?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, I wasn't too concerned about performance because this is mostly an insertion-into-sorted operation where we're just shuffling pointers down the slice after the insertion point. But I've added a new commit that adds a linksDirty
state to help defer the costs here. Note that it's a bit subtle because we can't call it dirty if it comes deserialized unsorted, we want to capture that state. So we have to make a decision when to sort - so linksDirty
is done whenever you mutate the Links list, then we sort whenever we perform an operation that would need them to be in a sorted state (if they should be sorted!). But there's also operations where we have to re-sort regardless of linksDirty
, like cloning the node, or re-serializing... Which brings up some additional questions that I've been meaning to add to the thread. Will comment separately on these.
There's some additional options I wanted to flag here if anyone feels strongly enough to engage on them. IMO this currently is the right approach, but it's not the only approach. Consider the main case we're dealing with here - of a deserialized block that came with Links not properly sorted as per spec:
Basically, sort your Links properly to avoid all of this. |
5c480ad
to
31cdb6f
Compare
@@ -132,7 +154,6 @@ func (n *ProtoNode) GetPBNode() *pb.PBNode { | |||
// EncodeProtobuf returns the encoded raw data version of a Node instance. | |||
// It may use a cached encoded version, unless the force flag is given. | |||
func (n *ProtoNode) EncodeProtobuf(force bool) ([]byte, error) { | |||
sort.Stable(LinkSlice(n.links)) // keep links sorted |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
best to highlight the biggest culprit of the problems we're having right now - re-sorting links regardless of whether we have a cached encoded form means that just calling RawData()
will re-sort links regardless of whether we have the original raw data or not
includes ipfs/go-merkledag#87 to keep deserialized state of dag-pb blocks stable for stable traversals
includes ipfs/go-merkledag#87 to keep deserialized state of dag-pb blocks stable for stable traversals
ipfs/kubo#9202 👍 a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's good, TL;DR can you please use slices.Clone
for slice copy ?
When decoding a badly serialised block with Links out of order, don't sort the list until we receive an explicit mutation operation. This ensures stable DAG traversal ordering based on the links as they appear in the serialised form and removes surprise-sorting when performing certain operations that wouldn't be expected to mutate. The pre-v0.4.0 behaviour was to always sort, but this behaviour wasn't baked in to the dag-pb spec and wasn't built into go-codec-dagpb which now forms the backend of ProtoNode, although remnants of sorting remain in some operations. Almost all CAR-from-DAG creation code in Go uses go-codec-dagpb and go-ipld-prime's traversal engine. However this can result in a different order when encountering badly encoded blocks (unsorted Links) where certain intermediate operations are performed on the ProtoNode prior to obtaining the Links() list (Links() itself doesn't sort, but e.g. RawData() does). The included "TestLinkSorting/decode" test is the only case that passes without this patch. Ref: ipld/ipld#233 Ref: filecoin-project/boost#673 Ref: filecoin-project/boost#675
22e403f
to
0a25b5f
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
includes ipfs/go-merkledag#87 to keep deserialized state of dag-pb blocks stable for stable traversals
includes ipfs/go-merkledag#87 to keep deserialized state of dag-pb blocks stable for stable traversals
includes ipfs/go-merkledag#87 to keep deserialized state of dag-pb blocks stable for stable traversals
When decoding a badly serialised block with Links out of order, don't sort
the list until we receive an explicit mutation operation. This ensures stable
DAG traversal ordering based on the links as they appear in the serialised
form and removes surprise-sorting when performing certain operations that
wouldn't be expected to mutate.
The pre-v0.4.0 behaviour was to always sort, but this behaviour wasn't baked
in to the dag-pb spec and wasn't built into go-codec-dagpb which now forms the
backend of ProtoNode, although remnants of sorting remain in some operations.
Almost all CAR-from-DAG creation code in Go uses go-codec-dagpb and
go-ipld-prime's traversal engine. However this can result in a different order
when encountering badly encoded blocks (unsorted Links) where certain
intermediate operations are performed on the ProtoNode prior to obtaining the
Links()
list (Links()
itself doesn't sort, but e.g.RawData()
does).The included "TestLinkSorting/decode" test is the only case that passes without
this patch.
Ref: ipld/ipld#233
Ref: filecoin-project/boost#673
Ref: filecoin-project/boost#675
Further notes: