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

zkEmail circuit #86

Merged
merged 30 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
c6c5727
decode base64 signature
mitschabaude Dec 4, 2024
f8767e5
add comment
mitschabaude Dec 4, 2024
9570127
implement rsa with proper padding
mitschabaude Dec 5, 2024
28206d9
example rsa proof
mitschabaude Dec 5, 2024
f4b1b7a
minor
mitschabaude Dec 5, 2024
c4fd9e3
working o1js dkim verification
mitschabaude Dec 5, 2024
70cde3a
Merge branch 'feature/string-parsing' into feature/zk-email-circuit
mitschabaude Dec 5, 2024
fa0e6f9
easier creation of staticarray and dynamicstring
mitschabaude Dec 5, 2024
a75008f
better error in assert contains
mitschabaude Dec 5, 2024
3504905
refactor into function, add body hash check
mitschabaude Dec 5, 2024
88a5ea5
rename
mitschabaude Dec 5, 2024
5abdf86
inspect constraints - we got 150k
mitschabaude Dec 5, 2024
93d3353
shift around code
mitschabaude Dec 6, 2024
8ea58b1
array splitAt
mitschabaude Dec 6, 2024
bc9012d
add split test
mitschabaude Dec 6, 2024
295deaf
splitAt for string
mitschabaude Dec 6, 2024
a3813fa
merkelize dynarray
mitschabaude Dec 6, 2024
9549188
expose low-level dyn sha2 components
mitschabaude Dec 6, 2024
c25fb95
implement recursive header hashing
mitschabaude Dec 6, 2024
76b17ec
test recursive zkemail
mitschabaude Dec 6, 2024
b3d65ab
implement combined header and body proof
mitschabaude Dec 6, 2024
c84a215
test header + body
mitschabaude Dec 6, 2024
636bd01
minor
mitschabaude Dec 9, 2024
d0621a7
api tweaks
mitschabaude Dec 9, 2024
ff18fd3
Merge branch 'feature/zkemail-prep-2' into feature/zk-email-circuit
mitschabaude Dec 9, 2024
f3c98fe
minor
mitschabaude Dec 9, 2024
1cccc74
Merge branch 'feature/zkemail-prep-2' into feature/zk-email-circuit
mitschabaude Jan 29, 2025
e2b81eb
adapt to released o1js
mitschabaude Jan 29, 2025
05fdcb7
move to local o1js
mitschabaude Jan 29, 2025
ad7eed9
bump o1js
mitschabaude Jan 29, 2025
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
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
"zod": "3.23.8"
},
"peerDependencies": {
"o1js": "2.2.0"
"o1js": "https://pkg.pr.new/o1-labs/o1js@53f3bb0"
},
"engines": {
"node": ">=22.0"
Expand Down
26 changes: 26 additions & 0 deletions src/credentials/dynamic-array.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,32 @@ console.log(
);
}

// test for DynamicArray.splitAt

{
const String = DynamicString({ maxLength: 20 });

function main() {
let string = Provable.witness(String, () => 'hello world!');

let [first, second] = string.splitAt(5);
first.assertEquals('hello');
second.assertEquals(' world!');

let [all, empty] = string.splitAt(19);
all.assertEquals(string);
empty.assertEquals('');
empty.length.assertEquals(0);
assert(empty.maxLength === 1);
}

// can run normally
main();

// can run while checking constraints
console.log(`splitAt`, await runAndConstraints(main));
}

// helper

