When you timestamp a file, what does that timestamp actually say about when the file was made?
The obvious answer is "when it was made." That's wrong, and fixing it changes how you set up your pipeline.
What an anchored hash actually proves
Here's the mechanism. You hash the file:
sha256sum release-v2.tar.gz
That digest goes into a transaction, and the transaction lands in a block. Anyone can later recompute the hash of your file, find it in that block, and read the block's time.
So what have you proven? That this exact sequence of bytes existed no later than that block. Someone had to know the hash to put it on chain, and you can't know a SHA-256 digest without having the bytes. That part holds up, and it doesn't depend on trusting whoever submitted it.
But look at what it doesn't say.
It doesn't say the file was created at that moment. It doesn't say it was created near that moment. The file could have been sitting on a disk for a long time before anyone hashed it. A timestamp is an upper bound. It closes off one side of the timeline and leaves the other side wide open.
Why the open side matters
For a lot of uses the upper bound really is all you need. "This build artifact existed before the incident." "This dataset existed before the model was trained on it." "I had this design before you published yours." In every one of those, earlier is fine. Nobody's hurt if your file turns out to be older than the proof says.
Now flip it. Say you're trying to show something was produced after a point. A log that was written after a deploy and not prepared beforehand. A test report that reflects the code as it stood after a fix, not one generated earlier and held in reserve. A prediction that you want to show was made fresh, not pre-written in several versions with only the winner anchored.
An anchored hash says nothing about any of those. If I can prepare ten versions of a document today and anchor whichever one turns out right next week, the anchor proves the winning version existed next week. True, and useless.
So the honest version of what a timestamp gives you is narrower than the word suggests. You get one bound. Whether that's enough depends entirely on which direction the person checking it cares about.
Getting the other bound
The trick is old and it's simple: put something unpredictable from a known moment inside the file before you hash it.
A recent block hash works well, because nobody can know it before that block exists:
# grab the current best block hash from a Bitcoin node you run
BLOCK=$(bitcoin-cli getbestblockhash)
# write it into the artifact itself, then hash
echo "anchor-after: $BLOCK" >> build-report.txt
sha256sum build-report.txt
Now the file carries its own lower bound. It contains a value that didn't exist until that block was mined, so the file can't be older than that block. Anchor the result and you've got the upper bound too. The file existed somewhere inside a window, and both edges of the window can be checked by a stranger without asking you.
That's the step that turns "a timestamp" into "a time range someone can rely on."
A couple of things to be careful with:
- The value has to be inside the bytes you hash. Putting it in a filename, a database row next to the file, or a metadata field your hashing step skips gets you nothing. If it isn't in the hash, it isn't in the proof.
- The value has to be unpredictable. A date string doesn't work. Anyone can type tomorrow's date. It needs to be something nobody could have known in advance.
ProofLedger doesn't do this for you, for what it's worth. It hashes whatever bytes you hand it and doesn't care what's inside them. That was a deliberate choice: one engine, no file-type pipelines. The cost is that the lower bound is your job. If your format can't hold an extra line, wrap it. Put the original file and a small manifest with the block hash in an archive, and hash the archive.
Narrowing the window from the other side
The upper bound has slack in it too, and the slack is the time between "the file was finished" and "the hash landed in a block."
Part of that is the chain. A block's timestamp isn't an atomic clock reading. Consensus rules give miners some leeway, so read it as approximate, not to the second.
Most of it, though, is your own process. If an artifact gets built in CI and somebody anchors it by hand later, the window is however long "later" turned out to be. That's why I'd anchor at the point of production, in the same job that builds the thing. ProofLedger's API has one endpoint for that, POST /api/v1/proof, which takes a hash with a Bearer sk_ key. There's also a GitHub Action for CI/CD if you'd rather not script the call yourself.
Dual anchoring helps a bit here too. The same hash goes to Polygon and to Bitcoin, so you get two independently checkable upper bounds. Each one stands alone, and whichever confirms first is the tighter one. It also means a checker isn't relying on a single chain to read the time.
Where the time on your proof comes from
One more thing worth checking, because it's easy to blur. A timestamping service can show you several different "times":
- when you clicked submit
- when the issuer's server logged the request
- when the transaction was confirmed in a block
Only the last one can be checked without trusting the issuer. The first two are the vendor's word. They might be perfectly accurate, but a stranger reading your proof can't confirm them independently, and independent confirmation was the whole point.
This is why I made ProofLedger's verify path public. GET /api/v1/verify?hash=<sha256> needs no auth, so an auditor or the other side of a dispute can run it themselves. The verify-proof PyPI package does the same check locally and offline. And the owner-only status endpoint, GET /api/v1/proof/:id, hands back the actual chain_tx and bitcoin_txid. That way the time can always be traced to a transaction, not just to a field in my database.
curl "https://proofledger.io/api/v1/verify?hash=$(sha256sum build-report.txt | cut -d' ' -f1)"
A test you can run today
Pick one file you've already timestamped, with any tool or vendor, and work through it:
- Find the transaction. Can you get from the proof to a specific transaction ID on a specific chain? If the proof only shows a time and a "verified" badge, stop here. What you have is the issuer's claim.
- Read the time off the block yourself. Open that transaction in a block explorer you pick, not one the vendor links you to. Does the block time match what the proof shows you? If it doesn't, figure out which of the times above you were actually looking at.
- Ask which bound you need. Write one sentence about what this proof is meant to show. If the sentence contains "after," "since," or "fresh," an anchor on its own doesn't cover it.
- Check for a lower bound. Open the file. Is there anything in it that couldn't have been known before a specific moment? If not, add one next time, inside the bytes, before hashing.
- Measure your own gap. From your CI logs, look at the time between the artifact being produced and the hash being submitted. That gap is slack in your upper bound that you added yourself.
If you get through all five, you'll know what your timestamp proves and where its limits are.
I build and run ProofLedger myself, and the docs for the three endpoints above are at proofledger.io/api.html.
Disclosure: this article was drafted by an AI agent I built and run, from facts I supplied about my own project.
Top comments (0)