From 45d758b32686e81fba0995e21fece49fba82c7a9 Mon Sep 17 00:00:00 2001 From: Al Cutter Date: Mon, 7 Sep 2026 14:10:42 +0000 Subject: [PATCH 1/4] Use policy.TLogPolicy in append lifecycle --- append_lifecycle.go | 156 +++++++++++++++++++++++---------------- append_lifecycle_test.go | 127 ++++++++++++++++++++----------- 2 files changed, 178 insertions(+), 105 deletions(-) diff --git a/append_lifecycle.go b/append_lifecycle.go index 055905fd0..77826f897 100644 --- a/append_lifecycle.go +++ b/append_lifecycle.go @@ -19,7 +19,6 @@ import ( "context" "errors" "fmt" - "maps" "net/http" "net/url" "os" @@ -31,6 +30,7 @@ import ( "log/slog" f_log "github.com/transparency-dev/formats/log" + "github.com/transparency-dev/formats/policy" "github.com/transparency-dev/merkle/rfc6962" "github.com/transparency-dev/tessera/api/layout" m_gateway "github.com/transparency-dev/tessera/internal/mirror/gateway" @@ -677,11 +677,11 @@ type AppendOptions struct { checkpointRepublishInterval time.Duration checkpointPublicationTimeout time.Duration - witnesses WitnessGroup - witnessOpts WitnessOptions + witnessPolicy policy.TLogPolicy + witnessOpts WitnessOptions - mirrors WitnessGroup - mirrorOpts MirroringOptions + mirrorPolicy policy.TLogPolicy + mirrorOpts MirroringOptions addDecorators []func(AddFn) AddFn followers []Follower @@ -772,19 +772,6 @@ func (o *AppendOptions) WithAntispam(inMemEntries uint, as Antispam) *AppendOpti return o } -// parseURLs converts a list of URL strings to a list of *url.URL, failing if any cannot be parsed. -func parseURLs(us []string) ([]*url.URL, error) { - ret := make([]*url.URL, 0, len(us)) - for _, s := range us { - u, err := url.Parse(s) - if err != nil { - return nil, err - } - ret = append(ret, u) - } - return ret, nil -} - // CheckpointPublisher should not be used. // Deprecated: Use CheckpointPublisherContext. func (o AppendOptions) CheckpointPublisher(lr LogReader, httpClient *http.Client) func(context.Context, uint64, []byte) ([]byte, error) { @@ -821,7 +808,7 @@ func (o AppendOptions) CheckpointPublisherContext(ctx context.Context, lr LogRea defer cancel() var err error - ws, err = witnessCheckpoint(ctx, witnessGateway.CosignCheckpoint, &o.witnesses, cp, cpSize, o.witnessOpts.FailOpen, o.witnessOpts.Greedy) + ws, err = witnessCheckpoint(ctx, witnessGateway.CosignCheckpoint, o.witnessPolicy, cp, cpSize, o.witnessOpts.FailOpen, o.witnessOpts.Greedy) return err }) } @@ -832,7 +819,7 @@ func (o AppendOptions) CheckpointPublisherContext(ctx context.Context, lr LogRea defer cancel() var err error - ms, err = mirrorCheckpoint(ctx, mirrorGateway.CosignCheckpoint, &o.mirrors, cp, cpSize, o.mirrorOpts.FailOpen, false) + ms, err = mirrorCheckpoint(ctx, mirrorGateway.CosignCheckpoint, o.mirrorPolicy, cp, cpSize, o.mirrorOpts.FailOpen, false) return err }) } @@ -850,14 +837,13 @@ func (o AppendOptions) CheckpointPublisherContext(ctx context.Context, lr LogRea // witnessGateway creates and returns a witnessGateway instance, or nil if no witnesses are configured. func (o AppendOptions) witnessGateway(ctx context.Context, lr LogReader, httpClient *http.Client) (*witness.WitnessGateway, error) { witnesses := []witness.Witness{} - for uStr, vs := range o.witnesses.WitnessEndpoints() { - u, err := url.Parse(uStr) - if err != nil { - return nil, fmt.Errorf("failed to parse witness URL: %w", err) + for _, w := range o.witnessPolicy.Witnesses { + if w.URL == nil { + return nil, fmt.Errorf("invalid witness policy: witness %q has no URL", w.Name) } witnesses = append(witnesses, witness.Witness{ - URL: u, - Verifiers: vs, + URL: w.URL, + Verifiers: []note.Verifier{w.Verifier}, }) } if len(witnesses) == 0 { @@ -873,9 +859,17 @@ func (o AppendOptions) witnessGateway(ctx context.Context, lr LogReader, httpCli // mirrorGateway creates and returns a mirrorGateway instance, or nil if no mirrors are configured. func (o AppendOptions) mirrorGateway(ctx context.Context, lr LogReader, httpClient *http.Client) (*m_gateway.Gateway, error) { - mirrorURLs, err := parseURLs(slices.Collect(maps.Keys(o.mirrors.WitnessEndpoints()))) - if err != nil { - return nil, fmt.Errorf("failed to parse mirror URLs: %w", err) + mirrorURLs := []*url.URL{} + seen := make(map[string]bool) + for _, m := range o.mirrorPolicy.Witnesses { + if m.URL == nil { + return nil, fmt.Errorf("invalid mirror policy: mirror %q has no URL", m.Name) + } + uStr := m.URL.String() + if !seen[uStr] { + seen[uStr] = true + mirrorURLs = append(mirrorURLs, m.URL) + } } if len(mirrorURLs) == 0 { return nil, nil @@ -894,12 +888,12 @@ func (o AppendOptions) mirrorGateway(ctx context.Context, lr LogReader, httpClie // witnessCheckpoint takes care of witnessing the given checkpoint with the provided witness policy. // Returns signatures from witnesses, ready to append to the checkpoint, or an error. -func witnessCheckpoint(ctx context.Context, cosign cosigSource, policy *WitnessGroup, cp []byte, cpSize uint64, failOpen bool, greedy bool) ([]byte, error) { +func witnessCheckpoint(ctx context.Context, cosign cosigSource, wPol policy.TLogPolicy, cp []byte, cpSize uint64, failOpen bool, greedy bool) ([]byte, error) { return otel.Trace(ctx, "tessera.CheckpointPublisher.Witness", tracer, func(ctx context.Context, span trace.Span) ([]byte, error) { start := time.Now() witAttr := []attribute.KeyValue{} - sigs, err := gatherCosignatures(ctx, "witness", cosign, policy, cp, cpSize, failOpen, greedy) + sigs, err := gatherCosignatures(ctx, "witness", cosign, wPol, cp, cpSize, failOpen, greedy) if err != nil { if !errors.Is(err, errFailedOpen) { appenderWitnessRequests.Add(ctx, 1, metric.WithAttributes(attribute.String("error.type", "failed"))) @@ -918,9 +912,9 @@ func witnessCheckpoint(ctx context.Context, cosign cosigSource, policy *WitnessG // 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, cosign cosigSource, policy *WitnessGroup, cp []byte, cpSize uint64, failOpen bool, greedy bool) ([]byte, error) { +func mirrorCheckpoint(ctx context.Context, cosign cosigSource, mPol policy.TLogPolicy, cp []byte, cpSize uint64, failOpen bool, greedy bool) ([]byte, error) { return otel.Trace(ctx, "tessera.CheckpointPublisher.Mirror", tracer, func(ctx context.Context, span trace.Span) ([]byte, error) { - sigs, err := gatherCosignatures(ctx, "mirror", cosign, policy, cp, cpSize, failOpen, greedy) + sigs, err := gatherCosignatures(ctx, "mirror", cosign, mPol, cp, cpSize, failOpen, greedy) if err != nil { if !errors.Is(err, errFailedOpen) { slog.WarnContext(ctx, "Failed to collect mirror signatures", slog.Any("error", err)) @@ -932,6 +926,8 @@ func mirrorCheckpoint(ctx context.Context, cosign cosigSource, policy *WitnessGr } // cosigSource defines a function that can be called to fetch cosignatures. +// Implementations should send cosignatures via the returned channel as they become available, and MUST +// close the channel once no further signatures will be sent, or the context is cancelled. type cosigSource func(ctx context.Context, cp []byte, cpSize uint64) <-chan []byte // errFailedOpen is returned by gatherCosignatures if it did not get sufficient cosignatures to satisfy @@ -940,13 +936,11 @@ var errFailedOpen = errors.New("failed-open") // gatherCosignatures gathers signatures from a source, applying a policy to determine if the signatures are sufficient. // It returns a set of signatures which satisfy the policy (potentially more than required if greedy is true), or an error if the policy is not met and failOpen is false. -func gatherCosignatures(ctx context.Context, name string, fetcher cosigSource, policy *WitnessGroup, cp []byte, cpSize uint64, failOpen bool, greedy bool) ([]byte, error) { - maxExpectedResponses := len(policy.WitnessEndpoints()) - +func gatherCosignatures(ctx context.Context, name string, fetcher cosigSource, pol policy.TLogPolicy, cp []byte, cpSize uint64, failOpen bool, greedy bool) ([]byte, error) { // checkPolicy checks if the provided signatures satisfy the given policy. checkPolicy := func(sigs []byte, failOpen bool) ([]byte, error) { newCP := append(slices.Clone(cp), sigs...) - if policy.Satisfied(newCP) { + if pol.Satisfied(newCP) { return sigs, nil } if failOpen { @@ -961,7 +955,6 @@ func gatherCosignatures(ctx context.Context, name string, fetcher cosigSource, p // or the context is done. collectSigs := func(ctx context.Context, sigCh <-chan []byte) ([]byte, error) { var sigBlock bytes.Buffer - gotResponses := 0 for { select { case <-ctx.Done(): @@ -972,19 +965,20 @@ func gatherCosignatures(ctx context.Context, name string, fetcher cosigSource, p return sigs, pErr case sig, ok := <-sigCh: if !ok { - // No more signatures are coming. + // The source has closed the channel, no more signatures will be coming. + // So check what we have against the policy, and return accordingly. sigs, pErr := checkPolicy(sigBlock.Bytes(), failOpen) if pErr != nil { pErr = fmt.Errorf("%w: no more signatures available", pErr) } return sigs, pErr } - gotResponses++ + sigBlock.Write(sig) // If we're greedy, we need to keep collecting until we've got all the responses // (or the context is cancelled). // Otherwise we can return as soon as we've met the policy. - if !greedy || gotResponses == maxExpectedResponses { + if !greedy { // Don't allow failOpen here, or we'll break out of the collection loop prematurely. sigs, err := checkPolicy(sigBlock.Bytes(), false) if err == nil { @@ -996,7 +990,19 @@ func gatherCosignatures(ctx context.Context, name string, fetcher cosigSource, p } return otel.Trace(ctx, "tessera.gatherCosignatures", tracer, func(ctx context.Context, span trace.Span) ([]byte, error) { - if len(policy.Components) == 0 { + if len(pol.Witnesses) == 0 { + return nil, nil + } + + // A policy can name witnesses and yet be satisfied by the empty set of cosignatures, + // e.g. one whose quorum is "none". There's nothing to wait for in that case, so publish + // straight away rather than delaying every checkpoint by a witness round-trip (or, if + // the witnesses are unreachable, by the full timeout). + // + // Greedy is the exception: there we've been explicitly asked to collect whatever surplus + // cosignatures we can within the time available. + if !greedy && pol.Satisfied(cp) { + span.AddEvent("Policy satisfied with no cosignatures") return nil, nil } @@ -1157,14 +1163,13 @@ func (o *AppendOptions) WithCheckpointPublicationTimeout(timeout time.Duration) return o } -// 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. +// WithWitnessPolicy 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 policy. +// The checkpoint will be accepted for publishing when a sufficient number of witnesses to satisfy +// the policy have responded. // -// If this method is not called, then the default empty WitnessGroup will be used, which contacts zero -// witnesses and requires zero witnesses in order to publish. -func (o *AppendOptions) WithWitnesses(witnesses WitnessGroup, opts *WitnessOptions) *AppendOptions { +// If this method is not called, then witnessing will not be performed. +func (o *AppendOptions) WithWitnessPolicy(witnessPolicy policy.TLogPolicy, opts *WitnessOptions) *AppendOptions { if opts == nil { opts = &WitnessOptions{} } @@ -1172,20 +1177,26 @@ func (o *AppendOptions) WithWitnesses(witnesses WitnessGroup, opts *WitnessOptio opts.Timeout = DefaultWitnessTimeout } - o.witnesses = witnesses + o.witnessPolicy = witnessPolicy o.witnessOpts = *opts return o } -// WithMirrors configures the set of tlog-mirror servers that Tessera will contact in order to obtain +// WithWitnesses configures the set of witnesses that Tessera will contact in order to cosign +// a checkpoint before publishing it. +func (o *AppendOptions) WithWitnesses(witnesses WitnessGroup, opts *WitnessOptions) *AppendOptions { + return o.WithWitnessPolicy(witnesses.toPolicy(), opts) +} + +// WithMirrorPolicy configures the set of tlog-mirror servers that Tessera will contact in order to obtain // 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 +// Requests will be sent to every mirror referenced by the policy 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 policy // have responded. // -// 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 this method is not called, then mirroring will not be performed. +func (o *AppendOptions) WithMirrorPolicy(mirrorPolicy policy.TLogPolicy, opts *MirroringOptions) *AppendOptions { if opts == nil { opts = &MirroringOptions{} } @@ -1193,11 +1204,17 @@ func (o *AppendOptions) WithMirrors(mirrors WitnessGroup, opts *MirroringOptions opts.Timeout = DefaultMirrorTimeout } - o.mirrors = mirrors + o.mirrorPolicy = mirrorPolicy o.mirrorOpts = *opts return o } +// WithMirrors configures the set of tlog-mirror servers that Tessera will contact in order to obtain +// mirror cosignatures on a checkpoint before publishing it. +func (o *AppendOptions) WithMirrors(mirrors WitnessGroup, opts *MirroringOptions) *AppendOptions { + return o.WithMirrorPolicy(mirrors.toPolicy(), opts) +} + // WitnessOptions contains extra optional configuration for how Tessera should use/interact with // a user-provided WitnessGroup policy. type WitnessOptions struct { @@ -1290,14 +1307,29 @@ func (o *AppendOptions) LogValue() slog.Value { attrs = append(attrs, slog.Any("additionalSigners", names)) } - if len(o.witnesses.Components) > 0 { - endpoints := o.witnesses.WitnessEndpoints() - urls := make([]string, 0, len(endpoints)) - for u := range endpoints { - urls = append(urls, u) + if len(o.witnessPolicy.Witnesses) > 0 { + urls := make([]string, 0, len(o.witnessPolicy.Witnesses)) + for _, w := range o.witnessPolicy.Witnesses { + if w.URL != nil { + urls = append(urls, w.URL.String()) + } } attrs = append(attrs, slog.Group("witnesses", - slog.Int("threshold", o.witnesses.N), + slog.Int("groups", len(o.witnessPolicy.Groups)), + slog.Any("quorum", o.witnessPolicy.Quorum), + slog.Any("endpoints", urls), + )) + } + if len(o.mirrorPolicy.Witnesses) > 0 { + urls := make([]string, 0, len(o.mirrorPolicy.Witnesses)) + for _, w := range o.mirrorPolicy.Witnesses { + if w.URL != nil { + urls = append(urls, w.URL.String()) + } + } + attrs = append(attrs, slog.Group("mirrors", + slog.Int("groups", len(o.mirrorPolicy.Groups)), + slog.Any("quorum", o.mirrorPolicy.Quorum), slog.Any("endpoints", urls), )) } diff --git a/append_lifecycle_test.go b/append_lifecycle_test.go index d761357b6..16d2b4623 100644 --- a/append_lifecycle_test.go +++ b/append_lifecycle_test.go @@ -32,6 +32,7 @@ import ( "time" f_note "github.com/transparency-dev/formats/note" + "github.com/transparency-dev/formats/policy" "github.com/transparency-dev/merkle/rfc6962" "github.com/transparency-dev/witness/config" "github.com/transparency-dev/witness/persistence/inmemory" @@ -97,23 +98,23 @@ func TestAppendOptionsValid(t *testing.T) { name: "Valid: CheckpointPublicationTimeout < WitnessTimeout adjusts publication timeout", opts: NewAppendOptions(). WithCheckpointSigner(mustCreateSigner(t, testSignerKey)). - WithCheckpointPublicationTimeout(1 * time.Second). - WithWitnesses(NewWitnessGroup(0), &WitnessOptions{Timeout: 10 * time.Second}), + WithCheckpointPublicationTimeout(1*time.Second). + WithWitnessPolicy(NewWitnessGroup(0).toPolicy(), &WitnessOptions{Timeout: 10 * time.Second}), wantPublicationTimeout: 10 * time.Second, }, { name: "Valid: CheckpointPublicationTimeout < MirrorTimeout adjusts publication timeout", opts: NewAppendOptions(). WithCheckpointSigner(mustCreateSigner(t, testSignerKey)). - WithCheckpointPublicationTimeout(1 * time.Second). - WithMirrors(NewWitnessGroup(0), &MirroringOptions{Timeout: 15 * time.Second}), + WithCheckpointPublicationTimeout(1*time.Second). + WithMirrorPolicy(NewWitnessGroup(0).toPolicy(), &MirroringOptions{Timeout: 15 * time.Second}), wantPublicationTimeout: 15 * time.Second, }, { name: "Valid: CheckpointPublicationTimeout adjusts to max of WitnessTimeout and MirrorTimeout", opts: NewAppendOptions(). WithCheckpointSigner(mustCreateSigner(t, testSignerKey)). - WithCheckpointPublicationTimeout(1 * time.Second). - WithWitnesses(NewWitnessGroup(0), &WitnessOptions{Timeout: 10 * time.Second}). - WithMirrors(NewWitnessGroup(0), &MirroringOptions{Timeout: 20 * time.Second}), + WithCheckpointPublicationTimeout(1*time.Second). + WithWitnessPolicy(NewWitnessGroup(0).toPolicy(), &WitnessOptions{Timeout: 10 * time.Second}). + WithMirrorPolicy(NewWitnessGroup(0).toPolicy(), &MirroringOptions{Timeout: 20 * time.Second}), wantPublicationTimeout: 20 * time.Second, }, { name: "Error: CheckpointRepublishInterval < CheckpointInterval", @@ -268,7 +269,7 @@ func TestWithMirrors(t *testing.T) { if err != nil { t.Fatalf("failed to create witness: %v", err) } - mirrors := NewWitnessGroup(1, wit) + mirrors := NewWitnessGroup(1, wit).toPolicy() for _, test := range []struct { desc string @@ -302,10 +303,7 @@ func TestWithMirrors(t *testing.T) { }, } { 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)) - } + opts := NewAppendOptions().WithMirrorPolicy(mirrors, test.mirrorOpts) if got, want := opts.mirrorOpts.Timeout, test.expectTimeout; got != want { t.Errorf("expected timeout %v, got %v", want, got) } @@ -316,9 +314,9 @@ func TestWithMirrors(t *testing.T) { } } -func TestWithWitnesses(t *testing.T) { +func TestWithWitnessPolicy(t *testing.T) { wit := mustNewWitness(t, testWit1VKey, "https://witness.example.com") - witnesses := NewWitnessGroup(1, wit) + witnesses := NewWitnessGroup(1, wit).toPolicy() for _, test := range []struct { desc string @@ -358,10 +356,7 @@ func TestWithWitnesses(t *testing.T) { }, } { t.Run(test.desc, func(t *testing.T) { - opts := NewAppendOptions().WithWitnesses(witnesses, test.witnessOpts) - if len(opts.witnesses.Components) != 1 { - t.Errorf("expected 1 witness component, got %d", len(opts.witnesses.Components)) - } + opts := NewAppendOptions().WithWitnessPolicy(witnesses, test.witnessOpts) if got, want := opts.witnessOpts.Timeout, test.expectTimeout; got != want { t.Errorf("expected timeout %v, got %v", want, got) } @@ -375,6 +370,52 @@ func TestWithWitnesses(t *testing.T) { } } +func TestWithWitnesses_BackwardsCompatibility(t *testing.T) { + wit := mustNewWitness(t, testWit1VKey, "https://witness.example.com") + wg := NewWitnessGroup(1, wit) + opts := NewAppendOptions().WithWitnesses(wg, &WitnessOptions{Timeout: 5 * time.Second}) + if got, want := opts.witnessOpts.Timeout, 5*time.Second; got != want { + t.Errorf("expected timeout %v, got %v", want, got) + } + if got, want := len(opts.witnessPolicy.Witnesses), 1; got != want { + t.Errorf("expected 1 witness in policy, got %d", got) + } +} + +func TestWithMirrors_BackwardsCompatibility(t *testing.T) { + u, _ := url.Parse("https://mirror.example.com") + wit, _ := NewWitness(testWit1VKey, u) + wg := NewWitnessGroup(1, wit) + opts := NewAppendOptions().WithMirrors(wg, &MirroringOptions{Timeout: 5 * time.Second}) + if got, want := opts.mirrorOpts.Timeout, 5*time.Second; got != want { + t.Errorf("expected timeout %v, got %v", want, got) + } + if got, want := len(opts.mirrorPolicy.Witnesses), 1; got != want { + t.Errorf("expected 1 mirror in policy, got %d", got) + } +} + +func TestMirrorGateway_DeduplicateURLs(t *testing.T) { + mURL, _ := url.Parse("https://mirror.example.com") + lr := newFakeLogReaderForTest(t) + + pol := policy.TLogPolicy{ + Witnesses: []policy.Witness{ + {Name: "m1", URL: mURL, VKey: testMirrorVKey}, + {Name: "m2", URL: mURL, VKey: testMirrorVKey}, + }, + Quorum: "m1", + } + opts := NewAppendOptions().WithMirrorPolicy(pol, nil) + gw, err := opts.mirrorGateway(t.Context(), lr, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if gw == nil { + t.Fatal("expected non-nil mirror gateway") + } +} + const ( testWit1VKey = "Wit1+55ee4561+AVhZSmQj9+SoL+p/nN0Hh76xXmF7QcHfytUrI1XfSClk" testWit1SKey = "PRIVATE+KEY+Wit1+55ee4561+AeadRiG7XM4XiieCHzD8lxysXMwcViy5nYsoXURWGrlE" @@ -432,7 +473,7 @@ func TestGatherCosignatures(t *testing.T) { for _, test := range []struct { desc string - policy WitnessGroup + policy policy.TLogPolicy fetcher func(ctx context.Context, cp []byte, cpSize uint64) <-chan []byte timeout time.Duration failOpen bool @@ -443,7 +484,7 @@ func TestGatherCosignatures(t *testing.T) { }{ { desc: "empty policy", - policy: WitnessGroup{}, + policy: policy.TLogPolicy{}, fetcher: func(ctx context.Context, cp []byte, cpSize uint64) <-chan []byte { ch := make(chan []byte) close(ch) @@ -452,7 +493,7 @@ func TestGatherCosignatures(t *testing.T) { }, { desc: "non-greedy stops after quorum is satisfied (1 of 2)", - policy: NewWitnessGroup(1, wit1, wit2), + policy: NewWitnessGroup(1, wit1, wit2).toPolicy(), fetcher: func(ctx context.Context, cp []byte, cpSize uint64) <-chan []byte { ch := make(chan []byte, 2) ch <- sig1 @@ -464,7 +505,7 @@ func TestGatherCosignatures(t *testing.T) { }, { desc: "greedy gathers surplus signatures (2 of 3 required, 3 provided)", - policy: NewWitnessGroup(2, wit1, wit2, wit3), + policy: NewWitnessGroup(2, wit1, wit2, wit3).toPolicy(), fetcher: func(ctx context.Context, cp []byte, cpSize uint64) <-chan []byte { ch := make(chan []byte, 3) ch <- sig1 @@ -477,7 +518,7 @@ func TestGatherCosignatures(t *testing.T) { }, { desc: "greedy succeeds when quorum is met and channel closes without further signatures (1 of 2 required, 1 provided)", - policy: NewWitnessGroup(1, wit1, wit2), + policy: NewWitnessGroup(1, wit1, wit2).toPolicy(), fetcher: func(ctx context.Context, cp []byte, cpSize uint64) <-chan []byte { ch := make(chan []byte, 1) ch <- sig1 @@ -489,7 +530,7 @@ func TestGatherCosignatures(t *testing.T) { }, { desc: "greedy fails when quorum is not met and channel closes (failOpen=false)", - policy: NewWitnessGroup(2, wit1, wit2), + policy: NewWitnessGroup(2, wit1, wit2).toPolicy(), fetcher: func(ctx context.Context, cp []byte, cpSize uint64) <-chan []byte { ch := make(chan []byte, 1) ch <- sig1 @@ -502,7 +543,7 @@ func TestGatherCosignatures(t *testing.T) { }, { desc: "greedy fails open when quorum is not met and channel closes (failOpen=true)", - policy: NewWitnessGroup(2, wit1, wit2), + policy: NewWitnessGroup(2, wit1, wit2).toPolicy(), fetcher: func(ctx context.Context, cp []byte, cpSize uint64) <-chan []byte { ch := make(chan []byte, 1) ch <- sig1 @@ -516,7 +557,7 @@ func TestGatherCosignatures(t *testing.T) { }, { desc: "greedy fails when quorum is not met on timeout (failOpen=false)", - policy: NewWitnessGroup(2, wit1, wit2), + policy: NewWitnessGroup(2, wit1, wit2).toPolicy(), fetcher: func(ctx context.Context, cp []byte, cpSize uint64) <-chan []byte { ch := make(chan []byte, 1) ch <- sig1 @@ -529,7 +570,7 @@ func TestGatherCosignatures(t *testing.T) { }, { desc: "greedy fails open when quorum is not met on timeout (failOpen=true)", - policy: NewWitnessGroup(2, wit1, wit2), + policy: NewWitnessGroup(2, wit1, wit2).toPolicy(), fetcher: func(ctx context.Context, cp []byte, cpSize uint64) <-chan []byte { ch := make(chan []byte, 1) ch <- sig1 @@ -549,7 +590,7 @@ func TestGatherCosignatures(t *testing.T) { ctx, cancel = context.WithTimeout(ctx, test.timeout) defer cancel() } - sigs, err := gatherCosignatures(ctx, "witness", test.fetcher, &test.policy, signedCP, 5, test.failOpen, test.greedy) + sigs, err := gatherCosignatures(ctx, "witness", test.fetcher, test.policy, signedCP, 5, test.failOpen, test.greedy) switch { case test.expectFailedOpen: if !errors.Is(err, errFailedOpen) { @@ -628,7 +669,7 @@ func TestCheckpointPublisher(t *testing.T) { if err != nil { t.Fatalf("failed to create witness 1: %v", err) } - witnesses := NewWitnessGroup(1, wit1) + witnesses := NewWitnessGroup(1, wit1).toPolicy() wit1Verifier, err := f_note.NewVerifierForCosignatureV1(testWit1VKey) if err != nil { t.Fatalf("failed to create witness 1 verifier: %v", err) @@ -651,7 +692,7 @@ func TestCheckpointPublisher(t *testing.T) { t.Fatalf("failed to create witness 2 verifier: %v", err) } - multiWitnesses := NewWitnessGroup(1, wit1, wit2) + multiWitnesses := NewWitnessGroup(1, wit1, wit2).toPolicy() mirrorServer := httptest.NewServer(newMirrorHandler(t, testMirrorSKey)) t.Cleanup(mirrorServer.Close) @@ -665,7 +706,7 @@ func TestCheckpointPublisher(t *testing.T) { if err != nil { t.Fatalf("failed to create mirror: %v", err) } - mirrors := NewWitnessGroup(1, m) + mirrors := NewWitnessGroup(1, m).toPolicy() mirrorVerifier, err := f_note.NewVerifierForCosignatureV1(testMirrorVKey) if err != nil { t.Fatalf("failed to create mirror verifier: %v", err) @@ -686,43 +727,43 @@ func TestCheckpointPublisher(t *testing.T) { }, { desc: "witnesses only", - opts: NewAppendOptions().WithCheckpointSigner(logSigner).WithWitnesses(witnesses, &WitnessOptions{Timeout: time.Second}), + opts: NewAppendOptions().WithCheckpointSigner(logSigner).WithWitnessPolicy(witnesses, &WitnessOptions{Timeout: time.Second}), expectCosignatures: []note.Verifier{wit1Verifier}, }, { desc: "mirrors only", - opts: NewAppendOptions().WithCheckpointSigner(logSigner).WithMirrors(mirrors, &MirroringOptions{Timeout: time.Second}), + opts: NewAppendOptions().WithCheckpointSigner(logSigner).WithMirrorPolicy(mirrors, &MirroringOptions{Timeout: time.Second}), expectCosignatures: []note.Verifier{mirrorVerifier}, }, { desc: "witnesses and mirrors", - opts: NewAppendOptions().WithCheckpointSigner(logSigner).WithWitnesses(witnesses, &WitnessOptions{Timeout: time.Second}).WithMirrors(mirrors, &MirroringOptions{Timeout: time.Second}), + opts: NewAppendOptions().WithCheckpointSigner(logSigner).WithWitnessPolicy(witnesses, &WitnessOptions{Timeout: time.Second}).WithMirrorPolicy(mirrors, &MirroringOptions{Timeout: time.Second}), expectCosignatures: []note.Verifier{wit1Verifier, mirrorVerifier}, }, { desc: "witness fails, failOpen=false", - opts: NewAppendOptions().WithCheckpointSigner(logSigner).WithWitnesses(witnesses, &WitnessOptions{FailOpen: false, Timeout: time.Second}), + opts: NewAppendOptions().WithCheckpointSigner(logSigner).WithWitnessPolicy(witnesses, &WitnessOptions{FailOpen: false, Timeout: time.Second}), witnessFails: true, expectErr: true, }, { desc: "witness fails, failOpen=true", - opts: NewAppendOptions().WithCheckpointSigner(logSigner).WithWitnesses(witnesses, &WitnessOptions{FailOpen: true, Timeout: time.Second}), + opts: NewAppendOptions().WithCheckpointSigner(logSigner).WithWitnessPolicy(witnesses, &WitnessOptions{FailOpen: true, Timeout: time.Second}), witnessFails: true, }, { desc: "multi witnesses greedy=false", - opts: NewAppendOptions().WithCheckpointSigner(logSigner).WithWitnesses(multiWitnesses, &WitnessOptions{Timeout: time.Second, Greedy: false}), + opts: NewAppendOptions().WithCheckpointSigner(logSigner).WithWitnessPolicy(multiWitnesses, &WitnessOptions{Timeout: time.Second, Greedy: false}), expectNumCosignatures: 1, }, { desc: "multi witnesses greedy=true", - opts: NewAppendOptions().WithCheckpointSigner(logSigner).WithWitnesses(multiWitnesses, &WitnessOptions{Timeout: time.Second, Greedy: true}), + opts: NewAppendOptions().WithCheckpointSigner(logSigner).WithWitnessPolicy(multiWitnesses, &WitnessOptions{Timeout: time.Second, Greedy: true}), expectCosignatures: []note.Verifier{wit1Verifier, wit2Verifier}, }, { desc: "multi witnesses greedy=true with one failing witness", - opts: NewAppendOptions().WithCheckpointSigner(logSigner).WithWitnesses(multiWitnesses, &WitnessOptions{Timeout: time.Second, Greedy: true}), + opts: NewAppendOptions().WithCheckpointSigner(logSigner).WithWitnessPolicy(multiWitnesses, &WitnessOptions{Timeout: time.Second, Greedy: true}), partialWitnessFails: true, expectCosignatures: []note.Verifier{wit1Verifier}, }, @@ -737,10 +778,10 @@ func TestCheckpointPublisher(t *testing.T) { failingURL, _ := url.Parse(failingWitnessServer.URL) failingWit, _ := NewWitness(testWit1VKey, failingURL) - failingWitnesses := NewWitnessGroup(1, failingWit) + failingWitnesses := NewWitnessGroup(1, failingWit).toPolicy() // Re-configure option to use failing witnesses - test.opts.WithWitnesses(failingWitnesses, &test.opts.witnessOpts) + test.opts.WithWitnessPolicy(failingWitnesses, &test.opts.witnessOpts) } if test.partialWitnessFails { failingWitnessServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -750,9 +791,9 @@ func TestCheckpointPublisher(t *testing.T) { failingURL, _ := url.Parse(failingWitnessServer.URL) failingWit, _ := NewWitness(testWit2VKey, failingURL) - partiallyFailingWitnesses := NewWitnessGroup(1, wit1, failingWit) + partiallyFailingWitnesses := NewWitnessGroup(1, wit1, failingWit).toPolicy() - test.opts.WithWitnesses(partiallyFailingWitnesses, &test.opts.witnessOpts) + test.opts.WithWitnessPolicy(partiallyFailingWitnesses, &test.opts.witnessOpts) } lr := newFakeLogReaderForTest(t) From 3afe70822e4b2b3519478567a7cc5fb160ecff90 Mon Sep 17 00:00:00 2001 From: Al Cutter Date: Tue, 8 Sep 2026 10:54:31 +0000 Subject: [PATCH 2/4] Migrate append_lifecycle_test --- append_lifecycle.go | 12 ++- append_lifecycle_test.go | 187 ++++++++++++++++++++++----------------- witness_test.go | 31 ++----- 3 files changed, 127 insertions(+), 103 deletions(-) diff --git a/append_lifecycle.go b/append_lifecycle.go index 77826f897..cc87834be 100644 --- a/append_lifecycle.go +++ b/append_lifecycle.go @@ -1185,7 +1185,11 @@ func (o *AppendOptions) WithWitnessPolicy(witnessPolicy policy.TLogPolicy, opts // WithWitnesses configures the set of witnesses that Tessera will contact in order to cosign // a checkpoint before publishing it. func (o *AppendOptions) WithWitnesses(witnesses WitnessGroup, opts *WitnessOptions) *AppendOptions { - return o.WithWitnessPolicy(witnesses.toPolicy(), opts) + p, err := witnesses.toPolicy() + if err != nil { + panic(fmt.Sprintf("invalid WitnessGroup: %v", err)) + } + return o.WithWitnessPolicy(p, opts) } // WithMirrorPolicy configures the set of tlog-mirror servers that Tessera will contact in order to obtain @@ -1212,7 +1216,11 @@ func (o *AppendOptions) WithMirrorPolicy(mirrorPolicy policy.TLogPolicy, opts *M // WithMirrors configures the set of tlog-mirror servers that Tessera will contact in order to obtain // mirror cosignatures on a checkpoint before publishing it. func (o *AppendOptions) WithMirrors(mirrors WitnessGroup, opts *MirroringOptions) *AppendOptions { - return o.WithMirrorPolicy(mirrors.toPolicy(), opts) + p, err := mirrors.toPolicy() + if err != nil { + panic(fmt.Sprintf("invalid WitnessGroup: %v", err)) + } + return o.WithMirrorPolicy(p, opts) } // WitnessOptions contains extra optional configuration for how Tessera should use/interact with diff --git a/append_lifecycle_test.go b/append_lifecycle_test.go index 16d2b4623..eb98f118f 100644 --- a/append_lifecycle_test.go +++ b/append_lifecycle_test.go @@ -99,22 +99,22 @@ func TestAppendOptionsValid(t *testing.T) { opts: NewAppendOptions(). WithCheckpointSigner(mustCreateSigner(t, testSignerKey)). WithCheckpointPublicationTimeout(1*time.Second). - WithWitnessPolicy(NewWitnessGroup(0).toPolicy(), &WitnessOptions{Timeout: 10 * time.Second}), + WithWitnessPolicy(policy.TLogPolicy{Quorum: "none"}, &WitnessOptions{Timeout: 10 * time.Second}), wantPublicationTimeout: 10 * time.Second, }, { name: "Valid: CheckpointPublicationTimeout < MirrorTimeout adjusts publication timeout", opts: NewAppendOptions(). WithCheckpointSigner(mustCreateSigner(t, testSignerKey)). WithCheckpointPublicationTimeout(1*time.Second). - WithMirrorPolicy(NewWitnessGroup(0).toPolicy(), &MirroringOptions{Timeout: 15 * time.Second}), + WithMirrorPolicy(policy.TLogPolicy{Quorum: "none"}, &MirroringOptions{Timeout: 15 * time.Second}), wantPublicationTimeout: 15 * time.Second, }, { name: "Valid: CheckpointPublicationTimeout adjusts to max of WitnessTimeout and MirrorTimeout", opts: NewAppendOptions(). WithCheckpointSigner(mustCreateSigner(t, testSignerKey)). WithCheckpointPublicationTimeout(1*time.Second). - WithWitnessPolicy(NewWitnessGroup(0).toPolicy(), &WitnessOptions{Timeout: 10 * time.Second}). - WithMirrorPolicy(NewWitnessGroup(0).toPolicy(), &MirroringOptions{Timeout: 20 * time.Second}), + WithWitnessPolicy(policy.TLogPolicy{Quorum: "none"}, &WitnessOptions{Timeout: 10 * time.Second}). + WithMirrorPolicy(policy.TLogPolicy{Quorum: "none"}, &MirroringOptions{Timeout: 20 * time.Second}), wantPublicationTimeout: 20 * time.Second, }, { name: "Error: CheckpointRepublishInterval < CheckpointInterval", @@ -269,7 +269,7 @@ func TestWithMirrors(t *testing.T) { if err != nil { t.Fatalf("failed to create witness: %v", err) } - mirrors := NewWitnessGroup(1, wit).toPolicy() + mirrorGrp := NewWitnessGroup(1, wit) for _, test := range []struct { desc string @@ -303,7 +303,7 @@ func TestWithMirrors(t *testing.T) { }, } { t.Run(test.desc, func(t *testing.T) { - opts := NewAppendOptions().WithMirrorPolicy(mirrors, test.mirrorOpts) + opts := NewAppendOptions().WithMirrors(mirrorGrp, test.mirrorOpts) if got, want := opts.mirrorOpts.Timeout, test.expectTimeout; got != want { t.Errorf("expected timeout %v, got %v", want, got) } @@ -315,8 +315,11 @@ func TestWithMirrors(t *testing.T) { } func TestWithWitnessPolicy(t *testing.T) { - wit := mustNewWitness(t, testWit1VKey, "https://witness.example.com") - witnesses := NewWitnessGroup(1, wit).toPolicy() + witnesses := policy.TLogPolicy{} + policy := fmt.Appendf(nil, "witness w1 %s %s\nquorum w1\n", testWit1VKey, "https://witness.example.com") + if err := witnesses.Unmarshal(policy); err != nil { + t.Fatalf("failed to unmarshal witness policy: %v", err) + } for _, test := range []struct { desc string @@ -417,15 +420,15 @@ func TestMirrorGateway_DeduplicateURLs(t *testing.T) { } const ( - testWit1VKey = "Wit1+55ee4561+AVhZSmQj9+SoL+p/nN0Hh76xXmF7QcHfytUrI1XfSClk" - testWit1SKey = "PRIVATE+KEY+Wit1+55ee4561+AeadRiG7XM4XiieCHzD8lxysXMwcViy5nYsoXURWGrlE" - testWit2VKey = "Wit2+85ecc407+AWVbwFJte9wMQIPSnEnj4KibeO6vSIOEDUTDp3o63c2x" - testWit2SKey = "PRIVATE+KEY+Wit2+85ecc407+AfPTvxw5eUcqSgivo2vaiC7JPOMUZ/9baHPSDrWqgdGm" - testWit3VKey = "Wit3+d3ed3be7+ASb6Uz1+fxAcXkMvDd7nGa3FjDce7LxIKmbbTCT0MpVn" - testWit3SKey = "PRIVATE+KEY+Wit3+d3ed3be7+AR2Kg8k6ccBr5QXz5SHtnkOS4UGQGEQaWi6Gfr6Mm3X5" - - testMirrorVKey = "Mirror1+66ee4561+AVhZSmQj9+SoL+p/nN0Hh76xXmF7QcHfytUrI1XfSClk" - testMirrorSKey = "PRIVATE+KEY+Mirror1+66ee4561+AeadRiG7XM4XiieCHzD8lxysXMwcViy5nYsoXURWGrlE" + testWit1VKey = "Wit1+4dd489c8+BDd/u4KCwMnyfkbOqopYZJIGSuMxMnjGav6Pjb9W8Y3c" + testWit1SKey = "PRIVATE+KEY+Wit1+362a3b47+AVq3ou1kOLb/aTkJLoMwbMULJQNr8EQVGbpyZxMEZMyT" + testWit2VKey = "Wit2+ef71459d+BJ1w/MdWovBzZtRD4pNwmb9SHl1U+hZzCMsgx6MmMJ0L" + testWit2SKey = "PRIVATE+KEY+Wit2+4bf908f8+AXLjVp1/sY1o5exVavpVt8zWIVFAD7ejqaMBU38CoYgg" + testWit3VKey = "Wit3+e1fc6196+BO1XsCjtkV0G56JUYj7n6LykElL1GcNo1BytsQMsRjyZ" + testWit3SKey = "PRIVATE+KEY+Wit3+86588ce7+AS6qbV1WaGUoVgAz3CajG9iCm1pLZ5eUTKBD6XxJVl5x" + + testMirrorVKey = "Mirror1+e2466b7b+BECHU/Mq/HN+4Nmsxw/NxmRTZ1dvOkgf3IkAS/+XJ9Za" + testMirrorSKey = "PRIVATE+KEY+Mirror1+eccc0fa7+ATLe/CQcL8aCY0TofdnDKFX43pZj6NOY8BNQIgqJ885A" ) func createCosignature(t *testing.T, baseNote *note.Note, witnessSKey string) []byte { @@ -452,9 +455,6 @@ func TestGatherCosignatures(t *testing.T) { t.Fatalf("failed to create log verifier: %v", err) } - wit1 := mustNewWitness(t, testWit1VKey, "https://wit1.example.com") - wit2 := mustNewWitness(t, testWit2VKey, "https://wit2.example.com") - wit3 := mustNewWitness(t, testWit3VKey, "https://wit3.example.com") wit1Verifier, _ := f_note.NewVerifierForCosignatureV1(testWit1VKey) wit2Verifier, _ := f_note.NewVerifierForCosignatureV1(testWit2VKey) wit3Verifier, _ := f_note.NewVerifierForCosignatureV1(testWit3VKey) @@ -471,11 +471,12 @@ func TestGatherCosignatures(t *testing.T) { sig2 := createCosignature(t, n, testWit2SKey) sig3 := createCosignature(t, n, testWit3SKey) + timeout := time.Second + for _, test := range []struct { desc string policy policy.TLogPolicy fetcher func(ctx context.Context, cp []byte, cpSize uint64) <-chan []byte - timeout time.Duration failOpen bool greedy bool expectCosignatures []note.Verifier @@ -487,13 +488,15 @@ func TestGatherCosignatures(t *testing.T) { policy: policy.TLogPolicy{}, fetcher: func(ctx context.Context, cp []byte, cpSize uint64) <-chan []byte { ch := make(chan []byte) - close(ch) + defer close(ch) return ch }, }, { - desc: "non-greedy stops after quorum is satisfied (1 of 2)", - policy: NewWitnessGroup(1, wit1, wit2).toPolicy(), + desc: "non-greedy stops after quorum is satisfied (1 of 2)", + policy: makeGroupPolicy(t, 1, []keyUrl{ + {key: testWit1VKey, url: "https://wit1.example.com"}, + {key: testWit2VKey, url: "https://wit2.example.com"}}), fetcher: func(ctx context.Context, cp []byte, cpSize uint64) <-chan []byte { ch := make(chan []byte, 2) ch <- sig1 @@ -504,10 +507,14 @@ func TestGatherCosignatures(t *testing.T) { expectCosignatures: []note.Verifier{wit1Verifier}, }, { - desc: "greedy gathers surplus signatures (2 of 3 required, 3 provided)", - policy: NewWitnessGroup(2, wit1, wit2, wit3).toPolicy(), + desc: "greedy gathers surplus signatures (2 of 3 required, 3 provided)", + policy: makeGroupPolicy(t, 2, []keyUrl{ + {key: testWit1VKey, url: "https://wit1.example.com"}, + {key: testWit2VKey, url: "https://wit2.example.com"}, + {key: testWit3VKey, url: "https://wit3.example.com"}}), fetcher: func(ctx context.Context, cp []byte, cpSize uint64) <-chan []byte { ch := make(chan []byte, 3) + defer close(ch) ch <- sig1 ch <- sig2 ch <- sig3 @@ -517,24 +524,28 @@ func TestGatherCosignatures(t *testing.T) { expectCosignatures: []note.Verifier{wit1Verifier, wit2Verifier, wit3Verifier}, }, { - desc: "greedy succeeds when quorum is met and channel closes without further signatures (1 of 2 required, 1 provided)", - policy: NewWitnessGroup(1, wit1, wit2).toPolicy(), + desc: "greedy succeeds when quorum is met and channel closes without further signatures (1 of 2 required, 1 provided)", + policy: makeGroupPolicy(t, 1, []keyUrl{ + {key: testWit1VKey, url: "https://wit1.example.com"}, + {key: testWit2VKey, url: "https://wit2.example.com"}}), fetcher: func(ctx context.Context, cp []byte, cpSize uint64) <-chan []byte { ch := make(chan []byte, 1) + defer close(ch) ch <- sig1 - close(ch) return ch }, greedy: true, expectCosignatures: []note.Verifier{wit1Verifier}, }, { - desc: "greedy fails when quorum is not met and channel closes (failOpen=false)", - policy: NewWitnessGroup(2, wit1, wit2).toPolicy(), + desc: "greedy fails when quorum is not met and channel closes (failOpen=false)", + policy: makeGroupPolicy(t, 2, []keyUrl{ + {key: testWit1VKey, url: "https://wit1.example.com"}, + {key: testWit2VKey, url: "https://wit2.example.com"}}), fetcher: func(ctx context.Context, cp []byte, cpSize uint64) <-chan []byte { ch := make(chan []byte, 1) + defer close(ch) ch <- sig1 - close(ch) return ch }, greedy: true, @@ -542,12 +553,14 @@ func TestGatherCosignatures(t *testing.T) { expectErr: true, }, { - desc: "greedy fails open when quorum is not met and channel closes (failOpen=true)", - policy: NewWitnessGroup(2, wit1, wit2).toPolicy(), + desc: "greedy fails open when quorum is not met and channel closes (failOpen=true)", + policy: makeGroupPolicy(t, 2, []keyUrl{ + {key: testWit1VKey, url: "https://wit1.example.com"}, + {key: testWit2VKey, url: "https://wit2.example.com"}}), fetcher: func(ctx context.Context, cp []byte, cpSize uint64) <-chan []byte { ch := make(chan []byte, 1) + defer close(ch) ch <- sig1 - close(ch) return ch }, greedy: true, @@ -556,27 +569,29 @@ func TestGatherCosignatures(t *testing.T) { expectCosignatures: []note.Verifier{wit1Verifier}, }, { - desc: "greedy fails when quorum is not met on timeout (failOpen=false)", - policy: NewWitnessGroup(2, wit1, wit2).toPolicy(), + desc: "greedy fails when quorum is not met on timeout (failOpen=false)", + policy: makeGroupPolicy(t, 2, []keyUrl{ + {key: testWit1VKey, url: "https://wit1.example.com"}, + {key: testWit2VKey, url: "https://wit2.example.com"}}), fetcher: func(ctx context.Context, cp []byte, cpSize uint64) <-chan []byte { ch := make(chan []byte, 1) ch <- sig1 return ch }, - timeout: 50 * time.Millisecond, greedy: true, failOpen: false, expectErr: true, }, { - desc: "greedy fails open when quorum is not met on timeout (failOpen=true)", - policy: NewWitnessGroup(2, wit1, wit2).toPolicy(), + desc: "greedy fails open when quorum is not met on timeout (failOpen=true)", + policy: makeGroupPolicy(t, 2, []keyUrl{ + {key: testWit1VKey, url: "https://wit1.example.com"}, + {key: testWit2VKey, url: "https://wit2.example.com"}}), fetcher: func(ctx context.Context, cp []byte, cpSize uint64) <-chan []byte { ch := make(chan []byte, 1) ch <- sig1 return ch }, - timeout: 50 * time.Millisecond, greedy: true, failOpen: true, expectFailedOpen: true, @@ -584,12 +599,8 @@ func TestGatherCosignatures(t *testing.T) { }, } { t.Run(test.desc, func(t *testing.T) { - ctx := t.Context() - if test.timeout > 0 { - var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, test.timeout) - defer cancel() - } + ctx, cancel := context.WithTimeout(t.Context(), timeout) + defer cancel() sigs, err := gatherCosignatures(ctx, "witness", test.fetcher, test.policy, signedCP, 5, test.failOpen, test.greedy) switch { case test.expectFailedOpen: @@ -664,17 +675,13 @@ func TestCheckpointPublisher(t *testing.T) { if err != nil { t.Fatalf("failed to parse witness server 1 url: %v", err) } - - wit1, err := NewWitness(testWit1VKey, witnessServerURL1) - if err != nil { - t.Fatalf("failed to create witness 1: %v", err) - } - witnesses := NewWitnessGroup(1, wit1).toPolicy() wit1Verifier, err := f_note.NewVerifierForCosignatureV1(testWit1VKey) if err != nil { t.Fatalf("failed to create witness 1 verifier: %v", err) } + witnessPolicy := makeGroupPolicy(t, 1, []keyUrl{{key: testWit1VKey, url: witnessServerURL1.String()}}) + witnessServer2 := httptest.NewServer(newWitnessHandler(t, logVerifier, testWit2SKey)) t.Cleanup(witnessServer2.Close) @@ -682,17 +689,14 @@ func TestCheckpointPublisher(t *testing.T) { if err != nil { t.Fatalf("failed to parse witness server 2 url: %v", err) } - - wit2, err := NewWitness(testWit2VKey, witnessServerURL2) - if err != nil { - t.Fatalf("failed to create witness 2: %v", err) - } wit2Verifier, err := f_note.NewVerifierForCosignatureV1(testWit2VKey) if err != nil { t.Fatalf("failed to create witness 2 verifier: %v", err) } - multiWitnesses := NewWitnessGroup(1, wit1, wit2).toPolicy() + multiWitnessPolicy := makeGroupPolicy(t, 1, []keyUrl{ + {key: testWit1VKey, url: witnessServerURL1.String()}, + {key: testWit2VKey, url: witnessServerURL2.String()}}) mirrorServer := httptest.NewServer(newMirrorHandler(t, testMirrorSKey)) t.Cleanup(mirrorServer.Close) @@ -701,17 +705,13 @@ func TestCheckpointPublisher(t *testing.T) { if err != nil { t.Fatalf("failed to parse mirror server url: %v", err) } - - m, err := NewWitness(testMirrorVKey, mirrorServerURL) - if err != nil { - t.Fatalf("failed to create mirror: %v", err) - } - mirrors := NewWitnessGroup(1, m).toPolicy() mirrorVerifier, err := f_note.NewVerifierForCosignatureV1(testMirrorVKey) if err != nil { t.Fatalf("failed to create mirror verifier: %v", err) } + mirrorPolicy := makeGroupPolicy(t, 1, []keyUrl{{testMirrorVKey, mirrorServerURL.String()}}) + for _, test := range []struct { desc string opts *AppendOptions @@ -727,43 +727,43 @@ func TestCheckpointPublisher(t *testing.T) { }, { desc: "witnesses only", - opts: NewAppendOptions().WithCheckpointSigner(logSigner).WithWitnessPolicy(witnesses, &WitnessOptions{Timeout: time.Second}), + opts: NewAppendOptions().WithCheckpointSigner(logSigner).WithWitnessPolicy(witnessPolicy, &WitnessOptions{Timeout: time.Second}), expectCosignatures: []note.Verifier{wit1Verifier}, }, { desc: "mirrors only", - opts: NewAppendOptions().WithCheckpointSigner(logSigner).WithMirrorPolicy(mirrors, &MirroringOptions{Timeout: time.Second}), + opts: NewAppendOptions().WithCheckpointSigner(logSigner).WithMirrorPolicy(mirrorPolicy, &MirroringOptions{Timeout: time.Second}), expectCosignatures: []note.Verifier{mirrorVerifier}, }, { desc: "witnesses and mirrors", - opts: NewAppendOptions().WithCheckpointSigner(logSigner).WithWitnessPolicy(witnesses, &WitnessOptions{Timeout: time.Second}).WithMirrorPolicy(mirrors, &MirroringOptions{Timeout: time.Second}), + opts: NewAppendOptions().WithCheckpointSigner(logSigner).WithWitnessPolicy(witnessPolicy, &WitnessOptions{Timeout: time.Second}).WithMirrorPolicy(mirrorPolicy, &MirroringOptions{Timeout: time.Second}), expectCosignatures: []note.Verifier{wit1Verifier, mirrorVerifier}, }, { desc: "witness fails, failOpen=false", - opts: NewAppendOptions().WithCheckpointSigner(logSigner).WithWitnessPolicy(witnesses, &WitnessOptions{FailOpen: false, Timeout: time.Second}), + opts: NewAppendOptions().WithCheckpointSigner(logSigner).WithWitnessPolicy(witnessPolicy, &WitnessOptions{FailOpen: false, Timeout: time.Second}), witnessFails: true, expectErr: true, }, { desc: "witness fails, failOpen=true", - opts: NewAppendOptions().WithCheckpointSigner(logSigner).WithWitnessPolicy(witnesses, &WitnessOptions{FailOpen: true, Timeout: time.Second}), + opts: NewAppendOptions().WithCheckpointSigner(logSigner).WithWitnessPolicy(witnessPolicy, &WitnessOptions{FailOpen: true, Timeout: time.Second}), witnessFails: true, }, { desc: "multi witnesses greedy=false", - opts: NewAppendOptions().WithCheckpointSigner(logSigner).WithWitnessPolicy(multiWitnesses, &WitnessOptions{Timeout: time.Second, Greedy: false}), + opts: NewAppendOptions().WithCheckpointSigner(logSigner).WithWitnessPolicy(multiWitnessPolicy, &WitnessOptions{Timeout: time.Second, Greedy: false}), expectNumCosignatures: 1, }, { desc: "multi witnesses greedy=true", - opts: NewAppendOptions().WithCheckpointSigner(logSigner).WithWitnessPolicy(multiWitnesses, &WitnessOptions{Timeout: time.Second, Greedy: true}), + opts: NewAppendOptions().WithCheckpointSigner(logSigner).WithWitnessPolicy(multiWitnessPolicy, &WitnessOptions{Timeout: time.Second, Greedy: true}), expectCosignatures: []note.Verifier{wit1Verifier, wit2Verifier}, }, { desc: "multi witnesses greedy=true with one failing witness", - opts: NewAppendOptions().WithCheckpointSigner(logSigner).WithWitnessPolicy(multiWitnesses, &WitnessOptions{Timeout: time.Second, Greedy: true}), + opts: NewAppendOptions().WithCheckpointSigner(logSigner).WithWitnessPolicy(multiWitnessPolicy, &WitnessOptions{Timeout: time.Second, Greedy: true}), partialWitnessFails: true, expectCosignatures: []note.Verifier{wit1Verifier}, }, @@ -777,11 +777,13 @@ func TestCheckpointPublisher(t *testing.T) { defer failingWitnessServer.Close() failingURL, _ := url.Parse(failingWitnessServer.URL) - failingWit, _ := NewWitness(testWit1VKey, failingURL) - failingWitnesses := NewWitnessGroup(1, failingWit).toPolicy() + failingWitnessPolicy := makeGroupPolicy(t, 1, []keyUrl{{ + key: testWit1VKey, + url: failingURL.String()}, + }) // Re-configure option to use failing witnesses - test.opts.WithWitnessPolicy(failingWitnesses, &test.opts.witnessOpts) + test.opts.WithWitnessPolicy(failingWitnessPolicy, &test.opts.witnessOpts) } if test.partialWitnessFails { failingWitnessServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -790,10 +792,12 @@ func TestCheckpointPublisher(t *testing.T) { defer failingWitnessServer.Close() failingURL, _ := url.Parse(failingWitnessServer.URL) - failingWit, _ := NewWitness(testWit2VKey, failingURL) - partiallyFailingWitnesses := NewWitnessGroup(1, wit1, failingWit).toPolicy() + partiallyFailingPolicy := makeGroupPolicy(t, 1, []keyUrl{ + {key: testWit1VKey, url: witnessServerURL1.String()}, + {key: testWit2VKey, url: failingURL.String()}, + }) - test.opts.WithWitnessPolicy(partiallyFailingWitnesses, &test.opts.witnessOpts) + test.opts.WithWitnessPolicy(partiallyFailingPolicy, &test.opts.witnessOpts) } lr := newFakeLogReaderForTest(t) @@ -971,3 +975,28 @@ func mustNewWitness(t *testing.T, vkey, urlStr string) Witness { } return wit } + +type keyUrl struct { + key string + url string +} + +func makeGroupPolicy(t *testing.T, N int, ws []keyUrl) policy.TLogPolicy { + t.Helper() + + b := []byte{} + grpMembers := []string{} + for i, w := range ws { + wName := fmt.Sprintf("w%d", i) + b = fmt.Appendf(b, "witness %s %s %s\n", wName, w.key, w.url) + grpMembers = append(grpMembers, wName) + } + b = fmt.Appendf(b, "group g1 %d %s\n", N, strings.Join(grpMembers, " ")) + b = fmt.Appendf(b, "quorum g1\n") + + r := policy.TLogPolicy{} + if err := r.Unmarshal(b); err != nil { + t.Fatalf("failed to unmarshal policy %q: %v", string(b), err) + } + return r +} diff --git a/witness_test.go b/witness_test.go index 0484f19e0..8da6d68d0 100644 --- a/witness_test.go +++ b/witness_test.go @@ -207,19 +207,6 @@ func TestPopulatePolicy(t *testing.T) { } func TestNewWitnessGroupFromPolicy(t *testing.T) { - wit1CoSigVKey, err := f_note.VKeyToCosignatureV1(testWit1VKey) - if err != nil { - t.Fatalf("failed to convert witness 1 vkey: %v", err) - } - wit2CoSigVKey, err := f_note.VKeyToCosignatureV1(testWit2VKey) - if err != nil { - t.Fatalf("failed to convert witness 2 vkey: %v", err) - } - wit3CoSigVKey, err := f_note.VKeyToCosignatureV1(testWit3VKey) - if err != nil { - t.Fatalf("failed to convert witness 3 vkey: %v", err) - } - w1Signer, err := f_note.NewSignerForCosignatureV1(testWit1SKey) if err != nil { t.Fatalf("failed to create witness 1 signer: %v", err) @@ -250,7 +237,7 @@ func TestNewWitnessGroupFromPolicy(t *testing.T) { policy: fmt.Sprintf(`witness w1 %s https://wit1.example.com group q 1 w1 quorum q -`, wit1CoSigVKey), +`, testWit1VKey), wantN: 1, wantChildren: 1, checkGroup: func(t *testing.T, wg WitnessGroup) { @@ -261,7 +248,7 @@ quorum q if got, want := w.URL, "https://wit1.example.com"; got != want { t.Errorf("w.URL = %q, want %q", got, want) } - if got, want := w.vkey, wit1CoSigVKey; got != want { + if got, want := w.vkey, testWit1VKey; got != want { t.Errorf("w.vkey = %q, want %q", got, want) } if got, want := w.Key.Name(), "Wit1"; got != want { @@ -281,7 +268,7 @@ quorum q desc: "single witness direct quorum", policy: fmt.Sprintf(`witness w1 %s https://wit1.example.com quorum w1 -`, wit1CoSigVKey), +`, testWit1VKey), wantN: 1, wantChildren: 1, checkGroup: func(t *testing.T, wg WitnessGroup) { @@ -292,7 +279,7 @@ quorum w1 if got, want := w.URL, "https://wit1.example.com"; got != want { t.Errorf("w.URL = %q, want %q", got, want) } - if got, want := w.vkey, wit1CoSigVKey; got != want { + if got, want := w.vkey, testWit1VKey; got != want { t.Errorf("w.vkey = %q, want %q", got, want) } if got, want := w.Key.Name(), "Wit1"; got != want { @@ -328,7 +315,7 @@ witness w2 %s https://wit2.example.com witness w3 %s https://wit3.example.com group q 2 w1 w2 w3 quorum q -`, wit1CoSigVKey, wit2CoSigVKey, wit3CoSigVKey), +`, testWit1VKey, testWit2VKey, testWit3VKey), wantN: 2, wantChildren: 3, satisfyTests: []struct { @@ -351,7 +338,7 @@ witness w3 %s https://wit3.example.com group sub 1 w2 w3 group q 2 w1 sub quorum q -`, wit1CoSigVKey, wit2CoSigVKey, wit3CoSigVKey), +`, testWit1VKey, testWit2VKey, testWit3VKey), wantN: 2, wantChildren: 2, checkGroup: func(t *testing.T, wg WitnessGroup) { @@ -388,7 +375,7 @@ witness w2 %s https://wit2.example.com group sub any w1 w2 group q all sub quorum q -`, wit1CoSigVKey, wit2CoSigVKey), +`, testWit1VKey, testWit2VKey), wantN: 1, wantChildren: 1, satisfyTests: []struct { @@ -418,7 +405,7 @@ quorum q policy: fmt.Sprintf(`witness w1 %s https://wit1.example.com group q 1 undefined quorum q -`, wit1CoSigVKey), +`, testWit1VKey), wantErr: true, }, { @@ -426,7 +413,7 @@ quorum q policy: fmt.Sprintf(`witness w1 %s https://wit1.example.com group q 1 w1 quorum unknown -`, wit1CoSigVKey), +`, testWit1VKey), wantErr: true, }, } { From 05e7891bb8da5c61e324ef6fdd2800136e4650df Mon Sep 17 00:00:00 2001 From: Al Cutter Date: Mon, 7 Sep 2026 14:15:16 +0000 Subject: [PATCH 3/4] Update docs --- README.md | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 7f647a729..387339ae7 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,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 cosignatures 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 [WithWitnessPolicy](https://pkg.go.dev/github.com/transparency-dev/tessera#AppendOptions.WithWitnessPolicy) 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). @@ -325,17 +325,11 @@ Logs are required to be append-only data structures. This property can be verified by witnesses, and signatures from witnesses can be provided in the published checkpoint to increase confidence for users of the log. Personalities can configure Tessera with options that specify witnesses compatible with the [C2SP Witness Protocol](https://github.com/C2SP/C2SP/blob/main/tlog-witness.md). -Configuring the witnesses is done by either using the [`NewWitnessGroupFromPolicy`](https://pkg.go.dev/github.com/transparency-dev/tessera@main#NewWitnessGroupFromPolicy) -helper, or programatically creating a top-level [`WitnessGroup`](https://pkg.go.dev/github.com/transparency-dev/tessera@main#WitnessGroup) that contains either -sub `WitnessGroup`s, or [`Witness`es](https://pkg.go.dev/github.com/transparency-dev/tessera@main#Witness). +Configuring the witnesses is done by defining a witness policy using [`github.com/transparency-dev/formats/policy`](https://pkg.go.dev/github.com/transparency-dev/formats/policy), typically parsed from a policy file adhering to the [C2SP tlog-policy specification](https://c2sp.org/tlog-policy). -Each `Witness` is configured with a URL at which the witness can be reached, and a `Verifier` for the key that it must sign with. -`WitnessGroup`s are configured with their sub-components, and a number of these components that must be satisfied in order for the group to be satisfied. - -These primitives allow arbitrarily complex witness policies to be specified. - -Once a top-level `WitnessGroup` is configured, it is passed in to the `Appender` lifecycle options using -[AppendOptions#WithWitnesses](https://pkg.go.dev/github.com/transparency-dev/tessera@main#AppendOptions.WithWitnesses). +The configured policy is passed to the `Appender` lifecycle options using +[`AppendOptions#WithWitnessPolicy`](https://pkg.go.dev/github.com/transparency-dev/tessera@main#AppendOptions.WithWitnessPolicy). +(For backwards compatibility, [`AppendOptions#WithWitnesses`](https://pkg.go.dev/github.com/transparency-dev/tessera@main#AppendOptions.WithWitnesses) is also supported with legacy `WitnessGroup`s). If this option is not set, no witnessing will be configured. > [!Note] From 002758b8481f3cff9092f3f2d811ddf1c84b1f3c Mon Sep 17 00:00:00 2001 From: Al Cutter Date: Thu, 10 Sep 2026 10:50:25 +0000 Subject: [PATCH 4/4] Address comments --- README.md | 2 +- append_lifecycle.go | 6 ++++++ append_lifecycle_test.go | 28 ++++++++++++++-------------- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 387339ae7..f9c027413 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,7 @@ and 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 cosignatures to satisfy any witness policy configured by [WithWitnessPolicy](https://pkg.go.dev/github.com/transparency-dev/tessera#AppendOptions.WithWitnessPolicy) - 3. If the witness policy is satisfied, make this new Checkpoint public available + 3. If the witness policy is satisfied, make this new Checkpoint publicly 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). Due to the nature of append-only logs, all Checkpoints issued after this point will also commit to inclusion of this entry. diff --git a/append_lifecycle.go b/append_lifecycle.go index cc87834be..a4afa92df 100644 --- a/append_lifecycle.go +++ b/append_lifecycle.go @@ -841,12 +841,18 @@ func (o AppendOptions) witnessGateway(ctx context.Context, lr LogReader, httpCli if w.URL == nil { return nil, fmt.Errorf("invalid witness policy: witness %q has no URL", w.Name) } + if w.Verifier == nil { + return nil, fmt.Errorf("invalid witness policy: witness %q verifier is nil", w.Name) + } witnesses = append(witnesses, witness.Witness{ URL: w.URL, Verifiers: []note.Verifier{w.Verifier}, }) } if len(witnesses) == 0 { + if o.witnessPolicy.Quorum != "" && o.witnessPolicy.Quorum != "none" { + return nil, fmt.Errorf("invalid witness policy: invalid quorum %q for zero witnesses", o.witnessPolicy.Quorum) + } return nil, nil } witnessGateway, err := witness.NewGateway(ctx, witness.Options{ diff --git a/append_lifecycle_test.go b/append_lifecycle_test.go index eb98f118f..a2c503de1 100644 --- a/append_lifecycle_test.go +++ b/append_lifecycle_test.go @@ -494,7 +494,7 @@ func TestGatherCosignatures(t *testing.T) { }, { desc: "non-greedy stops after quorum is satisfied (1 of 2)", - policy: makeGroupPolicy(t, 1, []keyUrl{ + policy: makeGroupPolicy(t, 1, []keyURL{ {key: testWit1VKey, url: "https://wit1.example.com"}, {key: testWit2VKey, url: "https://wit2.example.com"}}), fetcher: func(ctx context.Context, cp []byte, cpSize uint64) <-chan []byte { @@ -508,7 +508,7 @@ func TestGatherCosignatures(t *testing.T) { }, { desc: "greedy gathers surplus signatures (2 of 3 required, 3 provided)", - policy: makeGroupPolicy(t, 2, []keyUrl{ + policy: makeGroupPolicy(t, 2, []keyURL{ {key: testWit1VKey, url: "https://wit1.example.com"}, {key: testWit2VKey, url: "https://wit2.example.com"}, {key: testWit3VKey, url: "https://wit3.example.com"}}), @@ -525,7 +525,7 @@ func TestGatherCosignatures(t *testing.T) { }, { desc: "greedy succeeds when quorum is met and channel closes without further signatures (1 of 2 required, 1 provided)", - policy: makeGroupPolicy(t, 1, []keyUrl{ + policy: makeGroupPolicy(t, 1, []keyURL{ {key: testWit1VKey, url: "https://wit1.example.com"}, {key: testWit2VKey, url: "https://wit2.example.com"}}), fetcher: func(ctx context.Context, cp []byte, cpSize uint64) <-chan []byte { @@ -539,7 +539,7 @@ func TestGatherCosignatures(t *testing.T) { }, { desc: "greedy fails when quorum is not met and channel closes (failOpen=false)", - policy: makeGroupPolicy(t, 2, []keyUrl{ + policy: makeGroupPolicy(t, 2, []keyURL{ {key: testWit1VKey, url: "https://wit1.example.com"}, {key: testWit2VKey, url: "https://wit2.example.com"}}), fetcher: func(ctx context.Context, cp []byte, cpSize uint64) <-chan []byte { @@ -554,7 +554,7 @@ func TestGatherCosignatures(t *testing.T) { }, { desc: "greedy fails open when quorum is not met and channel closes (failOpen=true)", - policy: makeGroupPolicy(t, 2, []keyUrl{ + policy: makeGroupPolicy(t, 2, []keyURL{ {key: testWit1VKey, url: "https://wit1.example.com"}, {key: testWit2VKey, url: "https://wit2.example.com"}}), fetcher: func(ctx context.Context, cp []byte, cpSize uint64) <-chan []byte { @@ -570,7 +570,7 @@ func TestGatherCosignatures(t *testing.T) { }, { desc: "greedy fails when quorum is not met on timeout (failOpen=false)", - policy: makeGroupPolicy(t, 2, []keyUrl{ + policy: makeGroupPolicy(t, 2, []keyURL{ {key: testWit1VKey, url: "https://wit1.example.com"}, {key: testWit2VKey, url: "https://wit2.example.com"}}), fetcher: func(ctx context.Context, cp []byte, cpSize uint64) <-chan []byte { @@ -584,7 +584,7 @@ func TestGatherCosignatures(t *testing.T) { }, { desc: "greedy fails open when quorum is not met on timeout (failOpen=true)", - policy: makeGroupPolicy(t, 2, []keyUrl{ + policy: makeGroupPolicy(t, 2, []keyURL{ {key: testWit1VKey, url: "https://wit1.example.com"}, {key: testWit2VKey, url: "https://wit2.example.com"}}), fetcher: func(ctx context.Context, cp []byte, cpSize uint64) <-chan []byte { @@ -680,7 +680,7 @@ func TestCheckpointPublisher(t *testing.T) { t.Fatalf("failed to create witness 1 verifier: %v", err) } - witnessPolicy := makeGroupPolicy(t, 1, []keyUrl{{key: testWit1VKey, url: witnessServerURL1.String()}}) + witnessPolicy := makeGroupPolicy(t, 1, []keyURL{{key: testWit1VKey, url: witnessServerURL1.String()}}) witnessServer2 := httptest.NewServer(newWitnessHandler(t, logVerifier, testWit2SKey)) t.Cleanup(witnessServer2.Close) @@ -694,7 +694,7 @@ func TestCheckpointPublisher(t *testing.T) { t.Fatalf("failed to create witness 2 verifier: %v", err) } - multiWitnessPolicy := makeGroupPolicy(t, 1, []keyUrl{ + multiWitnessPolicy := makeGroupPolicy(t, 1, []keyURL{ {key: testWit1VKey, url: witnessServerURL1.String()}, {key: testWit2VKey, url: witnessServerURL2.String()}}) @@ -710,7 +710,7 @@ func TestCheckpointPublisher(t *testing.T) { t.Fatalf("failed to create mirror verifier: %v", err) } - mirrorPolicy := makeGroupPolicy(t, 1, []keyUrl{{testMirrorVKey, mirrorServerURL.String()}}) + mirrorPolicy := makeGroupPolicy(t, 1, []keyURL{{testMirrorVKey, mirrorServerURL.String()}}) for _, test := range []struct { desc string @@ -777,7 +777,7 @@ func TestCheckpointPublisher(t *testing.T) { defer failingWitnessServer.Close() failingURL, _ := url.Parse(failingWitnessServer.URL) - failingWitnessPolicy := makeGroupPolicy(t, 1, []keyUrl{{ + failingWitnessPolicy := makeGroupPolicy(t, 1, []keyURL{{ key: testWit1VKey, url: failingURL.String()}, }) @@ -792,7 +792,7 @@ func TestCheckpointPublisher(t *testing.T) { defer failingWitnessServer.Close() failingURL, _ := url.Parse(failingWitnessServer.URL) - partiallyFailingPolicy := makeGroupPolicy(t, 1, []keyUrl{ + partiallyFailingPolicy := makeGroupPolicy(t, 1, []keyURL{ {key: testWit1VKey, url: witnessServerURL1.String()}, {key: testWit2VKey, url: failingURL.String()}, }) @@ -976,12 +976,12 @@ func mustNewWitness(t *testing.T, vkey, urlStr string) Witness { return wit } -type keyUrl struct { +type keyURL struct { key string url string } -func makeGroupPolicy(t *testing.T, N int, ws []keyUrl) policy.TLogPolicy { +func makeGroupPolicy(t *testing.T, N int, ws []keyURL) policy.TLogPolicy { t.Helper() b := []byte{}