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.

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.

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

Ed25519 public key (raw, base64) — verify weekly root signatures with this:
PNMoAM0Lq+gqQJaFN4iZJf1RxXZkP6IYQNb6CtaCnYk=

← back to Proof of existence