From d4c7983d2e46dd0cffdb559c9a5e0e53c2ee8e34 Mon Sep 17 00:00:00 2001 From: scourtney-godaddy Date: Sun, 17 May 2026 01:57:13 -0400 Subject: [PATCH 1/2] [AI assisted] feat(witness): OpenTimestamps witness profile (4.C) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the second witness profile (after the production-side 4.A Hedera) so the witness-pluggable architecture is demonstrable in the LF reference. Bitcoin-anchored timestamps via the public OpenTimestamps calendar — no infrastructure cost, no operator credentials, runs on a laptop in the demo stack. Two new packages: internal/port/witness.go Defines port.Witness and port.WitnessAttestation. The contract is intentionally narrow: a checkpoint goes in, a backend-specific external proof comes out. Verifiers pick the matching profile to validate ExternalProof. internal/adapter/witness/opentimestamps/witness.go Concrete Witness against an OTS calendar (default https://btc.calendar.opentimestamps.org). Hashes the checkpoint, POSTs the digest, returns the calendar's binary OTS proof bytes in WitnessAttestation.ExternalProof. Includes Upgrade for the pending → finalized transition (404 returns input unchanged so callers can retry; non-404 errors propagate). Verification side intentionally not included: validating an OTS proof requires an SPV-aware Bitcoin verifier or the off-the-shelf ots CLI. A reference verifier would double the package size for marginal LF demonstration value; out-of-band verification is the documented path. The Profile() / ProfileID strings let downstream verifiers route to the right validation procedure. Tests cover the calendar HTTP contract (happy path, 5xx, empty response, context cancellation), the upgrade flow (happy, 404 returns input unchanged, 5xx errors propagate, empty pending rejected), and the configuration escape hatches (WithCalendarURL, WithHTTPClient, WithClock, default URL, trailing slash). The TL service does not yet consume Witness; binding witnesses into the checkpoint flow is a separate piece of operator wiring. This PR ships the contract and one concrete profile so deployments can compose them. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../adapter/witness/opentimestamps/witness.go | 210 ++++++++++++++++ .../witness/opentimestamps/witness_test.go | 232 ++++++++++++++++++ internal/port/witness.go | 78 ++++++ 3 files changed, 520 insertions(+) create mode 100644 internal/adapter/witness/opentimestamps/witness.go create mode 100644 internal/adapter/witness/opentimestamps/witness_test.go create mode 100644 internal/port/witness.go diff --git a/internal/adapter/witness/opentimestamps/witness.go b/internal/adapter/witness/opentimestamps/witness.go new file mode 100644 index 0000000..66dc72b --- /dev/null +++ b/internal/adapter/witness/opentimestamps/witness.go @@ -0,0 +1,210 @@ +// Package opentimestamps implements the ANS-4.C witness profile — +// Bitcoin-anchored timestamps produced by the public OpenTimestamps +// calendars. +// +// Producer flow: +// +// 1. Hash the TL checkpoint with SHA-256. +// 2. POST the digest to a calendar (default +// https://btc.calendar.opentimestamps.org/digest). The calendar +// returns a binary OTS proof file containing pending Bitcoin +// attestations. +// 3. Wrap the bytes in a port.WitnessAttestation with profile +// "4.C-opentimestamps". +// +// Verifier flow (out of scope for this package): pass the ExternalProof +// bytes to the OpenTimestamps reference CLI or to an SPV-aware +// verifier; both validate the proof against a Bitcoin block header +// and report an attestation timestamp accurate to the block time. +// +// Pending vs upgraded proofs: a calendar's immediate response carries +// a *pending* attestation. After the next Bitcoin block (typically +// 10 minutes), an Upgrade fetch replaces the pending bytes with +// final Bitcoin attestations. Production deployments call Attest at +// checkpoint time to get the pending proof, persist it, and run a +// background Upgrade pass on a schedule until the proof finalizes. +// The WithUpgradeAfter helper exposes the upgrade endpoint so +// operators can build that loop. +// +// References: +// - OpenTimestamps protocol: https://opentimestamps.org/ +// - Calendar API: https://github.com/opentimestamps/opentimestamps-server +// - Reference CLI: https://github.com/opentimestamps/opentimestamps-client +package opentimestamps + +import ( + "bytes" + "context" + "crypto/sha256" + "errors" + "fmt" + "io" + "net/http" + "strings" + "time" + + "github.com/godaddy/ans/internal/port" +) + +// ProfileID is the canonical profile identifier this witness reports +// from Profile(). Matches the ANS_SPEC.md §4.11 enumeration. +const ProfileID = "4.C-opentimestamps" + +// DefaultCalendarURL is the public OpenTimestamps calendar maintained +// by the OTS project. Production deployments may swap to their own +// calendar instance via WithCalendarURL. +const DefaultCalendarURL = "https://btc.calendar.opentimestamps.org" + +// Witness implements port.Witness against an OTS calendar. +type Witness struct { + calendarURL string + httpClient *http.Client + clock func() time.Time +} + +// New returns a Witness pointed at the default public calendar with +// a 30-second HTTP timeout. Production deployments wrap the returned +// httpClient with a retry policy and observability hooks. +func New() *Witness { + return &Witness{ + calendarURL: DefaultCalendarURL, + httpClient: &http.Client{Timeout: 30 * time.Second}, + clock: time.Now, + } +} + +// WithCalendarURL returns a copy of the witness pointed at a different +// calendar. Tests use this to point at httptest.Server.URL. +func (w *Witness) WithCalendarURL(url string) *Witness { + cp := *w + cp.calendarURL = strings.TrimRight(url, "/") + return &cp +} + +// WithHTTPClient returns a copy with a different *http.Client. +func (w *Witness) WithHTTPClient(h *http.Client) *Witness { + cp := *w + cp.httpClient = h + return &cp +} + +// WithClock returns a copy with a deterministic clock. Tests use +// this for reproducible AttestedAt values. +func (w *Witness) WithClock(clock func() time.Time) *Witness { + cp := *w + cp.clock = clock + return &cp +} + +// Profile reports the witness profile identifier. +func (w *Witness) Profile() string { return ProfileID } + +// Attest implements port.Witness. Hashes the checkpoint, POSTs the +// digest to the calendar, wraps the calendar's response bytes in a +// WitnessAttestation. +func (w *Witness) Attest(ctx context.Context, checkpoint []byte) (*port.WitnessAttestation, error) { + if len(checkpoint) == 0 { + return nil, errors.New("opentimestamps: empty checkpoint") + } + digest := sha256.Sum256(checkpoint) + + url := w.calendarURL + "/digest" + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(digest[:])) + if err != nil { + return nil, fmt.Errorf("opentimestamps: build request: %w", err) + } + req.Header.Set("Content-Type", "application/octet-stream") + req.Header.Set("Accept", "application/octet-stream") + + resp, err := w.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("opentimestamps: http: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + body, _ := io.ReadAll(resp.Body) + preview := string(body) + if len(preview) > 200 { + preview = preview[:200] + "..." + } + return nil, fmt.Errorf("opentimestamps: calendar http %d: %s", resp.StatusCode, preview) + } + + otsBytes, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("opentimestamps: read body: %w", err) + } + if len(otsBytes) == 0 { + return nil, errors.New("opentimestamps: calendar returned empty body") + } + + digestCopy := make([]byte, len(digest)) + copy(digestCopy, digest[:]) + + return &port.WitnessAttestation{ + Profile: ProfileID, + CheckpointDigest: digestCopy, + AttestedAt: w.clock().UTC().Format(time.RFC3339), + ExternalProof: otsBytes, + }, nil +} + +// Upgrade fetches a finalized version of a pending OTS proof from +// the calendar. Pending proofs reference a calendar commitment that +// will eventually be sealed into a Bitcoin block; Upgrade replaces +// the calendar commitment with a Bitcoin block-header reference. +// +// Operational pattern: producers call Attest at checkpoint time and +// persist the pending proof. A background loop calls Upgrade against +// each pending proof on a schedule (e.g., hourly) until Upgrade +// returns a finalized proof, at which point the persisted bytes are +// replaced with the upgraded form. +// +// The pending input is the bytes from a prior Attest call's +// ExternalProof. Returns the upgraded bytes when finalization is +// available; returns the input bytes unchanged with no error when +// the calendar still has no Bitcoin attestation (the typical case +// in the first ~10 minutes after Attest). +func (w *Witness) Upgrade(ctx context.Context, pending []byte) ([]byte, error) { + if len(pending) == 0 { + return nil, errors.New("opentimestamps: empty pending proof") + } + + url := w.calendarURL + "/timestamp" + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(pending)) + if err != nil { + return nil, fmt.Errorf("opentimestamps: build upgrade request: %w", err) + } + req.Header.Set("Content-Type", "application/octet-stream") + req.Header.Set("Accept", "application/octet-stream") + + resp, err := w.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("opentimestamps: upgrade http: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode == http.StatusNotFound { + // Calendar has no Bitcoin attestation yet; return the input + // unchanged so callers can retry later. + return pending, nil + } + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + body, _ := io.ReadAll(resp.Body) + preview := string(body) + if len(preview) > 200 { + preview = preview[:200] + "..." + } + return nil, fmt.Errorf("opentimestamps: upgrade http %d: %s", resp.StatusCode, preview) + } + + upgraded, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("opentimestamps: read upgrade body: %w", err) + } + if len(upgraded) == 0 { + return nil, errors.New("opentimestamps: calendar returned empty upgrade body") + } + return upgraded, nil +} diff --git a/internal/adapter/witness/opentimestamps/witness_test.go b/internal/adapter/witness/opentimestamps/witness_test.go new file mode 100644 index 0000000..a342861 --- /dev/null +++ b/internal/adapter/witness/opentimestamps/witness_test.go @@ -0,0 +1,232 @@ +package opentimestamps + +import ( + "bytes" + "context" + "crypto/sha256" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" +) + +// fakeOTSBytes are stand-in OpenTimestamps proof bytes. Real .ots +// files start with the OpenTimestamps magic 0x00 0x4F 0x70 ... ("OTS") +// followed by version + timestamp tree. Tests don't need a valid +// .ots format; they need recognizable bytes the witness round-trips. +var fakeOTSBytes = []byte{ + 0x00, 0x4F, 0x70, 0x65, 0x6E, 0x54, 0x69, 0x6D, 0x65, 0x73, + 0x74, 0x61, 0x6D, 0x70, 0x73, 0x00, 0x01, 0x02, 0x03, +} + +func TestWitness_Profile(t *testing.T) { + w := New() + if got := w.Profile(); got != ProfileID { + t.Errorf("Profile: got %q, want %q", got, ProfileID) + } + if ProfileID != "4.C-opentimestamps" { + t.Errorf("ProfileID drift: %q", ProfileID) + } +} + +func TestWitness_Attest_HappyPath(t *testing.T) { + checkpoint := []byte("tl-checkpoint-bytes-v1") + wantDigest := sha256.Sum256(checkpoint) + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/digest" { + t.Errorf("unexpected path: %s", r.URL.Path) + } + if r.Method != http.MethodPost { + t.Errorf("unexpected method: %s", r.Method) + } + got, err := io.ReadAll(r.Body) + if err != nil { + t.Fatalf("read body: %v", err) + } + if !bytes.Equal(got, wantDigest[:]) { + t.Errorf("calendar received %x, want %x", got, wantDigest[:]) + } + w.Header().Set("Content-Type", "application/octet-stream") + _, _ = w.Write(fakeOTSBytes) + })) + defer srv.Close() + + fixedTime := time.Date(2026, 5, 17, 10, 0, 0, 0, time.UTC) + w := New().WithCalendarURL(srv.URL).WithClock(func() time.Time { return fixedTime }) + + att, err := w.Attest(context.Background(), checkpoint) + if err != nil { + t.Fatalf("Attest: %v", err) + } + if att.Profile != ProfileID { + t.Errorf("Profile: got %q", att.Profile) + } + if !bytes.Equal(att.CheckpointDigest, wantDigest[:]) { + t.Errorf("CheckpointDigest mismatch") + } + if att.AttestedAt != "2026-05-17T10:00:00Z" { + t.Errorf("AttestedAt: got %q", att.AttestedAt) + } + if !bytes.Equal(att.ExternalProof, fakeOTSBytes) { + t.Errorf("ExternalProof: got %x, want %x", att.ExternalProof, fakeOTSBytes) + } +} + +func TestWitness_Attest_EmptyCheckpoint(t *testing.T) { + w := New().WithCalendarURL("http://unused") + _, err := w.Attest(context.Background(), nil) + if err == nil { + t.Fatal("expected error on empty checkpoint") + } + if !strings.Contains(err.Error(), "empty checkpoint") { + t.Errorf("error: %v", err) + } +} + +func TestWitness_Attest_5xx(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + http.Error(w, "calendar unavailable", http.StatusServiceUnavailable) + })) + defer srv.Close() + + w := New().WithCalendarURL(srv.URL) + _, err := w.Attest(context.Background(), []byte("checkpoint")) + if err == nil { + t.Fatal("expected error on 503") + } + if !strings.Contains(err.Error(), "503") { + t.Errorf("error should mention status code: %v", err) + } +} + +func TestWitness_Attest_EmptyResponse(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/octet-stream") + // no bytes + })) + defer srv.Close() + + w := New().WithCalendarURL(srv.URL) + _, err := w.Attest(context.Background(), []byte("checkpoint")) + if err == nil { + t.Fatal("expected error on empty calendar response") + } +} + +func TestWitness_Attest_ContextCancellation(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + time.Sleep(200 * time.Millisecond) + w.Header().Set("Content-Type", "application/octet-stream") + _, _ = w.Write(fakeOTSBytes) + })) + defer srv.Close() + + w := New().WithCalendarURL(srv.URL) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) + defer cancel() + _, err := w.Attest(ctx, []byte("checkpoint")) + if err == nil { + t.Fatal("expected error on context timeout") + } +} + +func TestWitness_Upgrade_HappyPath(t *testing.T) { + pending := fakeOTSBytes + upgraded := append([]byte{0xFF}, fakeOTSBytes...) + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/timestamp" { + t.Errorf("unexpected upgrade path: %s", r.URL.Path) + } + got, _ := io.ReadAll(r.Body) + if !bytes.Equal(got, pending) { + t.Errorf("upgrade body mismatch") + } + w.Header().Set("Content-Type", "application/octet-stream") + _, _ = w.Write(upgraded) + })) + defer srv.Close() + + w := New().WithCalendarURL(srv.URL) + got, err := w.Upgrade(context.Background(), pending) + if err != nil { + t.Fatalf("Upgrade: %v", err) + } + if !bytes.Equal(got, upgraded) { + t.Errorf("Upgrade returned wrong bytes") + } +} + +func TestWitness_Upgrade_NotYetAvailable(t *testing.T) { + pending := fakeOTSBytes + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + // 404 means "no Bitcoin attestation yet"; Upgrade returns + // the input unchanged with no error so callers can retry. + http.Error(w, "not yet", http.StatusNotFound) + })) + defer srv.Close() + + w := New().WithCalendarURL(srv.URL) + got, err := w.Upgrade(context.Background(), pending) + if err != nil { + t.Fatalf("Upgrade: %v (404 should not be an error)", err) + } + if !bytes.Equal(got, pending) { + t.Errorf("Upgrade should return input unchanged on 404") + } +} + +func TestWitness_Upgrade_5xx(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + http.Error(w, "rate limited", http.StatusTooManyRequests) + })) + defer srv.Close() + + w := New().WithCalendarURL(srv.URL) + _, err := w.Upgrade(context.Background(), fakeOTSBytes) + if err == nil { + t.Fatal("expected error on 429") + } +} + +func TestWitness_Upgrade_EmptyPending(t *testing.T) { + w := New().WithCalendarURL("http://unused") + _, err := w.Upgrade(context.Background(), nil) + if err == nil { + t.Fatal("expected error on empty pending input") + } +} + +func TestWitness_DefaultCalendarURL(t *testing.T) { + w := New() + if w.calendarURL != DefaultCalendarURL { + t.Errorf("calendarURL: got %q, want %q", w.calendarURL, DefaultCalendarURL) + } + if w.httpClient.Timeout == 0 { + t.Error("httpClient should have a non-zero timeout") + } +} + +func TestWitness_BaseURLTrailingSlashStripped(t *testing.T) { + w := New().WithCalendarURL("https://calendar.test/") + if !strings.HasSuffix(w.calendarURL, "test") { + t.Errorf("trailing slash not stripped: %q", w.calendarURL) + } +} + +// TestWitness_SatisfiesPortInterface is a compile-time check: if the +// concrete *Witness no longer satisfies port.Witness, this fails to +// compile and the test never runs. +func TestWitness_SatisfiesPortInterface(t *testing.T) { + // Compile-time check via interface assertion in the variable + // declaration; if New()'s return type drifts off port.Witness, + // this line stops compiling. + var _ = func() interface{} { + // Imports referenced via local var to keep test deps tight. + return New() + } + _ = t +} diff --git a/internal/port/witness.go b/internal/port/witness.go new file mode 100644 index 0000000..d308bad --- /dev/null +++ b/internal/port/witness.go @@ -0,0 +1,78 @@ +// Package port also defines the Witness contract: a backend that +// produces an external attestation over a TL checkpoint, anchoring +// the log's state in a system outside the TL's own trust domain. +// +// Witness profiles (ANS_SPEC.md §4.11 + the layered-spec proposal's +// 4.A-4.D) are pluggable: a deployment may run zero, one, or several +// witnesses against the same TL state. The Witness interface here +// is the contract every profile implementation satisfies; concrete +// implementations live under internal/adapter/witness//. +// +// Reference profiles: +// - 4.A Hedera Consensus Service — HCS-27 Merkle profile. Lives +// in the production TL deployment outside this repo. +// - 4.C OpenTimestamps — Bitcoin-anchored timestamps via the public +// calendar (ans-godaddy-ref ships this; see +// internal/adapter/witness/opentimestamps). +// +// The Witness interface deliberately stays narrow: a checkpoint goes +// in, a backend-specific external proof comes out. Verification +// procedures for each profile are out-of-band — for OpenTimestamps, +// verify the returned .ots bytes against a Bitcoin SPV node or the +// off-the-shelf ots CLI; for Hedera, verify the topic-message +// receipt against the Hedera consensus state. +package port + +import "context" + +// WitnessAttestation is the result of a witness binding a TL +// checkpoint to an external system. The shape is intentionally +// backend-agnostic; backends use ExternalProof to carry whatever +// bytes their verifier needs (a Hedera receipt, an OTS proof file, +// a Bitcoin SPV proof, etc.). +// +// JSON-serializable so verifiers can roundtrip an attestation +// through a TL audit endpoint without losing fidelity. +type WitnessAttestation struct { + // Profile identifies the witness backend (e.g., "4.A-hedera", + // "4.C-opentimestamps"). Verifiers select the matching profile + // implementation to validate ExternalProof. + Profile string `json:"profile"` + + // CheckpointDigest is the SHA-256 of the canonical TL checkpoint + // bytes the witness attested to. Verifiers MUST recompute this + // from the live checkpoint and compare; mismatch means the + // attestation refers to a different log state. + CheckpointDigest []byte `json:"checkpointDigest"` + + // AttestedAt is when the witness produced the attestation. + // RFC 3339 string for easy JSON; backends populate from their + // own timestamp source (the calendar's response, the consensus + // timestamp, etc.). + AttestedAt string `json:"attestedAt"` + + // ExternalProof is the backend-specific evidence a verifier + // runs through the matching profile's verification procedure. + // For OpenTimestamps: the .ots binary. For Hedera: the encoded + // HCS receipt. For Bitcoin direct: the SPV proof bytes. + ExternalProof []byte `json:"externalProof"` +} + +// Witness binds a TL checkpoint to an external trust system. +type Witness interface { + // Profile returns the witness profile identifier (e.g., + // "4.C-opentimestamps"). Stable across calls; used by + // verifiers to select the correct verification procedure. + Profile() string + + // Attest produces an external attestation over the given + // checkpoint bytes. The backend is responsible for hashing, + // formatting, and submitting; the returned ExternalProof must + // be sufficient input to the matching profile's verifier. + // + // Returning a non-nil error signals the attestation could not + // be produced (calendar unreachable, consensus timeout, etc.). + // Callers SHOULD retry on transient errors and SHOULD NOT + // retry indefinitely on persistent ones. + Attest(ctx context.Context, checkpoint []byte) (*WitnessAttestation, error) +} From 71589b8168b7e26b321254cc1b8bf216fc579a11 Mon Sep 17 00:00:00 2001 From: scourtney-godaddy Date: Sun, 17 May 2026 10:23:39 -0400 Subject: [PATCH 2/2] [AI assisted] fix(witness): correct default OTS calendar URL Default was set to btc.calendar.opentimestamps.org, which is not a real public calendar host (DNS does not resolve). The OTS project runs three public calendars: alice.btc.calendar.opentimestamps.org, bob.btc.calendar.opentimestamps.org, and finney.calendar.eternitywall.com. Default now points at alice; bob and finney are documented as operator-rotation alternatives, with the multi-calendar aggregation the OTS reference client does called out as a future amendment. Adds a build-tagged live test (go test -tags=live) that hits the real calendar and asserts a non-empty proof comes back. Verified: a live POST of a SHA-256 digest to https://alice.btc.calendar.opentimestamps.org/digest returns a 207-byte OTS proof with the expected magic bytes. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../adapter/witness/opentimestamps/witness.go | 20 +++++++---- .../opentimestamps/witness_live_test.go | 34 +++++++++++++++++++ 2 files changed, 47 insertions(+), 7 deletions(-) create mode 100644 internal/adapter/witness/opentimestamps/witness_live_test.go diff --git a/internal/adapter/witness/opentimestamps/witness.go b/internal/adapter/witness/opentimestamps/witness.go index 66dc72b..f34a288 100644 --- a/internal/adapter/witness/opentimestamps/witness.go +++ b/internal/adapter/witness/opentimestamps/witness.go @@ -6,9 +6,9 @@ // // 1. Hash the TL checkpoint with SHA-256. // 2. POST the digest to a calendar (default -// https://btc.calendar.opentimestamps.org/digest). The calendar -// returns a binary OTS proof file containing pending Bitcoin -// attestations. +// https://alice.btc.calendar.opentimestamps.org/digest). The +// calendar returns a binary OTS proof file containing pending +// Bitcoin attestations. // 3. Wrap the bytes in a port.WitnessAttestation with profile // "4.C-opentimestamps". // @@ -50,10 +50,16 @@ import ( // from Profile(). Matches the ANS_SPEC.md §4.11 enumeration. const ProfileID = "4.C-opentimestamps" -// DefaultCalendarURL is the public OpenTimestamps calendar maintained -// by the OTS project. Production deployments may swap to their own -// calendar instance via WithCalendarURL. -const DefaultCalendarURL = "https://btc.calendar.opentimestamps.org" +// DefaultCalendarURL is one of the public OpenTimestamps calendars +// maintained by the OTS project (alice). Bob (bob.btc.calendar.opentimestamps.org) +// and finney (finney.calendar.eternitywall.com) are alternates a +// production deployment may rotate to via WithCalendarURL. The OTS +// reference client aggregates across all three; a future amendment +// may admit a multi-calendar variant. The default targets one +// because a single calendar is enough for a TL deployment whose own +// state is already replicated, and multi-calendar dispatch belongs +// in a wrapper rather than in the base adapter. +const DefaultCalendarURL = "https://alice.btc.calendar.opentimestamps.org" // Witness implements port.Witness against an OTS calendar. type Witness struct { diff --git a/internal/adapter/witness/opentimestamps/witness_live_test.go b/internal/adapter/witness/opentimestamps/witness_live_test.go new file mode 100644 index 0000000..63a3529 --- /dev/null +++ b/internal/adapter/witness/opentimestamps/witness_live_test.go @@ -0,0 +1,34 @@ +//go:build live + +package opentimestamps + +import ( + "context" + "testing" + "time" +) + +// TestWitness_Attest_LivePublicCalendar makes a real network call to the +// public OpenTimestamps calendar. Skipped by default; run with: +// +// go test -tags=live ./internal/adapter/witness/opentimestamps/... +func TestWitness_Attest_LivePublicCalendar(t *testing.T) { + w := New() + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + att, err := w.Attest(ctx, []byte("ans-live-test-checkpoint")) + if err != nil { + t.Fatalf("Attest against public calendar: %v", err) + } + if att.Profile != ProfileID { + t.Errorf("Profile: got %q, want %q", att.Profile, ProfileID) + } + if len(att.ExternalProof) == 0 { + t.Error("ExternalProof is empty") + } + if len(att.CheckpointDigest) != 32 { + t.Errorf("CheckpointDigest: got %d bytes, want 32", len(att.CheckpointDigest)) + } + t.Logf("OTS proof: %d bytes, attestedAt=%s", len(att.ExternalProof), att.AttestedAt) +}