From 559bccccd40d028e412d9f11709ded0250ba6dcd Mon Sep 17 00:00:00 2001 From: Linus Nordberg Date: Tue, 24 May 2022 23:33:38 +0200 Subject: implement primary and secondary role, for replication --- internal/db/trillian_test.go | 543 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 543 insertions(+) create mode 100644 internal/db/trillian_test.go (limited to 'internal/db/trillian_test.go') diff --git a/internal/db/trillian_test.go b/internal/db/trillian_test.go new file mode 100644 index 0000000..9ae682e --- /dev/null +++ b/internal/db/trillian_test.go @@ -0,0 +1,543 @@ +package db + +import ( + "bytes" + "context" + "fmt" + "reflect" + "testing" + "time" + + mocksTrillian "git.sigsum.org/log-go/internal/mocks/trillian" + "git.sigsum.org/sigsum-go/pkg/merkle" + "git.sigsum.org/sigsum-go/pkg/requests" + "git.sigsum.org/sigsum-go/pkg/types" + "github.com/golang/mock/gomock" + "github.com/google/trillian" + ttypes "github.com/google/trillian/types" + //"google.golang.org/grpc/codes" + //"google.golang.org/grpc/status" +) + +// TODO: Add TestAddSequencedLeaves +// TODO: Update TestAddLeaf +//func TestAddLeaf(t *testing.T) { +// req := &requests.Leaf{ +// ShardHint: 0, +// Message: merkle.Hash{}, +// Signature: types.Signature{}, +// PublicKey: types.PublicKey{}, +// DomainHint: "example.com", +// } +// for _, table := range []struct { +// description string +// req *requests.Leaf +// rsp *trillian.QueueLeafResponse +// err error +// wantErr bool +// }{ +// { +// description: "invalid: backend failure", +// req: req, +// err: fmt.Errorf("something went wrong"), +// wantErr: true, +// }, +// { +// description: "invalid: no response", +// req: req, +// wantErr: true, +// }, +// { +// description: "invalid: no queued leaf", +// req: req, +// rsp: &trillian.QueueLeafResponse{}, +// wantErr: true, +// }, +// { +// description: "invalid: leaf is already queued or included", +// req: req, +// rsp: &trillian.QueueLeafResponse{ +// QueuedLeaf: &trillian.QueuedLogLeaf{ +// Leaf: &trillian.LogLeaf{ +// LeafValue: []byte{0}, // does not matter for test +// }, +// Status: status.New(codes.AlreadyExists, "duplicate").Proto(), +// }, +// }, +// wantErr: true, +// }, +// { +// description: "valid", +// req: req, +// rsp: &trillian.QueueLeafResponse{ +// QueuedLeaf: &trillian.QueuedLogLeaf{ +// Leaf: &trillian.LogLeaf{ +// LeafValue: []byte{0}, // does not matter for test +// }, +// Status: status.New(codes.OK, "ok").Proto(), +// }, +// }, +// }, +// } { +// // Run deferred functions at the end of each iteration +// func() { +// ctrl := gomock.NewController(t) +// defer ctrl.Finish() +// grpc := mocksTrillian.NewMockTrillianLogClient(ctrl) +// grpc.EXPECT().QueueLeaf(gomock.Any(), gomock.Any()).Return(table.rsp, table.err) +// client := TrillianClient{GRPC: grpc} +// +// _, err := client.AddLeaf(context.Background(), table.req, 0) +// if got, want := err != nil, table.wantErr; got != want { +// t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err) +// } +// }() +// } +//} + +func TestGetTreeHead(t *testing.T) { + // valid root + root := &ttypes.LogRootV1{ + TreeSize: 0, + RootHash: make([]byte, merkle.HashSize), + TimestampNanos: 1622585623133599429, + } + buf, err := root.MarshalBinary() + if err != nil { + t.Fatalf("must marshal log root: %v", err) + } + // invalid root + root.RootHash = make([]byte, merkle.HashSize+1) + bufBadHash, err := root.MarshalBinary() + if err != nil { + t.Fatalf("must marshal log root: %v", err) + } + + for _, table := range []struct { + description string + rsp *trillian.GetLatestSignedLogRootResponse + err error + wantErr bool + wantTh *types.TreeHead + }{ + { + description: "invalid: backend failure", + err: fmt.Errorf("something went wrong"), + wantErr: true, + }, + { + description: "invalid: no response", + wantErr: true, + }, + { + description: "invalid: no signed log root", + rsp: &trillian.GetLatestSignedLogRootResponse{}, + wantErr: true, + }, + { + description: "invalid: no log root", + rsp: &trillian.GetLatestSignedLogRootResponse{ + SignedLogRoot: &trillian.SignedLogRoot{}, + }, + wantErr: true, + }, + { + description: "invalid: no log root: unmarshal failed", + rsp: &trillian.GetLatestSignedLogRootResponse{ + SignedLogRoot: &trillian.SignedLogRoot{ + LogRoot: buf[1:], + }, + }, + wantErr: true, + }, + { + description: "invalid: unexpected hash length", + rsp: &trillian.GetLatestSignedLogRootResponse{ + SignedLogRoot: &trillian.SignedLogRoot{ + LogRoot: bufBadHash, + }, + }, + wantErr: true, + }, + { + description: "valid", + rsp: &trillian.GetLatestSignedLogRootResponse{ + SignedLogRoot: &trillian.SignedLogRoot{ + LogRoot: buf, + }, + }, + wantTh: &types.TreeHead{ + Timestamp: 1622585623, + TreeSize: 0, + RootHash: merkle.Hash{}, + }, + }, + } { + // Run deferred functions at the end of each iteration + func() { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + grpc := mocksTrillian.NewMockTrillianLogClient(ctrl) + grpc.EXPECT().GetLatestSignedLogRoot(gomock.Any(), gomock.Any()).Return(table.rsp, table.err) + client := TrillianClient{GRPC: grpc} + + th, err := client.GetTreeHead(context.Background()) + if got, want := err != nil, table.wantErr; got != want { + t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err) + } + if err != nil { + return + } + + // we would need a clock that can be mocked to make a nicer test + now := uint64(time.Now().Unix()) + if got, wantLow, wantHigh := th.Timestamp, now-5, now+5; got < wantLow || got > wantHigh { + t.Errorf("got tree head with timestamp %d but wanted between [%d, %d] in test %q", + got, wantLow, wantHigh, table.description) + } + if got, want := th.TreeSize, table.wantTh.TreeSize; got != want { + t.Errorf("got tree head with tree size %d but wanted %d in test %q", got, want, table.description) + } + if got, want := th.RootHash[:], table.wantTh.RootHash[:]; !bytes.Equal(got, want) { + t.Errorf("got root hash %x but wanted %x in test %q", got, want, table.description) + } + }() + } +} + +func TestGetConsistencyProof(t *testing.T) { + req := &requests.ConsistencyProof{ + OldSize: 1, + NewSize: 3, + } + for _, table := range []struct { + description string + req *requests.ConsistencyProof + rsp *trillian.GetConsistencyProofResponse + err error + wantErr bool + wantProof *types.ConsistencyProof + }{ + { + description: "invalid: backend failure", + req: req, + err: fmt.Errorf("something went wrong"), + wantErr: true, + }, + { + description: "invalid: no response", + req: req, + wantErr: true, + }, + { + description: "invalid: no consistency proof", + req: req, + rsp: &trillian.GetConsistencyProofResponse{}, + wantErr: true, + }, + { + description: "invalid: not a consistency proof (1/2)", + req: req, + rsp: &trillian.GetConsistencyProofResponse{ + Proof: &trillian.Proof{ + Hashes: [][]byte{}, + }, + }, + wantErr: true, + }, + { + description: "invalid: not a consistency proof (2/2)", + req: req, + rsp: &trillian.GetConsistencyProofResponse{ + Proof: &trillian.Proof{ + Hashes: [][]byte{ + make([]byte, merkle.HashSize), + make([]byte, merkle.HashSize+1), + }, + }, + }, + wantErr: true, + }, + { + description: "valid", + req: req, + rsp: &trillian.GetConsistencyProofResponse{ + Proof: &trillian.Proof{ + Hashes: [][]byte{ + make([]byte, merkle.HashSize), + make([]byte, merkle.HashSize), + }, + }, + }, + wantProof: &types.ConsistencyProof{ + OldSize: 1, + NewSize: 3, + Path: []merkle.Hash{ + merkle.Hash{}, + merkle.Hash{}, + }, + }, + }, + } { + // Run deferred functions at the end of each iteration + func() { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + grpc := mocksTrillian.NewMockTrillianLogClient(ctrl) + grpc.EXPECT().GetConsistencyProof(gomock.Any(), gomock.Any()).Return(table.rsp, table.err) + client := TrillianClient{GRPC: grpc} + + proof, err := client.GetConsistencyProof(context.Background(), table.req) + if got, want := err != nil, table.wantErr; got != want { + t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err) + } + if err != nil { + return + } + if got, want := proof, table.wantProof; !reflect.DeepEqual(got, want) { + t.Errorf("got proof\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) + } + }() + } +} + +func TestGetInclusionProof(t *testing.T) { + req := &requests.InclusionProof{ + TreeSize: 4, + LeafHash: merkle.Hash{}, + } + for _, table := range []struct { + description string + req *requests.InclusionProof + rsp *trillian.GetInclusionProofByHashResponse + err error + wantErr bool + wantProof *types.InclusionProof + }{ + { + description: "invalid: backend failure", + req: req, + err: fmt.Errorf("something went wrong"), + wantErr: true, + }, + { + description: "invalid: no response", + req: req, + wantErr: true, + }, + { + description: "invalid: bad proof count", + req: req, + rsp: &trillian.GetInclusionProofByHashResponse{ + Proof: []*trillian.Proof{ + &trillian.Proof{}, + &trillian.Proof{}, + }, + }, + wantErr: true, + }, + { + description: "invalid: not an inclusion proof (1/2)", + req: req, + rsp: &trillian.GetInclusionProofByHashResponse{ + Proof: []*trillian.Proof{ + &trillian.Proof{ + LeafIndex: 1, + Hashes: [][]byte{}, + }, + }, + }, + wantErr: true, + }, + { + description: "invalid: not an inclusion proof (2/2)", + req: req, + rsp: &trillian.GetInclusionProofByHashResponse{ + Proof: []*trillian.Proof{ + &trillian.Proof{ + LeafIndex: 1, + Hashes: [][]byte{ + make([]byte, merkle.HashSize), + make([]byte, merkle.HashSize+1), + }, + }, + }, + }, + wantErr: true, + }, + { + description: "valid", + req: req, + rsp: &trillian.GetInclusionProofByHashResponse{ + Proof: []*trillian.Proof{ + &trillian.Proof{ + LeafIndex: 1, + Hashes: [][]byte{ + make([]byte, merkle.HashSize), + make([]byte, merkle.HashSize), + }, + }, + }, + }, + wantProof: &types.InclusionProof{ + TreeSize: 4, + LeafIndex: 1, + Path: []merkle.Hash{ + merkle.Hash{}, + merkle.Hash{}, + }, + }, + }, + } { + // Run deferred functions at the end of each iteration + func() { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + grpc := mocksTrillian.NewMockTrillianLogClient(ctrl) + grpc.EXPECT().GetInclusionProofByHash(gomock.Any(), gomock.Any()).Return(table.rsp, table.err) + client := TrillianClient{GRPC: grpc} + + proof, err := client.GetInclusionProof(context.Background(), table.req) + if got, want := err != nil, table.wantErr; got != want { + t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err) + } + if err != nil { + return + } + if got, want := proof, table.wantProof; !reflect.DeepEqual(got, want) { + t.Errorf("got proof\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) + } + }() + } +} + +func TestGetLeaves(t *testing.T) { + req := &requests.Leaves{ + StartSize: 1, + EndSize: 2, + } + firstLeaf := &types.Leaf{ + Statement: types.Statement{ + ShardHint: 0, + Checksum: merkle.Hash{}, + }, + Signature: types.Signature{}, + KeyHash: merkle.Hash{}, + } + secondLeaf := &types.Leaf{ + Statement: types.Statement{ + ShardHint: 0, + Checksum: merkle.Hash{}, + }, + Signature: types.Signature{}, + KeyHash: merkle.Hash{}, + } + + for _, table := range []struct { + description string + req *requests.Leaves + rsp *trillian.GetLeavesByRangeResponse + err error + wantErr bool + wantLeaves *types.Leaves + }{ + { + description: "invalid: backend failure", + req: req, + err: fmt.Errorf("something went wrong"), + wantErr: true, + }, + { + description: "invalid: no response", + req: req, + wantErr: true, + }, + { + description: "invalid: unexpected number of leaves", + req: req, + rsp: &trillian.GetLeavesByRangeResponse{ + Leaves: []*trillian.LogLeaf{ + &trillian.LogLeaf{ + LeafValue: firstLeaf.ToBinary(), + LeafIndex: 1, + }, + }, + }, + wantErr: true, + }, + { + description: "invalid: unexpected leaf (1/2)", + req: req, + rsp: &trillian.GetLeavesByRangeResponse{ + Leaves: []*trillian.LogLeaf{ + &trillian.LogLeaf{ + LeafValue: firstLeaf.ToBinary(), + LeafIndex: 1, + }, + &trillian.LogLeaf{ + LeafValue: secondLeaf.ToBinary(), + LeafIndex: 3, + }, + }, + }, + wantErr: true, + }, + { + description: "invalid: unexpected leaf (2/2)", + req: req, + rsp: &trillian.GetLeavesByRangeResponse{ + Leaves: []*trillian.LogLeaf{ + &trillian.LogLeaf{ + LeafValue: firstLeaf.ToBinary(), + LeafIndex: 1, + }, + &trillian.LogLeaf{ + LeafValue: secondLeaf.ToBinary()[1:], + LeafIndex: 2, + }, + }, + }, + wantErr: true, + }, + { + description: "valid", + req: req, + rsp: &trillian.GetLeavesByRangeResponse{ + Leaves: []*trillian.LogLeaf{ + &trillian.LogLeaf{ + LeafValue: firstLeaf.ToBinary(), + LeafIndex: 1, + }, + &trillian.LogLeaf{ + LeafValue: secondLeaf.ToBinary(), + LeafIndex: 2, + }, + }, + }, + wantLeaves: &types.Leaves{ + *firstLeaf, + *secondLeaf, + }, + }, + } { + // Run deferred functions at the end of each iteration + func() { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + grpc := mocksTrillian.NewMockTrillianLogClient(ctrl) + grpc.EXPECT().GetLeavesByRange(gomock.Any(), gomock.Any()).Return(table.rsp, table.err) + client := TrillianClient{GRPC: grpc} + + leaves, err := client.GetLeaves(context.Background(), table.req) + if got, want := err != nil, table.wantErr; got != want { + t.Errorf("got error %v but wanted %v in test %q: %v", got, want, table.description, err) + } + if err != nil { + return + } + if got, want := leaves, table.wantLeaves; !reflect.DeepEqual(got, want) { + t.Errorf("got leaves\n\t%v\nbut wanted\n\t%v\nin test %q", got, want, table.description) + } + }() + } +} -- cgit v1.2.3