Proof specification

How BeatTime's proof-of-existence layer works — precisely enough to verify it yourself, without trusting us.

protocol: beattime-proof-v1 · last updated June 2026

BeatTime can record that a file existed at a point in time — without ever receiving the file. Only its SHA-256 hash is submitted. Every hash is woven into an append-only hash-chain, batched weekly into a Merkle tree, and the weekly root is sealed by three independent means: an Ed25519 signature, the Bitcoin blockchain (via OpenTimestamps), and an independent, bank-issued reference. None of these require trusting BeatTime to check — each can be verified against an outside authority.

  your file ──SHA-256──▶  digest (64-hex, computed on your device)
                              │   the file never leaves your device
                              ▼
  append-only hash-chain:  linkₙ = SHA-256( linkₙ₋₁ ‖ digest ‖ UTC )
                              │   one ISO week of stamps
                              ▼
  Merkle tree (RFC 6962-style)  ───────▶  weekly root
                              │   frozen when the week closes
              ┌───────────────┼────────────────────┐
              ▼               ▼                     ▼
        Ed25519          Bitcoin            independent
       signature      (OpenTimestamps)        bank reference

1Stamping a hash

The unit of proof is a document's SHA-256 digest, written as 64 lowercase hexadecimal characters. The digest is computed on your device (in the browser via Web Crypto, or in the app). Only those 64 characters are sent to POST /api/proof/stamp — never the file, its name, or its size.

Stamping is idempotent and first-seen: the first time a digest is recorded it is timestamped; re-submitting the same digest always returns that original timestamp (and HTTP 200 rather than 201). A stamp stores the digest, its @beat and UTC time (to centibeat precision), its position in the hash-chain, and its ISO week.

The recorded time is an “existed by” time — the moment the hash reached the server. When stamping a photo, the app additionally embeds the capture @beat in EXIF; that capture time is a self-declared label, while the proof time is the server-witnessed one.

2The append-only hash-chain

Stamps form a tamper-evident chain in insertion order. Each new link folds in the previous link, so no earlier entry can be altered, reordered or removed without breaking every link after it:

link₀     = "0000…0000"            (GENESIS, 64 zero hex chars)
linkₙ     = SHA-256( linkₙ₋₁ ‖ digestₙ ‖ utcₙ )

The three strings are concatenated and hashed as UTF-8. The result is this stamp's chain value and becomes the input to the next one.

3Weekly Merkle tree

Time is partitioned into ISO weeks (YYYY-Www, Monday→Sunday; a week closes the following Monday 00:00 UTC). All stamps in a week, ordered by insertion, form the leaves of a binary Merkle tree built the RFC 6962 way, with domain separation between leaves and internal nodes:

leaf  = SHA-256( 0x00 ‖ digest_bytes )      // digest_bytes = the 32 raw bytes of the document's SHA-256
node  = SHA-256( 0x01 ‖ left ‖ right )      // left, right = 32-byte child hashes

Leaves are paired left-to-right at each level. If a level has an odd number of nodes, the lonely node is carried up unchanged (it is not duplicated) — avoiding the well-known second-preimage ambiguity of duplicate-last-node schemes. The single hash remaining at the top is the weekly root.

While a week is open its root is provisional (recomputed as stamps arrive). When the week is closed the root is frozen and stored; from then on it never changes, and the signing and anchoring steps below apply to that frozen value.

4Signing the weekly root

At closing, the frozen root is signed with an Ed25519 private key (held only in the server environment, never in the repository). The signed message is canonical and binds the week to its root, so a signature cannot be replayed onto a different week or root:

message  = "beattime-proof-v1|" + week_key + "|" + root_hex        (UTF-8 bytes)
signature = Ed25519-Sign( private_key, message )                   (returned base64)

The corresponding public key is published (below and via the API) so anyone can verify the signature independently with any standard Ed25519 library.

