Registering applications

Before the UPA can aggregate proofs for an application, it must know about the application's verifying key(s).

The UPA contracts expose a registerVK method, which accepts the verifying key to be used, stores it on-chain (for use during censorship claims), and emits an event informing aggregators of the key data.

The guide here assumes that you have correctly installed the development environment, and have a upa.instance file pointing to a Saturn deployment. See the setup guide for details.

Converting to UPA-compatible format

Verification keys must be in the UPA-compatible format before registration.

SnarkJS

Exporting the verifying key

This may be done with a command of the form:

$ yarn snarkjs zkey export verificationkey path/to/circuit.zkey app_vk.json

Converting the verifying key via the upa tool

The key retrieved above can be converted to the UPA format as follows:

$ upa convert vk-snarkjs --snarkjs-vk app_vk.json --vk-file app_vk.upa.json

Converting the verifying key via the typescript sdk

The UPA sdk supports conversion from snarjks zkeys to the UPA-compatible Groth16VerifyingKey:

import { Groth16VerifyingKey } from "@nebrazkp/upa/sdk";

const vkSnarkjs = snarkjs.zkey.exportVerificationKey( ... );
const vk = Groth16VerifyingKey.from_snarkjs(vkSnarkjs);

Gnark

Exporting the verifying key

You can modify your circuit compilation code to save the verifying key to a file:

import ("encoding/json")

_, vk, _ := groth16.Setup(ccs)
vkJSON, _ := json.MarshalIndent(vk, "", "    ")
_ = os.WriteFile("gnark_vk.json", vkJSON, 0644)

where ccs is the gnark ConstraintSystem of your circuit.

Note: Currently, the NEBRA UPA only supports gnark configurations for which:

  • The circuit has zero or one Pedersen commitment points, that is, the field Commitments of your proofs is a vector of length 0 or 1.

  • Public inputs are not committed into the commitment point. That is, the PublicAndCommitmentCommittedarray in the verifying key must be empty.

Converting the verifying key via the upa tool

The key retrieved above can be converted to the UPA format as follows:

$ upa convert vk-gnark --gnark-vk gnark_vk.json --vk-file app_vk.upa.json

If the verifying key belongs to a circuit which uses a Pedersen commitment, you must add the flag

$ --has-commitment

to the command above. Your circuit uses a Pedersen commitment if the field Commitments of your proof is a vector of length 1.

Converting the verifying key via the typescript sdk

import { Groth16VerifyingKey } from "@nebrazkp/upa/sdk";

const vk = Groth16VerifyingKey.from_gnark(vkGnark, hasCommitment);

where vkGnark can be obtained by parsing the gnark_vk.json file extracted above. For example:

import type GnarkVerificationKey from "@nebrazkp/upa/sdk";

const vkGnark = JSON.parse(
    fs.readFileSync("path/to/gnark_vk.json", "ascii")
  ) as GnarkVerificationKey;

Registering via the upa tool

Once you have the file app_vk.upa.json with the UPA-compatible verifying key, you can register it with the following command:

$ upa registervk app_vk.upa.json

The circuit Id for this key is then output to stdout. Record this for use in your application. It can be recomputed via

$ upa compute circuit-id app_vk.upa.json

Registering from Typescript

(See the setup guide for instructions on creating a UpaClient)

Let vk be the variable holding a Groth16VerifyingKey, generated e.g. from snarkjs or gnark with the UPA sdk. You can easily register the verifying key with the UpaClient:

// Register the verifying key (upaClient is assumed to be
// correctly initialized)
const txResponse = await upaClient.registerVK(vk);

The circuit Id can also be computed from the VerifyingKey from the typescript sdk.

import { utils } from "@nebrazkp/upa/sdk";

await utils.computeCircuitId(vk);

What is Circuit Id?

Circuit Id is a unique identifier, assigned to each circuit when its verifying key is registered with the UPA contracts. It is computed (deterministically) as the keccak hash of the verifying key contents, with a domain tag. The UPA contract store a map from Circuit Id to Verifying Key data, for use during censorship claims. When submitting proofs to UPA, applications specify the Circuit Id for each proof being submitted.

Circuit Ids are also used to compute the unique Proof Id for each proof submitted to the UPA (where Proof Id is the Keccak digest of the Circuit Id followed by the public inputs).

Aggregators use the Circuit Id to look up the corresponding Verifying Key to be used as witness data in the aggregation proof. The aggregation proof then attests to the set of Proof Ids that appear in the aggregation. When application contracts query UPA to determine the validity of a given proof, the Proof Id is computed and used to check whether a batch including that proof has been verified.

NOTE: The Circuit Id is required when proofs are submitted to NEBRA UPA.

See upa compute circuit-id --help.

A note about G2 formats

Most developers will not have to deal with the details of this, but it can be helpful to be aware of the following potential pitfall.

Some attributes of the Groth16 Verifying Key are elements of the so-called G2 curve group. The details can be found elsewhere (e.g. in the upa-sdk reference documentation), but it is important to be aware that there are two incompatible formats for these points. Most off-chain libraries, including snarkjs, use a natural ordering of coordinates in G2, while the EVM expects them to be reversed.

The UPA SDK and contracts work as follows:

  • All sdk functions and types use the natural ordering, including Groth16VerifyingKey, Groth16Proof etc, compatible with snarkjs. These types generally expose a static constructor such as Groth16VerifyingKey.from_snarkjs()that accepts the snarkjs version, and a method solidity() which returns the data as expected by the NEBRA contract.

  • Conversion should generally be handled automatically by the UPA SDK and tools, and the SDK uses types where possible to catch any compatibility problems. For reference:

    • VerifyingKeys are passed to the UPA contracts with G2 coordinates in the natural order, since they are generally only used by off-chain tools. (On-chain verification only happens in the case of censorship claims).

    • Proofs are passed to the UPA with G2 coordinates in the EVM order. This is because this is the the form in which application generally pass proofs to their contracts. This simplifies the integration of UPA into existing applications, since no conversion is required.

    • The solidity() method on application objects adheres to the convention described above, that is, vk.solidity() is compatible with the UPA contracts, but may not be compatible with on-chain Groth16 verifiers.

  • For most applications, the Verifying Key is embedded automatically in a verification contract, and the application does not have to interact with it. The above pitfalls are therefore only expected to be relevant to applications with custom pipelines for their circuits.

Last updated