ProofPoll Protocol
A MIR profile for independently verifiable polling evidence
Version 0.1.0 -- Working Draft -- 2026-10-21
0. What this is#
ProofPoll is an open protocol that lets a polling organization produce evidence of what it committed to measure, what it collected, what it excluded, what it calculated, and what it published -- in a form that anyone can verify offline, without trusting the pollster, the registry, or proofpoll.net.
It is a profile of the MIR Protocol (mirprotocol.net, v0.2.0). Every ProofPoll event is an ordinary MIR claim: Ed25519-signed by the pollster's domain, canonicalized per MIR 04, keys discovered per MIR 05. A conformant MIR verifier can verify every individual ProofPoll claim without knowing this profile exists. ProofPoll adds three things MIR deliberately leaves out:
- Ordering. Each poll is a hash-chained log, so a missing or reordered entry is detectable.
- Independent time. Third-party witnesses countersign log heads, so "registered before fieldwork" rests on someone other than the pollster.
- Completeness within a pollster. Registrations are sequenced per pollster, so a registered poll cannot quietly disappear.
MIR 10 lists claim chaining and global ordering as non-goals because they would make a registry necessary. ProofPoll keeps that property: the chain lives in claim metadata (which MIR 10 explicitly permits for application-layer linking), and the ordering authority is a set of interchangeable witnesses, not a registry.
0.1 The three claims a verifier can make -- and the one it cannot#
| Verdict | Meaning | Established by |
|---|---|---|
| INTEGRITY | The disclosed records match their signed commitments, in order, with no gaps, and were witnessed when the log says. | Signatures, chain, witness checkpoints |
| DISCLOSURE | The pollster supplied each document the profile requires, and each matches its committed hash. | Document hashes in the bundle |
| TIMING | Registration was witnessed before the first fieldwork batch; amendments are classified pre- or post-fieldwork. | Witness times only, never pollster times |
| Not established by this protocol, ever. Whether the sample represents a population is a statistical question. Hashing cannot answer it. | Out of scope |
A ProofPoll verifier MUST NOT output a single score, grade, or trust rating. It outputs the report in section 7. This follows MIR's non-scoring principle and is not negotiable in later versions.
0.2 What ProofPoll cannot prove#
- That unregistered polls do not exist. A pollster can run a study entirely off the protocol. No cryptography detects work that was never committed. Completeness of registered work is provable; completeness of all work is a policy problem, addressed in section 9.
- That responses came from real, eligible, distinct people. The protocol commits to what the pollster recorded. Respondent authenticity is the pollster's sampling and fraud-control problem.
- That the pollster's claims are true. As in MIR: a signature proves authorship, not accuracy.
1. Terminology#
RFC 2119 keywords apply.
- Pollster -- the organization conducting the poll. Signs as a MIR domain.
- Poll -- one study with one fixed questionnaire and population definition.
- Poll log -- the ordered, hash-chained sequence of ProofPoll claims for one poll.
- Pollster sequence -- the contiguous numbering of every poll a pollster has ever registered.
- Witness -- an independent MIR domain that countersigns log heads. Witnesses attest when they saw a head. They attest nothing about content.
- Checkpoint -- a witness's signed countersignature over a log head.
- Bundle -- the self-contained archive a verifier checks offline.
- Claim digest -- SHA-256 over the canonical serialization of the full claim object,
sigincluded, lowercase hex. Concretely: apply MIR 04 canonical serialization rules 3 and 4 (recursive lexicographic key sort, no whitespace, number handling, UTF-8) to the whole claim, skipping rule 2 (the removal ofsig).sigsorts as an ordinary key, betweenmirandsubject. Because Ed25519 is deterministic (MIR 04), every implementation computes the same digest, and the chain binds to the signed bytes, not just the payload. - Document -- any artifact committed by hash: questionnaire, sampling plan, weighting specification, analysis code, published report.
2. Identifiers#
2.1 Poll subject#
The MIR subject of every claim in a poll log is the poll, not a person:
subject = hex(SHA256("{domain}:poll:{pollId}"))
pollId is an opaque identifier chosen by the pollster (UUID RECOMMENDED). This makes "every event for this poll" a standard MIR subject lookup, and "every poll by this pollster" a standard MIR domain lookup -- with no new registry index.
2.2 Respondents are never subjects#
Respondents MUST NOT appear as MIR subjects, in any claim, under any derivation. Political and opinion data is among the most sensitive categories that exist. Response data enters the protocol only as salted Merkle leaves (section 4), and leaves are disclosed only at the pollster's chosen disclosure level.
2.3 Reserved subject inputs#
ProofPoll subjects are structural: they name a poll or a sequence, never a person. To keep them from colliding with user subjects on a domain that also issues ordinary MIR claims, a ProofPoll domain MUST NOT use an externalUserId that begins with poll: or pollster:.
3. Claim types#
All types use the MIR extension namespace proofpoll.net:{category}.{action}. All are signed by the pollster's domain except witness.checkpoint.
All profile fields -- the four below and every per-type field in 3.1 to 3.10 -- MUST live inside the MIR metadata object, never as top-level claim fields. MIR 03 defines a closed top-level set (mir, type, domain, subject, timestamp, metadata, keyFingerprint, sig), and MIR 10 rules out top-level linking fields. Keeping everything in metadata is what lets a plain MIR verifier check a ProofPoll claim without knowing this profile exists. An implementation that lifts seq or prev to the top level is not MIR-conformant.
The semantics of seq and prev belong to the ProofPoll verifier alone. A core MIR verifier signs over them and interprets neither, and a MIR registry stores them verbatim without tracking any sequence (MIR 07). Later versions of this profile MUST preserve that split.
Every poll-log claim MUST carry these metadata fields:
| Field | Type | Meaning |
|---|---|---|
pp |
integer | Profile version. 1. |
poll |
string | The pollId. |
seq |
integer | Position in the poll log. 0 for registration, contiguous thereafter. |
prev |
string | Claim digest of the claim at seq - 1. Empty string at seq = 0. |
Large content never goes in metadata (MIR caps it at 4 KB). Documents are committed by SHA256 hex digest and travel in the bundle.
Metadata values MUST be strings, safe integers (|n| < 2^53), or objects of those. No floats. The claim digest is computed by every implementation, in every language, and floats are where serializers disagree (MIR 04 Number Handling). A share or a rate belongs in a committed document or a decimal string. A verifier rejects a float with INVALID_SCHEMA.
3.1 proofpoll.net:poll.registered (seq 0)#
Commits the poll's design before any fieldwork.
| Field | Required | Meaning |
|---|---|---|
pseq |
yes | Pollster sequence number. Contiguous across all polls this domain has ever registered, starting at 0. |
pprev |
yes | Claim digest of the registration at pseq - 1. Empty at pseq = 0. |
questionnaire |
yes | Digest of exact question wording, response options, and order/randomization rules. |
population |
yes | Digest of the target population definition. |
samplingPlan |
yes | Digest of recruitment method, mode, quotas, planned n. |
weightingSpec |
yes | Digest of the weighting specification, or of a document stating "unweighted." |
disclosurePolicy |
yes | Digest of what the pollster commits to publish, and when, whatever the result. |
sponsor |
yes | Digest of the sponsor and funding disclosure. |
product |
yes | "open" or "sample" -- see 3.9. |
fieldOpen, fieldClose |
yes | Planned field window, ISO 8601. |
3.2 proofpoll.net:poll.amended#
Replaces one committed document. Carries field (which of the 3.1 digests), digest (new), and reason (digest of the explanation). Amendments are always allowed. The verifier classifies each as pre-fieldwork or post-fieldwork by witness time (section 6). A post-fieldwork change to questionnaire or weightingSpec is reported prominently, never hidden and never forbidden.
3.3 proofpoll.net:fieldwork.batch#
Commits a batch of collected responses, including excluded ones.
| Field | Meaning |
|---|---|
root |
Merkle root of the batch's response leaves (section 4). |
received |
Count of all responses in the batch. |
accepted |
Count retained for analysis. |
excluded |
Object mapping exclusion reason codes to counts. received = accepted + sum(excluded) MUST hold. |
from, to |
Collection window of the batch. |
Committing exclusions is the point. "We dropped 412 responses for failed attention checks" becomes a signed, witnessed number fixed at collection time, instead of a footnote written after the result is known.
Pollsters SHOULD commit a batch at least every 24 hours of active fieldwork.
3.4 proofpoll.net:fieldwork.closed#
batches (count), received, accepted, excluded totals. MUST equal the sums over all batch claims.
3.5 proofpoll.net:analysis.computed#
| Field | Meaning |
|---|---|
inputs |
Merkle root over all accepted leaves across batches, in batch order. |
rawTally |
Digest of unweighted results: the canonical JSON (MIR 04 rules 3-4) of { questionId: { answer: count } } over accepted leaves, answers keyed by their string form. Fixing the format is what lets an L2 verifier recompute it. |
weightingSpec |
Digest of the spec actually applied. MUST equal the registered (or last amended) value, or the verifier reports a mismatch. |
code |
Digest of the analysis code or container image, if disclosed. |
output |
Digest of weighted results and uncertainty estimates. |
3.6 proofpoll.net:poll.published#
report (digest of the published document) and analysis (claim digest of the analysis.computed it reports).
3.7 proofpoll.net:poll.cancelled#
reason (one of funding, design_error, field_failure, sponsor_withdrawal, other) and explanation (digest). A cancelled poll is a closed, accounted-for entry in the pollster sequence. Cancelling is legitimate and visible. Disappearing is not possible.
3.8 proofpoll.net:poll.corrected#
Supersedes a published result. Carries new output, new report, and reason digest. The original stays in the log.
3.9 Two products, one log format#
product in 3.1 fixes, before fieldwork, which claim the poll may make:
"open"-- open participation. Results MUST be framed "of those who participated."weightingSpecSHOULD be "unweighted.""sample"-- recruited sample. Weighted estimates with uncertainty are permitted.
The verifier reports the declared product. It cannot check that a publication's framing honors it; a human reader can.
3.10 proofpoll.net:witness.checkpoint (signed by a witness)#
| Field | Meaning |
|---|---|
pp |
1 |
pollster |
The pollster domain. |
log |
"poll" or "pollster". |
poll |
The pollId (for log = "poll"). |
seq |
The seq (or pseq) of the head being witnessed. |
head |
Claim digest of that head. |
The checkpoint's MIR subject is the poll subject (2.1) for poll logs, or hex(SHA256("{pollster}:pollster:sequence")) for the pollster sequence. Its MIR timestamp is the witness's own clock. This is the only time the profile trusts.
4. Response leaves#
Each response record, accepted or excluded, becomes one leaf:
{
"rid": "opaque per-response id, never a respondent identity",
"salt": "base64url, 16 random bytes, unique per leaf",
"status": "accepted | excluded:{reasonCode}",
"answers": { "q1": "B", "q2": 4 }
}
leaf = SHA256(0x00 || canonical(record))
node = SHA256(0x01 || left || right)
Domain-separated hashing follows RFC 6962 (Certificate Transparency) to block second-preimage attacks. An odd node at any level is promoted unchanged. Leaves are taken in commitment order, which is the line order of the batch's leaf file. The root of an empty batch is SHA256(""). The analysis inputs root is built the same way over every accepted leaf, batch by batch in seq order.
The salt is mandatory. Answer spaces are tiny: "candidate A or B" can be brute-forced from an unsalted hash in microseconds. Without the salt, the Merkle root would leak every answer.
4.1 Disclosure levels#
The pollster chooses, per poll, and declares the choice in disclosurePolicy:
| Level | Bundle contains | Verifier can check |
|---|---|---|
| L0 Commitments | Claims and checkpoints only | Ordering, timing, counts, gaps |
| L1 Documents | L0 + all committed documents | Plus: design matched commitment, weighting spec unchanged |
| L2 Leaves | L1 + all leaf records (salted) | Plus: recompute every batch root, recompute raw tally |
| L3 Reproducible | L2 + analysis code | Plus: rerun the pipeline and match output |
Selective proof is available at any level: a pollster can hand an auditor the inclusion proof for specific leaves without disclosing the rest.
5. Witnessing#
- A witness MUST countersign a head only after verifying the head claim's signature and its
prevlink to the last head it witnessed for that log. - A witness MUST NOT countersign two different heads at the same
seqfor the same log. Doing so is equivocation, and the two checkpoints are its proof. - A witness SHOULD publish all checkpoints it issues, so a pollster who shows different heads to different witnesses is caught when anyone compares them.
- Pollsters MUST obtain checkpoints for every poll-log claim, amendments included. An amendment's timing is exactly what a reader needs to trust.
- Pollsters MUST obtain a checkpoint on the pollster sequence for every new registration.
- Witness threshold. A claim counts as witnessed at time T when at least k distinct witness domains have a checkpoint at exactly its
seqwhoseheadequals its claim digest, and T is the k-th earliest of those witnesses' first checkpoint timestamps. The profile default is k = 2. Verifiers MAY require more. - A witness caught equivocating counts toward no threshold anywhere in the bundle. A checkpoint signed by the pollster's own domain is ignored.
Witnesses are ordinary MIR domains. Any organization can run one. Natural candidates are poll archives, university survey centers, standards bodies, and newsrooms. Proofpoll.net MAY run one; it MUST NOT be required.
6. Timing rules#
All timing judgments use witnessed time (section 5), never the pollster's claim timestamp.
- Pre-registered: witnessed time of
seq 0< earliestfromof any batch AND < witnessed time of the first batch. - Pre-fieldwork amendment: sits before the first batch in
seqorder AND was witnessed before the first batch's witnessed time. An amendment after the first batch inseqorder is post-fieldwork whatever its timestamps say, because the pollster's own chain places it there. - Batch staleness: a batch whose witnessed time trails its
toby more than 48 hours is reported as late-committed. Late commitment is exactly the window in which data could have been shaped after the fact. - The pollster's own timestamps are reported alongside, so discrepancies are visible, as with MIR's
timestampvsingestedAt. - Where witnesses submit checkpoints to a MIR registry, the registry's
ingestedAt(MIR 07) is a third clock, independent of both pollster and witness. A verifier MAY use it to harden timing against a pollster and a witness colluding on timestamps.
7. Bundle and verification#
7.1 Bundle layout#
bundle/
manifest.json # { pp, pollster, poll, level }
claims/000000.json ... # poll log, ordered by seq
pollster/ # every registration from pseq 0 to the pollster's witnessed head
checkpoints/*.json # witness checkpoints
keys/{domain}.json # snapshot of each domain's .well-known/mir.json at bundle time
docs/{sha256} # committed documents (L1+)
leaves/{batchSeq}.jsonl# leaf records (L2+)
code/ # analysis code (L3)
Key snapshots make offline verification possible. They are trust-on-capture: an offline verifier MUST say it used snapshots, and an online verifier SHOULD re-fetch keys per MIR 05 and report any difference.
The pollster assembles the bundle. A bundle that drops the last few claims and their checkpoints looks like an unfinished poll, not a truncated one. Offline verification can detect truncation only against checkpoints the bundle includes. An online verifier SHOULD ask each witness for its latest head for the poll and the pollster sequence, and report LOG_TRUNCATED if any witness has seen further than the bundle goes. An offline verifier MUST say it did not.
7.2 Algorithm (summary)#
- Verify every claim and checkpoint as a MIR claim (MIR 06), and reject any top-level field outside MIR 03's set (
NON_MIR_FIELD). - Check the poll log:
seqcontiguous from 0, eachprevequals the prior claim digest, all claims from the pollster domain, and lifecycle order: registration at seq 0; batches beforeclosed;analysis.computedafterclosed;publishedafter an analysis; nothing aftercancelled;correctedonly afterpublished. - Check the pollster sequence from pseq 0, and that it contains this poll's registration.
- Check witnesses: equivocation, checkpoints that disagree with the bundle, checkpoints beyond the bundle's end, and the k-threshold for every poll-log claim and for this poll's pollster-sequence entry.
- Check count arithmetic across batches,
closed, and exclusions. - Check that every
analysis.computedapplied the weighting spec in force at that point in the log, and thatpublishedcites an earlier analysis. - At L1+: hash each document and match its commitment.
- At L2+: check salts, recompute every batch root, recount every batch, recompute the
inputsroot and the raw tally. - At L3: rerun the code and match
output. - Apply the timing rules (section 6) and emit the report.
| Code | Step | Meaning |
|---|---|---|
| MIR 06 codes | 1 | INVALID_SCHEMA, INVALID_SIGNATURE, KEY_NOT_FOUND, ... passed through unchanged |
NON_MIR_FIELD |
1 | A claim has a top-level field outside MIR 03 (section 3, C1) |
DOMAIN_MISMATCH |
2, 3 | A poll-log or pollster-sequence claim is not signed by the pollster |
CHAIN_GAP |
2 | A seq is missing |
CHAIN_BROKEN |
2 | A prev does not match, or two claims share a seq |
ORDER_VIOLATION |
2 | Lifecycle out of order |
PSEQ_GAP |
3 | A pseq is missing, or this poll's registration is not in the sequence |
PSEQ_BROKEN |
3 | A pprev does not match, or two registrations share a pseq |
EQUIVOCATION |
4 | One witness signed two heads for the same log position |
HEAD_MISMATCH |
4 | A witness countersigned a different claim than the bundle holds at that position |
LOG_TRUNCATED |
4 | A witness saw a position beyond the bundle's end |
UNWITNESSED |
4 | Fewer than k honest witnesses for a claim |
COUNT_MISMATCH |
5, 8 | Counts do not add up, or leaves do not match committed counts |
WEIGHTING_MISMATCH |
6 | Analysis applied a weighting spec never committed |
REF_MISMATCH |
6 | published cites no earlier analysis |
DOC_MISSING |
7, 8 | A document or leaf file the declared level requires is absent (disclosure) |
DOC_MISMATCH |
7 | A document does not hash to its name (disclosure) |
UNSALTED_LEAF |
8 | A leaf lacks a 16-byte salt |
ROOT_MISMATCH |
8 | Leaves do not hash to a committed root |
TALLY_MISMATCH |
8 | Recomputed raw tally differs from rawTally |
OUTPUT_MISMATCH |
9 | Rerun output differs from output |
DOC_MISSING and DOC_MISMATCH are reported under DISCLOSURE. Every other code is reported under INTEGRITY.
7.3 Report#
The verifier emits structured findings, never a score:
INTEGRITY verified | failed: [codes]
DISCLOSURE level L2 declared, L2 present; missing: []
TIMING pre-registered: yes (witnessed 2027-03-02T14:00Z by k=3)
post-fieldwork amendments: weightingSpec (1)
late-committed batches: 0 of 9
COUNTS received 2,417 / accepted 2,005 / excluded {attention: 301, duplicate: 111}
PRODUCT sample
POLLSTER pseq 41 of 57 registered
NOT ESTABLISHED
representativeness, respondent authenticity, unregistered studies
The NOT ESTABLISHED block is mandatory in every report.
7.4 Online latest-head check#
Offline, a poll still in progress and a poll whose tail was dropped look the same (section 7.1). Online, a verifier closes the gap:
- For the poll subject and the pollster-sequence subject, gather every
witness.checkpointclaim it can find: from each witness's own domain, and from any MIR registries (MIR 07 requires lookup by subject). - Verify each one as in step 4.
- Report
LOG_TRUNCATEDif any honest witness checkpointed aseq(orpseq) beyond the bundle's end.
The verifier chooses where to look. MIR registries are federated and non-exclusive, so a pollster cannot suppress the check by picking a friendly registry.
This is a liveness property. It catches only claims that were witnessed, which is why section 5 requires a checkpoint on every claim. An unwitnessed claim was never part of the provable record -- the same boundary as section 0.2.
8. Relationship to MIR history#
Because every ProofPoll claim is a MIR claim under the pollster's domain, a MIR registry that ingests them accumulates the pollster's participation history at no extra cost: polls registered, published, cancelled, amended after fieldwork, corrected. That is the record. MIR surfaces it. It does not score pollsters, and neither does ProofPoll. A newsroom or aggregator reading that history makes its own decision about whom to cite.
9. The completeness problem, and the policy that addresses it#
Section 0.2 is the protocol's real limit: unregistered work is invisible. The protocol makes registration cheap and checkable. What makes it expected is a consumer-side rule, by analogy with clinical-trial registration (ICMJE journals, 2005: no publication without registration before enrollment):
Consumer policy (non-normative): "We cite or aggregate a poll only if its ProofPoll registration was witnessed before its first fieldwork batch."
Proofpoll.net's role is to make that rule easy for publishers and aggregators to adopt. The protocol does not enforce it and cannot.
This boundary is permanent. No later version of the profile, and no verifier report, may upgrade "the pollster's registered work is complete" into "the pollster's work is complete."
10. Open questions#
- Witness bootstrapping. Who runs the first k = 2 independent witnesses? An archive and a university survey center would be ideal. Proofpoll.net alone does not count as independent.
- Respondent-level dedup across batches without identity -- is a pollster-side keyed hash of the panel ID, committed as a set root, worth specifying?
- Reason-code vocabulary for exclusions. Free-form strings will fragment; a starter list is needed.
- Panel reuse. Long-running panels re-interview the same people across polls. Nothing here links them, by design. Confirm that this is what pollsters want.
MIR alignment.Resolved 2026-10-21. The mir.org maintainer (mir-claude), working from the text of MIR 03, 04, 07 and 10, ruled the metadata hash chain a sanctioned use, on two conditions: all profile fields stay inmetadata(section 3), and chain semantics stay in the ProofPoll verifier. The ruling also fixed the claim-digest wording (section 1). The test it applied: a chain is a back door only if it forces an ordering authority or registry into MIR itself. Here the ordering comes from plural, interchangeable witness claims inside the profile.Test vectors.Done 2026-10-21: 15 vectors intest-vectors/, normative in the same sense as mirprotocol.net's.- Neighbour status in the pollster sequence. A report could show whether pseq 40 and 42 were published or cancelled, but that needs those polls' terminal claims in the bundle. Is that worth the extra bundle weight?
- L3 execution. The reference verifier hash-checks analysis code but does not run it. Running it needs a reproducible container spec. That is the hard part of L3, and it is not yet specified.
- Witness query API. Section 7.4 defines the check. The shape of a witness's own endpoint for serving its checkpoints is not yet specified. MIR registry lookup by subject already works without one.
- Shared canonicalizer. The claim digest uses the same algorithm as the MIR reference SDK's
canonicalize(), but as a separate copy, because the SDK exposes only the sig-stripping form. A shared primitive in the MIR SDK would remove the copy. That is a change to the reference SDK, and Richard's decision.