Independent verification
Check a passport against the Bitcoin blockchain
Every published passport version is folded into a Merkle tree and the root is timestamped into the Bitcoin blockchain through the public OpenTimestamps calendars. This page recomputes that chain in your browser and reads the block from a public explorer we do not run. Nothing below depends on trusting DPPLive — that is the point.
- •1. Fetch the passport as the public sees it
- •2. Recompute the anchored copy’s SHA-256 in your browser
- •3. Match the manifest, and compare it to the live passport
- •4. Hash the manifest → commitment
- •5. Replay the Merkle path → batch root
- •6. Follow the OpenTimestamps proof → Bitcoin block
- •7. Read that block from an independent explorer
Show the raw evidence
- Anchored manifest (the exact bytes that were hashed)
- Commitment
- Batch Merkle root
- Merkle path
- Anchored public copy (the bytes
public_sha256covers) - Bitcoin block
- Block merkle root reported by the explorer
Verify it without this page
A verification tool written by the party being verified is worth very little. So the proof is emitted in the standard OpenTimestamps format. Download both files and check them with the reference client, which has nothing to do with us:
pip install opentimestamps-client curl -o manifest.json "https://api.dpplive.com/anchor/<UPID>/manifest.json" curl -o manifest.json.ots "https://api.dpplive.com/anchor/<UPID>/proof.ots" ots verify manifest.json.ots
A successful run prints Success! Bitcoin block <height> attests existence as of <date>.
Then check that public_sha256 inside manifest.json equals the SHA-256 of the
passport’s public JSON — that is the one step tying the timestamp to this specific product.
The specification
Frozen under the name dpplive-anchor-v1. Any change to it gets a new name, and proofs already issued keep verifying under the old one.
What is committed
Not the raw document hash. The full published version contains fields the public view strips (chemical detail, supplier chain, importer EORI), so an outsider could never recompute that hash — a proof nobody can check is worth nothing. Instead each version commits to a small manifest binding both views:
manifest = {
"alg": "dpplive-anchor-v1",
"full_sha256": sha256(canonicalJson(full published version)),
"public_sha256": sha256(canonicalJson(public projection)),
"published_at": "YYYY-MM-DD HH:MM:SS", // UTC, verbatim from the database
"upid": "...",
"version": n
}
commitment = sha256(canonicalJson(manifest))
A buyer hashes the public JSON they can actually see, matches public_sha256, and
walks the rest. full_sha256 rides along so the unredacted document can still be
proven later in a dispute — against a commitment made before the dispute existed.
The manifest itself leaks nothing: two hashes, an identifier, a version, a date.
The public projection is frozen at anchor time and served back verbatim at
/anchor/<UPID>/manifest.json’s sibling field public_json. It has to be:
the list of fields DPPLive publishes changes as the regulation and our own review of it change, and
re-deriving the hash from today’s list would make every older proof read as “does not match” — a false
accusation against a passport nobody touched. So this page hashes the frozen copy, then separately
compares it against the live passport and tells you which of the two kinds of difference it found:
a changed value (the record moved — a real problem) or only a changed set of visible fields
(we changed what we publish).
Past versions are not enumerable. Asking for ?v=N requires also presenting that version’s
commitment, which anyone holding the proof already has. Without that, this endpoint would
hand out every passport’s complete edit timeline to anyone who counted upwards.
canonicalJson
canonicalJson(v) = JSON.stringify(sort(v)) sort(array) = array.map(sort) // order preserved sort(object) = keys sorted ascending (UTF-16 code units), values sorted recursively sort(other) = unchanged
Merkle tree
leaf(c) = SHA256( 0x00 || c ) c = 32-byte commitment node(l, r) = SHA256( 0x01 || l || r ) odd node = promoted unchanged to the next level (never duplicated) leaf order = commitments sorted ascending as lowercase hex root of one leaf = that leaf
The 0x00/0x01 prefixes stop an internal node being passed off as a
leaf (the standard second-preimage attack on naive Merkle trees). Promoting the odd node
instead of duplicating it avoids Bitcoin’s own CVE-2012-2459 ambiguity, where two different
trees can produce one root.
A path step records where the sibling sits:
{"d":"l","h":…} → next = node(h, cur);
{"d":"r","h":…} → next = node(cur, h).
Anchoring
The root is submitted to four independent OpenTimestamps calendars every six hours. They aggregate it with everyone else’s digests into a single Bitcoin transaction and pay for it themselves — there is no wallet, no gas, and no private key anywhere in this system. Once the transaction confirms (typically 1–6 hours) we upgrade the pending proof into a permanent one and stop depending on the calendars at all.
What this proves — and what it does not
It does prove
- This exact passport content existed no later than the timestamp of that Bitcoin block.
- It has not been altered since — any edit changes the hash and the old proof stops matching.
- DPPLive could not have back-dated it. We cannot move a Bitcoin block either.
- Each republished version is separately committed, so the version history is provable, not just the latest state.
It does not prove
- That the data is true. If a supplier enters a false fibre composition, the blockchain faithfully preserves the false statement. Timestamping is an integrity control, not an audit.
- That any certificate referenced in the passport is genuine — check those with the issuing body.
- That the physical product in your hand is the one described. That is the job of the data carrier and the identifiers, not of the timestamp.
No personal data and no commercial data is written to the blockchain — only hashes. That is deliberate: content published to a public chain cannot be erased, which would collide with the GDPR right to erasure. Everything erasable stays in our database, where it can actually be erased.
Neither the ESPR (Regulation (EU) 2024/1781) nor Implementing Regulation (EU) 2026/1778 requires a blockchain. This is a voluntary integrity control on top of the hash and trusted timestamp those texts do ask for — not a compliance claim.