Skip to content

Commit

Permalink
Merge pull request #49 from mhutchinson/tiles-client
Browse files Browse the repository at this point in the history
Added client for tlog-tiles

This was largely copied from serverless-log and minimally changed to
bind this to the tlog-tiles API which has a few differences from the
serverless API. The obvious changes are the URL paths, but that is
largely hidden behind the layout API. The more significant delta is
that serverless returns fully populated tiles, where tlog-tiles returns
only the leaf nodes and thus the client needs to perform some
repopulation of the internal nodes in order to get at all node hashes
for proof generation.

The test data is largely the same as the serverless-log test data in
shape, though it needed to be completely regenerated due to the above
changes. This required the posix log implementation to be ported across
to the tlog-tiles API too. I have this in a local branch and will get it
up in its own PR after this one.

This client is intended to be a minimal foundation on which we can build.
It has known inefficiencies such as only returning a single leaf from an
entry bundle, and not storing the results of computed internal hashes
within a tile. These will be addressed as I write the hammer tool that uses
this client.

Towards #66.
  • Loading branch information
mhutchinson authored Jul 17, 2024
2 parents d2fb910 + 57dd2f0 commit 18d1999
Show file tree
Hide file tree
Showing 53 changed files with 948 additions and 2 deletions.
459 changes: 459 additions & 0 deletions client/client.go

Large diffs are not rendered by default.

346 changes: 346 additions & 0 deletions client/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,346 @@
// Copyright 2024 Google LLC. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package client

import (
"bytes"
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"testing"

"github.com/transparency-dev/formats/log"
"github.com/transparency-dev/merkle/compact"
"github.com/transparency-dev/trillian-tessera/api"
"golang.org/x/mod/sumdb/note"
)

var (
testOrigin = "astra"
testLogVerifier = mustMakeVerifier("astra+cad5a3d2+AZJqeuyE/GnknsCNh1eCtDtwdAwKBddOlS8M2eI1Jt4b")
// Built using serverless/testdata/build_log.sh
testRawCheckpoints, testCheckpoints = mustLoadTestCheckpoints()
)

func mustMakeVerifier(vs string) note.Verifier {
v, err := note.NewVerifier(vs)
if err != nil {
panic(fmt.Errorf("NewVerifier(%q): %v", vs, err))
}
return v
}

func mustLoadTestCheckpoints() ([][]byte, []log.Checkpoint) {
raws, cps := make([][]byte, 0), make([]log.Checkpoint, 0)
for i := 0; ; i++ {
cpName := fmt.Sprintf("checkpoint.%d", i)
r, err := testLogFetcher(context.Background(), cpName)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
// Probably just no more checkpoints left
break
}
panic(err)
}
cp, _, _, err := log.ParseCheckpoint(r, testOrigin, testLogVerifier)
if err != nil {
panic(fmt.Errorf("ParseCheckpoint(%s): %v", cpName, err))
}
raws, cps = append(raws, r), append(cps, *cp)
}
if len(raws) == 0 {
panic("no checkpoints loaded")
}
return raws, cps
}

// testLogFetcher is a fetcher which reads from the checked-in golden test log
// data stored in ../testdata/log
func testLogFetcher(_ context.Context, p string) ([]byte, error) {
path := filepath.Join("../testdata/log", p)
return os.ReadFile(path)
}

// fetchCheckpointShim allows fetcher requests for checkpoints to be intercepted.
type fetchCheckpointShim struct {
// Checkpoints holds raw checkpoints to be returned when the fetcher is asked to retrieve a checkpoint path.
// The zero-th entry will be returned until Advance is called.
Checkpoints [][]byte
}

// Fetcher intercepts requests for the checkpoint file, returning the zero-th
// entry in the Checkpoints field. All other requests are passed through
// to the delegate fetcher.
func (f *fetchCheckpointShim) Fetcher(deleg Fetcher) Fetcher {
return func(ctx context.Context, path string) ([]byte, error) {
if strings.HasSuffix(path, "checkpoint") {
if len(f.Checkpoints) == 0 {
return nil, os.ErrNotExist
}
r := f.Checkpoints[0]
return r, nil
}
return deleg(ctx, path)
}
}

