Skip to content

Commit

Permalink
Client that submits QueueLeaf w/ integration test (#309)
Browse files Browse the repository at this point in the history
* Create a LogClient

- Repeatable test with ResetDB

* Use NewLog api in tests

* LeafIdentityHash

* Use sql.DB rather than connection strings

* Move contxt to caller
  • Loading branch information
gdbelvin authored Jan 27, 2017
1 parent b3661d2 commit b25082f
Show file tree
Hide file tree
Showing 5 changed files with 276 additions and 5 deletions.
57 changes: 57 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright 2016 Google Inc. 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 verifies responses from the Trillian log.
package client

import (
"context"
"crypto/sha256"

"github.com/google/trillian"
"google.golang.org/grpc"
)

// LogClient represents a client for a given Trillian log instance.
type LogClient struct {
LogID int64
client trillian.TrillianLogClient
}

// New returns a new LogClient.
func New(logID int64, cc *grpc.ClientConn) *LogClient {
return &LogClient{
LogID: logID,
client: trillian.NewTrillianLogClient(cc),
}
}

// AddLeaf adds leaf to the append only log. It blocks until a verifiable response is received.
func (c *LogClient) AddLeaf(ctx context.Context, data []byte) error {
hash := sha256.Sum256(data)
leaf := &trillian.LogLeaf{
LeafValue: data,
MerkleLeafHash: hash[:],
LeafIdentityHash: hash[:],
}
req := trillian.QueueLeafRequest{
LogId: c.LogID,
Leaf: leaf,
}
_, err := c.client.QueueLeaf(ctx, &req)
// TODO(gdbelvin): Get proof by hash
// TODO(gdbelvin): backoff with jitter
// TODO(gdbelvin): verify proof
return err
}
64 changes: 64 additions & 0 deletions client/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright 2016 Google Inc. 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 (
"context"
"testing"

"github.com/google/trillian/storage/mysql"
"github.com/google/trillian/testonly/integration"
)

func TestAddLeaf(t *testing.T) {
ctx := context.Background()
logID := int64(1234)
env, err := integration.NewLogEnv("client")
if err != nil {
t.Fatal(err)
}
defer env.Close()
if err := mysql.CreateTree(logID, env.DB); err != nil {
t.Errorf("Failed to create log: %v", err)
}

client := New(logID, env.ClientConn)

if err := client.AddLeaf(ctx, []byte("foo")); err != nil {
t.Errorf("Failed to add Leaf: %v", err)
}
}

func TestAddSameLeaf(t *testing.T) {
ctx := context.Background()
logID := int64(1234)
t.Skip("Submitting two leaves currently breaks")
env, err := integration.NewLogEnv("client")
if err != nil {
t.Fatal(err)
}
defer env.Close()
client := New(logID, env.ClientConn)

if err := mysql.CreateTree(logID, env.DB); err != nil {
t.Errorf("Failed to create log: %v", err)
}
if err := client.AddLeaf(ctx, []byte("foo")); err != nil {
t.Error(err)
}
if err := client.AddLeaf(ctx, []byte("foo")); err != nil {
t.Error(err)
}
}
14 changes: 10 additions & 4 deletions server/log_rpc_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,11 @@ func (t *TrillianLogRPCServer) QueueLeaf(ctx context.Context, req *trillian.Queu
LogId: req.LogId,
Leaves: []*trillian.LogLeaf{req.Leaf},
}
resp, err := t.QueueLeaves(ctx, queueReq)
_, err := t.QueueLeaves(ctx, queueReq)
if err != nil {
return nil, err
}
return nil, grpc.Errorf(codes.Code(resp.Status.StatusCode), resp.Status.Description)
return &empty.Empty{}, nil
}

// QueueLeaves submits a batch of leaves to the log for later integration into the underlying tree.
Expand All @@ -74,7 +74,11 @@ func (t *TrillianLogRPCServer) QueueLeaves(ctx context.Context, req *trillian.Qu
leaves := depointerify(req.Leaves)

if len(leaves) == 0 {
return &trillian.QueueLeavesResponse{Status: buildStatusWithDesc(trillian.TrillianApiStatusCode_ERROR, "Must queue at least one leaf")}, nil
return &trillian.QueueLeavesResponse{
Status: buildStatusWithDesc(
trillian.TrillianApiStatusCode_ERROR,
"Must queue at least one leaf"),
}, grpc.Errorf(codes.InvalidArgument, "len(leafs)=0, want > 0")
}

// TODO(al): TreeHasher must be selected based on log config.
Expand All @@ -98,7 +102,9 @@ func (t *TrillianLogRPCServer) QueueLeaves(ctx context.Context, req *trillian.Qu
return nil, err
}

return &trillian.QueueLeavesResponse{Status: buildStatus(trillian.TrillianApiStatusCode_OK)}, nil
return &trillian.QueueLeavesResponse{
Status: buildStatus(trillian.TrillianApiStatusCode_OK),
}, nil
}

