From 1ab40b9a667eb9150dde5db60f2ea9b1fab35cb5 Mon Sep 17 00:00:00 2001 From: Al Cutter Date: Thu, 23 Jul 2026 14:40:13 +0000 Subject: [PATCH 1/4] Add mirroring support to append lifecycle. --- append_lifecycle.go | 86 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 81 insertions(+), 5 deletions(-) diff --git a/append_lifecycle.go b/append_lifecycle.go index d13094143..ad48cbc25 100644 --- a/append_lifecycle.go +++ b/append_lifecycle.go @@ -58,7 +58,8 @@ const ( DefaultAntispamInMemorySize = 256 << 10 // DefaultWitnessTimeout is the default maximum time to wait for responses from configured witnesses. DefaultWitnessTimeout = 1 * time.Second - + // DefaultMirrorTimeout is the default maximum time to wait for responses from configured mirrors. + DefaultMirrorTimeout = 10 * time.Second // DefaultEntrySizeLimit is the maximum possible size of data for a single entry, as specified by C2SP tlog-tiles. DefaultEntrySizeLimit = 1<<16 - 1 @@ -670,6 +671,9 @@ type AppendOptions struct { witnesses WitnessGroup witnessOpts WitnessOptions + mirrors WitnessGroup + mirrorOpts MirroringOptions + addDecorators []func(AddFn) AddFn followers []Follower @@ -735,12 +739,31 @@ func (o AppendOptions) CheckpointPublisher(lr LogReader, httpClient *http.Client span.AddEvent("Created CP") appenderSignedSize.Record(ctx, otel.Clamp64(size)) - wSigs, err := witnessCheckpoint(ctx, cp, size, o.witnesses, lr, httpClient, o.witnessOpts) - if err != nil { - return nil, err + eg := errgroup.Group{} + sigC := make(chan []byte, 2) + eg.Go(func() error { + ws, err := witnessCheckpoint(ctx, cp, size, o.witnesses, lr, httpClient, o.witnessOpts) + if ws != nil { + sigC <- ws + } + return err + }) + eg.Go(func() error { + ms, err := mirrorCheckpoint(ctx, cp, size, o.mirrors, lr, httpClient, o.mirrorOpts) + if ms != nil { + sigC <- ms + } + return err + }) + + if err := eg.Wait(); err != nil { + return nil, fmt.Errorf("failed to fetch cosignatures: %v", err) } - cp = append(cp, wSigs...) + close(sigC) + for sigs := range sigC { + cp = append(cp, sigs...) + } return cp, nil }) @@ -796,6 +819,18 @@ func witnessCheckpoint(ctx context.Context, cp []byte, cpSize uint64, witnesses }) } +// mirrorCheckpoint takes care of mirroring the given checkpoint with the provided mirror policy. +// Returns signatures from mirrors, ready to append to the checkpoint, or an error. +func mirrorCheckpoint(ctx context.Context, cp []byte, cpSize uint64, mirrors WitnessGroup, lr LogReader, httpClient *http.Client, opts MirroringOptions) ([]byte, error) { + return otel.Trace(ctx, "tessera.mirrorCheckpoint", tracer, func(ctx context.Context, span trace.Span) ([]byte, error) { + if len(mirrors.Components) == 0 { + return nil, nil + } + span.AddEvent("Starting mirroring") + return nil, nil + }) +} + func (o AppendOptions) BatchMaxAge() time.Duration { return o.batchMaxAge } @@ -950,6 +985,27 @@ func (o *AppendOptions) WithWitnesses(witnesses WitnessGroup, opts *WitnessOptio return o } +// WithMirrors configures the set of tlog-mirror servers that Tessera will contact in order to obtain +// mirror counter-signatures on a checkpoint before publishing it. +// +// Requests will be sent to every mirror referenced by the group using the tlog-mirror API at the configured URL. +// The checkpoint will be accepted for publishing when a sufficient number of mirrors to satisfy the group +// have responded. +// +// If this method is not called, then no mirror counter signatures will be required to publish. +func (o *AppendOptions) WithMirrors(mirrors WitnessGroup, opts *MirroringOptions) *AppendOptions { + if opts == nil { + opts = &MirroringOptions{} + } + if opts.Timeout == 0 { + opts.Timeout = DefaultMirrorTimeout + } + + o.mirrors = mirrors + o.mirrorOpts = *opts + return o +} + // WitnessOptions contains extra optional configuration for how Tessera should use/interact with // a user-provided WitnessGroup policy. type WitnessOptions struct { @@ -970,6 +1026,26 @@ type WitnessOptions struct { FailOpen bool } +// MirroringOptions contains extra optional configuration for how Tessera should use/interact with +// the tlog-mirror servers. +type MirroringOptions struct { + // Timeout is the maximum time to wait while attempting to satisfy the configured mirror policy. + // + // If the policy has not already been satisfied at the point this duration has passed, Tessera + // will stop waiting for more responses. The FailOpen option below controls whether or not the + // checkpoint will be published in this case. + // + // If unset, uses DefaultMirrorTimeout. + Timeout time.Duration + + // FailOpen controls whether a checkpoint, for which the mirror policy was unable to be met, + // should still be published. + // + // This setting is intended only for facilitating early "non-blocking" adoption of mirroring, + // and will be disabled and/or removed in the future. + FailOpen bool +} + // WithGarbageCollectionInterval allows the interval between scans to remove obsolete partial // tiles and entry bundles. // From a19806872b13d18184c08d281be9b8c6295f819a Mon Sep 17 00:00:00 2001 From: Al Cutter Date: Fri, 24 Jul 2026 10:40:18 +0000 Subject: [PATCH 2/4] Add tests --- append_lifecycle_test.go | 251 +++++++++++++++++++++++++++++++++++ cmd/mtc/mirror/posix/main.go | 2 +- mirror_lifecycle.go | 6 +- 3 files changed, 255 insertions(+), 4 deletions(-) diff --git a/append_lifecycle_test.go b/append_lifecycle_test.go index 858205c43..c44d6c9a4 100644 --- a/append_lifecycle_test.go +++ b/append_lifecycle_test.go @@ -18,10 +18,17 @@ import ( "context" "errors" "fmt" + "net/http" + "net/http/httptest" + "net/url" "strings" "testing" "time" + f_note "github.com/transparency-dev/formats/note" + "github.com/transparency-dev/witness/config" + "github.com/transparency-dev/witness/persistence/inmemory" + "github.com/transparency-dev/witness/witness" "golang.org/x/mod/sumdb/note" ) @@ -218,3 +225,247 @@ func TestAddUpdatesWantTreeSize(t *testing.T) { t.Fatalf("wantTreeSize should be %d after adding index %d, got %d", wantIdx+1, wantIdx, got) } } + +type appendFakeLogReader struct { + readCheckpoint func(context.Context) ([]byte, error) + readTile func(context.Context, uint64, uint64, uint8) ([]byte, error) + readEntryBundle func(context.Context, uint64, uint8) ([]byte, error) +} + +func (f *appendFakeLogReader) ReadCheckpoint(ctx context.Context) ([]byte, error) { + if f.readCheckpoint != nil { + return f.readCheckpoint(ctx) + } + return nil, nil +} + +func (f *appendFakeLogReader) ReadTile(ctx context.Context, level, index uint64, p uint8) ([]byte, error) { + if f.readTile != nil { + return f.readTile(ctx, level, index, p) + } + return nil, nil +} + +func (f *appendFakeLogReader) ReadEntryBundle(ctx context.Context, index uint64, p uint8) ([]byte, error) { + if f.readEntryBundle != nil { + return f.readEntryBundle(ctx, index, p) + } + return nil, nil +} + +func (f *appendFakeLogReader) IntegratedSize(ctx context.Context) (uint64, error) { + return 0, nil +} + +func (f *appendFakeLogReader) NextIndex(ctx context.Context) (uint64, error) { + return 0, nil +} + +func TestWithMirrors(t *testing.T) { + u, err := url.Parse("https://mirror.example.com") + if err != nil { + t.Fatalf("failed to parse url: %v", err) + } + wit, err := NewWitness("Wit1+55ee4561+AVhZSmQj9+SoL+p/nN0Hh76xXmF7QcHfytUrI1XfSClk", u) + if err != nil { + t.Fatalf("failed to create witness: %v", err) + } + mirrors := NewWitnessGroup(1, wit) + + for _, test := range []struct { + desc string + mirrorOpts *MirroringOptions + expectTimeout time.Duration + expectFailOpen bool + }{ + { + desc: "nil options", + mirrorOpts: nil, + expectTimeout: DefaultMirrorTimeout, + expectFailOpen: false, + }, + { + desc: "custom options", + mirrorOpts: &MirroringOptions{ + Timeout: 5 * time.Second, + FailOpen: true, + }, + expectTimeout: 5 * time.Second, + expectFailOpen: true, + }, + { + desc: "zero timeout uses default", + mirrorOpts: &MirroringOptions{ + Timeout: 0, + FailOpen: true, + }, + expectTimeout: DefaultMirrorTimeout, + expectFailOpen: true, + }, + } { + t.Run(test.desc, func(t *testing.T) { + opts := NewAppendOptions().WithMirrors(mirrors, test.mirrorOpts) + if len(opts.mirrors.Components) != 1 { + t.Errorf("expected 1 mirror component, got %d", len(opts.mirrors.Components)) + } + if got, want := opts.mirrorOpts.Timeout, test.expectTimeout; got != want { + t.Errorf("expected timeout %v, got %v", want, got) + } + if got, want := opts.mirrorOpts.FailOpen, test.expectFailOpen; got != want { + t.Errorf("expected FailOpen %t, got %t", want, got) + } + }) + } +} + +const ( + testWit1VKey = "Wit1+55ee4561+AVhZSmQj9+SoL+p/nN0Hh76xXmF7QcHfytUrI1XfSClk" + testWit1SKey = "PRIVATE+KEY+Wit1+55ee4561+AeadRiG7XM4XiieCHzD8lxysXMwcViy5nYsoXURWGrlE" +) + +func newWitnessHandler(t *testing.T, logVerifier note.Verifier, witnessSKey string) http.HandlerFunc { + witnessSigner, err := f_note.NewSignerForCosignatureV1(witnessSKey) + if err != nil { + t.Fatalf("failed to create witness signer: %v", err) + } + + p := inmemory.New() + logCfg := config.Log{ + Origin: "example.com/log/testdata", + Verifier: logVerifier, + VKey: "example.com/log/testdata+33d7b496+AeHTu4Q3hEIMHNqc6fASMsq3rKNx280NI+oO5xCFkkSx", + } + if err := p.AddLogs(t.Context(), []config.Log{logCfg}); err != nil { + t.Fatalf("failed to add log config to persistence: %v", err) + } + + wOpts := witness.Opts{ + Persistence: p, + Signers: []note.Signer{witnessSigner}, + VerifierForLog: func(ctx context.Context, origin string) (note.Verifier, bool, error) { + if origin == "example.com/log/testdata" { + return logVerifier, true, nil + } + return nil, false, nil + }, + } + witSvc, err := witness.New(t.Context(), wOpts) + if err != nil { + t.Fatalf("failed to create witness service: %v", err) + } + + return witness.NewHTTPHandler(witSvc).AddCheckpoint +} + +func TestCheckpointPublisher(t *testing.T) { + logSigner := mustCreateSigner(t, testSignerKey) + logVerifier, err := note.NewVerifier("example.com/log/testdata+33d7b496+AeHTu4Q3hEIMHNqc6fASMsq3rKNx280NI+oO5xCFkkSx") + if err != nil { + t.Fatalf("failed to create log verifier: %v", err) + } + + witnessServer := httptest.NewServer(newWitnessHandler(t, logVerifier, testWit1SKey)) + t.Cleanup(witnessServer.Close) + + witnessServerURL, err := url.Parse(witnessServer.URL) + if err != nil { + t.Fatalf("failed to parse witness server url: %v", err) + } + + wit, err := NewWitness(testWit1VKey, witnessServerURL) + if err != nil { + t.Fatalf("failed to create witness: %v", err) + } + witnesses := NewWitnessGroup(1, wit) + witVerifier, err := f_note.NewVerifierForCosignatureV1(testWit1VKey) + if err != nil { + t.Fatalf("failed to create witness verifier: %v", err) + } + + dummyMirrors := NewWitnessGroup(1, wit) + + for _, test := range []struct { + desc string + opts *AppendOptions + witnessFails bool + expectCosignatures []note.Verifier + expectErr bool + }{ + { + desc: "no witnesses, no mirrors", + opts: NewAppendOptions().WithCheckpointSigner(logSigner), + }, + { + desc: "witnesses only", + opts: NewAppendOptions().WithCheckpointSigner(logSigner).WithWitnesses(witnesses, nil), + expectCosignatures: []note.Verifier{witVerifier}, + }, + { + desc: "mirrors only", + opts: NewAppendOptions().WithCheckpointSigner(logSigner).WithMirrors(dummyMirrors, nil), + }, + { + desc: "witnesses and mirrors", + opts: NewAppendOptions().WithCheckpointSigner(logSigner).WithWitnesses(witnesses, nil).WithMirrors(dummyMirrors, nil), + expectCosignatures: []note.Verifier{witVerifier}, + }, + { + desc: "witness fails, failOpen=false", + opts: NewAppendOptions().WithCheckpointSigner(logSigner).WithWitnesses(witnesses, &WitnessOptions{FailOpen: false}), + witnessFails: true, + expectErr: true, + }, + { + desc: "witness fails, failOpen=true", + opts: NewAppendOptions().WithCheckpointSigner(logSigner).WithWitnesses(witnesses, &WitnessOptions{FailOpen: true}), + witnessFails: true, + }, + } { + t.Run(test.desc, func(t *testing.T) { + client := http.DefaultClient + if test.witnessFails { + failingWitnessServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "internal error", http.StatusInternalServerError) + })) + defer failingWitnessServer.Close() + + failingURL, _ := url.Parse(failingWitnessServer.URL) + failingWit, _ := NewWitness(testWit1VKey, failingURL) + failingWitnesses := NewWitnessGroup(1, failingWit) + + // Re-configure option to use failing witnesses + wOpts := &WitnessOptions{FailOpen: test.opts.witnessOpts.FailOpen} + test.opts.WithWitnesses(failingWitnesses, wOpts) + } + + lr := &appendFakeLogReader{ + readCheckpoint: func(ctx context.Context) ([]byte, error) { + return nil, errors.New("no checkpoint yet") + }, + } + + publisher := test.opts.CheckpointPublisher(lr, client) + cp, err := publisher(t.Context(), 5, []byte("12345678901234567890123456789012")) + if (err != nil) != test.expectErr { + t.Fatalf("expected error %v but got: %v", test.expectErr, err) + } + if err != nil { + return + } + + // Open checkpoint to verify signatures + wantV := append([]note.Verifier{logVerifier}, test.expectCosignatures...) + n, err := note.Open(cp, note.VerifierList(wantV...)) + if err != nil { + t.Fatalf("failed to open signed checkpoint: %v", err) + } + + // Check that all required verifiers signed it + if len(n.Sigs) != len(wantV) { + t.Logf("cp = %q", string(cp)) + t.Logf("n.Sigs = %+v", n.Sigs) + t.Errorf("expected %d signatures, got %d", len(wantV), len(n.Sigs)) + } + }) + } +} diff --git a/cmd/mtc/mirror/posix/main.go b/cmd/mtc/mirror/posix/main.go index 9714cdc3e..921cb8449 100644 --- a/cmd/mtc/mirror/posix/main.go +++ b/cmd/mtc/mirror/posix/main.go @@ -175,7 +175,7 @@ func mirrorConfigFromFlags(ctx context.Context) []witnessConfig.Log { slog.ErrorContext(ctx, "Duplicate origin in mirror config", slog.String("origin", o)) os.Exit(1) } - seenOrigins[o]=struct{}{} + seenOrigins[o] = struct{}{} } return cfg } diff --git a/mirror_lifecycle.go b/mirror_lifecycle.go index 8af6f8df0..a2c94eb0c 100644 --- a/mirror_lifecycle.go +++ b/mirror_lifecycle.go @@ -64,7 +64,7 @@ type MirrorOptions struct { signer note.Signer cpSource func(context.Context) ([]byte, error) logVerifier note.Verifier - origin string + origin string } // NewMirrorOptions creates a new options struct with defaults. @@ -147,7 +147,7 @@ type MirrorTarget struct { writer MirrorWriter reader LogReader cpSource func(context.Context) ([]byte, error) - origin string + origin string signer note.Signer logVerifier note.Verifier verifySubtreeProof func(hasher merkle.LogHasher, start, end, size uint64, proof [][]byte, subRoot []byte, root []byte) error @@ -186,7 +186,7 @@ func NewMirrorTarget(ctx context.Context, d Driver, opts *MirrorOptions) (*Mirro cpSource: opts.cpSource, signer: opts.signer, logVerifier: opts.logVerifier, - origin: opts.origin, + origin: opts.origin, verifySubtreeProof: proof.VerifySubtreeConsistency, ticketKey: tK, }, nil From 1663c67032756ccb318a7ab9eb35a065aeeeb358 Mon Sep 17 00:00:00 2001 From: Al Cutter Date: Fri, 24 Jul 2026 11:08:01 +0000 Subject: [PATCH 3/4] Share a common fake logreader across tests --- append_lifecycle_test.go | 37 +------------------------ fake_logreader_test.go | 60 ++++++++++++++++++++++++++++++++++++++++ mirror_lifecycle_test.go | 25 +---------------- 3 files changed, 62 insertions(+), 60 deletions(-) create mode 100644 fake_logreader_test.go diff --git a/append_lifecycle_test.go b/append_lifecycle_test.go index c44d6c9a4..128bedba6 100644 --- a/append_lifecycle_test.go +++ b/append_lifecycle_test.go @@ -226,41 +226,6 @@ func TestAddUpdatesWantTreeSize(t *testing.T) { } } -type appendFakeLogReader struct { - readCheckpoint func(context.Context) ([]byte, error) - readTile func(context.Context, uint64, uint64, uint8) ([]byte, error) - readEntryBundle func(context.Context, uint64, uint8) ([]byte, error) -} - -func (f *appendFakeLogReader) ReadCheckpoint(ctx context.Context) ([]byte, error) { - if f.readCheckpoint != nil { - return f.readCheckpoint(ctx) - } - return nil, nil -} - -func (f *appendFakeLogReader) ReadTile(ctx context.Context, level, index uint64, p uint8) ([]byte, error) { - if f.readTile != nil { - return f.readTile(ctx, level, index, p) - } - return nil, nil -} - -func (f *appendFakeLogReader) ReadEntryBundle(ctx context.Context, index uint64, p uint8) ([]byte, error) { - if f.readEntryBundle != nil { - return f.readEntryBundle(ctx, index, p) - } - return nil, nil -} - -func (f *appendFakeLogReader) IntegratedSize(ctx context.Context) (uint64, error) { - return 0, nil -} - -func (f *appendFakeLogReader) NextIndex(ctx context.Context) (uint64, error) { - return 0, nil -} - func TestWithMirrors(t *testing.T) { u, err := url.Parse("https://mirror.example.com") if err != nil { @@ -438,7 +403,7 @@ func TestCheckpointPublisher(t *testing.T) { test.opts.WithWitnesses(failingWitnesses, wOpts) } - lr := &appendFakeLogReader{ + lr := &fakeLogReader{ readCheckpoint: func(ctx context.Context) ([]byte, error) { return nil, errors.New("no checkpoint yet") }, diff --git a/fake_logreader_test.go b/fake_logreader_test.go new file mode 100644 index 000000000..9a7037a25 --- /dev/null +++ b/fake_logreader_test.go @@ -0,0 +1,60 @@ +// 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 tessera + +import "context" + +type fakeLogReader struct { + readCheckpoint func(context.Context) ([]byte, error) + readTile func(context.Context, uint64, uint64, uint8) ([]byte, error) + readEntryBundle func(context.Context, uint64, uint8) ([]byte, error) + sizeFunc func(context.Context) (uint64, error) + nextIndex func(context.Context) (uint64, error) +} + +func (f *fakeLogReader) ReadCheckpoint(ctx context.Context) ([]byte, error) { + if f.readCheckpoint != nil { + return f.readCheckpoint(ctx) + } + return nil, nil +} + +func (f *fakeLogReader) ReadTile(ctx context.Context, level, index uint64, p uint8) ([]byte, error) { + if f.readTile != nil { + return f.readTile(ctx, level, index, p) + } + return nil, nil +} + +func (f *fakeLogReader) ReadEntryBundle(ctx context.Context, index uint64, p uint8) ([]byte, error) { + if f.readEntryBundle != nil { + return f.readEntryBundle(ctx, index, p) + } + return nil, nil +} + +func (f *fakeLogReader) IntegratedSize(ctx context.Context) (uint64, error) { + if f.sizeFunc != nil { + return f.sizeFunc(ctx) + } + return 0, nil +} + +func (f *fakeLogReader) NextIndex(ctx context.Context) (uint64, error) { + if f.nextIndex != nil { + return f.nextIndex(ctx) + } + return 0, nil +} diff --git a/mirror_lifecycle_test.go b/mirror_lifecycle_test.go index 6e048f36f..79de95edf 100644 --- a/mirror_lifecycle_test.go +++ b/mirror_lifecycle_test.go @@ -58,29 +58,6 @@ func (f *fakeMirrorWriter) UpdateCheckpoint(ctx context.Context, g func(oldCP [] return nil } -type fakeLogReader struct { - sizeFunc func(ctx context.Context) (uint64, error) - readEntryBundleFunc func(ctx context.Context, index uint64, p uint8) ([]byte, error) -} - -func (f *fakeLogReader) IntegratedSize(ctx context.Context) (uint64, error) { - if f.sizeFunc != nil { - return f.sizeFunc(ctx) - } - return 0, nil -} -func (f *fakeLogReader) ReadCheckpoint(ctx context.Context) ([]byte, error) { return nil, nil } -func (f *fakeLogReader) ReadTile(ctx context.Context, level, index uint64, p uint8) ([]byte, error) { - return nil, nil -} -func (f *fakeLogReader) ReadEntryBundle(ctx context.Context, index uint64, p uint8) ([]byte, error) { - if f.readEntryBundleFunc != nil { - return f.readEntryBundleFunc(ctx, index, p) - } - return nil, nil -} -func (f *fakeLogReader) NextIndex(ctx context.Context) (uint64, error) { return 0, nil } - const ( testPendingCPOrigin = "test-origin" testPendingCPSize = uint64(512) @@ -579,7 +556,7 @@ func TestMirrorTarget_AddEntries_Unaligned_PadsFirstBundle(t *testing.T) { }, reader: &fakeLogReader{ sizeFunc: func(ctx context.Context) (uint64, error) { return testIntegratedSize, nil }, - readEntryBundleFunc: func(ctx context.Context, index uint64, p uint8) ([]byte, error) { + readEntryBundle: func(ctx context.Context, index uint64, p uint8) ([]byte, error) { readEntryBundleCalled = true if got, want := index, testUploadStart/layout.EntryBundleWidth; got != want { t.Errorf("ReadEntryBundle index: got %d, want %d", got, want) From 677f008510baa329453a397c17bd95806a71511a Mon Sep 17 00:00:00 2001 From: Al Cutter Date: Fri, 24 Jul 2026 12:20:51 +0000 Subject: [PATCH 4/4] Address comments --- README.md | 2 +- append_lifecycle.go | 27 ++++++++++----------------- 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index c74b97e16..62db5b77c 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,7 @@ and [WithCheckpointRepublishInterval](https://pkg.go.dev/github.com/transparency-dev/tessera#AppendOptions.WithCheckpointRepublishInterval)) and performs the following steps: 1. Create a new Checkpoint and sign it with the signer provided by [WithCheckpointSigner](https://pkg.go.dev/github.com/transparency-dev/tessera#AppendOptions.WithCheckpointSigner) - 2. Contact witnesses and collect enough countersignatures to satisfy any witness policy configured by [WithWitnesses](https://pkg.go.dev/github.com/transparency-dev/tessera#AppendOptions.WithWitnesses) + 2. Contact witnesses and collect enough cosignatures to satisfy any witness policy configured by [WithWitnesses](https://pkg.go.dev/github.com/transparency-dev/tessera#AppendOptions.WithWitnesses) 3. If the witness policy is satisfied, make this new Checkpoint public available An entry is considered published once it is committed to by a published Checkpoint (i.e. a published Checkpoint's size is larger than the entry's assigned index). diff --git a/append_lifecycle.go b/append_lifecycle.go index ad48cbc25..0169c3c28 100644 --- a/append_lifecycle.go +++ b/append_lifecycle.go @@ -20,6 +20,7 @@ import ( "fmt" "net/http" "os" + "slices" "sync" "sync/atomic" "time" @@ -739,20 +740,16 @@ func (o AppendOptions) CheckpointPublisher(lr LogReader, httpClient *http.Client span.AddEvent("Created CP") appenderSignedSize.Record(ctx, otel.Clamp64(size)) + var ws, ms []byte eg := errgroup.Group{} - sigC := make(chan []byte, 2) eg.Go(func() error { - ws, err := witnessCheckpoint(ctx, cp, size, o.witnesses, lr, httpClient, o.witnessOpts) - if ws != nil { - sigC <- ws - } + var err error + ws, err = witnessCheckpoint(ctx, cp, size, o.witnesses, lr, httpClient, o.witnessOpts) return err }) eg.Go(func() error { - ms, err := mirrorCheckpoint(ctx, cp, size, o.mirrors, lr, httpClient, o.mirrorOpts) - if ms != nil { - sigC <- ms - } + var err error + ms, err = mirrorCheckpoint(ctx, cp, size, o.mirrors, lr, httpClient, o.mirrorOpts) return err }) @@ -760,11 +757,7 @@ func (o AppendOptions) CheckpointPublisher(lr LogReader, httpClient *http.Client return nil, fmt.Errorf("failed to fetch cosignatures: %v", err) } - close(sigC) - for sigs := range sigC { - cp = append(cp, sigs...) - } - + cp = append(append(slices.Clone(cp), ws...), ms...) return cp, nil }) } @@ -965,7 +958,7 @@ func (o *AppendOptions) WithCheckpointRepublishInterval(interval time.Duration) return o } -// WithWitnesses configures the set of witnesses that Tessera will contact in order to counter-sign +// WithWitnesses configures the set of witnesses that Tessera will contact in order to cosign // a checkpoint before publishing it. A request will be sent to every witness referenced by the group // using the URLs method. The checkpoint will be accepted for publishing when a sufficient number of // witnesses to Satisfy the group have responded. @@ -986,13 +979,13 @@ func (o *AppendOptions) WithWitnesses(witnesses WitnessGroup, opts *WitnessOptio } // WithMirrors configures the set of tlog-mirror servers that Tessera will contact in order to obtain -// mirror counter-signatures on a checkpoint before publishing it. +// mirror cosignatures on a checkpoint before publishing it. // // Requests will be sent to every mirror referenced by the group using the tlog-mirror API at the configured URL. // The checkpoint will be accepted for publishing when a sufficient number of mirrors to satisfy the group // have responded. // -// If this method is not called, then no mirror counter signatures will be required to publish. +// If this method is not called, then no mirror cosignatures will be required to publish. func (o *AppendOptions) WithMirrors(mirrors WitnessGroup, opts *MirroringOptions) *AppendOptions { if opts == nil { opts = &MirroringOptions{}