Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Packed provable type which can save constraints #1376

Merged
merged 13 commits into from
Feb 1, 2024
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,14 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

## [Unreleased](https://github.com/o1-labs/o1js/compare/08ba27329...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

- **SHA256 hash function** exposed via `Hash.SHA2_256` or `Gadgets.SHA256`. https://github.com/o1-labs/o1js/pull/1285
- Provable type `Packed<T>` to pack small field elements into fewer field elements https://github.com/o1-labs/o1js/pull/1376

### Fixed

Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +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 { Gadgets } from './lib/gadgets/gadgets.js';
export { Types } from './bindings/mina-transaction/types.js';

Expand Down
8 changes: 7 additions & 1 deletion src/lib/circuit_value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -555,12 +555,18 @@ and Provable.asProver() blocks, which execute outside the proof.
);
}

static provable: Provable<Unconstrained<any>> = {
static provable: Provable<Unconstrained<any>> & {
toInput: (x: Unconstrained<any>) => {
fields?: Field[];
packed?: [Field, number][];
};
} = {
sizeInFields: () => 0,
toFields: () => [],
toAuxiliary: (t?: any) => [t ?? new Unconstrained(false)],
fromFields: (_, [t]) => t,
check: () => {},
toInput: () => ({}),
};
}

Expand Down
15 changes: 14 additions & 1 deletion src/lib/gadgets/elliptic-curve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +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';

// external API
export { EllipticCurve, Point, Ecdsa };
Expand Down Expand Up @@ -413,6 +414,14 @@ 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);

let packedTables = tables.map((table) =>
table.map((point) => PackedPoint.pack(point))
);

ia ??= initialAggregator(Curve);
let sum = Point.from(ia);

Expand All @@ -426,7 +435,11 @@ function multiScalarMul(
let sjP =
windowSize === 1
? points[j]
: arrayGetGeneric(Point.provable, tables[j], sj);
: arrayGetGeneric(
PackedPoint.provable,
packedTables[j],
sj
).unpack();

// ec addition
let added = add(sum, sjP, Curve);
Expand Down
9 changes: 8 additions & 1 deletion src/lib/gadgets/foreign-field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Bool } from '../bool.js';
import { Unconstrained } from '../circuit_value.js';
import { Field } from '../field.js';
import { Gates, foreignFieldAdd } from '../gates.js';
import { modifiedField } from '../provable-types/fields.js';
import { Tuple, TupleN } from '../util/types.js';
import { assertOneOf } from './basic.js';
import { assert, bitSlice, exists, toVar, toVars } from './common.js';
Expand Down Expand Up @@ -427,6 +428,12 @@ function equals(x: Field3, c: bigint, f: bigint) {
}
}

const provableLimb = modifiedField({
toInput(x) {
return { packed: [[x, Number(l)]] };
},
});

const Field3 = {
/**
* Turn a bigint into a 3-tuple of Fields
Expand Down Expand Up @@ -462,7 +469,7 @@ const Field3 = {
* Note: Witnessing this creates a plain tuple of field elements without any implicit
* range checks.
*/
provable: provableTuple([Field, Field, Field]),
provable: provableTuple([provableLimb, provableLimb, provableLimb]),
};

type Field2 = [Field, Field];
Expand Down
44 changes: 44 additions & 0 deletions src/lib/provable-types/fields.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { ProvablePureExtended } from '../circuit_value.js';
import { Field } from '../field.js';

export { modifiedField, fields };

const zero = new Field(0);

// provable for a single field element

const ProvableField: ProvablePureExtended<Field, string> = {
sizeInFields: () => 1,
toFields: (x) => [x],
toAuxiliary: () => [],
fromFields: ([x]) => x,
check: () => {},
toInput: (x) => ({ fields: [x] }),
toJSON: Field.toJSON,
fromJSON: Field.fromJSON,
empty: () => zero,
};

function modifiedField(
methods: Partial<ProvablePureExtended<Field, string>>
): ProvablePureExtended<Field, string> {
return Object.assign({}, ProvableField, methods);
}

// provable for a fixed-size array of field elements

let id = <T>(t: T) => t;

