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.

Leave both extra fields empty to check the current passport. To re-check an older version you are holding a proof for, enter its version number and the commitment printed in that proof’s manifest — past versions are not enumerable, so that a passport’s full edit timeline stays private.

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.

How far this proof reaches

What the timestamp settles

  • 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.

What you verify elsewhere

  • Whether the figures are accurate — read the passport against the supplier’s test reports and audits. A timestamp faithfully preserves whatever was entered, true or not; it is an integrity control, not an audit.
  • Whether each certificate is genuine — confirm the number with the issuing body. The passport shows the issuer and number for exactly this reason.
  • Whether the item in your hand is this one — that is what the data carrier and the identifiers (GTIN, batch, facility) are for. The timestamp covers the record, not the garment.

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.

For garment factories

Make your first Digital Product Passport this week

Start with a free four-week pilot on one style. After that DPPLive is free for up to 10 passports a month; above 10 a month we agree a custom price with you.

Prefer email? [email protected] · We reply within 24 hours · Contact page