How to verify an OpenTimestamps proof
To verify an OpenTimestamps proof, keep the original file next to its .ots file and run ots verify with the official client. When the proof is complete, the client names a Bitcoin block whose time shows that the file's exact bytes existed by then. A full check needs a Bitcoin Core node the client can reach, and a pruned node is enough; without one you can still inspect the proof with ots info or use the web verifier at opentimestamps.org.
- Updated
- Reading
- 5 min
- The need
- Confirm that a file with an .ots proof really existed by a certain date, without trusting whoever published it.
- Plain text
- /guides/verify-opentimestamps.md
#What a timestamp proves
An OpenTimestamps proof is a small .ots file. It links the digest of one file, SHA-256 by default, to a Bitcoin block header through a list of hash operations. If every step checks out, the file's exact bytes existed no later than the time of that block.
That is all it proves. It does not show that the content is true, who wrote it, or that the file was made at that moment rather than earlier. Change one byte, even a trailing newline, and the proof no longer matches.
#Before you start
You need three things:
- Python 3 and the official client:
pip3 install opentimestamps-client. - The original file and its
.otsproof, downloaded byte for byte. - For a full check, a Bitcoin Core node the client can reach. The client's README says a pruned node is fine.
#Step by step
This walkthrough uses public files: the day file for October 19, 2026 from Upvera's root repository, and its proof.
curl -sO https://raw.githubusercontent.com/upvera-co/ledger-roots/main/days/2026/10/2026-10-19.json
curl -sO https://raw.githubusercontent.com/upvera-co/ledger-roots/main/days/2026/10/2026-10-19.json.ots
1. Check that the digests match
The first line of ots info is the digest the proof commits to. Compare it with your own hash of the file.
shasum -a 256 2026-10-19.json # on Linux: sha256sum 2026-10-19.json
ots info 2026-10-19.json.ots | head -n 1
Both should show 1a5758739b37ad1eb0afdbe9552e641d78a29a830368840dcdb688bbe0a13a72. If they differ, stop. You have different bytes from the ones that were stamped.
2. Read the proof
The rest of ots info lists the operations (append, prepend, sha256) and how each path ends:
verify PendingAttestation('https://alice.btc.calendar.opentimestamps.org')
verify BitcoinBlockHeaderAttestation(<height>)
A PendingAttestation is a calendar server's promise to anchor the digest later. A BitcoinBlockHeaderAttestation is a path that ends in a specific block. Only the second can be checked against Bitcoin.
3. Verify
Run the client with the original file in the same folder:
ots verify 2026-10-19.json.ots
The client assumes the target has the same name without .ots. Use -f path/to/file if it lives elsewhere. If the proof holds only pending paths, the client asks the calendars for the missing parts. You will see one of these:
- A line ending in
Pending confirmation in Bitcoin blockchain. The calendar has not anchored the digest yet. The README notes this takes a few hours, so try again later. Success! Bitcoin block <n> attests existence as of <date>. The path ends in block<n>, and its result matches that block's header on your node.- An error saying the client could not connect to a Bitcoin node. It has nothing to check the path against. Point it at your node with
ots --bitcoin-node http://USER:PASS@127.0.0.1:8332/ verify 2026-10-19.json.ots.
4. Keep the complete proof
ots upgrade 2026-10-19.json.ots writes the Bitcoin path into the proof itself, so later checks no longer depend on the calendar servers being online. The client keeps the old file with a .bak suffix.
#Without a Bitcoin node
A node of your own is the independent route, but not the only useful one.
ots infoworks offline. It shows which digest is committed and whether any path already ends in a Bitcoin block.- The web verifier at opentimestamps.org hashes the file in your browser and checks the proof there. It reads block data from a public block explorer, so for the last step you trust that service instead of your own node.
#Common mistakes
- Changed bytes. Opening the file in an editor and saving it, copying text from a web page, or a Git checkout with
core.autocrlf=true(which writes CRLF line endings) all change the digest. Download the raw file and compare hashes first. - Reading pending as failure, or as success. Pending means not anchored yet. It is neither a broken proof nor a confirmed one.
- Trusting the calendar. A calendar can fail to deliver a path, but it cannot forge one that your node accepts. Until the path ends in a block header, you hold a promise, not a proof.
- Reading the time too precisely. A block's time is set by the miner. It must be later than the median of the previous 11 blocks, and nodes reject headers more than two hours in their future. Read the result as "by about this time", not to the minute.
- Reading more into it. A valid timestamp says nothing about who made the file or whether its content is correct.
#Questions
Do I need a full Bitcoin node?
No. The client checks block headers, and the README states that a pruned node is fine. The node must have synced at least up to the block named in the proof.
How long until a new proof is complete?
A few hours, according to the client's README. Calendars collect many digests and anchor them together instead of making one transaction per timestamp.
Can someone backdate a file with OpenTimestamps?
Not to an earlier block. The digest has to be part of a block that was mined after the digest existed. Someone can stamp an old file late, though, so a timestamp is an upper bound, never a creation date.
What if the calendar servers go offline?
An upgraded proof contains the full path to the block header and can be checked with the client and a node alone. Upgrade a proof once it is confirmed, and keep that copy.
Does the calendar see my file?
No. The README explains that a calendar only receives an opaque digest protected by a nonce, never the file's contents.
#How Upvera does it
Upvera Day Roots publishes one file per day to the public repository upvera-co/ledger-roots. Each day file is canonical JSON holding that day's root and the digest of the previous day, with an .ots proof beside it. You can run the steps above on any day. Whether a given proof shows pending or success depends on when you check and whether it has been upgraded, so run the check instead of taking our word for it.
A timestamp covers one file. To confirm that no day was rewritten in between, also follow the prev links, as described in How to verify a hash chain. The design is written up in the Day Roots report.
#Sources
- OpenTimestamps client README: commands, node requirements and privacy.
- opentimestamps.org: the web verifier.
- Bitcoin block header reference: the rules for a block's time field.
- git-config: core.autocrlf: line ending conversion on checkout.
- upvera-co/ledger-roots: the files used in the example.