# Introduction

Scale and compose zero-knowledge proofs on Ethereum.

NEBRA is a research & development organization working to make the zero-knowledge future a reality. We research and build technologies, infrastructure, and products to facilitate the mass adoption of zero-knowledge proofs.&#x20;

Our first step towards this goal is to scale the proof settlement capabilities of Ethereum in a trustless and censorship resistant manner. To achieve this, we are developing [Universal Proof Aggregation](https://eprint.iacr.org/2023/869) technologies, to aggregate proofs generated from different circuits, different proof systems and different parties.&#x20;

### The Problem: expensive ZKP onchain settlement

One of the biggest problems preventing the zero-knowledge future from becoming reality is the high cost of onchain settlement (verification). The table below shows the cost of verifying different kinds of zero-knowledge proofs on Ethereum today:

| Proof System | Gas Cost  | FIAT cost (30 gwei gas/ Ether 3000 USD) |
| ------------ | --------- | --------------------------------------- |
| Groth16      | 250,000   | 22.5 US Dollar                          |
| Halo2-KZG    | 400,000   | 36 US Dollar                            |
| STARK-FRI    | 1,500,000 | 105 US Dollar                           |

This expensive proof verification cost means that only a few kinds of applications can be built today (namely those that can justify the high verification cost), and only a subset of users (those that can afford it) have access to proof verification. We believe that a future where onboarding to vote privately on a DAO costs $20 will exclude a majority of the world, a majority who we believe blockchain technology should also serve.

NEBRA proposes using zero knowledge proofs themselves to scale zero knowledge proof verification. As a result, zero knowledge proof settlement on Ethereum can be more accessible to the general public. The core idea is to use highly efficient recursive SNARKs ([IVCs](https://iacr.org/archive/tcc2008/49480001/49480001.pdf)/[PCDs](https://dspace.mit.edu/handle/1721.1/61151)) to get a near unlimited amount of recursion (almost) for free. This means we can recursively prove multiple zero-knowledge proofs **off-chain**, and verify only *a single aggregated proof* **onchain**. This significantly improves on the status quo, and provides nearly unbounded efficiency.


# What is NEBRA UPA?

NEBRA UPA is the first **Universal Proof Aggregation** protocol that scales and composes zero-knowledge proof verification on Ethereum/EVM Chains. NEBRA UPA achieves that using recursive SNARKs, which means NEBRA UPA is secured by 1 million Ethereum validators, without adding new trust assumptions on hardware i.e. TEE or incentive assumptions i.e. restaking.&#x20;

<figure><picture><source srcset="/files/ZvotCUp5fvUWQHTyos3S" media="(prefers-color-scheme: dark)"><img src="/files/j02XqBBd4pZV4x7nGC05" alt="" width="375"></picture><figcaption><p>NEBRA UPA</p></figcaption></figure>

There are 3 key properties of NEBRA UPA:

* **Universality**: NEBRA UPA can aggregate proofs from **any** circuit: this means in the same batch, NEBRA UPA aggregates proofs from different sources, such as proofs from zkEVMs, zkDIDs, and zkCoprocessors. Universality brings "economy of scale" to NEBRA users. Effectively, you don't need to generate a huge number of proofs to enjoy **cheap** amortized verification cost.&#x20;
* **Permissionless**: NEBRA UPA is an on-chain protocol, meaning that anyone can submit proofs to NEBRA.&#x20;
* **Censorship resistant**: NEBRA UPA is made censorship resistant by adopting a forced-inclusion design similar to Ethereum L2s. You can trigger a force inclusion to include your proofs if our off-chain worker refuses to put your proof in the aggregated proof. In addition, we would be slashed if a force inclusion happens.&#x20;

### Why should you use NEBRA?

* **Verify zero-knowledge proofs cheaply without compromises**: Using NEBRA can effectively lower the cost of proof verification by 5x and more for **any** application, while security is still inherited from Ethereum L1, through recursive zero-knowlege proofs.
* **Composing proofs from different sources**: NEBRA UPA can be used to compose proofs from different sources into a single proof. This can unlock powerful use cases, such as:
  * Interoperability between different zkRollups
  * Composing proofs from different ZK verticals such as zkML, zkVM, and zkCoprocessors &#x20;


# How it works

NEBRA UPA v1 (currently v1.2) aggregates `Groth16` proofs (more proof systems to be supported in 2.0) from different sources. After a certain number of proofs (our current batch size is `32`) from different parties have been submitted to our address (see [Deployments](/developer-guide/deployments)), our off-chain prover network will generate an aggregated proof $$\pi\_{agg}$$. This aggregated proof effectively states that all the proofs in this batch have been verified.  By only verifying aggregated proofs on-chain, the verification cost of each proof is amortized, bringing up to 10x gas savings (see [Gas costs on L1s](/developer-guide/gas-costs-on-l1s) for details). &#x20;

<figure><img src="/files/a4edFviYhY1n4Mq0XQkv" alt=""><figcaption><p>NEBRA Overview</p></figcaption></figure>

The workflow of a blockchain application powered by zero-knowledge proof using NEBRA is as follows:

1. Register your verification key with NEBRA UPA. This will give you a `circuitId` (see [Registering applications](/developer-guide/registering-applications))
2. Submit your proof to NEBRA UPA together with the public input associated with the proof&#x20;
3. Wait for the event indicating that the proof is verified. After this event, application can query `isVerified` function to execute the application logic
4. Verified (or rejected) proofs can be viewed at NEBRA proof explorer: [nebrascan.io](https://docs.nebra.one/introduction/www.nebrascan.io)

<br>


# Quickstart

{% embed url="<https://www.youtube.com/watch?v=468eT6PRxO4>" %}

To start using NEBRA UPA in your application you'll need to

* Submit proofs to the UPA from your app  [#submitting-proofs-to-upa](#submitting-proofs-to-upa "mention")
* Query the UPA contract [#querying-the-upa-contract](#querying-the-upa-contract "mention")
* Modify your deployment [#deployment](#deployment "mention")
* Set up a UPA-enabled test environment [#test-environment](#test-environment "mention")

This guide will take you through those steps, using our[ SDK. ](https://github.com/orgs/NebraZKP/packages/npm/package/upa)Here we keep the code concise and just stub out the main ideas. For a working example, check out our [Demo app](https://github.com/NebraZKP/simple-app-demo).

## Submitting Proofs to UPA

Your app will submit proof to the UPA using our SDK. Add it to your project with

```
yarn add @nebrazkp/upa
```

If you're using SnarkJS to generate Groth16 proofs, then your app may look something like this:

```typescript
// App.js without UPA

// Your app generates a proof from the user inputs
const proofInputs = generateProofInputs(userInputs);
const { proof, publicInputs } = snarkjs.groth16.fullProve(
    proofInputs,
    circuitWasm,
    circuitZkey
);

// Your app submits the proof as part of a transaction.
// It's verified on chain by your contract.
const tx = await yourApp.submitTransaction(proof, publicInputs);
```

In this example, your application's smart contract verifies proofs on-chain. Let's see how to save on gas using NEBRA's UPA:

{% code lineNumbers="true" %}

```typescript
// App.js with UPA
...
// Import the NEBRA UPA SDK
import { application, Groth16Proof, UpaClient } from "@nebrazkp/upa/sdk";
import { config } from "@nebrazkp/upa/tool";

...

// Connect also to a UPAClient (provider is an ethers.Provider)
const upaInstanceFile = "...";
const upaInstanceDescriptor = config.loadInstance(upaInstanceFile);
const upaClient = UpaClient.fromInstanceFile(upaInstanceDescriptor, provider);

...

// Your app generates a proof based on certain inputs
 const proofData = snarkjs.groth16.fullProve(
    proofInputs,
    circuitWasm,
    circuitZkey
);

// Convert it to UPA-compatible format
const proof = Groth16Proof.from_snarkjs(proofData.proof);
const publicInputs: bigint[] = proofData.publicSignals.map(BigInt);

// Submit proof to UPA for verification. It's possible to 
// submit batches of proofs here for greater gas savings.
const circuitId = "..."; // Identifies your circuit. See deployment section
const submissionHandle = await upaClient.submitProofs([
      {
        circuitId,
        proof,
        publicInputs,
      },
    ]);

// Wait for proof to be verified
await upaClient.waitForProofVerified(submissionHandle);

// Your app now submits only public inputs, not the proof
const tx = await yourApp.submitTransaction(publicInputs);
```

{% endcode %}

We've used the `UpaClient` from the SDK to

* Submit the proof to the UPA smart contract using `upaClient.submitProofs`.&#x20;
* &#x20;`await` while the UPA aggregates the proof using `upaClient.waitForProofVerified`

Note that the proof is only submitted to the UPA contract. It is no longer part of your app's transaction calldata. Instead, your app now queries the UPA contract for the verification result.

## Querying the UPA Contract

Prior to using NEBRA's UPA, your app has an on-chain contract that verifies Groth16 proofs and executes some business logic.&#x20;

If you're using SnarkJS, it may look something like this:

{% code lineNumbers="true" %}

```solidity
// App.sol without UPA

// Inheriting from Groth16Verifier means your
// contract can verify Groth16 proofs
contract YourApp is Groth16Verifier {
    // ... app state
    
    function submitTransaction(
        Proof calldata proof,
        uint256[] calldata publicInputs,
    ) public {
        // Your contract verifies the proof on chain
        bool isProofCorrect = this.verifyProof(proof, publicInputs);
        require(isProofCorrect, "Proof was not correct.");
        
        // Proceed with app's business logic 
        // ...
    }
}
```

{% endcode %}

As you know, verifying with `this.verifyProof` is expensive. Let's instead query the UPA contract. We need

* A reference to the UPA contract (See [Deployments](/developer-guide/deployments))
* Your application's `circuitId` (computed with SDK [below](#deployment))

We'll add those data to your contract's state. Then we'll query the UPA contract instead of verifying proofs directly.

<pre class="language-solidity"><code class="lang-solidity"><strong>// App.sol with UPA
</strong>
// NEBRA's UPA contract interface
import "@nebrazkp/upa/contracts/IUpaVerifier.sol";

// It is not strictly necessary to inherit the
// Groth16Verifier because now UPA verifies proofs.
// You may still use it as a backup.
contract YourApp is Groth16Verifier {
    // ... app state
    
    // NEBRA's UPA contract interface
    IUpaVerifier public upaContract;
    // Circuit identifier for your app
    uint256 public circuitId;
    
    // See deployment (next section)
    constructor(IUpaVerifier _upaContract, uint256 _circuitId) {
        upaContract = _upaContract;
        circuitId = _circuitId;
    }
    
    // Note: Proof is no longer part of calldata.
    // It was previously submitted to the UPA.
    function submitTransaction(
        uint256[] calldata publicInputs,
    ) public {
        // Query UPA contract
        bool isProofCorrect = upaContract.isProofVerified(circuitId, publicInputs);
        require(isProofCorrect, "Proof was not correct.");
        
        // Proceed with app's business logic 
        // ...
    }
}
</code></pre>

Congratulations, you are now saving gas. :tada:

## Deployment

Above we added two things to your smart contract's state:&#x20;

* `upaContract` interface to the UPA
* `circuitId` identifying your app's verification key to the UPA

You'll need to supply that information when deploying your new contract. The `circuitId` will be computed below using our SDK and the latest UPA contract deployment can be found in [Deployments](/developer-guide/deployments).

The deployment may look something like this

```typescript
// deploy.js with UPA

// SDK imports
import { config } from "@nebrazkp/upa/tool";
import { utils } from "@nebrazkp/upa/sdk";
const { upaFromInstanceFile, loadAppVK } = config;

// Load UPA contract interface
const upaInstanceFile = "...";
const upaInstance = upaFromInstanceFile(upaInstanceFile, provider);

// Load your app's VK and compute circuitId
const vk = loadAppVK(vkFile);
const circuitId = utils.computeCircuitId(vk);

// Deploy
const wallet = await loadWallet(walletKeyfile, provider);
const YourApp = new YourApp_factory(wallet);
const YourApp = await YourApp.deploy(upaInstance.verifier, circuitId);
```

Here the `upaInstanceFile` is a JSON file containing the UPA contract address, as well as its deployment's transaction id and block number

```json
{
    "verifier": "0x3B946743DEB7B6C97F05B7a31B23562448047E3E",
    "deploymentBlockNumber": 20528085,
    "deploymentTx": "0xd84efac6fc5304747cf72def5bcb7bc2248bd43a6ea4fa7e00f6097269880077",
    "chainId": "1"
}
```

The latest deployment information: [Deployments](/developer-guide/deployments)

To summarize, your app now deploys using

* `upaContract`: A reference to NEBRA's UPA deployment, loaded from file
* `circuitId`: An app identifier, computed from your app's VK

### Register App

The UPA only accepts proofs from registered verification keys. Registration is permissionless. You'll use one of the two methods demonstrated in [Registering applications](/developer-guide/registering-applications)

## Test Environment

The basic ingredients of a UPA-enabled test environment are a

* Local test network (we'll use a Hardhat node)
* UPA contract deployment
* Dev Aggregator

The Dev Aggregator simulates NEBRA's off-chain worker. It monitors the local testnet for proof submissions and produces aggregated proofs. Without this running you could still submit proofs to the UPA contract, but they would never be marked as verified.

The UPA contract is deployed with the `upa` tool

```bash
upa owner deploy \
    --keyfile ${UPA_KEYFILE} \
    --verifier "node_modules/@nebrazkp/upa/test/data/test.bin" \
    --instance ${UPA_INSTANCE} \
    --use-test-config
```

and the Dev Aggregator is then deployed with

```bash
upa dev aggregator \
    --keyfile ${UPA_KEYFILE} \
    --instance ${UPA_INSTANCE} \
    > DevAggregator.log 2>&1 &
```

For more complete instructions, see [Testing workflow](/developer-guide/testing-workflow).

Congrats, you're up and running!&#x20;

If you need to see anything we did here in more detail, go check out our [Demo App](https://github.com/NebraZKP/simple-app-demo).


# Developer Guide


# Setup

Step by step guide to set up the UPA for a zk-app.

## Requirements

* node.js >= 18
* ethers >= 6
* snarkjs >= 0.7.1

## Add the client package

To add the client package as a dependency to your application:

<pre class="language-sh"><code class="lang-sh"><strong>yarn add @nebrazkp/upa ethers
</strong></code></pre>

## UPA instance

After adding the client package, you will need a file `upa.instance` containing the details of a particular deployment of the UPA contracts.&#x20;

Get the JSON file for the instance you wish to use from the [Deployments](/developer-guide/deployments) section.

## Initialize the UPA Client

The UPA client is the main way your app will interact with the UPA. It allows you to easily submit proofs among other things. To initialize it, first load the contents of `upa.instance` as json into a variable `upaInstanceDescriptor`, and then pass this to the constructor with an `ethers.Provider`or `ethers.Signer.`

> **NOTE**: The examples below are simplified for illustration purposes

### In nodejs:

```typescript
import { UpaClient } from "@nebrazkp/upa/sdk";
import { config } from "@nebrazkp/upa/tool";
import * as ethers from "ethers";
import * as fs from "fs";

...

const upaInstanceDescriptor = config.loadInstance("upa.instance");

const provider = new ethers.JsonRpcProvider(endpoint);
const keystoreStr = fs.readFileSync(keyfile, "ascii");
const signer = await ethers.Wallet.fromEncryptedJson(keystoreStr, password);

const upaClient = new UpaClient(signer, upaInstanceDescriptor);
```

### In the browser:

```typescript
import { UpaClient } from "@nebrazkp/upa/sdk";
import * as ethers from "ethers";

...

const upaInstanceDescriptor = { }; // embed or read from server
const provider = new ethers.BrowserProvider((window as any).ethereum);
const signer = await provider.getSigner();
const upaClient = new UpaClient(signer, upaInstanceDescriptor);
```

where `signer` is the account which will execute the transactions underlying the client methods.

## Add the UPA command line tool

Another way to interact with the UPA is through the command line tool. To set it up, run&#x20;

<pre class="language-bash"><code class="lang-bash"><strong>export PATH=$PATH:$(pwd)/node_modules/.bin
</strong></code></pre>

to enable the `upa` command in your current shell. If successful, running the `upa` command will give output like:

<figure><img src="/files/fOjhXFFKX6w9Dz60dx8w" alt=""><figcaption></figcaption></figure>

> **NOTE**: the `upa` tool is intended to be self documenting.  See `upa --help` for the full list of available commands.


# 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](/developer-guide/setup) for details.

## Converting to UPA-compatible format

Verification keys must be in the UPA-compatible format before registration.&#x20;

### SnarkJS

#### Exporting the verifying key

This may be done with a command of the form:

```bash
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:

```bash
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 `zkey`s to the UPA-compatible `Groth16VerifyingKey`:

```typescript
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:

```go
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 `PublicAndCommitmentCommitted`array 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:

```sh
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

```bash
--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

```typescript
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:

```typescript
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:

```bash
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

```bash
upa compute circuit-id app_vk.upa.json
```

## Registering from Typescript

(See the [setup guide](/developer-guide/setup) for instructions on creating a `UpaClient`)

Let `vk` be the variable holding a `Groth16VerifyingKey`, generated e.g. from [snarkjs](#converting-the-verifying-key-via-the-typescript-sdk) or [gnark](#converting-the-verifying-key-via-the-typescript-sdk-1) with the UPA sdk. You can easily register the verifying key with the `UpaClient`:

```typescript
// 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.

```typescript
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 Id*s 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 Id*s 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.


# 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 contract.

```solidity
function submit(
        bytes32[] calldata circuitIds,
        Groth16CompressedProof[] 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.

```typescript
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

```bash
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`:

```typescript
import { Groth16Proof } from "@nebrazkp/upa/sdk";

const proof = Groth16Proof.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:

```go
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

```bash
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

```typescript
import { Groth16Proof } from "@nebrazkp/upa/sdk";

const proof = Groth16Proof.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

```typescript
import type GnarkProof from "@nebrazkp/upa/sdk";
import type GnarkInputs from "@nebrazkp/upa/sdk";

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 Pedersen 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:

```go
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`, defined as the following type in the `application` module of the sdk.

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

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

Alternatively, proofs may be submitted alongside a verifying key instead of a circuit Id, as an array of `AppVkProofInputs`:

```typescript
export class AppVkProofInputs<VK = Groth16VerifyingKey, PROOF = Groth16Proof> {
  constructor(
    public readonly vk: VK,
    public readonly proof: PROOF,
    public readonly inputs: bigint[]
  ) {}
...
}
```

### Step 3: Submit proofs

### Option A: Off-chain submission

Before sending an off-chain submission, you need to deposit ether to the off-chain aggregator's deposit contract. You can make a deposit using the command

```
upa off-chain deposit --deposits-contract <CONTRACT_ADDRESS> --amount-eth <AMOUNT_ETH>
```

\
Then prepare a JSON file containing an array of `AppVkProofInputs` objects. Note that for off-chain submissions, you must submit a verifying key along with the proofs, not a circuit Id.

To submit a proof(s) file named `proof.upa.json`, run the following command

```
upa off-chain submit proof.upa.json --submission-endpoint <SUBMISISION_ENDPOINT> --deposits-contract <CONTRACT_ADDRESS> 
```

If the aggregator agrees to aggregate your submission, then this command outputs a signed response from the aggregator, which you may use to refund the aggregation fee if your submission has not been aggregated by a certain expiration block. (See the command `upa off-chain refund-fee`)

Otherwise, the aggregator rejects your submission then it will respond with an error message.

### Option B: On-chain submission

Using your `UpaClient` [(see setup)](/developer-guide/setup), submit your array `CircuitIdProofsAndInputs[]`.

```typescript
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*](#what-is-a-proof-id) for each submitted proof and a *submission Id* for the entire submission. See [Single and multi-proof submissions](#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.

```typescript
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.

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

#### Proof submission via the `upa` tool&#x20;

If you have a json file with UPA-compatible proof data such as `proof.upa.json` generated in the previous step.

First, you need to create a file with the circuitId, the proof and the inputs. You can do that e.g. using `jq`:

```bash
jq '. | {circuitId: ${cid}, proof: .proof, inputs: .inputs}' proof.upa.json > cid_proof.upa.json
```

where `cid` is the actual value of the circuit Id computed at vk registration time.

To submit the proof(s), run the following command

```bash
upa submit-proofs --proofs-file cid_proof.upa.json --proof-ids-file proof-id.json --submission-file submission-data.json
```

The option `--proof-ids-file` produces an output file with the proof id(s). In the case of multi-proof submissions, the option`--submission-file` saves the submission data to a file.

If more convenient, it is also possible to submit a file with the verifying key (instead of the circuitId), the proof and the inputs. You can generate such a file with the following command:

```bash
jq --argfile vk vk.upa.json '. | {vk: $vk, proof: .proof, inputs: .inputs}' proof.upa.json > vk_proof.upa.json
```

and then submit the file as before

```bash
upa submit-proofs --proofs-file vk_proof.upa.json --proof-ids-file proof-id.json --submission-file submission-data.json
```

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

Using your submission's `submissionId`, wait for NEBRA UPA to verify your submission by awaiting `waitForSubmissionVerified`.&#x20;

```typescript
const submitProofTxReceipt = await waitForSubmissionVerified(
  upaInstance,
  submissionId
);
```

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 `isProofVerified` from the NEBRA UPA contracts to check whether a proof has been verified or not.

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

// Verify a single proof in a multi-proof 
// submission
function isProofVerified(
    bytes32 circuitId,
    uint256[] calldata publicInputs,
    ProofReference calldata proofReference
) external view returns (bool);

```

\
For single-proof submissions, your smart contract calls `isProofVerified` as follows.

<pre class="language-solidity"><code class="lang-solidity"><strong>// `upaVerifier` is an instance of the `IUpaVerifier` contract interface
</strong><strong>bool isProofVerified = upaVerifier.isProofVerified(circuitId, publicInputs);
</strong></code></pre>

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)](#multi-submissions)[.](#proof-references)

```solidity
bool isProofVerified = upaVerifier.isProofVerified(
  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.

```typescript
// 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 $$\mathsf{proofId}$$ to each proof it receives. This $$\mathsf{proofId}$$ is calculated as the Keccak hash of the proof's circuit id and public inputs:

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

### Verifying a Proof Id directly

The `IUpaVerifier` interface contract also provides the following variations of `isProofVerified`:

```solidity
function isProofVerified(bytes32 proofId) external view returns (bool);

function isProofVerified(
    bytes32 proofId,
    ProofReference calldata proofReference
) external view returns (bool);
```

In some cases, computing the proof Id internally in the application contract and calling `isProofVerified` with that proof Id instead of the circuit Id and the public inputs will translate into further gas savings. In that case, we provide the following library function to call from your application contract internally:

```solidity
// [...]
import "@nebrazkp/upa/contracts/UpaLib.sol";

contract YourApp is Groth16Verifier {
    // [...]
    function submitTransaction(
        uint256[] calldata publicInputs,
    ) public {
        bytes32 proofId = UpaLib.computeProofId(circuitId, publicInputs);
        bool isProofCorrect = upaVerifier.isVerified(proofId);
        require(isProofCorrect, "Proof was not correct.");

        // Proceed with app's business logic 
        // ...
    }
}
```

We recommend application developers to always measure the gas costs of both variants before choosing an implementation.

## Single and multi-proof submissions

The majority of the cost of single-proof submissions comes from storing metadata about each proof such as its $$\mathsf{proofId}$$. This storage cost may be [significantly reduced](/developer-guide/gas-costs-on-l1s) by taking advantage of *multi-proof submissions*.

* Multi-proof submissions store their corresponding $$\mathsf{proofId}$$s in a Merkle tree.
  * The $$\mathsf{submissionId}$$ of a multi-proof submission is the Merkle root of the $$\mathsf{proofId}$$s.
  * A single-proof submission's $$\mathsf{submissionId}$$ is the $$\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.
* A submission may span multiple batches.

#### Proof references

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

#### Atomic verification of multi-proof submissions

In the case of multi-proof submissions, you can save even more gas at verification time by calling `isSubmissionVerified` instead of repeated calls to `isProofVerified`. The `IUpaVerifier` interfaces provides the following functions

```solidity
// General case
function isSubmissionVerified(
    bytes32[] calldata circuitIds,
    uint256[][] memory publicInputsArray
) external view returns (bool);

// Special case when all circuit Ids are the same
function isSubmissionVerified(
    bytes32 circuitId,
    uint256[][] memory publicInputsArray
) external view returns (bool)
```

which attests to whether every proof in a submission has been verified or not.&#x20;

As with `isProofVerified`, we provide the following variant which only takes a submission Id

```solidity
function isSubmissionVerified(
    bytes32 submissionId
) external view returns (bool);
```

and the following `UpaLib` library functions to compute the submission Id in your application contract:

```solidity
// general case
function computeSubmissionId(
        bytes32[] calldata circuitIds,
        uint256[][] memory publicInputsArray
) internal pure returns (bytes32)

// special case, all circuit ids are the same
function computeSubmissionId(
        bytes32 circuitId,
        uint256[][] memory publicInputsArray
) internal pure returns (bytes32)

// general case, the caller knows the proof Ids
// in the submission
function computeSubmissionId(
        bytes32[] memory proofIds
) internal pure returns (bytes32)
```

<br>


# Proof explorer

View the verification status of your proofs on [NEBRA's proof explorer](https://www.nebrascan.io/).

<figure><img src="/files/Y731ADE1QrLvBNo8NdNc" alt=""><figcaption></figcaption></figure>

The proof explorer landing page displays information about the latest verified batches as well as statistics such as the total number of proofs/batches that UPA has verified so far, and how many proofs are pending.\
\
To view the status of all the proofs submitted for your circuit, enter your circuit ID into the search bar on the top right.<br>

<figure><img src="/files/S7X1O85KQ0swDpPdjLD4" alt=""><figcaption></figcaption></figure>

This will bring you to a page listing the status of each proof for this circuit, which is either:

* `VERIFIED` - The proof is valid and has been aggregated into a verified batch on-chain.
* `PENDING` - The proof has not been aggregated yet and may be either accepted or rejected.
* `REJECTED` - The proof was invalid.

<figure><img src="/files/QQzzGvszhX8CFcdyilRF" alt=""><figcaption><p>Verified proofs</p></figcaption></figure>

<figure><img src="/files/Z3WEejQlS8rooqphD3EL" alt=""><figcaption><p>Rejected proofs</p></figcaption></figure>

Click on the "Proof ID" links here or enter a proof ID in the search bar to see more information about an individual proof such as its corresponding public inputs.

<figure><img src="/files/hEyH7MtsvMJGpqd39Vbo" alt=""><figcaption></figcaption></figure>


# Gas costs on L1s

End-to-end gas costs of using UPA on L1 chains (e.g. Ethereum Mainnet)

\
Let's walk through the factors determining the end-to-end gas costs for a ZK app using NEBRA UPA. There are three on-chain steps that can consume gas:

1. Submitting proofs
   * on-chain (**\~13-100k gas per proof- depends on submission size**)
   * off-chain (**0 gas per proof**)
2. Verifying the aggregated proof (**\~18k gas per proof**)
3. Querying the verification result (**\~22k gas per proof or per submission**)

The total per-proof gas cost, in both the on-chain submission (UPA v1) and off-chain submission (UPA v2) case is shown below:

<table><thead><tr><th>Submission Size</th><th>Per-Proof Gas w/o Aggregation</th><th width="201">Per-Proof Gas (v1)</th><th>Per-Proof Gas (v2)</th></tr></thead><tbody><tr><td>1</td><td>250k</td><td>150k</td><td>40k</td></tr><tr><td>2</td><td>250k</td><td>100k</td><td>40k</td></tr><tr><td>4</td><td>250k</td><td>75k</td><td>40k</td></tr><tr><td>8</td><td>250k</td><td>62.5k</td><td>40k</td></tr><tr><td>16</td><td>250k</td><td>56k</td><td>40k</td></tr><tr><td>32</td><td>250k</td><td>53k</td><td>40k</td></tr></tbody></table>

Summing up these costs, we find that in total:

* Applications submitting their proofs on-chain can save as much as **197k gas** (\~80%) per proof.
* Applications submitting their proofs off-chain can save upwards of **210k gas** (\~85%) per proof.

You can estimate your application's gas savings using our gas calculator at [**gas.nebra.one**](https://gas.nebra.one). For simplicity we show gas estimates for an application with four public inputs, but the actual costs for each step will vary a bit depending on the number of public inputs per proof.&#x20;

### Step 1: Proof Submissions

<div data-full-width="false"><figure><img src="/files/KcQdkLspUHHvWD1YdEKc" alt=""><figcaption></figcaption></figure></div>

NEBRA UPA collects **submissions** of one or more proofs and places them in a queue to be aggregated. Note that UPA does not aggregate submission-by-submission. Instead, **aggregated batches** are chosen independently of the way the proofs were submitted.

Applications will have two options for proof submissions: on-chain and off-chain. On-chain submissions cost gas in exchange for censorship-resistance (an aggregator that skips verifying a valid proof can be slashed). Off-chain submissions have no gas cost, but offer weaker censorship resistance.

The current UPA release (v1.3) supports on-chain submission. Off-chain submission will be available soon.

#### Cost of on-chain submission

Due to per-transaction storage costs, using a whole Ethereum transaction to submit a single proof is relatively expensive. Instead, we recommend that apps take advantage of **multi-proof submissions**.

The contracts currently deployed to the Sepolia testnet are initial implementations with large scope for gas optimization, but they serve to illustrate the approximate cost model. Currently, the fixed cost per submission is about **100k gas**. The marginal cost of each additional proof in a submission is about **10k gas** (for processing the additional proof and public input data).

Therefore the per-proof gas cost for an on-chain submission of $$M$$ proofs is approximately:

$$
100000/M + 10000
$$

The table below shows the measured cost of submissions of different sizes.

| Submission Batch Size | Per-proof Submission Gas |
| --------------------- | ------------------------ |
| 4                     | 33,127                   |
| 8                     | 22,013                   |
| 16                    | 16,451                   |
| 32                    | 13,679                   |

#### Cost of off-chain submission

Off-chain submissions have no associated gas cost, but a weaker censorship resistance mechanism: In response to a submission, the aggregator sends back a confirmation that they will aggregate the proof before a certain deadline. If this aggregator misses this deadline then they are subject to slashing.

### Step 2: Verifying the aggregated proof

The current version of NEBRA UPA aggregates proofs into a Halo2-KZG proof. The cost of verifying such a proof in isolation is about **350k gas**, and is roughly independent of batch size. In addition, the UPA contract emits an event and updates its storage to mark each application proof in the batch as verified. This incurs a marginal per-proof cost of about **7k gas**.

Therefore the per-proof gas cost for verifying one batch of $$N$$ proofs and storing the result is approximately:

$$
350000/N + 7000
$$

The current configuration of NEBRA UPA sets the batch size to $$N=32$$. With this batch size the aggregated verification cost comes out to **\~18k gas** per proof [(Etherscan)](https://sepolia.etherscan.io/tx/0x5ab92e9f37faef3d5d7b7a96d613ee16ac632dc94ddcd3263c6bf4baf05b8455).

### Step 3: Query of the verification result

Once a proof has been verified by the UPA contract, the app contract may query the UPA contract to confirm that the associated public inputs are valid. This typically looks like:

```solidity
require(upaVerifier.isProofVerified(circuitId, publicInputs, "Not verified"));
```

The gas cost of this external contract call is about **22k gas** per proof (assuming four public inputs).

Applications may also query the verification status of an entire submission as follows:

```solidity
bytes32 submissionId = UpaLib.computeSubmissionId(
    circuitId,
    publicInputsArray
);

require(
    upaVerifier.isSubmissionVerified(submissionId),
    "Sequence not verified"
);
```

This costs about the same amount of gas as calling `isProofVerified`.

### Calculation of end-to-end gas savings

#### Without UPA

The total cost for an app to verify an individual Groth16 proof is about **250-270k gas**, which can be broken down into:

1. Submitting and verifying the proof (**\~250k gas**)
2. Retrieving the verification result
   * from an external contract (**\~10-20k gas**)
   * from within the app contract (**0 gas**)

**Using UPA with on-chain submission**

With UPA, assuming submissions of size $$M$$ and proof aggregations of size $$N$$, the total end-to-end per-proof gas cost **with on-chain submissions** comes out to about:&#x20;

$$
100000/M + 350000/N + 39000
$$

Concretely, with submissions of $$M=32$$ proofs and aggregations of $$N=32$$ proofs, this per-proof gas cost comes out to&#x20;

$$
100000/32 + 350000/32 + 39000 \approx 53000
$$

which represents a savings of about **197k gas per proof** (\~80%).

**Using UPA with off-chain submission**

The end-to-end gas cost **with off-chain submissions** will be approximately:&#x20;

$$
350000/N + 29000
$$

With the current UPA configuration of $$N=32$$, the per-proof cost comes out to

$$
350000/32 + 29000 \approx 40000
$$

&#x20;which represents a savings of about **210k gas per proof** (\~85%).

<br>


# Gas Costs on L2s

End-to-end gas costs of using UPA on L2 chains (e.g. Base Chain)

The gas costs on L2s can be split into two parts.

* **L2 Execution Costs**: which describes the L2 execution-related gas costs.
* **L1 Data Fee Costs**: which describes costs associated with posting L2 calldata on the associated L1.
* **Cost Comparison to Groth16 Verification w/o UPA**

We also provide `gwei` numbers for Base at the time of writing this article. A summary of the results is shown in the table below.

| Batch Size | gwei cost w/ UPA on Base | gwei Cost w/o UPA on Base |
| ---------- | ------------------------ | ------------------------- |
| 32         | 3000                     | 11,560                    |
| 64         | 1160                     | 11,560                    |

## L2 Execution Costs

The L2 execution gas of `verifyMixedAggregatedProof` is about the same as the L1 execution cost for this transaction. For a batch size of 32, this is about [775,000](https://sepolia.etherscan.io/tx/0x2b387f0b4970735bd8c6117389ba7b49b46e425c192b85be05cde016c7354756).

### L2 gas for querying if a proof/submission is verified

This cost is the same as in the[ L1 case ](/developer-guide/gas-costs-on-l1s)and comes out to **22,000.**

### Total per-proof L2 gas

For batch size 32, the total per-proof **L2 gas**, comes out to:

775,000 / 32 + 22,000 = 24,000 + 22,000 = **46,000 gas**

### `gwei` cost

At the time of writing this doc, [Base gas price](https://tokentool.bitbond.com/gas-price/base) was 0.036 gwei, so the total per-proof gwei cost is 46,000\*0.036 = **1650 gwei** for a batch size of 32.

## L1 data fee

To get the exact L1 cost in `gwei` we need to compute the `l1_data_fee`, as explained in this [document](https://docs.optimism.io/stack/transactions/fees).

`l1_data_fee = tx_compressed_size * weighted_gas_price`

The `weighted_gas_price` is independent of the particular transaction. This number is also chain-specific. It is a bit difficult to get exact number but backtracking of BaseScan it seems to be in the 10 `gwei` range.

The `tx_compressed_size` is th enumber of non-zero bytes in the calldata which is about 4300 bytes for a `verifyMixedAggregated` proof [transaction](https://sepolia.etherscan.io/tx/0x2b387f0b4970735bd8c6117389ba7b49b46e425c192b85be05cde016c7354756). So the L1 data fee comes out to about 43,000 `gwei` for the entire transaction or **1350** `gwei` per-proof.

## **Cost Comparison to Groth16 Verification w/o UPA**

* L2 execution cost is about **250,000 gas**. Converting to `gwei` gas prices on Base Chain at the time of writing this docs gives 250,000 \* 0.036 = **9000** `gwei`.
* Groth16 proof consists of 256 bytes of calldata so the `l1_data_fee` for Base Chain is 256 \* 10 = **2560** `gwei`.

So totally the cost is **11,560** `gwei` per-proof, as compared to **1650 + 1350 = 3000** `gwei` per-proof when using UPA. This is about a 4x savings of **7560** `gwei.`


# Testing workflow

During blockchain application development, a "local" blockchain (generally consisting of a single node) is used to simulate deployment and interaction with smart contracts. When developing an application that leverages NEBRA UPA, a few extra components are required in order to test the end-to-end workflow in this simulation environment:

* UPA contracts must be deployed to the target chain, AND
* An aggregator must monitor the chain and supply aggregated proofs

The `upa` tool can be used to run a "development" aggregator, which aggregates all submitted proofs, and the UPA contracts can be deployed in a specific configuration which accepts these aggreagtions.

In this way, the complete UPA pipeline is available on the local development chain, and applications can be fully tested.

> **NOTE**: In contrast to the production deployments, the development aggregator accepts and aggregates invalid proofs.  Submission of invalid proofs should be tested against a NEBRA UPA testnet deployment.

## Unit testing using the `dev-aggregator`

## Launch a development node

For example, in a separate terminal, run:

```console
yarn hardhat node
```

> **NOTE**: Unless otherwise specified, the `upa` command will attempt to connect to an RPC node at `http://localhost:8545`. If your development node listens on a different port, use the `--rpc-endpoint` option, or the `RPC_ENDPOINT` env variable (see ./setup.md) to specify the correct endpoint.

### Create a deployer account

In order to deploy the UPA contracts and submit aggregated proofs, a keyfile for a funded account is required. A suitable keyfile can be created using the `upa` tool.

> **NOTE**: This keyfile should only be used on development networks, not on live networks holding tokens of real value.

```console
upa dev ethkeygen --keyfile upa-dev.keyfile
```

The keyfile can be recorded in a .env file for convenience:

```console
echo KEYFILE=upa-dev.keyfile >> .env
echo KEYFILE_PASSWORD= >> .env
```

(Otherwise, it must be specified in subsequent commands)

> **NOTE**: For development chains, the `upa dev fund` command can be used to fund the keyfile from a hosted account.  (See `upa dev fund --help`)

### Deploy a development version of UPA

```console
upa owner deploy \
    --verifier "node_modules/@nebrazkp/upa/test/data/test.bin" \
    --use-test-config
```

This creates a `upa.instance` file in the current directory.

> **NOTE**: The development version of the UPA contracts behave exactly the same as the production versions, except that they accept fake aggregation proofs. All other checks are performed identically. This allows any issues to be caught during local testing.
>
> **NOTE:** The command above deploys a upa contract with a default test config. This can be customized by providing a `upa_config.json` file, and replacing the flag `--use-test-config` with `--config upa_config.json`

### Launch a *development aggregator*

```console
upa dev aggregator
```

> **NOTE**: By default `dev-aggregator` checks for a `upa.instance` file in the current directory. Therefore, if run from the same directory as deployment above, the instance file does not need to be specified. (See `--help` for how to specify the instance file).

### Execute tests

Application tests can then be run against the local node, using `upa.instance` to initialize a `UpaClient`.

For example, if unit tests are written to always initialize a `UpaClient` from a `upa.instance`, they can be executed, pointing at the local node:

```console
yarn hardhat test --network localhost
```

Similarly any CLI or browser client code can now be exercised against the local node.


# Frontend integration

Use the NEBRA UPA SDK in your dApp

zkApps using NEBRA UPA will need to allow users to submit and monitor proofs. To make this workflow easy we have created a `@nebrazkp/upa/sdk` package that is browser-compatible. This package is a strict subset of the `@nebrazkp/upa` package and most of the functionality for submitting and monitoring proofs (as described in the previous sections) is available in the browser.

To see an example of how to integrate the `@nebrazkp/upa/sdk` package in a NextJS or React project please refer to `ui` folder of the `simple-app-demo` [repository](https://github.com/NebraZKP/simple-app-demo) on GitHub. To see the finished product, you can visit the [demo website](https://simple-app.nebra.one/).


# Deployments

### UPA v1.2.0 (Ethereum mainnet)

```json
{
    "verifier":"0x3B946743DEB7B6C97F05B7a31B23562448047E3E",
    "deploymentBlockNumber":20528085,
    "deploymentTx":"0xd84efac6fc5304747cf72def5bcb7bc2248bd43a6ea4fa7e00f6097269880077",
    "chainId":"1"
}
```

[Etherscan Link](https://etherscan.io/address/0x3B946743DEB7B6C97F05B7a31B23562448047E3E)

### UPA v2.0.0 (Sepolia)

```json
{
    "verifier":"0x3B946743DEB7B6C97F05B7a31B23562448047E3E",
    "deploymentBlockNumber":6405136,
    "deploymentTx":"0xa8626318b76b71cd21cdfb93ef67c9571d94e01383e852a3eb6dc5dc6188808e",
    "chainId":"11155111"
}
```

[Sepolia Etherscan Link](https://sepolia.etherscan.io/address/0x3B946743DEB7B6C97F05B7a31B23562448047E3E)

### UPA v2.0.0 (World Chain Sepolia)

```json
{
    "verifier":"0xF0acdB9Dbd8d14EE159cda2e6E0d5292590246AC",
    "deploymentBlockNumber":3160300,
    "deploymentTx":"0x2eb6f704b1fa6085985997bfa19f6f1f54592eab5c934fd778c63cc149c4b6d3",
    "chainId":"4801"
}
```

[World Chain Sepolia Explorer Link](https://worldchain-sepolia.explorer.alchemy.com/address/0xF0acdB9Dbd8d14EE159cda2e6E0d5292590246AC)

### UPA v2.0.0 (World Chain Mainnet)

```json
{
    "verifier":"0x913aB040d7a0AD38Ad4eC8845847Cc5F49D2b42A",
    "deploymentBlockNumber":4062674,
    "deploymentTx":"0xc50a3419abe03ef9da21188ed9f4a72a99b9c7543438cbce6e7d3baf3b213a3f",
    "chainId":"480"
}

```

[World Chain Mainnet Explorer Link](https://worldchain-mainnet.explorer.alchemy.com/address/0x913aB040d7a0AD38Ad4eC8845847Cc5F49D2b42A)

### Base Chain v2.0.0 (Mainnet)

Supports off-chain submisison, batch size `64`.&#x20;

```json
{
    "verifier":"0x2818947A104B3E437821fD39ba0b76e7B415323f",
    "deploymentBlockNumber":22930094,
    "deploymentTx":"0xfadd3ed1f9a00ddaa47d147ee32a66511a75df6de7f4988f96fce6d441da247f",
    "chainId":"8453"
}
```

Deposit Contract Address:

```
0x4cB806AcE6603a53a0220050DC7e19bB3eBAA004
```

Off Chain Submission Endpoint

```
offchain-server-v2-0-base-mainnet.prod.nebra.one
```

### &#x20;<a href="#upa-v20-sepolia-upgrade" id="upa-v20-sepolia-upgrade"></a>


# Off-chain verification

Verification of proofs via an off-chain verification service

Proofs can be submitted to a verification service for checking.  In order to use a verification service, an endpoint must be obtained from Nebra.  (TODO: link)

Proofs can be submitted using the `upa` command line tool, or programmatically with the Typescript SDK.

## Verification using the `upa`tool

### Install the tool

The standalone `upa`tool can be installed with:

```sh
$ npm install --global @nebrazkp/upa
```

It should then be available in the shell:

```sh
$ upa version
2.0.3
```

### Specify a verification service endpoint

The `--verify-endpoint`flag, or the `VERIFY_ENDPOINT`env var can be used to specify the URL of the verification service to be queried.  It is often convenient to use a `.env`file:

```
VERIFY_ENDPOINT=<endpoint>
```

### Verifying proofs in snarkjs format

Use the `upa off-chain verify-snarkjs`command:

```
$ upa off-chain verify-snarkjs vk.json proof.json
```

(Use `--help`for the full list of arguments)

### Verify proofs in gnark format

Export the VK, proof and inputs to JSON as described [here](/developer-guide/submission-and-verification#gnark).&#x20;

Use the `upa off-chain verify-gnark`command:

```
$ upa off-chain verify-gnark vk.json proof.json inputs.json
```

### Verifying proofs in UPA format

If proofs have already been converted to the UPA format (in particular the `AppVkProofInputs` format - see the [section about proof formats](/developer-guide/submission-and-verification#step-1-export-proof-data)) they can be submitted individually or in batches using the `upa off-chain verify` command.

## Verification using the SDK

In your Typescript project, add the SDK:

```
$ yarn add @nebrazkp/upa
```

The `VerifierClient`class can then be used to submit proofs for verification:

```typescript
import VerifierClient from "@nebrazkp/sdk/offChainVerify";

const client = new VerifierClient("<ENDPOINT>");
const valid = await client.verify([vkProofInputs]);
```

Note that `vkProofInputs`must be of type \`AppVkProofInputs\`.  See [here](/developer-guide/submission-and-verification#step-1-export-proof-data) for information about converting vk and proof formats into instances of this type.


# UPA protocol specification

Version 1.2.0

## Overview

*Application developers* register Groth16 *verification keys* (VKs) for their circuits with the `UpaVerifier` contract (through the `IUpaProofReceiver` interface). Upon registration, each VK is assigned a $$\mathsf{circuitId}$$ (the keccak hash of the VK).

*Application Clients* submit proofs and public inputs (PIs) to the `UpaVerifier` contract as tuples$$(\pi, \mathsf{PI}, \mathsf{circuitId})$$, where $$\pi$$ is expected to be a (compressed) proof of knowledge that $$\mathsf{PI}$$ is an instance of the circuit with *circuit id* $$\mathsf{circuitId}$$.

A single call to the contract submits an *ordered list* $$(\pi\_i, \mathsf{PI}\_i, \mathsf{circuitId}*i)*{i=0}^{n-1}$$ (of any size $$n$$ up to some implementation-defined maximum $$N$$) of these tuples. This ordered list of tuples is referred to as a *Submission*. Submissions of more than 1 proof allow the client to amortize the cost of submitting proofs. Note that there is no requirement for the $$\mathsf{circuitId}\_i$$s to match. A single *Submission* may contain proofs for multiple application circuits.

Each tuple in a submission is assigned:

* $$\mathsf{proofId}$$ - a unique *proof id* (equal to the Keccak hash of the circuit ID and PIs)

Each submission is assigned:

* a *Submission Id* $$\mathsf{submissionId}$$, computed as the Merkle root of the list of $$\mathsf{proofId}\_i$$s, padded to the nearest power of 2 with `bytes32(0)`.
* a *submission index* $$\mathsf{submissionIndex}$$, a simple incrementing counter of submissions, used later for censorship resistance.

Note that:

* for submissions that consist of a single proof, $$\mathsf{submissionId} = \mathsf{keccak}(\mathsf{proofId\_0})$$, whereas
* for submissions of multiple proofs, each proof is referred to by $$\mathsf{submissionId}$$ along with an index (or *location*) of the proof within the submission. Where required, a Merkle proof can be used to show that a proof with $$\mathsf{proofId}\_i$$ is indeed at the given index within the submission $$\mathsf{submissionId}$$.

\
The proof and public inputs are not stored on-chain. The aggregator monitors for transactions submitting proofs to the contract and pulls this information from the transaction calldata. The contract stores information about the submission (including $$\mathsf{submissionIndex}$$, $$n$$ and some further metadata), indexed by the $$\mathsf{submissionId}$$.

There is a single *Aggregator* that puts together *batches* of proofs with *increasing* submission index values. The proofs in a batch must be ordered exactly as they appear within submissions. Aggregated batches do not need to align with submissions- a batch may contain multiple submissions, and a submission may span multiple batches. If a submission contains any invalid proofs, the entire submission is considered *invalid*. The aggregator may skip *only* invalid submissions. If the Aggregator skips a valid submission, it will be punished [(see Censorship Resistance)](#censorship-resistance).

Once a submission is verified by the UPA contract, its submission id is marked as verified. Applications can confirm that an individual proof id is verified by providing a `ProofReference`, which is a Merkle proof that the proof id was included in a verified submission. Note that for proofs in a multi-proof submission with $$\mathsf{submissionId}$$, the contract does not mark the proof as verified until the entire submission has been verified.

Once the UPA contract marks a proof (or the submission containing a proof) as verified, an application client can submit a transaction to the application contract (optionally with some `ProofReference` metadata), and the application contract can verify the existence of an associated ZKP as follows:

* The application computes the public inputs for the proof, exactly as it would in the absence of UPA.
* The application contract calls `isProofVerified` on the `UpaVerifier` contract, passing in the public inputs $$\mathsf{PI}$$, the circuit Id $$\mathsf{circuitId}$$, and a `ProofReference` (when required).
* The `UpaVerifier` contract computes $$\mathsf{proofId} = \mathsf{keccak}(\mathsf{circuitId}, \mathsf{PI})$$ from the public inputs and then checks that `ProofReference` contains a valid Merkle proof that $$\mathsf{proofId}$$ belongs to a verified submission.
* The `UpaVerifier` returns `true` if it has a record of a valid proof for $$(\mathsf{circuitId}, \mathsf{proofId})$$, and `false` otherwise.

Application contracts can also verify the existence of multiple ZKPs belonging to the same submission. In this case:

* Application contract computes an array of public inputs $$\[\mathsf{PI}\_i]$$ where the $$i$$-th entry corresponds to the $$i$$-th proof of a submission with $$\mathsf{submissionId}$$.
* Application contract submits an array of tuples $$\[(\mathsf{circuitId}\_i, \mathsf{PI}\_i)]$$ to the UPA contract.
* The UPA contract computes the (unique) $$\mathsf{submissionId}$$ corresponding to the submitted array of circuit ids and public inputs.
* The UPA contract returns 1 if it has verified the submission $$\mathsf{submissionId}$$ (i.e. it has verified all of the proofs within $$\mathsf{submissionId}$$), and 0 otherwise.

Note that in this case, there is no need to submit a `ProofReference`.

## Protocol

### Circuit registration

Before submitting proofs on-chain, the application developer submits a transaction calling the `registerVK` method to the UPA contract (through the `IUpaProofReceiver` interface), passing their verification key $$\mathsf{VK}$$.

The circuit's $$\mathsf{circuitId}$$ is computed as

$$
\mathsf{circuitId} = \mathsf{keccak}(\mathsf{DT}\_\mathsf{circuitId} || \mathsf{VK})
$$

where $$\mathsf{DT}\_\mathsf{circuitId}$$ denotes a domain tag derived from a string describing the context, such as `UPA Groth16 circuit id` (See the Universal Batch Verifier specification for details.)

$$\mathsf{VK}$$ is stored on the contract (for censorship resistance) in a mapping indexed by $$\mathsf{circuitId}$$, and the aggregator is notified via an event. This $$\mathsf{circuitId}$$ will be used to reference the circuit for future operations.

### Application proof submission

The a*pplication client* creates the parameters for its smart contract as normal, including one or more proofs $$\pi\_i$$ and public inputs $$\mathsf{PI}\_i$$. It then passes these, along with the relevant (pre-registered) circuit Ids $$\mathsf{circuitId}\_i$$, to the `submit` method on the `IUpaProofReceiver` interface, paying the aggregation fee in ether:

```solidity
interface IUpaProofReceiver
{
    ...
    function submit(
        uint256[] calldata circuitIds,
        Proof[] calldata proofs,
        uint256[][] calldata publicInputs
    ) external payable override returns (bytes32 submissionId)
    ...
}
```

The `UpaProofReceiver.submit` method:

* computes $$\mathsf{proofId}\_i = \mathsf{keccak}(\mathsf{circuitId}\_i, \mathsf{PI}\_i)$$ for $$i = 0, \ldots, n-1$$.
* computes a `proofDigest` $$\mathsf{proofDigest}$$ for each proof, as $$\mathsf{keccak}(\pi\_i)$$
* computes the submission Id $$\mathsf{submissionId}$$ as the Merkle root of the list $$(\mathsf{\mathsf{keccak}(proofId}*i))*{i=0}^{n-1}$$ (padded as required to the nearest power of 2)
* computes the `digestRoot` as the Merkle root of the list $$(\mathsf{proofDigest}*i)*{i=0}^{n-1}$$ (again padded as required to the nearest power of 2)
* rejects the tx if an entry for $$\mathsf{submissionId}$$ already exists
* assigns a $$\mathsf{submissionIndex}$$ to the submission (using a single incrementing counter)
* assigns a $$\mathsf{proofIndex}\_i$$ to each $$(\pi\_i, \mathsf{PI}\_i)$$ (using a single incrementing counter)
* emits an event for each proof, including $$(\mathsf{circuitId}\_i, \pi\_i, \mathsf{PI}\_i, \mathsf{proofIndex}\_i)$$
* updates the contract state to record the fact that a submission with id $$\mathsf{submissionId}$$ has been made, mapping it to `digestRoot`, $$\mathsf{submissionIndex}$$, $$n$$ and the block number at submission time.

> **NOTE:** Proof data itself does not appear in the input data used to compute $$\mathsf{proofId}$$. This is because when the proof is verified by the application, the application does not have access to (and does not require) any proof data. The application is only verifying the *existence* of a valid proof for the given circuit and public inputs.

> **NOTE:** Application authors must ensure that the public inputs to their ZKPs contain some element that is hard to compute without the corresponding private witness (and in general this will already be the case for sound protocols, in order to prevent replay attacks). If the set of public inputs can be predicted by a malicious party, that malicious party can submit an invalid proof for the public inputs, preventing submission of further (valid) proofs for that same set of public inputs.

### Aggregated proof submission

There is a single (permissioned) *Aggregator* that submits aggregated proofs to the `Upa.verifyAggregatedProof` method. Each aggregated proof attests to the validity of a batch of application proofs. In return, the aggregator can claim submission fees (for on-chain submissions). An aggregated batch may contain proofs from both on-chain and off-chain submissions, as well as *dummy proofs* which are used to fill partial batches.

```solidity
function verifyAggregatedProof(
        bytes calldata proof,
        bytes32[] calldata proofIds,
        uint16 numOnchainProofs,
        SubmissionProof[] calldata submissionProofs,
        uint256 offChainSubmissionMarkers
) external onlyWorker
```

> `proof` - An aggregated proof for the validity of this batch.
>
> `proofIds` - The list of proofIds that are verified by the aggregated proof `proof`. These are assumed to be arranged in the order: \[On-chain, Dummy, Off-chain]. Furthermore, it is assumed that if there are dummy proofIds in this batch, these appear after the last proof in a submission. I.e. where dummy proof ids are used, the on-chain proof ids do not end with a partial submission.
>
> `numOnChainProofs` - The number of proofIds that were from on-chain submissions. This count includes dummy proofs.
>
> `submissionProofs` - An array of 0 or more Merkle proofs, each showing that some of the entries in `proofIds` belong to a specific multi-proof on-chain submission. These are required as we do not have a map from `proofId` to `submissionId` or `submissionIdx`. See the algorithm below for details.
>
> `offChainSubmissionMarkers` - Represents a `bool[]` marking each off-chain member of `proofIds` with a 0 or 1. A proofId is marked with a 1 precisely when the proofId is the last one in an off-chain submission. This `bool[]` is packed into a `uint256` to compress calldata.

The `UpaVerifier` contract:

* checks that `proof` is valid for `proofIds`
* for each $$\mathsf{proofId}$$ in `proofIds`:
  * skips $$\mathsf{proofId}$$ if it corresponds to a dummy proof,
  * checks that $$\mathsf{proofId}$$ has been submitted to the contract, and that proofs appear in the aggregated batch in the order of submission (see below)
  * marks $$\mathsf{proofId}$$ as valid (see below)
  * if $$\mathsf{proofId}$$ is the last proof in a submission $$\mathsf{submissionId}$$, emit an event indicating that the submission $$\mathsf{submissionId}$$ has been verified

Specifically, the algorithm for verifying (in the correct order) submissions of `proofIds`  and marking them as verified, is as follows.

**State:** the contract holds

* a dynamic array `uint16[] numVerifiedInSubmission` of counters, where the $$i$$-th entry corresponds to the number of proofs that have been verified (in order) of the submission with $$\mathsf{submissionId} == i$$
* the submission index `lastVerifiedSubmissionIdx` of the last submission from which a proof was verified.

Given a list of `proofIds` and `submissionProofs`, the contract verifies that `proofIds` appear in previous submissions as follows:

* For each $$\mathsf{proofId}$$ in `proofIds`:
  * If $$\mathsf{proofId}$$ corresponds to a dummy proof, then the rest of the proofs in the batch are assumed to be dummy proofs. No more proofs from this batch will be marked as valid.
  * Attempt to lookup the submission data (see [Proof Submission](#proof-submission)) for a submission with Id $$\mathsf{keccak}(\mathsf{proofId})$$. If such a submission exists:
    * The proof was submitted as a single-proof submission. The contract extracts the $$\mathsf{submissionIndex}$$ from the submission data and then checks that $$\mathsf{submissionIndex}$$ is greater than or equal to`nextSubmissionIdxToVerify`. If not reject the transaction.
    * The entry `numVerifiedInSubmission[` $$\mathsf{submissionIndex}$$ `]` should logically be 0 (this can be sanity checked by the contract). Set this entry to 1
    * Update `nextSubmissionIdxToVerify` in contract state
  * Otherwise (if no submission data was found for $$\mathsf{submissionId} = \mathsf{keccak}(\mathsf{proofId})$$)
    * the proof is expected to be part of a multi-proof submission with $$\mathsf{submissionIndex} \geq$$ `nextSubmissionIdxToVerify`.
      * Note that if a previous aggregated proof verified some subset, but not all, of the entries in the submission, `nextSubmissionIdxToVerify` would still refer to the partially verified submission at this stage. In this case, `numVerifiedInSubmission[` $$\mathsf{submissionIndex}$$ `]` should contain the number of entries already verified.
    * Take the next entry in `submissionProofs`. This includes the following information:
      * the $$\mathsf{submissionId}$$ for the submission to be verified
      * a Merkle "interval" proof for a contiguous set of entries from that submission.
* Determine the number `m` of entries in `proofIds`, including the current $$\mathsf{proofId}$$, that belong to this submission, as follows:
  * Let `numProofIdsRemaining` be the number of entries (including $$\mathsf{proofId}$$) still unchecked in `proofIds`.
  * Look up the submission data for $$\mathsf{submissionId}$$, in particular $$\mathsf{submissionIndex}$$ and $$n$$.
  * Let `numUnverifiedFromSubmission =` $$n$$ `- numVerifiedInSubmission[` $$\mathsf{submissionIndex}$$ `]`.
  * The number `m` of entries from `proofIds` to consider as part of $$\mathsf{submissionId}$$ is given by `Min(numUnverifiedFromSubmission, numProofIdsRemaining)`.
* Use the submission Id $$\mathsf{submissionId}$$ and the Merkle "interval" proof from the submission proof, to check that the hashes of the `m` next entries from `proofIds` (including $$\mathsf{keccak}(\mathsf{proofId})$$) indeed belong to the submission $$\mathsf{submissionId}$$. Reject the transaction if this check fails.
* Increment the entry `numVerifiedInSubmission[` $$\mathsf{submissionIndex}$$ `]` by `m`, indicating that `m` additional proofs from the submission have been verified.
* update `nextSubmissionIdxToVerify` in the contract state

> **NOTE:** The arguments `offChainSubmissionMarkers` and `numOnchainProofs` are there for future off-chain submission support. For now, aggregators call this function with `numOnchainProofs = BATCH_SIZE`, which will skip the off-chain logic of this function.&#x20;

### Proof verification by the application

The application client now creates the transaction calling the application's smart contract to perform the business logic. Since the proof has already been submitted to UPA, the proof is not required in this transaction.  If the proof was submitted as part of a multi-entry submission, the client must compute and send a `ProofReference` structure indicating which submission the proof belongs to, and its "location" (or index) within it.

The application contract computes the public inputs, exactly as it otherwise would under normal operation, and queries the `isProofVerified` on the `UpaVerifier` contract (using the `ProofReference` if given) to confirm the existence of a corresponding verified proof.

For proofs from single-entry submissions, the UPA provides the entry points:

```solidity
function isProofVerified(
        uint256 circuitId,
        uint256[] calldata publicInputs)
    external
    view
    returns (bool);

function isProofVerified(bytes32 proofId) external view returns (bool);
```

For proofs from multi-entry submissions, the UPA provides entry points:

```solidity
function isProofVerified(
        uint256 circuitId,
        uint256[] calldata publicInputs,
        ProofReference calldata proofRef)
    external
    view
    returns (bool);

function isProofVerified(
        bytes32 proofId,
        ProofReference calldata proofReference
    ) external view returns (bool);
```

The UPA contract:

* receives $$\mathsf{proofId}$$ or computes $$\mathsf{proofId}$$ from the public inputs
* (using the `ProofReference` if necessary) confirms that $$\mathsf{proofId}$$ belongs to a submission $$\mathsf{submissionId}$$.&#x20;
* Checks if there was an on-chain submission for $$\mathsf{submissionId}$$, and if so reads the stored submission index $$\mathsf{submissionIdx}$$ and the total number of proofs `numProofs` contained in the submission $$\mathsf{submissionId}$$. If it finds that `numVerifiedInSubmission[`$$\mathsf{submissionIdx}$$`] == numProofs` then the submission $$\mathsf{submissionId}$$ was verified, and therefore so was the proof $$\mathsf{proofId}$$.

The application contract can also look up the verification status of entire submissions by computing the corresponding (nested) array of public inputs. The contract can then either use a submissionId computed from this array, or the array itself, to query the submission's status in the UPA contract.

The UPA provides the entry points:

```solidity
// If all proofs have the same circuitId.
function isSubmissionVerified(
    uint256 circuitId,
    uint256[][] memory publicInputsArray
) external view returns (bool);

function isSubmissionVerified(
    uint256[] calldata circuitIds,
    uint256[][] memory publicInputsArray
) external view returns (bool);

function isSubmissionVerified(
    bytes32 submissionId
) external view returns (bool);
```

The UPA contract:

* receives $$\mathsf{submissionId}$$ or computes $$\mathsf{submissionId}$$ from the public inputs
* Looks up the number of proofs `numProofsInSubmission` in $$\mathsf{submissionId}$$ and then checks if `numVerifiedInSubmission[`$$\mathsf{submissionIdx}$$`] = numProofsInSubmission`.

### Censorship resistance

A censorship event is considered to have occurred for a submission with Id $$\mathsf{submissionId}$$ (with submission index $$\mathsf{submissionIndex}$$, consisting of $$n$$ entries) if all of the following are satisfied:

* a submission with Id $$\mathsf{submissionId}$$ has been made, and **all** proofs in the submission are valid for the corresponding public inputs and circuit Ids
* some of the entries in $$\mathsf{submissionId}$$ remain unverified, namely
  * `numVerifiedInSubmission[`$$\mathsf{submissionIndex}$$`] <` $$n$$
* one or more proofs from a submission with index greater than $$\mathsf{submissionIndex}$$ (the submission index of the submission with id $$\mathsf{submissionId}$$) have been included in an aggregated batch
  * namely, there exists $$j > \mathsf{submissionIndex}$$ s.t. `numVerifiedInSubmission[`$$j$$`] > 0`

Note that, if one or more entries in a submission are invalid, aggregators are not obliged to verify any proofs from that submission.

Censorship by the *Aggregator* can be proven by a *claimant*, by calling the method:

```solidity
function challenge(
        bytes32 circuitId,
        Groth16Proof calldata proof,
        uint256[] calldata publicInputs,
        bytes32 submissionId,
        bytes32[] calldata proofIdMerkleProof,
        bytes32[] calldata proofDataMerkleProof
) external returns (bool challengeSuccessful);
```

providing:

* the **valid** tuple $$(\mathsf{circuitId}, \pi, \mathsf{PI})$$, or `circuitId`, `proof` and `publicInputs`, the claimed next unverified entry in the submission
* $$\mathsf{submissionId}$$ or `submissionId`
* $$j$$ or `laterSubmissionIdx`
* A Merkle proof that $$\mathsf{proofId}\_i$$ (computed from $$\mathsf{circuitId}\_i$$ and $$\mathsf{PI}$$ belongs to the submission (at the "next index" - see below)
* A Merkle proof that $$\pi\_i$$ belongs to the submission's `proofDigest` entry (at the "next index" - see below)

On receipt of a transaction calling this method, the contract:

* checks that the conditions above hold and that the provided proof has indeed been skipped
* looks up the verification key $$\mathsf{VK}$$ using $$\mathsf{circuitId}$$ and performs the full proof verification for $$(\mathsf{VK}, \pi, \mathsf{PI})$$. The transaction is rejected if the proof is not valid or if the verification key hasn't been registered.
* increments the stored count `numVerifiedInSubmission[`$$\mathsf{submissionIndex}$$`]`

The aggregator is punished only when all proofs in the submission have been shown to be valid. As such, after the above, the contract:

* checks the condition `numVerifiedInSubmission[`$$\mathsf{submissionIndex}$$`] == n` (where `n` is the number of proofs in the original submission $$\mathsf{submissionId}$$).
* if this final condition holds then validity of all proofs in the submission has been shown and the aggregator is punished.

Note: `proofDigest` is used here to prevent malicious clients from submitting invalid proofs, forcing aggregators to skip their proofs, and then later providing valid proofs for the same public inputs. This would otherwise be an attack vector since $$\mathsf{proofId}$$ is not dependent on the proof data.

### Collecting Aggregation Fees

The application contract pays an aggregation fee at submission time. These fees are held in the UPA contract. In order for the aggregator to claim the fees for a given submission, the UPA contract must have verified that submission.

The aggregator collects fees in two steps. First it calls

```solidity
function allocateAggregatorFee(uint64 lastSubmittedSubmissionIdx)
```

which stores the current value of `lastSubmittedSubmissionIdx` and allocates all fees collected up to now to be claimable by the aggregator once it has verified the submission at `lastSubmittedSubmissionIdx` (which implies that all previous submissions have also been verified). Once the aggregator has done this, it can call

```solidity
function claimAggregatorFee(
    address aggregator,
    uint64 lastVerifiedSubmissionIdx
)
```

to withdraw the previously allocated fees.

### Circuit Statements

Batches of $$n$$ application proofs are verified in a *batch verify circuit.*

A *keccak circuit* computes all $$\mathsf{circuitId}$$s and $$\mathsf{proofId}$$s of application proofs appearing in the *batch verify proof*, along with a *final digest* (the keccak hash of these $$\mathsf{proofId}$$s, used to reduce the public input size of the outer circuit below).

A collection of $$N$$ *batch verify proofs* along with the *keccak proof* for their $$\mathsf{circuitId}$$s, $$\mathsf{proofId}$$s and *final digest* is verified in an *outer circuit*.

On-chain verification of an outer circuit proof thereby attests to the validity of $$n \times N$$ application proofs with given $$\mathsf{proofId}$$s.

$$n$$ - inner batch size. Application proofs per batch verify circuit.

$$N$$ - outer batch size. Number of batch-verify circuits per outer proof.&#x20;

$$L$$ - the maximum number of public inputs for an application circuit.

#### Batch Verify Circuit: Groth16 batch verifier

The batch verify circuit corresponds to the following relation:

* *Public inputs*:
  * $$(\ell\_i, \overline{\mathsf{VK}}\_i, \overline{\mathsf{PI}}*i)*{i=1}^n$$ where
    * $$\mathsf{PI}*i = (x*{i,j})\_{j=1}^{\ell\_i}$$ is the public inputs to the $$i$$-th proof
    * $$\overline{\mathsf{PI}}\_i = \mathsf{PI}*i | {0}*{j=\ell\_i + 1}^{L}$$ is $$\mathsf{PI}\_i$$ after zero-padded to extend it to length $$L$$
    * $$\overline{\mathsf{VK}}\_i$$ - application verification keys, each padded to length $$L$$
* *Witness values*:
  * $$(\pi\_i)\_{i=1}^n$$ - application proofs
* *Statement*:
  * $$\mathsf{PI}\_i = \mathsf{truncate}(\ell\_i, \overline{\mathsf{PI}}*i) | {0}*{j=\ell\_i + 1}^{L}$$
  * $$\mathsf{Groth16.Verify}(\overline{\mathsf{VK}}\_i, \pi\_i, \overline{\mathsf{PI}}\_i) = 1$$ for $$i=1,\ldots,n$$
  * where
    * $$\mathsf{truncate}(\ell, \overline{\mathsf{VK}})$$ is the truncation of the size $$L$$ verification key $$\overline{\mathsf{VK}}$$ to a verification key of size $$\ell$$, and
    * $$\mathsf{truncate}(\ell, \overline{\mathsf{PI}})$$ is the truncation of the public inputs to an array of size $$\ell$$

#### Keccak Circuit: ProofIds and Final Digest

Computes the $$\mathsf{proofId}$$ for each entry in each application proof in one or more verify circuit proofs.

* *Public inputs*:
  * $$c^\*, (\ell\_i, \overline{\mathsf{VK}}\_i, \mathsf{circuitId}\_i, \overline{\mathsf{PI}}*i)*{i=1}^{n \times N}$$ where
    * $$\mathsf{PI}*i = (x*{i,j})\_{j=1}^{\ell\_i}$$ is the public inputs to the $$i$$-th proof
    * $$\overline{\mathsf{PI}}\_i = \mathsf{PI}*i | {0}*{j=\ell\_i + 1}^{L}$$ is $$\mathsf{PI}\_i$$ after zero-padded to extend it to length $$L$$
    * $$\overline{\mathsf{VK}}\_i$$ - application verification keys, each padded to length $$L$$
    * $$c^\* = (c^*\_1, c^*\_2)$$ (digest, which consists of 32 bytes and is represented by two field elements)
* *Witness values*: (none)
* *Statement*:
  * $$c\_i = \mathsf{keccak}(\mathsf{circuitId}\_i || \mathsf{truncate}(\ell\_i, \overline{\mathsf{PI}}\_i))$$
  * $$c^\* = \mathsf{keccak}(c\_1 || c\_2 || \ldots || c\_{n \times N})$$
  * $$\mathsf{circuitId}\_i = \mathsf{keccak}(\mathsf{truncate}(\ell\_i, \overline{\mathsf{VK}}\_i))$$

#### Outer Circuit: Recursive verification of Batch Verifier and Keccak circuits

This step aggregates $$N$$ batch verify proofs $${\pi\_{\text{bv}}}^{(j)}, j = 1, \ldots N$$ as well as a single corresponding Keccak proof $$\pi\_{keccak}$$.

* Public Inputs:
  * $$c^\*$$ - final 32-byte public input digest, encoded as $$(c\_1, c\_2) \in \mathbb{F}\_r^2$$
  * $$(L, R) \in \mathbb{G}\_1^2$$ - overall KZG accumulator, encoded as $$12 = 4 \* \texttt{num\_limbs}$$ points of $$\mathbb{F}\_r$$
* Witness values:
  * $$(\ell\_{i,j}, \overline{\mathsf{VK}}*{i, j}, \overline{\mathsf{PI}}*{i,j}) \text{ for } i=1,\ldots, n, j=1, \ldots, N$$: the number of public inputs, the padded verifying key, and padded public inputs for the $$i$$-th application proof in the $$j$$-th BV proof.
  * &#x20;for $$j=1, \ldots, N$$ BV proofs
  * $$\pi\_{\mathsf{keccak}}$$ the Keccak proof for the public inputs
    * $$c^\*$$, and
    * $${ (\ell\_{i, j}, \overline{\mathsf{VK}}*{i, j}, \overline{\mathsf{PI}}*{i, j} }*{\substack{i=1,\ldots, n \ j=1,\ldots,  N}}$$$$(\ell*{1,N}, \mathsf{circuitId}*{1,N}, \overline{\mathsf{PI}}*{1,N}), (\ell\_{2,N}, \mathsf{circuitId}*{2,N}, \overline{\mathsf{PI}}*{2,N}), \ldots , (\ell\_{n,N}, \mathsf{circuitId}*{n,N}, \overline{\mathsf{PI}}*{n,N}),$$
* "Equivalent Statement": (actual statement is shown as multiple sub-statements, given below)
  * All BV proofs are valid, and therefore there exist valid application proofs for each $$\mathsf{PI}*{i,j}$$: $$\textsf{SNARK}*{\text{BV}}.\textsf{Verify} \left( \pi\_{\text{bv}}^{(j)}, (\ell\_{i,j}, \overline{\mathsf{VK}}*{i,j}, \overline{\mathsf{PI}}*{i,j})*{i=1}^n, \mathsf{VK}*{\text{BV}} \right)$$ for $$j=1,\ldots, N$$
  * Keccak proof is valid, and therefore $$c^*$$ is the final digest for all application PIs and vk hashes: $$\textsf{SNARK}*{\mathsf{keccak}}.\textsf{Verify} \left(\pi*\mathsf{keccak}, c^*,(\ell\_{i,j}, \overline{\mathsf{VK}}*{i,j}, \overline{\mathsf{PI}}*{i,j})*{\substack{i=1,\ldots, n \ j=1,\ldots,  N}}, \mathsf{VK}*\mathsf{keccak} \right)=1$$
* Actual Statement:

  * "Succinct" Plonk verification ($$\textsf{SuccinctVerify}$$) namely "GWC Steps 1-11" using Shplonk, without final pairing, for random challenge scalar $$r$$:

  &#x20;$$\begin{gathered} (L\_j, R\_j) = \textsf{SuccinctVerify} \left( \pi\_{\text{bv}}^{(j)}, (\ell\_{i,j}, \overline{\mathsf{VK}}*{i,j}, \overline{\mathsf{PI}}*{i,j})*{i=1}^n, \mathsf{VK}*{\text{BV}} \right) \~\text{ for } j=1,\ldots N \ (L\_{N+1}, R\_{N+1}) = \textsf{SuccinctVerify} \left( \pi\_\mathsf{keccak}, c^\*,(\ell\_{i,j}, \overline{\mathsf{VK}}*{i,j}, \overline{\mathsf{PI}}*{i,j})*{\substack{i=1,\ldots, n \ j=1,\ldots, N}}, \mathsf{VK}*\mathsf{keccak} \right) \ (L, R) = \sum\_{j=1}^{N+1} r^j (L\_j, R\_j) \end{gathered}$$&#x20;
* Verification: The EVM verifier does the following, given $$(\pi\_{\text{outer}}, L, R, c^\*)$$.
  * $$(L\_\text{outer}, R\_\text{outer}) := \textsf{SuccinctVerify}(\mathsf{PI}*\text{outer}, L, R, c^\*, \mathsf{VK}*\text{outer})$$
  * $$e(L + r' L\_\text{outer}, \[\tau]*2) \stackrel{?}{=} e(R + r' R*\text{outer}, \[1]\_2)$$ for random challenge scalar $$r'$$

Note that:

* The same witness values $$\overline{\mathsf{PI}}*{i,j}$$ are used to verify $$\pi*{\text{bv}}^{(j)}$$ and $$\pi\_{\mathsf{keccak}}$$, implying that $$c^\*$$ is indeed the commitment to all application public inputs and circuit IDs.
* The outer circuit does not include the pairing checks, therefore its statement is not that the BV/Keccak proofs are *valid*, but rather that they have been correctly accumulated into a single KZG accumulator $$(L,R)$$. Checking that $$e(L + r' L\_\text{outer}, \[\tau]*2) \stackrel{?}{=} e(R + r' R*\text{outer}, \[1]\_2)$$, for random scalar $$r'$$, therefore implies their validity.
* In the case there is a Pedersen commitment point for proofs coming from e.g. gnark, the statements of the batch verifier and keccak circuits are a bit different. For each application proof:
  * \[Batch verifier circuit] The Pedersen proof is verified: $$e(\mathsf{comm}, h\_1) e( \mathsf{pok}, h\_2) = 1$$, where
    * $$h\_1, h\_2 \in \mathbb{G}\_2$$ is the Pedersen verification key (which is part of the corresponding app $$\mathsf{VK}$$).
    * $$\mathsf{comm}$$ is the Pedersen commitment point and $$\mathsf{pok}$$ the corresponding Pedersen proof of knowledge.
  * \[Keccak circuit] The last public input is computed as the keccak hash of the commitment point: $$\overline{PI}\_{\ell + 1} = \mathsf{keccak}(\mathsf{comm})$$. Note that this last public input is not used in the computation of the proof Id.$$f(x) = x \* e^{2 pi i \xi x}$$


# Integrating with zkVMs

zkVMs produce ZKPs of the validity of arbitrary computations. Very often, these proofs are STARK proofs, which are expensive to verify in Ethereum smart contracts.&#x20;

Consequently, these STARK proofs can be recursively wrapped to generate a Groth16 or PLONK proof which can be efficiently verified on-chain (in the EVM or similar execution environment). In this section, we describe how to take such Groth16 proofs and aggregate them via UPA.


# Integrating with SP1

SP1 by Succinct Labs is a zkVM technology. It generates STARK proofs but provides tooling for wrapping these STARK proofs with Groth16 or PLONK proofs, so they are more amenable for on-chain verification.&#x20;

The [SP1 Project Template](https://github.com/succinctlabs/sp1-project-template) provided by Succinct Labs provides an end-to-end workflow of how one can verify zkVM proofs on-chain. Roughly the workflow in this repo is:

* A STARK is generated by SP1 which proves the validity of a Fibonacci program written in Rust, which computes the $$n$$th Fibonacci number.
* This STARK proof is wrapped into a PLONK proof via SP1's SDK.
* In the `Fibonacci.sol` smart contract, a `verifyFibonacciProof` function is provided, which takes in the PLONK proof and public inputs and verifies the proof on-chain via a call to a [standardized smart contract](https://github.com/succinctlabs/sp1-contracts/blob/main/contracts/src/v1.2.0/SP1VerifierPlonk.sol) (`SP1VerifierPlonk.sol`) developed by SP1.

NEBRA has created an [UPA-SP1 Project Template](https://github.com/NebraZKP/upa-sp1-project-template) which shows how the above Fibonacci program written in Rust can be aggregated via UPA. Roughly the workflow in this repo is:

* A STARK is generated by SP1 which proves the validity of a Fibonacci program written in Rust, which computes the $$n$$th Fibonacci number (same as above).
* This STARK proof is wrapped into a Groth16 proof via SP1's SDK.
* In the `UpaFibonacci.sol` smart contract,  a `verifyFibonacci` function is provided. However, unlike the workflow above, the Groth16 proof is *not* submitted to this contract. Rather, this function *only* takes in the public inputs and calls into the `UPAVerfier` contract to check whether a Groth16 proof corresponding to these public inputs and the SP1 Groth16 circuit have been aggregated by the UPA system.
* A [shell script](https://github.com/NebraZKP/upa-sp1-project-template/blob/main/upa/scripts/upa_sp1) showing the end-end-end flow is provided. In this script:
  * A `UPAVerifier` contract is deployed to a local hardhat node.
  * The SP1 Groth16 proof corresponding to the Fibonacci circuit, along with another Groth16 proof is submitted to the `UPAVerifier` contract.
  * UPA's`dev-aggregator` dev tool, which mimics our actual aggregator, is used to aggregate these two proofs and submit the result on-chain.
  * The `UPAFibonacci` contract is called to check whether the SP1 Groth16 Fibonacci proof has been verified.

In general, for apps looking to integrate UPA with SP1, the workflow would be as follows:

* Use SP1 SDK to wrap your SP1 STARK proof and generate a Groth16 proof.
* Using UPA's CLI tool or SDK, parse the Groth16 proof data into UPA proof file format.
* Use UPA's CLI tool or SDK to submit the proof to `UPAVerifier` contract.
* Wait for proof to be aggregated and then use the result in business logic of the app.


# Security and Transparency

## UPA Security and Transparency

NEBRA's Universal Proof Aggregation (UPA) protocol enables applications on Ethereum to outsource the task of verifying zero-knowledge proofs (ZKPs) to an off-chain aggregator. The UPA verifies ZKPs at a lower cost than direct verification in a smart contract, while maintaining the same security and soundness guarantees.

NEBRA is committed to security and transparency, and we believe that users of the UPA have a right to examine the code and read the audit reports themselves before entrusting us with their proofs. This page serves as a security-oriented guide to the UPA protocol and its current implementation. We begin with an overview of the UPA's guarantees and potential concerns, then provide details on the on- and off-chain components of the UPA.

#### UPA Guarantees

At a high level, the security guarantees of the UPA are:

* Proof validity: If the UPA marks a given proof ID as "verified" then the user must have submitted a valid proof to the UPA.
* Equal privacy: the UPA does not require additional information beyond the proof and public inputs. It therefore maintains the existing level of privacy offered by each application.

Additionally, the UPA offers a censorship-resistance guarantee via on-chain proof submission:

* Proofs submitted on-chain are indexed by the UPA contract and must be verified in this order (assuming they are valid). Failure to include valid proofs or failure to respect the ordering of proofs exposes the aggregator to a penalty.

#### Application Developer Responsibilities

The UPA *cannot* address the following security concerns. It is the application developer's responsibility to prevent:

* Replay attacks: once a proof has been verified by the UPA, its inputs are forever marked as "verified." Application developers must decide whether it should be possible to reuse these inputs for multiple transactions. When reuse is undesirable the application itself must prevent this with some mechanism such as a nullifier. (This should already be the case, because even without the UPA it is possible to generate multiple valid proofs for the same set of inputs!)
* Under-constrained circuits: it is the application developer's responsibility to write circuits that correctly enforce the in-circuit portion of their application logic.

#### Liveness and Redundancy

Developers should understand that the UPA protocol has an off-chain component, and therefore cannot guarantee liveness equal to that of Ethereum. NEBRA will strive to match Ethereum's liveness by building redundancy into the prover network that powers the UPA's off-chain component. Nonetheless, we recommend that application developers build in resilience to any potential outages the UPA may experience.

This simply means that application smart contracts should maintain the ability to directly verify users' proofs, as they would in the absence of the UPA. Then, in the event of UPA downtime, users can choose to submit their transaction with "direct verification" rather than "UPA verification," albeit at a higher cost.

### UPA On-Chain Component

The UPA's on-chain component is a collection of smart contracts on Ethereum. Its responsibilities include

* Verifying Key Registration: the UPA records verifying keys submitted by developers and assigns to each key a unique circuit ID.
* Proof Submission: the UPA accepts (circuit ID, proof, inputs) tuples and
  * Ensures that the circuit ID belongs to a previously-registered VK
  * Computes a proof ID from the circuit ID and inputs
  * Indexes submissions, determining the order in which the aggregator must verify them
  * Records a "proof digest," identifying the proof for potential censorship challenges
* Aggregated Proof Verification: the UPA checks proofs submitted by the aggregator, ensuring that only valid application proofs will be marked as verified
* Verification Queries: allowing applications to query whether a given proof ID has been marked as verified
* Fee Collection: determining and collecting the fee due for proof aggregation
* Fee Disbursement: ensuring that the aggregator can collect the fee only *after* aggregating proofs
* Censorship resistance: handling censorship challenges and punishing the aggregator for censoring valid proofs

The on-chain portion of the protocol is specified [here](https://github.com/NebraZKP/upa/blob/develop/spec/protocol/upa-1.2.0.pdf) and the smart contracts implementing the protocol can be found [here](https://github.com/NebraZKP/upa/tree/develop/upa/contracts). All contracts are written in Solidity (with one exception, see [below](#aggregatedproofverifier-contract)). A list of current deployments can be found [here](/developer-guide/deployments).

The on-chain protocol and its implementation have been audited by ABDK and Zellic. Their reports may be found [here](https://github.com/NebraZKP/upa/tree/develop/audits).

An open-source SDK for integrating with the UPA is available [here](https://www.npmjs.com/package/@nebrazkp/upa). It is unaudited, and NEBRA assumes no responsibility for its behavior. Please submit any bug reports or feature requests in NEBRA's [Telegram channel](https://t.me/c/1924667284/3).

### UPA Off-Chain Component

The UPA's off-chain component is a collection of zero-knowledge circuits that ensure that only valid application proofs will be marked as verified by the UPA. More precisely, the soundness guarantee is that if a given proof ID is an input to a valid aggregated proof, then the aggregator has knowledge of a valid application proof for the verifying key and public inputs corresponding to that proof ID.

Applications using the UPA therefore receive a cryptographic guarantee that their users submitted valid application proofs for any inputs that the UPA marks as verified.

The off-chain portion of the protocol is specified [here](https://github.com/NebraZKP/upa/tree/develop/spec/circuits) and its open-source implementation as Halo2 circuits can be found [here](https://github.com/NebraZKP/upa/tree/develop/circuits). The off-chain protocol and its implementation have been audited by ABDK and Zellic. Their reports may be found [here](https://github.com/NebraZKP/upa/tree/develop/audits).

Proofs for these circuits are produced by a permissioned off-chain aggregator. This aggregator is operated by NEBRA (though the protocol allows for the role to eventually be shared or even decentralized). We emphasize that although the aggregator is centralized, it is not trusted. Even a malicious aggregator cannot aggregate invalid application proofs, thanks to the cryptographic soundness guarantee of the circuits. We explain below how the UPA smart contracts verify the work done by the aggregator.

Because the aggregator is untrusted, there is technically no need to inspect its source code. Nonetheless, NEBRA has open-sourced a [prover tool](https://github.com/NebraZKP/upa/tree/develop/prover) capable of performing all the core functionality of the aggregator.

### `AggregatedProofVerifier` Contract

The `AggregatedProofVerifier` contract is the on-chain component of the UPA that is responsible for verifying the aggregated proofs produced by the aggregator. This contract links the on- and off-chain parts of the protocol, allowing the UPA contract to verify the aggregator's claim that a list of application inputs have valid proofs. It plays an essential role in the overall soundness of the UPA.

Unlike the other UPA contracts, the `AggregatedProofVerifier` is not written in Solidity. It is generated by the open-source `snark-verifier` [library](https://github.com/axiom-crypto/snark-verifier) from the verifying key of the Halo2 circuit that produces aggregated proofs (the "outer" circuit). This library generates an on-chain verifier in the form of Yul code, a low-level assembly language that is not really human-readable (at least not easily).

Auditing the `AggregatedProofVerifier` contract is therefore more challenging than the main UPA contracts, but not impossible. Because the contract is generated deterministically using open-source tools and data, any third party is free to repeat the procedure and check that the resulting bytecode matches the actual deployment. This is similar to how ordinary smart contracts written in Solidity can have their source code verified by blockchain explorers like Etherscan; the blockchain explorer compiles the provided Solidity code and checks that it matches the contract's EVM bytecode.

In the `AggregatedProofVerifier`'s case, the compilation pipeline is more complex. To ensure the UPA protocol's transparency, we provide a [script](https://github.com/NebraZKP/upa/tree/develop/verification) that runs through this pipeline and compares the result to our on-chain deployment. The pipeline that produces this bytecode is:

1. Trusted Input: The pipeline begins with a KZG structured reference string (SRS). The UPA uses an SRS produced by the [Perpetual Powers of Tau Ceremony](https://pse.dev/en/projects/perpetual-powers-of-tau) (PPoT), a secure multiparty computation orchestrated by the Ethereum Foundation's Privacy and Scaling Explorations group.
2. Transform the SRS to a Halo2-compatible format. The PPoT SRS is in a format that is not immediately usable by Halo2 circuits. An open-source [tool](https://github.com/NebraZKP/phase2-bn254/tree/halo2/powersoftau) (written by Kobi Gurkan and modified by Axiom Crypto and NEBRA) converts the SRS to the appropriate format.
3. Compute Halo2 Verifying Key. This step takes the description of the outer circuit as a Halo2 circuit and produces a verifying key from the SRS produced by the previous step. The open-source tool that performs this step is NEBRA's [prover](https://github.com/NebraZKP/upa/tree/develop/prover) tool, which relies on the `halo2-proofs` [library](https://github.com/privacy-scaling-explorations/halo2.git) for key generation.
4. Generate verifier Yul code. This step takes the Outer Circuit verifying key produced above and the SRS from Step 2 and produces Yul code that verifies the proofs submitted by the aggregator to the UPA contract. The open-source tool that performs this step is the `snark-verifier` [library](https://github.com/axiom-crypto/snark-verifier).
5. Compile to EVM bytecode. Finally, `solc` compiles the Yul verifier generated in the previous step to EVM bytecode. For the sake of comparison to the deployed contract code, it is important to use the same `solc` version (`v0.8.17`) used by NEBRA during deployment.


