Verify Evidence

PNTMAP™ evidence is designed as an append-only, tamper-evident log. This page publishes the trust model and the verification record format. ML-DSA-65 signature verification at ingest is live on all nodes (MAN-1, MAN-2). Merkle inclusion proofs are published for every ledgered record and can be recomputed independently, in your browser or with the reference Python script below. Independent evidence anchoring is live: each block header is timestamped by the Bitcoin blockchain (OpenTimestamps) and by an RFC 3161 timestamp authority (DigiCert). The first confirmed Bitcoin attestation is ledger block 55 in Bitcoin block 962625 (2026-08-15 21:00:27 UTC), verified externally against two independent block explorers. Recent blocks are anchored on the next scheduled run.

PNTMAP is an intelligence and situational-awareness service. It is not an authorised navigation source and must not be used as the sole basis for flight planning, navigation or aviation safety decisions.

Proof viewer (runs in your browser)

Enter a report ID to fetch its proof and recompute every hash locally with the Web Crypto API. Nothing on this page asks the server whether a proof is valid.
All checks run in your browser with the Web Crypto API.

Canonical string specification (v1.0, effective 2026-08-15)

This specification is the contract between PNTMAP™ and any third-party verifier. It is published exactly as implemented. Any future change is issued as a new version with its own effective date and applies only to blocks built after that date. It is never changed silently, and superseded versions stay published so historical blocks remain verifiable.
  • Field orderid, sensor_id, window_start, cell, aircraft_count, low_nic_count, pct_low_nic, min_nic, avg_nic, avg_nacp, signature, signature_key_id, signature_verified, received_at. Exactly these fields, in exactly this order.
  • SeparatorSingle ASCII pipe | between fields. No trailing separator, no whitespace padding, no escaping: field values in this schema are constrained and never contain a pipe.
  • Timestampswindow_start and received_at are normalised to ISO-8601 UTC with millisecond precision and a literal Z, for example 2026-08-15T20:08:50.000Z. This is JavaScript Date.toISOString() and Python strftime("%Y-%m-%dT%H:%M:%S.") + "%03dZ".
  • NULL handlingNULL and undefined serialise to the empty string, so a NULL field contributes nothing between its two pipes. Booleans serialise as true / false. Numbers use their plain decimal form with no thousands separators and no forced decimal places.
  • Leaf ruleleaf = SHA-256 of the UTF-8 bytes of the canonical string, expressed as lowercase hex.
  • Node ruleparent = SHA-256(left_bytes || right_bytes), hashing the raw 32-byte digests, not their hex text. An odd node at any level is promoted unchanged to the next level; it is never duplicated.
  • Block header ruleblock_hash = SHA-256("height|prev|root|count|created_at(ISO ms Z)"), where the fields are block_height, prev_block_hash, merkle_root, record_count and created_at, pipe-joined in that order. Genesis uses 64 zero characters as prev.
  • Chain ruleBlock N carries the block_hash of block N-1 as its prev_block_hash, so recomputing one header transitively covers every earlier block.

Reference verifier (Python, stdlib only)

Do not trust the in-browser viewer either. The script below repeats every step against the same public endpoints using only hashlib, urllib and json. No dependencies, no PNTMAP code. Save it, or download verify_pntmap.py, then run python3 verify_pntmap.py <report_id>. It prints PASS or FAIL per step with the computed and expected hex side by side.

Scope of the script. It checks internal proof components only: the canonical string, the leaf hash, the Merkle inclusion path, the block header rule and the chain link. It reports the anchor state as informational and does not verify the Bitcoin attestation or the RFC 3161 token. It is therefore not an end-to-end check of origin, time and external anchoring on its own: steps 5 and 6 below, run with the OpenTimestamps client and OpenSSL, are the parts that involve third parties.

verify_pntmap.py
#!/usr/bin/env python3
"""Independent PNTMAP ledger proof verifier. Python 3 stdlib only.

Usage: python3 verify_pntmap.py <report_id> [base_url]

Checks, against the public API and with no PNTMAP code:
  1 leaf recompute      sha256(canonical_string) == leaf_hash
  2 merkle fold         leaf + path == block merkle_root
  3 block header        sha256(height|prev|root|count|created_at) == block_hash
  4 prev-hash link      previous block's block_hash == this block's prev_block_hash
"""
import hashlib, json, sys, urllib.request

def get(base, path):
    with urllib.request.urlopen(base + path, timeout=30) as r:
        return json.load(r)

def sha(b): return hashlib.sha256(b).hexdigest()
def pair(a, b): return sha(bytes.fromhex(a) + bytes.fromhex(b))

def step(n, name, got, want):
    ok = got == want
    print("%s  %d %-16s computed=%s expected=%s" % ("PASS" if ok else "FAIL", n, name, got, want))
    return ok