async function runAndConstraints(fn: () => Promise<void> | void) {
Expand Down
46 changes: 45 additions & 1 deletion src/credentials/dynamic-array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
type ProvablePure,
type IsPure,
Poseidon,
MerkleList,
} from 'o1js';
import { assert, assertHasProperty, chunk, fill, pad, zip } from '../util.ts';
import {
Expand Down Expand Up @@ -298,6 +299,32 @@ class DynamicArrayBase<T = any, V = any> {
return state;
}

/**
* Split the array at index i, i.e. returns `[splice(0, i), splice(i)]`.
*
* If i is 0, the first array will be empty.
* If i it >= the length, the second array will be empty.
*
* Note: this method uses very few constraints, it's only rearranging the array contents
* and recomputing the two lengths.
*/
splitAt(i: number): [DynamicArray<T, V>, DynamicArray<T, V>] {
assert(i >= 0 && i < 1 << 16, 'index must be in [0, 2^16)');
let maxLength1 = Math.min(i, this.maxLength);
let maxLength2 = Math.max(this.maxLength - i, 0);

let Array1 = DynamicArray(this.innerType, { maxLength: maxLength1 });
let Array2 = DynamicArray(this.innerType, { maxLength: maxLength2 });
let array1 = this.array.slice(0, maxLength1);
let array2 = this.array.slice(maxLength1);

let ltLength = lessThan16(Field(i), this.length);
let length1 = Provable.if(ltLength, Field(i), this.length);
let length2 = Provable.if(ltLength, this.length.sub(Field(i)), Field(0));

return [new Array1(array1, length1), new Array2(array2, length2)];
}

/**
* Dynamic array hash that only depends on the actual values (not the padding).
*
Expand Down Expand Up @@ -367,6 +394,17 @@ class DynamicArrayBase<T = any, V = any> {
return state[0];
}

merkelize(listHash?: (hash: Field, t: T) => Field): MerkleList<T> {
let type = this.innerType;
listHash ??= (h, t) => Poseidon.hash([h, packToField(t, type)]);
const List = MerkleList.create(type, listHash);
let list = List.empty();
this.forEach((t, isDummy) => {
list.pushIf(isDummy.not(), t);
});
return list;
}

/**
* Returns a dynamic number of full chunks and a final, smaller chunk.
*
Expand Down Expand Up @@ -669,7 +707,10 @@ class DynamicArrayBase<T = any, V = any> {
/**
* Assert that this array contains the given subarray, and returns the index where it starts.
*/
assertContains(subarray: DynamicArray<T, V> | StaticArray<T, V>) {
assertContains(
subarray: DynamicArray<T, V> | StaticArray<T, V>,
message?: string
) {
let type = this.innerType;
assert(subarray.maxLength <= this.maxLength, 'subarray must be smaller');

Expand All @@ -693,6 +734,9 @@ class DynamicArrayBase<T = any, V = any> {
}
return -1n;
});
// explicit constraint for !== -1, just to get a nice error message
// TODO: would be better to have error message in `Gadgets.rangeCheck16()`
i.assertNotEquals(-1, message ?? 'Array does not contain subarray');

// i + subarray.length - 1 < this.length
Gadgets.rangeCheck16(i);
Expand Down
35 changes: 28 additions & 7 deletions src/credentials/dynamic-sha2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,20 @@ import { hashSafe } from './dynamic-hash.ts';
import { ProvableType, toFieldsPacked } from '../o1js-missing.ts';
import type { Constructor } from '../types.ts';

export { DynamicSHA2, Sha2IterationState, Sha2Iteration, Sha2FinalIteration };
export {
DynamicSHA2,
Sha2IterationState,
Sha2Iteration,
Sha2FinalIteration,
State32,
State64,
Block32,
Block64,
Bytes28,
Bytes32,
Bytes48,
Bytes64,
};

const DynamicSHA2 = {
/**
Expand Down Expand Up @@ -92,8 +105,16 @@ const DynamicSHA2 = {
*/
finalize,

// low-level API

padding256,
padding512,
commitBlock256,
commitBlock512,
hashBlock256,
hashBlock512,
initialState256: (l: 224 | 256) => State32.from(SHA2.initialState256(l)),
initialState512: (l: 384 | 512) => State64.from(SHA2.initialState512(l)),
};

function sha2(len: 224 | 256 | 384 | 512, bytes: DynamicArray<UInt8>): Bytes {
Expand Down Expand Up @@ -396,7 +417,7 @@ function update(

// update hash state and commitment
let { state, commitment } = iterState;
state = iteration.blocks.reduce(state, processBlock256);
state = iteration.blocks.reduce(state, hashBlock256);
commitment = iteration.blocks.reduce(commitment, commitBlock256);

return { len: iterState.len, state, commitment };
Expand All @@ -406,7 +427,7 @@ function update(

// update hash state and commitment
let { state, commitment } = iterState;
state = iteration.blocks.reduce(state, processBlock512);
state = iteration.blocks.reduce(state, hashBlock512);
commitment = iteration.blocks.reduce(commitment, commitBlock512);

return { len: iterState.len, state, commitment };
Expand All @@ -423,7 +444,7 @@ function finalize(

// update hash state and commitment
let { state, commitment } = iterState;
state = final.blocks.reduce(State32, state, processBlock256);
state = final.blocks.reduce(State32, state, hashBlock256);
commitment = final.blocks.reduce(Field, commitment, commitBlock256);

// recompute commitment from scratch to confirm we really hashed the input bytes
Expand All @@ -439,7 +460,7 @@ function finalize(

// update hash state and commitment
let { state, commitment } = iterState;
state = final.blocks.reduce(State64, state, processBlock512);
state = final.blocks.reduce(State64, state, hashBlock512);
commitment = final.blocks.reduce(Field, commitment, commitBlock512);

// recompute commitment from scratch to confirm we really hashed the input bytes
Expand Down Expand Up @@ -496,11 +517,11 @@ function Sha2FinalIteration<L extends Length>(

// helpers for update API

function processBlock256(state: State32, block: Block32): State32 {
function hashBlock256(state: State32, block: Block32): State32 {
let W = SHA2.messageSchedule256(block.array);
return State32.from(SHA2.compression256(state.array, W));
}
function processBlock512(state: State64, block: Block64): State64 {
function hashBlock512(state: State64, block: Block64): State64 {
let W = SHA2.messageSchedule512(block.array);
return State64.from(SHA2.compression512(state.array, W));
}
Expand Down
25 changes: 20 additions & 5 deletions src/credentials/dynamic-string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ function DynamicString({ maxLength }: { maxLength: number }) {

return DynamicString;
}

DynamicString.from = function (s: string | DynamicStringBase) {
if (typeof s !== 'string') return s;
return DynamicString({ maxLength: stringLength(s) }).from(s);
};

BaseType.DynamicString = DynamicString;

const enc = new TextEncoder();
Expand Down Expand Up @@ -220,15 +226,24 @@ class DynamicStringBase extends DynamicArrayBase<UInt8, { value: bigint }> {
super.assertEquals(other);
}

splitAt(index: number): [DynamicString, DynamicString] {
let [a, b] = super.splitAt(index);
let StringA = DynamicString({ maxLength: a.maxLength });
let StringB = DynamicString({ maxLength: b.maxLength });
return [new StringA(a.array, a.length), new StringB(b.array, b.length)];
}

assertContains(
substring: StaticArray<UInt8, UInt8V> | DynamicArray<UInt8, UInt8V> | string
substring:
| StaticArray<UInt8, UInt8V>
| DynamicArray<UInt8, UInt8V>
| string,
message?: string
): Field {
if (typeof substring === 'string') {
substring = DynamicString({ maxLength: stringLength(substring) }).from(
substring
);
substring = DynamicString.from(substring);
}
return super.assertContains(substring);
return super.assertContains(substring, message);
}

growMaxLengthTo(maxLength: number): DynamicStringBase {
Expand Down
65 changes: 55 additions & 10 deletions src/credentials/gadgets.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
/**
* Misc gadgets for attestation contracts.
*/
import { Bool, Field, Gadgets, Provable, UInt32, UInt64, UInt8 } from 'o1js';
import {
Bool,
Field,
Gadgets,
Provable,
TupleN,
UInt32,
UInt64,
UInt8,
} from 'o1js';
import { assert } from '../util.ts';

export {
Expand All @@ -24,7 +33,7 @@ export {
*
* **Warning**: Assumes, but doesn't prove, that each chunk fits in the chunk size.
*/
function pack(chunks: Field[], chunkSize: number) {
function pack(chunks: Field[], chunkSize: number): Field {
let p = chunks.length * chunkSize;
assert(
chunks.length <= 1 || p < Field.sizeInBits,
Expand All @@ -43,15 +52,25 @@ function pack(chunks: Field[], chunkSize: number) {
*
* Proves that the output fields have at most `chunkSize` bits.
*/
function unpack(word: Field, chunkSize: 8 | 16 | 32 | 64, numChunks: number) {
let chunks = Provable.witnessFields(numChunks, () => {
let x = word.toBigInt();
function unpack<N extends number>(
word: Field | bigint,
chunkSize: 1 | 4 | 8 | 16 | 32 | 64,
numChunks: N
) {
function computeChunks() {
let x = Field.from(word).toBigInt();
let mask = (1n << BigInt(chunkSize)) - 1n;
return Array.from(
{ length: numChunks },
(_, i) => (x >> BigInt(i * chunkSize)) & mask
return TupleN.fromArray(
numChunks,
Array.from({ length: numChunks }, (_, i) =>
Field((x >> BigInt(i * chunkSize)) & mask)
)
);
});
}
let chunks = Field.from(word).isConstant()
? computeChunks()
: Provable.witnessFields(numChunks, computeChunks);

// range check fields, so decomposition is unique and outputs are in range
chunks.forEach((chunk) => rangeCheck(chunk, chunkSize));

Expand Down Expand Up @@ -80,8 +99,13 @@ function uint64ToBytesBE(x: UInt64) {
return unpackBytes(x.value, 8).toReversed();
}

function rangeCheck(x: Field, bits: 8 | 16 | 32 | 64) {
function rangeCheck(x: Field, bits: 1 | 4 | 8 | 16 | 32 | 64) {
switch (bits) {
case 1:
x.assertBool();
break;
case 4:
rangeCheckLessThan16(4, x);
case 8:
Gadgets.rangeCheck8(x);
break;
Expand Down Expand Up @@ -170,3 +194,24 @@ function lessThan16(i: Field, x: Field | number): Bool {
);
return isLessThan;
}

// copied from o1js
// https://github.com/o1-labs/o1js/blob/main/src/lib/provable/gadgets/range-check.ts
function rangeCheckLessThan16(bits: number, x: Field) {
assert(bits < 16, `bits must be less than 16, got ${bits}`);

if (x.isConstant()) {
assert(
x.toBigInt() < 1n << BigInt(bits),
`rangeCheckLessThan16: expected field to fit in ${bits} bits, got ${x}`
);
return;
}

// check that x fits in 16 bits
Gadgets.rangeCheck16(x);

// check that 2^(16 - bits)*x < 2^16, i.e. x < 2^bits
let xM = x.mul(1 << (16 - bits)).seal();
Gadgets.rangeCheck16(xM);
}
4 changes: 3 additions & 1 deletion src/credentials/sha2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { uint64FromBytesBE, uint64ToBytesBE } from './gadgets.ts';

export { SHA2 };

type FlexibleBytes = Bytes | (UInt8 | bigint | number)[] | Uint8Array;
type FlexibleBytes = Bytes | (UInt8 | bigint | number)[] | Uint8Array | string;

// sha2 spec: https://csrc.nist.gov/pubs/fips/180-4/upd1/final

Expand Down Expand Up @@ -261,6 +261,7 @@ function messageSchedule512(M: UInt64[]) {
function padding256(data: FlexibleBytes): UInt32[][] {
// create a provable Bytes instance from the input data
// the Bytes class will be static sized according to the length of the input data
if (typeof data === 'string') data = Bytes.fromString(data);
let message = Bytes.from(data);

// now pad the data to reach the format expected by sha256
Expand Down Expand Up @@ -301,6 +302,7 @@ function padding256(data: FlexibleBytes): UInt32[][] {
}

function padding512(data: FlexibleBytes): UInt64[][] {
if (typeof data === 'string') data = Bytes.fromString(data);
let message = Bytes.from(data);

// pad the data to reach the format expected by sha512
Expand Down
Loading