// GetInclusionProof obtains the proof of inclusion in the tree for a leaf that has been sequenced.
Expand Down
2 changes: 1 addition & 1 deletion server/log_rpc_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,7 @@ func TestQueueLeavesNoLeavesRejected(t *testing.T) {

resp, err := server.QueueLeaves(context.Background(), &queueRequestEmpty)

if err != nil || resp.Status.StatusCode != trillian.TrillianApiStatusCode_ERROR {
if err == nil || resp.Status.StatusCode != trillian.TrillianApiStatusCode_ERROR {
t.Fatal("Allowed zero leaves to be queued")
}
}
Expand Down
144 changes: 144 additions & 0 deletions testonly/integration/clientserver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// Copyright 2016 Google Inc. 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 integration

import (
"database/sql"
"fmt"
"io/ioutil"
"net"
"strings"

"github.com/google/trillian"
"github.com/google/trillian/extension/builtin"
"github.com/google/trillian/server"
"github.com/google/trillian/util"
"google.golang.org/grpc"
)

const (
createSQLFile = "../storage/mysql/storage.sql"
mysqlRootURI = "root@tcp(127.0.0.1:3306)/"
)

// LogEnv is a test environment that contains both a log server and a connection to it.
type LogEnv struct {
grpcServer *grpc.Server
logServer *server.TrillianLogRPCServer
ClientConn *grpc.ClientConn
DB *sql.DB
}

// listen opens a random high numbered port for listening.
func listen() (string, net.Listener, error) {
lis, err := net.Listen("tcp", ":0")
if err != nil {
return "", nil, err
}
_, port, err := net.SplitHostPort(lis.Addr().String())
if err != nil {
return "", nil, err
}
addr := "localhost:" + port
return addr, lis, nil
}

// getTestDB drops and recreates the test database.
// Returns a database connection to the test database.
func getTestDB(testID string) (*sql.DB, error) {
var testDBURI = fmt.Sprintf("root@tcp(127.0.0.1:3306)/log_unittest_%v", testID)
builtin.MySQLURIFlag = &testDBURI

// Drop existing database.
dbRoot, err := sql.Open("mysql", mysqlRootURI)
if err != nil {
return nil, err
}
defer dbRoot.Close()
resetSQL := []string{
fmt.Sprintf("DROP DATABASE IF EXISTS log_unittest_%v;", testID),
fmt.Sprintf("CREATE DATABASE log_unittest_%v;", testID),
fmt.Sprintf("GRANT ALL ON log_unittest_%v.* TO 'log_unittest'@'localhost' IDENTIFIED BY 'zaphod';", testID),
}
for _, sql := range resetSQL {
if _, err := dbRoot.Exec(sql); err != nil {
return nil, err
}
}

// Create new database.
dbTest, err := sql.Open("mysql", testDBURI)
if err != nil {
return nil, err
}
createSQL, err := ioutil.ReadFile(createSQLFile)
if err != nil {
return nil, err
}
sqlSlice := strings.Split(string(createSQL), ";\n")
// Omit the last element of the slice, since it will be "".
for _, sql := range sqlSlice[:len(sqlSlice)-1] {
if _, err := dbTest.Exec(sql); err != nil {
return nil, err
}
}

return dbTest, nil
}

// NewLogEnv creates a fresh DB, log server, and client.
// testID should be unique to each unittest package so as to allow parallel tests.
func NewLogEnv(testID string) (*LogEnv, error) {
db, err := getTestDB(testID)
if err != nil {
return nil, err
}

timesource := &util.SystemTimeSource{}
registry, err := builtin.NewDefaultExtensionRegistry()
if err != nil {
return nil, err
}

grpcServer := grpc.NewServer()
logServer := server.NewTrillianLogRPCServer(registry, timesource)
trillian.RegisterTrillianLogServer(grpcServer, logServer)

// Listen and start server.
addr, lis, err := listen()
if err != nil {
return nil, err
}
go grpcServer.Serve(lis)

// Connect to the server.
cc, err := grpc.Dial(addr, grpc.WithInsecure())
if err != nil {
return nil, err
}

return &LogEnv{
grpcServer: grpcServer,
logServer: logServer,
ClientConn: cc,
DB: db,
}, nil
}

// Close shuts down the server.
func (env *LogEnv) Close() {
env.grpcServer.Stop()
env.DB.Close()
}

0 comments on commit b25082f

Please sign in to comment.