From 25288e34ae192522e803a505599db13b6143be56 Mon Sep 17 00:00:00 2001 From: Al Cutter Date: Wed, 10 Jun 2026 15:13:43 +0000 Subject: [PATCH] Use transparency-dev/witness --- cmd/mtc/mirror/internal/handler/handler.go | 123 +------- .../mirror/internal/handler/handler_test.go | 73 ++--- .../mirror.go => handler/mirror_mux.go} | 58 ++-- cmd/mtc/mirror/internal/mirror/mirror_test.go | 83 ------ cmd/mtc/mirror/posix/main.go | 71 ++++- go.mod | 4 +- go.sum | 10 +- internal/witness/witness.go | 263 ------------------ internal/witness/witness_test.go | 249 ----------------- mirror_lifecycle.go | 45 +-- 10 files changed, 139 insertions(+), 840 deletions(-) rename cmd/mtc/mirror/internal/{mirror/mirror.go => handler/mirror_mux.go} (51%) delete mode 100644 cmd/mtc/mirror/internal/mirror/mirror_test.go delete mode 100644 internal/witness/witness.go delete mode 100644 internal/witness/witness_test.go diff --git a/cmd/mtc/mirror/internal/handler/handler.go b/cmd/mtc/mirror/internal/handler/handler.go index f431e18f2..1f5a36513 100644 --- a/cmd/mtc/mirror/internal/handler/handler.go +++ b/cmd/mtc/mirror/internal/handler/handler.go @@ -15,9 +15,6 @@ package handler import ( - "bufio" - "context" - "encoding/base64" "encoding/binary" "errors" "fmt" @@ -27,7 +24,7 @@ import ( "strings" "github.com/transparency-dev/tessera" - "github.com/transparency-dev/tessera/internal/witness" + "github.com/transparency-dev/witness/witness" ) const ( @@ -38,125 +35,15 @@ const ( maxTicketSize = 1<<16 - 1 ) -// Mirror is the interface that the handler uses to interact with the mirror's state. -type Mirror interface { - AddCheckpoint(ctx context.Context, origin string, oldSize uint64, proof [][]byte, cp []byte) ([]byte, uint64, error) - AddEntries(ctx context.Context, origin string, uploadStart, uploadEnd uint64, ticket []byte, next func() (*tessera.MirrorPackage, error)) ([]byte, error) -} - -// New returns a new http.Handler for the mirror service. -func New(m Mirror) http.Handler { +// New returns a new http.Handler for the tlog-mirror service, based on the provided mux and witness. +func New(m *MirrorMux, w *witness.Witness) http.Handler { mux := http.NewServeMux() - mux.HandleFunc("POST /add-checkpoint", addCheckpoint(m)) + mux.HandleFunc("POST /add-checkpoint", witness.NewHTTPHandler(w).AddCheckpoint) mux.HandleFunc("POST /add-entries", addEntries(m)) return mux } -func addCheckpoint(m Mirror) http.HandlerFunc { - const maxRequestBodyBytes = 64 << 10 - - return func(w http.ResponseWriter, r *http.Request) { - origin, oldSize, proof, cp, err := parseBody(http.MaxBytesReader(w, r.Body, maxRequestBodyBytes)) - if err != nil { - slog.InfoContext(r.Context(), "Invalid witness request", slog.Any("error", err.Error())) - w.WriteHeader(http.StatusBadRequest) - return - } - - sc, body, contentType, err := handleCheckpointUpdate(r.Context(), m, origin, oldSize, cp, proof) - if err != nil { - slog.InfoContext(r.Context(), "Witness update failed", slog.Any("error", err.Error())) - w.WriteHeader(http.StatusInternalServerError) - return - } - - if contentType != "" { - w.Header().Add("Content-Type", contentType) - } - w.WriteHeader(sc) - if len(body) > 0 { - if _, err := w.Write(body); err != nil { - slog.InfoContext(r.Context(), "Witness failed to write response", slog.Any("error", err.Error())) - } - } - } -} - -// handleCheckpointUpdate submits the provided checkpoint to the witness and interprets any errors which may result. -// -// Returns an appropriate HTTP status code, response body, and Content Type representing the outcome. -func handleCheckpointUpdate(ctx context.Context, m Mirror, origin string, oldSize uint64, cp []byte, proof [][]byte) (int, []byte, string, error) { - sigs, trustedSize, updateErr := m.AddCheckpoint(ctx, origin, oldSize, proof, cp) - // Finally, handle any "soft" error from the update: - if updateErr != nil { - switch { - case errors.Is(updateErr, witness.ErrCheckpointStale): - return http.StatusConflict, fmt.Appendf(nil, "%d\n", trustedSize), "text/x.tlog.size", nil - case errors.Is(updateErr, witness.ErrUnknownLog): - return http.StatusNotFound, nil, "", nil - case errors.Is(updateErr, witness.ErrNoValidSignature): - return http.StatusForbidden, nil, "", nil - case errors.Is(updateErr, witness.ErrOldSizeInvalid): - return http.StatusBadRequest, nil, "", nil - case errors.Is(updateErr, witness.ErrInvalidProof): - return http.StatusUnprocessableEntity, nil, "", nil - case errors.Is(updateErr, witness.ErrRootMismatch): - return http.StatusConflict, nil, "", nil - default: - return http.StatusInternalServerError, nil, "", updateErr - } - } - - return http.StatusOK, sigs, "", nil -} - -// parseBody reads the incoming request and parses into constituent parts. -// -// The request body MUST be a sequence of -// - a previous size line, -// - zero or more consistency proof lines, -// - and an empty line, -// - followed by a checkpoint. -func parseBody(r io.Reader) (string, uint64, [][]byte, []byte, error) { - b := bufio.NewReader(r) - sizeLine, err := b.ReadString('\n') - if err != nil { - return "", 0, nil, nil, err - } - var size uint64 - if n, err := fmt.Sscanf(strings.TrimSuffix(sizeLine, "\n"), "old %d", &size); err != nil || n != 1 { - return "", 0, nil, nil, err - } - proof := [][]byte{} - for { - l, err := b.ReadString('\n') - if err != nil { - return "", 0, nil, nil, err - } - l = strings.TrimSuffix(l, "\n") - if len(l) == 0 { - break - } - hash, err := base64.StdEncoding.DecodeString(l) - if err != nil { - return "", 0, nil, nil, err - } - proof = append(proof, hash) - } - cp, err := io.ReadAll(b) - if err != nil { - return "", 0, nil, nil, err - } - s := strings.SplitN(string(cp), "\n", 2) - if len(s) != 2 { - return "", 0, nil, nil, errors.New("invalid checkpoint") - } - - origin := s[0] - return origin, size, proof, cp, nil -} - -func addEntries(m Mirror) http.HandlerFunc { +func addEntries(m *MirrorMux) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // SPEC: The request body MUST have Content-Type of application/octet-stream ... if t := strings.ToLower(r.Header.Get("Content-Type")); t != "application/octet-stream" { diff --git a/cmd/mtc/mirror/internal/handler/handler_test.go b/cmd/mtc/mirror/internal/handler/handler_test.go index 4e6fa6ff2..e8167e8b1 100644 --- a/cmd/mtc/mirror/internal/handler/handler_test.go +++ b/cmd/mtc/mirror/internal/handler/handler_test.go @@ -17,7 +17,6 @@ package handler import ( "bytes" "context" - "encoding/base64" "encoding/binary" "fmt" "net/http" @@ -27,54 +26,6 @@ import ( "github.com/transparency-dev/tessera" ) -type mockMirror struct { - addCheckpointFunc func(ctx context.Context, logOrigin string, oldSize uint64, proof [][]byte, cp []byte) ([]byte, uint64, error) - addEntriesFunc func(ctx context.Context, logOrigin string, uploadStart, uploadEnd uint64, ticket []byte, next func() (*tessera.MirrorPackage, error)) ([]byte, error) -} - -func (m *mockMirror) AddCheckpoint(ctx context.Context, logOrigin string, oldSize uint64, proof [][]byte, cp []byte) ([]byte, uint64, error) { - if m.addCheckpointFunc != nil { - return m.addCheckpointFunc(ctx, logOrigin, oldSize, proof, cp) - } - return nil, 0, nil -} - -func (m *mockMirror) AddEntries(ctx context.Context, logOrigin string, uploadStart, uploadEnd uint64, ticket []byte, next func() (*tessera.MirrorPackage, error)) ([]byte, error) { - if m.addEntriesFunc != nil { - return m.addEntriesFunc(ctx, logOrigin, uploadStart, uploadEnd, ticket, next) - } - return []byte("— dummy-cosig\n"), nil -} - -func TestAddCheckpoint(t *testing.T) { - const cpOld = 100 - - mock := &mockMirror{ - addCheckpointFunc: func(ctx context.Context, logOrigin string, oldSize uint64, proof [][]byte, cp []byte) ([]byte, uint64, error) { - if oldSize != cpOld { - return nil, cpOld, fmt.Errorf("want oldSize %d, got %d", cpOld, oldSize) - } - if len(proof) != 1 { - return nil, cpOld, fmt.Errorf("want 1 proof hash, got %d", len(proof)) - } - return nil, 0, nil - }, - } - h := New(mock) - - cp := "example-log\n123\nSGVsbG8sIHdvcmxkIQ==\n\n" - proofHash := base64.StdEncoding.EncodeToString(make([]byte, 32)) - body := fmt.Sprintf("old %d\n%s\n\n%s", cpOld, proofHash, cp) - - req := httptest.NewRequest(http.MethodPost, "/add-checkpoint", bytes.NewBufferString(body)) - w := httptest.NewRecorder() - h.ServeHTTP(w, req) - - if w.Code != http.StatusOK { - t.Errorf("want status 200, got %d: %s", w.Code, w.Body.String()) - } -} - func TestAddEntries(t *testing.T) { const ( testOrigin = "example-log" @@ -83,11 +34,8 @@ func TestAddEntries(t *testing.T) { testUploadEnd = 110 ) - mock := &mockMirror{ - addEntriesFunc: func(ctx context.Context, logOrigin string, uploadStart, uploadEnd uint64, ticket []byte, next func() (*tessera.MirrorPackage, error)) ([]byte, error) { - if logOrigin != testOrigin { - return nil, fmt.Errorf("want logOrigin %s, got %s", testOrigin, logOrigin) - } + mock := &mockTarget{ + addEntriesFunc: func(ctx context.Context, uploadStart, uploadEnd uint64, ticket []byte, next func() (*tessera.MirrorPackage, error)) ([]byte, error) { if uploadStart != testUploadStart || uploadEnd != testUploadEnd { return nil, fmt.Errorf("want range %d-%d, got %d-%d", testUploadStart, testUploadEnd, uploadStart, uploadEnd) } @@ -110,7 +58,11 @@ func TestAddEntries(t *testing.T) { return []byte("— test-cosig\n"), nil }, } - h := New(mock) + mux := NewMirrorMux() + if err := mux.AddTarget(testOrigin, mock); err != nil { + t.Fatalf("AddTarget() failed: %v", err) + } + h := New(mux, nil) var body bytes.Buffer @@ -143,3 +95,14 @@ func TestAddEntries(t *testing.T) { t.Errorf("response does not contain expected cosignature: %s", w.Body.String()) } } + +type mockTarget struct { + addEntriesFunc func(ctx context.Context, uploadStart, uploadEnd uint64, ticket []byte, next func() (*tessera.MirrorPackage, error)) ([]byte, error) +} + +func (m *mockTarget) AddEntries(ctx context.Context, uploadStart, uploadEnd uint64, ticket []byte, next func() (*tessera.MirrorPackage, error)) ([]byte, error) { + if m.addEntriesFunc != nil { + return m.addEntriesFunc(ctx, uploadStart, uploadEnd, ticket, next) + } + return []byte("— dummy-cosig\n"), nil +} diff --git a/cmd/mtc/mirror/internal/mirror/mirror.go b/cmd/mtc/mirror/internal/handler/mirror_mux.go similarity index 51% rename from cmd/mtc/mirror/internal/mirror/mirror.go rename to cmd/mtc/mirror/internal/handler/mirror_mux.go index 4323ca6a9..ad215c926 100644 --- a/cmd/mtc/mirror/internal/mirror/mirror.go +++ b/cmd/mtc/mirror/internal/handler/mirror_mux.go @@ -12,13 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -package mirror +package handler import ( "context" "errors" + "fmt" "log/slog" - "maps" "sync" "github.com/transparency-dev/tessera" @@ -29,33 +29,33 @@ var ( ErrUnknownLog = errors.New("unknown log origin") ) -// New creates a new Mirror from the provided map of origins to mirror target. -func New(targets map[string]Target) *Mirror { - m := &Mirror{ - targets: maps.Clone(targets), +// NewMirrorMux creates a new MirrorMux from the provided map of origins to mirror targets. +func NewMirrorMux() *MirrorMux { + return &MirrorMux{ + targets: make(map[string]MirrorTarget), } - return m } -// Mirror is the backend for the tlog-mirror HTTP service. -// -// Mirror is mostly a multiplexer over the various log targets. It knows about -// the configured logs and routes requests to the appropriate target. -type Mirror struct { - lock sync.RWMutex - targets map[string]Target +// AddTarget adds a new mirror target for the given origin. +// It is an error to add a target for an origin that already has been added. +func (m *MirrorMux) AddTarget(origin string, t MirrorTarget) error { + m.mu.Lock() + defer m.mu.Unlock() + if _, ok := m.targets[origin]; ok { + return fmt.Errorf("origin %q already added", origin) + } + m.targets[origin] = t + return nil } -func (m *Mirror) AddCheckpoint(ctx context.Context, origin string, oldSize uint64, proof [][]byte, cpRaw []byte) ([]byte, uint64, error) { - t, err := m.target(origin) - if err != nil { - return nil, 0, err - } - slog.InfoContext(ctx, "AddCheckpoint", slog.String("origin", origin), slog.Uint64("old_size", oldSize), slog.Int("proof_len", len(proof)), slog.String("cp", string(cpRaw))) - return t.AddCheckpoint(ctx, oldSize, proof, cpRaw) +// MirrorMux is a backend for the tlog-mirror HTTP service that multiplexes incoming requests +// over a set of target mirrors based on the log origin. +type MirrorMux struct { + mu sync.RWMutex + targets map[string]MirrorTarget // keyed by log origin. } -func (m *Mirror) AddEntries(ctx context.Context, origin string, uploadStart, uploadEnd uint64, ticket []byte, next func() (*tessera.MirrorPackage, error)) ([]byte, error) { +func (m *MirrorMux) AddEntries(ctx context.Context, origin string, uploadStart, uploadEnd uint64, ticket []byte, next func() (*tessera.MirrorPackage, error)) ([]byte, error) { t, err := m.target(origin) if err != nil { return nil, err @@ -65,9 +65,9 @@ func (m *Mirror) AddEntries(ctx context.Context, origin string, uploadStart, upl } // target returns the target for the given origin, or ErrUnknownLog if it doesn't exist. -func (m *Mirror) target(origin string) (Target, error) { - m.lock.RLock() - defer m.lock.RUnlock() +func (m *MirrorMux) target(origin string) (MirrorTarget, error) { + m.mu.RLock() + defer m.mu.RUnlock() r, ok := m.targets[origin] if !ok { return nil, ErrUnknownLog @@ -75,10 +75,8 @@ func (m *Mirror) target(origin string) (Target, error) { return r, nil } -// Target describes the contract that a mirror target must satisfy. -type Target interface { - // AddCheckpoint is a tlog-witness. - AddCheckpoint(ctx context.Context, oldSize uint64, proof [][]byte, cpRaw []byte) ([]byte, uint64, error) +// MirrorTarget describes the contract that a mirror target must satisfy. +type MirrorTarget interface { // AddEntries adds verified consistent entries to the mirror. AddEntries(ctx context.Context, uploadStart, uploadEnd uint64, ticket []byte, next func() (*tessera.MirrorPackage, error)) ([]byte, error) -} \ No newline at end of file +} diff --git a/cmd/mtc/mirror/internal/mirror/mirror_test.go b/cmd/mtc/mirror/internal/mirror/mirror_test.go deleted file mode 100644 index 7e080b518..000000000 --- a/cmd/mtc/mirror/internal/mirror/mirror_test.go +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2026 The Tessera authors. 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 mirror - -import ( - "bytes" - "context" - "errors" - "testing" - - "github.com/transparency-dev/tessera" -) - -const ( - testLogOrigin = "example.com/log" -) - -func TestAddCheckpointUnknownLog(t *testing.T) { - ctx := t.Context() - m := New(nil) - if _, _, err := m.AddCheckpoint(ctx, "unknown-log", 0, nil, nil); !errors.Is(err, ErrUnknownLog) { - t.Errorf("AddCheckpoint() error = %v, want %v", err, ErrUnknownLog) - } -} - -func TestAddCheckpointHitsTarget(t *testing.T) { - ctx := t.Context() - dummy := &dummyTarget{} - oldSize := uint64(10) - proof := [][]byte{{1}} - cpRaw := []byte("hello") - m := New(map[string]Target{testLogOrigin: dummy}) - if _, _, err := m.AddCheckpoint(ctx, testLogOrigin, oldSize, proof, cpRaw); err != nil { - t.Errorf("AddCheckpoint() error = %v, want %v", err, nil) - } - if !dummy.calledAddCheckpoint { - t.Errorf("AddCheckpoint() was not called") - } - if dummy.addCheckpointOldSize != oldSize { - t.Errorf("AddCheckpoint() was called with oldSize %d, want %d", dummy.addCheckpointOldSize, oldSize) - } - if !bytes.Equal(dummy.addCheckpointProof[0], []byte{1}) { - t.Errorf("AddCheckpoint() was called with proof %v, want %v", dummy.addCheckpointProof, proof) - } - if !bytes.Equal(dummy.addCheckpointCpRaw, []byte("hello")) { - t.Errorf("AddCheckpoint() was called with cpRaw %v, want %v", dummy.addCheckpointCpRaw, cpRaw) - } -} - -type dummyTarget struct { - calledAddCheckpoint bool - addCheckpointOldSize uint64 - addCheckpointProof [][]byte - addCheckpointCpRaw []byte -} - -func (d *dummyTarget) AddCheckpoint(ctx context.Context, oldSize uint64, proof [][]byte, cpRaw []byte) (cosig []byte, wantSize uint64, err error) { - d.addCheckpointCpRaw = cpRaw - d.addCheckpointOldSize = oldSize - d.addCheckpointProof = proof - d.calledAddCheckpoint = true - return nil, 0, nil -} - -func (d *dummyTarget) AddEntries(ctx context.Context, uploadStart, uploadEnd uint64, ticket []byte, next func() (*tessera.MirrorPackage, error)) ([]byte, error) { - return nil, nil -} - -func (d *dummyTarget) IntegratedSize(ctx context.Context) (uint64, error) { - return 0, nil -} \ No newline at end of file diff --git a/cmd/mtc/mirror/posix/main.go b/cmd/mtc/mirror/posix/main.go index 564bc362d..dbb7e5b3e 100644 --- a/cmd/mtc/mirror/posix/main.go +++ b/cmd/mtc/mirror/posix/main.go @@ -16,14 +16,17 @@ package main import ( "context" + "errors" "flag" "net/http" "os" "log/slog" + "github.com/transparency-dev/tessera" "github.com/transparency-dev/tessera/cmd/mtc/mirror/internal/handler" - "github.com/transparency-dev/tessera/cmd/mtc/mirror/internal/mirror" + "github.com/transparency-dev/witness/witness" + "golang.org/x/mod/sumdb/note" ) var ( @@ -34,9 +37,16 @@ func main() { flag.Parse() slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, nil))) ctx := context.Background() + + w := witnessFromFlags(ctx) - m := &mirror.Mirror{} - h := handler.New(m) + mux := handler.NewMirrorMux() + if err := mux.AddTarget("example.com/log", &fakeTarget{}); err != nil { + slog.ErrorContext(ctx, "Failed to add target", slog.Any("error", err)) + os.Exit(1) + } + + h := handler.New(mux, w) slog.InfoContext(ctx, "Starting mirror service", slog.String("addr", *listenAddr)) if err := http.ListenAndServe(*listenAddr, h); err != nil { @@ -44,3 +54,58 @@ func main() { os.Exit(1) } } + +// witnessFromFlags returns a witness instance configured from the provided flags. +// Exits if the witness could not be created. +func witnessFromFlags(ctx context.Context) *witness.Witness { + w, err := witness.New(ctx, witness.Opts{ + Persistence: &fakePersistence{}, + Signers: []note.Signer{}, + VerifierForLog: func(ctx context.Context, origin string) (note.Verifier, bool, error) { + return nil, false, errors.New("unimplemented") + }, + }) + if err != nil { + slog.ErrorContext(ctx, "Failed to create witness", slog.Any("error", err)) + os.Exit(1) + } + return w +} + +// fakePersistence is a temporary witness persistence impl, and will be removed in due course. +type fakePersistence struct {} + +// Init sets up the persistence layer. This should be idempotent, +// and will be called once per process startup. +func (f *fakePersistence) Init(ctx context.Context) error { + slog.InfoContext(ctx, "fake persistence: Init") + return nil +} + +// Latest returns the latest checkpoint. +// If no checkpoint exists, it must return nil. +func (f *fakePersistence) Latest(ctx context.Context, origin string) ([]byte, error) { + slog.InfoContext(ctx, "fake persistence: Latest", slog.String("origin", origin)) + return nil, nil +} + +// Update allows for atomically updating the currently stored (if any) +// checkpoint for the given origin. +// +// The provided function will be passed the currently stored checkpoint +// for the provided log origin (or nil if no such checkpoint exists), and +// should return the serialised form of the updated checkpoint, or an +// error. +func (f *fakePersistence) Update(ctx context.Context, origin string, update func([]byte) ([]byte, error)) error { + slog.InfoContext(ctx, "fake persistence: Update", slog.String("origin", origin)) + return nil +} + +// fakeTarget is a temporary mirror target impl, and will be removed in due course. +type fakeTarget struct {} + +func (f fakeTarget) AddEntries(ctx context.Context, uploadStart, uploadEnd uint64, ticket []byte, next func() (*tessera.MirrorPackage, error)) ([]byte, error) { + slog.InfoContext(ctx, "fake target: AddEntries", slog.Uint64("uploadStart", uploadStart), slog.Uint64("uploadEnd", uploadEnd)) + return nil, nil +} + diff --git a/go.mod b/go.mod index f6ad64619..9d4b29fe2 100644 --- a/go.mod +++ b/go.mod @@ -28,7 +28,8 @@ require ( github.com/muesli/termenv v0.16.0 github.com/rivo/tview v0.42.0 github.com/transparency-dev/formats v0.1.1 - github.com/transparency-dev/merkle v0.0.2 + github.com/transparency-dev/merkle v0.0.3-0.20240919113952-3c979d16ee14 + github.com/transparency-dev/witness v0.0.0-20260611105320-36c93e78a380 go.opentelemetry.io/contrib/detectors/aws/ec2/v2 v2.5.1 go.opentelemetry.io/contrib/detectors/aws/ecs v1.44.0 go.opentelemetry.io/contrib/detectors/gcp v1.44.0 @@ -88,6 +89,7 @@ require ( go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect + k8s.io/klog/v2 v2.140.0 // indirect mvdan.cc/sh/v3 v3.7.0 // indirect ) diff --git a/go.sum b/go.sum index 9ca21ee97..4990c5e8c 100644 --- a/go.sum +++ b/go.sum @@ -211,6 +211,8 @@ github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2J github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.17 h1:78v8ZlW0bP43XfmAfPsdXcoNCelfMHsDmd/pkENfrjQ= github.com/mattn/go-runewidth v0.0.17/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8= +github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= @@ -244,8 +246,10 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/transparency-dev/formats v0.1.1 h1:4bVHJc+KdBgpA1OJD1yjI+g0i5Z1graCppTMH8lWKJI= github.com/transparency-dev/formats v0.1.1/go.mod h1:qtZ8goRuJ8FTBG9c9+Bj0rn2rUG7eG/AUTkr+Aw3jFw= -github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG81+twTK4= -github.com/transparency-dev/merkle v0.0.2/go.mod h1:pqSy+OXefQ1EDUVmAJ8MUhHB9TXGuzVAT58PqBoHz1A= +github.com/transparency-dev/merkle v0.0.3-0.20240919113952-3c979d16ee14 h1:K8JqF1HyGDXfTdDHtHe7VsIzeuFEcfLhioOXaupKB+Q= +github.com/transparency-dev/merkle v0.0.3-0.20240919113952-3c979d16ee14/go.mod h1:EoKPjljyIALg1rldsJwRQVKOJO7sLd6eUqki19ruI80= +github.com/transparency-dev/witness v0.0.0-20260611105320-36c93e78a380 h1:8q/jB5xwo+ZrbwmcnkpE3wQS5rUQcUHJVUKu1sn2P3Q= +github.com/transparency-dev/witness v0.0.0-20260611105320-36c93e78a380/go.mod h1:62TbU4g76lGcmbcISmrfA3SYSZAguofRimrw+SY/xcQ= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -404,5 +408,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= +k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= mvdan.cc/sh/v3 v3.7.0 h1:lSTjdP/1xsddtaKfGg7Myu7DnlHItd3/M2tomOcNNBg= mvdan.cc/sh/v3 v3.7.0/go.mod h1:K2gwkaesF/D7av7Kxl0HbF5kGOd2ArupNTX3X44+8l8= diff --git a/internal/witness/witness.go b/internal/witness/witness.go deleted file mode 100644 index cb6d2d18f..000000000 --- a/internal/witness/witness.go +++ /dev/null @@ -1,263 +0,0 @@ -// Copyright 2021 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. -// - -// This file contains an implementation of the tlog-witness server logic. -// Large parts of this come from github.com/transparency-dev/witness. - -package witness - -import ( - "bytes" - "context" - "encoding/base64" - "encoding/binary" - "errors" - "fmt" - "log/slog" - "strings" - "unicode" - "unicode/utf8" - - "github.com/transparency-dev/formats/log" - "github.com/transparency-dev/merkle/proof" - "github.com/transparency-dev/merkle/rfc6962" - "go.opentelemetry.io/otel/trace" - "golang.org/x/mod/sumdb/note" - - iotel "github.com/transparency-dev/tessera/internal/otel" -) - -var ( - // ErrNoValidSignature is returned by calls to Update if the provided checkpoint has no valid signature by the expected key. - ErrNoValidSignature = errors.New("no valid signatures") - // ErrUnknownLog is returned by calls to Update if the provided checkpoint carries an Origin which is unknown to the - // witness. - ErrUnknownLog = errors.New("unknown log") - // ErrOldSizeInvalid is returned by calls to Update if the provided oldSize parameter is larger than the size of the - // submitted checkpoint. - ErrOldSizeInvalid = errors.New("old size > current") - // ErrCheckpointStale is returned by calls to Update if the oldSize parameter does not match the size of the currently - // stored checkpoint for the same log. - ErrCheckpointStale = errors.New("old size != current") - // ErrInvalidProof is returned by calls to Update if the provided consistency proof is invalid. - ErrInvalidProof = errors.New("consistency proof invalid") - // ErrRootMismatch is returned by calls to Update if the provided checkpoint is for the same size tree as the currently - // stored one, but their root hashes differ. - ErrRootMismatch = errors.New("roots do not match") -) - -// Persistence is the storage contract for a witness's checkpoints. -type Persistence interface { - // UpdateCheckpoint must atomically call the provided update func with the witness' current latest checkpoint for the log, and update - // that view with the checkpoint the function returns. - UpdateCheckpoint(ctx context.Context, update func(oldCP []byte) (newCP []byte, err error)) error -} - -// Opts is the options passed to a witness. -type Opts struct { - Persistence Persistence - Signers []note.Signer - LogVerifier note.Verifier -} - -// Witness is a witness for a single log. -type Witness struct { - p Persistence - signers []note.Signer - logVerifier note.Verifier -} - -// New creates a new witness for the configured log. -func New(ctx context.Context, wo Opts) (*Witness, error) { - return &Witness{ - p: wo.Persistence, - signers: wo.Signers, - logVerifier: wo.LogVerifier, - }, nil -} - -// verifyCheckpoint verifies the checkpoint under the configured key & origin and returns -// the parsed checkpoint and the note itself. -func (w *Witness) verifyCheckpoint(ctx context.Context, chkptRaw []byte) (*log.Checkpoint, *note.Note, error) { - cp, _, n, err := log.ParseCheckpoint(chkptRaw, w.logVerifier.Name(), w.logVerifier) - return cp, n, err -} - -// Update updates the latest checkpoint if nextRaw is consistent with the current -// latest one for this log. -// -// The values returned depend on whether or not the new checkpoint is accepted, and -// if not, the reason it was rejected. This can be determined through the error: -// -// - no error: The checkpoint was accepted, and a serialised note-signature is returned. -// - ErrCheckpointStale or ErrOldSizeInvalid: the presented checkpoint is out of date, the size of the current checkpoint is returned. -// - Any other error, no supporting values are returned. -func (w *Witness) Update(ctx context.Context, oldSize uint64, nextRaw []byte, cProof [][]byte) ([]byte, uint64, error) { - return iotel.Trace2(ctx, "tessera.witness.server.update", tracer, func(ctx context.Context, span trace.Span) ([]byte, uint64, error) { - // Check the signatures on the raw checkpoint and parse it - // into the log.Checkpoint format. - // - // SPEC: The witness MUST verify the checkpoint signature against the public key(s) it trusts for the - // checkpoint origin, and it MUST ignore signatures from unknown keys. - next, nextNote, err := w.verifyCheckpoint(ctx, nextRaw) - if err != nil { - return nil, 0, err - } - - var retSigs []byte - var retSize uint64 - - // Attempt to atomically update the latest known checkpoint for the given origin. - err = w.p.UpdateCheckpoint(ctx, func(prevRaw []byte) ([]byte, error) { - // If there was nothing stored already then treat this new - // checkpoint as trust-on-first-use (TOFU). - if prevRaw == nil { - // Store a witness cosigned version of the checkpoint. - signed, sigs, err := w.signChkpt(nextNote) - if err != nil { - return nil, fmt.Errorf("couldn't sign input checkpoint: %v", err) - } - retSigs = sigs - return signed, nil - } - - prev, _, err := w.verifyCheckpoint(ctx, prevRaw) - if err != nil { - retSize, retSigs = 0, nil - return nil, fmt.Errorf("couldn't parse stored checkpoint: %v", err) - } - - switch { - case oldSize > next.Size: - // SPEC: The old size MUST be equal to or lower than the (submitted) checkpoint size. - retSize, retSigs = prev.Size, nil - return nil, ErrOldSizeInvalid - - case oldSize != prev.Size: - // SPEC: The witness MUST check that the old size matches the size of the latest checkpoint it cosigned - // for the checkpoint's origin (or zero if it never cosigned a checkpoint for that origin) - retSize, retSigs = prev.Size, nil - return nil, fmt.Errorf("%w (%d != %d)", ErrCheckpointStale, oldSize, prev.Size) - - case next.Size == prev.Size: - // SPEC: If the old size matches the checkpoint size, the witness MUST check that the root hashes are - // also identical. - if !bytes.Equal(next.Hash, prev.Hash) { - slog.ErrorContext(ctx, "INCONSISTENT CHECKPOINTS", slog.String("origin", w.logVerifier.Name()), slog.Any("prev", prev), slog.Any("next", next)) - retSize, retSigs = 0, nil - return nil, ErrRootMismatch - } - // We'll continue on to signing the checkpoints below, so the log gets a fresh cosignature with updated timestamp. - - case prev.Size == 0: - // SPEC: The proof MUST be empty if the old size is zero. - // - // Checkpoints of size 0 are really placeholders and consistency proofs can't be performed. - // If we initialized on a tree size of 0, then we simply ratchet forward and effectively TOFU the new checkpoint. - if len(cProof) > 0 { - retSize, retSigs = 0, nil - return nil, ErrInvalidProof - } - - case next.Size > prev.Size: - // The only remaining option is next.Size > prev.Size. This might be valid so we verify the consistency proof. - if err := proof.VerifyConsistency(rfc6962.DefaultHasher, prev.Size, next.Size, cProof, prev.Hash, next.Hash); err != nil { - // Complain if the checkpoints aren't consistent. - return nil, ErrInvalidProof - } - - default: - // This should never occur, but if it does, fail safe. - slog.ErrorContext(ctx, "unexpected state in Update", slog.String("origin", w.logVerifier.Name()), slog.Any("prev", prev), slog.Any("next", next)) - return nil, fmt.Errorf("unexpected state: prev.Size=%d, next.Size=%d", prev.Size, next.Size) - } - - // All checks complete, we're satisfied with the new checkpoint and will sign it. - signed, sigs, err := w.signChkpt(nextNote) - if err != nil { - retSize, retSigs = 0, nil - return nil, fmt.Errorf("couldn't sign input checkpoint: %v", err) - } - - retSize, retSigs = next.Size, sigs - return signed, nil - }) - return retSigs, retSize, err - }) -} - -// signChkpt adds the witness' signature to a checkpoint. -// -// Returns: -// - A serialised signed note including new witness signatures. -// - A serialised representation of just the witness signature line(s). -func (w *Witness) signChkpt(n *note.Note) ([]byte, []byte, error) { - // Code below is a lightly tweaked snippet from sumdb/note/note.go - // https://cs.opensource.google/go/x/mod/+/refs/tags/v0.24.0:sumdb/note/note.go;l=625-649 - - // Prepare signatures. - // - // We need to return both a full serialised signed note, as well as the just the - // signature lines we're adding - this is because we want to _store_ the full note, but - // the tlog-witness API requires that we only return the signature lines. - // - // Rather than using note.Sign, then running note.Open in order to get access to our - // signatures, we'll instead use our note.Signer(s) directly to sign the note message - // and then use the returned signature bytes to create both the serialised signed note - // as well as the serialised signature lines. - - var sigs = bytes.Buffer{} - for _, s := range w.signers { - name := s.Name() - hash := s.KeyHash() - if !isValidSignerName(name) { - return nil, nil, errors.New("invalid signer") - } - - sig, err := s.Sign([]byte(n.Text)) - if err != nil { - return nil, nil, err - } - - // Create serialised signature line and append it to our sigs buffer: - var hbuf [4]byte - binary.BigEndian.PutUint32(hbuf[:], hash) - b64 := base64.StdEncoding.EncodeToString(append(hbuf[:], sig...)) - sigs.WriteString("— ") - sigs.WriteString(name) - sigs.WriteString(" ") - sigs.WriteString(b64) - sigs.WriteString("\n") - - // Also create a new note.Signature and pop it into the note's Sigs list (this will cause - // the signature to be present in the output when we call note.Sign below. - n.Sigs = append(n.Sigs, note.Signature{Name: name, Hash: hash, Base64: b64}) - } - // Serialise the full signed note by calling Sign. - // Note that we're not passing any signers here because we've already added signatures in the loop above, so - // this call becomes just a serialisation function. - signed, err := note.Sign(n) - if err != nil { - return nil, nil, err - } - - return signed, sigs.Bytes(), nil -} - -// isValidSignerName reports whether name is valid. -// It must be non-empty and not have any Unicode spaces or pluses. -func isValidSignerName(name string) bool { - return name != "" && utf8.ValidString(name) && strings.IndexFunc(name, unicode.IsSpace) < 0 && !strings.Contains(name, "+") -} diff --git a/internal/witness/witness_test.go b/internal/witness/witness_test.go deleted file mode 100644 index 48706296f..000000000 --- a/internal/witness/witness_test.go +++ /dev/null @@ -1,249 +0,0 @@ -// Copyright 2021 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 witness - -import ( - "bytes" - "context" - "encoding/hex" - "errors" - "fmt" - "sync" - "testing" - - "github.com/transparency-dev/formats/log" - f_note "github.com/transparency-dev/formats/note" - "github.com/transparency-dev/merkle/rfc6962" - "golang.org/x/mod/sumdb/note" -) - -var ( - // https://go.dev/play/p/FVJgyhl7URt to regenerate any messages if needed. - mPK = "monkeys+db4d9f7e+AULaJMvTtDLHPUcUrjdDad9vDlh/PTfC2VV60JUtCfWT" - mSK = "PRIVATE+KEY+monkeys+db4d9f7e+ATWIAF3yVBG+Hv1rZFQoNt/BaURkLPtOFMAM2HrEeIr6" - // wPK = "witness+f13a86db+AdYV1Ztajd9BvyjP2HgpwrqYL6TjOwIjGMOq8Bu42xbN" - wSK = "PRIVATE+KEY+witness+f13a86db+AaLa/dfyBhyo/m0Z7WCi98ENVZWtrP8pxgRNrx7tIWiA" - mInit = []byte("monkeys\n5\n41smjBUiAU70EtKlT6lIOIYtRTYxYXsDB+XHfcvu/BE=\n\n— monkeys 202fftzGl3LVoqjXfwCFZZXs8I+5G22+Ek2K0AOyBuSJ/8/CZawNF+6fNlTKOCd622pbzJNkkJFWuw9DbicZCkEx9AY=\n") - mNext = []byte("monkeys\n8\nV8K9aklZ4EPB+RMOk1/8VsJUdFZR77GDtZUQq84vSbo=\n\n— monkeys 202ffoUEboiQYpHzICeaFmoy3RNviHTpAxYrq/eO4QQVQMvu9UebKBMX2MJC76NLthZaKsnKbCA8GxrjePZhvDCH7Ag=\n") - consProof = [][]byte{ - dh("b9e1d62618f7fee8034e4c5010f727ab24d8e4705cb296c374bf2025a87a10d2", 32), - dh("aac66cd7a79ce4012d80762fe8eec3a77f22d1ca4145c3f4cee022e7efcd599d", 32), - dh("89d0f753f66a290c483b39cd5e9eafb12021293395fad3d4a2ad053cfbcfdc9e", 32), - dh("29e40bb79c966f4c6fe96aff6f30acfce5f3e8d84c02215175d6e018a5dee833", 32), - } -) - -type logOpts struct { - origin string - PK string -} - -func newWitness(t *testing.T, log logOpts, p *testPersistence) *Witness { - // Set up Opts for the witness. - ns, err := f_note.NewSignerForCosignatureV1(wSK) - if err != nil { - t.Fatalf("couldn't create a witness signer: %v", err) - } - logV, err := note.NewVerifier(log.PK) - if err != nil { - t.Fatalf("couldn't create a log verifier: %v", err) - } - opts := Opts{ - Persistence: p, - Signers: []note.Signer{ns}, - LogVerifier: logV, - } - // Create the witness - w, err := New(t.Context(), opts) - if err != nil { - t.Fatalf("couldn't create witness: %v", err) - } - return w -} - -// dh is taken from https://github.com/google/trillian/blob/master/merkle/logverifier/log_verifier_test.go. -func dh(h string, expLen int) []byte { - r, err := hex.DecodeString(h) - if err != nil { - panic(err) - } - if got := len(r); got != expLen { - panic(fmt.Sprintf("decode %q: len=%d, want %d", h, got, expLen)) - } - return r -} - -func mustCreateCheckpoint(t *testing.T, sk string, origin string, size uint64, rootHash []byte) []byte { - t.Helper() - cp := log.Checkpoint{ - Origin: origin, - Size: size, - Hash: rootHash, - } - signer, err := note.NewSigner(sk) - if err != nil { - t.Fatal(err) - } - - msg, err := note.Sign(¬e.Note{Text: string(cp.Marshal())}, signer) - if err != nil { - t.Fatal(err) - } - return msg -} - -func TestUpdate(t *testing.T) { - for _, test := range []struct { - desc string - origin string - initC []byte - oldSize uint64 - newC []byte - pf [][]byte - wantUpdate bool - wantError error - }{ - { - desc: "vanilla consistency happy path", - origin: "monkeys", - initC: mustCreateCheckpoint(t, mSK, "monkeys", 5, dh("e35b268c1522014ef412d2a54fa94838862d453631617b0307e5c77dcbeefc11", 32)), - oldSize: 5, - newC: mNext, - pf: consProof, - wantUpdate: true, - }, { - desc: "oldSize doesn't match current state", - origin: "monkeys", - initC: mustCreateCheckpoint(t, mSK, "monkeys", 5, dh("e35b268c1522014ef412d2a54fa94838862d453631617b0307e5c77dcbeefc11", 32)), - oldSize: 2, - newC: mNext, - wantError: ErrCheckpointStale, - }, { - desc: "vanilla consistency starting from tree size 0 with proof", - origin: "monkeys", - initC: mustCreateCheckpoint(t, mSK, "monkeys", 0, rfc6962.DefaultHasher.EmptyRoot()), - oldSize: 0, - newC: mustCreateCheckpoint(t, mSK, "monkeys", 5, dh("e35b268c1522014ef412d2a54fa94838862d453631617b0307e5c77dcbeefc11", 32)), - pf: consProof, - // Proof should be empty for oldSize=zero. - wantError: ErrInvalidProof, - }, { - desc: "vanilla resubmit known CP", - origin: "monkeys", - initC: mustCreateCheckpoint(t, mSK, "monkeys", 5, dh("e35b268c1522014ef412d2a54fa94838862d453631617b0307e5c77dcbeefc11", 32)), - oldSize: 5, - newC: mustCreateCheckpoint(t, mSK, "monkeys", 5, dh("e35b268c1522014ef412d2a54fa94838862d453631617b0307e5c77dcbeefc11", 32)), - wantUpdate: true, - }, { - desc: "resubmit known CP with changed root", - origin: "monkeys", - initC: mustCreateCheckpoint(t, mSK, "monkeys", 5, dh("e35b268c1522014ef412d2a54fa94838862d453631617b0307e5c77dcbeefc11", 32)), - oldSize: 5, - newC: mustCreateCheckpoint(t, mSK, "monkeys", 5, dh("fffffffffffffffef412d2a54fa94838862d453631617b0307e5c77dcbeefc11", 32)), - wantError: ErrRootMismatch, - }, { - desc: "missing proof", - origin: "monkeys", - initC: mustCreateCheckpoint(t, mSK, "monkeys", 4, dh("e35b268c1522014ef412d2a54fa94838862d453631617b0307e5c77dcbeefc11", 32)), - oldSize: 4, - newC: mustCreateCheckpoint(t, mSK, "monkeys", 5, dh("e35b268c1522014ef412d2a54fa94838862d453631617b0307e5c77dcbeefc11", 32)), - pf: [][]byte{}, - wantError: ErrInvalidProof, - }, { - desc: "submit smaller checkpoint", - initC: mNext, - oldSize: 8, - newC: mInit, - pf: consProof, - wantError: ErrOldSizeInvalid, - }, { - desc: "vanilla consistency garbage proof", - initC: mInit, - oldSize: 5, - newC: mNext, - pf: [][]byte{ - dh("aaaa", 2), - dh("bbbb", 2), - dh("cccc", 2), - dh("dddd", 2), - }, - wantError: ErrInvalidProof, - }, - } { - t.Run(test.desc, func(t *testing.T) { - ctx := context.Background() - p := newPersistence() - // Set up witness. - w := newWitness(t, logOpts{ - origin: "monkeys", - PK: mPK, - }, p) - // Set an initial checkpoint for the log. - if _, _, err := w.Update(ctx, 0, test.initC, nil); err != nil { - t.Errorf("failed to set checkpoint: %v", err) - } - // Now update from this checkpoint to a newer one. - _, _, err := w.Update(ctx, test.oldSize, test.newC, test.pf) - if err != nil { - if !errors.Is(err, test.wantError) { - t.Fatalf("Got error %v, want %v", err, test.wantError) - } - return - } - if test.wantUpdate { - curCP, err := p.latest(ctx) - if err != nil { - t.Fatalf("failed to get latest checkpoint: %v", err) - } - // curCP should be a prefix of the new CP as the witness will have added a signature. - if !bytes.HasPrefix(curCP, test.newC) { - t.Fatalf("updated checkpoint != test.newC:\nGot:\n%s\nWant:\n%s\n", curCP, test.newC) - } - } - }) - } -} - -func newPersistence() *testPersistence { - return &testPersistence{} -} - -type testPersistence struct { - mu sync.RWMutex - checkpoint []byte -} - -func (p *testPersistence) latest(_ context.Context) ([]byte, error) { - p.mu.RLock() - defer p.mu.RUnlock() - return p.checkpoint, nil -} - -func (p *testPersistence) UpdateCheckpoint(_ context.Context, f func([]byte) ([]byte, error)) error { - p.mu.Lock() - defer p.mu.Unlock() - u, err := f(p.checkpoint) - if err != nil { - return err - } - - bits := bytes.Split(u, []byte{'\n'}) - if len(bits) == 0 { - return errors.New("invalid checkpoint") - } - - p.checkpoint = u - return nil -} diff --git a/mirror_lifecycle.go b/mirror_lifecycle.go index 4e57e90e0..04805f575 100644 --- a/mirror_lifecycle.go +++ b/mirror_lifecycle.go @@ -22,14 +22,14 @@ import ( "github.com/transparency-dev/tessera/api" "github.com/transparency-dev/tessera/api/layout" - "github.com/transparency-dev/tessera/internal/witness" + "github.com/transparency-dev/witness/witness" "golang.org/x/mod/sumdb/note" ) // MirrorOptions holds mirror lifecycle settings for all storage implementations. type MirrorOptions struct { - logVerifier note.Verifier signer note.Signer + witness *witness.Witness } // NewMirrorOptions creates a new options struct with defaults. @@ -37,21 +37,15 @@ func NewMirrorOptions() *MirrorOptions { return &MirrorOptions{} } -// WithLogVerifier configures the note.Verifier to use when verifying log checkpoints. -func (o *MirrorOptions) WithLogVerifier(v note.Verifier) *MirrorOptions { - o.logVerifier = v - return o -} - // WithSigner configures the note.Signer to use when cosigning checkpoints. func (o *MirrorOptions) WithSigner(s note.Signer) *MirrorOptions { o.signer = s return o } -// LogVerifier returns the configured note.Verifier. -func (o *MirrorOptions) LogVerifier() note.Verifier { - return o.logVerifier +func (o *MirrorOptions) WithWitness(w *witness.Witness) *MirrorOptions { + o.witness = w + return o } // Signer returns the configured note.Signer. @@ -68,19 +62,17 @@ func (o *MirrorOptions) LeafHasher() func(bundle []byte) (leafHashes [][]byte, e } func (o *MirrorOptions) valid() error { - if o.logVerifier == nil { - return errors.New("invalid MirrorOptions: WithLogVerifier must be set") - } if o.signer == nil { return errors.New("invalid MirrorOptions: WithSigner must be set") } + if o.witness == nil { + return errors.New("invalid MirrorOptions: WithWitness must be set") + } return nil } // mirrorWriter describes the contract for storage implementation required to support the mirroring lifecycle. type MirrorWriter interface { - // MirrorWriters must also implement the contract for storing witness data. - witness.Persistence // IntegrateBundles integrates bundles of log entries, starting at the given index, into the local tree. // Returns the size of the tree and its new root hash if successful. IntegrateBundles(ctx context.Context, from uint64, bundles iter.Seq[api.EntryBundle]) (uint64, []byte, error) @@ -115,17 +107,8 @@ func NewMirrorTarget(ctx context.Context, d Driver, opts *MirrorOptions) (*Mirro if err != nil { return nil, fmt.Errorf("failed to init MirrorTarget lifecycle: %v", err) } - w, err := witness.New(ctx, witness.Opts{ - Persistence: mw, - Signers: []note.Signer{opts.signer}, - LogVerifier: opts.logVerifier, - }, - ) - if err != nil { - return nil, fmt.Errorf("failed to create witness: %v", err) - } return &MirrorTarget{ - witness: w, + witness: opts.witness, writer: mw, reader: r, }, nil @@ -137,16 +120,6 @@ type MirrorPackage struct { Proof [][]byte } -// AddCheckpoint attempt to register a new checkpoint from the configured log, updating the local latest consistent view if possible. -// -// Returns a cosignature for the checkpoint. -// If unsuccessful, returns an error describing the reason for the failure, and, if that reason is a conflict, returns the size of the current consistent view. -// -// TODO(al): something, something, pending checkpoints. -func (mt *MirrorTarget) AddCheckpoint(ctx context.Context, oldSize uint64, proof [][]byte, cp []byte) (cosig []byte, wantSize uint64, err error) { - return mt.witness.Update(ctx, oldSize, cp, proof) -} - // AddEntries processes a stream of entry packages, verifies subtree consistency proofs, // and durably commits entries to the log. //