Skip to content

Commit 1fc56b8

Browse files
committed
Fix hash mismatch error on matching link pointer
On load, the default IPLD Link System checks the hash of loaded data against the given hash to assure that they match. The actual hash is calculated using the given link's `LinkPrototype.BuildLink`, and then is directly compared with the given link via `==`. Because the comparison is done by value, if the given link is a pointer the comparison fails which will result in false-positive hash mismatch error. To address this issue, instead of directly comparing objects using `==` check that their `Binary` value, i.e. their densest possible encoding, is equal. Add tests that assert the issue is fixed and works with existing link representations.
1 parent 2ab2e8c commit 1fc56b8

File tree

3 files changed

+77
-4
lines changed

3 files changed

+77
-4
lines changed

datamodel/link.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ type Link interface {
4040
// the golang string type is used for immutability and for ease of use as a map key.
4141
// As with the String method, the returned value may not elide any parts of the hash.
4242
//
43-
// Note that there is still no contract that the returned value be able to be parsed back into a Link value;
43+
// Note that there is still no contract that the returned value should be parsable back into a Link value;
4444
// not even in the case of `lnk.Prototype().BuildLink(lnk.Binary()[:])`.
4545
// This is because the value returned by this method may contain data that the LinkPrototype would also restate.
4646
// (For a concrete example: if using CIDs, this method will return a binary string that includes

linking/functions.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
// This file contains all the functions on LinkSystem.
1212
// These are the helpful, user-facing functions we expect folks to use "most of the time" when loading and storing data.
1313

14-
// Varations:
14+
// Variations:
1515
// - Load vs Store vs ComputeLink
1616
// - Load vs LoadPlusRaw
1717
// - With or without LinkContext?
@@ -141,7 +141,7 @@ func (lsys *LinkSystem) LoadRaw(lnkCtx LinkContext, lnk datamodel.Link) ([]byte,
141141
hasher.Write(buf.Bytes())
142142
hash := hasher.Sum(nil)
143143
lnk2 := lnk.Prototype().BuildLink(hash)
144-
if lnk2 != lnk {
144+
if lnk2.Binary() != lnk.Binary() {
145145
return nil, ErrHashMismatch{Actual: lnk2, Expected: lnk}
146146
}
147147
// No codec to deploy; this is the raw load function.
@@ -205,7 +205,7 @@ func (lsys *LinkSystem) Fill(lnkCtx LinkContext, lnk datamodel.Link, na datamode
205205
// (Then do a bit of a jig to build a link out of it -- because that's what we do the actual hash equality check on.)
206206
hash := hasher.Sum(nil)
207207
lnk2 := lnk.Prototype().BuildLink(hash)
208-
if lnk2 != lnk {
208+
if lnk2.Binary() != lnk.Binary() {
209209
return ErrHashMismatch{Actual: lnk2, Expected: lnk}
210210
}
211211
// If we got all the way through IO and through the hash check:

linking/functions_test.go

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package linking_test
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"testing"
7+
8+
qt "github.com/frankban/quicktest"
9+
"github.com/ipfs/go-cid"
10+
"github.com/ipld/go-ipld-prime"
11+
"github.com/ipld/go-ipld-prime/codec/dagcbor"
12+
"github.com/ipld/go-ipld-prime/datamodel"
13+
"github.com/ipld/go-ipld-prime/fluent"
14+
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
15+
"github.com/ipld/go-ipld-prime/node/basicnode"
16+
"github.com/ipld/go-ipld-prime/storage/memstore"
17+
"github.com/multiformats/go-multicodec"
18+
)
19+
20+
func TestLinkSystem_LoadHashMismatch(t *testing.T) {
21+
subject := cidlink.DefaultLinkSystem()
22+
storage := &memstore.Store{}
23+
subject.SetReadStorage(storage)
24+
subject.SetWriteStorage(storage)
25+
26+
// Construct some test IPLD node.
27+
wantNode := fluent.MustBuildMap(basicnode.Prototype.Map, 1, func(na fluent.MapAssembler) {
28+
na.AssembleEntry("fish").AssignString("barreleye")
29+
})
30+
31+
// Encode as raw value to be used for testing LoadRaw
32+
var buf bytes.Buffer
33+
qt.Check(t, dagcbor.Encode(wantNode, &buf), qt.IsNil)
34+
wantNodeRaw := buf.Bytes()
35+
36+
// Store the test IPLD node and get link back.
37+
lctx := ipld.LinkContext{Ctx: context.TODO()}
38+
gotLink, err := subject.Store(lctx, cidlink.LinkPrototype{
39+
Prefix: cid.Prefix{
40+
Version: 1,
41+
Codec: uint64(multicodec.DagCbor),
42+
MhType: uint64(multicodec.Sha2_256),
43+
MhLength: -1,
44+
},
45+
}, wantNode)
46+
qt.Check(t, err, qt.IsNil)
47+
gotCidlink := gotLink.(cidlink.Link)
48+
49+
// Assert all load variations return expected values for different link representations.
50+
for _, test := range []struct {
51+
name string
52+
link datamodel.Link
53+
}{
54+
{"datamodel.Link", gotLink},
55+
{"cidlink.Link", gotCidlink},
56+
{"&cidlink.Link", &gotCidlink},
57+
} {
58+
t.Run(test.name, func(t *testing.T) {
59+
gotNode, err := subject.Load(lctx, test.link, basicnode.Prototype.Any)
60+
qt.Check(t, err, qt.IsNil)
61+
qt.Check(t, ipld.DeepEqual(wantNode, gotNode), qt.IsTrue)
62+
63+
gotNodeRaw, err := subject.LoadRaw(lctx, test.link)
64+
qt.Check(t, err, qt.IsNil)
65+
qt.Check(t, bytes.Equal(wantNodeRaw, gotNodeRaw), qt.IsTrue)
66+
67+
gotNode, gotNodeRaw, err = subject.LoadPlusRaw(lctx, test.link, basicnode.Prototype.Any)
68+
qt.Check(t, err, qt.IsNil)
69+
qt.Check(t, ipld.DeepEqual(wantNode, gotNode), qt.IsTrue)
70+
qt.Check(t, bytes.Equal(wantNodeRaw, gotNodeRaw), qt.IsTrue)
71+
})
72+
}
73+
}

0 commit comments

Comments
 (0)