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)
Canonical string specification (v1.0, effective 2026-08-15)
- 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)
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.
#!/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
pip install opentimestamps-client curl -O https://pntmap.co.uk/api/public/ledger/anchor/<height>.ots ots verify pntmap-block-<height>.ots
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)
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
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)
- 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)
- 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'.
- 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
- 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
Capability status
Current status
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
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
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.
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.