// Advance causes subsequent intercepted checkpoint requests to return
// the next entry in the Checkpoints slice.
func (f *fetchCheckpointShim) Advance() {
f.Checkpoints = f.Checkpoints[1:]
}

func TestCheckLogStateTracker(t *testing.T) {
ctx := context.Background()

for _, test := range []struct {
desc string
cpRaws [][]byte
wantCpRaws [][]byte
}{
{
desc: "Consistent",
cpRaws: [][]byte{
testRawCheckpoints[0],
testRawCheckpoints[2],
testRawCheckpoints[3],
testRawCheckpoints[5],
testRawCheckpoints[6],
testRawCheckpoints[10],
},
wantCpRaws: [][]byte{
testRawCheckpoints[0],
testRawCheckpoints[2],
testRawCheckpoints[3],
testRawCheckpoints[5],
testRawCheckpoints[6],
testRawCheckpoints[10],
},
}, {
desc: "Identical CP",
cpRaws: [][]byte{
testRawCheckpoints[0],
testRawCheckpoints[0],
testRawCheckpoints[0],
testRawCheckpoints[0],
},
wantCpRaws: [][]byte{
testRawCheckpoints[0],
testRawCheckpoints[0],
testRawCheckpoints[0],
testRawCheckpoints[0],
},
}, {
desc: "Identical CP pairs",
cpRaws: [][]byte{
testRawCheckpoints[0],
testRawCheckpoints[0],
testRawCheckpoints[5],
testRawCheckpoints[5],
},
wantCpRaws: [][]byte{
testRawCheckpoints[0],
testRawCheckpoints[0],
testRawCheckpoints[5],
testRawCheckpoints[5],
},
}, {
desc: "Out of order",
cpRaws: [][]byte{
testRawCheckpoints[5],
testRawCheckpoints[2],
testRawCheckpoints[0],
testRawCheckpoints[3],
},
wantCpRaws: [][]byte{
testRawCheckpoints[5],
testRawCheckpoints[5],
testRawCheckpoints[5],
testRawCheckpoints[5],
},
},
} {
t.Run(test.desc, func(t *testing.T) {
shim := fetchCheckpointShim{Checkpoints: test.cpRaws}
f := shim.Fetcher(testLogFetcher)
lst, err := NewLogStateTracker(ctx, f, testRawCheckpoints[0], testLogVerifier, testOrigin, UnilateralConsensus(f))
if err != nil {
t.Fatalf("NewLogStateTracker: %v", err)
}

for i := range test.cpRaws {
_, _, newCP, err := lst.Update(ctx)
if err != nil {
t.Errorf("Update %d: %v", i, err)
}
if got, want := newCP, test.wantCpRaws[i]; !bytes.Equal(got, want) {
t.Errorf("Update moved to:\n%s\nwant:\n%s", string(got), string(want))
}

shim.Advance()
}
})
}
}

func TestCheckConsistency(t *testing.T) {
ctx := context.Background()

for _, test := range []struct {
desc string
cp []log.Checkpoint
wantErr bool
}{
{
desc: "2 CP",
cp: []log.Checkpoint{
testCheckpoints[2],
testCheckpoints[5],
},
}, {
desc: "5 CP",
cp: []log.Checkpoint{
testCheckpoints[0],
testCheckpoints[2],
testCheckpoints[3],
testCheckpoints[5],
testCheckpoints[6],
},
}, {
desc: "big CPs",
cp: []log.Checkpoint{
testCheckpoints[3],
testCheckpoints[7],
testCheckpoints[8],
},
}, {
desc: "Identical CP",
cp: []log.Checkpoint{
testCheckpoints[0],
testCheckpoints[0],
testCheckpoints[0],
testCheckpoints[0],
},
}, {
desc: "Identical CP pairs",
cp: []log.Checkpoint{
testCheckpoints[0],
testCheckpoints[0],
testCheckpoints[5],
testCheckpoints[5],
},
}, {
desc: "Out of order",
cp: []log.Checkpoint{
testCheckpoints[5],
testCheckpoints[2],
testCheckpoints[0],
testCheckpoints[3],
},
}, {
desc: "no checkpoints",
cp: []log.Checkpoint{},
wantErr: true,
}, {
desc: "one checkpoint",
cp: []log.Checkpoint{
testCheckpoints[3],
},
wantErr: true,
}, {
desc: "two inconsistent CPs",
cp: []log.Checkpoint{
{
Size: 2,
Hash: []byte("This is a banana"),
},
testCheckpoints[4],
},
wantErr: true,
}, {
desc: "Inconsistent",
cp: []log.Checkpoint{
testCheckpoints[5],
testCheckpoints[2],
{
Size: 4,
Hash: []byte("This is a banana"),
},
testCheckpoints[3],
},
wantErr: true,
}, {
desc: "Inconsistent - clashing CPs",
cp: []log.Checkpoint{
{
Size: 2,
Hash: []byte("This is a banana"),
},
{
Size: 2,
Hash: []byte("This is NOT a banana"),
},
},
wantErr: true,
},
} {
t.Run(test.desc, func(t *testing.T) {
err := CheckConsistency(ctx, testLogFetcher, test.cp)
if gotErr := err != nil; gotErr != test.wantErr {
t.Fatalf("wantErr: %t, got %v", test.wantErr, err)
}
})
}
}