function fields(length: number): ProvablePureExtended<Field[], string[]> {
return {
sizeInFields: () => length,
toFields: id,
toAuxiliary: () => [],
fromFields: id,
check: () => {},
toInput: (x) => ({ fields: x }),
toJSON: (x) => x.map(Field.toJSON),
fromJSON: (x) => x.map(Field.fromJSON),
empty: () => new Array(length).fill(zero),
};
}
139 changes: 139 additions & 0 deletions src/lib/provable-types/packed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { provableFromClass } from '../../bindings/lib/provable-snarky.js';
import {
HashInput,
ProvableExtended,
Unconstrained,
} from '../circuit_value.js';
import { Field } from '../field.js';
import { assert } from '../gadgets/common.js';
import { Poseidon, packToFields } from '../hash.js';
import { Provable } from '../provable.js';
import { fields } from './fields.js';

export { Packed };

/**
* Packed<T> 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, `Provable.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<T> {
packed: Field[];
value: Unconstrained<T>;

/**
* Create a packed representation of `type`. You can then use `PackedType.pack(x)` to pack a value.
*/
static create<T>(type: ProvableExtended<T>): typeof Packed<T> {
// compute size of packed representation
let input = type.toInput(type.empty());
let packedSize = countFields(input);

return class Packed_ extends Packed<T> {
static _innerProvable = type;
static _provable = provableFromClass(Packed_, {
packed: fields(packedSize),
value: Unconstrained.provable,
}) as ProvableHashable<Packed<T>>;
};
}

constructor(packed: Field[], value: Unconstrained<T>) {
this.packed = packed;
this.value = value;
}

/**
* Pack a value.
*/
static pack<T>(x: T): Packed<T> {
let input = this.innerProvable.toInput(x);
let packed = packToFields(input);
return new this(packed, Unconstrained.from(x));
}

/**
* Unpack a value.
*/
unpack(): T {
let value = Provable.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(): Field[] {
return this.packed;
}

// dynamic subclassing infra
static _provable: ProvableHashable<Packed<any>> | undefined;
static _innerProvable: ProvableExtended<any> | undefined;

get Constructor(): typeof Packed {
return this.constructor as typeof Packed;
}

static get provable(): ProvableHashable<Packed<any>> {
assert(this._provable !== undefined, 'Packed not initialized');
return this._provable;
}
static get innerProvable(): ProvableExtended<any> {
assert(this._innerProvable !== undefined, 'Packed not initialized');
return this._innerProvable;
}
}

type ProvableHashable<T> = Provable<T> & { toInput: (x: T) => HashInput };

function countFields(input: HashInput) {
let n = input.fields?.length ?? 0;
let pendingBits = 0;

for (let [, bits] of input.packed ?? []) {
pendingBits += bits;
if (pendingBits >= Field.sizeInBits) {
n++;
pendingBits = bits;
}
}
if (pendingBits > 0) n++;

return n;
}
20 changes: 10 additions & 10 deletions tests/vk-regression/vk-regression.json
Original file line number Diff line number Diff line change
Expand Up @@ -236,29 +236,29 @@
}
},
"ecdsa-only": {
"digest": "1e6bef24a2e02b573363f3512822d401d53ec7220c8d5837cd49691c19723028",
"digest": "529de0fe555c4359b6a25097cc0c873736532f64c045155f68aa868517632e4",
"methods": {
"verifySignedHash": {
"rows": 30680,
"digest": "5fe00efee7ecfb82b3ca69c8dd23d07f"
"rows": 28334,
"digest": "e8bcb74aa4278de6e4c31d9fec724f81"
}
},
"verificationKey": {
"data": "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",
"hash": "13090090603196708539583054794074329820941412942119534377592583560995629520780"
"data": "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",
"hash": "15135058674307280144061130356720654447302609112077173286437081384990231021958"
}
},
"ecdsa": {
"digest": "3e80a93d93a93f6152d70a9c21a2979ff0094c2d648a67c6d2daa4b2b0d18309",
"digest": "20942077320476aefd62a3271f60caf17cd442039d7572529bb77315db971d17",
"methods": {
"verifyEcdsa": {
"rows": 45178,
"digest": "0b6ce4cc658bcca79b1b1373569aa9b9"
"rows": 42832,
"digest": "29cb625b78e1ccf5dc4e897ecf08aca1"
}
},
"verificationKey": {
"data": "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",
"hash": "25447212082831819715054236631079960883754611880602728284997977929479384060913"
"data": "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",
"hash": "6055195771858836384417585817623837831345819726617689986064045716966540411450"
}
},
"sha256": {
Expand Down