An expert witness gets handed a PDF. It says "Verified" in green text, with a transaction hash underneath. The other side's attorney asks the obvious question: verified by whom?
If the answer is "the company that sold this to the claimant," that's not verification. That's a receipt. The entire premise of anchoring evidence hashes to a public blockchain is that you shouldn't need to trust the vendor at all. The proof should stand up even if the vendor's servers are down, the company folded, or nobody on the stand has ever heard of it. This post builds a verifier that does exactly that: given a file and a certificate, it confirms the anchor by talking directly to public blockchain infrastructure. No vendor API in the loop.
What independent verification actually requires
Three things, and none of them involve calling back to whoever issued the certificate:
1. Recompute the file's hash yourself. Don't trust the hash printed on the certificate. 2. Look up the anchoring transaction directly on a public node or block explorer. 3. Confirm the hash you computed is actually embedded in that transaction's data.
If all three line up, the proof holds regardless of who generated the certificate. If the vendor is lying, or gone, or wrong, the chain itself contradicts them and you'll see it.
Step 1: recompute the hash
Never trust a hash string sitting in a JSON file. Read the actual bytes.
import hashlib
def sha256_file(path, chunk_size=65536):
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(chunk_size), b""):
h.update(chunk)
return h.hexdigest()
Compare this against the file_hash field in the certificate. If they don't match, stop. Nothing else in this process matters until they do.
Step 2: check the Polygon anchor against a public node
A Polygon anchor typically records the hash as calldata in a contract transaction, or as an event log. Either way, you don't need the vendor's dashboard to read it. You need a JSON-RPC call to any public Polygon node.
import requests
POLYGON_RPC = "https://polygon-rpc.com"
def get_transaction(tx_hash):
payload = {
"jsonrpc": "2.0",
"method": "eth_getTransactionByHash",
"params": [tx_hash],
"id": 1,
}
resp = requests.post(POLYGON_RPC, json=payload, timeout=10)
resp.raise_for_status()
return resp.json()["result"]
def hash_in_calldata(tx, file_hash):
input_data = tx["input"].lower().replace("0x", "")
return file_hash.lower() in input_data
polygon-rpc.com is a public endpoint maintained by the Polygon Foundation, not by whoever sold you the anchoring service. Anyone with the transaction hash from the certificate can run this and get the same answer, forever, independent of the original vendor.
Step 3: verify the Bitcoin batch with a merkle proof
Instant Polygon anchoring is useful, but Bitcoin gives you a settlement layer with a much longer track record and no single foundation controlling it. Anchoring every file's hash directly to Bitcoin isn't practical (fees, block space), so batch systems anchor once per day: every hash from that day gets folded into a merkle tree, and only the root goes into a Bitcoin transaction, usually via OP_RETURN.
Your certificate should include a merkle proof: the sibling hashes needed to walk from your file's leaf up to that day's root. Verifying it means recomputing the path yourself.
def sha256d(data):
return hashlib.sha256(hashlib.sha256(data).digest()).digest()
def verify_merkle_proof(leaf_hash_hex, proof, expected_root_hex):
current = bytes.fromhex(leaf_hash_hex)
for step in proof:
sibling = bytes.fromhex(step["hash"])
if step["position"] == "left":
current = sha256d(sibling + current)
else:
current = sha256d(current + sibling)
return current.hex() == expected_root_hex.lower()
Then pull the actual on-chain transaction from a public Bitcoin explorer API and confirm the OP_RETURN output matches the root your proof produced:
def get_op_return_data(txid):
url = f"https://blockstream.info/api/tx/{txid}"
resp = requests.get(url, timeout=10)
resp.raise_for_status()
tx = resp.json()
for vout in tx["vout"]:
script = vout["scriptpubkey"]
if script.startswith("6a"): # OP_RETURN opcode
return script[4:] # strip opcode + push length byte
return None
blockstream.info runs an open-source explorer with no account required. Any court, any auditor, any opposing expert can hit the same endpoint and reproduce your result.
Tying it together
def verify(file_path, certificate):
file_hash = sha256_file(file_path)
assert file_hash == certificate["file_hash"], "File hash mismatch"
poly_tx = get_transaction(certificate["polygon_tx_hash"])
assert hash_in_calldata(poly_tx, file_hash), "Hash not found in Polygon tx"
root_verified = verify_merkle_proof(
file_hash, certificate["merkle_proof"], certificate["btc_merkle_root"]
)
assert root_verified, "Merkle proof failed"
op_return = get_op_return_data(certificate["btc_txid"])
assert certificate["btc_merkle_root"].lower() in op_return.lower(), "Root not on-chain"
return True
Four assertions. Two public APIs, neither belonging to the company that generated the certificate. If this function returns True, the timestamp claim holds up on its own, and it'll still hold up in five years even if the original service is gone.
Why this matters more than the anchoring itself
Most write-ups on evidence timestamping focus on the capture side: hash the file, send it somewhere, get a certificate back. That's the easy half. The half that actually gets tested in a deposition is whether the proof survives scrutiny from someone who has every incentive to poke holes in it.
A certificate that can only be validated by calling the vendor's own verification page is a certificate that depends on the vendor's continued existence and continued honesty. A certificate that can be validated with hashlib, a public RPC endpoint, and a block explorer API doesn't have that dependency. That's the actual point of putting the hash on a public chain instead of a private database: the proof stops being about who you trust and starts being about what you can independently recompute.
If you're building or evaluating evidence documentation tooling, this is the test worth running before anything else: strip away the vendor's dashboard and ask whether the proof still stands on public infrastructure alone. ProofLedger anchors to both chains for exactly this reason, but the verification logic above doesn't need to know that. It just needs the certificate and a network connection.