func TestNodeCacheHandlesInvalidRequest(t *testing.T) {
ctx := context.Background()
wantBytes := []byte("one")
f := func(_ context.Context, _, _ uint64) (*api.HashTile, error) {
return &api.HashTile{
Nodes: [][]byte{wantBytes},
}, nil
}

// Large tree, but we're emulating skew since f, above, will return a tile which only knows about 1
// leaf.
nc := newNodeCache(f, 10)

if got, err := nc.GetNode(ctx, compact.NewNodeID(0, 0)); err != nil {
t.Errorf("got %v, want no error", err)
} else if !bytes.Equal(got, wantBytes) {
t.Errorf("got %v, want %v", got, wantBytes)
}

if _, err := nc.GetNode(ctx, compact.NewNodeID(0, 1)); err == nil {
t.Error("got no error, want error because ID is out of range")
}
}

func TestHandleZeroRoot(t *testing.T) {
zeroCP := testCheckpoints[0]
if zeroCP.Size != 0 {
t.Fatal("BadData: checkpoint has non-zero size")
}
if len(zeroCP.Hash) == 0 {
t.Fatal("BadTestData: checkpoint.0 has empty root hash")
}
if _, err := NewProofBuilder(context.Background(), zeroCP, testLogFetcher); err != nil {
t.Fatalf("NewProofBuilder: %v", err)
}
}
9 changes: 7 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ require (
cloud.google.com/go/storage v1.43.0
github.com/globocom/go-buffer v1.2.2
github.com/google/go-cmp v0.6.0
github.com/transparency-dev/formats v0.0.0-20240708083310-9b0b58067af6
github.com/transparency-dev/merkle v0.0.2
google.golang.org/api v0.188.0
golang.org/x/exp v0.0.0-20220827204233-334a2380cb91
golang.org/x/mod v0.19.0
google.golang.org/api v0.188.0
google.golang.org/grpc v1.65.0
k8s.io/klog/v2 v2.130.1
)
Expand All @@ -25,6 +27,9 @@ require (
require (
cloud.google.com/go v0.115.0 // indirect
cloud.google.com/go/auth v0.7.0 // indirect
)

require (
cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect
cloud.google.com/go/compute/metadata v0.4.0 // indirect
cloud.google.com/go/iam v1.1.10 // indirect
Expand Down Expand Up @@ -54,7 +59,7 @@ require (
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/oauth2 v0.21.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sync v0.7.0
golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/time v0.5.0 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -910,6 +910,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/transparency-dev/formats v0.0.0-20240708083310-9b0b58067af6 h1:3HfNa+Pc/u/v6DGaMSzDHq9hDkhSS58qc4SEFDaeF1w=
github.com/transparency-dev/formats v0.0.0-20240708083310-9b0b58067af6/go.mod h1:D/QMvgv1kz9Q1TfUcDnUcDPsiSbtLV8q8LvTCdcvygw=
github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG81+twTK4=
github.com/transparency-dev/merkle v0.0.2/go.mod h1:pqSy+OXefQ1EDUVmAJ8MUhHB9TXGuzVAT58PqBoHz1A=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
Expand Down Expand Up @@ -1014,6 +1016,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
Expand Down
Loading

0 comments on commit 18d1999

Please sign in to comment.