def main(report_id, base="https://pntmap.co.uk"):
    p = get(base, "/api/public/proof/%s" % report_id)
    b = p["block"]
    ok = step(1, "leaf recompute", sha(p["record"]["canonical_string"].encode()), p["record"]["leaf_hash"])

    acc = p["record"]["leaf_hash"]
    for s in p["merkle_path"]:
        acc = pair(s["hash"], acc) if s["position"] == "left" else pair(acc, s["hash"])
    ok &= step(2, "merkle fold", acc, b["merkle_root"])

    header = "|".join([str(b["height"]), b["prev_block_hash"], b["merkle_root"],
                       str(b["record_count"]), b["created_at"]])
    ok &= step(3, "block header", sha(header.encode()), b["block_hash"])

    if b["height"] == 0:
        print("PASS  4 prev-hash link  genesis block, no predecessor")
    else:
        prev = get(base, "/api/public/ledger/blocks?before_height=%d&limit=1" % b["height"])["blocks"][0]
        ok &= step(4, "prev-hash link", prev["block_hash"], b["prev_block_hash"])

    a = p["anchor"]
    print("INFO  5 bitcoin anchor  status=%s btc_block=%s" % (a["status"], a.get("bitcoin_block_height")))
    print("INFO    ots verify: curl -O %s/api/public/ledger/anchor/%d.ots && ots verify pntmap-block-%d.ots"
          % (base, b["height"], b["height"]))
    print("RESULT %s (report %s, block %d)" % ("PASS" if ok else "FAIL", report_id, b["height"]))
    return 0 if ok else 1

if __name__ == "__main__":
    sys.exit(main(*sys.argv[1:]))

Step 5: independent Bitcoin timestamp check

The four steps above show internal consistency of the log. Step 5 shows the block hash existed at a point in time, without relying on any PNTMAP infrastructure. Download the .ots proof for the block and verify it with the open-source OpenTimestamps client:
step 5
pip install opentimestamps-client
curl -O https://pntmap.co.uk/api/public/ledger/anchor/<height>.ots
ots verify pntmap-block-<height>.ots
This checks the block hash against a transaction in the Bitcoin blockchain and reports the attested time. A pending proof is still held by the calendar servers and upgrades to a full Bitcoin attestation once the transaction confirms; re-download it after that and verify again.

Anchoring cadence. Blocks are sealed every 10 minutes, one block is submitted to the OpenTimestamps calendars every hour (the newest block that is not yet anchored), and pending proofs are polled for their Bitcoin attestation every 30 minutes. A confirmation typically lands a few hours after submission, once the calendar aggregates and its transaction is mined.

This is why recent blocks show not yet anchored. That is by design, not a gap in the record: each block hash commits to its predecessor, so a single confirmed anchor retrospectively fixes the entire chain below it in Bitcoin. Anchoring every block would add cost without adding evidence. Every block, anchored or not, carries its own RFC 3161 timestamp within seconds of sealing (below).

RFC 3161 verification (DigiCert timestamp authority)

Each ledger block header is independently timestamped via two unrelated mechanisms: the Bitcoin blockchain (OpenTimestamps) and an RFC 3161 timestamp authority (DigiCert). The RFC 3161 token confirms in seconds, so it stands on its own while a Bitcoin attestation is still pending. Download the DER token and check it with OpenSSL:
RFC 3161
curl -o block.tsr https://pntmap.co.uk/api/public/ledger/tsa/<height>.tsr

# inspect the token
openssl ts -reply -in block.tsr -text

# verify against the block hash (hex, as published in the ledger explorer)
openssl ts -verify -digest <block_hash> -in block.tsr -CAfile digicert-tsa-chain.pem
The -CAfile bundle is the DigiCert trusted root and timestamping chain, published by DigiCert at digicert.com/kb/digicert-root-certificates.htm (DigiCert Trusted Root G4 plus the DigiCert Trusted G4 RSA4096 SHA256 TimeStamping CA). Concatenate the root and intermediate PEM files into digicert-tsa-chain.pem.

This is a standard, non-qualified timestamp authority. It attests that the block hash was presented to DigiCert at the stated time. Blocks sealed before this mechanism was introduced were stamped retrospectively and are labelled backfilled: for those, the token attests the time of stamping, not the time the block was created.

Proof endpoints (no web UI required)

