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

Substring circuit #46

Merged
merged 12 commits into from
Dec 9, 2024
8 changes: 4 additions & 4 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 @@ -51,7 +51,7 @@
"zod": "^3.23.8"
},
"peerDependencies": {
"o1js": "^2.0.0"
"o1js": "^2.1.0"
},
"engines": {
"node": ">=22.0"
Expand Down
84 changes: 65 additions & 19 deletions src/credentials/dynamic-array.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { Provable, UInt8 } from 'o1js';
import { DynamicArray, DynamicString } from '../dynamic.ts';
import { log } from './dynamic-hash.ts';
import assert from 'assert';
import { stringLength } from '../util.ts';

// concatenation of two strings

let String1 = DynamicString({ maxLength: 100 });
let String2 = DynamicString({ maxLength: 100 });
Expand All @@ -10,69 +13,112 @@ let StringLike2 = DynamicArray(UInt8, { maxLength: String2.maxLength });
let String12 = DynamicString({
maxLength: String1.maxLength + String2.maxLength,
});
let string1 = 'blub';
let string2 = 'blob';
let string12 = String12.from('blubblob');

console.log(
'baseline',
await runAndConstraints(() => {
let s1 = Provable.witness(String1, () => 'blub');
let s2 = Provable.witness(String2, () => 'blob');
let s1 = Provable.witness(String1, () => string1);
let s2 = Provable.witness(String2, () => string2);
})
);

console.log(
'baseline + chunk',
await runAndConstraints(() => {
let s1 = Provable.witness(String1, () => 'blub');
let s2 = Provable.witness(String2, () => 'blob');
let s1 = Provable.witness(String1, () => string1);
let s2 = Provable.witness(String2, () => string2);
s1.chunk(8);
})
);

console.log(
'concat naive',
await runAndConstraints(() => {
let s1 = Provable.witness(StringLike1, () => String1.from('blub').array);
let s2 = Provable.witness(StringLike2, () => String2.from('blob').array);

let s1 = Provable.witness(StringLike1, () => String1.from(string1));
let s2 = Provable.witness(StringLike2, () => String2.from(string2));
let s12 = s1.concat(s2);
log(new String12(s12.array, s12.length));
s12.assertEquals(string12);
})
);

console.log(
'concat transposed',
await runAndConstraints(() => {
let s1 = Provable.witness(String1, () => 'blub');
let s2 = Provable.witness(String2, () => 'blob');
let s1 = Provable.witness(String1, () => string1);
let s2 = Provable.witness(String2, () => string2);

let s12 = s1.concatTransposed(s2);
log(new String12(s12.array, s12.length));
new String12(s12.array, s12.length).assertEquals(string12);
})
);

console.log(
'concat with hashing',
await runAndConstraints(() => {
let s1 = Provable.witness(String1, () => 'blub');
let s2 = Provable.witness(String2, () => 'blob');
let s1 = Provable.witness(String1, () => string1);
let s2 = Provable.witness(String2, () => string2);

let s12 = s1.concatByHashing(s2);
log(new String12(s12.array, s12.length));
new String12(s12.array, s12.length).assertEquals(string12);
})
);

console.log(
'concat string',
await runAndConstraints(() => {
let s1 = Provable.witness(String1, () => 'blub');
let s2 = Provable.witness(String2, () => 'blob');
let s1 = Provable.witness(String1, () => string1);
let s2 = Provable.witness(String2, () => string2);

let s12 = s1.concat(s2);
log(s12);
s12.assertEquals(string12);
})
);

// substring check

{
const String = DynamicString({ maxLength: 100 });
const SmallString = DynamicString({ maxLength: 10 });

function main() {
let string = Provable.witness(String, () => 'hello world');
let contained = Provable.witness(SmallString, () => 'lo wo');

let i = string.assertContains(contained);
i.assertEquals(3);

if (Provable.inProver()) {
let notContained = Provable.witness(SmallString, () => 'worldo');
assert.throws(() => string.assertContains(notContained));
}
}
function mainStatic() {
let string = Provable.witness(String, () => 'hello world');
let i = string.assertContains('lo wo');
i.assertEquals(3);
}

// can run normally
main();
mainStatic();

// can run while checking constraints
console.log(
`substring check (${SmallString.maxLength} in ${String.maxLength})`,
await runAndConstraints(main)
);
console.log(
`substring check static (5 in ${String.maxLength})`,
await runAndConstraints(mainStatic)
);
}

// helper

async function runAndConstraints(fn: () => Promise<void> | void) {
await Provable.runAndCheck(fn);
return (await Provable.constraintSystem(fn)).summary();
return (await Provable.constraintSystem(fn)).rows;
}
71 changes: 71 additions & 0 deletions src/credentials/dynamic-array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,23 @@ class DynamicArrayBase<T = any, V = any> {
});
}

