Submission and verification

Once you have registered your application's verifying key(s) you are ready to submit application proofs to NEBRA UPA. In this section you will learn how to submit and verify proofs using our SDK.

Steps to submit and verify proofs

Proofs are submitted to UPA on-chain by calling the submit function in the NEBRA UPA contracts.

function submit(
    uint256[] calldata circuitIds,
    Proof[] calldata proofs,
    uint256[][] calldata publicInputs
) external payable returns (bytes32 submissionId);

Each submission can contain one or more proofs. For convenience and type-safety, we recommend that you use our SDK to submit proofs instead of calling this function directly.

Step 1: Export proof data

SnarkJS

Let proofData be the output of snarkjs' fullProve function, i.e.

const proofData = await snarkjs.groth16.fullProve(
    inputs,
    circuitWasm,
    circuitZkey
  );

You may save the json serialization of proofData into a file snarkjs_proof.json if you intend to submit the proof with the upa tool. You can generate a UPA-compatible proof data file with the following command

$ upa convert-proof-snarkjs \
    --snarkjs-proof snarkjs_proof.json \
    --proof-file proof.upa.json

Alternatively, if you want to submit via the typescript sdk, you can easily extract the UPA-compatible proof and inputs from proofData:

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

const proof = Proof.from_snarkjs(proofData.proof);
const inputs: bigint[] = proofData.publicSignals.map(BigInt);

Gnark

You can modify your gnark circuit code to export the proof and the inputs as follows:

import ("encoding/json")

proof, _ := groth16.prove(ccs, pk, witness, opt.proverOpts...)
proofJSON, _ := json.MarshalIndent(vk, "", "    ")
_ = os.WriteFile("gnark_proof.json", proofJSON, 0644)

pubWitness, _ := witness.Public()
publicWitnessJSON, _ := json.Marshal(pubWitness)
_ = os.WriteFile("gnark_inputs.json", publicWitnessJSON, 0644)

If you intend to submit the proof with the upa tool, you may generate a UPA-compatible proof data file with the following command

$ upa convert-proof-gnark \
    --gnark-proof gnark_proof.json \
    --gnark-inputs gnark_inputs.json \
    --proof-file proof.upa.json

Alternatively, if you want to submit via the typescript sdk, you can convert the gnark proofs and inputs to the UPA-compatible format as follows

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

const proof = Proof.from_gnark(gnarkProof);
const inputs: bigint[] = gnarkInputs.map(BigInt);

where gnarkProof and gnarkInputs can be obtained from the gnark_proof.json and gnark_inputs.json files, respectively. For example

import { GnarkProof, GnarkInputs } from "@nebrazkp/upa/sdk/gnark";

const gnarkProof = JSON.parse(
    fs.readFileSync("path/to/gnark_proof.json", "ascii")
  ) as GnarkProof;
const gnarkInputs = JSON.parse(
    fs.readFileSync("path/to/gnark_inputs.json", "ascii")
  ).map(BigInt) as GnarkInputs;

Note on gnark proofs

For gnark proofs with a LegoSNARK commitment point, the UPA only supports those which have been generated with keccak256 as the hash to field function. In other words, you must run the prover with the following options:

import("golang.org/x/crypto/sha3")

groth16.prove(..., backend.WithProverHashToFieldFunction(sha3.NewLegacyKeccak256()))

Note gnark's default is the hash function RFC9380, which is not currently supported by NEBRA's UPA.

Step 2: Prepare proof data

Each proof is submitted along with its corresponding Circuit Id and public inputs as a CircuitIdProofsAndInputs.

type CircuitIdProofAndInputs = {
    circuitId: bigint;
    proof: Proof;
    inputs: BigNumberish[];
};

Prepare an array CircuitIdProofsAndInputs[] of the proofs you will submit.

Step 3: Submit proofs

Using your UpaClient (see setup), submit your array CircuitIdProofsAndInputs[].

const submissionHandle = await upaClient.submitProofs(circuitIdProofAndInputs);

Be sure to keep the returned submissionHandle as it contains information used by your application contract to check whether the proof has been verified by NEBRA UPA. It contains a Submission object that stores the proof Ids for each submitted proof and a submission Id for the entire submission. See Single and multi-proof submissions for more details.

