Skip to content

Circuit Artifacts

GPC proofs use a family of configurable ZK circuits pre-built using the Circom language. The GPC compiler handles the details of these circuits for you, but requires access to some binary files (artifacts) generated by Circom. This page describes what those artifacts are for and how to access them in your app.

Configurable Circuits

GPC is named for the General Purpose Circuits used to make proofs. A ZK circuit can generate (and verify) a proof about numbers, some of which are secret while others are revealed. The GPC compiler handles translating your app’s configuration and inputs into these raw numbers.

GPC circuits provide flexibility in two key ways. A GPC circuit is made up of modules which can be configured and linked together to make different proofs. Each circuit is also a member of a family of circuits, each containing a different combination of modules. This section describes the circuits which make up a family, and how to understand the circuit identifier you’ll see in your bound config.

Modular Circuits

GPC circuits are built from modules which implement reusable logic. These modules correspond to the constraints which appear in a proof configuration. Each module is independent but can be linked to other modules to meet the needs of the proof. Here are a few key examples of how modules are linked together:

  • An Object module checks the signature of a single POD using its content ID and public key.
  • An Entry module checks that an entry’s name hash and value hash are included in a Merkle tree rooted in its content ID. By linking the content ID to an Object module, we prove the entry is part of a specific POD. We can optionally reveal the value hash in an output of this module.
  • An Numeric Value module hashes a specific number and checks that the result matches the value hash of a linked Entry module. It then applies numeric constraints to the value, such as a bounds check.
  • An Entry Inequality module is linked to two Numeric Entry modules, and can prove one value is greater than the other.

Further modules can be chained together in similar ways. The GPC compiler needs enough modules of the right types to meet the needs of a configuration. Extra modules can be disabled or filled in with default values, so the modules in a circuit don’t need to be an exact match to the configuration. Some modules also have size parameters, such as the length of a list or tuple. As with the number of modules, this is a maximum size available to the proof, but doesn’t need to match exactly.

The count of all modules and sizes in a circuit can be seen in its circuit parameters, like the JSON example below. For instance, maxEntries indicates the number of Entry modules available, while merkleMaxDepth is a size parameter of the Entry module affecting its ability to handle large PODs.

{
"maxObjects": 3,
"maxEntries": 10,
"merkleMaxDepth": 8,
"maxNumericValues": 4,
"maxEntryInequalities": 2,
"maxLists": 4,
"maxListElements": 20,
"maxTuples": 5,
"tupleArity": 3,
"includeOwnerV3": true,
"includeOwnerV4": true
}

You can see the parameters of all of the circuits in the default family in this JSON file.

Circuit Family

A family of circuits is made up of many circuits with different module counts. In future there might be multiple families based on different templates, but currently all circuits are part of the family identified as ProtoPODGPC. Not all possible combinations of modules have been pre-compiled for use, so the GPC library needs to be told which circuits are available.

When you call gpcProve() or gpcVerify() you can specify a circuit family using an array of circuit descriptions. Each description contains the circuit parameters, as well as description fields including a name and a cost (size). The cost is used to pick the smallest circuit for any given proof.

Most of the time, you can use the default without passing your own circuit family. You might want to pass a custom family in order to reduce the number of artifacts you need to embed in your app, or you might instead decide to compile more circuit artifacts to generate a larger family (using the scripts in the @pcd/gpcircuits package).

Circuit identifiers

The family and parameters of a circuit are summarized in a unique identifier string. The circuit whose parameters are shown above is identified as proto-pod-gpc_3o-10e-8md-4nv-2ei-4x20l-5x3t-1ov3-1ov4. This string is intended to be unique rather than readable, but you can see the family name and abbreviated parameters in the name. This format could change in future, so we don’t recommend parsing a circuit identifier to obtain parameters.

The circuit identifier is used to look up the pre-compiled artifacts for the circuit. It’s important for the prover and verifier to agree on the same circuit, which is why the circuit identifier is always included in the bound config. It’s also important for prover and verifier to use artifacts compiled at the same time, which is covered by the rest of this page.

What are artifacts?

The GPC circuits in the default family have been pre-compiled for easy use. The pre-compilation of each circuit generates 3 binary files needed by either the prover or the verifier:

  • Proving Key (*-pkey.zkey) is data used by the prover as the mathematical definition of the circuit.
  • Witness Generator (*.wasm) is code used by the prover to calculate all the intermediate values needed to tailor the inputs to the circuit.
  • Verification Key (*-vkey.json) is data used by the verifier to check the proof against the mathematical definition of the circuit.

Proving artifacts are somewhat large (10-25MB) while the verification key is smaller (15-40KB). Both scale up roughly with the size of the circuit. You can see an example of these artifacts for the default family on NPM.

