Skip to content

Commit 64e1d20

Browse files
authored
Add c'tor to create ProofBuilder using only tree size (#277)
This PR adds a second c'tor for `ProofBuilder` which enables construction of a new instance based solely on the tree size. This provides a means to construct consistency proofs without having to hold both checkpoints (e.g. if you're trying to convince _someone else_ of consistency between a checkpoint _they_ hold, and a newer checkpoint _you_ hold - this is exactly what happens during the `tlog-witness` protocol). This is safe as generated proofs must be verified against checkpoint root hashes by the consumer anyway; any inconsistency will be detected at that point.
1 parent bf1265e commit 64e1d20

File tree

2 files changed

+61
-10
lines changed

2 files changed

+61
-10
lines changed

client/client.go

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -77,28 +77,35 @@ func FetchCheckpoint(ctx context.Context, f Fetcher, v note.Verifier, origin str
7777
// Since the tiles commit only to immutable nodes, the job of building proofs is slightly
7878
// more complex as proofs can touch "ephemeral" nodes, so these need to be synthesized.
7979
type ProofBuilder struct {
80-
cp log.Checkpoint
80+
treeSize uint64
8181
nodeCache nodeCache
8282
h compact.HashFn
8383
}
8484

85-
// NewProofBuilder creates a new ProofBuilder object for a given tree size.
85+
// NewProofBuilderForsize returns a new ProofBuilding for the given tree size.
86+
//
87+
// Unlike NewProofBuilder below, no correctness checking of the root hash for the given tree size is performed.
88+
func NewProofBuilderForSize(ctx context.Context, size uint64, h compact.HashFn, f Fetcher) *ProofBuilder {
89+
tf := newTileFetcher(f, size)
90+
return &ProofBuilder{
91+
treeSize: size,
92+
nodeCache: newNodeCache(tf, size),
93+
h: h,
94+
}
95+
}
96+
8697
// The returned ProofBuilder can be re-used for proofs related to a given tree size, but
8798
// it is not thread-safe and should not be accessed concurrently.
8899
func NewProofBuilder(ctx context.Context, cp log.Checkpoint, h compact.HashFn, f Fetcher) (*ProofBuilder, error) {
89-
tf := newTileFetcher(f, cp.Size)
90-
pb := &ProofBuilder{
91-
cp: cp,
92-
nodeCache: newNodeCache(tf, cp.Size),
93-
h: h,
94-
}
100+
pb := NewProofBuilderForSize(ctx, cp.Size, h, f)
101+
95102
// Can't re-create the root of a zero size checkpoint other than by convention,
96103
// so return early here in that case.
97104
if cp.Size == 0 {
98105
return pb, nil
99106
}
100107

101-
hashes, err := FetchRangeNodes(ctx, cp.Size, tf)
108+
hashes, err := FetchRangeNodes(ctx, cp.Size, pb.nodeCache.getTile)
102109
if err != nil {
103110
return nil, fmt.Errorf("failed to fetch range nodes: %w", err)
104111
}
@@ -127,7 +134,7 @@ func NewProofBuilder(ctx context.Context, cp log.Checkpoint, h compact.HashFn, f
127134
// This function uses the passed-in function to retrieve tiles containing any log tree
128135
// nodes necessary to build the proof.
129136
func (pb *ProofBuilder) InclusionProof(ctx context.Context, index uint64) ([][]byte, error) {
130-
nodes, err := proof.Inclusion(index, pb.cp.Size)
137+
nodes, err := proof.Inclusion(index, pb.treeSize)
131138
if err != nil {
132139
return nil, fmt.Errorf("failed to calculate inclusion proof node list: %w", err)
133140
}

client/client_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ import (
2525
"testing"
2626

2727
"github.com/transparency-dev/formats/log"
28+
"github.com/transparency-dev/merkle"
2829
"github.com/transparency-dev/merkle/compact"
30+
"github.com/transparency-dev/merkle/proof"
2931
"github.com/transparency-dev/merkle/rfc6962"
3032
"github.com/transparency-dev/serverless-log/api"
3133
"golang.org/x/mod/sumdb/note"
@@ -348,3 +350,45 @@ func TestHandleZeroRoot(t *testing.T) {
348350
t.Fatalf("NewProofBuilder: %v", err)
349351
}
350352
}
353+
354+
func doTestProofBuilder(t *testing.T, pb *ProofBuilder, h merkle.LogHasher) {
355+
t.Helper()
356+
for _, from := range testCheckpoints {
357+
if from.Size > pb.treeSize {
358+
return
359+
}
360+
cp, err := pb.ConsistencyProof(t.Context(), from.Size, pb.treeSize)
361+
if err != nil {
362+
t.Fatalf("pb.ConsistencyProof(%d, %d): %v", from.Size, pb.treeSize, err)
363+
}
364+
if err := proof.VerifyConsistency(h, from.Size, pb.treeSize, cp, from.Hash, testCheckpoints[pb.treeSize].Hash); err != nil {
365+
t.Fatalf("pb generated invalid consistency proof between %d and %d: %v", from.Size, pb.treeSize, err)
366+
}
367+
}
368+
for i := range pb.treeSize {
369+
leaf, err := GetLeaf(t.Context(), testLogFetcher, i)
370+
if err != nil {
371+
t.Fatalf("GetLeaf(%d): %v", i, err)
372+
}
373+
ip, err := pb.InclusionProof(t.Context(), i)
374+
if err != nil {
375+
t.Fatalf("pb.InclusionProof(%d): %v", i, err)
376+
}
377+
if err := proof.VerifyInclusion(h, i, pb.treeSize, h.HashLeaf(leaf), ip, testCheckpoints[pb.treeSize].Hash); err != nil {
378+
t.Fatalf("pb generated invalid inclusion proof for leaf %d: %v", i, err)
379+
}
380+
}
381+
}
382+
383+
func TestProofBuilder(t *testing.T) {
384+
pb, err := NewProofBuilder(t.Context(), testCheckpoints[len(testCheckpoints)-1], rfc6962.DefaultHasher.HashChildren, testLogFetcher)
385+
if err != nil {
386+
t.Fatalf("NewProofBuilder: %v", err)
387+
}
388+
doTestProofBuilder(t, pb, rfc6962.DefaultHasher)
389+
}
390+
391+
func TestProofBuilderForSize(t *testing.T) {
392+
pb := NewProofBuilderForSize(t.Context(), testCheckpoints[len(testCheckpoints)-1].Size, rfc6962.DefaultHasher.HashChildren, testLogFetcher)
393+
doTestProofBuilder(t, pb, rfc6962.DefaultHasher)
394+
}

0 commit comments

Comments
 (0)