Quickstart
Last updated
Was this helpful?
Last updated
Was this helpful?
To start using NEBRA UPA in your application you'll need to
Submit proofs to the UPA from your app Submitting Proofs to UPA
Query the UPA contract Querying the UPA Contract
Modify your deployment Deployment
Set up a UPA-enabled test environment Test Environment
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.
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, 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);
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.
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 {
// 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 "@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
// ...
}
}
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.
The deployment may look something like this
// 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
{
"verifier": "0x3B946743DEB7B6C97F05B7a31B23562448047E3E",
"deploymentBlockNumber": 20528085,
"deploymentTx": "0xd84efac6fc5304747cf72def5bcb7bc2248bd43a6ea4fa7e00f6097269880077",
"chainId": "1"
}
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
The UPA only accepts proofs from registered verification keys. Registration is permissionless. You'll use one of the two methods demonstrated in Registering applications
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 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
upa dev aggregator \
--keyfile ${UPA_KEYFILE} \
--instance ${UPA_INSTANCE} \
> 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.
Congratulations, you are now saving gas.