The circuit compilation involves a setup process which securely generates random numbers used to prove and verify. (This will eventually also involve a distributed trusted setup ceremony, see Disclaimers for more details.) This means that the prover and verifier need to use artifacts generated at the same time in order to remain compatible. The artifact distribution mechanisms below use NPM packages with semantic versioning to distinguish between different incompatible artifacts for the same family.

Using Artifacts

When you call gpcProve() or gpcVerify() you are required to provide an artifact path. This identifies a folder containing artifacts for the circuits in the configured circuit family. The library will combine that path with the circuit identifier to form the full path to the file(s) it needs to do its work.

If your code is running in a browser, the artifact path must be a URL where files can be downloaded using fetch(). If your code is not in a browser, the artifact path must be a filesystem path accessible to the app using the Node fs module. (This is the reason for some of the polyfill requirements mentioned in Installation, which we hope to eliminate in future.)

When you deploy your app, you need to make sure the right artifact files are available when needed. To do so you can select artifacts of a specific version from NPM. Then decide how your app will obtain those artifacts using a URL, or from the filesystem.

If your app interacts with Zupass using the Z API, it’s likely that Zupass will be generating proofs for you, but your app will still need artifacts to verify proofs. In this case it’s important that your app is using the same version of artifacts as Zupass. That version is currently fixed at 0.13.0, but a protocol for version negotiation will come in future.

Obtaining Artifacts

Pre-compiled GPC artifacts are distributed via NPM in the @pcd/proto-pod-gpc-artifacts package. To be sure of picking an artifact package compatible with your GPC library, you can look at the GPC_ARTIFACTS_NPM_VERSION constant. Currently version 0.13.0 is the stable beta version released in November 2024.

Since artifacts are large, you might not want to package them directly into your app bundle, so you should add the artifact package as a devDependency.

npm i -D @pcd/proto-pod-gpc-artifacts

We also don’t recommend including artifacts directly in your app’s git repo, since they’ll cause your repo size to grow each time a new artifact version is available. Instead see the sections below for suggestions on how to access artifacts in your app.

Customizing a Circuit Family

Your app likely won’t need all of these artifacts unless it can make a lot of different types of proofs. For instance, if your app only makes proofs about one POD at a time, you don’t need any artifacts for circuits with more than one Object module. You can reduce the size of the necessary artifacts by reducing the GPC family to a subset of circuits which fit the proofs you need to make.

To do this you can use the optional circuitFamily argument to gpcProve() or gpcVerify() to pass a smaller array of circuit descriptions, obtained by copying and filtering the default family. This could be as small as a single circuit for apps which make only one type of proof.

Artifact URLs

For your clients running in a browser, you’ll need artifacts to be available for download at a known URL. For testing and small deployments, we provide some public options to download the artifacts from GitHub or NPM. For larger or critical production deployments, we recommend deploying artifacts to your own web server.

Public Download

You can directly download GPC artifacts directly from GitHub or NPM using one of several mechanisms listed below. The gpcArtifactDownloadURL() function will help format a URL for each of these services, but an example is provided below of for each service using artifact version 0.13.0. While the examples show the artifact path, they link to a specific vkey file since not all download sources support listing folders:

Note that the zupass source listed in the code is intended only for use inside the Zupass website, and won’t work on other domains.

Deploying to Your Website

To avoid reliance on the download mechanisms above, which could go down or be rate-limited at any time, we recommend deploying GPC artifacts to your own web server where your client can download them reliably. To do so you’ll need to copy the artifacts from node_modules into some location which your web server will make available to your client.

The details of how to do this will be specific to how your server is deployed and hosted. You can see examples of how this is done in the Zupass server (deployed on Render) and the ZuKYC server (deployed on Vercel).

Artifacts from Filesystem

For your servers, command-line apps, and unit tests, you’ll need the artifacts to be available on the filesystem. If your code is running directly from the repo where it was built, you can point directly into the node_modules directory (specific path will vary based on your package layout):

const GPC_ARTIFACTS_PATH = path.join(
__dirname,
"../node_modules/@pcd/proto-pod-gpc-artifacts"
);

When deploying a server app which doesn’t have direct access to the build repo, you’ll need to copy the artifacts from node_modules into some location your server can access. The details of how to do this will be specific to how your server is deployed and hosted. You can see examples of how this is done in the Zupass server (deployed on Render) and the Zupass Discount Codes server (deployed on Vercel).

Future Improvements

The use of artifacts as described above is more complex to set up than we’d like, and may change in future versions. Stay tuned or send us feedback on how you think this might be easier.