Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add simple ZkProgram use to Mina template #74

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions mina/contracts/mina/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "sudoku",
"name": "@game/mina-contracts",
"version": "0.1.0",
"description": "",
"author": "",
Expand All @@ -24,6 +24,10 @@
"lint:fix": "npm run lint -- --fix",
"start": "node build/src/run.js"
},
"dependencies": {
"o1js": "^1.3.1",
"viem": "^2.12.0"
},
"devDependencies": {
"@types/jest": "^29.5.11",
"@typescript-eslint/eslint-plugin": "^7.10.0",
Expand All @@ -35,9 +39,6 @@
"ts-jest": "^29.1.1",
"typescript": "^5.3.3"
},
"peerDependencies": {
"o1js": "^1.3.0"
},
"engines": {
"node": ">=18.14.0"
}
Expand Down
4 changes: 1 addition & 3 deletions mina/contracts/mina/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
import { SudokuZkApp } from './sudoku.js';

export { SudokuZkApp };
export { Sudoku, SudokuSolution, SudokuSolutionProof, SudokuZkApp } from './sudoku.js';
137 changes: 106 additions & 31 deletions mina/contracts/mina/src/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,18 @@
* Build the project: `$ npm run build`
* Run with node: `$ node build/src/run.js`.
*/
import { Sudoku, SudokuZkApp } from './sudoku.js';
import { Sudoku, SudokuSolution, SudokuSolutionProof, SudokuZkApp } from './sudoku.js';
import { cloneSudoku, generateSudoku, solveSudoku } from './sudoku-lib.js';
import { AccountUpdate, Lightnet, Mina, PrivateKey, PublicKey, fetchAccount } from 'o1js';
import { Abi, createPublicClient, createTestClient, createWalletClient, encodeFunctionData, getContract, http, toHex, walletActions } from 'viem';
import { anvil } from 'viem/chains';
import paimaL2Abi from '@paima/evm-contracts/abi/PaimaL2Contract.json' with { type: 'json' };
import assert from 'assert';
import { privateKeyToAccount } from 'viem/accounts';

console.log('Event names:', Object.keys(SudokuZkApp.events));
console.log('Compiling SudokuZkApp...');
console.log('Compiling SudokuSolution and SudokuZkApp...');
await SudokuSolution.compile();
await SudokuZkApp.compile();

/** Scaling factor from human-friendly MINA amount to raw integer fee amount. */
Expand All @@ -23,13 +29,60 @@ const MINA_TO_RAW_FEE = 1_000_000_000;
// ----------------------------------------------------------------------------
// Connect to Lightnet
const lightnetAccountManagerEndpoint = 'http://localhost:8181';
Mina.setActiveInstance(Mina.Network({
mina: 'http://localhost:8080/graphql',
lightnetAccountManager: lightnetAccountManagerEndpoint,
}));
Mina.setActiveInstance(
Mina.Network({
mina: 'http://localhost:8080/graphql',
lightnetAccountManager: lightnetAccountManagerEndpoint,
})
);

