Vero Presence Record Specification¶
Version: 1.0.0-draft Status: DRAFT — implements the design settled in the anchoring spike (13 Aug 2026) Scope: All platforms
A Presence Record is the retained, exportable artifact asserting that a live
human being was physically present in a specific session at a specific moment.
This document specifies its format, construction, time-bracketing, anchoring,
and third-party verification precisely enough to reimplement from this text
alone. The reference implementations are packages/presence-record (builder)
and packages/presence-record-verify (independent verifier).
The claim a record supports — exactly this, and no more:
A Vero presence record is non-backdatable and tamper-evident against a public ledger that Zeroth does not control.
A record does NOT prove the presence check was performed honestly (a lying verifier can anchor its lie on time), and it attests presence, not identity, unless a TIP binding is present. Implementations and documentation MUST NOT describe records as "irrefutable."
1. Design invariants¶
- Non-biometric. No field carries — or is derived from — the entangled vector, any embedding, or any image. There is no field that could.
- Non-retroactive. A record's session is bracketed in time by two independent mechanisms (§5, §7): a public randomness beacon that did not exist before the session (T_lower), and a public timestamp that proves the record existed by a certain moment (T_upper).
- Third-party checkable. Verification requires only the record, the signers' public keys, and two public RPC endpoints. No Zeroth service, no Zeroth credentials, no Zeroth existence (§8).
- Nothing meaningful on chain. The only value that ever reaches a chain is a 32-byte Merkle root. Leaf preimages carry 256 bits of blinding entropy (§4), so no on-chain artifact can be linked to any record, person, or session without the record itself.
2. Payload¶
The payload is a JSON object with EXACTLY these members (no extensions in v1; unknown members MUST be rejected by conformant builders and treated as a different spec version by verifiers):
| Member | Type | Meaning |
|---|---|---|
spec |
string | Always "presence-record/1" |
sessionId |
string | Opaque ULID. MUST NOT be derived from any identity |
session |
object | startedAt, endedAt (ISO 8601 Z, seconds precision), durationMs (integer) |
verdict |
string | "accept" | "review" | "reject". Rejects are recorded and anchored like accepts — an archive that anchors only accepts is not an audit trail |
scores |
object | facialSimilarity, sequenceFidelity, depthGate — numbers in [0,1]. Depth is recorded, non-voting (comparison-spec) |
protocol |
object | specVersion (string), decodeTier (1|2), blinkRateHz (number), deviceClass (string) |
policy |
object | digest: "sha256:<64 hex>" of the applied policy. Never the policy itself, never an enumerable ID |
identityBinding |
object | null | null, or { "type": "tip", "anchorDigest": "sha256:<64 hex>" }. Null means the record attests presence only |
beacon |
object | null | T_lower evidence (§5), or null = upper-bound-only record |
prev |
string | null | Previous record's commitment ("sha256:<64 hex>") in the issuing archive's hash chain, or null for the first |
blinding |
string | 32 CSPRNG bytes, base64url unpadded (§4) |
Timestamps use ISO 8601 with seconds precision and a trailing Z
(2026-08-13T14:32:07Z). Hex is lowercase. Base64url is unpadded.
3. Canonicalisation¶
All hashing and signing operates on the payload's RFC 8785 (JCS) canonical form, UTF-8 encoded:
- Object members sorted by UTF-16 code units of their names
- No insignificant whitespace
- Numbers in ECMAScript shortest form (
Number::toString); negative zero serialises as0; non-finite numbers are unserialisable and MUST be rejected at construction undefinedanywhere is a construction error, never silently dropped
Two structurally different but semantically identical payloads MUST canonicalise to identical bytes. This property is conformance-tested.
4. Commitment and blinding¶
commitment = "sha256:" || hex( SHA-256( blindingBytes || canonicalPayloadBytes ) )
blindingBytes: the payload'sblindingfield, decoded — exactly 32 bytes drawn fresh from a CSPRNG per record. Reuse across records is a conformance violation.canonicalPayloadBytes: §3 canonical form of the FULL payload, including theblindingmember.
The commitment is the record's identity everywhere outside the record: the
EAS attestation digest, the Merkle leaf, the prev chain link. Because the
preimage carries 256 bits of unguessable entropy, a low-entropy payload
(e.g. verdict strings) cannot be brute-forced from any published digest —
and once a record (with its blinding) is erased, surviving digests are
information-theoretically unlinkable to anything.
5. Beacon — T_lower¶
The lower time bound binds public unpredictability into the session itself:
the session key is derived with the beacon value in the HKDF info field
(crypto-spec §4.2 amendment, versioned separately), so no valid blink
sequence for the session could exist before the beacon value was published.
- Source: Ethereum L1
prevrandao— themixHashfield of a recent finalized block.chainIdMUST be 1. - L2 block hashes are prohibited as beacons. An L2 sequencer is a single party that could predict or grind its own block hashes. The beacon comes from L1 even when the anchor (§7) lands on an L2.
- An all-zero
mixHashMUST be rejected (pre-Merge block or mock endpoint). publishedAtis the beacon block's timestamp and IS T_lower.- If no beacon can be fetched at session start, the session proceeds and the
record carries
beacon: null— an upper-bound-only record. Builders MUST NOT substitute a cached value from an earlier session. Honesty about the missing bound is the conformance requirement.
Grinding margin (informative): an L1 proposer can bias prevrandao by at most one bit for one slot; the beacon's role is coarse (12-second granularity) session bracketing, far above that influence.
6. Merkle batching¶
Records batch by commitment; only the root is anchored.
leaf = SHA-256( 0x00 || commitmentBytes ) // commitmentBytes = 32-byte digest
node = SHA-256( 0x01 || min(a,b) || max(a,b) ) // byte-wise big-endian ordering
- Domain separation is normative. The
0x00/0x01tags put leaves and internal nodes in disjoint hash domains, closing the second-preimage (node-as-leaf) attack. - Sorted-pair hashing makes proofs direction-free: a proof is the bare sibling list, leaf level upward.
- Odd node promotion. An unpaired node at any level is promoted to the next level unhashed — NEVER duplicated. Duplication admits batches containing the same leaf twice.
- Batch window: 10 minutes or 1,024 records, whichever first (both deployment-configurable). An empty window anchors nothing.
- A single-record batch is legal:
root = leaf, proof[].
7. Anchors — T_upper¶
The envelope carries an open anchors[] array. Any single valid anchor
establishes T_upper; verifiers MUST ignore anchor types they do not
recognise; the record format never names a chain outside an anchor entry.
A just-issued record MAY have an empty anchors[] — the record is valid on
its signatures alone, and the anchor is added when its batch lands (its
absence widens T_upper to "unbounded above", it does not invalidate).
v1 defines three anchor types:
eas-offchain — the record's EAS off-chain attestation (EIP-712 v2,
domain name "EAS Attestation", primary type Attest, fields
uint16 version, bytes32 schema, address recipient, uint64 time,
uint64 expirationTime, bool revocable, bytes32 refUID, bytes data,
bytes32 salt), where data is the 32-byte commitment digest. Carries
uid, schemaUid, attester. The UID derivation follows the EAS SDK v2
scheme (keccak-256 over the packed fields with the schema UID string
UTF-8-encoded and a trailing uint32 0 bump).
eas-onchain-timestamp — the batch root, timestamped via the EAS core
contract's timestamp(bytes32) / multiTimestamp(bytes32[]) entry points
(verified present in EAS.sol). Carries chainId, merkleRoot, proof
(sibling list, §6), txHash, blockNumber, blockTime. The EAS contract
address is NEVER embedded in records or code — it is resolved from the
official EAS deployments registry at build/deploy time.
rfc3161 — a classical TSA token over the commitment, for deployments
whose auditors require it. Carries token (DER, base64), optional
policyOid.
8. Envelope and signatures¶
{
"payload": "<base64url of canonical payload>",
"signatures": [ { "role", "kid", "alg": "ES256", "sig" } ],
"commitment": "sha256:<64 hex>",
"anchors": [ /* §7 */ ]
}
- Signatures are ES256 (ECDSA P-256 + SHA-256 — the protocol's existing curve family) over the canonical payload bytes, so a re-serialised payload still verifies.
- Roles:
verifier(REQUIRED for validity),presenter(additive),zeroth(additive). The Zeroth countersignature is a convenience and a reputational mark, not a validity condition. - Key discovery:
kidis a resolvable identifier (e.g.did:web). The Zeroth countersigning key history is additionally published as an on-chain EAS attestation so the key set outlives the website (deployment requirement, not a format requirement).
9. Verification procedure (normative)¶
Given a record, signer public keys, an L1 RPC, an anchor-chain RPC, and the EAS address from the official registry:
- Decode
payload; recompute §3 canonical bytes and §4 commitment; compare withcommitment. Mismatch → TAMPERED. - Verify each signature over the canonical bytes. Any failing signature → TAMPERED. (A role with no available key is reported, not failed.)
- For an
eas-onchain-timestampanchor: recompute the leaf from the commitment and walkproofper §6 tomerkleRoot. Mismatch → TAMPERED. - Read the root's on-chain timestamp (EAS
getTimestamp(bytes32), selector0xd45c4435). Zero → the root was never timestamped → TAMPERED. RPC unreachable → UNPROVEN (not tampered). Nonzero value → T_upper. - If
beaconis non-null: first check ordering —beacon.publishedAtMUST precedesession.startedAt(a beacon from at-or-after session start cannot lower-bound the session; that IS the backdating forgery this bound exists to stop) — ordering violation → TAMPERED. Then fetch the named L1 block; comparemixHashand timestamp. Mismatch → TAMPERED. Match → T_lower. RPC unreachable → beacon unverified (record remains, at best, upper-bound-only for this run). - If an
eas-offchainanchor is present: readgetRevokeOffchain(attester, uid)(selector0xb469318d). Nonzero → the attester has publicly disputed this attestation → DISPUTED (the record is intact and anchored, but not cleanly verified; the revocation timestamp is the dispute's own T_upper). RPC unreachable → report unverified revocation status without failing. - Verdict: all checks pass with both bounds → VERIFIED with bracket [T_lower, T_upper]. All pass, beacon null or unverifiable → VERIFIED (upper bound only). Attester-revoked → DISPUTED. No checkable anchor → UNPROVEN.
Disputes: a disputed record stays immutable; EAS revokeOffchain by the
attester places a public, timestamped dispute signal against the
attestation UID — a correction is a NEW record carrying the disputed
record's commitment in prev, never an edit.
10. Privacy and erasure (normative consequences)¶
- The on-chain surface is exactly one 32-byte root per batch. Verdicts, scores, session IDs, identities, policy IDs, and timestamps at session granularity MUST NOT appear on chain in any form, hashed or otherwise.
- Erasure: deleting a record deletes its blinding; every published digest derived from that record becomes unlinkable (§4). This is the designed answer to erasure-rights requests and MUST NOT be weakened by logging commitments alongside identifying data.
11. Conformance¶
Golden fixtures live in protocol/conformance/presence-record/. A
conformant implementation MUST:
- Reproduce the golden record's canonical bytes, commitment, leaf, root, and proofs exactly.
- Fail each invalid fixture for its stated reason:
bad-proof,root-not-on-chain,beacon-postdates-session,mutated-payload,revoked-attestation,beacon-value-mismatch. - Refuse to construct payloads with non-finite numbers,
undefinedmembers, wrong-length blinding, or a beacon whosepublishedAtis aftersession.startedAt.