Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 6 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,8 @@ 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)
3. If the witness policy is satisfied, make this new Checkpoint public available
2. Contact witnesses and/or mirrors, and collect enough cosignatures to satisfy any policies configured by [WithWitnessPolicy](https://pkg.go.dev/github.com/transparency-dev/tessera#AppendOptions.WithWitnessPolicy) and/or [WithMirrorPolicy](https://pkg.go.dev/github.com/transparency-dev/tessera#AppendOptions.WithMirrorPolicy).
3. If the witness/mirror policies are satisfied, make this new Checkpoint available via the [tlog-tiles API][]

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.
Expand Down Expand Up @@ -326,18 +326,12 @@ 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).

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.
Configuring the witnesses is done by passing an instance of a populated [`TLogPolicy`](https://pkg.go.dev/github.com/transparency-dev/formats/policy@main#TLogPolicy) struct to the [`WithWitnessPolicy`](https://pkg.go.dev/github.com/transparency-dev/tessera@main#AppendOptions.WithWitnessPolicy) option.
A `TLogPolicy` can either be constructed directly, or parsed from a policy file in the format described at [c2sp.org/tlog-policy](https://c2sp.org/tlog-policy) using [`TLogPolicy.Unmarshal`](https://pkg.go.dev/github.com/transparency-dev/formats/policy@main#TLogPolicy.Unmarshal).
Every witness in the policy which Tessera is expected to contact must have a URL configured.

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).
If this option is not set, no witnessing will be configured.
If this option is not specified, no witnessing is performed.

> [!Note]
> If the policy cannot be satisfied then no checkpoint will be published.
Expand Down
140 changes: 85 additions & 55 deletions append_lifecycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import (
"context"
"errors"
"fmt"
"maps"
"net/http"
"net/url"
"os"
Expand All @@ -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"
Expand Down Expand Up @@ -677,10 +677,10 @@ type AppendOptions struct {
checkpointRepublishInterval time.Duration
checkpointPublicationTimeout time.Duration

witnesses WitnessGroup
witnesses policy.TLogPolicy
witnessOpts WitnessOptions

mirrors WitnessGroup
mirrors policy.TLogPolicy
mirrorOpts MirroringOptions

addDecorators []func(AddFn) AddFn
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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.witnesses, cp, cpSize, o.witnessOpts.FailOpen, o.witnessOpts.Greedy)
return err
})
}
Expand All @@ -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.mirrors, cp, cpSize, o.mirrorOpts.FailOpen, false)
return err
})
}
Expand All @@ -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.witnesses.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 {
Expand All @@ -873,9 +859,12 @@ 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{}
for _, m := range o.mirrors.Witnesses {
if m.URL == nil {
return nil, fmt.Errorf("invalid mirror policy: mirror %q has no URL", m.Name)
}
mirrorURLs = append(mirrorURLs, m.URL)
}
if len(mirrorURLs) == 0 {
return nil, nil
Expand All @@ -894,12 +883,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")))
Expand All @@ -918,9 +907,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))
Expand All @@ -940,13 +929,13 @@ 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) {
maxExpectedResponses := len(pol.Witnesses)

// 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 {
Expand Down Expand Up @@ -996,7 +985,18 @@ 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
}

Expand Down Expand Up @@ -1157,35 +1157,42 @@ 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(witPolicy policy.TLogPolicy, opts *WitnessOptions) *AppendOptions {
if opts == nil {
opts = &WitnessOptions{}
}
if opts.Timeout == 0 {
opts.Timeout = DefaultWitnessTimeout
}

o.witnesses = witnesses
o.witnesses = witPolicy
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.
//
// Deprecated: Use WithWitnessPolicy instead.
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(mirrors policy.TLogPolicy, opts *MirroringOptions) *AppendOptions {
if opts == nil {
opts = &MirroringOptions{}
}
Expand All @@ -1198,8 +1205,16 @@ func (o *AppendOptions) WithMirrors(mirrors WitnessGroup, opts *MirroringOptions
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.
//
// Deprecated: Use WithMirrorPolicy instead.
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.
// a user-provided witness policy.
type WitnessOptions struct {
// Timeout is the maximum time to wait while attempting to satisfy the configured witness policy.
//
Expand Down Expand Up @@ -1290,14 +1305,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.witnesses.Witnesses) > 0 {
urls := make([]string, 0, len(o.witnesses.Witnesses))
for _, w := range o.witnesses.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.witnesses.Groups)),
slog.Any("quorum", o.witnesses.Quorum),
slog.Any("endpoints", urls),
))
}
if len(o.mirrors.Witnesses) > 0 {
urls := make([]string, 0, len(o.mirrors.Witnesses))
for _, w := range o.mirrors.Witnesses {
if w.URL != nil {
urls = append(urls, w.URL.String())
}
}
attrs = append(attrs, slog.Group("mirrors",
slog.Int("groups", len(o.mirrors.Groups)),
slog.Any("quorum", o.mirrors.Quorum),
slog.Any("endpoints", urls),
))
}
Expand Down
Loading