let lightnetAccount;
try {
const sudoku = generateSudoku(0.5);
const solution = solveSudoku(sudoku);
if (solution === undefined) throw Error('Failed to solve randomly generated puzzle');

// --------------------------------------------------------------------------
// Use a ZkProgram to prove the solution
console.log('Proving Sudoku solution...');
// ZkPrograms make recursion possible, and also allow proofs to be created
// and verified outside of the actual Mina blockchain transaction. We could
// serialize `JSON.stringify(proof.toJSON())` and send that wherever and the
// recipient could check it independently.
const proof = await SudokuSolution.solve(Sudoku.from(sudoku), Sudoku.from(solution));
const serializedProof = JSON.stringify(proof.toJSON());

console.log('serializedProof.length =', serializedProof.length);

console.log('Verifying deserialized proof...');
const deserializedProof = await SudokuSolutionProof.fromJSON(JSON.parse(serializedProof));
assert(await SudokuSolution.verify(deserializedProof));

// --------------------------------------------------------------------------
console.log('Posting proof to PaimaL2Contract');
{
const publicClient = createWalletClient({
// This is one of Hardhat's well-known test private keys.
account: privateKeyToAccount('0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'),
chain: anvil,
transport: http(),
});

const paimaL2 = getContract({
abi: paimaL2Abi,
address: '0x5FbDB2315678afecb367f032d93F642f64180aa3', // Good for localhost only
client: publicClient,
});

const hash = await paimaL2.write.paimaSubmitGameInput([toHex(`sp|${serializedProof}`)], {
value: 1n,
gas: 1000000n,
});
console.log('Submitted hash:', hash);
}

// ----------------------------------------------------------------------------
// Connect to localhost Lightnet
lightnetAccount = await Lightnet.acquireKeyPair({ lightnetAccountManagerEndpoint });
const { publicKey: sender, privateKey: senderKey } = lightnetAccount;

Expand Down Expand Up @@ -102,15 +155,17 @@ try {
// --------------------------------------------------------------------------
// Reset the puzzle

const sudoku = generateSudoku(0.5);
{
console.log('Resetting puzzle: preparing...');
const tx = await Mina.transaction({
sender,
fee: 0.01 * MINA_TO_RAW_FEE,
}, async () => {
await zkApp.update(Sudoku.from(sudoku));
});
const tx = await Mina.transaction(
{
sender,
fee: 0.01 * MINA_TO_RAW_FEE,
},
async () => {
await zkApp.update(Sudoku.from(sudoku));
}
);
console.log('Resetting puzzle: proving...');
await tx.prove();
console.log('Resetting puzzle: signing and sending...');
Expand All @@ -122,26 +177,41 @@ try {
await fetchAccount({ publicKey: zkApp.address });
console.log('Is the sudoku solved?', zkApp.isSolved.get().toBoolean());

let solution = solveSudoku(sudoku);
if (solution === undefined) throw Error('Failed to solve randomly generated puzzle');

// --------------------------------------------------------------------------
// Submit a wrong solution
let noSolution = cloneSudoku(solution);
const noSolution = cloneSudoku(solution);
noSolution[0][0] = (noSolution[0][0] % 9) + 1;

console.log('Submitting wrong solution...');
// Skip attempting to generate a SudokuSolution.solve proof for this solution
// because it breaks `zkApp.isSolved.get().toBoolean()` below... somehow
/*
console.log('Attempting to prove wrong solution...');
assert.rejects(async () => {
await SudokuSolution.solve(Sudoku.from(sudoku), Sudoku.from(noSolution));
});
*/

console.log('Attempting to submit invalid proof...');
const incorrectProof = new SudokuSolutionProof({
...proof,
// Make the correct proof invalid by attempting to repurpose it for a
// different Sudoku puzzle.
publicInput: Sudoku.from(generateSudoku(0.5)),
});
try {
let tx = await Mina.transaction({
sender,
fee: 0.01 * MINA_TO_RAW_FEE,
}, async () => {
await zkApp.submitSolution(Sudoku.from(sudoku), Sudoku.from(noSolution));
});
let tx = await Mina.transaction(
{
sender,
fee: 0.01 * MINA_TO_RAW_FEE,
},
async () => {
await zkApp.submitSolutionProof(incorrectProof);
}
);
await tx.prove();
await tx.sign([senderKey]).send();
} catch (err) {
console.log('There was an error submitting the solution, as expected', err);
console.log('There was an error submitting the solution, as expected');
}

await fetchAccount({ publicKey: zkApp.address });
Expand All @@ -151,12 +221,17 @@ try {
// Submit the actual solution
{
console.log('Submitting solution: preparing...');
const tx = await Mina.transaction({
sender,
fee: 0.01 * MINA_TO_RAW_FEE,
}, async () => {
await zkApp.submitSolution(Sudoku.from(sudoku), Sudoku.from(solution!));
});
const tx = await Mina.transaction(
{
sender,
fee: 0.01 * MINA_TO_RAW_FEE,
},
async () => {
// The proof object bundles the public input (puzzle to be solved) so we
// don't need to pass it again.
await zkApp.submitSolutionProof(proof);
}
);
console.log('Submitting solution: proving...');
await tx.prove();
console.log('Submitting solution: signing and sending...');
Expand Down
17 changes: 10 additions & 7 deletions mina/contracts/mina/src/sudoku.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Sudoku, SudokuZkApp } from './sudoku';
import { cloneSudoku, generateSudoku, solveSudoku } from './sudoku-lib';
import { Sudoku, SudokuSolution, SudokuZkApp } from './sudoku.js';
import { cloneSudoku, generateSudoku, solveSudoku } from './sudoku-lib.js';
import { PrivateKey, PublicKey, Mina, AccountUpdate } from 'o1js';

describe('sudoku', () => {
Expand All @@ -10,6 +10,11 @@ describe('sudoku', () => {
sender: Mina.TestPublicKey,
senderKey: PrivateKey;

it('compiles', async () => {
await SudokuSolution.compile();
await SudokuZkApp.compile();
});

beforeEach(async () => {
let Local = await Mina.LocalBlockchain({ proofsEnabled: false });
Mina.setActiveInstance(Local);
Expand All @@ -31,7 +36,8 @@ describe('sudoku', () => {
if (solution === undefined) throw Error('cannot happen');
let tx = await Mina.transaction(sender, async () => {
let zkApp = new SudokuZkApp(zkAppAddress);
await zkApp.submitSolution(Sudoku.from(sudoku), Sudoku.from(solution!));
let proof = await SudokuSolution.solve(Sudoku.from(sudoku), Sudoku.from(solution));
await zkApp.submitSolutionProof(proof);
});
await tx.prove();
await tx.sign([senderKey]).send();
Expand All @@ -52,10 +58,7 @@ describe('sudoku', () => {
await expect(async () => {
let tx = await Mina.transaction(sender, async () => {
let zkApp = new SudokuZkApp(zkAppAddress);
await zkApp.submitSolution(
Sudoku.from(sudoku),
Sudoku.from(noSolution)
);
await zkApp.submitSolution(Sudoku.from(sudoku), Sudoku.from(noSolution));
});
await tx.prove();
await tx.sign([senderKey]).send();
Expand Down
Loading