OpenTimestamps & Bitcoin anchoring
A signed log proves we attest to its history - it doesn't prove we can't quietly rewrite that history and re-sign it.
If the only clock in the system is the one the log operator controls, "tamper-evident" is a courtesy, not a guarantee. OpenTimestamps fixes that: it commits a hash into the Bitcoin blockchain, so "this existed by time T" no longer depends on anyone's word - including ours.
Whisper's transparency log already makes issuance and revocation append-only and independently checkable. This page is the layer underneath that: how the log's signed checkpoint gets a Bitcoin timestamp, why a block height is a clock nobody can move, and how to verify the proof yourself with nothing but a stock ots client and a block explorer.
The problem OpenTimestamps solves
A hash and a signature prove integrity ("this exact bytes, signed by this key") but not time. If Whisper wanted to back-date an identity's issuance - to make a compromised key look like it was registered before an incident, say - a self-signed timestamp is worthless as a defense: the log operator holds both the signing key and the clock. Auditors and courts have the same objection to any purely internal timestamp, which is why RFC 3161 Time-Stamping Authorities exist - except a TSA is itself a trusted third party you must trust not to backdate on request.
OpenTimestamps (Todd, 2016) removes the trusted party by borrowing a clock nobody controls: Bitcoin's proof-of-work chain. A commitment mined into a block can only have existed at or before that block's height, because moving it earlier would require re-mining every block since - an amount of work that grows with the security budget of the entire Bitcoin network. That is the whole idea: the timestamp's integrity is backed by physics and economics, not by trusting Whisper, a calendar server, or anyone else.
The mechanism, precisely
1. Hash, don't publish
The client never sends the document - only its SHA-256 digest. For Whisper this digest is the signed transparency-log checkpoint root: 32 bytes, the same C2SP signed-note structure served at /checkpoint. Anchoring only the root reveals nothing about the log's contents - not even how many entries it holds.
2. Calendar servers aggregate
A submitted hash goes to one or more public calendar servers (e.g. alice.btc.calendar.opentimestamps.org, bob.btc.calendar.opentimestamps.org, finney.calendar.eternitywall.com). A calendar batches everyone's hashes that arrive within its commit window into its own Merkle tree and returns a pending attestation: a Merkle path from your hash up to a root the calendar promises to commit to Bitcoin. This is the aggregation step that makes the scheme practically free - thousands of unrelated timestamps from unrelated services worldwide share a single on-chain transaction, so the marginal cost per timestamp is close to zero and no client needs a wallet, gas, or a node.
3. One Bitcoin transaction commits the root
Periodically (calendars typically batch on the order of an hour) the calendar mines its current aggregate root into a real Bitcoin transaction, committing the 32 bytes via a standard OP_RETURN output (or, in some implementations, via a Pay-to-Contract-style tweak of an output's public key - either way the commitment is unspendable data, not a transfer of value). Once that transaction confirms, the calendar's promise becomes checkable fact: anyone can walk the Merkle path from your original hash, through the calendar's aggregate root, into the transaction, into that block's own Merkle root of transactions - the same 80-byte block header structure defined for Bitcoin's block format - and match it byte-for-byte.
4. Block height is the timestamp - and it's unforgeable
The proof's timestamp is the height (and therefore the median time) of the block that mined the commitment. Rewriting it would mean re-mining that block and every block after it with more cumulative proof-of-work than the honest chain - the identical guarantee that secures every Bitcoin transaction, at Bitcoin's full difficulty. Nobody, including Whisper, including the calendar operator, can move a confirmed commitment to an earlier height after the fact. That's the "even we cannot back-date the log" claim, made literal.
5. The .ots file is the whole proof, portable and offline-verifiable
An .ots file is a small binary description of the operation chain from your original hash to a Bitcoin block: a sequence of sha256/ripemd160/append/prepend ops (the calendar's Merkle path) terminating in an attestation - either pending (a calendar promise, not yet a chain fact) or bitcoin (the transaction id and block height, with the path proven against a real header). Verifying it needs no calendar, no OTS service, and no Whisper endpoint at all - only Bitcoin block headers, which anyone can fetch from a full node or a public explorer.
A window to be plain about. A freshly stamped checkpoint is pending - a calendar's promise - until its batch confirms and the proof is upgraded with the real Bitcoin transaction and Merkle path, typically within an hour. Whisper reports this status plainly rather than implying real-time anchoring: the "cannot be back-dated" guarantee only applies once a root is upgraded and confirmed.
Whisper's anchor, end to end
Every signed checkpoint of the transparency log - the same Ed25519-signed origin / tree_size / root_hash triple served over C2SP tlog-tiles - gets its root submitted to public OTS calendars off any hot path. Minting, resolution, and the log itself never wait on a calendar; if every calendar were unreachable, checkpoints would still sign and serve, and the anchoring backlog would simply catch up later. Once a calendar's batch confirms, the proof is upgraded and the checkpoint's Bitcoin attestation is served for anyone to fetch and verify independently of Whisper's word:
GET https://whisper.online/checkpoint # origin, tree_size, root_hash, Ed25519 signature
GET https://whisper.online/checkpoint/ots # the OpenTimestamps proof for the current checkpoint
GET https://whisper.online/ots/<tree_size> # the proof for a specific historical checkpoint
Verify it yourself: stock tools vs. Whisper
With stock tools - no Whisper software, just the open OpenTimestamps client and a public Bitcoin explorer. Fetch /checkpoint/ots for the checkpoint you just pulled and the proof can still be pending (the window above) - /checkpoint/ots/latest-confirmed instead names the newest checkpoint that has already upgraded to a Bitcoin fact, with its own size and root, so the client below has something to verify against. For a minute or so right after a checkpoint upgrades, that pointer can name a size whose proof is still a calendar promise; the script below prints exactly that and tells you to ask again, rather than comparing nothing. Run it in a directory of its own; it writes lc.json and checkpoint.ots there and puts the client in an otsenv/ beside them:
# the newest checkpoint that's already a confirmed Bitcoin fact, not a pending promise
curl -s https://whisper.online/checkpoint/ots/latest-confirmed | tee lc.json
{"object":"whisper-ledger-ots-latest-confirmed","tree_size":260301,
"root":"q9Qc45nBkOYbcBhWPx/44YyHgECXkDn39gYf053oGg8=","bitcoin_block":966095,
"stamped_at_millis":1788881464627,"confirmed_at_millis":1788893113172,
"ots":"/ots/260301","consistency":"/consistency?from=260301"}
Then the client, the proof, and the two things worth checking about it. Save this as check-anchor.sh and run bash check-anchor.sh: it refuses rather than checking nothing at three separate points, and those refusals are exits, which belong in a script and not pasted into your shell.
# the standard, independent OpenTimestamps client, in a venv of its own so a
# PEP 668 system python has nothing to refuse
python3 -m venv otsenv && ./otsenv/bin/pip install -q "opentimestamps-client==0.7.2"
# the size comes from that JSON, never hard-coded: sizes settle and roll forward, and a
# proof for a size that has rolled past is a 404, not a broken anchor
SIZE=$(python3 -c 'import json;print(json.load(open("lc.json")).get("tree_size",""))')
[ -n "$SIZE" ] || { echo "no tree_size in lc.json" >&2; exit 1; }
curl -s "https://whisper.online/ots/$SIZE" -o checkpoint.ots
[ -s checkpoint.ots ] || { echo "no proof for size $SIZE" >&2; exit 1; }
# what the proof says about itself: the digest it stamps, the block it names, and the
# merkle root its whole operation chain folds to for that block
./otsenv/bin/ots info checkpoint.ots | grep -E 'File sha256|Attestation|merkle root'
# the digest has to be the root we publish, or the proof is over some other bytes. It sits
# 33 bytes in: after the 31-byte magic, the version byte and the hash-op byte
python3 -c 'import base64,json;lc=json.load(open("lc.json"));d=open("checkpoint.ots","rb").read();print(d[33:65].hex() == base64.b64decode(lc["root"]).hex())'
# now the block, from two unrelated explorers, all the way to the MERKLE ROOT. The block
# hash agreeing only proves they know the same block; the merkle root is what the proof's
# own operation chain computes, so it is the line that closes the chain. A proof still
# waiting on its calendar names no block, so this refuses rather than checking nothing
BLOCK=$(./otsenv/bin/ots info checkpoint.ots | sed -n 's/.*BitcoinBlockHeaderAttestation(\([0-9]*\)).*/\1/p')
if [ -z "$BLOCK" ]; then
echo "still a calendar promise, not a Bitcoin fact: ask for /ots/$SIZE again"
else
for API in https://mempool.space/api https://blockstream.info/api; do
HASH=$(curl -s "$API/block-height/$BLOCK")
[ -n "$HASH" ] || { echo "$API has no block $BLOCK" >&2; continue; }
echo "$HASH $(curl -s "$API/block/$HASH" | python3 -c 'import json,sys;print(json.load(sys.stdin)["merkle_root"])')"
done
fi
File sha256 hash: abd41ce399c190e61b7018563f1ff8e18c878040979039f7f6061fd39de81a0f
verify BitcoinBlockHeaderAttestation(966095)
# Bitcoin block merkle root b5d81f78e0d71095f224aaa2b6acfa03c80f98e662fe29a03b6f237ee71d0002
True
00000000000000000001e13dbe8d42237c3479d2f0b0a2353885bf333d993126 b5d81f78e0d71095f224aaa2b6acfa03c80f98e662fe29a03b6f237ee71d0002
00000000000000000001e13dbe8d42237c3479d2f0b0a2353885bf333d993126 b5d81f78e0d71095f224aaa2b6acfa03c80f98e662fe29a03b6f237ee71d0002
Read the last three lines together, because that is where the anchor actually settles. True says the 32 bytes the proof stamps are the checkpoint root we publish. The proof then computes b5d81f78… as block 966095's transaction merkle root, and two explorers with no relationship to us, or to each other, report the same value for that block. So the operation chain in the .ots file lands exactly where a real Bitcoin block header says it should.
Every number above is a snapshot of a live, growing log. Sizes roll forward, /ots/<size> answers 404 once a size has rolled past, and the block moves with them, so take the size from lc.json and expect your own run to print different digits. What has to hold is the two matches, not the values.
This section used to stop one line short. It printed the block hash from both explorers and called that the cross-check. Two explorers agreeing on a block hash only shows they know the same block; it says nothing about whether the proof folds into it. The merkle-root line above is the one that does that work. /verify step 5(c) walks the same check with the two traps spelled out: OpenTimestamps folds in Bitcoin's internal byte order while explorers display the reverse, and a hand-rolled proof walker that mishandles one operation prints a perfectly plausible 32 bytes that match nothing, which looks identical to a broken anchor.
ots info walks the proof - the calendar's Merkle path, then the Bitcoin transaction, then that block's own merkle root - without needing a Bitcoin node at all; ots verify additionally checks the terminal BitcoinBlockHeaderAttestation against a node's block index if you run one. Either way the comparison that decides it is the merkle root, and running that against two unrelated explorers means no single party can feed you a forged header. One gotcha worth knowing before you hit it: naively curl-ing /checkpoint and /checkpoint/ots as two separate requests can pull proofs for two different checkpoints on a ledger this active - pin the size from /checkpoint/ots/latest-confirmed and fetch /ots/<that size> instead, exactly as above, so the file and the proof always agree.
With Whisper - the trustless verifier folds the DNSSEC, DANE and transparency legs into a single command, alongside identity-document verification, for any agent identity:
whisper verify --trustless acef2002a323d40d4.t7af9242c44554e0f8e183ac8e6fbf645.agents.whisper.online
CHECK RESULT TRUST DETAIL
dnssec pass DNSSEC-root AAAA, PTR and TLSA(3 1 1) all DNSSEC-validated to the IANA root; …
dane pass DNSSEC-root served leaf SPKI-SHA256 == a published TLSA pin (1 published); …
transparency pass DNSSEC-root root signature verified; 1 event(s), root_hash bound; ledger checkpoint verified (no leaf for this address)
identity_doc pass DNSSEC-root JWS verified against the DNSSEC-anchored key; address/fqdn/tlsa claims match the DNSSEC-validated facts
whisper: acef2002a323d40d4.t7af9242c44554e0f8e183ac8e6fbf645.agents.whisper.online is CRYPTOGRAPHICALLY PROVEN - trust anchor: DNSSEC root (IANA anchor) + DANE-EE + DNSSEC-anchored transparency/ledger keys -- Whisper API NOT trusted
There is no standalone bitcoin row: the transparency check is where the ledger's checkpoint (and, transitively, its Bitcoin anchor) gets verified, and --trustless folds it in without a separate flag. The checkpoint's own confirmed-Bitcoin status, independent of any one agent, is the /checkpoint/ots/latest-confirmed call above.
The identity/transparency bundle for a specific agent is also plain JSON, for scripting into a compliance pipeline - and the checkpoint's Bitcoin status is a second, separate call, since one is per-agent and the other is per-checkpoint:
curl -s https://rdap.whisper.online/ip/2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4/transparency | jq '.root_hash, .ledger'
curl -s https://whisper.online/checkpoint/ots/latest-confirmed | jq '.tree_size, .bitcoin_block'
Why not just stamp Bitcoin directly?
A self-funded daily OP_RETURN transaction is a documented, optional sovereignty upgrade - it removes dependence on third-party calendars entirely - but it reintroduces a hot wallet into infrastructure that otherwise needs none, purely to save the calendars' aggregation step. Calendars keep the anchor at effectively zero marginal cost and zero custody risk, which is why they're the default; a wallet-funded path is a deliberate later choice, not a requirement to get a real Bitcoin timestamp today.
What this doesn't prove
OpenTimestamps proves existence by a time, not exclusivity or correctness of what's inside the log. A malicious log operator could still, in principle, run two consistent-looking logs in parallel (a "split view") and show each auditor a different one - which is exactly what independent witnessing (a second, non-Whisper party co-signing the same checkpoint) is for, layered on top of the Bitcoin anchor rather than replacing it. See Transparency log for the full witnessing picture and current status.
Next
- Transparency log - the RFC-6962 Merkle log this checkpoint comes from, and the checkpoint/inclusion-proof API in full.
- Verify an agent -
whisper verify --trustless, the full DNSSEC → DANE → transparency → Bitcoin chain in one command.