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

Dynamic hashing #66

Open
wants to merge 44 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
cc80728
dynamic struct hashing
mitschabaude Nov 4, 2024
df1a01b
fix weird error
mitschabaude Nov 4, 2024
be40c08
clean up
mitschabaude Nov 4, 2024
8439a5d
split stuff into another file
mitschabaude Nov 4, 2024
5842304
simplify test
mitschabaude Nov 4, 2024
33fe7a4
dynamic array custom to input
mitschabaude Nov 4, 2024
ea3eaa3
minor refactor
mitschabaude Nov 5, 2024
e932495
general packing gadget
mitschabaude Nov 5, 2024
080e938
switch to little endian packing
mitschabaude Nov 5, 2024
d41a135
compatible dynamic array hashing, first version
mitschabaude Nov 5, 2024
5f69348
more efficient length encoding
mitschabaude Nov 5, 2024
d7a66ee
convert between dynamic arrays of different sizes
mitschabaude Nov 5, 2024
25b6f80
use dynamic array hashing when packing attributes
mitschabaude Nov 5, 2024
1c1b911
test nested subschema
mitschabaude Nov 5, 2024
191eaf0
switch order in packToFields
mitschabaude Nov 5, 2024
fe21ccd
handle full field case in dynamic array hash
mitschabaude Nov 5, 2024
772cb2f
add two failing tests for array hashing
mitschabaude Nov 5, 2024
4b8b5c6
fix import cycles
mitschabaude Nov 5, 2024
e889027
generic hash
mitschabaude Nov 5, 2024
cedf584
more dynamic hashing
mitschabaude Nov 5, 2024
20c049e
failing test
mitschabaude Nov 5, 2024
61e227f
fix a few issues, make one test work
mitschabaude Nov 6, 2024
73a0793
tweak nested provable get type
mitschabaude Nov 6, 2024
472e2ba
handle general types in hashArray to fix other test
mitschabaude Nov 6, 2024
c4dc13c
test in circuit + analyze constraint efficiency
mitschabaude Nov 6, 2024
4e8ea3f
some test cleanup
mitschabaude Nov 6, 2024
53fc580
add more tests, and separate packing
mitschabaude Nov 6, 2024
c613ab6
add failing test
mitschabaude Nov 6, 2024
13a895a
support arrays of records of plain values
mitschabaude Nov 6, 2024
3af1a08
fix test
mitschabaude Nov 6, 2024
4ffd492
more cleanup
mitschabaude Nov 6, 2024
92670e5
simplify
mitschabaude Nov 6, 2024
d7d02eb
move comment
mitschabaude Nov 6, 2024
095e957
consolidate packing methods
mitschabaude Nov 6, 2024
34608a1
simplify
mitschabaude Nov 6, 2024
9d995c4
more tests
mitschabaude Nov 6, 2024
4554597
a few more tests
mitschabaude Nov 7, 2024
8ce8c70
improve logical flow of dynamic hashing
mitschabaude Nov 7, 2024
46f816e
add documentation, also of hash collisions
mitschabaude Nov 7, 2024
eb2674b
fix a bad hash collision and document a few others
mitschabaude Nov 7, 2024
ffb18e8
add method to check array equality, use to clean up tests
mitschabaude Nov 7, 2024
02df06b
one more comment
mitschabaude Nov 7, 2024
0f3e096
a nice abstraction
mitschabaude Nov 7, 2024
c1521a0
few more tests & cleanup
mitschabaude Nov 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions examples/unique-hash.eg.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Bytes, Field, Poseidon, UInt64 } from 'o1js';
import { Bytes, Field, UInt64 } from 'o1js';
import {
Spec,
Operation,
Expand All @@ -10,6 +10,7 @@ import {
type InferSchema,
DynamicString,
DynamicArray,
hashPacked,
} from '../src/index.ts';
import {
issuer,
Expand Down Expand Up @@ -80,9 +81,12 @@ console.log('✅ WALLET: imported and validated credential');
// VERIFIER: request a presentation

// it's enough to know a subset of the schema to create the request
// and we don't have to use the original string lengths
const NewString = DynamicString({ maxLength: 30 });

const Subschema = DynamicRecord(
{
nationality: String,
nationality: NewString,
expiresAt: UInt64, // we don't have to match the original order of keys
id: Bytes16,
},
Expand Down Expand Up @@ -136,7 +140,7 @@ let request = PresentationRequest.https(
spec,
{
acceptedNations: FieldArray.from(
acceptedNations.map((s) => Poseidon.hashPacked(String, String.from(s)))
acceptedNations.map((s) => hashPacked(String, String.from(s)))
),
acceptedIssuers: FieldArray.from(acceptedIssuers),
currentDate: UInt64.from(Date.now()),
Expand Down
8 changes: 3 additions & 5 deletions src/credential.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,9 @@ import {
type InferNestedProvable,
NestedProvable,
type NestedProvableFor,
type NestedProvablePure,
type NestedProvablePureFor,
} from './nested.ts';
import { zip } from './util.ts';
import { hashRecord } from './credentials/dynamic-record.ts';
import { hashDynamic } from './credentials/dynamic-hash.ts';

export {
type Credential,
Expand Down Expand Up @@ -262,7 +260,7 @@ function withOwner<DataType extends NestedProvable>(data: DataType) {
function HashableCredential<Data>(
dataType: NestedProvableFor<Data>
): ProvableHashable<Credential<Data>> {
return NestedProvable.get(withOwner(dataType)) as any;
return NestedProvable.get(withOwner(dataType));
}

function HashedCredential<Data>(
Expand All @@ -273,6 +271,6 @@ function HashedCredential<Data>(

function credentialHash({ owner, data }: Credential<unknown>) {
let ownerHash = Poseidon.hash(owner.toFields());
let dataHash = hashRecord(data);
let dataHash = hashDynamic(data);
return Poseidon.hash([ownerHash, dataHash]);
}
147 changes: 128 additions & 19 deletions src/credentials/dynamic-array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,20 @@ import {
type From,
type ProvablePure,
type IsPure,
Poseidon,
} from 'o1js';
import { assert, pad, zip } from '../util.ts';
import { ProvableType } from '../o1js-missing.ts';
import { assertInRange16, assertLessThan16, lessThan16 } from './gadgets.ts';
import { assert, assertHasProperty, chunk, fill, pad, zip } from '../util.ts';
import {
type ProvableHashablePure,
type ProvableHashableType,
ProvableType,
} from '../o1js-missing.ts';
import {
assertInRange16,
assertLessThan16,
lessThan16,
pack,
} from './gadgets.ts';
import { ProvableFactory } from '../provable-factory.ts';
import {
deserializeProvable,
Expand All @@ -23,6 +33,9 @@ import {
serializeProvableType,
} from '../serialize-provable.ts';
import { TypeBuilder, TypeBuilderPure } from '../provable-type-builder.ts';
import { StaticArray } from './static-array.ts';
import { bitSize, packToField } from './dynamic-hash.ts';
import { BaseType } from './dynamic-base-types.ts';

export { DynamicArray };

Expand Down Expand Up @@ -65,7 +78,7 @@ type DynamicArrayClassPure<T, V> = typeof DynamicArrayBase<T, V> &
* Instead, our methods ensure integrity of array operations _within_ the actual length.
*/
function DynamicArray<
A extends ProvableType,
A extends ProvableHashableType,
T extends InferProvable<A> = InferProvable<A>,
V extends InferValue<A> = InferValue<A>
>(
Expand All @@ -76,7 +89,7 @@ function DynamicArray<
: DynamicArrayClass<T, V>;

function DynamicArray<
A extends ProvableType,
A extends ProvableHashableType,
T extends InferProvable<A> = InferProvable<A>,
V extends InferValue<A> = InferValue<A>
>(
Expand Down Expand Up @@ -113,6 +126,7 @@ function DynamicArray<

return DynamicArray_;
}
BaseType.DynamicArray = DynamicArray;

class DynamicArrayBase<T = any, V = any> {
/**
Expand All @@ -126,7 +140,7 @@ class DynamicArrayBase<T = any, V = any> {
length: Field;

// props to override
get innerType(): ProvableType<T, V> {
get innerType(): ProvableHashableType<T, V> {
throw Error('Inner type must be defined in a subclass.');
}
static get maxLength(): number {
Expand Down Expand Up @@ -233,7 +247,7 @@ class DynamicArrayBase<T = any, V = any> {
*
* **Warning**: The callback will be passed unconstrained dummy values.
*/
map<S extends ProvableType>(
map<S extends ProvableHashableType>(
type: S,
f: (t: T, i: number) => From<S>
): DynamicArray<InferProvable<S>, InferValue<S>> {
Expand Down Expand Up @@ -277,6 +291,89 @@ class DynamicArrayBase<T = any, V = any> {
return state;
}

/**
* Dynamic array hash that only depends on the actual values (not the padding).
*
* Avoids hash collisions by encoding the number of actual elements at the beginning of the hash input.
*/
hash() {
let type = ProvableType.get(this.innerType);

// pack all elements into a single field element
let fields = this.array.map((x) => packToField(x, type));
let NULL = packToField(ProvableType.synthesize(type), type);

// assert that all padding elements are 0. this allows us to pack values into blocks
zip(fields, this._dummyMask()).forEach(([x, isPadding]) => {
Provable.assertEqualIf(isPadding, Field, x, NULL);
});

// create blocks of 2 field elements each
// TODO abstract this into a `chunk()` method that returns a DynamicArray<StaticArray<T>>
let elementSize = bitSize(type);
if (elementSize === 0) elementSize = 1; // edge case for empty types like `Undefined`
let elementsPerHalfBlock = Math.floor(254 / elementSize);
if (elementsPerHalfBlock === 0) elementsPerHalfBlock = 1; // larger types are compressed

let elementsPerBlock = 2 * elementsPerHalfBlock;

// we pack the length at the beginning of the first block
// for efficiency (to avoid unpacking the length), we first put zeros at the beginning
// and later just add the length to the first block
let elementsPerUint32 = Math.max(Math.floor(32 / elementSize), 1);
let array = fill(elementsPerUint32, Field(0)).concat(fields);

let maxBlocks = Math.ceil(
(elementsPerUint32 + this.maxLength) / elementsPerBlock
);
let padded = pad(array, maxBlocks * elementsPerBlock, NULL);
let chunked = chunk(padded, elementsPerBlock);
let blocks = chunked.map((block): [Field, Field] => {
let firstHalf = block.slice(0, elementsPerHalfBlock);
let secondHalf = block.slice(elementsPerHalfBlock);
return [pack(firstHalf, elementSize), pack(secondHalf, elementSize)];
});

// add length to the first block
let firstBlock = blocks[0]!;
firstBlock[0] = firstBlock[0].add(this.length).seal();

let Fieldx2 = StaticArray(Field, 2);
let Blocks = DynamicArray(Fieldx2, { maxLength: maxBlocks });

// nBlocks = ceil(length / elementsPerBlock) = floor((length + elementsPerBlock - 1) / elementsPerBlock)
let nBlocks = UInt32.Unsafe.fromField(
this.length.add(elementsPerUint32 + elementsPerBlock - 1)
).div(elementsPerBlock).value;
let dynBlocks = new Blocks(blocks.map(Fieldx2.from), nBlocks);

// now hash the 2-field elements blocks, one permutation at a time
// note: there's a padding element included at the end in the case of uneven number of blocks
// however, this doesn't cause hash collisions because we encoded the length at the beginning
let state = Poseidon.initialState();
dynBlocks.forEach((block, isPadding) => {
let newState = Poseidon.update(state, block.array);
state[0] = Provable.if(isPadding, state[0], newState[0]);
state[1] = Provable.if(isPadding, state[1], newState[1]);
state[2] = Provable.if(isPadding, state[2], newState[2]);
});
return state[0];
}

/**
* Assert that the array is exactly equal, in its representation in field elements, to another array.
*
* Warning: Also checks equality of the padding and maxLength, which don't contribute to the "meaningful" part of the array.
* Therefore, this method is mainly intended for testing.
*/
assertEqualsStrict(other: DynamicArray<T, V>) {
assert(this.maxLength === other.maxLength, 'max length mismatch');
this.length.assertEquals(other.length, 'length mismatch');
zip(this.array, other.array).forEach(([a, b]) => {
Provable.assertEqual(this.innerType, a, b);
});
}

/**
* Push a value, without changing the maxLength.
*
Expand Down Expand Up @@ -376,9 +473,8 @@ class DynamicArrayBase<T = any, V = any> {
}

toValue() {
return (
this.constructor as any as { provable: Provable<any, V[]> }
).provable.toValue(this);
assertHasProperty(this.constructor, 'provable', 'Need subclass');
return (this.constructor.provable as Provable<this, V[]>).toValue(this);
}
}

Expand All @@ -388,21 +484,21 @@ class DynamicArrayBase<T = any, V = any> {
DynamicArray.Base = DynamicArrayBase;

function provable<T, V, Class extends typeof DynamicArrayBase<T, V>>(
type: ProvablePure<T, V>,
type: ProvableHashablePure<T, V>,
Class: Class
): TypeBuilderPure<InstanceType<Class>, V[]>;

function provable<T, V, Class extends typeof DynamicArrayBase<T, V>>(
type: Provable<T, V>,
type: ProvableHashable<T, V>,
Class: Class
): TypeBuilder<InstanceType<Class>, V[]>;

function provable<T, V, Class extends typeof DynamicArrayBase<T, V>>(
type: Provable<T, V>,
type: ProvableHashable<T, V>,
Class: Class
) {
let maxLength = Class.maxLength;
let NULL = type.toValue(ProvableType.synthesize(type));
let NULL = ProvableType.synthesize(type);

return (
TypeBuilder.shape({
Expand All @@ -421,11 +517,22 @@ function provable<T, V, Class extends typeof DynamicArrayBase<T, V>>(
there({ array, length }) {
return array.slice(0, Number(length));
},
back(array) {
let padded = pad(array, maxLength, NULL);
return { array: padded, length: BigInt(array.length) };
backAndDistinguish(array) {
// gracefully handle different maxLength
if (array instanceof DynamicArrayBase) {
if (array.maxLength === maxLength) return array;
array = array.toValue();
}
// fully convert back so that we can pad with NULL
let converted = array.map((x) => type.fromValue(x));
let padded = pad(converted, maxLength, NULL);
return new Class(padded, Field(array.length));
},
distinguish: (s) => s instanceof DynamicArrayBase,
})

// custom hash input
.hashInput((array) => {
return { fields: [array.hash()] };
})
);
}
Expand All @@ -442,7 +549,9 @@ ProvableFactory.register(DynamicArray, {

typeFromJSON(json) {
let innerType = deserializeProvableType(json.innerType);
return DynamicArray(innerType, { maxLength: json.maxLength });
return DynamicArray(innerType as ProvableHashableType, {
maxLength: json.maxLength,
});
},

valueToJSON(_, { array, length }) {
Expand Down
18 changes: 18 additions & 0 deletions src/credentials/dynamic-base-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* This file is just a hack to break import cycles
*/
import type { DynamicArray } from './dynamic-array.ts';
import type { DynamicString } from './dynamic-string.ts';
import type { DynamicRecord, GenericRecord } from './dynamic-record.ts';
import { Required } from '../util.ts';

export { BaseType };

let baseType: {
DynamicArray?: typeof DynamicArray;
DynamicString?: typeof DynamicString;
DynamicRecord?: typeof DynamicRecord;
GenericRecord?: typeof GenericRecord;
} = {};

const BaseType = Required(baseType);
4 changes: 2 additions & 2 deletions src/credentials/dynamic-bytes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Bool, Bytes, Field, Provable, UInt8 } from 'o1js';
import { Bool, Bytes, Field, type ProvableHashable, UInt8 } from 'o1js';
import { DynamicArrayBase, provableDynamicArray } from './dynamic-array.ts';
import { ProvableFactory } from '../provable-factory.ts';
import { assert, chunk } from '../util.ts';
Expand Down Expand Up @@ -80,7 +80,7 @@ function DynamicBytes({ maxLength }: { maxLength: number }) {

class DynamicBytesBase extends DynamicArrayBase<UInt8, { value: bigint }> {
get innerType() {
return UInt8 as any as Provable<UInt8, { value: bigint }>;
return UInt8 as any as ProvableHashable<UInt8, { value: bigint }>;
}

/**
Expand Down
Loading