Skip to content

Commit dc3c3f7

Browse files
committed
proof: improve tests and readme
Signed-off-by: Ignacio Hagopian <jsign.uy@gmail.com>
1 parent f3cae11 commit dc3c3f7

File tree

4 files changed

+69
-41
lines changed

4 files changed

+69
-41
lines changed

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ test:
88
go test ./... -race
99
.PHONY: test
1010

11+
install:
12+
cd ./cmd/ufsproof && go install .
13+
1114
GOLANGCI_LINT=go run github.com/golangci/golangci-lint/cmd/golangci-lint@v1.42.1
1215
lint:
1316
$(GOLANGCI_LINT) run

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ Consider the following UnixFS DAG file with a fanout factor of 3:
3131

3232

3333
Considering a verifer is asking a prover to provide a proof that it contains the corresponding block at the _file level offset_ X, the prover generates the subdag inside the green zone:
34-
- RoundIndigo nodes are internal DAG nodes that are somewhat small-ish and don't contain file data.
35-
- Square blocks are leaves that contain part of the original file data.
34+
- Roundo nodes are internal DAG nodes that are somewhat small-ish and don't contain file data.
35+
- Square nodes contain chunks of the original file data.
3636
- The indigo colored nodes are necessary nodes to make the proof verify that the target block (red) is at the specified offset.
3737

3838

@@ -67,7 +67,7 @@ Notice that if the prover has missing internal nodes of the UnixFS, then the imp
6767

6868

6969
## Proof sizes and benchmark
70-
The size of the proof should be already close to the minimal level. Notice that these proofs are pretty big for the single reason that no assumptions are made of DAG layout nor chunking. Thus internal nodes at visited levels include many children. If the fan-out factor at each level is the default-ish ones, this involves a non-negligible number of blocks, which are unavoidable to allow having these minimal assumptions.
70+
The size of the proof should be already close to the minimal level. Notice that these proofs are pretty big for the single reason that no assumptions are made of DAG layout nor chunking. Thus internal nodes at visited levels include many children. If we're able to have some extra assumptions as fixed-size chunking, then we could potentially ignore untargeted raw leaves which are the biggest in size, and only include the targeted (red) leaf node.
7171

7272
Generating and verifying proofs are mostly symmetrical operations. The current implementation is very naive and not optimized in any way. Being stricter with the spec CAR serialization block order can make the implementation faster. Probably, not a big deal unless you're generating proofs for thousands of _Cids_.
7373

@@ -78,7 +78,7 @@ The following bullets will probably be implemented soon:
7878
- [ ] CLI command wirable to `go-ipfs`. The lib already supports any `DAGService` so anything can be pluggable.
7979
- [ ] Allow strict mode proof validation; maybe it makes sense to fail faster in some cases, nbd.
8080
- [ ] CLI for validation from DealID in Filecoin network; maybe fun, but `Labels` are unverified.
81-
- [ ] Many border-case tests.
81+
- [ ] godocs
8282

8383
This is a side-project made for fun, so a priori is a hand-wavy roadmap.
8484

proof.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ func ValidateProof(ctx context.Context, root cid.Cid, offset uint64, proof []byt
3737
return false, fmt.Errorf("the root isn't the expected one")
3838
}
3939