Everything the viewer uses is public, cached, CORS-open and returns UTC timestamps.
  • GET /api/public/proof/{id}Canonical string, leaf hash, Merkle path, block header and anchor state for one report. The anchor object carries ots_proof (base64, as first submitted), upgraded_proof (base64, present once Bitcoin-upgraded) and ots_url. The tsa object carries status, tsa_url, timestamped_at and tsr_url.
  • GET /api/public/ledger/blocksPaginated block headers, newest first, with before_height and limit.
  • GET /api/public/ledger/anchor/{height}.otsRaw OpenTimestamps proof bytes as application/octet-stream, ready for ots verify. Returns the upgraded proof when one exists, indicated by the X-PNTMAP-Proof-Upgraded header, and 404 while a block has no proof yet.
  • GET /api/public/ledger/tsa/{height}.tsrRaw DER RFC 3161 TimeStampResp as application/timestamp-reply, ready for openssl ts -verify.

Per-node signed ingest commitments (completeness)

A Merkle proof shows that a record we published is in the log. It cannot, on its own, show that nothing was quietly left out. Each node therefore maintains its own hash chain over every submission it makes and periodically publishes a signed head containing its node ID, its running submission count, the chain head and the generation time. The head is hashed as canonical JSON (recursively sorted keys, compact separators) and signed with ML-DSA-65; the platform appends it to a public, append-only commitment log and never edits or deletes a row. Nodes also anchor their own daily chain head to Bitcoin directly, independently of us.
  • POST /api/public/node-commitmentsNode publishes {head, head_sha256, signature_mldsa65}, authenticated with its node key. Malformed heads, digest mismatches and bad signatures are rejected; rate limited per node.
  • GET /api/public/node-commitments?node_id=MAN-1Full public history of that node's signed commitments.
  • Daily reconciliationEach node's latest signed count is compared against the number of distinct telemetry batches this service received from it, deduplicated on the batch payload digest, plus that node's fixed offset for known non-ingest chain entries. The result is published live on the ledger head endpoint and the explorer. Until the job has run against a stored commitment, the node reads 'awaiting reconciliation'.
node commitments: discrepancy alert
MAN-1discrepancy
Committed count
59,978
Batches received
59,980
Difference
-2
Signature verified
yes (ML-DSA-65)
Received at
2026-09-28 23:10:03 UTC
Chain head
5282dc3dfc5f4bac6cd0398e4766114d24c5c6d9c306e96f33f804455d4b3414
MAN-2discrepancy
Committed count
59,293
Batches received
59,301
Difference
-8
Signature verified
yes (ML-DSA-65)
Received at
2026-09-28 23:10:13 UTC
Chain head
d210d96459d19beeb7b2c9a5ea0adb1e663eabde00f5a3a3eb8972bd5c47f495

Last reconciliation applied 2026-09-29 04:25:01 UTC. Reconciliation runs daily and compares each node’s own signed submission count against the number of distinct telemetry batches this service received from it, deduplicated on the batch payload digest. Known non-ingest chain entries, such as a node self-test, submissions predating batch-level accounting, accepted submissions whose accounting row was lost to a since-fixed unawaited write, and submissions this service accepted that a node’s local chain never recorded during a node-side restart are carried as a documented per-node offset.

What this gives you, stated precisely: server-side omission is detectable, not impossible. If a submitted record never reaches a block, the node’s own signed count will exceed the ledger count at the next reconciliation, and that gap is published. The node’s independent Bitcoin anchor of its daily chain head means the count it committed to cannot later be revised to match ours.

Ledger explorer

Browse blocks, hash chaining and per-block anchor state on the ledger explorer.

Capability status

Current status

ML-DSA-65 telemetry signing: live on all nodes (MAN-1, MAN-2), verified at ingest. Since 2026-08-15T16:56:00Z for MAN-2 (key b13fca48275021c1) and 2026-08-15T17:28:00Z for MAN-1 (key 4f8a732ccad8fa4a), every telemetry batch is verified at ingest before it is stored. Records built only from earlier windows stay pending. Signing status is reported per node on its capability badges. Independent evidence anchoring is live network-wide: each ledger block header is timestamped by the Bitcoin blockchain (OpenTimestamps) and by an RFC 3161 timestamp authority (DigiCert), and recent blocks are anchored on the next scheduled run. A verified ingest signature on its own proves origin and integrity at ingest, not inclusion in the published log; the Merkle proof and the block anchor cover that.

Signature verification at ingest (live)

  • DigestSHA-256 over the exact raw ingest request body bytes, computed before any parsing or re-encoding.
  • SignatureThe node signs the raw 32-byte digest with ML-DSA-65 (NIST FIPS 204). The signature, key identifier, algorithm and declared digest travel as X-PNTMAP-* request headers.
  • VerificationThe platform recomputes the digest over the received body, compares it to the declared digest, then verifies the signature against the node's registered, non-revoked public key using @noble/post-quantum v0.7.0. A failed check is rejected with HTTP 400 and logged.
  • RecordedAccepted rows retain signature_verified, the signing key identifier and the signature. Unsigned batches are still accepted and recorded as unverified, so no record can silently appear signed.
  • Effective from2026-08-15T16:56:00Z for MAN-2, 2026-08-15T17:28:00Z for MAN-1. Records built only from windows before a node’s timestamp remain pending for that node.

