diff --git a/CHANGELOG.md b/CHANGELOG.md index e76325b791..433daf8f5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,12 +15,20 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm _Security_ in case of vulnerabilities. --> -## [Unreleased](https://github.com/o1-labs/o1js/compare/be748e42e...HEAD) +## [Unreleased](https://github.com/o1-labs/o1js/compare/e5d1e0f...HEAD) ### Breaking changes - Reduce number of constraints of ECDSA verification by 5%, which breaks deployed contracts using ECDSA https://github.com/o1-labs/o1js/pull/1376 +### Added + +- Provable type `Packed` to pack small field elements into fewer field elements https://github.com/o1-labs/o1js/pull/1376 +- Provable type `Hashed` to represent provable types by their hash https://github.com/o1-labs/o1js/pull/1377 + - This also exposes `Poseidon.hashPacked()` to efficiently hash an arbitrary type + +## [0.15.4](https://github.com/o1-labs/o1js/compare/be748e42e...e5d1e0f) + ### Changed - Improve performance of Wasm Poseidon hashing by a factor of 13x https://github.com/o1-labs/o1js/pull/1378 @@ -33,7 +41,6 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Configurable `networkId` when declaring a Mina instance. https://github.com/o1-labs/o1js/pull/1387 - Defaults to `"testnet"`, the other option is `"mainnet"` - The `networkId` parameter influences the algorithm used for signatures, and ensures that testnet transactions can't be replayed on mainnet -- Provable type `Packed` to pack small field elements into fewer field elements https://github.com/o1-labs/o1js/pull/1376 ## [0.15.3](https://github.com/o1-labs/o1js/compare/1ad7333e9e...be748e42e) diff --git a/src/bindings b/src/bindings index a9354195b4..772ce4ba92 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit a9354195b4bb369cb9da827ccdfe81628385c1b6 +Subproject commit 772ce4ba92e63453253250bb706339016a8d1e8c diff --git a/src/examples/crypto/ecdsa/ecdsa.ts b/src/examples/crypto/ecdsa/ecdsa.ts index bebe6484fb..d681a8037e 100644 --- a/src/examples/crypto/ecdsa/ecdsa.ts +++ b/src/examples/crypto/ecdsa/ecdsa.ts @@ -4,7 +4,6 @@ import { createEcdsa, createForeignCurve, Bool, - Keccak, Bytes, } from 'o1js'; diff --git a/src/index.ts b/src/index.ts index 57cf62e9ae..8fb4e0c9ef 100644 --- a/src/index.ts +++ b/src/index.ts @@ -36,7 +36,7 @@ export { Provable } from './lib/provable.js'; export { Circuit, Keypair, public_, circuitMain } from './lib/circuit.js'; export { UInt32, UInt64, Int64, Sign, UInt8 } from './lib/int.js'; export { Bytes } from './lib/provable-types/provable-types.js'; -export { Packed } from './lib/provable-types/packed.js'; +export { Packed, Hashed } from './lib/provable-types/packed.js'; export { Gadgets } from './lib/gadgets/gadgets.js'; export { Types } from './bindings/mina-transaction/types.js'; diff --git a/src/lib/gadgets/ecdsa.unit-test.ts b/src/lib/gadgets/ecdsa.unit-test.ts index b03266b1a2..250b088709 100644 --- a/src/lib/gadgets/ecdsa.unit-test.ts +++ b/src/lib/gadgets/ecdsa.unit-test.ts @@ -129,7 +129,7 @@ let msgHash = ); const ia = initialAggregator(Secp256k1); -const config = { G: { windowSize: 4 }, P: { windowSize: 3 }, ia }; +const config = { G: { windowSize: 4 }, P: { windowSize: 4 }, ia }; let program = ZkProgram({ name: 'ecdsa', diff --git a/src/lib/gadgets/elliptic-curve.ts b/src/lib/gadgets/elliptic-curve.ts index 865113fc2f..87876e788c 100644 --- a/src/lib/gadgets/elliptic-curve.ts +++ b/src/lib/gadgets/elliptic-curve.ts @@ -19,7 +19,7 @@ import { provable } from '../circuit_value.js'; import { assertPositiveInteger } from '../../bindings/crypto/non-negative.js'; import { arrayGet, assertBoolean } from './basic.js'; import { sliceField3 } from './bit-slices.js'; -import { Packed } from '../provable-types/packed.js'; +import { Hashed } from '../provable-types/packed.js'; // external API export { EllipticCurve, Point, Ecdsa }; @@ -228,7 +228,7 @@ function verifyEcdsa( G?: { windowSize: number; multiples?: Point[] }; P?: { windowSize: number; multiples?: Point[] }; ia?: point; - } = { G: { windowSize: 4 }, P: { windowSize: 3 } } + } = { G: { windowSize: 4 }, P: { windowSize: 4 } } ) { // constant case if ( @@ -414,12 +414,12 @@ function multiScalarMul( sliceField3(s, { maxBits, chunkSize: windowSizes[i] }) ); - // pack points to make array access more efficient - // a Point is 6 x 88-bit field elements, which are packed into 3 field elements - const PackedPoint = Packed.create(Point.provable); + // hash points to make array access more efficient + // a Point is 6 field elements, the hash is just 1 field element + const HashedPoint = Hashed.create(Point.provable); - let packedTables = tables.map((table) => - table.map((point) => PackedPoint.pack(point)) + let hashedTables = tables.map((table) => + table.map((point) => HashedPoint.hash(point)) ); ia ??= initialAggregator(Curve); @@ -436,10 +436,10 @@ function multiScalarMul( windowSize === 1 ? points[j] : arrayGetGeneric( - PackedPoint.provable, - packedTables[j], + HashedPoint.provable, + hashedTables[j], sj - ).unpack(); + ).unhash(); // ec addition let added = add(sum, sjP, Curve); diff --git a/src/lib/hash.ts b/src/lib/hash.ts index 1a55da5181..6f4dbc9347 100644 --- a/src/lib/hash.ts +++ b/src/lib/hash.ts @@ -14,6 +14,7 @@ export { Poseidon, TokenSymbol }; // internal API export { + ProvableHashable, HashInput, HashHelpers, emptyHashWithPrefix, @@ -24,6 +25,9 @@ export { hashConstant, }; +type Hashable = { toInput: (x: T) => HashInput; empty: () => T }; +type ProvableHashable = Provable & Hashable; + class Sponge { #sponge: unknown; @@ -96,6 +100,24 @@ const Poseidon = { 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); + }, + initialState(): [Field, Field, Field] { return [Field(0), Field(0), Field(0)]; }, diff --git a/src/lib/provable-types/packed.ts b/src/lib/provable-types/packed.ts index 2fcb67280d..a12b9cbb54 100644 --- a/src/lib/provable-types/packed.ts +++ b/src/lib/provable-types/packed.ts @@ -6,11 +6,11 @@ import { } from '../circuit_value.js'; import { Field } from '../field.js'; import { assert } from '../gadgets/common.js'; -import { Poseidon, packToFields } from '../hash.js'; +import { Poseidon, ProvableHashable, packToFields } from '../hash.js'; import { Provable } from '../provable.js'; -import { fields } from './fields.js'; +import { fields, modifiedField } from './fields.js'; -export { Packed }; +export { Packed, Hashed }; /** * Packed is a "packed" representation of any type T. @@ -63,6 +63,10 @@ class Packed { packed: fields(packedSize), value: Unconstrained.provable, }) as ProvableHashable>; + + static empty(): Packed { + return Packed_.pack(type.empty()); + } }; } @@ -120,8 +124,6 @@ class Packed { } } -type ProvableHashable = Provable & { toInput: (x: T) => HashInput }; - function countFields(input: HashInput) { let n = input.fields?.length ?? 0; let pendingBits = 0; @@ -137,3 +139,114 @@ function countFields(input: HashInput) { 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, `Provable.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: Field; + 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) => Field + ): typeof Hashed { + 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) => Field; + + static empty(): Hashed { + return new this(dummyHash, Unconstrained.from(type.empty())); + } + }; + } + + constructor(hash: Field, value: Unconstrained) { + this.hash = hash; + this.value = value; + } + + static _hash(_: any): Field { + assert(false, 'Hashed not initialized'); + } + + /** + * Wrap a value, and represent it by its hash in provable code. + */ + static hash(value: T): Hashed { + let hash = this._hash(value); + return new this(hash, Unconstrained.from(value)); + } + + /** + * Unwrap a value from its hashed variant. + */ + unhash(): T { + let value = Provable.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(): Field[] { + 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 provable(): ProvableHashable> { + assert(this._provable !== undefined, 'Hashed not initialized'); + return this._provable; + } + static get innerProvable(): ProvableHashable { + assert(this._innerProvable !== undefined, 'Hashed not initialized'); + return this._innerProvable; + } +} diff --git a/tests/vk-regression/vk-regression.json b/tests/vk-regression/vk-regression.json index acadee1f6b..2697fed274 100644 --- a/tests/vk-regression/vk-regression.json +++ b/tests/vk-regression/vk-regression.json @@ -236,29 +236,29 @@ } }, "ecdsa-only": { - "digest": "529de0fe555c4359b6a25097cc0c873736532f64c045155f68aa868517632e4", + "digest": "2a2beadf929015559514abb88dd77694dbe62a6a5904e5a9d05ab21a3f293c0c", "methods": { "verifySignedHash": { - "rows": 28334, - "digest": "e8bcb74aa4278de6e4c31d9fec724f81" + "rows": 28186, + "digest": "dd43cd30a8277b5af02fd85a4762a488" } }, "verificationKey": { - "data": "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", - "hash": "15135058674307280144061130356720654447302609112077173286437081384990231021958" + "data": "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", + "hash": "16626558875595050675741741208497473516532634598762284173771479275503819571624" } }, "ecdsa": { - "digest": "20942077320476aefd62a3271f60caf17cd442039d7572529bb77315db971d17", + "digest": "2f78a9c0307e6f9c6872499c3ca7c412f5771a7ac7e7e88c351ed1b62d6ac408", "methods": { "verifyEcdsa": { - "rows": 42832, - "digest": "29cb625b78e1ccf5dc4e897ecf08aca1" + "rows": 42684, + "digest": "2bb042b0fa7bbbb32f7e77d392f43d2c" } }, "verificationKey": { - "data": "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", - "hash": "6055195771858836384417585817623837831345819726617689986064045716966540411450" + "data": "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", + "hash": "13907522711254991952405567976395529829664716172124500348498751038796408381729" } }, "sha256": {