40-
// TODO(jsign): if we assume some ordering in the CAR file we could simply have a CAR-serial walker
41-
// which would make this much faster and probably simpler in a way avoiding blockstores, etc.
42-
// For now, not have those assumptions and do a naive-ish walk.
40+
// TODO: if we assume some ordering in the CAR file we could simply have a CAR-serial walker
41+
// which would make this much faster and probably simpler in a way avoiding blockstores, etc.
42+
// For now, not have those assumptions and do a naive-ish walk.
4343
for {
4444
block, err := cr.Next()
4545
if err == io.EOF {
@@ -68,6 +68,14 @@ func CreateProof(ctx context.Context, root cid.Cid, offset uint64, dserv ipld.DA
6868
if err != nil {
6969
return nil, fmt.Errorf("get %s from dag service: %s", root, err)
7070
}
71+
fsRoot, err := unixfs.ExtractFSNode(n)
72+
if err != nil {
73+
return nil, fmt.Errorf("extracting fsnode from merkle-dag: %s", err)
74+
}
75+
if fsRoot.FileSize() < offset {
76+
return nil, fmt.Errorf("the offset is greater than the file size")
77+
}
78+
7179
proofNodes := []ipld.Node{n}
7280

7381
var currOffset uint64

proof_test.go

Lines changed: 51 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import (
88
"math/rand"
99
"testing"
1010

11+
"github.com/ipfs/go-cid"
1112
chunker "github.com/ipfs/go-ipfs-chunker"
13+
ipld "github.com/ipfs/go-ipld-format"
1214
"github.com/ipfs/go-unixfs/importer/balanced"
1315
h "github.com/ipfs/go-unixfs/importer/helpers"
1416
testu "github.com/ipfs/go-unixfs/test"
@@ -18,64 +20,79 @@ import (
1820
func TestProofVerify(t *testing.T) {
1921
t.Parallel()
2022

21-
dserv := testu.GetDAGServ()
22-
r := rand.New(rand.NewSource(22))
23-
data := make([]byte, 100000)
24-
_, err := io.ReadFull(r, data)
25-
require.NoError(t, err)
26-
in := bytes.NewReader(data)
27-
opts := testu.UseCidV1
28-
dbp := h.DagBuilderParams{
29-
Dagserv: dserv,
30-
Maxlinks: 3,
31-
CidBuilder: opts.Prefix,
32-
RawLeaves: opts.RawLeavesUsed,
33-
}
23+
ctx := context.Background()
24+
dataSize := int64(100000)
3425
chunkSize := int64(256)
35-
db, err := dbp.New(chunker.NewSizeSplitter(in, chunkSize))
36-
require.NoError(t, err)
37-
node, err := balanced.Layout(db)
38-
require.NoError(t, err)
26+
rootCid, dserv := setupData(t, dataSize, chunkSize)
3927

4028
tests := []struct {
4129
proofOffset uint64
4230
verifOffset uint64
43-
ok bool
31+
notOkVerif bool
32+
notOkProof bool
4433
}{
4534
// Correct proofs.
46-
{proofOffset: 40, verifOffset: 40, ok: true},
47-
{proofOffset: 500, verifOffset: 500, ok: true},
48-
{proofOffset: 6000, verifOffset: 6000, ok: true},
49-
{proofOffset: 70000, verifOffset: 70000, ok: true},
35+
{proofOffset: 40, verifOffset: 40},
36+
{proofOffset: 500, verifOffset: 500},
37+
{proofOffset: 6000, verifOffset: 6000},
38+
{proofOffset: 70000, verifOffset: 70000},
39+
{proofOffset: uint64(dataSize), verifOffset: uint64(dataSize)},
5040

5141
// Correct proof due to being in same block
52-
{proofOffset: 40, verifOffset: 41, ok: true},
53-
{proofOffset: 41, verifOffset: 40, ok: true},
42+
{proofOffset: 40, verifOffset: 41},
43+
{proofOffset: 41, verifOffset: 40},
5444

5545
// Indirectly correct proofs; this should work unless we change
5646
// the verification to not allow unvisited blocks; not clear if that's
5747
// entirely useful.
58-
{proofOffset: 868, verifOffset: 1124, ok: true},
48+
{proofOffset: 868, verifOffset: 1124},
5949

6050
// Definitely wrong proofs.
61-
{proofOffset: 40, verifOffset: 50000, ok: false},
62-
{proofOffset: 70000, verifOffset: 10, ok: false},
51+
{proofOffset: 40, verifOffset: 50000, notOkVerif: true},
52+
{proofOffset: 70000, verifOffset: 10, notOkVerif: true},
53+
54+
// Offset bigger than file size.
55+
{proofOffset: uint64(dataSize) + 1, verifOffset: 0, notOkProof: true},
6356
}
6457

6558
for _, test := range tests {
6659
test := test
6760
tname := fmt.Sprintf("%d %d", test.proofOffset, test.verifOffset)
68-
6961
t.Run(tname, func(t *testing.T) {
70-
ctx, cls := context.WithCancel(context.Background())
71-
defer cls()
72-
proof, err := CreateProof(ctx, node.Cid(), test.proofOffset, dserv)
73-
require.NoError(t, err)
62+
t.Parallel()
7463

75-
ok, err := ValidateProof(ctx, node.Cid(), test.verifOffset, proof)
64+
proof, err := CreateProof(ctx, rootCid, test.proofOffset, dserv)
65+
if test.notOkProof {
66+
require.Error(t, err)
67+
return
68+
}
7669
require.NoError(t, err)
7770

78-
require.Equal(t, test.ok, ok)
71+
ok, err := ValidateProof(ctx, rootCid, test.verifOffset, proof)
72+
require.NoError(t, err)
73+
require.Equal(t, !test.notOkVerif, ok)
7974
})
8075
}
8176
}
77+
78+
func setupData(t *testing.T, dataSize, chunkSize int64) (cid.Cid, ipld.DAGService) {
79+
r := rand.New(rand.NewSource(22))
80+
data := make([]byte, dataSize)
81+
_, err := io.ReadFull(r, data)
82+
require.NoError(t, err)
83+
in := bytes.NewReader(data)
84+
opts := testu.UseCidV1
85+
dserv := testu.GetDAGServ()
86+
dbp := h.DagBuilderParams{
87+
Dagserv: dserv,
88+
Maxlinks: 3,
89+
CidBuilder: opts.Prefix,
90+
RawLeaves: opts.RawLeavesUsed,
91+
}
92+
db, err := dbp.New(chunker.NewSizeSplitter(in, chunkSize))
93+
require.NoError(t, err)
94+
n, err := balanced.Layout(db)
95+
require.NoError(t, err)
96+
97+
return n.Cid(), dserv
98+
}

0 commit comments

Comments
 (0)