diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e6ec2eb0b..67ca404a7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,13 +20,13 @@ This project adheres to ### Added -- Native Kimchi access from Node.js with x2 prover performance boost. +- Native Kimchi access from Node.js with x2 prover performance boost. https://github.com/o1-labs/o1js/pull/2823 - Enabled chunking through Native prover enabling x4 larger circuits on Node.js. https://github.com/o1-labs/o1js/pull/2823 - Expose `Client.getZkappCommandCommitments` and - `Client.getZkappCommandCommitmentsFromJSON` on `mina-signer` for computing - the commitment and full commitment of a zkApp transaction. + `Client.getZkappCommandCommitmentsFromJSON` on `mina-signer` for computing the + commitment and full commitment of a zkApp transaction. https://github.com/o1-labs/o1js/pull/2869 ## [3.0.0-mesa.0](https://github.com/o1-labs/o1js/compare/ff6c201b...v3.0.0-mesa.0) - 2026-03-26 diff --git a/flake.nix b/flake.nix index 344cc9ac8e..ca989107b7 100644 --- a/flake.nix +++ b/flake.nix @@ -286,6 +286,8 @@ name = "export_test_vectors"; version = "0.1.0"; CARGO_TARGET_DIR = "./target"; + cargoBuildFlags = [ "--features" "o1-utils/std" ]; + cargoTestFlags = [ "--features" "o1-utils/std" ]; cargoLock = { lockFile = ./src/mina/src/lib/crypto/proof-systems/Cargo.lock; outputHashes = narHashesFromCargoLock diff --git a/npmDepsHash b/npmDepsHash index f323861914..c90e8ee219 100644 --- a/npmDepsHash +++ b/npmDepsHash @@ -1 +1 @@ -sha256-ZCfyinpdAHM7LTcyCjOqbKc9AI3G41pDV6BSHM39HI0= +sha256-NYTcfClNuqJNCQgC4/YPyMt6Z3umTwo4w+pn4MHQ5Ts= diff --git a/package-lock.json b/package-lock.json index aadf104906..7852a859f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "o1js", - "version": "3.0.0-mesa.final", + "version": "3.0.0-mesa.final.pr2888.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "o1js", - "version": "3.0.0-mesa.final", + "version": "3.0.0-mesa.final.pr2888.0", "license": "Apache-2.0", "dependencies": { "@noble/hashes": "^1.7.1", @@ -56,32 +56,9 @@ "node": ">=22.19.5" }, "optionalDependencies": { - "@o1js/native": "3.0.0-mesa.final" + "@o1js/native": "3.0.0-mesa.final.pr2888.0" } }, - "native/darwin-arm64": { - "name": "@o1js/native-darwin-arm64", - "version": "0.0.0", - "cpu": [ - "arm64" - ], - "extraneous": true, - "os": [ - "darwin" - ] - }, - "native/darwin-x64": { - "extraneous": true - }, - "native/linux-arm64": { - "extraneous": true - }, - "native/linux-x64": { - "extraneous": true - }, - "native/win32-x64": { - "extraneous": true - }, "node_modules/@ampproject/remapping": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", @@ -3407,7 +3384,54 @@ } }, "node_modules/@o1js/native": { - "optional": true + "version": "3.0.0-mesa.final.pr2888.0", + "optional": true, + "optionalDependencies": { + "@o1js/native-darwin-arm64": "3.0.0-mesa.final.pr2888.0", + "@o1js/native-darwin-x64": "3.0.0-mesa.final.pr2888.0", + "@o1js/native-linux-arm64": "3.0.0-mesa.final.pr2888.0", + "@o1js/native-linux-x64": "3.0.0-mesa.final.pr2888.0" + } + }, + "node_modules/@o1js/native-darwin-arm64": { + "version": "3.0.0-mesa.final.pr2888.0", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@o1js/native-darwin-x64": { + "version": "3.0.0-mesa.final.pr2888.0", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@o1js/native-linux-arm64": { + "version": "3.0.0-mesa.final.pr2888.0", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@o1js/native-linux-x64": { + "version": "3.0.0-mesa.final.pr2888.0", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] }, "node_modules/@octokit/action": { "version": "6.1.0", diff --git a/package.json b/package.json index 31c303c715..60ef99d9de 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "o1js", "description": "TypeScript framework for zk-SNARKs and zkApps", - "version": "3.0.0-mesa.final", + "version": "3.0.0-mesa.final.pr2888.0", "license": "Apache-2.0", "homepage": "https://github.com/o1-labs/o1js/", "repository": { @@ -83,6 +83,7 @@ "clean": "./scripts/clean/clean.sh", "clean:artifacts": "./scripts/clean/clean-artifacts.sh", "clean:all": "./scripts/clean/clean-all.sh", + "cleanPackageLock": "./scripts/clean/clean-package-lock.sh", "lint": "oxlint", "lint:fix": "oxlint --fix --fix-suggestions", "lint:strict": "oxlint --max-warnings 0", @@ -145,9 +146,9 @@ "tslib": "^2.3.0" }, "o1js-internal": { - "native-version": "3.0.0-mesa.final" + "native-version": "3.0.0-mesa.final.pr2888.0" }, "optionalDependencies": { - "@o1js/native": "3.0.0-mesa.final" + "@o1js/native": "3.0.0-mesa.final.pr2888.0" } } diff --git a/run-ci-tests.sh b/run-ci-tests.sh index 76d21e2af1..5590f75668 100755 --- a/run-ci-tests.sh +++ b/run-ci-tests.sh @@ -10,6 +10,7 @@ case $TEST_TYPE in ./run src/examples/zkapps/composability.ts --bundle ./run src/tests/fake-proof.ts ./run src/tests/inductive-proofs-internal.ts --bundle + ./run src/tests/mesa-minimal-large-method-repro.ts --bundle ./run tests/vk-regression/diverse-zk-program-run.ts --bundle ;; diff --git a/scripts/check-version-sync.mjs b/scripts/check-version-sync.mjs index aeee0c81b2..3927ac0d5a 100644 --- a/scripts/check-version-sync.mjs +++ b/scripts/check-version-sync.mjs @@ -8,7 +8,15 @@ import { readFileSync } from 'node:fs'; +const nativePackages = [ + ['@o1js/native-darwin-arm64', { cpu: ['arm64'], os: ['darwin'] }], + ['@o1js/native-darwin-x64', { cpu: ['x64'], os: ['darwin'] }], + ['@o1js/native-linux-arm64', { cpu: ['arm64'], os: ['linux'] }], + ['@o1js/native-linux-x64', { cpu: ['x64'], os: ['linux'] }], +]; + let pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')); +let lock = JSON.parse(readFileSync(new URL('../package-lock.json', import.meta.url), 'utf8')); let rootVersion = pkg.version; let nativeVersion = pkg['o1js-internal']?.['native-version']; @@ -23,12 +31,62 @@ if (optionalNative !== rootVersion) { `optionalDependencies["@o1js/native"] (${optionalNative}) !== version (${rootVersion})` ); } +if (lock.version !== rootVersion) { + mismatches.push(`package-lock.json version (${lock.version}) !== version (${rootVersion})`); +} +if (lock.packages?.['']?.version !== rootVersion) { + mismatches.push( + `package-lock packages[""].version (${lock.packages?.['']?.version}) !== version (${rootVersion})` + ); +} +if (lock.packages?.['']?.optionalDependencies?.['@o1js/native'] !== rootVersion) { + mismatches.push( + `package-lock packages[""].optionalDependencies["@o1js/native"] (${lock.packages?.['']?.optionalDependencies?.['@o1js/native']}) !== version (${rootVersion})` + ); +} + +let nativeLock = lock.packages?.['node_modules/@o1js/native']; +if (nativeLock?.version !== rootVersion) { + mismatches.push( + `package-lock node_modules/@o1js/native version (${nativeLock?.version}) !== version (${rootVersion})` + ); +} +if (nativeLock?.optional !== true) { + mismatches.push('package-lock node_modules/@o1js/native must be optional'); +} +for (let [name, target] of nativePackages) { + if (nativeLock?.optionalDependencies?.[name] !== rootVersion) { + mismatches.push( + `package-lock node_modules/@o1js/native optionalDependencies["${name}"] (${nativeLock?.optionalDependencies?.[name]}) !== version (${rootVersion})` + ); + } + + let entry = lock.packages?.[`node_modules/${name}`]; + if (entry?.version !== rootVersion) { + mismatches.push( + `package-lock node_modules/${name} version (${entry?.version}) !== version (${rootVersion})` + ); + } + if (entry?.optional !== true) { + mismatches.push(`package-lock node_modules/${name} must be optional`); + } + if (JSON.stringify(entry?.cpu) !== JSON.stringify(target.cpu)) { + mismatches.push( + `package-lock node_modules/${name} cpu (${JSON.stringify(entry?.cpu)}) !== ${JSON.stringify(target.cpu)}` + ); + } + if (JSON.stringify(entry?.os) !== JSON.stringify(target.os)) { + mismatches.push( + `package-lock node_modules/${name} os (${JSON.stringify(entry?.os)}) !== ${JSON.stringify(target.os)}` + ); + } +} if (mismatches.length > 0) { - console.error('version drift detected in package.json:'); + console.error('version drift detected in package.json/package-lock.json:'); for (let m of mismatches) console.error(` - ${m}`); console.error('\nrun `npm run sync:native-version` to fix.'); process.exit(1); } -console.log(`ok: o1js, @o1js/native, native-version all = ${rootVersion}`); +console.log(`ok: o1js, @o1js/native, native-version, package-lock all = ${rootVersion}`); diff --git a/scripts/clean/clean-package-lock.sh b/scripts/clean/clean-package-lock.sh new file mode 100755 index 0000000000..bca27d36a6 --- /dev/null +++ b/scripts/clean/clean-package-lock.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Strip the gitignored, locally-built native/ packages out of +# package-lock.json WITHOUT changing any dependency versions. +# +# `npm run build:native` produces a local @o1js/native- package +# (version 0.0.0, os/cpu-locked) under ./native//. A subsequent `npm +# install` records it in the lockfile as an `extraneous` "native/" entry. +# On another platform `npm ci` then tries to install e.g. +# @o1js/native-darwin-arm64 and fails with EBADPLATFORM (this breaks linux CI). +# Only the registry @o1js/native-* packages (optional: true) belong in the lock. +# +# This deletes just those `native/` lockfile entries. It deliberately does +# NOT `rm package-lock.json` + reinstall: that would re-resolve every ^/~ range +# to its newest version (e.g. typescript 5.4 -> 5.9) and break unrelated builds. +set -Eeuo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +cd "$ROOT" + +echo "stripping local native/* entries from package-lock.json..." +node -e ' + const fs = require("fs"); + const l = JSON.parse(fs.readFileSync("package-lock.json", "utf8")); + let removed = []; + for (const k of Object.keys(l.packages ?? {})) { + if (/^native\//.test(k)) { delete l.packages[k]; removed.push(k); } + } + fs.writeFileSync("package-lock.json", JSON.stringify(l, null, 2) + "\n"); + console.log(removed.length ? "removed: " + removed.join(", ") : "nothing to strip"); +' + +# fail loudly if anything local-native remains +if grep -Eq '"native/[a-z0-9_-]+":|"extraneous": true' package-lock.json; then + echo "ERROR: package-lock.json still references local native/* packages:" >&2 + grep -nE '"native/[a-z0-9_-]+":|"extraneous": true' package-lock.json | head >&2 + exit 1 +fi + +# sanity: lockfile must still satisfy the npm ci sync check (the gate CI runs) +echo "verifying npm ci sync..." +npm ci --dry-run >/dev/null +echo "package-lock.json cleaned (no native/* leak, versions unchanged, npm-ci-consistent)" diff --git a/scripts/sync-native-version.mjs b/scripts/sync-native-version.mjs index de606c1dcb..d2e3218be1 100644 --- a/scripts/sync-native-version.mjs +++ b/scripts/sync-native-version.mjs @@ -1,9 +1,16 @@ #!/usr/bin/env node -// rewrites package.json so o1js-internal.native-version and +// rewrites package.json/package-lock.json so o1js-internal.native-version and // optionalDependencies["@o1js/native"] match the root `version`. // run after bumping the root version (e.g. npm version patch). -import { readFileSync, writeFileSync } from 'node:fs'; +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; + +const nativePackages = [ + ['@o1js/native-darwin-arm64', { cpu: ['arm64'], os: ['darwin'] }], + ['@o1js/native-darwin-x64', { cpu: ['x64'], os: ['darwin'] }], + ['@o1js/native-linux-arm64', { cpu: ['arm64'], os: ['linux'] }], + ['@o1js/native-linux-x64', { cpu: ['x64'], os: ['linux'] }], +]; let pkgPath = new URL('../package.json', import.meta.url); let raw = readFileSync(pkgPath, 'utf8'); @@ -32,3 +39,41 @@ if (changed) { } else { console.log(`already in sync at ${rootVersion}`); } + +let lockPath = new URL('../package-lock.json', import.meta.url); +if (existsSync(lockPath)) { + let lockRaw = readFileSync(lockPath, 'utf8'); + let lock = JSON.parse(lockRaw); + + lock.version = rootVersion; + lock.packages ??= {}; + + let rootPackage = (lock.packages[''] ??= {}); + rootPackage.version = rootVersion; + if (pkg.optionalDependencies?.['@o1js/native'] !== undefined) { + rootPackage.optionalDependencies ??= {}; + rootPackage.optionalDependencies['@o1js/native'] = rootVersion; + } + + lock.packages['node_modules/@o1js/native'] = { + version: rootVersion, + optional: true, + optionalDependencies: Object.fromEntries(nativePackages.map(([name]) => [name, rootVersion])), + }; + + for (let [name, target] of nativePackages) { + lock.packages[`node_modules/${name}`] = { + version: rootVersion, + cpu: target.cpu, + optional: true, + os: target.os, + }; + } + + let trailing = lockRaw.endsWith('\n') ? '\n' : ''; + let nextLockRaw = JSON.stringify(lock, null, 2) + trailing; + if (nextLockRaw !== lockRaw) { + writeFileSync(lockPath, nextLockRaw); + console.log(`synced package-lock native versions to ${rootVersion}`); + } +} diff --git a/src/lib/proof-system/proof.ts b/src/lib/proof-system/proof.ts index 6c8ea455f6..0223ad2ff6 100644 --- a/src/lib/proof-system/proof.ts +++ b/src/lib/proof-system/proof.ts @@ -1,26 +1,31 @@ -import { areBindingsInitialized, initializeBindings, withThreadPool } from '../../bindings.js'; -import { Pickles, Base64ProofString } from '../../bindings.js'; -import { Field, Bool } from '../provable/wrapped.js'; -import type { FlexibleProvable, InferProvable } from '../provable/types/struct.js'; -import { FeatureFlags } from './feature-flags.js'; -import type { JsonProof } from './zkprogram.js'; -import { Subclass } from '../util/types.js'; +import { + Base64ProofString, + Pickles, + areBindingsInitialized, + initializeBindings, + withThreadPool, +} from '../../bindings.js'; import type { Provable } from '../provable/provable.js'; -import { assert } from '../util/assert.js'; -import { Unconstrained } from '../provable/types/unconstrained.js'; import { ProvableType } from '../provable/types/provable-intf.js'; -import { ZkProgramContext } from './zkprogram-context.js'; +import type { FlexibleProvable, InferProvable } from '../provable/types/struct.js'; +import { Unconstrained } from '../provable/types/unconstrained.js'; +import { Bool, Field } from '../provable/wrapped.js'; +import { assert } from '../util/assert.js'; +import { Subclass } from '../util/types.js'; +import { FeatureFlags } from './feature-flags.js'; import { VerificationKey } from './verification-key.js'; +import { ZkProgramContext } from './zkprogram-context.js'; +import type { JsonProof } from './zkprogram.js'; // public API -export { ProofBase, Proof, DynamicProof, ProofClass }; +export { DynamicProof, Proof, ProofBase, ProofClass }; // internal API -export { dummyProof, extractProofs, extractProofTypes, type ProofValue }; +export { dummyProof, extractProofTypes, extractProofs, type ProofValue }; type MaxProofs = 0 | 1 | 2; -type ProofClass = Subclass; +type ProofClass = Subclass & { maxProofsVerified?: MaxProofs }; class ProofBase { static publicInputType: FlexibleProvable = undefined as any; diff --git a/src/lib/proof-system/zkprogram.ts b/src/lib/proof-system/zkprogram.ts index d870bce517..7eadefb4cf 100644 --- a/src/lib/proof-system/zkprogram.ts +++ b/src/lib/proof-system/zkprogram.ts @@ -225,13 +225,13 @@ type InferMethodType = { * public output, as well as defining the methods which can be executed provably. * @param config.numChunks Optional number of chunks to split each method's circuit into. Use a * value greater than 1 (1 < numChunks <= 4) if a method exceeds the single-circuit row limit - * of 2^16; default is 1. Up to 8 chunks are supported if the degree of the constraints of the + * of 2^16; default is 1. Up to 8 chunks are supported if the degree of the constraints of the * underlying circuit is low enough (e.g. generic gates) so the wrap domain can still be 2. * @param config.overrideWrapDomain Optional override for the wrap circuit domain (0 | 1 | 2). * Defaults to a value derived from the maximum proofs verified; set only if you need to force * a specific domain for chunking. In general, uses 0 if no chunking, 1 for 2 chunks, and 2 for - * 4 chunks. When otherwise needed, the logs guide you through the right choice. If the constraints - * are simple enough (e.g. generic gates), 8 chunks may also use domain 2. + * 4 chunks. When otherwise needed, the logs guide you through the right choice. If the constraints + * are simple enough (e.g. generic gates), 8 chunks may also use domain 2. * @returns an object that can be used to compile, prove, and verify the program. */ function ZkProgram< @@ -298,14 +298,14 @@ function ZkProgram< proofsEnabled: boolean; setProofsEnabled(proofsEnabled: boolean): void; } & { - [I in keyof Config['methods']]: Prover< - InferProvableOrUndefined>, - ProvableOrUndefined>, - InferProvableOrVoid>, - InferPrivateInput[I], - InferProvableOrUndefined[I]> - >; - } { + [I in keyof Config['methods']]: Prover< + InferProvableOrUndefined>, + ProvableOrUndefined>, + InferProvableOrVoid>, + InferPrivateInput[I], + InferProvableOrUndefined[I]> + >; +} { type PublicInputType = ProvableOrUndefined>; type PublicInput = InferProvableOrUndefined>; type PublicOutput = InferProvableOrVoid>; @@ -378,13 +378,13 @@ function ZkProgram< let compileOutput: | { - provers: Pickles.Prover[]; - maxProofsVerified: 0 | 1 | 2; - verify: ( - statement: Pickles.Statement, - proof: Pickles.Proof - ) => Promise; - } + provers: Pickles.Prover[]; + maxProofsVerified: 0 | 1 | 2; + verify: ( + statement: Pickles.Statement, + proof: Pickles.Proof + ) => Promise; + } | undefined; const programState = createProgramState(); @@ -472,7 +472,7 @@ function ZkProgram< if (compileOutput === undefined) { throw Error( `Cannot prove execution of program.${String(key)}(), no prover found. ` + - `Try calling \`await program.compile()\` first, this will cache provers in the background.\nIf you compiled your zkProgram with proofs disabled (\`proofsEnabled = false\`), you have to compile it with proofs enabled first.` + `Try calling \`await program.compile()\` first, this will cache provers in the background.\nIf you compiled your zkProgram with proofs disabled (\`proofsEnabled = false\`), you have to compile it with proofs enabled first.` ); } let picklesProver = compileOutput.provers[i]; @@ -644,7 +644,7 @@ type ZkProgram< * }); * ``` */ -class SelfProof extends Proof { } +class SelfProof extends Proof {} function sortMethodArguments( programName: string, @@ -674,7 +674,7 @@ function sortMethodArguments( if (proof === ProofBase || proof === Proof || proof === DynamicProof) { throw Error( `You cannot use the \`${proof.name}\` class directly. Instead, define a subclass:\n` + - `class MyProof extends ${proof.name} { ... }` + `class MyProof extends ${proof.name} { ... }` ); } }); @@ -683,7 +683,7 @@ function sortMethodArguments( if (numberOfProofs > 2) { throw Error( `${programName}.${methodName}() has more than two proof arguments, which is not supported.\n` + - `Suggestion: You can merge more than two proofs by merging two at a time in a binary tree.` + `Suggestion: You can merge more than two proofs by merging two at a time in a binary tree.` ); } return { methodName, args, auxiliaryType }; @@ -764,7 +764,26 @@ If you are using a SmartContract, make sure you are using the @method decorator. ); let maxProofs = computeMaxProofsVerified(proofs.map((p) => p.length)); - overrideWrapDomain ??= maxProofsToWrapDomain[maxProofs]; + let wrapDomain: 0 | 1 | 2 = maxProofsToWrapDomain[maxProofs]; + // `maxProofsToWrapDomain` maps 2 proofs -> wrap domain 1 (an optimization that + // assumes the verified proofs are themselves shallow). That is invalid when a + // 2-proof method verifies a proof which *itself* verifies 2 proofs: the wrap + // domain then has to be 2 (mirrors pickles' own `Wrap_domains.Make.f`, which + // takes max(max_proofs_verified, max_local_proofs_verified)). Only bump in the + // maxProofs === 2 case, where the heuristic deviates from the natural wrap + // domain. Circuits verifying 0 or 1 proofs keep their previous wrap domain, so + // existing circuits are left untouched (backwards compatible). + if (maxProofs === 2) { + let maxLocalProofs = computeMaxProofsVerified( + await Promise.all( + proofs.flatMap((methodProofs) => + methodProofs.map((Proof) => maxProofsVerifiedForProofClass(Proof, proofSystemTag)) + ) + ) + ); + wrapDomain = Math.max(wrapDomain, maxLocalProofs) as 0 | 1 | 2; + } + overrideWrapDomain ??= wrapDomain; let picklesCache: Pickles.Cache = [ 0, function read_(mlHeader) { @@ -998,7 +1017,7 @@ function picklesRuleFromFunction( if (verifiedProofs.length > 2) { throw Error( `${proofSystemTag.name}.${methodName}() has more than two proof arguments, which is not supported.\n` + - `Suggestion: You can merge more than two proofs by merging two at a time in a binary tree.` + `Suggestion: You can merge more than two proofs by merging two at a time in a binary tree.` ); } let proofsToVerify = verifiedProofs.map((Proof) => { @@ -1025,7 +1044,7 @@ function picklesRuleFromFunction( if (compiledTag === undefined) { throw Error( `${proofSystemTag.name}.compile() depends on ${tag.name}, but we cannot find compilation output for ${tag.name}.\n` + - `Try to run ${tag.name}.compile() first.` + `Try to run ${tag.name}.compile() first.` ); } return { isSelf: false, tag: compiledTag }; @@ -1049,6 +1068,13 @@ function computeMaxProofsVerified(proofs: number[]) { }, 0) as 0 | 1 | 2; } +async function maxProofsVerifiedForProofClass(Proof: ProofClass, proofSystemTag: { name: string }) { + let tag = Proof.tag() as { maxProofsVerified?: () => Promise<0 | 1 | 2> }; + if (tag === proofSystemTag) return 0; + if (Proof.maxProofsVerified !== undefined) return Proof.maxProofsVerified; + return (await tag.maxProofsVerified?.()) ?? 0; +} + function fromFieldVars(type: Provable, fields: MlFieldArray, auxData: any[] = []) { return type.fromFields(MlFieldArray.from(fields), auxData); } @@ -1124,10 +1150,10 @@ function Prover() { type Infer = T extends Subclass - ? InstanceType - : T extends ProvableType - ? InferProvableType - : never; + ? InstanceType + : T extends ProvableType + ? InferProvableType + : never; type TupleToInstances = { [I in keyof T]: Infer; @@ -1140,18 +1166,18 @@ type PrivateInput = ProvableType | Subclass; type MethodReturnType = PublicOutput extends void ? AuxiliaryOutput extends undefined - ? void - : { - auxiliaryOutput: AuxiliaryOutput; - } + ? void + : { + auxiliaryOutput: AuxiliaryOutput; + } : AuxiliaryOutput extends undefined - ? { - publicOutput: PublicOutput; - } - : { - publicOutput: PublicOutput; - auxiliaryOutput: AuxiliaryOutput; - }; + ? { + publicOutput: PublicOutput; + } + : { + publicOutput: PublicOutput; + auxiliaryOutput: AuxiliaryOutput; + }; type Method< PublicInput, @@ -1162,26 +1188,26 @@ type Method< }, > = PublicInput extends undefined ? { - method( - ...args: TupleToInstances - ): Promise< - MethodReturnType< - PublicOutput, - InferProvableOrUndefined> - > - >; - } + method( + ...args: TupleToInstances + ): Promise< + MethodReturnType< + PublicOutput, + InferProvableOrUndefined> + > + >; + } : { - method( - publicInput: PublicInput, - ...args: TupleToInstances - ): Promise< - MethodReturnType< - PublicOutput, - InferProvableOrUndefined> - > - >; - }; + method( + publicInput: PublicInput, + ...args: TupleToInstances + ): Promise< + MethodReturnType< + PublicOutput, + InferProvableOrUndefined> + > + >; + }; type RegularProver< PublicInput, @@ -1205,16 +1231,16 @@ type Prover< AuxiliaryOutput, > = PublicInput extends undefined ? (...args: TupleFrom) => Promise<{ - proof: Proof; - auxiliaryOutput: AuxiliaryOutput; - }> + proof: Proof; + auxiliaryOutput: AuxiliaryOutput; + }> : ( - publicInput: From, - ...args: TupleFrom - ) => Promise<{ - proof: Proof; - auxiliaryOutput: AuxiliaryOutput; - }>; + publicInput: From, + ...args: TupleFrom + ) => Promise<{ + proof: Proof; + auxiliaryOutput: AuxiliaryOutput; + }>; type ProvableOrUndefined = A extends undefined ? typeof Undefined : ToProvable; type ProvableOrVoid = A extends undefined ? typeof Void : ToProvable; @@ -1222,8 +1248,8 @@ type ProvableOrVoid = A extends undefined ? typeof Void : ToProvable; type InferProvableOrUndefined = A extends undefined ? undefined : A extends ProvableType - ? InferProvable - : InferProvable | undefined; + ? InferProvable + : InferProvable | undefined; type InferProvableOrVoid = A extends undefined ? void : InferProvable; type UnwrapPromise

