UPC — Universal Private ComplianceStandard (Draft)

The Envelope

The attestation envelope is what the wire carries today and what the standard should add as first-class fields.

The Envelope

Open draft: 4 fields in preview, 5 fields drafted, all up for revision

The 4 fields below are what our open-source preview IAttestationVerifier carries on-chain. The 5 proposed additions are a working draft. Both sets are open including the 4 already in the preview. We'd rather get the schema right with input from integrators and adjacent-standard maintainers than ship the schema we wrote first.

The envelope is the data structure that travels over the wire. It's the set of fields a verifier consumes when it answers "is this attestation valid?". Today's envelope is intentionally minimal because UPC was designed for a single use case: ZK membership in an ASP tree. A standard that's going to serve more than one privacy protocol needs to grow into something more general: typed subjects, lifecycle metadata, revocation, selective disclosure.

This page maps what's live today against the fields the standard should grow into.

TODAY (live wire)identityPoseidon(address)proofPLONK bytesrootMerkle rootnullifieranti-replay (optional)PROPOSED ADDITIONSsubjectType"address" | "note" | "tx" | …issueraddress or DIDissuedAt / expiresAtvalidity windownullifierModesingle-use | reusablerevocationPointerURI or registry id

The live wire carries four fields. Five proposed additions cover subject typing, issuer provenance, validity window, nullifier semantics, and revocation. All additions are forward-compatible. Verifiers that don't understand them ignore them.

Today's envelope

The on-chain IAttestationVerifier.verify(uint256 identity, bytes proof) carries four conceptual fields. They aren't typed at the ABI level: identity is a single uint and proof is opaque bytes, but the membership circuit's public inputs always include:

FieldTypeRole
identityuint256 (Poseidon commitment)Who/what the attestation is about. Hidden behind the Poseidon hash.
proofPLONK proof bytesThe zero-knowledge witness that identity is in the tree.
rootuint256 (Merkle root)The published ASP tree root being attested against.
nullifieruint256 (optional)Anti-replay token for state-changing operations.

This is the membership-only envelope. It works for the question "is this subject in this approved set?" and refuses to overreach beyond it.

What the envelope is missing

The questions about subject diversity, lifecycle, revocation, and disclosure mode all point to fields the envelope doesn't carry today. Each of these wants to be a typed field at the envelope level, not implicit in the verifier's behavior.

Subject type

Today the subject is always Poseidon(address). The envelope assumes "this is about an address". For the wire to carry attestations about wallets, notes, transactions, pools, credentials, or policy decisions, the envelope needs a subjectType tag so verifiers can dispatch correctly.

Proposed values: address, wallet, note, transaction, pool, credential, policyDecision. Open set: adapters declare which they accept. The full subject-type taxonomy is part of this draft and will firm up as integrators push back on the initial list.

Issuer

Today the trust model is implicit: whoever registered the ASP on ASPRegistryHub is the issuer, and their reputation is what backs the attestation. There's no field on the envelope that names the issuer.

A first-class issuer field, an Ethereum address or a W3C DID, makes provenance explicit. Verifiers can then authorize per-issuer ("I accept attestations from issuers in my whitelist") rather than per-ASP-id.

Validity window

Today validity is coarse: the ASP root has a TTL (default 24 hours) and a 64-root history buffer; a proof against an expired root is rejected. There's no way to express "this attestation is valid from time T1 to T2" at the per-attestation level.

issuedAt and expiresAt (unix timestamps) on the envelope let an issuer commit to a specific validity window. This is useful for credentials with natural expiry (KYC re-verification cycles, accreditation renewals).

Revocation pointer

Today revocation is implicit: the operator removes the member from the tree, publishes a new root, the old root falls out of the history buffer after 64 updates. Detecting "this attestation was revoked" requires watching root publications.

A revocationPointer field: a URI or a registry ID for a revocation list. It lets verifiers check active revocation status without reconstructing tree history.

Nullifier mode

Today nullifier semantics are implicit: state-changing operations (deposit, withdraw, transfer) require a nullifier; read-only checks (eligibility queries) don't. This is convention, not contract.

A nullifierMode field with values single-use or reusable makes the contract explicit at the envelope level.

The proposed envelope, in code

Putting the pieces together, the envelope grows from 4 fields to 9:

struct Attestation {
    bytes32 subjectType;       // proposed: typed dispatch tag
    uint256 identity;          // current: Poseidon commitment
    address issuer;            // proposed: explicit provenance
    uint64  issuedAt;          // proposed: validity window start
    uint64  expiresAt;         // proposed: validity window end
    uint256 root;              // current: Merkle root
    uint256 nullifier;         // current: optional anti-replay
    uint8   nullifierMode;     // proposed: single-use vs reusable
    bytes   revocationPointer; // proposed: URI or registry id
    bytes   proof;             // current: ZK proof bytes
}

This is a proposal, not the deployed schema. We'd want input on field naming, optional vs required, and version negotiation before shipping.

Forward compatibility

The proposed extensions are additive: all current fields remain; new fields default to zero/empty for backward compatibility; verifiers that don't understand the new fields ignore them. We'd ship the v1 envelope as a typed Solidity struct alongside the current (identity, proof) ABI, with a wrapper that emits both.

Disclosure modes

The envelope is silent on selective disclosure today. A UPC proof is binary (in-set / not-in-set). For "prove age ≥ 18 without revealing identity" or "prove jurisdiction = X" the envelope needs to compose with selective-disclosure credential formats.

We see three composition shapes worth supporting:

  • Embedded gadget: the SD primitive (e.g. BBS+ signature verification) is a Circom gadget compiled into the membership circuit. The envelope's proof carries both proofs together.
  • Two-stage verification: the SD attestation is verified off-chain or via a separate verifier; its result becomes a UPC tree leaf; UPC proves membership in the leaf set.
  • Side-channel: UPC carries the membership proof; the SD attestation travels alongside in the same transaction and is verified by a separate ABI.

The envelope itself doesn't pick anything. The subjectType tag plus the verifier adapter does. See Coexistence for how this plays out against EAS, VC, SD-JWT, and BBS+.

What this means for integrators

If you're building against UPC today, you're building against the 4-field envelope. The proposed extensions are forward-compatible: all current fields remain; new fields are additive; verifiers that don't understand them ignore them. Building now is safe; the v1 envelope arrives as a typed wrapper around the same bytes.

On this page