/**
* Assert that this array is equal to another.
*
* Note: This only requires the length and the actual elements to be equal, not the padding or the maxLength.
* To check for exact equality, use `assertEqualsStrict()`.
*/
assertEquals(other: DynamicArray<T, V> | StaticArray<T, V> | (T | V)[]) {
this.length.assertEquals(other.length, 'length mismatch');
let otherArray = Array.isArray(other) ? other : other.array;
let type = ProvableType.get(this.innerType);
let NULL = ProvableType.synthesize(type);
this.forEach((t, isDummy, i) => {
let s = type.fromValue(otherArray[i] ?? NULL);
Provable.assertEqualIf(isDummy.not(), type, t, s);
});
}

/**
* Concatenate two arrays.
*
Expand Down Expand Up @@ -649,6 +666,60 @@ class DynamicArrayBase<T = any, V = any> {
assertHasProperty(this.constructor, 'provable', 'Need subclass');
return (this.constructor.provable as Provable<this, V[]>).toValue(this);
}

/**
* Assert that this array contains the given subarray, and returns the index where it starts.
*/
assertContains(subarray: DynamicArray<T, V> | StaticArray<T, V>) {
let type = this.innerType;
assert(subarray.maxLength <= this.maxLength, 'subarray must be smaller');

// idea: witness an index i and show that the subarray is contained at i
let i = Provable.witness(Field, () => {
let length = Number(this.length);
let sublength = Number(subarray.length);
if (sublength === 0) return 0n;
for (let i = 0; i < length; i++) {
// check if subarray is contained at i
let isContained = true;
for (let j = 0; j < sublength; j++) {
if (i + j >= length) return -1n;
isContained &&= Provable.equal(
type,
this.array[i + j]!,
subarray.array[j]!
).toBoolean();
}
if (isContained) return BigInt(i);
}
return -1n;
});

// i + subarray.length - 1 < this.length
Gadgets.rangeCheck16(i);
this.assertIndexInRange(
UInt32.Unsafe.fromField(i.add(subarray.length).sub(1))
);

// assert that subarray is contained at i
// cost: M*(N*T + O(1))
let j = 0;
if (subarray instanceof DynamicArrayBase) {
subarray.forEach((si, isDummy) => {
let ai = this.getOrUnconstrained(i.add(j));
Provable.assertEqualIf(isDummy.not(), type, si, ai);
j++;
});
} else {
subarray.forEach((si) => {
let ai = this.getOrUnconstrained(i.add(j));
Provable.assertEqual(type, si, ai);
j++;
});
}

return i;
}
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/credentials/dynamic-hash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ function toValue(value: unknown): any {
if (typeof value === 'boolean') return value;
if (typeof value === 'bigint') return value;
if (value === undefined || value === null) return value;
if (Array.isArray(value)) value.map(toValue);
if (Array.isArray(value)) return value.map(toValue);

let type = provableTypeOfConstructor(value);

Expand Down
37 changes: 36 additions & 1 deletion src/credentials/dynamic-string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ import {
provableDynamicArray,
} from './dynamic-array.ts';
import { ProvableFactory } from '../provable-factory.ts';
import { assert, pad } from '../util.ts';
import { assert, pad, stringLength } from '../util.ts';
import { BaseType } from './dynamic-base-types.ts';
import { DynamicSHA2 } from './dynamic-sha2.ts';
import { packBytes } from './gadgets.ts';
import { StaticArray } from './static-array.ts';

export { DynamicString };

Expand Down Expand Up @@ -198,6 +199,38 @@ class DynamicStringBase extends DynamicArrayBase<UInt8, { value: bigint }> {
return ab;
}

/**
* Assert that this string is equal to another.
*
* Note: This only requires the length and the actual elements to be equal, not the padding or the maxLength.
* To check for exact equality, use `assertEqualsStrict()`.
*/
assertEquals(
// complicated type here because we have to extend the method signature on DynamicArrayBase
other:
| DynamicString
| DynamicArray<UInt8, UInt8V>
| StaticArray<UInt8, UInt8V>
| (UInt8 | UInt8V)[]
| string
) {
if (typeof other === 'string') {
other = DynamicString({ maxLength: stringLength(other) }).from(other);
}
super.assertEquals(other);
}

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

growMaxLengthTo(maxLength: number): DynamicStringBase {
assert(
maxLength >= this.maxLength,
Expand All @@ -210,6 +243,8 @@ class DynamicStringBase extends DynamicArrayBase<UInt8, { value: bigint }> {

DynamicString.Base = DynamicStringBase;

type UInt8V = { value: bigint };

// serialize/deserialize

ProvableFactory.register(DynamicString, {
Expand Down