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.
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ₙ )
linkₙ₋₁— the previous stamp's chain value, as a 64-char hex string;digestₙ— this stamp's 64-char hex SHA-256;utcₙ— the stamp's UTC time as an ISO-8601 string (the exact bytes used are returned by the API).
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:
-
Bitcoin, via OpenTimestamps.
The root is submitted to OpenTimestamps calendars (typically within the hour) and, once it is
included in a Bitcoin block, the
.otsproof upgrades to a Bitcoin attestation. The.otsfile is downloadable and verifiable with the standard OpenTimestamps client against the public Bitcoin blockchain — independent of BeatTime entirely. - An independent bank reference. Each closed week's root is also anchored against an independent, bank-issued reference — a number issued by a regulated financial institution, different for each anchoring, that records the root by a given date in a system we do not control. The individual reference is shown on the public proof page and on each certificate; a redacted statement may be published as supporting evidence.
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:
- Inclusion — recompute the leaf and walk the proof to the weekly root.
- Signature — check the Ed25519 signature over the canonical message of §4.
- Bitcoin — verify the downloaded
.otswith 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:
- authorship or ownership — anyone can stamp any hash;
- the truth or meaning of the content;
- that it is a qualified electronic timestamp under eIDAS or any specific legal regime — it is a best-effort, public attestation, provided as-is (see the Terms).
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
| Endpoint | Purpose |
|---|---|
POST /api/proof/stamp | Record a digest (body: {"digest": "<64-hex>"}). |
GET /api/proof/verify | Look 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/entries | The whole log, page by page (from, limit, follow next). |
GET /api/proof/checkpoints/latest | The latest signed checkpoint; /api/proof/checkpoints/<n> for any other. |
GET /api/proof/consistency | RFC 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:
- Recorded — BeatTime's clock, to the microsecond (
time.recorded). - Not earlier than — the Bitcoin block named in the last checkpoint issued
before the entry. The entry lies outside that checkpoint's tree, so it was added later, and
the checkpoint could not exist before its block was mined (
time.not_before). - Not later than — the earliest anchor that covers the entry: the OpenTimestamps
proof of the first checkpoint that contains it, the OpenTimestamps proof of its weekly root, or the
bank transfer for that root (
time.not_after; all of them intime.upper_bounds).
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.
- Checkpoints, one static page per week: /checkpoints/
- Independent copies and the exact formats: github.com/DeiFlagellum/sigelith-log
- The whole log:
GET /api/proof/entries, or weekly dumps at/dumps/<week>.jsonl
№Signing keys
№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:
- Authoritative roots are the ones published on /proof/, each carried by a bank reference issued by a third party and by a Bitcoin attestation.
- A weekly root frozen before its own week ended cannot be that week's final root.
The rejected
2026-W25root was frozen on the Monday of that week. - 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:
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.
- Changed: the name, the address, and the name of the repository with the weekly
releases —
DeiFlagellum/beattime-logbecameDeiFlagellum/sigelith-logbefore its first release; GitHub redirects the old address. Every weekly release, Internet Archive snapshot and Zenodo version is published under the new name, from the first one (week2026-W39). - Not changed, and never will be: the entries and checkpoints of the log, the
signing keys and their history above, the anchors, and the format identifiers inside signed data —
beattime-proof-v1,beattime-entry-v1andbeattime-checkpoint-v1. Those strings are part of the signed bytes; renaming them would break every proof already issued. A future format would get a new identifier, and the old ones would stay verifiable. - Documents issued as BeatTime — certificates, proofs, links — keep verifying exactly as before, at beattime.live and at sigelith.org alike.
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.