= P extends Promise ? T : never; diff --git a/src/mina b/src/mina index 487e95365a..b26d0f9530 160000 --- a/src/mina +++ b/src/mina @@ -1 +1 @@ -Subproject commit 487e95365ac43f8210a80426361b7258d45b1bb6 +Subproject commit b26d0f95307bef300046435e598287d7784c2e47 diff --git a/src/tests/mesa-minimal-large-method-repro.ts b/src/tests/mesa-minimal-large-method-repro.ts new file mode 100644 index 0000000000..d7b7683e99 --- /dev/null +++ b/src/tests/mesa-minimal-large-method-repro.ts @@ -0,0 +1,240 @@ +import { + Bool, + Cache, + Field, + Permissions, + Poseidon, + Provable, + PublicKey, + SelfProof, + SmartContract, + State, + Struct, + UInt64, + ZkProgram, + method, + state, +} from 'o1js'; + +const BATCH_SIZE = 54; + +class PairInput extends Struct({ + start: Field, + end: Field, +}) {} + +const TwoProofProgram = ZkProgram({ + name: 'mesa-minimal-large-method-two-proof-program', + publicInput: PairInput, + publicOutput: Field, + methods: { + base: { + privateInputs: [], + async method(input: PairInput) { + input.end.assertEquals(input.start.add(1)); + return { publicOutput: Field(1) }; + }, + }, + merge: { + privateInputs: [SelfProof, SelfProof], + async method( + input: PairInput, + left: SelfProof, + right: SelfProof + ) { + left.verify(); + right.verify(); + input.start.assertEquals(left.publicInput.start); + left.publicInput.end.assertEquals(right.publicInput.start); + input.end.assertEquals(right.publicInput.end); + return { publicOutput: left.publicOutput.add(right.publicOutput) }; + }, + }, + }, +}); + +const OneProofProgram = ZkProgram({ + name: 'mesa-minimal-large-method-one-proof-program', + publicInput: Field, + publicOutput: Field, + methods: { + base: { + privateInputs: [], + async method(input: Field) { + return { publicOutput: input.add(1) }; + }, + }, + step: { + privateInputs: [SelfProof], + async method(input: Field, previous: SelfProof) { + previous.verify(); + input.assertEquals(previous.publicOutput); + return { publicOutput: input.add(1) }; + }, + }, + }, +}); + +const PlainProgram = ZkProgram({ + name: 'mesa-minimal-large-method-plain-program', + publicInput: Field, + publicOutput: Field, + methods: { + check: { + privateInputs: [], + async method(input: Field) { + return { publicOutput: input.add(1) }; + }, + }, + }, +}); + +class TwoProof extends TwoProofProgram.Proof {} +class OneProof extends OneProofProgram.Proof {} +class PlainProof extends PlainProgram.Proof {} + +function prefixToField(prefix: string) { + const bytes = [...new TextEncoder().encode(prefix)]; + const size = (Field as any).sizeInBytes ?? 32; + return (Field as any).fromBytes(bytes.concat(Array(size - bytes.length).fill(0))); +} + +function emptyHashWithPrefix(prefix: string) { + return Poseidon.update(Poseidon.initialState(), [prefixToField(prefix)])[0]; +} + +const emptyActionListHash = emptyHashWithPrefix('MinaZkappActionsEmpty'); +function merkleActionsAdd(hash: Field, actionListHash: Field) { + return Poseidon.hashWithPrefix('MinaZkappSeqEvents**', [hash, actionListHash]); +} + +class SimpleAction extends Struct({ + kind: Field, + account: PublicKey, + amount: Field, + cosmosAddress: Field, + sigR: Field, + sigS: Field, +}) { + actionListHash() { + return Poseidon.hashWithPrefix('MinaZkappEvent******', [ + this.kind, + ...this.account.toFields(), + this.amount, + this.cosmosAddress, + this.sigR, + this.sigS, + ]); + } + + depositHash(previous: Field) { + return Poseidon.hash([ + previous, + ...this.account.toFields(), + this.amount, + this.cosmosAddress, + this.sigR, + this.sigS, + ]); + } + + withdrawalHash(previous: Field) { + return Poseidon.hash([previous, ...this.account.toFields(), this.amount]); + } +} + +class Batch extends Struct({ + actions: Provable.Array(SimpleAction, BATCH_SIZE), +}) {} + +class Mask extends Struct({ + list: Provable.Array(Bool, BATCH_SIZE), +}) { + toField() { + let packed = Field(0); + for (let i = 0; i < BATCH_SIZE; i++) { + packed = packed.add(this.list[i].toField().mul(1n << BigInt(i))); + } + return packed; + } +} + +class MinimalMesaVkUpdateRepro extends SmartContract { + @state(Field) actionState = State(); + @state(Field) depositHash = State(); + @state(Field) withdrawalHash = State(); + + async deploy() { + await super.deploy(); + this.account.permissions.set({ + ...Permissions.default(), + send: Permissions.proof(), + }); + } + + @method + async touchTwoProof(_proof: TwoProof) { + this.actionState.set(Field(0)); + } + + @method + async touchOneProof(_proof: OneProof) { + this.depositHash.set(Field(0)); + } + + @method + async reduce(batch: Batch, mask: Mask, _one: OneProof, _plain: PlainProof) { + let actionState = this.actionState.getAndRequireEquals(); + let depositHash = this.depositHash.getAndRequireEquals(); + let withdrawalHash = this.withdrawalHash.getAndRequireEquals(); + + for (let i = 0; i < BATCH_SIZE; i++) { + const action = batch.actions[i]; + const actionHash = action.actionListHash(); + actionState = Provable.if( + mask.list[i], + merkleActionsAdd( + actionState, + Poseidon.hashWithPrefix('MinaZkappSeqEvents**', [ + emptyActionListHash, + actionHash, + ]) + ), + actionState + ); + depositHash = Provable.if( + mask.list[i].and(action.kind.equals(Field(1))), + action.depositHash(depositHash), + depositHash + ); + withdrawalHash = Provable.if( + mask.list[i].and(action.kind.equals(Field(2))), + action.withdrawalHash(withdrawalHash), + withdrawalHash + ); + + const to = Provable.if(mask.list[i], action.account, PublicKey.empty()); + const amount = Provable.if( + mask.list[i], + UInt64.Unsafe.fromField(action.amount), + UInt64.zero + ); + this.send({ to, amount }); + } + + this.actionState.set(actionState); + this.depositHash.set(depositHash); + this.withdrawalHash.set(withdrawalHash); + mask.toField().assertEquals(mask.toField()); + } +} + +const cache = Cache.None; + +console.log('compiling proof programs'); +await TwoProofProgram.compile({ cache }); +await OneProofProgram.compile({ cache }); +await PlainProgram.compile({ cache }); + +console.log('compiling MinimalMesaVkUpdateRepro'); +await MinimalMesaVkUpdateRepro.compile({ cache });