package cache
import (
"encoding/binary"
"fmt"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/trillian/storage/storagepb"
"github.com/google/trillian/storage/tree"
"github.com/transparency-dev/merkle/compact"
"github.com/transparency-dev/merkle/rfc6962"
"github.com/golang/mock/gomock"
)
func ancestor(id compact.NodeID, levelsUp uint) compact.NodeID {
return compact.NewNodeID(id.Level+levelsUp, id.Index>>levelsUp)
}
func toPrefix(t *testing.T, id compact.NodeID) []byte {
t.Helper()
if level := id.Level; level%8 != 0 || level > 64 {
t.Fatalf("node %+v is not aligned", id)
}
var bytes [8]byte
binary.BigEndian.PutUint64(bytes[:], id.Index<<id.Level)
return bytes[:8-id.Level/8]
}
func getSubtrees(ns *MockNodeStorage) GetSubtreesFunc {
return func(ids [][]byte) ([]*storagepb.SubtreeProto, error) {
ret := make([]*storagepb.SubtreeProto, 0, len(ids))
for _, id := range ids {
if s, err := ns.GetSubtree(id); err != nil {
return nil, err
} else if s != nil {
ret = append(ret, s)
}
}
return ret, nil
}
}
func TestCacheFillOnlyReadsSubtrees(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
m := NewMockNodeStorage(mockCtrl)
c := NewLogSubtreeCache(rfc6962.DefaultHasher)
id := compact.NewNodeID(28, 0x112233445)
for id := ancestor(id, 4); id.Level <= 64; id = ancestor(id, 8) {
prefix := toPrefix(t, id)
m.EXPECT().GetSubtree(prefix).Return(&storagepb.SubtreeProto{
Depth: logStrataDepth,
Prefix: prefix,
}, nil)
}
var ids []compact.NodeID
for id := id; id.Level < 64; id = ancestor(id, 1) {
ids = append(ids, id)
}
if _, err := c.GetNodes(ids, getSubtrees(m)); err != nil {
t.Errorf("GetNodes: %v", err)
}
}
func TestCacheGetNodesReadsSubtrees(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
m := NewMockNodeStorage(mockCtrl)
c := NewLogSubtreeCache(rfc6962.DefaultHasher)
ids := []compact.NodeID{
compact.NewNodeID(0, 0x1234),
compact.NewNodeID(0, 0x1235),
compact.NewNodeID(0, 0x4567),
compact.NewNodeID(0, 0x89ab),
compact.NewNodeID(0, 0x89ac),
compact.NewNodeID(0, 0x89ad),
}
skips := map[int]bool{1: true, 4: true, 5: true}
for i, id := range ids {
if skips[i] {
continue
}
prefix := toPrefix(t, ancestor(id, 8))
m.EXPECT().GetSubtree(prefix).Return(&storagepb.SubtreeProto{
Prefix: prefix,
}, nil)
}
if _, err := c.GetNodes(ids, getSubtrees(m)); err != nil {
t.Errorf("GetNodes: %v", err)
}
}
func TestCacheDirty(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
m := NewMockNodeStorage(mockCtrl)
c := NewLogSubtreeCache(rfc6962.DefaultHasher)
id := compact.NewNodeID(0, 12345)
expectedSetIDs := make(map[string]string)
for id := ancestor(id, 8); id.Level <= 64; id = ancestor(id, 8) {
prefix := toPrefix(t, id)
expectedSetIDs[string(prefix)] = "expected"
m.EXPECT().GetSubtree(prefix).Do(func(id []byte) {
t.Logf("read %x", id)
}).Return((*storagepb.SubtreeProto)(nil), nil)
}
store := func(trees []*storagepb.SubtreeProto) {
for _, s := range trees {
if got, want := s.Depth, int32(8); got != want {
t.Errorf("Got subtree with depth %d, expected %d for prefix %x", got, want, s.Prefix)
}
state, ok := expectedSetIDs[string(s.Prefix)]
if !ok {
t.Errorf("Unexpected write to subtree %x", s.Prefix)
}
switch state {
case "expected":
expectedSetIDs[string(s.Prefix)] = "met"
case "met":
t.Errorf("Second write to subtree %x", s.Prefix)
default:
t.Errorf("Unknown state for subtree %x: %s", s.Prefix, state)
}
t.Logf("write %x -> (%d leaves)", s.Prefix, len(s.Leaves))
}
}
var nodes []tree.Node
for id := id; id.Level < 64; id = ancestor(id, 1) {
nodes = append(nodes, tree.Node{ID: id, Hash: []byte(fmt.Sprintf("hash-%v", id))})
}
if err := c.SetNodes(nodes, getSubtrees(m)); err != nil {
t.Fatalf("SetNodes: %v", err)
}
tiles, err := c.UpdatedTiles()
if err != nil {
t.Fatalf("failed to get updated tiles: %v", err)
}
store(tiles)
for k, v := range expectedSetIDs {
switch v {
case "expected":
t.Errorf("Subtree %x remains unset", k)
case "met":
default:
t.Errorf("Unknown state for subtree %x: %s", k, v)
}
}
}
func TestRepopulateLogSubtree(t *testing.T) {
fact := compact.RangeFactory{Hash: rfc6962.DefaultHasher.HashChildren}
cr := fact.NewEmptyRange(0)
cmtStorage := storagepb.SubtreeProto{
Leaves: make(map[string][]byte),
InternalNodes: make(map[string][]byte),
Depth: 8,
}
s := storagepb.SubtreeProto{
Leaves: make(map[string][]byte),
Depth: 8,
}
for numLeaves := int64(1); numLeaves <= 256; numLeaves++ {
s.InternalNodes = make(map[string][]byte)
leaf := []byte(fmt.Sprintf("this is leaf %d", numLeaves))
leafHash := rfc6962.DefaultHasher.HashLeaf(leaf)
store := func(id compact.NodeID, hash []byte) {
if id.Level > 0 && id.Level < 8 {
_, sfx := splitID(id)
cmtStorage.InternalNodes[sfx.String()] = hash
}
}
if err := cr.Append(leafHash, store); err != nil {
t.Fatalf("merkle tree update failed: %v", err)
}
sfxKey := toSuffix(compact.NewNodeID(0, uint64(numLeaves)-1))
s.Leaves[sfxKey] = leafHash
if numLeaves == 256 {
s.InternalNodeCount = uint32(len(cmtStorage.InternalNodes))
} else {
s.InternalNodeCount = 0
}
cmtStorage.Leaves[sfxKey] = leafHash
if err := PopulateLogTile(&s, rfc6962.DefaultHasher); err != nil {
t.Fatalf("failed populating tile: %v", err)
}
if numLeaves != 256 {
if len(s.InternalNodes) != 0 {
t.Fatalf("(it %d) internal nodes should be empty but got: %v", numLeaves, s.InternalNodes)
}
} else if diff := cmp.Diff(cmtStorage.InternalNodes, s.InternalNodes); diff != "" {
t.Fatalf("(it %d) CMT/sparse internal nodes diff:\n%v", numLeaves, diff)
}
}
}
func BenchmarkRepopulateLogSubtree(b *testing.B) {
hasher := rfc6962.DefaultHasher
s := storagepb.SubtreeProto{
Leaves: make(map[string][]byte),
Depth: 8,
InternalNodeCount: 254,
}
for i := 0; i < 256; i++ {
leaf := []byte(fmt.Sprintf("leaf %d", i))
hash := hasher.HashLeaf(leaf)
s.Leaves[toSuffix(compact.NewNodeID(0, uint64(i)))] = hash
}
for n := 0; n < b.N; n++ {
if err := PopulateLogTile(&s, hasher); err != nil {
b.Fatalf("failed populating tile: %v", err)
}
}
}
func TestIdempotentWrites(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
m := NewMockNodeStorage(mockCtrl)
id := compact.NewNodeID(24, 0x12300)
subtreePrefix := toPrefix(t, ancestor(id, 8))
expectedSetIDs := make(map[string]string)
expectedSetIDs[string(subtreePrefix)] = "expected"
m.EXPECT().GetSubtree(subtreePrefix).Do(func(id []byte) {
t.Logf("read %x", id)
}).Return((*storagepb.SubtreeProto)(nil), nil)
store := func(trees []*storagepb.SubtreeProto) {
for _, s := range trees {
state, ok := expectedSetIDs[string(s.Prefix)]
if !ok {
t.Errorf("Unexpected write to subtree %x", s.Prefix)
}
switch state {
case "expected":
expectedSetIDs[string(s.Prefix)] = "met"
case "met":
t.Errorf("Second write to subtree %x", s.Prefix)
default:
t.Errorf("Unknown state for subtree %x: %s", s.Prefix, state)
}
m.EXPECT().GetSubtree(s.Prefix).AnyTimes().Do(func(id []byte) {
t.Logf("read again %x", id)
}).Return(s, nil)
t.Logf("write %x -> %#v", s.Prefix, s)
}
}
for i := 0; i < 10; i++ {
c := NewLogSubtreeCache(rfc6962.DefaultHasher)
nodes := []tree.Node{{ID: id, Hash: []byte("noodled")}}
if err := c.SetNodes(nodes, getSubtrees(m)); err != nil {
t.Fatalf("%d: failed to set node hash: %v", i, err)
}
tiles, err := c.UpdatedTiles()
if err != nil {
t.Fatalf("%d: failed to get updated tiles: %v", i, err)
}
if i > 0 && len(tiles) > 0 {
t.Fatalf("unexpected updated tiles on write attempt %d", i)
}
store(tiles)
}
for k, v := range expectedSetIDs {
switch v {
case "expected":
t.Errorf("Subtree %x remains unset", k)
case "met":
default:
t.Errorf("Unknown state for subtree %x: %s", k, v)
}
}
}