Skip to content

Commit

Permalink
Merge pull request #62 from zksecurity/feature/flexible-subschema
Browse files Browse the repository at this point in the history
Presentations from partial Schema
  • Loading branch information
mitschabaude authored Nov 7, 2024
2 parents 0cf542d + f9efb97 commit 866f18b
Show file tree
Hide file tree
Showing 20 changed files with 1,277 additions and 572 deletions.
43 changes: 36 additions & 7 deletions examples/unique-hash.eg.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Bytes, Field, UInt64 } from 'o1js';
import { Bytes, Field, Poseidon, UInt64 } from 'o1js';
import {
Spec,
Operation,
Expand All @@ -18,9 +18,11 @@ import {
ownerKey,
randomPublicKey,
} from '../tests/test-utils.ts';
import { DynamicRecord } from '../src/credentials/dynamic-record.ts';

// example schema of the credential, which has enough entropy to be hashed into a unique id
const String = DynamicString({ maxLength: 50 });
const LongString = DynamicString({ maxLength: 200 });
const Bytes16 = Bytes(16);

const Schema = {
Expand All @@ -29,6 +31,16 @@ const Schema = {
*/
nationality: String,

/**
* Full name of the owner.
*/
name: LongString,

/**
* Date of birth of the owner.
*/
birthDate: UInt64,

/**
* Owner ID (16 bytes).
*/
Expand All @@ -45,6 +57,8 @@ const Schema = {

let data: InferSchema<typeof Schema> = {
nationality: String.from('United States of America'),
name: LongString.from('John Doe'),
birthDate: UInt64.from(Date.UTC(1990, 1, 1)),
id: Bytes16.random(),
expiresAt: UInt64.from(Date.UTC(2028, 7, 1)),
};
Expand All @@ -65,13 +79,23 @@ console.log('✅ WALLET: imported and validated credential');
// ---------------------------------------------
// VERIFIER: request a presentation

const StringArray = DynamicArray(String, { maxLength: 20 });
const FieldArray = DynamicArray(Field, { maxLength: 20 });
// it's enough to know a subset of the schema to create the request
const Subschema = DynamicRecord(
{
nationality: String,
expiresAt: UInt64, // we don't have to match the original order of keys
id: Bytes16,
},
// have to specify maximum number of entries of the original schema
{ maxEntries: 20 }
);

const FieldArray = DynamicArray(Field, { maxLength: 100 });

const spec = Spec(
{
credential: Credential.Simple(Schema), // schema needed here!
acceptedNations: Claim(StringArray),
credential: Credential.Simple(Subschema),
acceptedNations: Claim(FieldArray), // we represent nations as their hashes for efficiency
acceptedIssuers: Claim(FieldArray),
currentDate: Claim(UInt64),
appId: Claim(String),
Expand All @@ -87,7 +111,7 @@ const spec = Spec(
// 2. the credential was issued by one of the accepted issuers
// 3. the credential is not expired (by comparing with the current date)
let assert = Operation.and(
Operation.equalsOneOf(nationality, acceptedNations),
Operation.equalsOneOf(Operation.hash(nationality), acceptedNations),
Operation.equalsOneOf(issuer, acceptedIssuers),
Operation.lessThanEq(currentDate, expiresAt)
);
Expand All @@ -111,7 +135,9 @@ const acceptedIssuers = [issuer, randomPublicKey(), randomPublicKey()].map(
let request = PresentationRequest.https(
spec,
{
acceptedNations: StringArray.from(acceptedNations),
acceptedNations: FieldArray.from(
acceptedNations.map((s) => Poseidon.hashPacked(String, String.from(s)))
),
acceptedIssuers: FieldArray.from(acceptedIssuers),
currentDate: UInt64.from(Date.now()),
appId: String.from('my-app-id:123'),
Expand All @@ -133,6 +159,9 @@ let deserialized = PresentationRequest.fromJSON('https', requestJson);
let compiled = await Presentation.compile(deserialized);
console.timeEnd('compile');

let info = (await compiled.program.program.analyzeMethods()).run;
console.log('circuit gates summary', info?.summary());

console.time('create');
let presentation = await Presentation.create(ownerKey, {
request: compiled,
Expand Down
6 changes: 4 additions & 2 deletions src/credential-index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ import {
type NestedProvablePure,
} from './nested.ts';
import { assertPure } from './o1js-missing.ts';
import { serializeNestedProvableValue } from './serialize.ts';
import { deserializeNestedProvableValue } from './deserialize.ts';
import {
deserializeNestedProvableValue,
serializeNestedProvableValue,
} from './serialize-provable.ts';

export { Credential, validateCredential };

Expand Down
14 changes: 11 additions & 3 deletions src/credential.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Hashed,
PrivateKey,
Group,
Poseidon,
} from 'o1js';
import {
type InferNestedProvable,
Expand All @@ -16,6 +17,7 @@ import {
type NestedProvablePureFor,
} from './nested.ts';
import { zip } from './util.ts';
import { hashRecord } from './credentials/dynamic-record.ts';

export {
type Credential,
Expand Down Expand Up @@ -62,7 +64,7 @@ type CredentialSpec<
type: 'credential';
credentialType: Type;
witness: NestedProvableFor<Witness>;
data: NestedProvablePureFor<Data>;
data: NestedProvableFor<Data>;

verify(witness: Witness, credHash: Hashed<Credential<Data>>): void;

Expand Down Expand Up @@ -202,7 +204,7 @@ function defineCredential<

issuer(witness: InferNestedProvable<Witness>): Field;
}) {
return function credential<DataType extends NestedProvablePure>(
return function credential<DataType extends NestedProvable>(
dataType: DataType
): CredentialSpec<
Type,
Expand Down Expand Up @@ -266,5 +268,11 @@ function HashableCredential<Data>(
function HashedCredential<Data>(
dataType: NestedProvableFor<Data>
): typeof Hashed<Credential<Data>> {
return Hashed.create(HashableCredential(dataType));
return Hashed.create(HashableCredential(dataType), credentialHash);
}

function credentialHash({ owner, data }: Credential<unknown>) {
let ownerHash = Poseidon.hash(owner.toFields());
let dataHash = hashRecord(data);
return Poseidon.hash([ownerHash, dataHash]);
}
110 changes: 51 additions & 59 deletions src/credentials/dynamic-array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
type InferProvable,
Option,
Provable,
provable as struct,
UInt32,
type InferValue,
Gadgets,
Expand All @@ -17,11 +16,13 @@ import { assert, pad, zip } from '../util.ts';
import { ProvableType } from '../o1js-missing.ts';
import { assertInRange16, assertLessThan16, lessThan16 } from './gadgets.ts';
import { ProvableFactory } from '../provable-factory.ts';
import { serializeProvable, serializeProvableType } from '../serialize.ts';
import {
deserializeProvable,
deserializeProvableType,
} from '../deserialize.ts';
serializeProvable,
serializeProvableType,
} from '../serialize-provable.ts';
import { TypeBuilder, TypeBuilderPure } from '../provable-type-builder.ts';

export { DynamicArray };

Expand Down Expand Up @@ -108,7 +109,7 @@ function DynamicArray<
const provableArray = provable<T, V, typeof DynamicArrayBase<T, V>>(
ProvableType.get(type),
DynamicArray_
);
).build();

return DynamicArray_;
}
Expand Down Expand Up @@ -234,11 +235,11 @@ class DynamicArrayBase<T = any, V = any> {
*/
map<S extends ProvableType>(
type: S,
f: (t: T) => From<S>
f: (t: T, i: number) => From<S>
): DynamicArray<InferProvable<S>, InferValue<S>> {
let Array = DynamicArray(type, { maxLength: this.maxLength });
let provable = ProvableType.get(type);
let array = this.array.map((x) => provable.fromValue(f(x)));
let array = this.array.map((x, i) => provable.fromValue(f(x, i)));
let newArray = new Array(array, this.length);

// new array has same length/maxLength, so it can use the same cached masks
Expand All @@ -253,9 +254,9 @@ class DynamicArrayBase<T = any, V = any> {
*
* The callback will be passed an element and a boolean `isDummy` indicating whether the value is part of the actual array.
*/
forEach(f: (t: T, isDummy: Bool) => void) {
zip(this.array, this._dummyMask()).forEach(([t, isDummy]) => {
f(t, isDummy);
forEach(f: (t: T, isDummy: Bool, i: number) => void) {
zip(this.array, this._dummyMask()).forEach(([t, isDummy], i) => {
f(t, isDummy, i);
});
}

Expand Down Expand Up @@ -366,6 +367,14 @@ class DynamicArrayBase<T = any, V = any> {
return mask;
}

/**
* Returns true if the index is a dummy index,
* i.e. not actually in the array.
*/
isDummyIndex(i: number) {
return this._dummyMask()[i];
}

toValue() {
return (
this.constructor as any as { provable: Provable<any, V[]> }
Expand All @@ -381,61 +390,44 @@ DynamicArray.Base = DynamicArrayBase;
function provable<T, V, Class extends typeof DynamicArrayBase<T, V>>(
type: ProvablePure<T, V>,
Class: Class
): ProvableHashable<InstanceType<Class>, V[]> &
ProvablePure<InstanceType<Class>, V[]>;
): TypeBuilderPure<InstanceType<Class>, V[]>;

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

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

let PlainArray = struct({
array: Provable.Array(type, maxLength),
length: Field,
});
return {
...PlainArray,

// make fromFields return a class instance
fromFields(fields, aux) {
let raw = PlainArray.fromFields(fields, aux);
return new Class(raw.array, raw.length);
},

// convert to/from plain array that has the correct length
toValue(value) {
let length = Number(value.length);
return value.array.map((t) => type.toValue(t)).slice(0, length);
},
fromValue(value) {
if (value instanceof DynamicArrayBase) return value;
let array = value.map((t) => type.fromValue(t));
let padded = pad(array, maxLength, NULL);
return new Class(padded, Field(value.length));
},

// check has to validate length in addition to the other checks
check(value) {
PlainArray.check(value);
assertInRange16(value.length, maxLength);
},

empty() {
let raw = PlainArray.empty();
return new Class(raw.array, raw.length);
},

toCanonical(value) {
if (PlainArray.toCanonical === undefined) return value;
let { array, length } = PlainArray.toCanonical(value);
return new Class(array, length);
},
};
let NULL = type.toValue(ProvableType.synthesize(type));

return (
TypeBuilder.shape({
array: Provable.Array(type, maxLength),
length: Field,
})
.forConstructor((t) => new Class(t.array, t.length))

// check has to validate length in addition to the other checks
.withAdditionalCheck(({ length }) => {
assertInRange16(length, maxLength);
})

// convert to/from plain array that has the _actual_ length
.mapValue<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) };
},
distinguish: (s) => s instanceof DynamicArrayBase,
})
);
}

// serialize/deserialize
Expand Down
2 changes: 1 addition & 1 deletion src/credentials/dynamic-bytes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ function DynamicBytes({ maxLength }: { maxLength: number }) {
UInt8,
{ value: bigint },
typeof DynamicBytesBase
>(UInt8 as any, DynamicBytes);
>(UInt8 as any, DynamicBytes).build();

return DynamicBytes;
}
Expand Down
Loading

0 comments on commit 866f18b

Please sign in to comment.