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 in-browser delegation demo to Mina template #81

Draft
wants to merge 41 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
2f070be
Add simple ZkProgram use to Mina template
SpaceManiac Jun 10, 2024
22e6e69
Give contract both submitSolution and submitSolutionProof methods
SpaceManiac Jun 11, 2024
1fd7aef
Submit Mina proof to Hardhat EVM
SpaceManiac Jun 13, 2024
5e220fc
Verify the Sudoku proof on the state transition side
SpaceManiac Jun 13, 2024
7446d9d
Upgrade to o1js v1.3.1
SpaceManiac Jun 13, 2024
c455d01
Use forceRecompile to fix hang on verify
SpaceManiac Jun 13, 2024
8791d1e
Add first sketch of proving an ECDSA signature
SpaceManiac Jun 18, 2024
12b960e
Add secondary proof and no-op proof for speed comparison
SpaceManiac Jun 20, 2024
84a86e3
Add double-MerkleTree contract
SpaceManiac Jun 20, 2024
ede2978
Clean up duplication in DelegationOrder code
SpaceManiac Jun 21, 2024
5571a31
Get circuit size manageable by using just one MerkleTree layer
SpaceManiac Jun 21, 2024
bc70a23
Fix errors by adding missing awaits, improve formatting
SpaceManiac Jun 21, 2024
aac6b5c
Split contracts/run.ts into cli/ package
SpaceManiac Jun 27, 2024
205ad3b
Replace DelegationOrder code with @paima/mina-delegation import
SpaceManiac Jul 3, 2024
465b19b
Add basic frontend with headers for o1js
SpaceManiac Jul 8, 2024
451c140
Shove compile() call into web worker
SpaceManiac Jul 9, 2024
372acaf
Use new middleware to sign binary DelegationOrder
SpaceManiac Jul 9, 2024
7714f44
Add OPFS cache
SpaceManiac Jul 9, 2024
1ebcac7
Update to o1js 1.4.0
SpaceManiac Jul 9, 2024
5cfff2e
Regen package-lock.json to fix o1js duplication
SpaceManiac Jul 9, 2024
dde5191
Fix issues preventing actually arriving at a proof
SpaceManiac Jul 9, 2024
0de87b4
Enable middleware source maps
SpaceManiac Jul 16, 2024
f510735
Pass proof json to worker and back
SpaceManiac Jul 16, 2024
77ead50
Improve logging/timing
SpaceManiac Jul 16, 2024
599b82e
Update CLI
SpaceManiac Jul 17, 2024
ba0a5cd
Use delegateEvmToMina constructor
SpaceManiac Jul 19, 2024
99c2a95
Click & Moo demo assets
SpaceManiac Jul 23, 2024
8d5a4ef
Begin splitting key->signature->proof process into a library
SpaceManiac Jul 23, 2024
323a996
Revert "Click & Moo demo assets"
SpaceManiac Jul 23, 2024
35880a1
Add second worker.js output to middleware build
SpaceManiac Jul 24, 2024
ed78f96
Fix formatting of cli/package.json
SpaceManiac Jul 25, 2024
47e749f
Finish stuffing compile + prove into web worker
SpaceManiac Jul 25, 2024
237c6ec
Remove openIndexdDB, isHex, and leftover console.logs
SpaceManiac Jul 25, 2024
e9df55d
Remove OpfsCache
SpaceManiac Jul 25, 2024
fccb8fe
Lazy-initialize worker and pass in prefix
SpaceManiac Jul 25, 2024
132a543
Restore logging to UI, improve class namign
SpaceManiac Jul 25, 2024
fc7a411
Rename worker.ts to mina.worker.ts
SpaceManiac Jul 26, 2024
d5c2527
s/DelegationOrder/DelegationCommand/
SpaceManiac Jul 26, 2024
ab3d738
Pre-bundle Mina worker into the middleware build tree, for libraryfic…
SpaceManiac Jul 29, 2024
1a4270f
Import MinaDelegationCache from @paima/providers
SpaceManiac Jul 30, 2024
f350145
Upgrade to o1js v1.6.0
SpaceManiac Jul 30, 2024
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
19 changes: 19 additions & 0 deletions mina/cli/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "@game/cli",
"version": "0.1.0",
"type": "module",
"main": "build/src/index.js",
"types": "build/src/index.d.ts",
"scripts": {
"build": "tsc",
"prestart": "npm run build",
"start": "node --enable-source-maps build/run.js"
},
"dependencies": {
"o1js": "^1.6.0",
"viem": "^2.12.0"
},
"devDependencies": {
"typescript": "^5.3.3"
}
}
174 changes: 139 additions & 35 deletions mina/contracts/mina/src/run.ts → mina/cli/src/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,115 @@
* Build the project: `$ npm run build`
* Run with node: `$ node build/src/run.js`.
*/
import { Sudoku, SudokuZkApp } from './sudoku.js';
import { cloneSudoku, generateSudoku, solveSudoku } from './sudoku-lib.js';
import { AccountUpdate, Lightnet, Mina, PrivateKey, PublicKey, fetchAccount } from 'o1js';
import { delegateEvmToMina, Ecdsa, Secp256k1, Sudoku, SudokuSolution, SudokuSolutionProof, SudokuZkApp, cloneSudoku, generateSudoku, solveSudoku } from '@game/mina-contracts';
import paimaL2Abi from '@paima/evm-contracts/abi/PaimaL2Contract.json' with { type: 'json' };
import assert from 'assert';
import { AccountUpdate, Lightnet, Mina, PrivateKey, PublicKey, fetchAccount, verify } from 'o1js';
import { createWalletClient, getContract, http, toHex } from 'viem';
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
import { anvil } from 'viem/chains';

console.log('Event names:', Object.keys(SudokuZkApp.events));
console.log('Compiling SudokuZkApp...');
await SudokuZkApp.compile();
const { DelegationCommand, DelegationCommandProgram, DelegationCommandProof } = delegateEvmToMina('Test: ');

/** Scaling factor from human-friendly MINA amount to raw integer fee amount. */
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,
})
);

// ----------------------------------------------------------------------------
// Compile
console.log('Event names:', Object.keys(SudokuZkApp.events));
console.log('Compiling ...');
console.time('compile');
//await SudokuSolution.compile();
//await SudokuZkApp.compile();
const { verificationKey } = await DelegationCommandProgram.compile();
console.timeEnd('compile');

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

await Mina.waitForFunding(sender.toBase58());
console.log('Sender balance:', Mina.activeInstance.getAccount(sender).balance.toBigInt());

// ----------------------------------------------------------------------------
const viemAccount = privateKeyToAccount(generatePrivateKey());
const delegationOrder = new DelegationCommand({
target: sender,
signer: Secp256k1.fromHex(viemAccount.publicKey),
});

const delegationSignature = Ecdsa.fromHex(await viemAccount.signMessage({ message: { raw: DelegationCommand.bytesToSign(delegationOrder) } }));

console.time('DelegationCommandProgram.sign');
const delegateProof = await DelegationCommandProgram.sign(
delegationOrder,
delegationSignature,
);
console.timeEnd('DelegationCommandProgram.sign');

console.time('DelegationCommandProgram.verify');
console.log(await DelegationCommandProgram.verify(delegateProof));
//console.log(await verify(delegateProof, verificationKey));
//console.log(await verify(delegateProof.toJSON(), verificationKey));
console.timeEnd('DelegationCommandProgram.verify');

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

// ----------------------------------------------------------------------------
// Initialize our SudokuZkApp instance pointing to the preordained address.
// In a real project, DON'T hardcode keys.
Expand Down Expand Up @@ -102,15 +184,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 +206,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 +250,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
19 changes: 19 additions & 0 deletions mina/cli/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "build",

"target": "es2020",
"module": "ESNext",
"moduleResolution": "Node",
},
"include": ["src/**/*"],
"references": [
{ "path": "../utils" },
{ "path": "../game-logic" },
{ "path": "../api" },
{ "path": "../db" },
{ "path": "../contracts/mina" }
]
}
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": {
"@paima/mina-delegation": "^3.1.0",
"o1js": "^1.6.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
7 changes: 4 additions & 3 deletions mina/contracts/mina/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { SudokuZkApp } from './sudoku.js';

export { SudokuZkApp };
export { Sudoku, SudokuSolution, SudokuSolutionProof, SudokuZkApp } from './sudoku.js';
export { cloneSudoku, generateSudoku, solveSudoku } from './sudoku-lib.js';
export * from '@paima/mina-delegation';
export type * from '@paima/mina-delegation';
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