Fee estimation (optional)

NEBRA UPA charges a nominal fee for each proof submission. Your UpaClient can estimate this fee.

const value = await upaClient.estimateFee(submissionSize);

This fee amount value can then be passed as a PayableOverrides option into upaClient.submitProofs. If no value is specified then the fee is computed automatically.

const submissionHandle = await upaClient.submitProofs(
  circuitIdProofAndInputs,
  { value }
  );

Proof submission via the upa tool

If you have a json file with UPA-compatible proof data such as proof.upa.json generated in the previous step, you can also submit proofs with the following command:

$ upa submit-proof -c <circuitId> -p proof.upa.json -i proof-id.json

The argument -i above is optional and produces an output file with the proof id. Similarly, for multi-proof submissions:

$ upa submit-proofs -p proofs.upa.json -i proof-ids.json -s submission-data.json

where proofs.upa.json consists an array of objects with three UPA-compatible components: verifying key, proof and a public inputs array. The arguments -i and -s are optional and produce output files with the proof ids and the submission data, respectively.

Step 4: Wait for proofs to be verified on NEBRA UPA

Wait for NEBRA UPA to verify your submission by awaiting waitForSubmissionVerified from your UpaClient.

const submitProofTxReceipt = await upaClient.waitForSubmissionVerified(
  submissionHandle
);

Once your submission has been verified, you can send a request to your application contract with inputs corresponding to your submission. This request uses the same inputs as before, but you will no longer need to pass in a proof when using NEBRA UPA. Your application contract will use NEBRA UPA to check the verification status of these inputs before executing the request.

Step 5: Application contract checks verification status

Your app smart contract will call isVerified from the NEBRA UPA contracts to check whether a proof has been verified or not.

// For single-proof submissions
function isVerified(
    uint256 circuitId,
    uint256[] calldata publicInputs
) external view returns (bool);

// For multi-proof submissions
function isVerified(
    uint256 circuitId,
    uint256[] calldata publicInputs,
    ProofReference calldata proofReference
) external view returns (bool);

For single-proof submissions, your smart contract calls isVerified as follows.

// `upaVerifier` is an instance of the `IUpaVerifier` contract
bool isProofVerified = upaVerifier.isVerified(circuitId, publicInputs);

For multi-proof submissions, your application contract will also need to provide a ProofReference to identify a specific proof in the submission (see Proof references).

bool isProofVerified = upaVerifier.isVerified(
  circuitId,
  publicInputs,
  proofReference
);

If you used our typescript SDK for a multi-proof submission, your SubmissionHandle can compute this proof reference which can then be passed to your application contract as part of your request.

// Gets the proof reference of the j-th proof in this submission.
const proofReference = submissionHandle.submission.computeProofReference(j);

What is a Proof Id?

UPA assigns a proofId\mathsf{proofId} to each proof it receives. This proofId\mathsf{proofId} is calculated as the Keccak hash of the proof's circuit id and public inputs:

proofId=keccak(circuitId,PI)\mathsf{proofId} = \mathsf{keccak}(\mathsf{circuitId}, \mathsf{PI})

Single and multi-proof submissions

The majority of the cost of single-proof submissions comes from storing metadata about each proof such as its proofId\mathsf{proofId}. This storage cost may be significantly reduced by taking advantage of multi-proof submissions.

  • Multi-proof submissions store their corresponding proofId\mathsf{proofId}s in a Merkle tree.

    • The submissionId\mathsf{submissionId} of a multi-proof submission is the Merkle root of the proofId\mathsf{proofId}s.

    • A single-proof submission's submissionId\mathsf{submissionId} is the proofId\mathsf{proofId} of its single proof.

  • A submission's proofs are either all accepted if all of them are valid, or they are all rejected if any proof is invalid.

  • An aggregated batch can contain proofs from different submissions.

Proof references

To check the verification status of the j-th proof of a multi-proof submission identified by submissionId\mathsf{submissionId}, you must provide its ProofReference in addition to its proofId\mathsf{proofId}. A ProofReference is a Merkle proof that this proofId\mathsf{proofId} is indeed the j-th leaf of a Merkle tree with root submissionId\mathsf{submissionId}.

Last updated