diff --git a/src/bindings b/src/bindings index df9f2d6fa5..dee86310ac 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit df9f2d6fa56d9589eae3a7ace11cd10e7796ae26 +Subproject commit dee86310ac7c1c0ac745df17df0245ec64cbd743 diff --git a/src/index.ts b/src/index.ts index 81afa8f57a..5310349eac 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ export { export { createForeignCurve, ForeignCurve } from './lib/foreign-curve.js'; export { createEcdsa, EcdsaSignature } from './lib/foreign-ecdsa.js'; export { Poseidon, TokenSymbol, ProvableHashable } from './lib/hash.js'; +export { Poseidon as PoseidonBn254 } from './lib/hash-bn254.js'; export { Keccak } from './lib/keccak.js'; export { Hash } from './lib/hashes-combined.js'; diff --git a/src/lib/circuit-value-bn254.ts b/src/lib/circuit-value-bn254.ts index 417e521cf6..3eb58a1652 100644 --- a/src/lib/circuit-value-bn254.ts +++ b/src/lib/circuit-value-bn254.ts @@ -1,37 +1,55 @@ import 'reflect-metadata'; -import { ProvablePureBn254 } from '../snarky.js'; +import { ProvablePureBn254, Snarky } from '../snarky.js'; +import { FieldBn254, BoolBn254 } from './core-bn254.js'; import { + provable, provablePure, provableTuple, HashInput, -} from '../bindings/lib/provable-snarky.js'; -import { provable } from '../bindings/lib/provable-snarky-bn254.js'; + NonMethods, +} from '../bindings/lib/provable-snarky-bn254.js'; import type { InferJson, InferProvable, InferredProvable, -} from '../bindings/lib/provable-snarky.js'; -import { FieldBn254 } from './field-bn254.js'; + IsPure, +} from '../bindings/lib/provable-snarky-bn254.js'; import { ProvableBn254 } from './provable-bn254.js'; +import { assert } from './errors.js'; +import { inCheckedComputation } from './provable-context-bn254.js'; +import { Proof } from './proof-system.js'; // external API export { + CircuitValue, ProvableExtendedBn254, ProvablePureExtendedBn254, + prop, + arrayProp, + matrixProp, provable, provablePure, + Struct, + FlexibleProvableBn254, + FlexibleProvablePureBn254, + Unconstrained, }; // internal API export { provableTuple, + AnyConstructor, + cloneCircuitValue, + circuitValueEquals, + toConstant, InferProvable, HashInput, InferJson, InferredProvable, + StructNoJson, }; -type ProvableExtensionBn254 = { +type ProvableExtension = { toInput: (x: T) => { fields?: FieldBn254[]; packed?: [FieldBn254, number][] }; toJSON: (x: T) => TJson; fromJSON: (x: TJson) => T; @@ -39,7 +57,682 @@ type ProvableExtensionBn254 = { }; type ProvableExtendedBn254 = ProvableBn254 & - ProvableExtensionBn254; - + ProvableExtension; type ProvablePureExtendedBn254 = ProvablePureBn254 & - ProvableExtensionBn254; + ProvableExtension; + +type Struct = ProvableExtendedBn254> & + Constructor & { _isStruct: true }; +type StructPure = ProvablePureBn254> & + ProvableExtension> & + Constructor & { _isStruct: true }; +type FlexibleProvableBn254 = ProvableBn254 | Struct; +type FlexibleProvablePureBn254 = ProvablePureBn254 | StructPure; + +type Constructor = new (...args: any) => T; +type AnyConstructor = Constructor; + +/** + * @deprecated `CircuitValue` is deprecated in favor of {@link Struct}, which features a simpler API and better typing. + */ +abstract class CircuitValue { + constructor(...props: any[]) { + // if this is called with no arguments, do nothing, to support simple super() calls + if (props.length === 0) return; + + let fields = this.constructor.prototype._fields; + if (fields === undefined) return; + if (props.length !== fields.length) { + throw Error( + `${this.constructor.name} constructor called with ${props.length} arguments, but expected ${fields.length}` + ); + } + for (let i = 0; i < fields.length; ++i) { + let [key] = fields[i]; + (this as any)[key] = props[i]; + } + } + + static fromObject( + this: T, + value: NonMethods> + ): InstanceType { + return Object.assign(Object.create(this.prototype), value); + } + + static sizeInFields(): number { + const fields: [string, any][] = (this as any).prototype._fields; + return fields.reduce((acc, [_, typ]) => acc + typ.sizeInFields(), 0); + } + + static toFields( + this: T, + v: InstanceType + ): FieldBn254[] { + const res: FieldBn254[] = []; + const fields = this.prototype._fields; + if (fields === undefined || fields === null) { + return res; + } + for (let i = 0, n = fields.length; i < n; ++i) { + const [key, propType] = fields[i]; + const subElts: FieldBn254[] = propType.toFields((v as any)[key]); + subElts.forEach((x) => res.push(x)); + } + return res; + } + + static toAuxiliary(): [] { + return []; + } + + static toInput( + this: T, + v: InstanceType + ): HashInput { + let input: HashInput = { fields: [], packed: [] }; + let fields = this.prototype._fields; + if (fields === undefined) return input; + for (let i = 0, n = fields.length; i < n; ++i) { + let [key, type] = fields[i]; + if ('toInput' in type) { + input = HashInput.append(input, type.toInput(v[key])); + continue; + } + // as a fallback, use toFields on the type + // TODO: this is problematic -- ignores if there's a toInput on a nested type + // so, remove this? should every provable define toInput? + let xs: FieldBn254[] = type.toFields(v[key]); + input.fields!.push(...xs); + } + return input; + } + + toFields(): FieldBn254[] { + return (this.constructor as any).toFields(this); + } + + toJSON(): any { + return (this.constructor as any).toJSON(this); + } + + toConstant(): this { + return (this.constructor as any).toConstant(this); + } + + equals(x: this) { + return ProvableBn254.equal(this, x); + } + + assertEquals(x: this) { + ProvableBn254.assertEqual(this, x); + } + + isConstant() { + return this.toFields().every((x) => x.isConstant()); + } + + static fromFields( + this: T, + xs: FieldBn254[] + ): InstanceType { + const fields: [string, any][] = (this as any).prototype._fields; + if (xs.length < fields.length) { + throw Error( + `${this.name}.fromFields: Expected ${fields.length} field elements, got ${xs?.length}` + ); + } + let offset = 0; + const props: any = {}; + for (let i = 0; i < fields.length; ++i) { + const [key, propType] = fields[i]; + const propSize = propType.sizeInFields(); + const propVal = propType.fromFields( + xs.slice(offset, offset + propSize), + [] + ); + props[key] = propVal; + offset += propSize; + } + return Object.assign(Object.create(this.prototype), props); + } + + static check(this: T, v: InstanceType) { + const fields = (this as any).prototype._fields; + if (fields === undefined || fields === null) { + return; + } + for (let i = 0; i < fields.length; ++i) { + const [key, propType] = fields[i]; + const value = (v as any)[key]; + if (propType.check === undefined) + throw Error('bug: CircuitValue without .check()'); + propType.check(value); + } + } + + static toConstant( + this: T, + t: InstanceType + ): InstanceType { + const xs: FieldBn254[] = (this as any).toFields(t); + return (this as any).fromFields(xs.map((x) => x.toConstant())); + } + + static toJSON(this: T, v: InstanceType) { + const res: any = {}; + if ((this as any).prototype._fields !== undefined) { + const fields: [string, any][] = (this as any).prototype._fields; + fields.forEach(([key, propType]) => { + res[key] = propType.toJSON((v as any)[key]); + }); + } + return res; + } + + static fromJSON( + this: T, + value: any + ): InstanceType { + let props: any = {}; + let fields: [string, any][] = (this as any).prototype._fields; + if (typeof value !== 'object' || value === null || Array.isArray(value)) { + throw Error(`${this.name}.fromJSON(): invalid input ${value}`); + } + if (fields !== undefined) { + for (let i = 0; i < fields.length; ++i) { + let [key, propType] = fields[i]; + if (value[key] === undefined) { + throw Error(`${this.name}.fromJSON(): invalid input ${value}`); + } else { + props[key] = propType.fromJSON(value[key]); + } + } + } + return Object.assign(Object.create(this.prototype), props); + } + + static empty(): InstanceType { + const fields: [string, any][] = (this as any).prototype._fields ?? []; + let props: any = {}; + fields.forEach(([key, propType]) => { + props[key] = propType.empty(); + }); + return Object.assign(Object.create(this.prototype), props); + } +} + +function prop(this: any, target: any, key: string) { + const fieldType = Reflect.getMetadata('design:type', target, key); + if (!target.hasOwnProperty('_fields')) { + target._fields = []; + } + if (fieldType === undefined) { + } else if (fieldType.toFields && fieldType.fromFields) { + target._fields.push([key, fieldType]); + } else { + console.log( + `warning: property ${key} missing field element conversion methods` + ); + } +} + +function arrayProp(elementType: FlexibleProvableBn254, length: number) { + return function (target: any, key: string) { + if (!target.hasOwnProperty('_fields')) { + target._fields = []; + } + target._fields.push([key, ProvableBn254.Array(elementType, length)]); + }; +} + +function matrixProp( + elementType: FlexibleProvableBn254, + nRows: number, + nColumns: number +) { + return function (target: any, key: string) { + if (!target.hasOwnProperty('_fields')) { + target._fields = []; + } + target._fields.push([ + key, + ProvableBn254.Array(ProvableBn254.Array(elementType, nColumns), nRows), + ]); + }; +} + +/** + * `Struct` lets you declare composite types for use in o1js circuits. + * + * These composite types can be passed in as arguments to smart contract methods, used for on-chain state variables + * or as event / action types. + * + * Here's an example of creating a "Voter" struct, which holds a public key and a collection of votes on 3 different proposals: + * ```ts + * let Vote = { hasVoted: BoolBn254, inFavor: BoolBn254 }; + * + * class Voter extends Struct({ + * publicKey: PublicKey, + * votes: [Vote, Vote, Vote] + * }) {} + * + * // use Voter as SmartContract input: + * class VoterContract extends SmartContract { + * \@method register(voter: Voter) { + * // ... + * } + * } + * ``` + * In this example, there are no instance methods on the class. This makes `Voter` type-compatible with an anonymous object of the form + * `{ publicKey: PublicKey, votes: Vote[] }`. + * This mean you don't have to create instances by using `new Voter(...)`, you can operate with plain objects: + * ```ts + * voterContract.register({ publicKey, votes }); + * ``` + * + * On the other hand, you can also add your own methods: + * ```ts + * class Voter extends Struct({ + * publicKey: PublicKey, + * votes: [Vote, Vote, Vote] + * }) { + * vote(index: number, inFavor: BoolBn254) { + * let vote = this.votes[i]; + * vote.hasVoted = BoolBn254(true); + * vote.inFavor = inFavor; + * } + * } + * ``` + * + * In this case, you'll need the constructor to create instances of `Voter`. It always takes as input the plain object: + * ```ts + * let emptyVote = { hasVoted: BoolBn254(false), inFavor: BoolBn254(false) }; + * let voter = new Voter({ publicKey, votes: Array(3).fill(emptyVote) }); + * voter.vote(1, BoolBn254(true)); + * ``` + * + * In addition to creating types composed of FieldBn254 elements, you can also include auxiliary data which does not become part of the proof. + * This, for example, allows you to re-use the same type outside o1js methods, where you might want to store additional metadata. + * + * To declare non-proof values of type `string`, `number`, etc, you can use the built-in objects `String`, `Number`, etc. + * Here's how we could add the voter's name (a string) as auxiliary data: + * ```ts + * class Voter extends Struct({ + * publicKey: PublicKey, + * votes: [Vote, Vote, Vote], + * fullName: String + * }) {} + * ``` + * + * Again, it's important to note that this doesn't enable you to prove anything about the `fullName` string. + * From the circuit point of view, it simply doesn't exist! + * + * @param type Object specifying the layout of the `Struct` + * @param options Advanced option which allows you to force a certain order of object keys + * @returns Class which you can extend + */ +function Struct< + A, + T extends InferProvable = InferProvable, + J extends InferJson = InferJson, + Pure extends boolean = IsPure +>( + type: A +): (new (value: T) => T) & { _isStruct: true } & (Pure extends true + ? ProvablePureBn254 + : ProvableBn254) & { + toInput: (x: T) => { + fields?: FieldBn254[] | undefined; + packed?: [FieldBn254, number][] | undefined; + }; + toJSON: (x: T) => J; + fromJSON: (x: J) => T; + empty: () => T; + } { + class Struct_ { + static type = provable(type); + static _isStruct: true; + + constructor(value: T) { + Object.assign(this, value); + } + /** + * This method is for internal use, you will probably not need it. + * @returns the size of this struct in field elements + */ + static sizeInFields() { + return this.type.sizeInFields(); + } + /** + * This method is for internal use, you will probably not need it. + * @param value + * @returns the raw list of field elements that represent this struct inside the proof + */ + static toFields(value: T): FieldBn254[] { + return this.type.toFields(value); + } + /** + * This method is for internal use, you will probably not need it. + * @param value + * @returns the raw non-field element data contained in the struct + */ + static toAuxiliary(value: T): any[] { + return this.type.toAuxiliary(value); + } + /** + * This method is for internal use, you will probably not need it. + * @param value + * @returns a representation of this struct as field elements, which can be hashed efficiently + */ + static toInput(value: T): HashInput { + return this.type.toInput(value); + } + /** + * Convert this struct to a JSON object, consisting only of numbers, strings, booleans, arrays and plain objects. + * @param value + * @returns a JSON representation of this struct + */ + static toJSON(value: T): J { + return this.type.toJSON(value) as J; + } + /** + * Convert from a JSON object to an instance of this struct. + * @param json + * @returns a JSON representation of this struct + */ + static fromJSON(json: J): T { + let value = this.type.fromJSON(json); + let struct = Object.create(this.prototype); + return Object.assign(struct, value); + } + /** + * Create an instance of this struct filled with default values + * @returns an empty instance of this struct + */ + static empty(): T { + let value = this.type.empty(); + let struct = Object.create(this.prototype); + return Object.assign(struct, value); + } + /** + * This method is for internal use, you will probably not need it. + * Method to make assertions which should be always made whenever a struct of this type is created in a proof. + * @param value + */ + static check(value: T) { + return this.type.check(value); + } + /** + * This method is for internal use, you will probably not need it. + * Recover a struct from its raw field elements and auxiliary data. + * @param fields the raw fields elements + * @param aux the raw non-field element data + */ + static fromFields(fields: FieldBn254[], aux: any[]) { + let value = this.type.fromFields(fields, aux) as T; + let struct = Object.create(this.prototype); + return Object.assign(struct, value); + } + } + return Struct_ as any; +} + +function StructNoJson< + A, + T extends InferProvable = InferProvable, + Pure extends boolean = IsPure +>( + type: A +): (new (value: T) => T) & { _isStruct: true } & (Pure extends true + ? ProvablePureBn254 + : ProvableBn254) & { + toInput: (x: T) => { + fields?: FieldBn254[] | undefined; + packed?: [FieldBn254, number][] | undefined; + }; + empty: () => T; + } { + return Struct(type) satisfies ProvableBn254 as any; +} + +/** + * Container which holds an unconstrained value. This can be used to pass values + * between the out-of-circuit blocks in provable code. + * + * Invariants: + * - An `Unconstrained`'s value can only be accessed in auxiliary contexts. + * - An `Unconstrained` can be empty when compiling, but never empty when running as the prover. + * (there is no way to create an empty `Unconstrained` in the prover) + * + * @example + * ```ts + * let x = Unconstrained.from(0n); + * + * class MyContract extends SmartContract { + * `@method` myMethod(x: Unconstrained) { + * + * ProvableBn254.witness(FieldBn254, () => { + * // we can access and modify `x` here + * let newValue = x.get() + otherField.toBigInt(); + * x.set(newValue); + * + * // ... + * }); + * + * // throws an error! + * x.get(); + * } + * ``` + */ +class Unconstrained { + private option: + | { isSome: true; value: T } + | { isSome: false; value: undefined }; + + private constructor(isSome: boolean, value?: T) { + this.option = { isSome, value: value as any }; + } + + /** + * Read an unconstrained value. + * + * Note: Can only be called outside provable code. + */ + get(): T { + if (inCheckedComputation() && !Snarky.bn254.run.inProverBlock()) + throw Error(`You cannot use Unconstrained.get() in provable code. + +The only place where you can read unconstrained values is in ProvableBn254.witness() +and ProvableBn254.asProver() blocks, which execute outside the proof. +`); + assert(this.option.isSome, 'Empty `Unconstrained`'); // never triggered + return this.option.value; + } + + /** + * Modify the unconstrained value. + */ + set(value: T) { + this.option = { isSome: true, value }; + } + + /** + * Set the unconstrained value to the same as another `Unconstrained`. + */ + setTo(value: Unconstrained) { + this.option = value.option; + } + + /** + * Create an `Unconstrained` with the given `value`. + * + * Note: If `T` contains provable types, `Unconstrained.from` is an anti-pattern, + * because it stores witnesses in a space that's intended to be used outside the proof. + * Something like the following should be used instead: + * + * ```ts + * let xWrapped = Unconstrained.witness(() => ProvableBn254.toConstant(type, x)); + * ``` + */ + static from(value: T) { + return new Unconstrained(true, value); + } + + /** + * Create an `Unconstrained` from a witness computation. + */ + static witness(compute: () => T) { + return ProvableBn254.witness( + Unconstrained.provable, + () => new Unconstrained(true, compute()) + ); + } + + /** + * Update an `Unconstrained` by a witness computation. + */ + updateAsProver(compute: (value: T) => T) { + return ProvableBn254.asProver(() => { + let value = this.get(); + this.set(compute(value)); + }); + } + + static provable: ProvableBn254> & { + toInput: (x: Unconstrained) => { + fields?: FieldBn254[]; + packed?: [FieldBn254, number][]; + }; + } = { + sizeInFields: () => 0, + toFields: () => [], + toAuxiliary: (t?: any) => [t ?? new Unconstrained(false)], + fromFields: (_, [t]) => t, + check: () => { }, + toInput: () => ({}), + }; +} + +let primitives = new Set([FieldBn254, BoolBn254]); +function isPrimitive(obj: any) { + for (let P of primitives) { + if (obj instanceof P) return true; + } + return false; +} + +function cloneCircuitValue(obj: T): T { + // primitive JS types and functions aren't cloned + if (typeof obj !== 'object' || obj === null) return obj; + + // classes that define clone() are cloned using that method + if (obj.constructor !== undefined && 'clone' in obj.constructor) { + return (obj as any).constructor.clone(obj); + } + + // built-in JS datatypes with custom cloning strategies + if (Array.isArray(obj)) return obj.map(cloneCircuitValue) as any as T; + if (obj instanceof Set) + return new Set([...obj].map(cloneCircuitValue)) as any as T; + if (obj instanceof Map) + return new Map( + [...obj].map(([k, v]) => [k, cloneCircuitValue(v)]) + ) as any as T; + if (ArrayBuffer.isView(obj)) return new (obj.constructor as any)(obj); + + // o1js primitives and proofs aren't cloned + if (isPrimitive(obj)) { + return obj; + } + if (obj instanceof Proof) { + return obj; + } + + // cloning strategy that works for plain objects AND classes whose constructor only assigns properties + let propertyDescriptors: Record = {}; + for (let [key, value] of Object.entries(obj)) { + propertyDescriptors[key] = { + value: cloneCircuitValue(value), + writable: true, + enumerable: true, + configurable: true, + }; + } + return Object.create(Object.getPrototypeOf(obj), propertyDescriptors); +} + +function circuitValueEquals(a: T, b: T): boolean { + // primitive JS types and functions are checked for exact equality + if ( + typeof a !== 'object' || + a === null || + typeof b !== 'object' || + b === null + ) + return a === b; + + // built-in JS datatypes with custom equality checks + if (Array.isArray(a)) { + return ( + Array.isArray(b) && + a.length === b.length && + a.every((a_, i) => circuitValueEquals(a_, b[i])) + ); + } + if (a instanceof Set) { + return ( + b instanceof Set && a.size === b.size && [...a].every((a_) => b.has(a_)) + ); + } + if (a instanceof Map) { + return ( + b instanceof Map && + a.size === b.size && + [...a].every(([k, v]) => circuitValueEquals(v, b.get(k))) + ); + } + if (ArrayBuffer.isView(a) && !(a instanceof DataView)) { + // typed array + return ( + ArrayBuffer.isView(b) && + !(b instanceof DataView) && + circuitValueEquals([...(a as any)], [...(b as any)]) + ); + } + + // the two checks below cover o1js primitives and CircuitValues + // if we have an .equals method, try to use it + if ('equals' in a && typeof (a as any).equals === 'function') { + let isEqual = (a as any).equals(b).toBoolean(); + if (typeof isEqual === 'boolean') return isEqual; + if (isEqual instanceof BoolBn254) return isEqual.toBoolean(); + } + // if we have a .toFields method, try to use it + if ( + 'toFields' in a && + typeof (a as any).toFields === 'function' && + 'toFields' in b && + typeof (b as any).toFields === 'function' + ) { + let aFields = (a as any).toFields() as FieldBn254[]; + let bFields = (b as any).toFields() as FieldBn254[]; + return aFields.every((a, i) => a.equals(bFields[i]).toBoolean()); + } + + // equality test that works for plain objects AND classes whose constructor only assigns properties + let aEntries = Object.entries(a as any).filter(([, v]) => v !== undefined); + let bEntries = Object.entries(b as any).filter(([, v]) => v !== undefined); + if (aEntries.length !== bEntries.length) return false; + return aEntries.every( + ([key, value]) => key in b && circuitValueEquals((b as any)[key], value) + ); +} + +function toConstant(type: FlexibleProvableBn254, value: T): T; +function toConstant(type: ProvableBn254, value: T): T { + return type.fromFields( + type.toFields(value).map((x) => x.toConstant()), + type.toAuxiliary(value) + ); +} diff --git a/src/lib/field-bn254.ts b/src/lib/field-bn254.ts index 295878e92b..60dd2f2ee1 100644 --- a/src/lib/field-bn254.ts +++ b/src/lib/field-bn254.ts @@ -392,7 +392,7 @@ class FieldBn254 { isEven() { if (this.isConstant()) return new BoolBn254(this.toBigInt() % 2n === 0n); - let [, isOddVar, xDiv2Var] = Snarky.exists(2, () => { + let [, isOddVar, xDiv2Var] = Snarky.bn254.exists(2, () => { let bits = Fp.toBits(this.toBigInt()); let isOdd = bits.shift()! ? 1n : 0n; @@ -447,7 +447,7 @@ class FieldBn254 { return new FieldBn254(z); } // create a new witness for z = x*y - let z = Snarky.existsVar(() => + let z = Snarky.bn254.existsVar(() => FieldConst.fromBigint(Fp.mul(this.toBigInt(), toFp(y))) ); // add a multiplication constraint @@ -479,7 +479,7 @@ class FieldBn254 { return new FieldBn254(z); } // create a witness for z = x^(-1) - let z = Snarky.existsVar(() => { + let z = Snarky.bn254.existsVar(() => { let z = Fp.inverse(this.toBigInt()) ?? 0n; return FieldConst.fromBigint(z); }); @@ -546,7 +546,7 @@ class FieldBn254 { return new FieldBn254(Fp.square(this.toBigInt())); } // create a new witness for z = x^2 - let z = Snarky.existsVar(() => + let z = Snarky.bn254.existsVar(() => FieldConst.fromBigint(Fp.square(this.toBigInt())) ); // add a squaring constraint @@ -581,7 +581,7 @@ class FieldBn254 { return new FieldBn254(z); } // create a witness for sqrt(x) - let z = Snarky.existsVar(() => { + let z = Snarky.bn254.existsVar(() => { let z = Fp.sqrt(this.toBigInt()) ?? 0n; return FieldConst.fromBigint(z); }); @@ -599,7 +599,7 @@ class FieldBn254 { } // create witnesses z = 1/x, or z=0 if x=0, // and b = 1 - zx - let [, b, z] = Snarky.exists(2, () => { + let [, b, z] = Snarky.bn254.exists(2, () => { let x = this.toBigInt(); let z = Fp.inverse(x) ?? 0n; let b = Fp.sub(1n, Fp.mul(z, x)); @@ -640,7 +640,7 @@ class FieldBn254 { return this.sub(y).isZero(); } // if both are variables, we create one new variable for x-y so that `isZero` doesn't create two - let xMinusY = Snarky.existsVar(() => + let xMinusY = Snarky.bn254.existsVar(() => FieldConst.fromBigint(Fp.sub(this.toBigInt(), toFp(y))) ); Snarky.bn254.field.assertEqual(this.sub(y).value, xMinusY); diff --git a/src/lib/foreign-curve-bn254.ts b/src/lib/foreign-curve-bn254.ts index f111080307..145f984d3a 100644 --- a/src/lib/foreign-curve-bn254.ts +++ b/src/lib/foreign-curve-bn254.ts @@ -3,7 +3,6 @@ import { CurveAffine, createCurveAffine, } from '../bindings/crypto/elliptic-curve.js'; -import { ProvablePureExtended } from './circuit-value.js'; import { AlmostForeignFieldBn254, ForeignFieldBn254, createForeignFieldBn254 } from './foreign-field-bn254.js'; import { EllipticCurveBn254, PointBn254 } from './gadgets/elliptic-curve-bn254.js'; import { Field3 } from './gadgets/foreign-field-bn254.js'; @@ -11,6 +10,7 @@ import { assert } from './gadgets/common-bn254.js'; import { ProvableBn254 } from './provable-bn254.js'; import { provableFromClass } from '../bindings/lib/provable-snarky.js'; import { FieldConst, FieldVar } from './field-bn254.js'; +import { ProvablePureExtendedBn254 } from './circuit-value-bn254.js'; // external API export { createForeignCurveBn254, ForeignCurveBn254 }; @@ -293,7 +293,7 @@ class ForeignCurveBn254 { static _Bigint?: CurveAffine; static _Field?: typeof AlmostForeignFieldBn254; static _Scalar?: typeof AlmostForeignFieldBn254; - static _provable?: ProvablePureExtended< + static _provable?: ProvablePureExtendedBn254< ForeignCurveBn254, { x: string; y: string } >; diff --git a/src/lib/gadgets/basic-bn254.ts b/src/lib/gadgets/basic-bn254.ts new file mode 100644 index 0000000000..5b177cc71b --- /dev/null +++ b/src/lib/gadgets/basic-bn254.ts @@ -0,0 +1,134 @@ +/** + * Basic gadgets that only use generic gates + */ +import { Fp } from '../../bindings/crypto/finite-field.js'; +import type { FieldBn254, VarField } from '../field-bn254.js'; +import { existsOne, toVar } from './common-bn254.js'; +import { GatesBn254 } from '../gates-bn254.js'; +import { TupleN } from '../util/types.js'; +import { Snarky } from '../../snarky.js'; + +export { assertBoolean, arrayGet, assertOneOf }; + +/** + * Assert that x is either 0 or 1. + */ +function assertBoolean(x: VarField) { + Snarky.bn254.field.assertBoolean(x.value); +} + +// TODO: create constant versions of these and expose on Gadgets + +/** + * Get value from array in O(n) rows. + * + * Assumes that index is in [0, n), returns an unconstrained result otherwise. + * + * Note: This saves 0.5*n constraints compared to equals() + switch() + */ +function arrayGet(array: FieldBn254[], index: FieldBn254) { + let i = toVar(index); + + // witness result + let a = existsOne(() => array[Number(i.toBigInt())].toBigInt()); + + // we prove a === array[j] + z[j]*(i - j) for some z[j], for all j. + // setting j = i, this implies a === array[i] + // thanks to our assumption that the index i is within bounds, we know that j = i for some j + let n = array.length; + for (let j = 0; j < n; j++) { + let zj = existsOne(() => { + let zj = Fp.div( + Fp.sub(a.toBigInt(), array[j].toBigInt()), + Fp.sub(i.toBigInt(), Fp.fromNumber(j)) + ); + return zj ?? 0n; + }); + // prove that z[j]*(i - j) === a - array[j] + // TODO abstract this logic into a general-purpose assertMul() gadget, + // which is able to use the constant coefficient + // (snarky's assert_r1cs somehow leads to much more constraints than this) + if (array[j].isConstant()) { + // zj*i + (-j)*zj + 0*i + array[j] === a + assertBilinear(zj, i, [1n, -BigInt(j), 0n, array[j].toBigInt()], a); + } else { + let aMinusAj = toVar(a.sub(array[j])); + // zj*i + (-j)*zj + 0*i + 0 === (a - array[j]) + assertBilinear(zj, i, [1n, -BigInt(j), 0n, 0n], aMinusAj); + } + } + + return a; +} + +/** + * Assert that a value equals one of a finite list of constants: + * `(x - c1)*(x - c2)*...*(x - cn) === 0` + * + * TODO: what prevents us from getting the same efficiency with snarky DSL code? + */ +function assertOneOf(x: FieldBn254, allowed: [bigint, bigint, ...bigint[]]) { + let xv = toVar(x); + let [c1, c2, ...c] = allowed; + let n = c.length; + if (n === 0) { + // (x - c1)*(x - c2) === 0 + assertBilinear(xv, xv, [1n, -(c1 + c2), 0n, c1 * c2]); + return; + } + // z = (x - c1)*(x - c2) + let z = bilinear(xv, xv, [1n, -(c1 + c2), 0n, c1 * c2]); + + for (let i = 0; i < n; i++) { + if (i < n - 1) { + // z = z*(x - c) + z = bilinear(z, xv, [1n, -c[i], 0n, 0n]); + } else { + // z*(x - c) === 0 + assertBilinear(z, xv, [1n, -c[i], 0n, 0n]); + } + } +} + +// low-level helpers to create generic gates + +/** + * Compute bilinear function of x and y: + * `z = a*x*y + b*x + c*y + d` + */ +function bilinear(x: VarField, y: VarField, [a, b, c, d]: TupleN) { + let z = existsOne(() => { + let x0 = x.toBigInt(); + let y0 = y.toBigInt(); + return a * x0 * y0 + b * x0 + c * y0 + d; + }); + // b*x + c*y - z + a*x*y + d === 0 + GatesBn254.generic( + { left: b, right: c, out: -1n, mul: a, const: d }, + { left: x, right: y, out: z } + ); + return z; +} + +/** + * Assert bilinear equation on x, y and z: + * `a*x*y + b*x + c*y + d === z` + * + * The default for z is 0. + */ +function assertBilinear( + x: VarField, + y: VarField, + [a, b, c, d]: TupleN, + z?: VarField +) { + // b*x + c*y - z? + a*x*y + d === 0 + GatesBn254.generic( + { left: b, right: c, out: z === undefined ? 0n : -1n, mul: a, const: d }, + { left: x, right: y, out: z === undefined ? emptyCell() : z } + ); +} + +function emptyCell() { + return existsOne(() => 0n); +} diff --git a/src/lib/gadgets/bit-slices-bn254.ts b/src/lib/gadgets/bit-slices-bn254.ts new file mode 100644 index 0000000000..a52787e0da --- /dev/null +++ b/src/lib/gadgets/bit-slices-bn254.ts @@ -0,0 +1,156 @@ +/** + * Gadgets for converting between field elements and bit slices of various lengths + */ +import { bigIntToBits } from '../../bindings/crypto/bigint-helpers.js'; +import { BoolBn254 } from '../bool-bn254.js'; +import { FieldBn254 } from '../field-bn254.js'; +import { UInt8 } from '../int.js'; +import { ProvableBn254 } from '../provable-bn254.js'; +import { chunk } from '../util/arrays.js'; +import { assert, exists } from './common-bn254.js'; +import type { Field3 } from './foreign-field-bn254.js'; +import { l } from './range-check-bn254.js'; + +export { bytesToWord, wordToBytes, wordsToBytes, bytesToWords, sliceField3 }; + +// conversion between bytes and multi-byte words + +/** + * Convert an array of UInt8 to a FieldBn254 element. Expects little endian representation. + */ +function bytesToWord(wordBytes: UInt8[]): FieldBn254 { + return wordBytes.reduce((acc, byte, idx) => { + const shift = 1n << BigInt(8 * idx); + return acc.add(byte.value.mul(shift)); + }, FieldBn254.from(0)); +} + +/** + * Convert a FieldBn254 element to an array of UInt8. Expects little endian representation. + * @param bytesPerWord number of bytes per word + */ +function wordToBytes(word: FieldBn254, bytesPerWord = 8): UInt8[] { + let bytes = ProvableBn254.witness(ProvableBn254.Array(UInt8, bytesPerWord), () => { + let w = word.toBigInt(); + return Array.from({ length: bytesPerWord }, (_, k) => + UInt8.from((w >> BigInt(8 * k)) & 0xffn) + ); + }); + + // check decomposition + bytesToWord(bytes).assertEquals(word); + + return bytes; +} + +/** + * Convert an array of FieldBn254 elements to an array of UInt8. Expects little endian representation. + * @param bytesPerWord number of bytes per word + */ +function wordsToBytes(words: FieldBn254[], bytesPerWord = 8): UInt8[] { + return words.flatMap((w) => wordToBytes(w, bytesPerWord)); +} +/** + * Convert an array of UInt8 to an array of FieldBn254 elements. Expects little endian representation. + * @param bytesPerWord number of bytes per word + */ +function bytesToWords(bytes: UInt8[], bytesPerWord = 8): FieldBn254[] { + return chunk(bytes, bytesPerWord).map(bytesToWord); +} + +// conversion between 3-limb foreign fields and arbitrary bit slices + +/** + * ProvableBn254 method for slicing a 3x88-bit bigint into smaller bit chunks of length `chunkSize` + * + * This serves as a range check that the input is in [0, 2^maxBits) + */ +function sliceField3( + [x0, x1, x2]: Field3, + { maxBits, chunkSize }: { maxBits: number; chunkSize: number } +) { + let l_ = Number(l); + assert(maxBits <= 3 * l_, `expected max bits <= 3*${l_}, got ${maxBits}`); + + // first limb + let result0 = sliceField(x0, Math.min(l_, maxBits), chunkSize); + if (maxBits <= l_) return result0.chunks; + maxBits -= l_; + + // second limb + let result1 = sliceField(x1, Math.min(l_, maxBits), chunkSize, result0); + if (maxBits <= l_) return result0.chunks.concat(result1.chunks); + maxBits -= l_; + + // third limb + let result2 = sliceField(x2, maxBits, chunkSize, result1); + return result0.chunks.concat(result1.chunks, result2.chunks); +} + +/** + * ProvableBn254 method for slicing a field element into smaller bit chunks of length `chunkSize`. + * + * This serves as a range check that the input is in [0, 2^maxBits) + * + * If `chunkSize` does not divide `maxBits`, the last chunk will be smaller. + * We return the number of free bits in the last chunk, and optionally accept such a result from a previous call, + * so that this function can be used to slice up a bigint of multiple limbs into homogeneous chunks. + * + * TODO: atm this uses expensive boolean checks for each bit. + * For larger chunks, we should use more efficient range checks. + */ +function sliceField( + x: FieldBn254, + maxBits: number, + chunkSize: number, + leftover?: { chunks: FieldBn254[]; leftoverSize: number } +) { + let bits = exists(maxBits, () => { + let bits = bigIntToBits(x.toBigInt()); + // normalize length + if (bits.length > maxBits) bits = bits.slice(0, maxBits); + if (bits.length < maxBits) + bits = bits.concat(Array(maxBits - bits.length).fill(false)); + return bits.map(BigInt); + }); + + let chunks = []; + let sum = FieldBn254.from(0n); + + // if there's a leftover chunk from a previous sliceField() call, we complete it + if (leftover !== undefined) { + let { chunks: previous, leftoverSize: size } = leftover; + let remainingChunk = FieldBn254.from(0n); + for (let i = 0; i < size; i++) { + let bit = bits[i]; + BoolBn254.check(BoolBn254.Unsafe.ofField(bit)); + remainingChunk = remainingChunk.add(bit.mul(1n << BigInt(i))); + } + sum = remainingChunk = remainingChunk.seal(); + let chunk = previous[previous.length - 1]; + previous[previous.length - 1] = chunk.add( + remainingChunk.mul(1n << BigInt(chunkSize - size)) + ); + } + + let i = leftover?.leftoverSize ?? 0; + for (; i < maxBits; i += chunkSize) { + // prove that chunk has `chunkSize` bits + // TODO: this inner sum should be replaced with a more efficient range check when possible + let chunk = FieldBn254.from(0n); + let size = Math.min(maxBits - i, chunkSize); // last chunk might be smaller + for (let j = 0; j < size; j++) { + let bit = bits[i + j]; + BoolBn254.check(BoolBn254.Unsafe.ofField(bit)); + chunk = chunk.add(bit.mul(1n << BigInt(j))); + } + chunk = chunk.seal(); + // prove that chunks add up to x + sum = sum.add(chunk.mul(1n << BigInt(i))); + chunks.push(chunk); + } + sum.assertEquals(x); + + let leftoverSize = i - maxBits; + return { chunks, leftoverSize } as const; +} diff --git a/src/lib/gadgets/elliptic-curve-bn254.ts b/src/lib/gadgets/elliptic-curve-bn254.ts index cc3d8fcfe8..139549dfc8 100644 --- a/src/lib/gadgets/elliptic-curve-bn254.ts +++ b/src/lib/gadgets/elliptic-curve-bn254.ts @@ -17,9 +17,9 @@ import { import { BoolBn254 } from '../bool-bn254.js'; import { provable } from '../circuit-value-bn254.js'; import { assertPositiveInteger } from '../../bindings/crypto/non-negative.js'; -import { arrayGet, assertBoolean } from './basic.js'; -import { sliceField3 } from './bit-slices.js'; -import { Hashed } from '../provable-types/packed.js'; +import { arrayGet, assertBoolean } from './basic-bn254.js'; +import { sliceField3 } from './bit-slices-bn254.js'; +import { Hashed } from '../provable-types/packed-bn254.js'; // external API export { EllipticCurveBn254, PointBn254, Ecdsa }; diff --git a/src/lib/gadgets/foreign-field-bn254.ts b/src/lib/gadgets/foreign-field-bn254.ts index 2f5788aa56..fb11dcab74 100644 --- a/src/lib/gadgets/foreign-field-bn254.ts +++ b/src/lib/gadgets/foreign-field-bn254.ts @@ -7,12 +7,12 @@ import { } from '../../bindings/crypto/finite-field.js'; import { provableTuple } from '../../bindings/lib/provable-snarky.js'; import { BoolBn254 } from '../bool-bn254.js'; -import { Unconstrained } from '../circuit-value.js'; +import { Unconstrained } from '../circuit-value-bn254.js'; import { FieldBn254 } from '../field-bn254.js'; import { GatesBn254, foreignFieldAdd } from '../gates-bn254.js'; -import { modifiedField } from '../provable-types/fields.js'; +import { modifiedField } from '../provable-types/fields-bn254.js'; import { Tuple, TupleN } from '../util/types.js'; -import { assertOneOf } from './basic.js'; +import { assertOneOf } from './basic-bn254.js'; import { assert, bitSlice, exists, toVar, toVars } from './common-bn254.js'; import { l, diff --git a/src/lib/hash-bn254.ts b/src/lib/hash-bn254.ts new file mode 100644 index 0000000000..38989ae0af --- /dev/null +++ b/src/lib/hash-bn254.ts @@ -0,0 +1,248 @@ +import { HashInput, ProvableExtendedBn254, Struct } from './circuit-value-bn254.js'; +import { Snarky } from '../snarky.js'; +import { FieldBn254 } from './core-bn254.js'; +import { createHashHelpers } from './hash-generic.js'; +import { ProvableBn254 } from './provable-bn254.js'; +import { MlFieldArray } from './ml/fields-bn254.js'; +import { PoseidonBn254 as PoseidonBigint } from '../bindings/crypto/poseidon.js'; +import { assert } from './errors.js'; +import { rangeCheckN } from './gadgets/range-check-bn254.js'; +import { TupleN } from './util/types.js'; + +// external API +export { Poseidon, TokenSymbol }; + +// internal API +export { + ProvableHashable, + HashInput, + HashHelpers, + emptyHashWithPrefix, + hashWithPrefix, + salt, + packToFields, + emptyReceiptChainHash, + hashConstant, + isHashable, +}; + +type Hashable = { toInput: (x: T) => HashInput; empty: () => T }; +type ProvableHashable = ProvableBn254 & Hashable; + +class Sponge { + #sponge: unknown; + + constructor() { + let isChecked = ProvableBn254.inCheckedComputation(); + this.#sponge = Snarky.bn254.poseidon.sponge.create(isChecked); + } + + absorb(x: FieldBn254) { + Snarky.bn254.poseidon.sponge.absorb(this.#sponge, x.value); + } + + squeeze(): FieldBn254 { + return FieldBn254(Snarky.bn254.poseidon.sponge.squeeze(this.#sponge)); + } +} + +const Poseidon = { + hash(input: FieldBn254[]) { + if (isConstant(input)) { + return FieldBn254(PoseidonBigint.hash(toBigints(input))); + } + return Poseidon.update(Poseidon.initialState(), input)[0]; + }, + + update(state: [FieldBn254, FieldBn254, FieldBn254], input: FieldBn254[]) { + if (isConstant(state) && isConstant(input)) { + let newState = PoseidonBigint.update(toBigints(state), toBigints(input)); + return TupleN.fromArray(3, newState.map(FieldBn254)); + } + + let newState = Snarky.bn254.poseidon.update( + MlFieldArray.to(state), + MlFieldArray.to(input) + ); + return MlFieldArray.from(newState) as [FieldBn254, FieldBn254, FieldBn254]; + }, + + hashWithPrefix(prefix: string, input: FieldBn254[]) { + let init = Poseidon.update(Poseidon.initialState(), [ + prefixToField(prefix), + ]); + return Poseidon.update(init, input)[0]; + }, + + initialState(): [FieldBn254, FieldBn254, FieldBn254] { + return [FieldBn254(0), FieldBn254(0), FieldBn254(0)]; + }, + + hashToGroup(input: FieldBn254[]) { + if (isConstant(input)) { + let result = PoseidonBigint.hashToGroup(toBigints(input)); + assert(result !== undefined, 'hashToGroup works on all inputs'); + let { x, y } = result; + return { + x: FieldBn254(x), + y: { x0: FieldBn254(y.x0), x1: FieldBn254(y.x1) }, + }; + } + + // TODO: This is not used for the verifier circuit + // y = sqrt(y^2) + let [, xv, yv] = Snarky.poseidon.hashToGroup(MlFieldArray.to(input)); + + let x = FieldBn254(xv); + let y = FieldBn254(yv); + + let x0 = ProvableBn254.witness(FieldBn254, () => { + // the even root of y^2 will become x0, so the APIs are uniform + let isEven = y.toBigInt() % 2n === 0n; + + // we just change the order so the even root is x0 + // y.mul(-1); is the second root of sqrt(y^2) + return isEven ? y : y.mul(-1); + }); + + let x1 = x0.mul(-1); + + // we check that either x0 or x1 match the original root y + y.equals(x0).or(y.equals(x1)).assertTrue(); + + return { x, y: { x0, x1 } }; + }, + + /** + * Hashes a provable type efficiently. + * + * ```ts + * let skHash = Poseidon.hashPacked(PrivateKey, secretKey); + * ``` + * + * Note: Instead of just doing `Poseidon.hash(value.toFields())`, this + * uses the `toInput()` method on the provable type to pack the input into as few + * field elements as possible. This saves constraints because packing has a much + * lower per-field element cost than hashing. + */ + hashPacked(type: Hashable, value: T) { + let input = type.toInput(value); + let packed = packToFields(input); + return Poseidon.hash(packed); + }, + + Sponge, +}; + +function hashConstant(input: FieldBn254[]) { + return FieldBn254(PoseidonBigint.hash(toBigints(input))); +} + +const HashHelpers = createHashHelpers(FieldBn254, Poseidon); +let { salt, emptyHashWithPrefix, hashWithPrefix } = HashHelpers; + +// same as Random_oracle.prefix_to_field in OCaml +function prefixToField(prefix: string) { + if (prefix.length * 8 >= 255) throw Error('prefix too long'); + let bits = [...prefix] + .map((char) => { + // convert char to 8 bits + let bits = []; + for (let j = 0, c = char.charCodeAt(0); j < 8; j++, c >>= 1) { + bits.push(!!(c & 1)); + } + return bits; + }) + .flat(); + return FieldBn254.fromBits(bits); +} + +/** + * Convert the {fields, packed} hash input representation to a list of field elements + * Random_oracle_input.Chunked.pack_to_fields + */ +function packToFields({ fields = [], packed = [] }: HashInput) { + if (packed.length === 0) return fields; + let packedBits = []; + let currentPackedField = FieldBn254(0); + let currentSize = 0; + for (let [field, size] of packed) { + currentSize += size; + if (currentSize < 255) { + currentPackedField = currentPackedField + .mul(FieldBn254(1n << BigInt(size))) + .add(field); + } else { + packedBits.push(currentPackedField); + currentSize = size; + currentPackedField = field; + } + } + packedBits.push(currentPackedField); + return fields.concat(packedBits); +} + +function isHashable(obj: any): obj is Hashable { + if (!obj) { + return false; + } + const hasToInput = 'toInput' in obj && typeof obj.toInput === 'function'; + const hasEmpty = 'empty' in obj && typeof obj.empty === 'function'; + return hasToInput && hasEmpty; +} + +const TokenSymbolPure: ProvableExtendedBn254< + { symbol: string; field: FieldBn254 }, + string +> = { + toFields({ field }) { + return [field]; + }, + toAuxiliary(value) { + return [value?.symbol ?? '']; + }, + fromFields([field], [symbol]) { + return { symbol, field }; + }, + sizeInFields() { + return 1; + }, + check({ field }: TokenSymbol) { + rangeCheckN(48, field); + }, + toJSON({ symbol }) { + return symbol; + }, + fromJSON(symbol: string) { + let field = prefixToField(symbol); + return { symbol, field }; + }, + toInput({ field }) { + return { packed: [[field, 48]] }; + }, + empty() { + return { symbol: '', field: FieldBn254(0n) }; + }, +}; +class TokenSymbol extends Struct(TokenSymbolPure) { + static from(symbol: string): TokenSymbol { + let bytesLength = new TextEncoder().encode(symbol).length; + if (bytesLength > 6) + throw Error( + `Token symbol ${symbol} should be a maximum of 6 bytes, but is ${bytesLength}` + ); + let field = prefixToField(symbol); + return { symbol, field }; + } +} + +function emptyReceiptChainHash() { + return emptyHashWithPrefix('CodaReceiptEmpty'); +} + +function isConstant(fields: FieldBn254[]) { + return fields.every((x) => x.isConstant()); +} +function toBigints(fields: FieldBn254[]) { + return fields.map((x) => x.toBigInt()); +} diff --git a/src/lib/provable-types/packed-bn254.ts b/src/lib/provable-types/packed-bn254.ts new file mode 100644 index 0000000000..adfbd7786b --- /dev/null +++ b/src/lib/provable-types/packed-bn254.ts @@ -0,0 +1,271 @@ +import { provableFromClass } from '../../bindings/lib/provable-snarky-bn254.js'; +import { + HashInput, + ProvableExtendedBn254, + Unconstrained, +} from '../circuit-value-bn254.js'; +import { FieldBn254 } from '../field-bn254.js'; +import { assert } from '../gadgets/common-bn254.js'; +import { Poseidon, ProvableHashable, packToFields } from '../hash-bn254.js'; +import { ProvableBn254 } from '../provable-bn254.js'; +import { fields, modifiedField } from './fields-bn254.js'; + +export { Packed, Hashed }; + +/** + * `Packed` is a "packed" representation of any type `T`. + * + * "Packed" means that field elements which take up fewer than 254 bits are packed together into + * as few field elements as possible. + * + * For example, you can pack several Bools (1 bit) or UInt32s (32 bits) into a single field element. + * + * Using a packed representation can make sense in provable code where the number of constraints + * depends on the number of field elements per value. + * + * For example, `ProvableBn254.if(bool, x, y)` takes O(n) constraints, where n is the number of field + * elements in x and y. + * + * Usage: + * + * ```ts + * // define a packed type from a type + * let PackedType = Packed.create(MyType); + * + * // pack a value + * let packed = PackedType.pack(value); + * + * // ... operations on packed values, more efficient than on plain values ... + * + * // unpack a value + * let value = packed.unpack(); + * ``` + * + * **Warning**: Packing only makes sense where packing actually reduces the number of field elements. + * For example, it doesn't make sense to pack a _single_ Bool, because it will be 1 field element before + * and after packing. On the other hand, it does makes sense to pack a type that holds 10 or 20 Bools. + */ +class Packed { + packed: FieldBn254[]; + value: Unconstrained; + + /** + * Create a packed representation of `type`. You can then use `PackedType.pack(x)` to pack a value. + */ + static create(type: ProvableExtendedBn254): typeof Packed & { + provable: ProvableHashable>; + } { + // compute size of packed representation + let input = type.toInput(type.empty()); + let packedSize = countFields(input); + + return class Packed_ extends Packed { + static _innerProvable = type; + static _provable = provableFromClass(Packed_, { + packed: fields(packedSize), + value: Unconstrained.provable, + }) as ProvableHashable>; + + static empty(): Packed { + return Packed_.pack(type.empty()); + } + + static get provable() { + assert(this._provable !== undefined, 'Packed not initialized'); + return this._provable; + } + }; + } + + constructor(packed: FieldBn254[], value: Unconstrained) { + this.packed = packed; + this.value = value; + } + + /** + * Pack a value. + */ + static pack(x: T): Packed { + let type = this.innerProvable; + let input = type.toInput(x); + let packed = packToFields(input); + let unconstrained = Unconstrained.witness(() => + ProvableBn254.toConstant(type, x) + ); + return new this(packed, unconstrained); + } + + /** + * Unpack a value. + */ + unpack(): T { + let value = ProvableBn254.witness(this.Constructor.innerProvable, () => + this.value.get() + ); + + // prove that the value packs to the packed fields + let input = this.Constructor.innerProvable.toInput(value); + let packed = packToFields(input); + for (let i = 0; i < this.packed.length; i++) { + this.packed[i].assertEquals(packed[i]); + } + + return value; + } + + toFields(): FieldBn254[] { + return this.packed; + } + + // dynamic subclassing infra + static _provable: ProvableHashable> | undefined; + static _innerProvable: ProvableExtendedBn254 | undefined; + + get Constructor(): typeof Packed { + return this.constructor as typeof Packed; + } + + static get innerProvable(): ProvableExtendedBn254 { + assert(this._innerProvable !== undefined, 'Packed not initialized'); + return this._innerProvable; + } +} + +function countFields(input: HashInput) { + let n = input.fields?.length ?? 0; + let pendingBits = 0; + + for (let [, bits] of input.packed ?? []) { + pendingBits += bits; + if (pendingBits >= FieldBn254.sizeInBits) { + n++; + pendingBits = bits; + } + } + if (pendingBits > 0) n++; + + return n; +} + +/** + * `Hashed` represents a type `T` by its hash. + * + * Since a hash is only a single field element, this can be more efficient in provable code + * where the number of constraints depends on the number of field elements per value. + * + * For example, `ProvableBn254.if(bool, x, y)` takes O(n) constraints, where n is the number of field + * elements in x and y. With Hashed, this is reduced to O(1). + * + * The downside is that you will pay the overhead of hashing your values, so it helps to experiment + * in which parts of your code a hashed representation is beneficial. + * + * Usage: + * + * ```ts + * // define a hashed type from a type + * let HashedType = Hashed.create(MyType); + * + * // hash a value + * let hashed = HashedType.hash(value); + * + * // ... operations on hashes, more efficient than on plain values ... + * + * // unhash to get the original value + * let value = hashed.unhash(); + * ``` + */ +class Hashed { + hash: FieldBn254; + value: Unconstrained; + + /** + * Create a hashed representation of `type`. You can then use `HashedType.hash(x)` to wrap a value in a `Hashed`. + */ + static create( + type: ProvableHashable, + hash?: (t: T) => FieldBn254 + ): typeof Hashed & { + provable: ProvableHashable>; + } { + let _hash = hash ?? ((t: T) => Poseidon.hashPacked(type, t)); + + let dummyHash = _hash(type.empty()); + + return class Hashed_ extends Hashed { + static _innerProvable = type; + static _provable = provableFromClass(Hashed_, { + hash: modifiedField({ empty: () => dummyHash }), + value: Unconstrained.provable, + }) as ProvableHashable>; + + static _hash = _hash satisfies (t: T) => FieldBn254; + + static empty(): Hashed { + return new this(dummyHash, Unconstrained.from(type.empty())); + } + + static get provable() { + assert(this._provable !== undefined, 'Hashed not initialized'); + return this._provable; + } + }; + } + + constructor(hash: FieldBn254, value: Unconstrained) { + this.hash = hash; + this.value = value; + } + + static _hash(_: any): FieldBn254 { + assert(false, 'Hashed not initialized'); + } + + /** + * Wrap a value, and represent it by its hash in provable code. + * + * ```ts + * let hashed = HashedType.hash(value); + * ``` + * + * Optionally, if you already have the hash, you can pass it in and avoid recomputing it. + */ + static hash(value: T, hash?: FieldBn254): Hashed { + hash ??= this._hash(value); + let unconstrained = Unconstrained.witness(() => + ProvableBn254.toConstant(this.innerProvable, value) + ); + return new this(hash, unconstrained); + } + + /** + * Unwrap a value from its hashed variant. + */ + unhash(): T { + let value = ProvableBn254.witness(this.Constructor.innerProvable, () => + this.value.get() + ); + + // prove that the value hashes to the hash + let hash = this.Constructor._hash(value); + this.hash.assertEquals(hash); + + return value; + } + + toFields(): FieldBn254[] { + return [this.hash]; + } + + // dynamic subclassing infra + static _provable: ProvableHashable> | undefined; + static _innerProvable: ProvableHashable | undefined; + + get Constructor(): typeof Hashed { + return this.constructor as typeof Hashed; + } + + static get innerProvable(): ProvableHashable { + assert(this._innerProvable !== undefined, 'Hashed not initialized'); + return this._innerProvable; + } +} diff --git a/src/mina b/src/mina index 716f8eefff..10bd2aa90f 160000 --- a/src/mina +++ b/src/mina @@ -1 +1 @@ -Subproject commit 716f8eefff86d00456a64f5a8c748bfb45b0a507 +Subproject commit 10bd2aa90f0872a7f61983301870a68946eeaaa1 diff --git a/src/snarky.d.ts b/src/snarky.d.ts index 508a8731b5..44d88615c3 100644 --- a/src/snarky.d.ts +++ b/src/snarky.d.ts @@ -887,7 +887,7 @@ declare const Snarky: { publicInputSize: number, publicInput: MlArray, keypair: Snarky.Bn254.Keypair - ): Snarky.Bn254.Proof; + ): string; /** * Verifies a proof using the public input, the proof and the verification key of the circuit.