Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Static CT API entry bundle paths #178

Merged
merged 9 commits into from
Aug 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 13 additions & 14 deletions api/layout/paths.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,25 +37,24 @@ func EntriesPathForLogIndex(seq, logSize uint64) string {
return EntriesPath(seq/256, logSize)
}

// EntriesPath returns the local path for the nth entry bundle. p denotes the partial
// tile size, or 0 if the tile is complete.
func EntriesPath(n, logSize uint64) string {
// NWithSuffix returns a tiles-spec "N" path, with a partial suffix if applicable.
func NWithSuffix(l, n, logSize uint64) string {
phbnf marked this conversation as resolved.
Show resolved Hide resolved
suffix := ""
if p := partialTileSize(0, n, logSize); p > 0 {
if p := partialTileSize(l, n, logSize); p > 0 {
suffix = fmt.Sprintf(".p/%d", p)
}
return fmt.Sprintf("tile/entries%s%s", fmtN(n), suffix)
return fmt.Sprintf("%s%s", fmtN(n), suffix)
}

// EntriesPath returns the local path for the nth entry bundle. p denotes the partial
// tile size, or 0 if the tile is complete.
func EntriesPath(n, logSize uint64) string {
return fmt.Sprintf("tile/entries/%s", NWithSuffix(0, n, logSize))
}

// TilePath builds the path to the subtree tile with the given level and index in tile space.
func TilePath(tileLevel, tileIndex, logSize uint64) string {
suffix := ""
p := partialTileSize(tileLevel, tileIndex, logSize)
if p > 0 {
suffix = fmt.Sprintf(".p/%d", p)
}

return fmt.Sprintf("tile/%d%s%s", tileLevel, fmtN(tileIndex), suffix)
return fmt.Sprintf("tile/%d/%s", tileLevel, NWithSuffix(tileLevel, tileIndex, logSize))
}

// fmtN returns the "N" part of a Tiles-spec path.
Expand All @@ -67,10 +66,10 @@ func TilePath(tileLevel, tileIndex, logSize uint64) string {
//
// See https://github.com/C2SP/C2SP/blob/main/tlog-tiles.md#:~:text=index%201234067%20will%20be%20encoded%20as%20x001/x234/067
func fmtN(N uint64) string {
n := fmt.Sprintf("/%03d", N%1000)
n := fmt.Sprintf("%03d", N%1000)
N /= 1000
for N > 0 {
n = fmt.Sprintf("/x%03d%s", N%1000, n)
n = fmt.Sprintf("x%03d/%s", N%1000, n)
N /= 1000
}
return n
Expand Down
64 changes: 64 additions & 0 deletions api/layout/paths_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,70 @@ func TestTilePath(t *testing.T) {
}
}

func TestNWithSuffix(t *testing.T) {
for _, test := range []struct {
level uint64
index uint64
logSize uint64
wantPath string
}{
{
level: 0,
index: 0,
logSize: 256,
wantPath: "000",
}, {
level: 0,
index: 0,
logSize: 0,
wantPath: "000",
}, {
level: 0,
index: 0,
logSize: 255,
wantPath: "000.p/255",
}, {
level: 1,
index: 0,
logSize: math.MaxUint64,
wantPath: "000",
}, {
level: 1,
index: 0,
logSize: 256,
wantPath: "000.p/1",
}, {
level: 1,
index: 0,
logSize: 1024,
wantPath: "000.p/4",
}, {
level: 15,
index: 455667,
logSize: math.MaxUint64,
wantPath: "x455/667",
}, {
level: 3,
index: 1234567,
logSize: math.MaxUint64,
wantPath: "x001/x234/567",
}, {
level: 15,
index: 123456789,
logSize: math.MaxUint64,
wantPath: "x123/x456/789",
},
} {
desc := fmt.Sprintf("level %x index %x", test.level, test.index)
t.Run(desc, func(t *testing.T) {
gotPath := NWithSuffix(test.level, test.index, test.logSize)
if gotPath != test.wantPath {
t.Errorf("Got path %q want %q", gotPath, test.wantPath)
}
})
}
}

func TestParseTileLevelIndexWidth(t *testing.T) {
for _, test := range []struct {
pathLevel string
Expand Down
13 changes: 13 additions & 0 deletions ct_only.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ package tessera

import (
"context"
"fmt"

"github.com/transparency-dev/trillian-tessera/api/layout"
"github.com/transparency-dev/trillian-tessera/ctonly"
)

Expand Down Expand Up @@ -56,3 +58,14 @@ func convertCTEntry(e *ctonly.Entry) *Entry {

return r
}

// WithCTLayout instructs the underlying storage to use a Static CT API compatible scheme for layout.
func WithCTLayout() func(*StorageOptions) {
return func(opts *StorageOptions) {
opts.EntriesPath = ctEntriesPath
}
}

func ctEntriesPath(n, logSize uint64) string {
return fmt.Sprintf("tile/data/%s", layout.NWithSuffix(0, n, logSize))
}
64 changes: 64 additions & 0 deletions ct_only_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// 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 tessera

import (
"fmt"
"math"
"testing"
)

func TestCTEntriesPath(t *testing.T) {
for _, test := range []struct {
N uint64
logSize uint64
wantPath string
}{
{
N: 0,
logSize: 289,
wantPath: "tile/data/000",
},
{
N: 0,
logSize: 8,
wantPath: "tile/data/000.p/8",
}, {
N: 255,
logSize: 256 * 256,
wantPath: "tile/data/255",
}, {
N: 255,
logSize: 255*256 - 3,
wantPath: "tile/data/255.p/253",
}, {
N: 256,
logSize: 257 * 256,
wantPath: "tile/data/256",
}, {
N: 123456789000,
logSize: math.MaxUint64,
wantPath: "tile/data/x123/x456/x789/000",
},
} {
desc := fmt.Sprintf("N %d", test.N)
t.Run(desc, func(t *testing.T) {
gotPath := ctEntriesPath(test.N, test.logSize)
if gotPath != test.wantPath {
t.Errorf("got file %q want %q", gotPath, test.wantPath)
}
})
}
}
7 changes: 7 additions & 0 deletions log.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"time"

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

Expand All @@ -41,6 +42,9 @@ type NewCPFunc func(size uint64, hash []byte) ([]byte, error)
// ParseCPFunc is the signature of a function which knows how to verify and parse checkpoints.
type ParseCPFunc func(raw []byte) (*f_log.Checkpoint, error)

// EntriesPathFunc is the signature of a function which knows how to format entry bundle paths.
type EntriesPathFunc func(n, logSize uint64) string

// StorageOptions holds optional settings for all storage implementations.
type StorageOptions struct {
NewCP NewCPFunc
Expand All @@ -50,13 +54,16 @@ type StorageOptions struct {
BatchMaxSize uint

PushbackMaxOutstanding uint

EntriesPath EntriesPathFunc
}

// ResolveStorageOptions turns a variadic array of storage options into a StorageOptions instance.
func ResolveStorageOptions(opts ...func(*StorageOptions)) *StorageOptions {
defaults := &StorageOptions{
BatchMaxSize: DefaultBatchMaxSize,
BatchMaxAge: DefaultBatchMaxAge,
EntriesPath: layout.EntriesPath,
}
for _, opt := range opts {
opt(defaults)
Expand Down
20 changes: 11 additions & 9 deletions storage/gcp/gcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ type Storage struct {
projectID string
bucket string

newCP tessera.NewCPFunc
newCP tessera.NewCPFunc
entriesPath tessera.EntriesPathFunc

sequencer sequencer
objStore objStore
Expand Down Expand Up @@ -129,12 +130,13 @@ func New(ctx context.Context, cfg Config, opts ...func(*tessera.StorageOptions))
}

r := &Storage{
gcsClient: c,
projectID: cfg.ProjectID,
bucket: cfg.Bucket,
objStore: gcsStorage,
sequencer: seq,
newCP: opt.NewCP,
gcsClient: c,
projectID: cfg.ProjectID,
bucket: cfg.Bucket,
objStore: gcsStorage,
sequencer: seq,
newCP: opt.NewCP,
entriesPath: opt.EntriesPath,
}
r.queue = storage.NewQueue(ctx, opt.BatchMaxAge, opt.BatchMaxSize, r.sequencer.assignEntries)

Expand Down Expand Up @@ -229,7 +231,7 @@ func (s *Storage) getTiles(ctx context.Context, tileIDs []storage.TileID, logSiz
//
// Returns a wrapped os.ErrNotExist if the bundle does not exist.
func (s *Storage) getEntryBundle(ctx context.Context, bundleIndex uint64, logSize uint64) ([]byte, error) {
objName := layout.EntriesPath(bundleIndex, logSize)
objName := s.entriesPath(bundleIndex, logSize)
data, _, err := s.objStore.getObject(ctx, objName)
if err != nil {
if errors.Is(err, gcs.ErrObjectNotExist) {
Expand All @@ -245,7 +247,7 @@ func (s *Storage) getEntryBundle(ctx context.Context, bundleIndex uint64, logSiz

// setEntryBundle idempotently stores the serialised entry bundle at the location implied by the bundleIndex and treeSize.
func (s *Storage) setEntryBundle(ctx context.Context, bundleIndex uint64, logSize uint64, bundleRaw []byte) error {
objName := layout.EntriesPath(bundleIndex, logSize)
objName := s.entriesPath(bundleIndex, logSize)
// Note that setObject does an idempotent interpretation of DoesNotExist - it only
// returns an error if the named object exists _and_ contains different data to what's
// passed in here.
Expand Down
3 changes: 2 additions & 1 deletion storage/gcp/gcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,8 @@ func TestBundleRoundtrip(t *testing.T) {
ctx := context.Background()
m := newMemObjStore()
s := &Storage{
objStore: m,
objStore: m,
entriesPath: layout.EntriesPath,
}

for _, test := range []struct {
Expand Down
15 changes: 9 additions & 6 deletions storage/posix/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ type Storage struct {

curSize uint64
newCP tessera.NewCPFunc

entriesPath tessera.EntriesPathFunc
}

// NewTreeFunc is the signature of a function which receives information about newly integrated trees.
Expand All @@ -66,10 +68,11 @@ func New(ctx context.Context, path string, curTree func() (uint64, []byte, error
opt := tessera.ResolveStorageOptions(opts...)

r := &Storage{
path: path,
curSize: curSize,
curTree: curTree,
newCP: opt.NewCP,
path: path,
curSize: curSize,
curTree: curTree,
newCP: opt.NewCP,
entriesPath: opt.EntriesPath,
}
r.queue = storage.NewQueue(ctx, opt.BatchMaxAge, opt.BatchMaxSize, r.sequenceBatch)

Expand Down Expand Up @@ -124,7 +127,7 @@ func (s *Storage) Add(ctx context.Context, e *tessera.Entry) (uint64, error) {

// GetEntryBundle retrieves the Nth entries bundle for a log of the given size.
func (s *Storage) GetEntryBundle(ctx context.Context, index, logSize uint64) ([]byte, error) {
return os.ReadFile(filepath.Join(s.path, layout.EntriesPath(index, logSize)))
return os.ReadFile(filepath.Join(s.path, s.entriesPath(index, logSize)))
}

// sequenceBatch writes the entries from the provided batch into the entry bundle files of the log.
Expand Down Expand Up @@ -173,7 +176,7 @@ func (s *Storage) sequenceBatch(ctx context.Context, entries []*tessera.Entry) e
}
}
writeBundle := func(bundleIndex uint64) error {
bf := filepath.Join(s.path, layout.EntriesPath(bundleIndex, newSize))
bf := filepath.Join(s.path, s.entriesPath(bundleIndex, newSize))
if err := os.MkdirAll(filepath.Dir(bf), dirPerm); err != nil {
return fmt.Errorf("failed to make entries directory structure: %w", err)
}
Expand Down
Loading