Quickstart

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

This guide will take you through those steps, using our SDK. Here we keep the code concise and just stub out the main ideas. For a working example, check out our Demo app.

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:

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

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

...

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

...

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

// 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);

We've used the UPAClient from the SDK to

  • Submit the proof to the UPA smart contract using upaClient.submitProofs.

  • 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.

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

// 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 returns (bool r) {
        // 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 
        // ...
    }
}

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)

  • Your application's circuitId (computed with SDK below)

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

// App.sol with UPA

// NEBRA's UPA contract interface
import "./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 returns (bool r) {
        // Query UPA contract
        bool isProofCorrect = upaContract.isVerified(circuitId, publicInputs);
        require(isProofCorrect, "Proof was not correct.");
        
        // Proceed with app's business logic 
        // ...
    }
}

Congratulations, you are now saving gas. 🎉

Deployment

Above we added two things to your smart contract's state:

  • upaContract interface to the UPA

  • circuitId identifying your app 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.

The deployment may look something like this

// deploy.js with UPA

// SDK imports
import { tool } from "@nebrazkp/upa/sdk";
const { upaFromInstanceFile } = tool.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 = await upaInstance.proofReceiver.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 UPA contract addresses, such as

{
    "verifier": "0xB6D545eEe2d9B3653DaBCf1D0ea23A80FCd51B4C",
    "proofReceiver": "0xb85eb64229A46ee784ee504bc583E0820551fa7c",
    "poseidon": "0xA86A504434dFf5ACd00AD0B618cdCdF945cd7008",
    "aggregatedProofVerifier": "0xaf0e17dC4De072b9b6a356c51418e017804c560D",
    "feeModel": "0xd43Ed118ef8E2D8d5E11FeEbadADDf456d31639E"
}

The latest deployment information: 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

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

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

and the Dev Aggregator is then deployed with

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

For more complete instructions, see Testing workflow.

Congrats, you're up and running!

If you need to see anything we did here in more detail, go check out our Demo App.

Last updated