From 9a5dc6223bab0e1061b66b49757c2418c47b9f29 Mon Sep 17 00:00:00 2001 From: Ben Laurie Date: Mon, 16 Apr 2018 18:24:28 +0100 Subject: [PATCH] Example of dealing with server skew. (#995) --- client/log_client.go | 39 ++++---- integration/log.go | 37 +++++-- quota/etcd/quotapb/quotapb.pb.go | 4 +- quota/etcd/quotapb/quotapb.pb.gw.go | 22 +++-- quota/etcd/storagepb/storagepb.pb.go | 4 +- server/log_rpc_server.go | 101 ++++++++++++++----- server/log_rpc_server_test.go | 143 ++++++++++++++++++++++++++- server/map_rpc_server.go | 1 + storage/map_storage.go | 2 +- 9 files changed, 289 insertions(+), 64 deletions(-) diff --git a/client/log_client.go b/client/log_client.go index 0a5f17278b..f470d7a37e 100644 --- a/client/log_client.go +++ b/client/log_client.go @@ -18,7 +18,6 @@ package client import ( "bytes" "context" - "errors" "fmt" "time" @@ -222,19 +221,18 @@ func (c *LogClient) WaitForInclusion(ctx context.Context, data []byte) error { return err } for { - err = c.getAndVerifyInclusionProof(ctx, leaf.MerkleLeafHash, root) - switch status.Code(err) { - case codes.OK: + ok, err := c.getAndVerifyInclusionProof(ctx, leaf.MerkleLeafHash, root) + if err != nil && status.Code(err) != codes.NotFound { + return err + } + if ok { return nil - case codes.NotFound: - // Wait for TreeSize to update. - if root, err = c.WaitForRootUpdate(ctx, root.TreeSize+1); err != nil { - return err - } - // Retry - default: + } + // Wait for TreeSize to update. + if root, err = c.WaitForRootUpdate(ctx, root.TreeSize+1); err != nil { return err } + // Retry } } @@ -248,7 +246,14 @@ func (c *LogClient) VerifyInclusion(ctx context.Context, data []byte) error { if err != nil { return fmt.Errorf("UpdateRoot(): %v", err) } - return c.getAndVerifyInclusionProof(ctx, leaf.MerkleLeafHash, root) + ok, err := c.getAndVerifyInclusionProof(ctx, leaf.MerkleLeafHash, root) + if err != nil { + return err + } + if !ok { + return fmt.Errorf("no proof") + } + return nil } // GetAndVerifyInclusionAtIndex updates the log root and ensures that the given leaf data has been included in the log at a particular index. @@ -269,7 +274,7 @@ func (c *LogClient) GetAndVerifyInclusionAtIndex(ctx context.Context, data []byt return c.VerifyInclusionAtIndex(root, data, index, resp.Proof.Hashes) } -func (c *LogClient) getAndVerifyInclusionProof(ctx context.Context, leafHash []byte, sth *types.LogRootV1) error { +func (c *LogClient) getAndVerifyInclusionProof(ctx context.Context, leafHash []byte, sth *types.LogRootV1) (bool, error) { resp, err := c.client.GetInclusionProofByHash(ctx, &trillian.GetInclusionProofByHashRequest{ LogId: c.LogID, @@ -277,17 +282,17 @@ func (c *LogClient) getAndVerifyInclusionProof(ctx context.Context, leafHash []b TreeSize: int64(sth.TreeSize), }) if err != nil { - return err + return false, err } if len(resp.Proof) < 1 { - return errors.New("no inclusion proof supplied") + return false, nil } for _, proof := range resp.Proof { if err := c.VerifyInclusionByHash(sth, leafHash, proof); err != nil { - return fmt.Errorf("VerifyInclusionByHash(): %v", err) + return false, fmt.Errorf("VerifyInclusionByHash(): %v", err) } } - return nil + return true, nil } // QueueLeaf adds a leaf to a Trillian log without blocking. diff --git a/integration/log.go b/integration/log.go index 3fd29004c6..89e2cd5375 100644 --- a/integration/log.go +++ b/integration/log.go @@ -404,20 +404,34 @@ func checkInclusionProofLeafOutOfRange(logID int64, client trillian.TrillianLogC } // checkInclusionProofTreeSizeOutOfRange requests an inclusion proof for a leaf within the tree size at -// a tree size larger than the current tree size. This should fail. +// a tree size larger than the current tree size. This should succeed but with an STH for the current +// tree and an empty proof, because it is a result of skew. func checkInclusionProofTreeSizeOutOfRange(logID int64, client trillian.TrillianLogClient, params TestParameters) error { // Test is an in range leaf index for a tree size that doesn't exist ctx, cancel := getRPCDeadlineContext(params) - proof, err := client.GetInclusionProof(ctx, &trillian.GetInclusionProofRequest{ + req := &trillian.GetInclusionProofRequest{ LogId: logID, LeafIndex: int64(params.sequencerBatchSize), TreeSize: params.leafCount + int64(params.sequencerBatchSize), - }) + } + proof, err := client.GetInclusionProof(ctx, req) cancel() + if err != nil { + return fmt.Errorf("log returned error for tree size outside tree: %d v %d: %v", params.leafCount, req.TreeSize, err) + } - if err == nil { - return fmt.Errorf("log returned proof for tree size outside tree: %d v %d: %v", params.sequencerBatchSize, params.leafCount+int64(params.sequencerBatchSize), proof) + var root types.LogRootV1 + if err := root.UnmarshalBinary(proof.SignedLogRoot.LogRoot); err != nil { + return fmt.Errorf("could not read current log root: %v", err) + } + + if proof.Proof != nil { + return fmt.Errorf("log returned proof for tree size outside tree: %d v %d: %v", params.leafCount, req.TreeSize, proof) } + if int64(root.TreeSize) >= req.TreeSize { + return fmt.Errorf("log returned bad root for tree size outside tree: %d v %d: %v", params.leafCount, req.TreeSize, proof) + } + return nil } @@ -468,11 +482,22 @@ func checkConsistencyProof(consistParams consistencyProofParams, treeID int64, t } resp, err := client.GetConsistencyProof(ctx, req) cancel() - if err != nil { return fmt.Errorf("GetConsistencyProof(%v) = %v %v", consistParams, err, resp) } + if resp.SignedLogRoot == nil || resp.SignedLogRoot.LogRoot == nil { + return fmt.Errorf("received invalid response: %v", resp) + } + var root types.LogRootV1 + if err := root.UnmarshalBinary(resp.SignedLogRoot.LogRoot); err != nil { + return fmt.Errorf("could not read current log root: %v", err) + } + + if req.SecondTreeSize > int64(root.TreeSize) { + return fmt.Errorf("requested tree size %d > available tree size %d", req.SecondTreeSize, root.TreeSize) + } + verifier := merkle.NewLogVerifier(rfc6962.DefaultHasher) root1 := tree.RootAtSnapshot(req.FirstTreeSize).Hash() root2 := tree.RootAtSnapshot(req.SecondTreeSize).Hash() diff --git a/quota/etcd/quotapb/quotapb.pb.go b/quota/etcd/quotapb/quotapb.pb.go index 0c8fdeccd2..7987c66c2f 100644 --- a/quota/etcd/quotapb/quotapb.pb.go +++ b/quota/etcd/quotapb/quotapb.pb.go @@ -142,7 +142,9 @@ func (m *Config) String() string { return proto.CompactTextString(m) func (*Config) ProtoMessage() {} func (*Config) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } -type isConfig_ReplenishmentStrategy interface{ isConfig_ReplenishmentStrategy() } +type isConfig_ReplenishmentStrategy interface { + isConfig_ReplenishmentStrategy() +} type Config_SequencingBased struct { SequencingBased *SequencingBasedStrategy `protobuf:"bytes,4,opt,name=sequencing_based,json=sequencingBased,oneof"` diff --git a/quota/etcd/quotapb/quotapb.pb.gw.go b/quota/etcd/quotapb/quotapb.pb.gw.go index d9f402960d..4e9273cb7a 100644 --- a/quota/etcd/quotapb/quotapb.pb.gw.go +++ b/quota/etcd/quotapb/quotapb.pb.gw.go @@ -32,8 +32,10 @@ func request_Quota_CreateConfig_0(ctx context.Context, marshaler runtime.Marshal var protoReq CreateConfigRequest var metadata runtime.ServerMetadata - if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + if req.ContentLength > 0 { + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } } var ( @@ -134,8 +136,10 @@ func request_Quota_UpdateConfig_0(ctx context.Context, marshaler runtime.Marshal var protoReq UpdateConfigRequest var metadata runtime.ServerMetadata - if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + if req.ContentLength > 0 { + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } } var ( @@ -200,7 +204,7 @@ func RegisterQuotaHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc func RegisterQuotaHandlerClient(ctx context.Context, mux *runtime.ServeMux, client QuotaClient) error { mux.Handle("POST", pattern_Quota_CreateConfig_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(ctx) + ctx, cancel := context.WithCancel(req.Context()) defer cancel() if cn, ok := w.(http.CloseNotifier); ok { go func(done <-chan struct{}, closed <-chan bool) { @@ -229,7 +233,7 @@ func RegisterQuotaHandlerClient(ctx context.Context, mux *runtime.ServeMux, clie }) mux.Handle("DELETE", pattern_Quota_DeleteConfig_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(ctx) + ctx, cancel := context.WithCancel(req.Context()) defer cancel() if cn, ok := w.(http.CloseNotifier); ok { go func(done <-chan struct{}, closed <-chan bool) { @@ -258,7 +262,7 @@ func RegisterQuotaHandlerClient(ctx context.Context, mux *runtime.ServeMux, clie }) mux.Handle("GET", pattern_Quota_GetConfig_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(ctx) + ctx, cancel := context.WithCancel(req.Context()) defer cancel() if cn, ok := w.(http.CloseNotifier); ok { go func(done <-chan struct{}, closed <-chan bool) { @@ -287,7 +291,7 @@ func RegisterQuotaHandlerClient(ctx context.Context, mux *runtime.ServeMux, clie }) mux.Handle("GET", pattern_Quota_ListConfigs_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(ctx) + ctx, cancel := context.WithCancel(req.Context()) defer cancel() if cn, ok := w.(http.CloseNotifier); ok { go func(done <-chan struct{}, closed <-chan bool) { @@ -316,7 +320,7 @@ func RegisterQuotaHandlerClient(ctx context.Context, mux *runtime.ServeMux, clie }) mux.Handle("PATCH", pattern_Quota_UpdateConfig_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(ctx) + ctx, cancel := context.WithCancel(req.Context()) defer cancel() if cn, ok := w.(http.CloseNotifier); ok { go func(done <-chan struct{}, closed <-chan bool) { diff --git a/quota/etcd/storagepb/storagepb.pb.go b/quota/etcd/storagepb/storagepb.pb.go index 4ecc66ffc4..78a7d65b6c 100644 --- a/quota/etcd/storagepb/storagepb.pb.go +++ b/quota/etcd/storagepb/storagepb.pb.go @@ -130,7 +130,9 @@ func (m *Config) String() string { return proto.CompactTextString(m) func (*Config) ProtoMessage() {} func (*Config) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} } -type isConfig_ReplenishmentStrategy interface{ isConfig_ReplenishmentStrategy() } +type isConfig_ReplenishmentStrategy interface { + isConfig_ReplenishmentStrategy() +} type Config_SequencingBased struct { SequencingBased *SequencingBasedStrategy `protobuf:"bytes,4,opt,name=sequencing_based,json=sequencingBased,oneof"` diff --git a/server/log_rpc_server.go b/server/log_rpc_server.go index 4fa6784b68..bd05dc77e2 100644 --- a/server/log_rpc_server.go +++ b/server/log_rpc_server.go @@ -226,6 +226,12 @@ func (t *TrillianLogRPCServer) GetInclusionProof(ctx context.Context, req *trill return nil, status.Errorf(codes.Internal, "Could not read current log root: %v", err) } + r := &trillian.GetInclusionProofResponse{SignedLogRoot: &slr} + + if uint64(req.TreeSize) > root.TreeSize { + return r, nil + } + proof, err := getInclusionProofForLeafIndex(ctx, tx, hasher, req.TreeSize, req.LeafIndex, int64(root.TreeSize)) if err != nil { return nil, err @@ -235,7 +241,9 @@ func (t *TrillianLogRPCServer) GetInclusionProof(ctx context.Context, req *trill return nil, err } - return &trillian.GetInclusionProofResponse{Proof: &proof}, nil + r.Proof = &proof + + return r, nil } // GetInclusionProofByHash obtains proofs of inclusion by leaf hash. Because some logs can @@ -279,6 +287,12 @@ func (t *TrillianLogRPCServer) GetInclusionProofByHash(ctx context.Context, req return nil, status.Errorf(codes.Internal, "Could not read current log root: %v", err) } + r := &trillian.GetInclusionProofByHashResponse{SignedLogRoot: &slr} + + if len(leaves) < 1 { + return r, nil + } + // TODO(Martin2112): Need to define a limit on number of results or some form of paging etc. proofs := make([]*trillian.Proof, 0, len(leaves)) for _, leaf := range leaves { @@ -293,9 +307,8 @@ func (t *TrillianLogRPCServer) GetInclusionProofByHash(ctx context.Context, req return nil, err } - return &trillian.GetInclusionProofByHashResponse{ - Proof: proofs, - }, nil + r.Proof = proofs + return r, nil } // GetConsistencyProof obtains a proof that two versions of the tree are consistent with each @@ -327,6 +340,11 @@ func (t *TrillianLogRPCServer) GetConsistencyProof(ctx context.Context, req *tri if err := root.UnmarshalBinary(slr.LogRoot); err != nil { return nil, status.Errorf(codes.Internal, "Could not read current log root: %v", err) } + r := &trillian.GetConsistencyProofResponse{SignedLogRoot: &slr} + + if uint64(req.SecondTreeSize) > root.TreeSize { + return r, nil + } nodeFetches, err := merkle.CalcConsistencyProofNodeAddresses(req.FirstTreeSize, req.SecondTreeSize, int64(root.TreeSize), proofMaxBitLen) if err != nil { @@ -345,7 +363,8 @@ func (t *TrillianLogRPCServer) GetConsistencyProof(ctx context.Context, req *tri } // We have everything we need. Return the proof - return &trillian.GetConsistencyProofResponse{Proof: &proof}, nil + r.Proof = &proof + return r, nil } // GetLatestSignedLogRoot obtains the latest published tree root for the Merkle Tree that @@ -426,7 +445,12 @@ func (t *TrillianLogRPCServer) GetLeavesByIndex(ctx context.Context, req *trilli return nil, err } - return &trillian.GetLeavesByIndexResponse{Leaves: leaves}, nil + root, err := tx.LatestSignedLogRoot(ctx) + if err != nil { + return nil, err + } + + return &trillian.GetLeavesByIndexResponse{Leaves: leaves, SignedLogRoot: &root}, nil } // GetLeavesByRange obtains leaves based on a range of sequence numbers within the tree. @@ -447,16 +471,30 @@ func (t *TrillianLogRPCServer) GetLeavesByRange(ctx context.Context, req *trilli } defer tx.Close() - leaves, err := tx.GetLeavesByRange(ctx, req.StartIndex, req.Count) + slr, err := tx.LatestSignedLogRoot(ctx) if err != nil { return nil, err } + var root types.LogRootV1 + if err := root.UnmarshalBinary(slr.LogRoot); err != nil { + return nil, status.Errorf(codes.Internal, "Could not read current log root: %v", err) + } + + r := &trillian.GetLeavesByRangeResponse{SignedLogRoot: &slr} + + if req.StartIndex < int64(root.TreeSize) { + leaves, err := tx.GetLeavesByRange(ctx, req.StartIndex, req.Count) + if err != nil { + return nil, err + } + r.Leaves = leaves + } if err := t.commitAndLog(ctx, req.LogId, tx, "GetLeavesByRange"); err != nil { return nil, err } - return &trillian.GetLeavesByRangeResponse{Leaves: leaves}, nil + return r, nil } // GetLeavesByHash obtains one or more leaves based on their tree hash. It is not possible @@ -482,12 +520,18 @@ func (t *TrillianLogRPCServer) GetLeavesByHash(ctx context.Context, req *trillia return nil, err } + root, err := tx.LatestSignedLogRoot(ctx) + if err != nil { + return nil, err + } + if err := t.commitAndLog(ctx, req.LogId, tx, "GetLeavesByHash"); err != nil { return nil, err } return &trillian.GetLeavesByHashResponse{ - Leaves: leaves, + Leaves: leaves, + SignedLogRoot: &root, }, nil } @@ -522,30 +566,39 @@ func (t *TrillianLogRPCServer) GetEntryAndProof(ctx context.Context, req *trilli return nil, status.Errorf(codes.Internal, "Could not read current log root: %v", err) } - proof, err := getInclusionProofForLeafIndex(ctx, tx, hasher, req.TreeSize, req.LeafIndex, int64(root.TreeSize)) - if err != nil { - return nil, err - } + r := &trillian.GetEntryAndProofResponse{SignedLogRoot: &slr} - // We also need the leaf entry - leaves, err := tx.GetLeavesByIndex(ctx, []int64{req.LeafIndex}) - if err != nil { - return nil, err + if req.TreeSize > int64(root.TreeSize) && req.LeafIndex < int64(root.TreeSize) { + // return latest proof we can manage + req.TreeSize = int64(root.TreeSize) } - if len(leaves) != 1 { - return nil, status.Errorf(codes.Internal, "expected one leaf from storage but got: %d", len(leaves)) + if req.TreeSize <= int64(root.TreeSize) { + proof, err := getInclusionProofForLeafIndex(ctx, tx, hasher, req.TreeSize, req.LeafIndex, int64(root.TreeSize)) + if err != nil { + return nil, err + } + + // We also need the leaf entry + leaves, err := tx.GetLeavesByIndex(ctx, []int64{req.LeafIndex}) + if err != nil { + return nil, err + } + + if len(leaves) != 1 { + return nil, status.Errorf(codes.Internal, "expected one leaf from storage but got: %d", len(leaves)) + } + + // Work is complete, we have everything we need for the response + r.Proof = &proof + r.Leaf = leaves[0] } if err := tx.Commit(); err != nil { return nil, err } - // Work is complete, we have everything we need for the response - return &trillian.GetEntryAndProofResponse{ - Proof: &proof, - Leaf: leaves[0], - }, nil + return r, nil } func (t *TrillianLogRPCServer) commitAndLog(ctx context.Context, logID int64, tx storage.ReadOnlyLogTreeTX, op string) error { diff --git a/server/log_rpc_server_test.go b/server/log_rpc_server_test.go index f27392e5f4..6396e059fc 100644 --- a/server/log_rpc_server_test.go +++ b/server/log_rpc_server_test.go @@ -87,8 +87,10 @@ var ( getInclusionProofByIndexRequest7 = trillian.GetInclusionProofRequest{LogId: logID1, TreeSize: 7, LeafIndex: 2} getInclusionProofByIndexRequest25 = trillian.GetInclusionProofRequest{LogId: logID1, TreeSize: 50, LeafIndex: 25} - getEntryAndProofRequest17 = trillian.GetEntryAndProofRequest{LogId: logID1, TreeSize: 17, LeafIndex: 3} - getEntryAndProofRequest7 = trillian.GetEntryAndProofRequest{LogId: logID1, TreeSize: 7, LeafIndex: 2} + getEntryAndProofRequest17 = trillian.GetEntryAndProofRequest{LogId: logID1, TreeSize: 17, LeafIndex: 3} + getEntryAndProofRequest17_2 = trillian.GetEntryAndProofRequest{LogId: logID1, TreeSize: 17, LeafIndex: 2} + getEntryAndProofRequest17_11 = trillian.GetEntryAndProofRequest{LogId: logID1, TreeSize: 17, LeafIndex: 11} + getEntryAndProofRequest7 = trillian.GetEntryAndProofRequest{LogId: logID1, TreeSize: 7, LeafIndex: 2} getConsistencyProofRequest7 = trillian.GetConsistencyProofRequest{LogId: logID1, FirstTreeSize: 4, SecondTreeSize: 7} getConsistencyProofRequest44 = trillian.GetConsistencyProofRequest{LogId: logID1, FirstTreeSize: 4, SecondTreeSize: 4} @@ -174,6 +176,7 @@ func TestGetLeavesByIndex(t *testing.T) { mockTX := storage.NewMockLogTreeTX(ctrl) fakeStorage.EXPECT().SnapshotForTree(gomock.Any(), tree1).Return(mockTX, nil) mockTX.EXPECT().GetLeavesByIndex(gomock.Any(), []int64{0}).Return([]*trillian.LogLeaf{leaf1}, nil) + mockTX.EXPECT().LatestSignedLogRoot(gomock.Any()).Return(*signedRoot1, nil) mockTX.EXPECT().Commit().Return(nil) mockTX.EXPECT().Close().Return(nil) mockTX.EXPECT().IsOpen().AnyTimes().Return(false) @@ -202,6 +205,7 @@ func TestGetLeavesByIndexMultiple(t *testing.T) { mockTX := storage.NewMockLogTreeTX(ctrl) fakeStorage.EXPECT().SnapshotForTree(gomock.Any(), tree1).Return(mockTX, nil) mockTX.EXPECT().GetLeavesByIndex(gomock.Any(), []int64{0, 3}).Return([]*trillian.LogLeaf{leaf1, leaf3}, nil) + mockTX.EXPECT().LatestSignedLogRoot(gomock.Any()).Return(*signedRoot1, nil) mockTX.EXPECT().Commit().Return(nil) mockTX.EXPECT().Close().Return(nil) mockTX.EXPECT().IsOpen().AnyTimes().Return(false) @@ -313,6 +317,7 @@ func TestGetLeavesByRange(t *testing.T) { fakeStorage.EXPECT().SnapshotForTree(gomock.Any(), tree).Return(nil, test.txErr) } else { fakeStorage.EXPECT().SnapshotForTree(gomock.Any(), tree).Return(mockTX, nil) + mockTX.EXPECT().LatestSignedLogRoot(gomock.Any()).Return(*signedRoot1, nil) if test.getErr != nil { mockTX.EXPECT().GetLeavesByRange(gomock.Any(), test.start, test.count).Return(nil, test.getErr) } else { @@ -628,6 +633,7 @@ func TestLeavesByHashCommitFails(t *testing.T) { test := newParameterizedTest(ctrl, "GetLeavesByHash", readOnly, nopStorage, func(t *storage.MockLogTreeTX) { t.EXPECT().GetLeavesByHash(gomock.Any(), [][]byte{[]byte("test"), []byte("data")}, false).Return(nil, nil) + t.EXPECT().LatestSignedLogRoot(gomock.Any()).Return(*signedRoot1, nil) }, func(s *TrillianLogRPCServer) error { _, err := s.GetLeavesByHash(context.Background(), &getByHashRequest1) @@ -659,6 +665,7 @@ func TestGetLeavesByHash(t *testing.T) { mockTX := storage.NewMockLogTreeTX(ctrl) fakeStorage.EXPECT().SnapshotForTree(gomock.Any(), tree1).Return(mockTX, nil) mockTX.EXPECT().GetLeavesByHash(gomock.Any(), [][]byte{[]byte("test"), []byte("data")}, false).Return([]*trillian.LogLeaf{leaf1, leaf3}, nil) + mockTX.EXPECT().LatestSignedLogRoot(gomock.Any()).Return(*signedRoot1, nil) mockTX.EXPECT().Commit().Return(nil) mockTX.EXPECT().Close().Return(nil) @@ -995,6 +1002,37 @@ func TestGetProofByIndex(t *testing.T) { } } +func TestGetProofByIndexBeyondSTH(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + fakeStorage := storage.NewMockLogStorage(ctrl) + mockTx := storage.NewMockLogTreeTX(ctrl) + fakeStorage.EXPECT().SnapshotForTree(gomock.Any(), tree1).Return(mockTx, nil) + + mockTx.EXPECT().LatestSignedLogRoot(gomock.Any()).Return(*signedRoot1, nil) + mockTx.EXPECT().Close().Return(nil) + + registry := extension.Registry{ + AdminStorage: fakeAdminStorage(ctrl, storageParams{treeID: getEntryAndProofRequest17.LogId, numSnapshots: 1}), + LogStorage: fakeStorage, + } + server := NewTrillianLogRPCServer(registry, fakeTimeSource) + + proofResponse, err := server.GetInclusionProof(context.Background(), &getInclusionProofByIndexRequest25) + if err != nil { + t.Fatalf("get inclusion proof by index should have succeeded but we got: %v", err) + } + + if proofResponse == nil { + t.Fatalf("server response was not successful: %v", proofResponse) + } + + if proofResponse.Proof != nil { + t.Fatalf("expected nil proof but got: %v", proofResponse.Proof) + } +} + func TestGetEntryAndProofBeginTXFails(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -1176,6 +1214,92 @@ func TestGetEntryAndProof(t *testing.T) { } } +func TestGetEntryAndProofSkewNoProof(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + fakeStorage := storage.NewMockLogStorage(ctrl) + mockTx := storage.NewMockLogTreeTX(ctrl) + fakeStorage.EXPECT().SnapshotForTree(gomock.Any(), tree1).Return(mockTx, nil) + + mockTx.EXPECT().LatestSignedLogRoot(gomock.Any()).Return(*signedRoot1, nil) + mockTx.EXPECT().Commit().Return(nil) + mockTx.EXPECT().Close().Return(nil) + + registry := extension.Registry{ + AdminStorage: fakeAdminStorage(ctrl, storageParams{treeID: getEntryAndProofRequest17_11.LogId, numSnapshots: 1}), + LogStorage: fakeStorage, + } + server := NewTrillianLogRPCServer(registry, fakeTimeSource) + + response, err := server.GetEntryAndProof(context.Background(), &getEntryAndProofRequest17_11) + if err != nil { + t.Errorf("get entry and proof should have succeeded but we got: %v", err) + } + + if response.Proof != nil { + t.Errorf("expected nil proof but got: %v", response.Proof) + } + + if response.Leaf != nil { + t.Fatalf("Expected nil leaf but got: %v", response.Leaf) + } +} + +func TestGetEntryAndProofSkewSmallerTree(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + fakeStorage := storage.NewMockLogStorage(ctrl) + mockTx := storage.NewMockLogTreeTX(ctrl) + fakeStorage.EXPECT().SnapshotForTree(gomock.Any(), tree1).Return(mockTx, nil) + + mockTx.EXPECT().LatestSignedLogRoot(gomock.Any()).Return(*signedRoot1, nil) + mockTx.EXPECT().ReadRevision().Return(signedRoot1.TreeRevision) + mockTx.EXPECT().GetMerkleNodes(gomock.Any(), revision1, nodeIdsInclusionSize7Index2).Return([]storage.Node{ + {NodeID: nodeIdsInclusionSize7Index2[0], NodeRevision: 3, Hash: []byte("nodehash0")}, + {NodeID: nodeIdsInclusionSize7Index2[1], NodeRevision: 2, Hash: []byte("nodehash1")}, + {NodeID: nodeIdsInclusionSize7Index2[2], NodeRevision: 3, Hash: []byte("nodehash2")}}, nil) + mockTx.EXPECT().GetLeavesByIndex(gomock.Any(), []int64{2}).Return([]*trillian.LogLeaf{leaf1}, nil) + mockTx.EXPECT().Commit().Return(nil) + mockTx.EXPECT().Close().Return(nil) + + registry := extension.Registry{ + AdminStorage: fakeAdminStorage(ctrl, storageParams{treeID: getEntryAndProofRequest17_2.LogId, numSnapshots: 1}), + LogStorage: fakeStorage, + } + server := NewTrillianLogRPCServer(registry, fakeTimeSource) + + response, err := server.GetEntryAndProof(context.Background(), &getEntryAndProofRequest17_2) + if err != nil { + t.Fatalf("get entry and proof should have succeeded but we got: %v", err) + } + + // Check the proof is the one we expected + expectedProof := trillian.Proof{ + LeafIndex: 2, + Hashes: [][]byte{ + []byte("nodehash0"), + []byte("nodehash1"), + []byte("nodehash2"), + }, + } + + if !proto.Equal(response.Proof, &expectedProof) { + t.Fatalf("expected proof: %v but got: %v", expectedProof, response.Proof) + } + + // Check we got the correct leaf data + if !proto.Equal(response.Leaf, leaf1) { + t.Fatalf("Expected leaf %v but got: %v", leaf1, response.Leaf) + } + + // Check we got the right signed log root + if !proto.Equal(response.SignedLogRoot, signedRoot1) { + t.Fatalf("expected root: %v got: %v", signedRoot1, response.SignedLogRoot) + } +} + func TestGetSequencedLeafCountBeginTXFails(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -1326,10 +1450,9 @@ func TestGetConsistencyProof(t *testing.T) { noCommit: true, }, { - // Ask for a proof from size 4 to 8 but the tree is only size 7. This should fail. + // Ask for a proof from size 4 to 8 but the tree is only size 7. This should succeed but with no proof. req: getConsistencyProofRequest48, - errStr: "snapshot2 8 > treeSize 7", - wantHashes: [][]byte{}, + wantHashes: nil, nodeIDs: nil, noRev: true, noCommit: true, @@ -1391,6 +1514,12 @@ func TestGetConsistencyProof(t *testing.T) { t.Errorf("GetConsistencyProof(%+v)=_,%v; want: _,nil", test.req, err) continue } + if test.wantHashes == nil { + if response.Proof != nil { + t.Errorf("GetConsistencyProof(%+v) want nil proof, got %v", test.req, response.Proof) + } + continue + } // Ensure we got the expected proof. wantProof := trillian.Proof{ LeafIndex: 0, @@ -1819,6 +1948,7 @@ func newParameterizedTest(ctrl *gomock.Controller, operation string, m txMode, p } func (p *parameterizedTest) executeCommitFailsTest(t *testing.T, logID int64) { + withRoot := false t.Helper() mockTX := storage.NewMockLogTreeTX(p.ctrl) @@ -1832,6 +1962,9 @@ func (p *parameterizedTest) executeCommitFailsTest(t *testing.T, logID int64) { } if p.mode != noTX { p.prepareTX(mockTX) + if withRoot { + mockTX.EXPECT().LatestSignedLogRoot(gomock.Any()).Return(*signedRoot1, nil) + } mockTX.EXPECT().Commit().Return(errors.New("bang")) mockTX.EXPECT().Close().Return(errors.New("bang")) mockTX.EXPECT().IsOpen().AnyTimes().Return(false) diff --git a/server/map_rpc_server.go b/server/map_rpc_server.go index 0c83ada57a..10ce4586ec 100644 --- a/server/map_rpc_server.go +++ b/server/map_rpc_server.go @@ -115,6 +115,7 @@ func (t *TrillianMapServer) getLeavesByRevision(ctx context.Context, mapID int64 inclusions := make([]*trillian.MapLeafInclusion, 0, len(indices)) found := 0 for _, index := range indices { + // FIXME: why is the leaf index constrained to the tree's hash size? They don't have to be the same size. if got, want := len(index), hasher.Size(); got != want { return nil, status.Errorf(codes.InvalidArgument, "index len(%x): %v, want %v", index, got, want) diff --git a/storage/map_storage.go b/storage/map_storage.go index 02928e58e3..ecee8803f6 100644 --- a/storage/map_storage.go +++ b/storage/map_storage.go @@ -45,7 +45,7 @@ type ReadOnlyMapTreeTX interface { // LatestSignedMapRoot returns the most recently created SignedMapRoot. LatestSignedMapRoot(ctx context.Context) (trillian.SignedMapRoot, error) - // Get retrieves the values associates with the keyHashes, if any, at the + // Get retrieves the values associated with the keyHashes, if any, at the // specified revision. // Setting revision to -1 will fetch the latest revision. // The returned array of MapLeaves will only contain entries for which values