Making Proofs about Ticket PODs
Zupass tickets are PODs, Provable Object Datatypes. This means that they’re ZK-friendly, and it’s easy to make proofs about them.
ZK proofs in Zupass are used to reveal information about some POD-format data that the user has. This might involve directly revealing specific POD entries, such as the e-mail address associated with an event ticket. Or, it might involve revealing that an entry has a value in a given range, or with a value that matches a list of valid values. Or, it might even involve revealing that a set of related entry values match a sets of values from a list.
Zupass allows you to configure proofs by providing a set of criteria describing one or more PODs that the user must have. If the user has matching PODs, they can make a proof and share it with someone else - such as with your application or another user of your app. As the app developer, it’s your job to design the proof configuration.
Because this system is very flexible, it can also be intimidating at first. Mistakes can be subtle, and might result in a proof that doesn’t really prove what you want it to. To help with this, there’s a specialized library for preparing ticket proof requests, ticket-spec
. This guide will explain how to use it.
Quick Start
Below you will find more detailed background on ticket proofs. But if you just want to get started, here’s how to do it:
Following the Getting Started guide, install the App Connector module and verify that you can connect to Zupass.
Next, install the ticket-spec
package:
Then, you can create a ticket proof request, and use it to create a proof:
This will create a GPC proof which proves that the ticket has a given signer public key and event ID, and also reveals the attendeeEmail
address for the ticket.
If you know the signer public key and event ID of a ticket then you can specify those in the classificationTuples
array, but for getting started you can delete those values if you don’t know which ones to specify.
The result should be an object containing proof
, boundConfig
, and revealedClaims
fields. For now, we can ignore proof
and boundConfig
, and focus on revealedClaims
, which, given the above example, should look like this:
As we saw in the original proof request, the attendeeEmail
entry is revealed: revealedClaims.pods.ticket.entries.attendeeEmail
contains the value.
To understand how this works, read on!
Configuring a ticket proof request
Ticket proofs enable the user to prove that they hold an event ticket matching certain criteria. They also allow the user to selectively reveal certain pieces of data contained in the ticket, such as their email address or name.
A typical use-case for ticket proofs is to prove that the user has a ticket to a specific event, and possibly that the ticket is of a certain “type”, such as a speaker ticket or day pass. To determine this, we need to specify two or three entries:
- Public key of the ticket signer or issuer
- The unique ID of the event
- Optionally, the unique ID of the product type, if the event has multiple types of tickets
For example, if you want the user to prove that they have a ticket to a specific event, then you want them to prove the following:
- That their ticket POD was signed by the event organizer’s ticket issuance key
- That their ticket has the correct event identifier
- That their ticket is of the appropriate type, if you want to offer a different experience to holders of different ticket types
How to specify ticket details
To match a ticket based on the above criteria, you must specify either pairs of public key and event ID, or triples of public key, event ID, and product ID. For example:
The first example, containing only a public key and event ID, will match any ticket which has those attributes. The second is more precise, requiring that the ticket have a specific product type.
It’s possible to specify multiple pairs or triples. For example:
These would be used like this:
In this case, the proof request will match any ticket which matches either of the above pairs. If you provide more, then the ticket just needs to match any of the provided pairs.
This underlines an important principle: when the proof is created, you might not know which pair of values the user’s ticket matches. This is by design, and is part of how ZK proofs provide privacy. If you only need to know that the user has a ticket matching a list of values, but don’t need to know exactly which ticket the user has, then by default that information will not
What gets revealed in a ticket proof
If you have specified pairs or triples of public key, event ID and (optionally) product ID, then the list of valid values will be revealed in the proof. This might be the only information you want to reveal: the proof discloses that the user has a ticket which satisfies these criteria, but no more.
However, you might want the proof to disclose more information about the ticket. There are two further types of information that a proof might reveal: a “nullifier hash”, a unique value derived from the user’s identity, and a subset of the ticket’s entries.
Nullifier hash
A nullifier hash is a unique value which is derived from the user’s identity, but which cannot be used to determine the user’s identity. Typically this is used to pseudonymously identify a user: if the same user creates two proofs, both proofs will have the same nullifier hash, giving the user a consistent identity, but not one that can be used to determine their public key or other information.
The nullifier hash requires an “external nullifier”, a value which your application must provide. This ensures that the nullifier hash is derived from both the user’s identity and a value that your application provides. This means that the nullifier hash that the user has when creating proofs for your application will be different to the nullifier hash they have when creating proofs for another application.
Revealed entries
Proofs can also directly reveal the values held in certain entries, meaning that the revealedClaims
object in the proof result will be populated with values from the ticket that the use selects when making the proof. In the Quick Start example above, the attendeeEmail
entry is revealed, but you can reveal any of the ticket’s entries by setting them in the fieldsToReveal
parameter:
Watermark
You can add a watermark to your proof, which allows you to uniquely identify a proof. Precisely which value to use for the watermark depends on your application and use-case, but you might use a unique session ID, or a single-use number generated by your application.
If you add a watermark to your proof request, you can check the watermark when later verifying the proof. A typical workflow might involve your client application requesting a random number from your server, which stores the number. The number is passed as a watermark in the proof request, and then you can send the proof to the server for verification. The server then checks that the watermark is equal to the random number it generated. By requiring the watermark to equal some single-use secret value, you ensure that the client cannot re-use a previously-generated proof.
Verifying a ticket proof
Once a proof has been made, you can verify it.
Typically, verification is done by a different person, computer, or program than the one that produced the proof. You don’t really need to verify a proof that you just created on the same computer, but if you received a proof from someone else, or if you have an application which sends proofs between, say, a client and a server then you will want to verify any proofs that you receive.
Verification covers a few important principles:
- That, given a
revealedClaims
object which makes certain statements, a proof configuration, and a proof object, the proof object really does correspond to the configuration and revealed claims. If we didn’t check this, then therevealedClaims
might contain data for which there is no proof! - That the configuration really is the one that you expect it to be. This is important because a proof might really match a set of claims and a configuration, but if the configuration is not the one you expect then the claims might not be valid for your use-case.
Proof requests
To make this easy to get right, we have a concept of “proof requests”. When you call ticketProofRequest
as described above, you’re creating a proof request object which can be used to… request a proof. However, you can also use the proof request when verifying a proof, to ensure that the proof was produced in response to the correct request.
If you’re verifying the proof in the browser using the Z API, you can do so like this:
This performs both of the checks described above. Of course, since you’re using the same proof request object in both cases, you already know that the proof matches the request!
However, you can use a similar technique when verifying the same proof in another environment, such as on a server:
This ensures that our verified proof not only matches the claims that were sent, but that claims are those we expect them to be.
Devcon Ticket Proofs
Above we outlined a generic approach to making proofs about tickets. If you’re looking to make a proof about ownership of a Devcon ticket specifically, you can use this configuration: