diff --git a/.gitignore b/.gitignore index c41b02b8b5..fcb737b159 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ tests/report/ tests/test-artifacts/ profiling.md snarkyjs-reference +*.tmp.js diff --git a/package.json b/package.json index c6040520a8..c6ada8e394 100644 --- a/package.json +++ b/package.json @@ -44,10 +44,11 @@ }, "scripts": { "type-check": "tsc --noEmit", - "dev": "npx tsc -p tsconfig.node.json && cp src/snarky.d.ts dist/node/snarky.d.ts", + "dev": "npx tsc -p tsconfig.node.json && node src/build/copy-to-dist.js", "make": "make -C ../../.. snarkyjs", + "make:no-types": "npm run clean && make -C ../../.. snarkyjs_no_types", "bindings": "cd ../../.. && ./scripts/update-snarkyjs-bindings.sh && cd src/lib/snarkyjs", - "build": "rimraf ./dist/node && npx tsc -p tsconfig.node.json && cp -r src/bindings/compiled/node_bindings dist/node/_node_bindings && node src/build/buildNode.js && cp src/snarky.d.ts dist/node/snarky.d.ts", + "build": "node src/build/copy-artifacts.js && rimraf ./dist/node && npm run dev && node src/build/buildNode.js", "build:test": "npx tsc -p tsconfig.test.json && cp src/snarky.d.ts dist/node/snarky.d.ts", "build:node": "npm run build", "build:web": "rimraf ./dist/web && node src/build/buildWeb.js", @@ -61,8 +62,8 @@ "bootstrap": "npm run build && node src/build/extractJsooMethods.cjs && npm run build", "format": "prettier --write --ignore-unknown **/*", "test": "./run-jest-tests.sh", - "clean": "rimraf ./dist", - "clean-all": "rimraf ./dist && rimraf ./tests/report && rimraf ./tests/test-artifacts", + "clean": "rimraf ./dist && rimraf ./src/bindings/compiled/_node_bindings", + "clean-all": "npm run clean && rimraf ./tests/report && rimraf ./tests/test-artifacts", "test:integration": "./run-integration-tests.sh", "test:unit": "./run-unit-tests.sh", "test:e2e": "rimraf ./tests/report && rimraf ./tests/test-artifacts && npx playwright test", diff --git a/run-minimal-mina-tests.sh b/run-minimal-mina-tests.sh new file mode 100755 index 0000000000..3aae238cce --- /dev/null +++ b/run-minimal-mina-tests.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -e + +./run src/tests/inductive-proofs-small.ts --bundle diff --git a/src/bindings b/src/bindings index fe3b5e6992..e32513d680 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit fe3b5e699279d19a0676102d90026c50e060e7e5 +Subproject commit e32513d68095ecf2c19ee075989cb3c37d837e91 diff --git a/src/build/buildExample.js b/src/build/buildExample.js index 286cf74e1f..a7f75c3038 100644 --- a/src/build/buildExample.js +++ b/src/build/buildExample.js @@ -32,7 +32,11 @@ async function build(srcPath, isWeb = false) { logLevel: 'error', plugins: isWeb ? [typescriptPlugin(tsConfig)] - : [typescriptPlugin(tsConfig), makeNodeModulesExternal()], + : [ + typescriptPlugin(tsConfig), + makeNodeModulesExternal(), + makeJsooExternal(), + ], }); let absPath = path.resolve('.', outfile); @@ -105,6 +109,21 @@ function makeNodeModulesExternal() { }; } +function makeJsooExternal() { + let isJsoo = /(bc.cjs|plonk_wasm.cjs|wrapper.js)$/; + return { + name: 'plugin-external', + setup(build) { + build.onResolve({ filter: isJsoo }, ({ path: filePath, resolveDir }) => { + return { + path: path.resolve(resolveDir, filePath), + external: true, + }; + }); + }, + }; +} + function findTsConfig() { let tsConfigPath = ts.findConfigFile(process.cwd(), ts.sys.fileExists); if (tsConfigPath === undefined) return; diff --git a/src/build/copy-artifacts.js b/src/build/copy-artifacts.js new file mode 100644 index 0000000000..116abfab56 --- /dev/null +++ b/src/build/copy-artifacts.js @@ -0,0 +1,9 @@ +// copy compiled jsoo/wasm artifacts from a folder in the source tree to the folder where they are imported from +// (these are not the same folders so that we don't automatically pollute the source tree when rebuilding artifacts) +import { copyFromTo } from './utils.js'; + +await copyFromTo( + ['src/bindings/compiled/node_bindings/'], + 'node_bindings', + '_node_bindings' +); diff --git a/src/build/copy-to-dist.js b/src/build/copy-to-dist.js new file mode 100644 index 0000000000..96bc96937b --- /dev/null +++ b/src/build/copy-to-dist.js @@ -0,0 +1,8 @@ +// copy some files from /src to /dist/node that tsc doesn't copy because we have .d.ts files for them +import { copyFromTo } from './utils.js'; + +await copyFromTo( + ['src/snarky.d.ts', 'src/bindings/compiled/_node_bindings'], + 'src/', + 'dist/node/' +); diff --git a/src/build/utils.js b/src/build/utils.js new file mode 100644 index 0000000000..346ba82a0d --- /dev/null +++ b/src/build/utils.js @@ -0,0 +1,16 @@ +import fse from 'fs-extra'; + +export { copyFromTo }; + +function copyFromTo(files, srcDir = undefined, targetDir = undefined) { + return Promise.all( + files.map((source) => { + let target = source.replace(srcDir, targetDir); + return fse.copy(source, target, { + recursive: true, + overwrite: true, + dereference: true, + }); + }) + ); +} diff --git a/src/index.ts b/src/index.ts index b33f1d5539..0813d3da4d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,19 +1,22 @@ -export { ProvablePure, Ledger } from './snarky.js'; +export type { ProvablePure } from './snarky.js'; +export { Ledger } from './snarky.js'; export { Field, Bool, Group, Scalar } from './lib/core.js'; export { Poseidon, TokenSymbol } from './lib/hash.js'; export * from './lib/signature.js'; +export type { + ProvableExtended, + FlexibleProvable, + FlexibleProvablePure, + InferProvable, +} from './lib/circuit_value.js'; export { CircuitValue, - ProvableExtended, prop, arrayProp, matrixProp, provable, provablePure, Struct, - FlexibleProvable, - FlexibleProvablePure, - InferProvable, } from './lib/circuit_value.js'; export { Provable } from './lib/provable.js'; export { Circuit, Keypair, public_, circuitMain } from './lib/circuit.js'; @@ -21,21 +24,22 @@ export { UInt32, UInt64, Int64, Sign } from './lib/int.js'; export { Types } from './bindings/mina-transaction/types.js'; export * as Mina from './lib/mina.js'; +export type { DeployArgs } from './lib/zkapp.js'; export { SmartContract, method, - DeployArgs, declareMethods, Account, VerificationKey, Reducer, } from './lib/zkapp.js'; export { state, State, declareState } from './lib/state.js'; + +export type { JsonProof } from './lib/proof_system.js'; export { Proof, SelfProof, verify, - JsonProof, Empty, Undefined, Void, @@ -49,13 +53,13 @@ export { ZkappPublicInput, } from './lib/account_update.js'; +export type { TransactionStatus } from './lib/fetch.js'; export { fetchAccount, fetchLastBlock, fetchTransactionStatus, checkZkappTransaction, fetchEvents, - TransactionStatus, addCachedAccount, setGraphqlEndpoint, setGraphqlEndpoints, diff --git a/src/lib/circuit_value.ts b/src/lib/circuit_value.ts index b5d6a88112..3c990ee37f 100644 --- a/src/lib/circuit_value.ts +++ b/src/lib/circuit_value.ts @@ -6,6 +6,8 @@ import { provablePure, HashInput, NonMethods, +} from '../bindings/lib/provable-snarky.js'; +import type { InferJson, InferProvable, InferredProvable, diff --git a/src/tests/inductive-proofs-small.ts b/src/tests/inductive-proofs-small.ts new file mode 100644 index 0000000000..271a2bb6cc --- /dev/null +++ b/src/tests/inductive-proofs-small.ts @@ -0,0 +1,88 @@ +import { + SelfProof, + Field, + Experimental, + isReady, + shutdown, + Proof, +} from '../index.js'; +import { tic, toc } from '../examples/zkapps/tictoc.js'; + +await isReady; + +let MaxProofsVerifiedOne = Experimental.ZkProgram({ + publicInput: Field, + + methods: { + baseCase: { + privateInputs: [], + + method(publicInput: Field) { + publicInput.assertEquals(Field(0)); + }, + }, + + mergeOne: { + privateInputs: [SelfProof], + + method(publicInput: Field, earlierProof: SelfProof) { + earlierProof.verify(); + earlierProof.publicInput.add(1).assertEquals(publicInput); + }, + }, + }, +}); + +tic('compiling program'); +await MaxProofsVerifiedOne.compile(); +toc(); + +await testRecursion(MaxProofsVerifiedOne, 1); + +async function testRecursion( + Program: typeof MaxProofsVerifiedOne, + maxProofsVerified: number +) { + console.log(`testing maxProofsVerified = ${maxProofsVerified}`); + + let ProofClass = Experimental.ZkProgram.Proof(Program); + + tic('executing base case'); + let initialProof = await Program.baseCase(Field(0)); + toc(); + initialProof = testJsonRoundtrip(ProofClass, initialProof); + initialProof.verify(); + initialProof.publicInput.assertEquals(Field(0)); + + if (initialProof.maxProofsVerified != maxProofsVerified) { + throw Error( + `Expected initialProof to have maxProofsVerified = ${maxProofsVerified} but has ${initialProof.maxProofsVerified}` + ); + } + + let p1; + if (initialProof.maxProofsVerified === 0) return; + + tic('executing mergeOne'); + p1 = await Program.mergeOne(Field(1), initialProof); + toc(); + p1 = testJsonRoundtrip(ProofClass, p1); + p1.verify(); + p1.publicInput.assertEquals(Field(1)); + if (p1.maxProofsVerified != maxProofsVerified) { + throw Error( + `Expected p1 to have maxProofsVerified = ${maxProofsVerified} but has ${p1.maxProofsVerified}` + ); + } +} + +function testJsonRoundtrip(ProofClass: any, proof: Proof) { + let jsonProof = proof.toJSON(); + console.log( + 'json roundtrip', + JSON.stringify({ ...jsonProof, proof: jsonProof.proof.slice(0, 10) + '..' }) + ); + return ProofClass.fromJSON(jsonProof); +} + +shutdown(); diff --git a/src/tests/inductive-proofs.ts b/src/tests/inductive-proofs.ts new file mode 100644 index 0000000000..61c1c9dc1d --- /dev/null +++ b/src/tests/inductive-proofs.ts @@ -0,0 +1,156 @@ +import { + SelfProof, + Field, + Experimental, + isReady, + shutdown, + Proof, +} from '../index.js'; +import { tic, toc } from '../examples/zkapps/tictoc.js'; + +await isReady; + +let MaxProofsVerifiedZero = Experimental.ZkProgram({ + publicInput: Field, + + methods: { + baseCase: { + privateInputs: [], + + method(publicInput: Field) { + publicInput.assertEquals(Field(0)); + }, + }, + }, +}); + +let MaxProofsVerifiedOne = Experimental.ZkProgram({ + publicInput: Field, + + methods: { + baseCase: { + privateInputs: [], + + method(publicInput: Field) { + publicInput.assertEquals(Field(0)); + }, + }, + + mergeOne: { + privateInputs: [SelfProof], + + method(publicInput: Field, earlierProof: SelfProof) { + earlierProof.verify(); + earlierProof.publicInput.add(1).assertEquals(publicInput); + }, + }, + }, +}); + +let MaxProofsVerifiedTwo = Experimental.ZkProgram({ + publicInput: Field, + + methods: { + baseCase: { + privateInputs: [], + + method(publicInput: Field) { + publicInput.assertEquals(Field(0)); + }, + }, + + mergeOne: { + privateInputs: [SelfProof], + + method(publicInput: Field, earlierProof: SelfProof) { + earlierProof.verify(); + earlierProof.publicInput.add(1).assertEquals(publicInput); + }, + }, + + mergeTwo: { + privateInputs: [SelfProof, SelfProof], + + method( + publicInput: Field, + p1: SelfProof, + p2: SelfProof + ) { + p1.verify(); + p1.publicInput.add(1).assertEquals(p2.publicInput); + p2.verify(); + p2.publicInput.add(1).assertEquals(publicInput); + }, + }, + }, +}); +tic('compiling three programs'); +await MaxProofsVerifiedZero.compile(); +await MaxProofsVerifiedOne.compile(); +await MaxProofsVerifiedTwo.compile(); +toc(); + +await testRecursion(MaxProofsVerifiedZero as any, 0); +await testRecursion(MaxProofsVerifiedOne as any, 1); +await testRecursion(MaxProofsVerifiedTwo, 2); + +async function testRecursion( + Program: typeof MaxProofsVerifiedTwo, + maxProofsVerified: number +) { + console.log(`testing maxProofsVerified = ${maxProofsVerified}`); + + let ProofClass = Experimental.ZkProgram.Proof(Program); + + tic('executing base case'); + let initialProof = await Program.baseCase(Field(0)); + toc(); + initialProof = testJsonRoundtrip(ProofClass, initialProof); + initialProof.verify(); + initialProof.publicInput.assertEquals(Field(0)); + + if (initialProof.maxProofsVerified != maxProofsVerified) { + throw Error( + `Expected initialProof to have maxProofsVerified = ${maxProofsVerified} but has ${initialProof.maxProofsVerified}` + ); + } + + let p1, p2; + if (initialProof.maxProofsVerified === 0) return; + + tic('executing mergeOne'); + p1 = await Program.mergeOne(Field(1), initialProof); + toc(); + p1 = testJsonRoundtrip(ProofClass, p1); + p1.verify(); + p1.publicInput.assertEquals(Field(1)); + if (p1.maxProofsVerified != maxProofsVerified) { + throw Error( + `Expected p1 to have maxProofsVerified = ${maxProofsVerified} but has ${p1.maxProofsVerified}` + ); + } + + if (initialProof.maxProofsVerified === 1) return; + tic('executing mergeTwo'); + p2 = await Program.mergeTwo(Field(2), initialProof, p1); + toc(); + p2 = testJsonRoundtrip(ProofClass, p2); + p2.verify(); + p2.publicInput.assertEquals(Field(2)); + if (p2.maxProofsVerified != maxProofsVerified) { + throw Error( + `Expected p2 to have maxProofsVerified = ${maxProofsVerified} but has ${p2.maxProofsVerified}` + ); + } +} + +function testJsonRoundtrip(ProofClass: any, proof: Proof) { + let jsonProof = proof.toJSON(); + console.log( + 'json roundtrip', + JSON.stringify({ ...jsonProof, proof: jsonProof.proof.slice(0, 10) + '..' }) + ); + return ProofClass.fromJSON(jsonProof); +} + +shutdown();