Key rotation. A key that has signed a published root is never deleted from the record: when it is replaced it moves to the key history with the date it was retired and the reason. A retired key signs nothing new. Every closed root is then re-signed with the current key — the root itself does not change, and its existence in time is attested independently by Bitcoin and the bank reference (§5), so re-signing moves nothing in time. The API always reports the key that actually produced a given signature, so a signature from an older PDF certificate stays verifiable against the history below.

5External anchoring — Bitcoin & bank reference

The signature proves we attest to the root. The two anchors below give dated records of the root that exist outside our own infrastructure, so the timestamp does not rest on trusting BeatTime:

Anchoring runs after a week has ended, so a certificate downloaded for a current week correctly shows the seals as still pending. The timestamp itself is fixed at stamping time; the seals only add independent corroboration of it.

6Verifying a proof yourself

Call GET /api/proof/verify?digest=<hex>. For a stamped hash it returns the timestamp, the weekly root, the inclusion proof, the signature + public key, and the OpenTimestamps / Bitcoin status. Verification has three independent parts:

  1. Inclusion — recompute the leaf and walk the proof to the weekly root.
  2. Signature — check the Ed25519 signature over the canonical message of §4.
  3. Bitcoin — verify the downloaded .ots with the OpenTimestamps client.

Inclusion proof

The inclusion_proof is an ordered list of sibling hashes, each tagged with the side it sits on ("L" = sibling is on the left, "R" = on the right). Start from your leaf and fold in each sibling; the final value must equal week_root:

h = SHA-256( 0x00 ‖ digest_bytes )                  # your leaf
for (side, sibling) in inclusion_proof:
    if side == "L":  h = SHA-256( 0x01 ‖ sibling ‖ h )
    else:            h = SHA-256( 0x01 ‖ h ‖ sibling )
assert h == week_root

The Python and PHP SDKs and the REST API reference wrap these calls; you can also download a self-contained PDF certificate for any stamped hash at /api/proof/cert/<digest>.

7What a proof does and does not attest

A confirmed proof attests existence by a point in time and integrity: the exact bytes that hash to that digest existed no later than the recorded time, and have not changed since. It does not attest:

Entries are public and permanent (append-only). Do not stamp a hash you need to keep secret — although the hash reveals nothing about the file, the fact that some file with that hash existed becomes public.

8API & public key

EndpointPurpose
POST /api/proof/stampRecord a digest (body: {"digest": "<64-hex>"}).
GET /api/proof/verifyLook up a digest: timestamp, root, inclusion proof, signature, anchors.
GET /api/proof/cert/<digest>Download the PDF certificate for a stamped hash.
GET /api/proof/ots/<week_key>Download the week's OpenTimestamps .ots proof.
GET /api/proof/entriesThe whole log, page by page (from, limit, follow next).
GET /api/proof/checkpoints/latestThe latest signed checkpoint; /api/proof/checkpoints/<n> for any other.
GET /api/proof/consistencyRFC 9162 consistency proof between the tree sizes of two checkpoints (first, second).

Full request/response schemas are in the OpenAPI reference. Stamping is rate-limited; verification more generously so.

9Public log & checkpoints

Besides the weekly trees, one global Merkle tree covers the whole log from its first entry, in entry order. Every day, and right after each week closes, BeatTime signs the state of that tree — a checkpoint — and chains it to the previous checkpoint by the SHA-256 of its file. Each checkpoint names a recent Bitcoin block, so it cannot be older than that block, and is itself anchored in Bitcoin through OpenTimestamps. Every week's checkpoints, log dump and page are copied to an immutable release on GitHub.

This lets anyone audit the log itself, not only a single proof: download every entry, recompute the hash chain and every root, and check with an RFC 9162 consistency proof that a later checkpoint extends an earlier one — nothing removed, nothing rewritten. Take the checkpoint from an independent copy rather than from us.

Three times for every entry

GET /api/proof/verify and the PDF certificate give each entry three times. Only the first one is our word; the other two come from Bitcoin and the bank and bracket the moment the entry must have been recorded:

Block times come from block headers, which miners set within about two hours, so the bounds are accurate to hours, not seconds — and independent of our clock. The response also carries the entry's path in the global tree to the first checkpoint that contains it (checkpoint: number, file hash, leaf_index, tree_size, audit_path), so the certificate plus a checkpoint taken from an independent copy is enough to verify without us.

