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

Expose some types and gadgets #1860

Merged
merged 13 commits into from
Oct 16, 2024
8 changes: 7 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { TupleN } from './lib/util/types.js';
export type { ProvablePure } from './lib/provable/types/provable-intf.js';
export { Ledger, initializeBindings } from './snarky.js';
export { Field, Bool, Group, Scalar } from './lib/provable/wrapped.js';
Expand Down Expand Up @@ -37,7 +38,12 @@ export type {
FlexibleProvablePure,
InferProvable,
} from './lib/provable/types/struct.js';
export { From } from './bindings/lib/provable-generic.js';
export {
From,
InferValue,
InferJson,
IsPure,
} from './bindings/lib/provable-generic.js';
export { ProvableType } from './lib/provable/types/provable-intf.js';
export {
provable,
Expand Down
13 changes: 1 addition & 12 deletions src/lib/proof-system/zkprogram.ts
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some unused imports and an unused method purged here

Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { EmptyUndefined, EmptyVoid } from '../../bindings/lib/generic.js';
import { Snarky, initializeBindings, withThreadPool } from '../../snarky.js';
import { Pickles, Gate } from '../../snarky.js';
import { Field, Bool } from '../provable/wrapped.js';
import { Field } from '../provable/wrapped.js';
import {
FlexibleProvable,
FlexibleProvablePure,
InferProvable,
ProvablePureExtended,
Expand All @@ -12,7 +11,6 @@ import {
import {
InferProvableType,
provable,
provablePure,
} from '../provable/types/provable-derivers.js';
import { Provable } from '../provable/provable.js';
import { assert, prettifyStacktracePromise } from '../util/errors.js';
Expand Down Expand Up @@ -528,15 +526,6 @@ function isProvable(type: unknown): type is ProvableType<unknown> {
);
}

function isProofType(type: unknown): type is typeof ProofBase {
// the third case covers subclasses
return (
type === Proof ||
type === DynamicProof ||
(typeof type === 'function' && type.prototype instanceof ProofBase)
);
}

function isDynamicProof(
type: Subclass<typeof ProofBase>
): type is Subclass<typeof DynamicProof> {
Expand Down
9 changes: 7 additions & 2 deletions src/lib/provable/gadgets/basic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,18 @@ function assertMul(
*
* Assumes that index is in [0, n), returns an unconstrained result otherwise.
*
* Note: This saves 0.5*n constraints compared to equals() + switch()
* Note: This saves 0.5*n constraints compared to equals() + switch() even if equals() were implemented optimally.
*/
function arrayGet(array: Field[], index: Field) {
// if index is constant, we can return the value directly
if (index.isConstant()) {
return array[Number(index.toBigInt())] ?? createField(0n);
}

let i = toVar(index);

// witness result
let a = existsOne(() => array[Number(i.toBigInt())].toBigInt());
let a = existsOne(() => array[Number(i.toBigInt())].toBigInt() ?? 0n);

// we prove a === array[j] + z[j]*(i - j) for some z[j], for all j.
// setting j = i, this implies a === array[i]
Expand Down
19 changes: 19 additions & 0 deletions src/lib/provable/gadgets/gadgets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,29 @@ import {
import { divMod32, addMod32 } from './arithmetic.js';
import { SHA256 } from './sha256.js';
import { rangeCheck3x12 } from './lookup.js';
import { arrayGet } from './basic.js';

export { Gadgets, Field3, ForeignFieldSum };

const Gadgets = {
/**
* Get value from array at a Field element index, in O(n) constraints, where n is the array length.
*
* **Warning**: This gadget assumes that the index is within the array bounds `[0, n)`,
* and returns an unconstrained result otherwise.
* To use it with an index that is not already guaranteed to be within the array bounds, you should add a suitable range check.
*
* ```ts
* let array = Provable.witnessFields(3, () => [1n, 2n, 3n]);
* let index = Provable.witness(Field, () => 1n);
*
* let value = Gadgets.arrayGet(array, index);
* ```
*
* **Note**: This saves n constraints compared to `Provable.switch(array.map((_, i) => index.equals(i)), type, array)`.
*/
arrayGet,

/**
* Asserts that the input value is in the range [0, 2^64).
*
Expand Down
13 changes: 4 additions & 9 deletions src/lib/provable/gadgets/sha256.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { Bytes } from '../wrapped-classes.js';
import { chunk } from '../../util/arrays.js';
import { TupleN } from '../../util/types.js';
import { divMod32 } from './arithmetic.js';
import { bytesToWord, wordToBytes } from './bit-slices.js';
import { bitSlice } from './common.js';
import { rangeCheck16 } from './range-check.js';

Expand Down Expand Up @@ -70,11 +69,7 @@ function padding(data: FlexibleBytes): UInt32[][] {
for (let i = 0; i < paddedMessage.length; i += 4) {
// chunk 4 bytes into one UInt32, as expected by SHA256
// bytesToWord expects little endian, so we reverse the bytes
chunks.push(
UInt32.Unsafe.fromField(
bytesToWord(paddedMessage.slice(i, i + 4).reverse())
)
);
chunks.push(UInt32.fromBytesBE(paddedMessage.slice(i, i + 4)));
}

// split message into 16 element sized message blocks
Expand All @@ -97,11 +92,11 @@ const SHA256 = {
}

// the working variables H[i] are 32bit, however we want to decompose them into bytes to be more compatible
// wordToBytes expects little endian, so we reverse the bytes
return Bytes.from(H.map((x) => wordToBytes(x.value, 4).reverse()).flat());
return Bytes.from(H.map((x) => x.toBytesBE()).flat());
},
compression: sha256Compression,
createMessageSchedule,
padding,
get initialState() {
return SHA256Constants.H.map((x) => UInt32.from(x));
},
Expand Down Expand Up @@ -239,7 +234,7 @@ function sigma(u: UInt32, bits: TupleN<number, 3>, firstShifted = false) {
*
* @returns The updated intermediate hash values after compression.
*/
function sha256Compression(H: UInt32[], W: UInt32[]) {
function sha256Compression([...H]: UInt32[], W: UInt32[]) {
Comment on lines -242 to +237
Copy link
Contributor Author

@mitschabaude mitschabaude Oct 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this fixes a bug, the compression function mutated its input, which bit me because in dynamic sha2 you don't necessarily want to use the output

// initialize working variables
let a = H[0];
let b = H[1];
Expand Down
31 changes: 31 additions & 0 deletions src/lib/provable/int.ts
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exposing the uint32 / bytes converters that we already use internally for sha2

Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
lessThanOrEqualGeneric,
} from './gadgets/comparison.js';
import { assert } from '../util/assert.js';
import { TupleN } from '../util/types.js';
import { bytesToWord, wordToBytes } from './gadgets/bit-slices.js';

// external API
export { UInt8, UInt32, UInt64, Int64, Sign };
Expand Down Expand Up @@ -956,6 +958,35 @@ class UInt32 extends CircuitValue {
): InstanceType<T> {
return UInt32.from(x) as any;
}

/**
* Split a UInt32 into 4 UInt8s, in little-endian order.
*/
toBytes() {
return TupleN.fromArray(4, wordToBytes(this.value, 4));
}

/**
* Split a UInt32 into 4 UInt8s, in big-endian order.
*/
toBytesBE() {
return TupleN.fromArray(4, wordToBytes(this.value, 4).reverse());
}

/**
* Combine 4 UInt8s into a UInt32, in little-endian order.
*/
static fromBytes(bytes: UInt8[]): UInt32 {
assert(bytes.length === 4, '4 bytes needed to create a uint32');
return UInt32.Unsafe.fromField(bytesToWord(bytes));
}

/**
* Combine 4 UInt8s into a UInt32, in big-endian order.
*/
static fromBytesBE(bytes: UInt8[]): UInt32 {
return UInt32.fromBytes([...bytes].reverse());
}
}

class Sign extends CircuitValue {
Expand Down
63 changes: 40 additions & 23 deletions src/lib/provable/packed.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { provableFromClass } from './types/provable-derivers.js';
import { mapValue, provableFromClass } from './types/provable-derivers.js';
import { HashInput, ProvableExtended } from './types/struct.js';
import { Unconstrained } from './types/unconstrained.js';
import { Field } from './field.js';
Expand Down Expand Up @@ -50,22 +50,52 @@ class Packed<T> {
/**
* Create a packed representation of `type`. You can then use `PackedType.pack(x)` to pack a value.
*/
static create<T>(
type: WithProvable<ProvableExtended<T>>
static create<T, V>(
type: WithProvable<ProvableHashable<T, V>>
): typeof Packed<T> & {
provable: ProvableHashable<Packed<T>>;
provable: ProvableHashable<Packed<T>, V>;

/**
* Pack a value.
*/
pack(x: T): Packed<T>;
} {
let provable = ProvableType.get(type);
// compute size of packed representation
let input = provable.toInput(provable.empty());
let packedSize = countFields(input);
let packedFields = fields(packedSize);

return class Packed_ extends Packed<T> {
static _innerProvable = provable;
static _provable = provableFromClass(Packed_, {
packed: fields(packedSize),
value: Unconstrained,
}) satisfies ProvableHashable<Packed<T>> as ProvableHashable<Packed<T>>;
static _provable = mapValue(
provableFromClass(Packed_, {
packed: packedFields,
value: Unconstrained,
}),
({ value }: { value: Unconstrained<T> }) =>
provable.toValue(value.get()),
(x) => {
if (x instanceof Packed) return x;
let { packed, value } = Packed_.pack(provable.fromValue(x));
return {
packed: packedFields.toValue(packed),
value: Unconstrained.from(value),
};
}
) satisfies ProvableHashable<Packed<T>, V> as ProvableHashable<
Packed<T>,
V
>;

static pack(x: T): Packed<T> {
let input = provable.toInput(x);
let packed = packToFields(input);
let unconstrained = Unconstrained.witness(() =>
Provable.toConstant(provable, x)
);
return new Packed_(packed, unconstrained);
}

static empty(): Packed<T> {
return Packed_.pack(provable.empty());
Expand All @@ -83,19 +113,6 @@ class Packed<T> {
this.value = value;
}

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

/**
* Unpack a value.
*/
Expand All @@ -120,13 +137,13 @@ class Packed<T> {

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

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

static get innerProvable(): ProvableExtended<any> {
static get innerProvable(): ProvableHashable<any> {
assert(this._innerProvable !== undefined, 'Packed not initialized');
return this._innerProvable;
}
Expand Down
42 changes: 42 additions & 0 deletions src/lib/provable/types/provable-derivers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export {
InferredProvable,
IsPure,
NestedProvable,
mapValue,
};

type ProvableExtension<T, TJson = any> = {
Expand Down Expand Up @@ -169,3 +170,44 @@ function provableExtends<
},
} satisfies ProvableHashable<S, InferValue<A>>;
}

function mapValue<
A extends ProvableHashable<any>,
V extends InferValue<A>,
W,
T extends InferProvable<A>
>(
Comment on lines +174 to +179
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

method to take one provable and modify its "value" type

provable: A,
there: (x: V) => W,
back: (x: W | T) => V | T
): ProvableHashable<T, W> {
return {
sizeInFields() {
return provable.sizeInFields();
},
toFields(value) {
return provable.toFields(value);
},
toAuxiliary(value) {
return provable.toAuxiliary(value);
},
fromFields(fields, aux) {
return provable.fromFields(fields, aux);
},
check(value) {
provable.check(value);
},
toValue(value) {
return there(provable.toValue(value));
},
fromValue(value) {
return provable.fromValue(back(value));
},
empty() {
return provable.empty();
},
toInput(value) {
return provable.toInput(value);
},
};
}
3 changes: 3 additions & 0 deletions src/lib/provable/wrapped-classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@ function Bytes(size: number) {
Bytes.from = InternalBytes.from;
Bytes.fromHex = InternalBytes.fromHex;
Bytes.fromString = InternalBytes.fromString;

// expore base class so that we can detect Bytes with `instanceof`
Bytes.Base = InternalBytes;
Comment on lines +22 to +24
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should always expose base classes somewhere for type factories, so that users can detect what class they have using instanceof.
putting base classes on .Base of the factory function seems like a nice, non-intrusive pattern to me.

Loading