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 inputsconstproofInputs=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.consttx=awaityourApp.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 SDKimport { application, Groth16Proof, UpaClient } from"@nebrazkp/upa/sdk";import { config } from"@nebrazkp/upa/tool";...// Connect also to a UPAClient (provider is an ethers.Provider)constupaInstanceFile="...";constupaInstanceDescriptor=config.loadInstance(upaInstanceFile);constupaClient=UpaClient.fromInstanceFile(upaInstanceDescriptor, provider);...// Your app generates a proof based on certain inputsconstproofData=snarkjs.groth16.fullProve( proofInputs, circuitWasm, circuitZkey);// Convert it to UPA-compatible formatconstproof=Groth16Proof.from_snarkjs(proofData.proof);constpublicInputs:bigint[] =proofData.publicSignals.map(BigInt);// Submit proof to UPA for verification. It's possible to // submit batches of proofs here for greater gas savings.constcircuitId="..."; // Identifies your circuit. See deployment sectionconstsubmissionHandle=awaitupaClient.submitProofs([ { circuitId, proof, publicInputs, }, ]);// Wait for proof to be verifiedawaitupaClient.waitForProofVerified(submissionHandle);// Your app now submits only public inputs, not the proofconsttx=awaityourApp.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 proofscontractYourAppisGroth16Verifier {// ... app statefunctionsubmitTransaction(Proofcalldata proof,uint256[] calldata publicInputs, ) public {// Your contract verifies the proof on chainbool 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
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 interfaceimport"@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.contractYourAppisGroth16Verifier {// ... app state// NEBRA's UPA contract interface IUpaVerifier public upaContract;// Circuit identifier for your appuint256public 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.functionsubmitTransaction(uint256[] calldata publicInputs, ) public {// Query UPA contractbool isProofCorrect = upaContract.isProofVerified(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'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.
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.