diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index f3673a3154..eb45a454cb 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -8,7 +8,7 @@ on: workflow_dispatch: {} concurrency: - group: ${{ github.ref }} + group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: diff --git a/.github/workflows/build-action.yml b/.github/workflows/build-action.yml index 402107ea05..1a540c97bd 100644 --- a/.github/workflows/build-action.yml +++ b/.github/workflows/build-action.yml @@ -10,7 +10,7 @@ on: workflow_dispatch: {} concurrency: - group: ${{ github.ref }} + group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: @@ -301,13 +301,25 @@ jobs: run: | npm ci npm run prepublishOnly + - name: Publish to NPM if version has changed + id: publish uses: JS-DevTools/npm-publish@v3 with: token: ${{ secrets.NPM_TOKEN }} strategy: upgrade env: INPUT_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Tag new version + if: ${{ steps.publish.outputs.type }} # https://github.com/JS-DevTools/npm-publish?tab=readme-ov-file#action-output + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASED_VERSION: ${{ steps.publish.outputs.version }} + run: | + git tag $RELEASED_VERSION + git push origin $RELEASED_VERSION + Release-mina-signer-on-NPM: if: github.ref == 'refs/heads/main' diff --git a/.github/workflows/build-bindings.yml b/.github/workflows/build-bindings.yml index 7ffc1a7cff..d1b6772a06 100644 --- a/.github/workflows/build-bindings.yml +++ b/.github/workflows/build-bindings.yml @@ -1,4 +1,4 @@ -# Purpose: We want to build the o1js bindings in CI so that people in the +# Purpose: We want to build the o1js bindings in CI so that people in the # community can change them without being scared of breaking things, or # needing to do the complicated (without nix) build setup. @@ -7,19 +7,30 @@ name: Build o1js bindings on: pull_request: -jobs: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: nix-build: name: build-bindings-ubuntu runs-on: [sdk-self-hosted-linux-amd64-build-system] steps: + - name: Set up Nix + run: echo "PATH=$PATH:/nix/var/nix/profiles/default/bin" >> $GITHUB_ENV - name: Disable smudging run: echo "GIT_LFS_SKIP_SMUDGE=1" >> $GITHUB_ENV - uses: actions/checkout@v4 with: submodules: recursive - - run: | + - name: Build the o1js bindings + run: | set -Eeu - # Until we restart the runner and the PATH gets updated from /etc/bash.bashrc - export PATH="$PATH":/nix/var/nix/profiles/default/bin ./pin.sh nix develop o1js --command bash -c "npm run build:update-bindings" + - name: Cleanup the Nix store + run: | + nix-env --delete-generations old + nix-collect-garbage -d --quiet + nix-store --gc --print-dead + nix-store --optimise diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index 6bd4433615..b4f7eb790a 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -4,6 +4,10 @@ on: branches: - main +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: Build-Doc: runs-on: ubuntu-latest diff --git a/.github/workflows/live-tests.yml b/.github/workflows/live-tests.yml index 3f14642714..dc7db700a1 100644 --- a/.github/workflows/live-tests.yml +++ b/.github/workflows/live-tests.yml @@ -13,7 +13,7 @@ on: workflow_dispatch: {} concurrency: - group: ${{ github.ref }} + group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 67e89f95bb..707b5245a5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -58,13 +58,6 @@ jobs: git add CHANGELOG.md git commit -m "Update CHANGELOG for new version $NEW_VERSION" - - name: Delete existing release tag - run: | - if git rev-parse $NEW_VERSION >/dev/null 2>&1; then - git tag -d $NEW_VERSION - git push origin :refs/tags/$NEW_VERSION - fi - - name: Delete existing release branch run: | if git ls-remote --heads origin release/${NEW_VERSION} | grep release/${NEW_VERSION}; then diff --git a/CHANGELOG.md b/CHANGELOG.md index 3862a9083a..f895921488 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,16 +19,19 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Added -- Increased maximum supported amount of methods in a `SmartContract` or `ZkProgram` to 30. https://github.com/o1-labs/o1js/pull/1918 +-`ZkProgram` to support non-pure provable types as inputs and outputs https://github.com/o1-labs/o1js/pull/1828 + +- Add `enforceTransactionLimits` parameter on Network https://github.com/o1-labs/o1js/issues/1910 + +- Method for optional types to assert none https://github.com/o1-labs/o1js/pull/1922 ### Fixed - Compiling stuck in the browser for recursive zkprograms https://github.com/o1-labs/o1js/pull/1906 +- Error message in `rangeCheck16` gadget https://github.com/o1-labs/o1js/pull/1920 ## [2.1.0](https://github.com/o1-labs/o1js/compare/b04520d...e1bac02) - 2024-11-13 -### Added - - Support secp256r1 in elliptic curve and ECDSA gadgets https://github.com/o1-labs/o1js/pull/1885 ### Fixed diff --git a/src/examples/zkprogram/program-with-non-pure-input.ts b/src/examples/zkprogram/program-with-non-pure-input.ts new file mode 100644 index 0000000000..f217ed8721 --- /dev/null +++ b/src/examples/zkprogram/program-with-non-pure-input.ts @@ -0,0 +1,62 @@ +import { Field, Struct, ZkProgram, assert } from 'o1js'; + +class MyStruct extends Struct({ + label: String, + value: Field, +}) {} + +let NonPureIOprogram = ZkProgram({ + name: 'example-with-non-pure-io', + publicInput: MyStruct, + publicOutput: MyStruct, + + methods: { + baseCase: { + privateInputs: [], + async method(input: MyStruct) { + //update input in circuit + input.label = 'in-circuit'; + return { + publicOutput: input, + }; + }, + }, + }, +}); + +let NonPureOutputProgram = ZkProgram({ + name: 'example-with-non-pure-output', + publicOutput: MyStruct, + + methods: { + baseCase: { + privateInputs: [], + async method() { + return { + publicOutput: new MyStruct({ label: 'output', value: Field(5) }), + }; + }, + }, + }, +}); + +console.log('compiling NonPureIOprogram...'); +await NonPureIOprogram.compile(); +console.log('compile done'); +let input = new MyStruct({ label: 'input', value: Field(5) }); +let proof; +({ proof } = await NonPureIOprogram.baseCase(input)); +let isProof1Valid = await NonPureIOprogram.verify(proof); +assert(isProof1Valid, 'proof not valid!'); +assert(proof.publicOutput.label === 'in-circuit'); +console.log('i/o proof', proof); + +console.log('compiling NonPureOutputProgram...'); +await NonPureOutputProgram.compile(); +console.log('compile done'); + +({ proof } = await NonPureOutputProgram.baseCase()); +let isProof2Valid = await NonPureOutputProgram.verify(proof); +assert(isProof2Valid, 'proof not valid!'); +assert(proof.publicOutput.label === 'output'); +console.log('output proof', proof); diff --git a/src/lib/mina/mina.network.unit-test.ts b/src/lib/mina/mina.network.unit-test.ts new file mode 100644 index 0000000000..73b2a0e1d0 --- /dev/null +++ b/src/lib/mina/mina.network.unit-test.ts @@ -0,0 +1,202 @@ +import { + State, + state, + UInt64, + Bool, + SmartContract, + Mina, + AccountUpdate, + method, + PublicKey, + Permissions, + VerificationKey, + Field, + Int64, + TokenId, + TokenContract as TokenContractBase, + AccountUpdateForest, + PrivateKey, +} from 'o1js'; +import { test, describe, it, before } from 'node:test'; +import { expect } from 'expect'; + + + +const defaultNetwork = Mina.Network({ + networkId: "testnet", + mina: "https://example.com/graphql", + archive: "https://example.com//graphql" +}); + +const enforcedNetwork = Mina.Network({ + networkId: "testnet", + mina: "https://example.com/graphql", + archive: "https://example.com//graphql", + bypassTransactionLimits: false +}); + +const unlimitedNetwork = Mina.Network({ + networkId: "testnet", + mina: "https://unlimited.com/graphql", + archive: "https://unlimited.com//graphql", + bypassTransactionLimits: true +}); + +describe('Test default network', () => { + let bobAccount: PublicKey, + bobKey: PrivateKey; + + before(async () => { + + Mina.setActiveInstance(defaultNetwork); + bobKey = PrivateKey.random(); + bobAccount = bobKey.toPublicKey(); + }); + + + it('Simple account update', async () => { + + let txn = await Mina.transaction(async () => { + const accountUpdateBob = AccountUpdate.create(bobAccount, Field.from(1)); + accountUpdateBob.account.balance.requireEquals(UInt64.zero); + accountUpdateBob.balance.addInPlace(UInt64.one); + }); + await txn.prove(); + await txn.sign([bobKey]).safeSend(); + + }); + + it('Multiple account update', async () => { + + let txn = await Mina.transaction(async () => { + for (let index = 0; index < 2; index++) { + const accountUpdateBob = AccountUpdate.create(bobAccount, Field.from(index)); + accountUpdateBob.account.balance.requireEquals(UInt64.zero); + accountUpdateBob.balance.addInPlace(UInt64.one); + } + }); + await txn.prove(); + await txn.sign([bobKey]).safeSend(); + + }); + + it('More than limit account update', async () => { + + let txn = await Mina.transaction(async () => { + for (let index = 0; index < 12; index++) { + const accountUpdateBob = AccountUpdate.create(bobAccount, Field.from(index)); + accountUpdateBob.account.balance.requireEquals(UInt64.zero); + accountUpdateBob.balance.addInPlace(UInt64.one); + } + }); + await txn.prove(); + // failure with default bypassTransactionLimits value + await expect(txn.sign([bobKey]).safeSend()).rejects.toThrow(); + }); +}); + +describe('Test enforced network', () => { + let bobAccount: PublicKey, + bobKey: PrivateKey; + + before(async () => { + + Mina.setActiveInstance(enforcedNetwork); + bobKey = PrivateKey.random(); + bobAccount = bobKey.toPublicKey(); + }); + + + it('Simple account update', async () => { + + let txn = await Mina.transaction(async () => { + const accountUpdateBob = AccountUpdate.create(bobAccount, Field.from(1)); + accountUpdateBob.account.balance.requireEquals(UInt64.zero); + accountUpdateBob.balance.addInPlace(UInt64.one); + }); + await txn.prove(); + await txn.sign([bobKey]).safeSend(); + + }); + + it('Multiple account update', async () => { + + let txn = await Mina.transaction(async () => { + for (let index = 0; index < 2; index++) { + const accountUpdateBob = AccountUpdate.create(bobAccount, Field.from(index)); + accountUpdateBob.account.balance.requireEquals(UInt64.zero); + accountUpdateBob.balance.addInPlace(UInt64.one); + } + }); + await txn.prove(); + await txn.sign([bobKey]).safeSend(); + + }); + + it('More than limit account update', async () => { + + let txn = await Mina.transaction(async () => { + for (let index = 0; index < 12; index++) { + const accountUpdateBob = AccountUpdate.create(bobAccount, Field.from(index)); + accountUpdateBob.account.balance.requireEquals(UInt64.zero); + accountUpdateBob.balance.addInPlace(UInt64.one); + } + }); + await txn.prove(); + // failure with bypassTransactionLimits = false + await expect(txn.sign([bobKey]).safeSend()).rejects.toThrow(); + }); +}); + +describe('Test unlimited network', () => { + let bobAccount: PublicKey, + bobKey: PrivateKey; + + before(async () => { + + Mina.setActiveInstance(unlimitedNetwork); + bobKey = PrivateKey.random(); + bobAccount = bobKey.toPublicKey(); + }); + + + it('Simple account update', async () => { + + let txn = await Mina.transaction(async () => { + const accountUpdateBob = AccountUpdate.create(bobAccount, Field.from(1)); + accountUpdateBob.account.balance.requireEquals(UInt64.zero); + accountUpdateBob.balance.addInPlace(UInt64.one); + }); + await txn.prove(); + await txn.sign([bobKey]).safeSend(); + + }); + + it('Multiple account update', async () => { + + let txn = await Mina.transaction(async () => { + for (let index = 0; index < 2; index++) { + const accountUpdateBob = AccountUpdate.create(bobAccount, Field.from(index)); + accountUpdateBob.account.balance.requireEquals(UInt64.zero); + accountUpdateBob.balance.addInPlace(UInt64.one); + } + }); + await txn.prove(); + await txn.sign([bobKey]).safeSend(); + + }); + + it('More than limit account update', async () => { + + let txn = await Mina.transaction(async () => { + for (let index = 0; index < 12; index++) { + const accountUpdateBob = AccountUpdate.create(bobAccount, Field.from(index)); + accountUpdateBob.account.balance.requireEquals(UInt64.zero); + accountUpdateBob.balance.addInPlace(UInt64.one); + } + }); + await txn.prove(); + // success with bypassTransactionLimits = true + await txn.sign([bobKey]).safeSend(); + }); +}); \ No newline at end of file diff --git a/src/lib/mina/mina.ts b/src/lib/mina/mina.ts index 2dc41d5422..67edd659e5 100644 --- a/src/lib/mina/mina.ts +++ b/src/lib/mina/mina.ts @@ -104,21 +104,24 @@ function Network(options: { mina: string | string[]; archive?: string | string[]; lightnetAccountManager?: string; + bypassTransactionLimits?: boolean; }): Mina; function Network( options: | { - networkId?: NetworkId; - mina: string | string[]; - archive?: string | string[]; - lightnetAccountManager?: string; - } + networkId?: NetworkId; + mina: string | string[]; + archive?: string | string[]; + lightnetAccountManager?: string; + bypassTransactionLimits?: boolean; + } | string ): Mina { let minaNetworkId: NetworkId = 'testnet'; let minaGraphqlEndpoint: string; let archiveEndpoint: string; let lightnetAccountManagerEndpoint: string; + let enforceTransactionLimits: boolean = true; if (options && typeof options === 'string') { minaGraphqlEndpoint = options; @@ -158,6 +161,11 @@ function Network( lightnetAccountManagerEndpoint = options.lightnetAccountManager; Fetch.setLightnetAccountManagerEndpoint(lightnetAccountManagerEndpoint); } + + if (options.bypassTransactionLimits !== undefined && + typeof options.bypassTransactionLimits === 'boolean') { + enforceTransactionLimits = !options.bypassTransactionLimits; + } } else { throw new Error( "Network: malformed input. Please provide a string or an object with 'mina' and 'archive' endpoints." @@ -251,7 +259,7 @@ function Network( }, sendTransaction(txn) { return toPendingTransactionPromise(async () => { - verifyTransactionLimits(txn.transaction); + if (enforceTransactionLimits) verifyTransactionLimits(txn.transaction); let [response, error] = await Fetch.sendZkapp(txn.toJSON()); let errors: string[] = []; diff --git a/src/lib/proof-system/proof.ts b/src/lib/proof-system/proof.ts index 9111482cf9..f19402fc8d 100644 --- a/src/lib/proof-system/proof.ts +++ b/src/lib/proof-system/proof.ts @@ -2,7 +2,7 @@ import { initializeBindings, withThreadPool } from '../../snarky.js'; import { Pickles } from '../../snarky.js'; import { Field, Bool } from '../provable/wrapped.js'; import type { - FlexibleProvablePure, + FlexibleProvable, InferProvable, } from '../provable/types/struct.js'; import { FeatureFlags } from './feature-flags.js'; @@ -22,8 +22,8 @@ export { dummyProof, extractProofs, extractProofTypes, type ProofValue }; type MaxProofs = 0 | 1 | 2; class ProofBase { - static publicInputType: FlexibleProvablePure = undefined as any; - static publicOutputType: FlexibleProvablePure = undefined as any; + static publicInputType: FlexibleProvable = undefined as any; + static publicOutputType: FlexibleProvable = undefined as any; static tag: () => { name: string } = () => { throw Error( `You cannot use the \`Proof\` class directly. Instead, define a subclass:\n` + diff --git a/src/lib/proof-system/zkprogram.ts b/src/lib/proof-system/zkprogram.ts index 26b06039eb..29cd5d663d 100644 --- a/src/lib/proof-system/zkprogram.ts +++ b/src/lib/proof-system/zkprogram.ts @@ -3,7 +3,7 @@ import { Snarky, initializeBindings, withThreadPool } from '../../snarky.js'; import { Pickles, Gate } from '../../snarky.js'; import { Field } from '../provable/wrapped.js'; import { - FlexibleProvablePure, + FlexibleProvable, InferProvable, ProvablePureExtended, Struct, @@ -89,8 +89,15 @@ const Void: ProvablePureExtended = EmptyVoid(); function createProgramState() { let methodCache: Map = new Map(); - return { + setNonPureOutput(value: any[]) { + methodCache.set('__nonPureOutput__', value); + }, + getNonPureOutput(): any[] { + let entry = methodCache.get('__nonPureOutput__'); + if (entry === undefined) return []; + return entry as any[]; + }, setAuxiliaryOutput(value: unknown, methodName: string) { methodCache.set(methodName, value); }, @@ -100,8 +107,8 @@ function createProgramState() { throw Error(`Auxiliary value for method ${methodName} not defined`); return entry; }, - reset(methodName: string) { - methodCache.delete(methodName); + reset(key: string) { + methodCache.delete(key); }, }; } @@ -173,8 +180,8 @@ let SideloadedTag = { function ZkProgram< Config extends { - publicInput?: ProvableTypePure; - publicOutput?: ProvableTypePure; + publicInput?: ProvableType; + publicOutput?: ProvableType; methods: { [I in string]: { privateInputs: Tuple; @@ -250,10 +257,10 @@ function ZkProgram< let doProving = true; let methods = config.methods; - let publicInputType: ProvablePure = ProvableType.get( + let publicInputType: Provable = ProvableType.get( config.publicInput ?? Undefined ); - let publicOutputType: ProvablePure = ProvableType.get( + let publicOutputType: Provable = ProvableType.get( config.publicOutput ?? Void ); @@ -391,10 +398,20 @@ function ZkProgram< `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 publicInputFields = toFieldConsts(publicInputType, publicInput); + + let { publicInputFields, publicInputAux } = toFieldAndAuxConsts( + publicInputType, + publicInput + ); + let previousProofs = MlArray.to(getPreviousProofsForProver(args)); - let id = snarkContext.enter({ witnesses: args, inProver: true }); + let id = snarkContext.enter({ + witnesses: args, + inProver: true, + auxInputData: publicInputAux, + }); + let result: UnwrapPromise>; try { result = await picklesProver(publicInputFields, previousProofs); @@ -416,7 +433,16 @@ function ZkProgram< } let [publicOutputFields, proof] = MlPair.from(result); - let publicOutput = fromFieldConsts(publicOutputType, publicOutputFields); + + let nonPureOutput = programState.getNonPureOutput(); + + let publicOutput = fromFieldConsts( + publicOutputType, + publicOutputFields, + nonPureOutput + ); + + programState.reset('__nonPureOutput__'); return { proof: new ProgramProof({ @@ -649,8 +675,8 @@ async function compileProgram({ overrideWrapDomain, state, }: { - publicInputType: ProvablePure; - publicOutputType: ProvablePure; + publicInputType: Provable; + publicOutputType: Provable; methodIntfs: MethodInterface[]; methods: ((...args: any) => unknown)[]; gates: Gate[][]; @@ -762,7 +788,7 @@ If you are using a SmartContract, make sure you are using the @method decorator. } function analyzeMethod( - publicInputType: ProvablePure, + publicInputType: Provable, methodIntf: MethodInterface, method: (...args: any) => unknown ) { @@ -790,8 +816,8 @@ function inCircuitVkHash(inCircuitVk: unknown): Field { } function picklesRuleFromFunction( - publicInputType: ProvablePure, - publicOutputType: ProvablePure, + publicInputType: Provable, + publicOutputType: Provable, func: (...args: unknown[]) => unknown, proofSystemTag: { name: string }, { methodName, args, auxiliaryType }: MethodInterface, @@ -801,7 +827,11 @@ function picklesRuleFromFunction( async function main( publicInput: MlFieldArray ): ReturnType { - let { witnesses: argsWithoutPublicInput, inProver } = snarkContext.get(); + let { + witnesses: argsWithoutPublicInput, + inProver, + auxInputData, + } = snarkContext.get(); assert(!(inProver && argsWithoutPublicInput === undefined)); let finalArgs = []; let proofs: { @@ -837,10 +867,16 @@ function picklesRuleFromFunction( if (publicInputType === Undefined || publicInputType === Void) { result = (await func(...finalArgs)) as any; } else { - let input = fromFieldVars(publicInputType, publicInput); + let input = fromFieldVars(publicInputType, publicInput, auxInputData); result = (await func(input, ...finalArgs)) as any; } + if (result?.publicOutput) { + // store the nonPure auxiliary data in program state cache if it exists + let nonPureOutput = publicOutputType.toAuxiliary(result.publicOutput); + state?.setNonPureOutput(nonPureOutput); + } + proofs.forEach(({ Proof, proof }) => { if (!(proof instanceof DynamicProof)) return; @@ -869,7 +905,7 @@ function picklesRuleFromFunction( Pickles.sideLoaded.inCircuit(computedTag, circuitVk); }); - // if the public output is empty, we don't evaluate `toFields(result)` to allow the function to return something else in that case + // if the output is empty, we don't evaluate `toFields(result)` to allow the function to return something else in that case let hasPublicOutput = publicOutputType.sizeInFields() !== 0; let publicOutput = hasPublicOutput ? publicOutputType.toFields(result.publicOutput) @@ -957,20 +993,36 @@ function getMaxProofsVerified(methodIntfs: MethodInterface[]) { ) as any as 0 | 1 | 2; } -function fromFieldVars(type: ProvablePure, fields: MlFieldArray) { - return type.fromFields(MlFieldArray.from(fields)); +function fromFieldVars( + type: Provable, + fields: MlFieldArray, + auxData: any[] = [] +) { + return type.fromFields(MlFieldArray.from(fields), auxData); } -function fromFieldConsts(type: ProvablePure, fields: MlFieldConstArray) { - return type.fromFields(MlFieldConstArray.from(fields)); +function fromFieldConsts( + type: Provable, + fields: MlFieldConstArray, + aux: any[] = [] +) { + return type.fromFields(MlFieldConstArray.from(fields), aux); } -function toFieldConsts(type: ProvablePure, value: T) { + +function toFieldConsts(type: Provable, value: T) { return MlFieldConstArray.to(type.toFields(value)); } +function toFieldAndAuxConsts(type: Provable, value: T) { + return { + publicInputFields: MlFieldConstArray.to(type.toFields(value)), + publicInputAux: type.toAuxiliary(value), + }; +} + ZkProgram.Proof = function < - PublicInputType extends FlexibleProvablePure, - PublicOutputType extends FlexibleProvablePure + PublicInputType extends FlexibleProvable, + PublicOutputType extends FlexibleProvable >(program: { name: string; publicInputType: PublicInputType; diff --git a/src/lib/provable/core/provable-context.ts b/src/lib/provable/core/provable-context.ts index a697c5d00d..ee44ab9c5e 100644 --- a/src/lib/provable/core/provable-context.ts +++ b/src/lib/provable/core/provable-context.ts @@ -40,6 +40,7 @@ type SnarkContext = { inCheckedComputation?: boolean; inAnalyze?: boolean; inWitnessBlock?: boolean; + auxInputData?: any[]; }; let snarkContext = Context.create({ default: {} }); diff --git a/src/lib/provable/gadgets/range-check.ts b/src/lib/provable/gadgets/range-check.ts index da661c5def..f65c90e4db 100644 --- a/src/lib/provable/gadgets/range-check.ts +++ b/src/lib/provable/gadgets/range-check.ts @@ -332,7 +332,7 @@ function rangeCheck16(x: Field) { if (x.isConstant()) { assert( x.toBigInt() < 1n << 16n, - `rangeCheck16: expected field to fit in 8 bits, got ${x}` + `rangeCheck16: expected field to fit in 16 bits, got ${x}` ); return; } diff --git a/src/lib/provable/option.ts b/src/lib/provable/option.ts index cc82a568a3..edec98d60e 100644 --- a/src/lib/provable/option.ts +++ b/src/lib/provable/option.ts @@ -9,6 +9,7 @@ export { Option, OptionOrValue }; type Option = { isSome: Bool; value: T } & { assertSome(message?: string): T; + assertNone(message?: string): void; orElse(defaultValue: T | V): T; }; @@ -104,6 +105,10 @@ function Option( return this.value; } + assertNone(message?: string): void { + this.isSome.assertFalse(message); + } + static from(value?: V | T) { return value === undefined ? new Option_({ diff --git a/src/lib/util/arrays.ts b/src/lib/util/arrays.ts index 891de81015..b8d11660f3 100644 --- a/src/lib/util/arrays.ts +++ b/src/lib/util/arrays.ts @@ -1,9 +1,12 @@ import { assert } from './errors.js'; -export { chunk, chunkString }; +export { chunk, chunkString, zip, pad }; function chunk(array: T[], size: number): T[][] { - assert(array.length % size === 0, 'invalid input length'); + assert( + array.length % size === 0, + `chunk(): invalid input length, it must be a multiple of ${size}` + ); return Array.from({ length: array.length / size }, (_, i) => array.slice(size * i, size * (i + 1)) ); @@ -12,3 +15,19 @@ function chunk(array: T[], size: number): T[][] { function chunkString(str: string, size: number): string[] { return chunk([...str], size).map((c) => c.join('')); } + +function zip(a: T[], b: S[]) { + assert( + a.length <= b.length, + 'zip(): second array must be at least as long as the first array' + ); + return a.map((a, i): [T, S] => [a, b[i]!]); +} + +function pad(array: T[], size: number, value: T): T[] { + assert( + array.length <= size, + `target size ${size} should be greater or equal than the length of the array ${array.length}` + ); + return array.concat(Array.from({ length: size - array.length }, () => value)); +}