Zkapps (zero knowledge applications) are mina protocol smart contracts which are powered by zero knowledge proofs, specifically zk-Snarks [Zero-Knowledge Succinct Non-Interactive Argument of Knowledge].zkapps replaced snapps [Smart Non-Interactive Argument of Knowledge Applications]. ZkApp smart contracts are written using o1js (a TypeScript library). zkApps runs client side in a user’s web browser, and publish only a small validity proof which is then verified by the Mina nodes. Zkapp consists of smart contract and UI which I will further describe in the next section.
Application
I created zkapp regarding age verification where the user age gets validated without intervention to the personal data.
I proceeded by installing zkapp-cli npm package which actually created templates for proceeding with prover function and verifier function as part of the zk proof build process
Implementation
Below is the implementation of adding verification custom logic. It defines the circuit logic for zk-SNARKs, which is used during proof generation. The actual prover function is managed by the o1js library and invoked when zkApp methods are executed off-chain with private inputs.
import { Field, SmartContract, state, State, method } from 'o1js';
/**
* Private Age Verification Contract
* The contract will verify if the user's age is greater than or equal to the threshold age.
* The contract uses zero-knowledge proofs to keep the user's age private.
*/
export class AgeVerification extends SmartContract {
// State variable to store the verification result (valid or invalid)
@state(Field) valid = State<Field>();
// Method to initialize the state
init() {
super.init();
this.valid.set(Field(0)); // Default is invalid
}
// Method to verify the age
@method async verifyAge(age: Field, threshold: Field) {
// Compute age - threshold
const difference = age.sub(threshold);
// Use circuit-compatible logic to check if the difference is non-negative
const isValid = difference.equals(Field(0)).or(difference.greaterThanOrEqual(Field(0)))
? Field(1)
: Field(0);
// Set the validity of the verification result
this.valid.set(isValid);
}
}
The below script is a test suite that interacts with the AgeVerification zkApp. It invokes the prover logic during txn.prove() and verifies the zkApp's behavior by checking its updated state.
The actual prover function lies in the underlying zkApp method (verifyAge), and txn.prove() is the mechanism to generate the proof during testing.
To test the inputs I have edited the test script as follows.
import { AccountUpdate, Field, Mina, PrivateKey, PublicKey } from 'o1js';
import { AgeVerification } from './AgeVerification'; // Import the correct contract
let proofsEnabled = false;
describe('AgeVerification', () => {
let deployerAccount: Mina.TestPublicKey,
deployerKey: PrivateKey,
senderAccount: Mina.TestPublicKey,
senderKey: PrivateKey,
zkAppAddress: PublicKey,
zkAppPrivateKey: PrivateKey,
zkApp: AgeVerification; // Update to use AgeVerification instead of Add
beforeAll(async () => {
if (proofsEnabled) await AgeVerification.compile(); // Update compile for AgeVerification
});
beforeEach(async () => {
const Local = await Mina.LocalBlockchain({ proofsEnabled });
Mina.setActiveInstance(Local);
[deployerAccount, senderAccount] = Local.testAccounts;
let feePayer = Local.testAccounts[0].key;
deployerKey = deployerAccount.key;
senderKey = senderAccount.key;
zkAppPrivateKey = PrivateKey.random();
zkAppAddress = zkAppPrivateKey.toPublicKey();
zkApp = new AgeVerification(zkAppAddress); // Instantiate AgeVerification contract
});
async function localDeploy() {
const txn = await Mina.transaction(deployerAccount, async () => {
AccountUpdate.fundNewAccount(deployerAccount);
await zkApp.deploy();
});
await txn.prove();
// this tx needs .sign(), because `deploy()` adds an account update that requires signature authorization
await txn.sign([deployerKey, zkAppPrivateKey]).send();
}
it('generates and deploys the `AgeVerification` smart contract', async () => {
await localDeploy();
const valid = zkApp.valid.get(); // Access the 'valid' state variable
expect(valid).toEqual(Field(0)); // Initially, the contract should set 'valid' to Field(0)
});
it('correctly verifies the age in the `AgeVerification` smart contract', async () => {
await localDeploy();
const age = Field(25); // Example age value
const threshold = Field(18); // Example threshold value
// Call the verifyAge method
const txn = await Mina.transaction(senderAccount, async () => {
await zkApp.verifyAge(age, threshold); // Use the verifyAge method
});
await txn.prove();
await txn.sign([senderKey]).send();
const valid = zkApp.valid.get(); // Check the validity state after verification
expect(valid).toEqual(Field(1)); // Expected to be valid if age >= threshold
});
});
Below is the testing result
I have added the prover mechanism in interact.ts file which basically generates a zk-SNARK proof and submits the proof when it goes for transaction in mina blockchain. While the interact.ts script generates the proof, the verification is performed by the Mina blockchain when the transaction is processed. This is a key aspect of zk-SNARK systems that the prover generates a proof that the verifier (Mina network) checks.
import fs from 'fs/promises';
import { Mina, NetworkId, PrivateKey, Field } from 'o1js';
import { AgeVerification } from './AgeVerification';
// check command line arg
let deployAlias = process.argv[2];
if (!deployAlias)
throw Error(`Missing <deployAlias> argument.
Usage:
node build/src/interact.js <deployAlias>
`);
Error.stackTraceLimit = 1000;
const DEFAULT_NETWORK_ID = 'testnet';
// parse config and private key from file
type Config = {
deployAliases: Record<
string,
{
networkId?: string;
url: string;
keyPath: string;
fee: string;
feepayerKeyPath: string;
feepayerAlias: string;
}
>;
};
let configJson: Config = JSON.parse(await fs.readFile('config.json', 'utf8'));
let config = configJson.deployAliases[deployAlias];
let feepayerKeysBase58: { privateKey: string; publicKey: string } = JSON.parse(
await fs.readFile(config.feepayerKeyPath, 'utf8')
);
let zkAppKeysBase58: { privateKey: string; publicKey: string } = JSON.parse(
await fs.readFile(config.keyPath, 'utf8')
);
let feepayerKey = PrivateKey.fromBase58(feepayerKeysBase58.privateKey);
let zkAppKey = PrivateKey.fromBase58(zkAppKeysBase58.privateKey);
// set up Mina instance and contract we interact with
const Network = Mina.Network({
// We need to default to the testnet networkId if none is specified for this deploy alias in config.json
// This is to ensure the backward compatibility.
networkId: (config.networkId ?? DEFAULT_NETWORK_ID) as NetworkId,
mina: config.url,
});
const fee = Number(config.fee) * 1e9; // in nanomina (1 billion = 1.0 mina)
Mina.setActiveInstance(Network);
let feepayerAddress = feepayerKey.toPublicKey();
let zkAppAddress = zkAppKey.toPublicKey();
let zkApp = new AgeVerification(zkAppAddress);
let age = Field(25); // Example age
let threshold = Field(18); // Example threshold age
// compile the contract to create prover keys
console.log('compile the contract...');
await AgeVerification.compile();
try {
// call verifyAge() and send transaction
console.log('build transaction and create proof...');
let tx = await Mina.transaction(
{ sender: feepayerAddress, fee },
async () => {
await zkApp.verifyAge(age, threshold); // Replacing update() with verifyAge
}
);
await tx.prove();
console.log('send transaction...');
const sentTx = await tx.sign([feepayerKey]).send();
if (sentTx.status === 'pending') {
console.log(
'\nSuccess! Age verification transaction sent.\n' +
'\nYour smart contract state will be updated' +
`\nas soon as the transaction is included in a block:` +
`\n${getTxnUrl(config.url, sentTx.hash)}`
);
}
} catch (err) {
console.log(err);
}
function getTxnUrl(graphQlUrl: string, txnHash: string | undefined) {
const hostName = new URL(graphQlUrl).hostname;
const txnBroadcastServiceName = hostName
.split('.')
.filter((item) => item === 'minascan')?.[0];
const networkName = graphQlUrl
.split('/')
.filter((item) => item === 'mainnet' || item === 'devnet')?.[0];
if (txnBroadcastServiceName && networkName) {
return `https://minascan.io/${networkName}/tx/${txnHash}?type=zk-tx`;
}
return `Transaction hash: ${txnHash}`;
}
I have used inputs for age and threshold as 25 and 18.
Since testing has been done successfully by running npm run test
. I proceeded with deploying on devnet by using zk config
Where I provided following inputs:
Deploy alias: test
Network kind: testnet
URL: https://api.minascan.io/node/devnet/v1/graphql
Feepayer: New feepayer keys
Transaction: 0.1
The URL can be retrieved from here:
Then upon deployment I got the following response.
The contract is deployed at the following devnet
After deployment I proceeded with UI where I chose simple html, css and js by giving my RPC URL and deployed contract address and this is the final UI.
This concludes the creation of zkapp after integrating smart contract with UI. After building the user interface (UI) for the AgeVerification zkApp, the integration of the frontend with the smart contract allows users to interact seamlessly with the zero-knowledge proof system. The UI facilitates the submission of user age and threshold data to the contract, while maintaining privacy through zk-SNARKs. This enables users to verify their age without revealing the actual value, preserving confidentiality. The backend, utilizing the prover function, generates the proof, and the Mina blockchain verifies it efficiently. This end-to-end solution ensures a secure, user-friendly experience while taking full advantage of the privacy and scalability features offered by Mina's zk-SNARK-based architecture.
Top comments (0)