Trust model: what the log is

The primary construct is an append-only, tamper-evident evidence log. Each accepted telemetry record is hashed on receipt and appended; periodic Merkle roots summarise the log over a fixed interval. Tamper-evidence means a later alteration of a published record is detectable, not that alteration is physically prevented. Each sealed block header is published and independently timestamped by the Bitcoin blockchain (OpenTimestamps) and by an RFC 3161 timestamp authority (DigiCert). Those are timestamping mechanisms over the log, not the log itself, and they are not qualified or eIDAS-certified trust services.

Trust model: keys

  • GenerationPlanned: node signing keys generated on the node at commissioning, never transported. The private key does not leave the node and is not held by the platform.
  • StoragePlanned: hardware-backed key storage on the node where the platform supports it, with file-based storage under OS access control as the interim fallback. The chosen mode will be published per node.
  • RotationPlanned: scheduled rotation with overlapping validity, so records remain verifiable against the key that was current at signing time. Key IDs are carried in every record for this reason.
  • RevocationPlanned: a published revocation list keyed by key ID and effective timestamp. Records signed before the revocation timestamp remain verifiable; records after it are rejected.

Trust model: node compromise

A signature proves a record came from a node's key and has not changed since. It does not prove the node reported the truth. A compromised node can sign false telemetry. This is why multisensor corroboration sits above single-node observation on the evidence ladder, and why no single node's output can reach a corroborated class on its own. Suspected compromise is handled by revoking the node's key and marking its historical records as disputed rather than deleting them, because the log is append-only.

Trust model: anchoring and control

  • CadenceLive: a block is sealed every 10 minutes with a Merkle root over its records. Every block header is submitted to an RFC 3161 timestamp authority (DigiCert) on sealing, with automatic retry on failure. Each block's TSA state is shown on the ledger exactly as recorded. Block headers are also submitted to the OpenTimestamps calendars on the hourly anchoring run; Bitcoin confirmation follows a few hours later. Recent blocks are anchored on the next scheduled run, and hash chaining means one confirmed Bitcoin attestation fixes every block below it.
  • ValidatorsTwo unrelated third parties, neither operated by us: the Bitcoin blockchain via the public OpenTimestamps calendars, and DigiCert as an RFC 3161 timestamp authority. First confirmed Bitcoin attestation: ledger block 55 in Bitcoin block 962625 (2026-08-15 21:00:27 UTC), checked externally against two independent block explorers; RFC 3161 responses verify against DigiCert's root CA.
  • Can the operator rewrite the log?Not undetectably, within stated assumptions. Rebuilding a block covered by a confirmed Bitcoin attestation, or any block hash-chained below it, breaks a timestamp we do not control, so the alteration becomes detectable to anyone who checks. That holds only if the anchoring, hashing, key control and verifier implementation work as documented. Blocks sealed since the last anchoring run are covered by their RFC 3161 timestamp only until the next run adds a Bitcoin attestation, so the most recent blocks carry the weaker of the two assurances for that interval.
  • What anchoring does not coverAnchoring fixes what was published, not whether it was right. It does not prevent:
    • bad data being submitted before anchoring,
    • a compromised node signing false telemetry,
    • Omission. Anchoring alone does not prevent the server from silently dropping submissions. However, server-side omission of node-submitted data is now separately detectable: each node publishes a daily ML-DSA-65-signed commitment to its ingest chain, reconciled daily against batches received. See the node commitments section on this page and the ledger panel. A node itself failing to submit remains outside the scope of server-side detection.
    • event-selection rules being written or changed in our favour,
    • future methodology changes,
    • a misleading interpretation of accurate records, or
    • our control over which data is made public.
    Those risks are addressed, where they are addressed at all, by per-node completeness commitments, published methodology and the evidence ladder, not by the anchor.

Verification record format

  • Record hashSHA-256 digest of the canonicalised telemetry record.
  • Node IDThe PNTMAP™ node identifier that produced the record.
  • Timestamp sourceThe clock the timestamp came from (GNSS-disciplined or network time), stated explicitly, since GNSS disruption is the subject being measured.
  • ML-DSA parameter setThe FIPS 204 parameter set used to sign.
  • Key IDIdentifier of the signing key, for rotation and revocation.
  • SignatureThe ML-DSA signature over the record hash.
  • Merkle inclusion proofThe path proving the record hash is included in the published root.
  • RootThe Merkle root covering the record's interval.
  • Anchor referenceWhere that root was published, with the anchoring transaction identifier.
  • Methodology versionThe version of the published methodology in force when the record was produced.

Related

The detection parameters behind each record are published on the Data Methodology page.
Traffic
LOADING LIVE TRAFFIC…