№Signing keys

Current Ed25519 public key (raw, base64) — in use since 2026-09-21. Verify weekly root signatures with this:
e7y9THJIUKvNKOZHmdBjJ8E0bOKyBFVxxMpAJ8w574Y=
Retired key — used 2026-06-15 to 2026-09-21. Signs nothing after 2026-09-21.
YNVYXDyg3hQGM3F+/ec+ZNmeN1JI/hZX+CxLqyJdyN0=
Why it was retired: Rotated on 2026-09-21 after this key turned out to be configured on two machines at once — production and a development mirror — and the mirror signed and Bitcoin-anchored its own weekly roots for 2026-W25 and 2026-W30. A signature by this key therefore no longer identifies a root as ours on its own. Full account, including the rejected roots and how to tell them apart: /spec/#incident-2026-09

№Key incident, September 2026

Until 21 September 2026 the same Ed25519 private key was configured on two machines: this service and a development mirror. The mirror runs the same scheduled jobs, so it closed weeks of its own, signed those roots with that shared key and anchored them in Bitcoin through OpenTimestamps. Two such roots exist, for 2026-W25 and 2026-W30. They are listed below.

The practical consequence is worth stating plainly: for the period that key was in use, a valid signature alone no longer proves a root is ours, because the mirror could produce one too. Nothing about the hashes recorded in the log changed, and no stamp was lost, altered or backdated — but a claim resting on that key's signature and nothing else is weaker than this page previously implied.

If you hold a certificate issued before 21 September 2026: your timestamp stands. The document hash, the inclusion path, the weekly root, the Bitcoin attestation and the bank reference are unchanged and still verify. The certificate names the retired key; download it again (or re-verify the hash) to get the current signature.

How to tell an authoritative root from a rejected one — three checks you can run without trusting anything we say:

  1. Authoritative roots are the ones published on /proof/, each carried by a bank reference issued by a third party and by a Bitcoin attestation.
  2. A weekly root frozen before its own week ended cannot be that week's final root. The rejected 2026-W25 root was frozen on the Monday of that week.
  3. Re-verify your own hash against the published root: the inclusion path on your certificate must recompute to it. A root that no stamp of yours leads to is not the root that recorded your document.

Roots we publish in order to reject them — signed with the retired key, never authoritative:

Rejected root for 2026-W25 — frozen 2026-06-15 on the development mirror, anchored in Bitcoin at block 953768.
b84714db78def4c98a429e0d0e422994cbc32e9de189fe60c96367e10a448969
Frozen on the Monday of the very week it claims to summarise, so it cannot be that week's final root.
Rejected root for 2026-W30 — frozen 2026-08-03 on the development mirror, anchored in Bitcoin at block 960882.
7225b61edee6cd82c583759cdca1ae5155b8332cc4e806e5c88bc69c28c270b0
Covers a single test stamp made on the development mirror; the authoritative root for this week is anchored against bank reference 1144955485.

What changed since: the key was rotated on 21 September 2026 and every published root was re-signed with the current one. The retired key is refused by the signing code itself, each machine now pins the public key it is allowed to sign with, the mirror no longer anchors anything, and on 23 September 2026 the two roots above were removed from the mirror's database — after their full contents, including the raw OpenTimestamps proofs, were archived so that this account can be checked rather than believed.

№Naming history

On 27 September 2026 the proof infrastructure described on this page — the log, its checkpoints, certificates and the verification API — took the name Sigelith, with sigelith.org as its address. BeatTime remains the name of the time notation and of the clock apps. The operator is the same, and so is the system: sigelith.org and beattime.live are served by one installation, with one log and one signing key.

The name was announced in naming-2026-09-27.txt, recorded in this log as entry #191 on 27 September 2026 at 17:17:27 UTC (@720.45, week 2026-W39). Its SHA-256 is 9b7a539e…c60271 — download the file, hash it yourself and compare.

← back to Proof of existence