From d281c3683be3edca6793a3d03e1e6a762e781d00 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 21 Jun 2023 11:06:54 -0700 Subject: [PATCH 01/90] feat(snarky.d.ts): add sha.create and sha.fieldBytesFromHex functions to the Snarky type definition file to support SHA hashing and conversion from hex to field bytes. --- src/snarky.d.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/snarky.d.ts b/src/snarky.d.ts index d7f24b000..bba88fb5a 100644 --- a/src/snarky.d.ts +++ b/src/snarky.d.ts @@ -244,6 +244,16 @@ declare const Snarky: { }; }; + sha: { + create( + message: MlArray, + nist: boolean, + length: number + ): MlArray; + + fieldBytesFromHex(hex: string): MlArray; + }; + poseidon: { hash(input: MlArray, isChecked: boolean): FieldVar; From c85a95911982811e17cc10791ef3501a4914c1ab Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 21 Jun 2023 11:14:37 -0700 Subject: [PATCH 02/90] refactor(hash.ts): rename Hash to HashHelpers --- src/lib/hash.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/hash.ts b/src/lib/hash.ts index a2684d655..dd8dd2262 100644 --- a/src/lib/hash.ts +++ b/src/lib/hash.ts @@ -11,7 +11,7 @@ export { Poseidon, TokenSymbol }; // internal API export { HashInput, - Hash, + HashHelpers, emptyHashWithPrefix, hashWithPrefix, salt, @@ -98,8 +98,8 @@ function hashConstant(input: Field[]) { return Field(digest); } -const Hash = createHashHelpers(Field, Poseidon); -let { salt, emptyHashWithPrefix, hashWithPrefix } = Hash; +const HashHelpers = createHashHelpers(Field, Poseidon); +let { salt, emptyHashWithPrefix, hashWithPrefix } = HashHelpers; // same as Random_oracle.prefix_to_field in OCaml function prefixToField(prefix: string) { From 18e9afebce185571f2d33cb1923ef9f5f6354bb7 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 21 Jun 2023 11:18:47 -0700 Subject: [PATCH 03/90] feat(hash.ts): add support for SHA224, SHA256, SHA385, SHA512, and Keccak256 hash functions --- src/lib/hash.ts | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/lib/hash.ts b/src/lib/hash.ts index dd8dd2262..f6fe0dee0 100644 --- a/src/lib/hash.ts +++ b/src/lib/hash.ts @@ -6,7 +6,7 @@ import { Provable } from './provable.js'; import { MlFieldArray } from './ml/fields.js'; // external API -export { Poseidon, TokenSymbol }; +export { Poseidon, TokenSymbol, Hash }; // internal API export { @@ -192,3 +192,29 @@ class TokenSymbol extends Struct(TokenSymbolPure) { function emptyReceiptChainHash() { return emptyHashWithPrefix('CodaReceiptEmpty'); } + +function buildSHA(length: 224 | 256 | 385 | 512, nist: boolean) { + return { + hash(message: Field[]) { + return Snarky.sha + .create([0, ...message.map((f) => f.value)], nist, length) + .map(Field); + }, + }; +} + +const Hash = { + default: Poseidon.hash, + + Poseidon: Poseidon.hash, + + SHA224: buildSHA(224, true).hash, + + SHA256: buildSHA(256, true).hash, + + SHA385: buildSHA(385, true).hash, + + SHA512: buildSHA(512, true).hash, + + Keccack256: buildSHA(256, false).hash, +}; From 556b81fe4a1703537038c581b4049015449525e9 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 21 Jun 2023 11:19:36 -0700 Subject: [PATCH 04/90] feat(index.ts): export Hash from lib/hash --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index b33f1d553..e79d0577c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ export { ProvablePure, Ledger } from './snarky.js'; export { Field, Bool, Group, Scalar } from './lib/core.js'; -export { Poseidon, TokenSymbol } from './lib/hash.js'; +export { Poseidon, TokenSymbol, Hash } from './lib/hash.js'; export * from './lib/signature.js'; export { CircuitValue, From cb223a5021b61354b9dfda50c2406df20e5c40f6 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 21 Jun 2023 11:21:36 -0700 Subject: [PATCH 05/90] fix(hash.ts): correct SHA384 function name to match the correct length of 384 bits --- src/lib/hash.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/hash.ts b/src/lib/hash.ts index f6fe0dee0..003300dca 100644 --- a/src/lib/hash.ts +++ b/src/lib/hash.ts @@ -193,7 +193,7 @@ function emptyReceiptChainHash() { return emptyHashWithPrefix('CodaReceiptEmpty'); } -function buildSHA(length: 224 | 256 | 385 | 512, nist: boolean) { +function buildSHA(length: 224 | 256 | 384 | 512, nist: boolean) { return { hash(message: Field[]) { return Snarky.sha @@ -212,7 +212,7 @@ const Hash = { SHA256: buildSHA(256, true).hash, - SHA385: buildSHA(385, true).hash, + SHA384: buildSHA(384, true).hash, SHA512: buildSHA(512, true).hash, From 83432329e4fa30ab5909682c8162dffbb6893f5c Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 21 Jun 2023 13:25:29 -0700 Subject: [PATCH 06/90] feat(int.ts): add UInt8 class to represent 8-bit unsigned integers in the circuit. --- src/index.ts | 2 +- src/lib/int.ts | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index e79d0577c..374256f5c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,7 +17,7 @@ export { } from './lib/circuit_value.js'; export { Provable } from './lib/provable.js'; export { Circuit, Keypair, public_, circuitMain } from './lib/circuit.js'; -export { UInt32, UInt64, Int64, Sign } from './lib/int.js'; +export { UInt8, UInt32, UInt64, Int64, Sign } from './lib/int.js'; export { Types } from './bindings/mina-transaction/types.js'; export * as Mina from './lib/mina.js'; diff --git a/src/lib/int.ts b/src/lib/int.ts index 39744a84c..fca2b6c97 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -1,11 +1,11 @@ import { Field, Bool } from './core.js'; -import { AnyConstructor, CircuitValue, prop } from './circuit_value.js'; +import { AnyConstructor, CircuitValue, Struct, prop } from './circuit_value.js'; import { Types } from '../bindings/mina-transaction/types.js'; import { HashInput } from './hash.js'; import { Provable } from './provable.js'; // external API -export { UInt32, UInt64, Int64, Sign }; +export { UInt8, UInt32, UInt64, Int64, Sign }; /** * A 64 bit unsigned integer with values ranging from 0 to 18,446,744,073,709,551,615. @@ -959,3 +959,18 @@ class Int64 extends CircuitValue implements BalanceChange { return this.sgn.isPositive(); } } + +class UInt8 extends Struct({ + value: Field, +}) { + constructor(x: number | Field) { + super({ value: Field(x) }); + + // Make sure that the Field element that is exactly a byte + this.value.toBits(8); + } + + check() { + this.value.toBits(8); + } +} From b6868e0d626b4fd6716fef776a8e6c20ff7d2630 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 21 Jun 2023 13:56:54 -0700 Subject: [PATCH 07/90] refactor(hash.ts): add support for hashing UInt8 values --- src/lib/hash.ts | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/lib/hash.ts b/src/lib/hash.ts index 003300dca..3368201e5 100644 --- a/src/lib/hash.ts +++ b/src/lib/hash.ts @@ -4,6 +4,8 @@ import { Field } from './core.js'; import { createHashHelpers } from './hash-generic.js'; import { Provable } from './provable.js'; import { MlFieldArray } from './ml/fields.js'; +import { UInt8 } from './int.js'; +import { isField } from './field.js'; // external API export { Poseidon, TokenSymbol, Hash }; @@ -195,10 +197,21 @@ function emptyReceiptChainHash() { function buildSHA(length: 224 | 256 | 384 | 512, nist: boolean) { return { - hash(message: Field[]) { - return Snarky.sha - .create([0, ...message.map((f) => f.value)], nist, length) - .map(Field); + hash(message: (Field | UInt8)[]) { + if (message.length === 0) { + throw Error('SHA hash of empty message'); + } + + const values = message.map((f) => { + if (isField(f)) { + // Make sure that the field is exactly a byte. + f.toBits(8); + return f.value; + } + return f.value.value; + }); + + return Snarky.sha.create([0, ...values], nist, length).map(Field); }, }; } From 3f6da96e74fe73b90efddfef123ef31fb8a1ba3c Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 21 Jun 2023 13:58:57 -0700 Subject: [PATCH 08/90] feat(keccak.ts): add tests for SHA224, SHA256, SHA384, SHA512, Poseidon, default and Keccack256 --- src/examples/keccak.ts | 43 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/examples/keccak.ts diff --git a/src/examples/keccak.ts b/src/examples/keccak.ts new file mode 100644 index 000000000..15b582d88 --- /dev/null +++ b/src/examples/keccak.ts @@ -0,0 +1,43 @@ +import { Hash, Field, Provable, UInt8 } from 'snarkyjs'; + +console.log('Running SHA224 test'); +Provable.runAndCheck(() => { + let digest = Hash.SHA224([Field(1), Field(30000), new UInt8(2)]); + Provable.log(digest); +}); + +console.log('Running SHA256 test'); +Provable.runAndCheck(() => { + let digest = Hash.SHA256([Field(1), Field(1), new UInt8(2)]); + Provable.log(digest); +}); + +console.log('Running SHA384 test'); +Provable.runAndCheck(() => { + let digest = Hash.SHA384([Field(1), Field(1), new UInt8(2)]); + Provable.log(digest); +}); + +console.log('Running SHA512 test'); +Provable.runAndCheck(() => { + let digest = Hash.SHA512([Field(1), Field(1), new UInt8(2)]); + Provable.log(digest); +}); + +console.log('Running Poseidon test'); +Provable.runAndCheck(() => { + let digest = Hash.Poseidon([Field(1), Field(1), Field(2)]); + Provable.log(digest); +}); + +console.log('Running default hash test'); +Provable.runAndCheck(() => { + let digest = Hash.default([Field(1), Field(1), Field(2)]); + Provable.log(digest); +}); + +console.log('Running keccack hash test'); +Provable.runAndCheck(() => { + let digest = Hash.Keccack256([Field(1), Field(1), new UInt8(2)]); + Provable.log(digest); +}); From 43fb1822851ff334ae8529d1650b800126f49dcc Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Mon, 26 Jun 2023 15:31:09 -0700 Subject: [PATCH 09/90] feat(bindings): update bindings --- src/bindings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bindings b/src/bindings index 020284367..a92e78311 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit 0202843678471768ea70b53f21306186f782677c +Subproject commit a92e7831122164e67e0374b8aac893d738ff6626 From 20d4eb733406611569f33e644f4f5b276439d894 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Mon, 26 Jun 2023 15:33:04 -0700 Subject: [PATCH 10/90] refactor(hash.ts): remove unnecessary check for empty message in buildSHA function --- src/lib/hash.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/lib/hash.ts b/src/lib/hash.ts index 3368201e5..a93234ae9 100644 --- a/src/lib/hash.ts +++ b/src/lib/hash.ts @@ -198,10 +198,6 @@ function emptyReceiptChainHash() { function buildSHA(length: 224 | 256 | 384 | 512, nist: boolean) { return { hash(message: (Field | UInt8)[]) { - if (message.length === 0) { - throw Error('SHA hash of empty message'); - } - const values = message.map((f) => { if (isField(f)) { // Make sure that the field is exactly a byte. From 2d58c7ee7cc028622de8f3a0f70f8ec5748132f9 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Tue, 27 Jun 2023 09:32:02 -0700 Subject: [PATCH 11/90] refactor(hash.ts): remove Field type from buildSHA --- src/examples/keccak.ts | 10 +++++----- src/lib/hash.ts | 15 ++++----------- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/src/examples/keccak.ts b/src/examples/keccak.ts index 15b582d88..171b76acf 100644 --- a/src/examples/keccak.ts +++ b/src/examples/keccak.ts @@ -2,25 +2,25 @@ import { Hash, Field, Provable, UInt8 } from 'snarkyjs'; console.log('Running SHA224 test'); Provable.runAndCheck(() => { - let digest = Hash.SHA224([Field(1), Field(30000), new UInt8(2)]); + let digest = Hash.SHA224([new UInt8(1), new UInt8(2), new UInt8(3)]); Provable.log(digest); }); console.log('Running SHA256 test'); Provable.runAndCheck(() => { - let digest = Hash.SHA256([Field(1), Field(1), new UInt8(2)]); + let digest = Hash.SHA256([new UInt8(1), new UInt8(1), new UInt8(2)]); Provable.log(digest); }); console.log('Running SHA384 test'); Provable.runAndCheck(() => { - let digest = Hash.SHA384([Field(1), Field(1), new UInt8(2)]); + let digest = Hash.SHA384([new UInt8(1), new UInt8(1), new UInt8(2)]); Provable.log(digest); }); console.log('Running SHA512 test'); Provable.runAndCheck(() => { - let digest = Hash.SHA512([Field(1), Field(1), new UInt8(2)]); + let digest = Hash.SHA512([new UInt8(1), new UInt8(1), new UInt8(2)]); Provable.log(digest); }); @@ -38,6 +38,6 @@ Provable.runAndCheck(() => { console.log('Running keccack hash test'); Provable.runAndCheck(() => { - let digest = Hash.Keccack256([Field(1), Field(1), new UInt8(2)]); + let digest = Hash.Keccack256([new UInt8(1), new UInt8(1), new UInt8(2)]); Provable.log(digest); }); diff --git a/src/lib/hash.ts b/src/lib/hash.ts index a93234ae9..b77a0bc6b 100644 --- a/src/lib/hash.ts +++ b/src/lib/hash.ts @@ -197,17 +197,10 @@ function emptyReceiptChainHash() { function buildSHA(length: 224 | 256 | 384 | 512, nist: boolean) { return { - hash(message: (Field | UInt8)[]) { - const values = message.map((f) => { - if (isField(f)) { - // Make sure that the field is exactly a byte. - f.toBits(8); - return f.value; - } - return f.value.value; - }); - - return Snarky.sha.create([0, ...values], nist, length).map(Field); + hash(message: UInt8[]) { + return Snarky.sha + .create([0, ...message.map((f) => f.value.value)], nist, length) + .map(Field); }, }; } From 10bda74c0818767d78bd10bd8ab7b061eea2d943 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Tue, 27 Jun 2023 09:47:52 -0700 Subject: [PATCH 12/90] refactor(hash.ts): return hash function for hash objects --- src/examples/keccak.ts | 12 ++++++------ src/lib/hash.ts | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/examples/keccak.ts b/src/examples/keccak.ts index 171b76acf..004ae436d 100644 --- a/src/examples/keccak.ts +++ b/src/examples/keccak.ts @@ -2,31 +2,31 @@ import { Hash, Field, Provable, UInt8 } from 'snarkyjs'; console.log('Running SHA224 test'); Provable.runAndCheck(() => { - let digest = Hash.SHA224([new UInt8(1), new UInt8(2), new UInt8(3)]); + let digest = Hash.SHA224.hash([new UInt8(1), new UInt8(2), new UInt8(3)]); Provable.log(digest); }); console.log('Running SHA256 test'); Provable.runAndCheck(() => { - let digest = Hash.SHA256([new UInt8(1), new UInt8(1), new UInt8(2)]); + let digest = Hash.SHA256.hash([new UInt8(1), new UInt8(1), new UInt8(2)]); Provable.log(digest); }); console.log('Running SHA384 test'); Provable.runAndCheck(() => { - let digest = Hash.SHA384([new UInt8(1), new UInt8(1), new UInt8(2)]); + let digest = Hash.SHA384.hash([new UInt8(1), new UInt8(1), new UInt8(2)]); Provable.log(digest); }); console.log('Running SHA512 test'); Provable.runAndCheck(() => { - let digest = Hash.SHA512([new UInt8(1), new UInt8(1), new UInt8(2)]); + let digest = Hash.SHA512.hash([new UInt8(1), new UInt8(1), new UInt8(2)]); Provable.log(digest); }); console.log('Running Poseidon test'); Provable.runAndCheck(() => { - let digest = Hash.Poseidon([Field(1), Field(1), Field(2)]); + let digest = Hash.Poseidon.hash([Field(1), Field(1), Field(2)]); Provable.log(digest); }); @@ -38,6 +38,6 @@ Provable.runAndCheck(() => { console.log('Running keccack hash test'); Provable.runAndCheck(() => { - let digest = Hash.Keccack256([new UInt8(1), new UInt8(1), new UInt8(2)]); + let digest = Hash.Keccack256.hash([new UInt8(1), new UInt8(1), new UInt8(2)]); Provable.log(digest); }); diff --git a/src/lib/hash.ts b/src/lib/hash.ts index b77a0bc6b..afb83112e 100644 --- a/src/lib/hash.ts +++ b/src/lib/hash.ts @@ -208,15 +208,15 @@ function buildSHA(length: 224 | 256 | 384 | 512, nist: boolean) { const Hash = { default: Poseidon.hash, - Poseidon: Poseidon.hash, + Poseidon: Poseidon, - SHA224: buildSHA(224, true).hash, + SHA224: buildSHA(224, true), - SHA256: buildSHA(256, true).hash, + SHA256: buildSHA(256, true), - SHA384: buildSHA(384, true).hash, + SHA384: buildSHA(384, true), - SHA512: buildSHA(512, true).hash, + SHA512: buildSHA(512, true), - Keccack256: buildSHA(256, false).hash, + Keccack256: buildSHA(256, false), }; From 49a294bb8ebdcc2c8eab64c698809716c1f281df Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Tue, 27 Jun 2023 09:48:43 -0700 Subject: [PATCH 13/90] refactor(keccak.ts): rename Hash.default to Hash.hash --- src/examples/keccak.ts | 2 +- src/lib/hash.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/examples/keccak.ts b/src/examples/keccak.ts index 004ae436d..b794772d2 100644 --- a/src/examples/keccak.ts +++ b/src/examples/keccak.ts @@ -32,7 +32,7 @@ Provable.runAndCheck(() => { console.log('Running default hash test'); Provable.runAndCheck(() => { - let digest = Hash.default([Field(1), Field(1), Field(2)]); + let digest = Hash.hash([Field(1), Field(1), Field(2)]); Provable.log(digest); }); diff --git a/src/lib/hash.ts b/src/lib/hash.ts index afb83112e..c4039662f 100644 --- a/src/lib/hash.ts +++ b/src/lib/hash.ts @@ -206,7 +206,7 @@ function buildSHA(length: 224 | 256 | 384 | 512, nist: boolean) { } const Hash = { - default: Poseidon.hash, + hash: Poseidon.hash, Poseidon: Poseidon, From 22977f58bb6c7101f2a20b46a9ed32721ce2b4d3 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Tue, 27 Jun 2023 10:00:35 -0700 Subject: [PATCH 14/90] refactor(hash.ts): change buildSHA function to return a UInt8 array instead of a Field array --- src/lib/hash.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/hash.ts b/src/lib/hash.ts index c4039662f..853006b9f 100644 --- a/src/lib/hash.ts +++ b/src/lib/hash.ts @@ -197,10 +197,10 @@ function emptyReceiptChainHash() { function buildSHA(length: 224 | 256 | 384 | 512, nist: boolean) { return { - hash(message: UInt8[]) { + hash(message: UInt8[]): UInt8[] { return Snarky.sha .create([0, ...message.map((f) => f.value.value)], nist, length) - .map(Field); + .map((f) => new UInt8(Field(f))); }, }; } From 42d9f74d56685312e28e60f304d6dae216c257a9 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Tue, 27 Jun 2023 10:08:15 -0700 Subject: [PATCH 15/90] feat(int.ts): add fromFields to UInt8 --- src/lib/int.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/lib/int.ts b/src/lib/int.ts index fca2b6c97..2455a2f3e 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -965,12 +965,14 @@ class UInt8 extends Struct({ }) { constructor(x: number | Field) { super({ value: Field(x) }); - - // Make sure that the Field element that is exactly a byte - this.value.toBits(8); + this.value.toBits(8); // Make sure that the Field element that is exactly a byte } check() { this.value.toBits(8); } + + fromFields(xs: Field[]): UInt8[] { + return xs.map((x) => new UInt8(x)); + } } From 957c1f8e3818d66dd617c960d5ae1f4c88f36d30 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Tue, 27 Jun 2023 10:10:59 -0700 Subject: [PATCH 16/90] feat(int.ts): add fromHex method to UInt8 class to convert hex string to UInt8 array using Snarky.sha library --- src/lib/int.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/lib/int.ts b/src/lib/int.ts index 2455a2f3e..f22b6033f 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -3,6 +3,7 @@ import { AnyConstructor, CircuitValue, Struct, prop } from './circuit_value.js'; import { Types } from '../bindings/mina-transaction/types.js'; import { HashInput } from './hash.js'; import { Provable } from './provable.js'; +import { Snarky } from 'src/snarky.js'; // external API export { UInt8, UInt32, UInt64, Int64, Sign }; @@ -975,4 +976,8 @@ class UInt8 extends Struct({ fromFields(xs: Field[]): UInt8[] { return xs.map((x) => new UInt8(x)); } + + static fromHex(xs: string): UInt8[] { + return Snarky.sha.fieldBytesFromHex(xs).map((x) => new UInt8(Field(x))); + } } From ae01aa777d162f09a1fea192e4a4d8a360dc97e4 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Tue, 27 Jun 2023 16:01:36 -0700 Subject: [PATCH 17/90] feat(int.ts): add static method toHex to UInt8 class to convert an array of UInt8 to a hex string --- src/lib/int.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/lib/int.ts b/src/lib/int.ts index f22b6033f..bf935e03a 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -3,7 +3,7 @@ import { AnyConstructor, CircuitValue, Struct, prop } from './circuit_value.js'; import { Types } from '../bindings/mina-transaction/types.js'; import { HashInput } from './hash.js'; import { Provable } from './provable.js'; -import { Snarky } from 'src/snarky.js'; +import { Snarky } from '../snarky.js'; // external API export { UInt8, UInt32, UInt64, Int64, Sign }; @@ -980,4 +980,20 @@ class UInt8 extends Struct({ static fromHex(xs: string): UInt8[] { return Snarky.sha.fieldBytesFromHex(xs).map((x) => new UInt8(Field(x))); } + static toHex(xs: UInt8[]): string { + return xs + .map((x) => x.value) + .map((value) => byteArrayToHex(Field.toBytes(value))) + .join(''); + } +} + +// TODO: Move to more appropriate place? +function byteArrayToHex(byteArray: number[]): string { + return byteArray + .map((byte) => { + const hexValue = byte.toString(16).padStart(2, '0'); + return hexValue === '00' ? '' : hexValue; + }) + .join(''); } From 92deb9e7a2c46a22498844d86dfa8747482a5d14 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Tue, 27 Jun 2023 16:01:53 -0700 Subject: [PATCH 18/90] test(keccak.ts): add tests for checking hex->digest, digest->hex conversion for SHA224, SHA256, SHA384, SHA512, and Keccack256 hash functions. Add helper functions to check the conversion and compare the digests. --- src/examples/keccak.ts | 53 ++++++++++++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/src/examples/keccak.ts b/src/examples/keccak.ts index b794772d2..818aff372 100644 --- a/src/examples/keccak.ts +++ b/src/examples/keccak.ts @@ -1,43 +1,66 @@ -import { Hash, Field, Provable, UInt8 } from 'snarkyjs'; +import { Field, Provable, Hash, UInt8 } from 'snarkyjs'; + +function equals(a: UInt8[], b: UInt8[]): boolean { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) + if (a[i].value.toConstant() === b[i].value.toConstant()) return false; + + return true; +} + +function checkDigestHexConversion(digest: UInt8[]) { + console.log('Checking hex->digest, digest->hex matches'); + Provable.asProver(() => { + const hex = UInt8.toHex(digest); + const expected = UInt8.fromHex(hex); + Provable.log(hex, digest, expected); + if (equals(digest, expected)) { + console.log('✅ Digest matches'); + } else { + console.log('❌ Digest does not match'); + } + }); +} console.log('Running SHA224 test'); Provable.runAndCheck(() => { let digest = Hash.SHA224.hash([new UInt8(1), new UInt8(2), new UInt8(3)]); - Provable.log(digest); + checkDigestHexConversion(digest); }); console.log('Running SHA256 test'); Provable.runAndCheck(() => { - let digest = Hash.SHA256.hash([new UInt8(1), new UInt8(1), new UInt8(2)]); - Provable.log(digest); + let digest = Hash.SHA256.hash([new UInt8(1), new UInt8(2), new UInt8(3)]); + checkDigestHexConversion(digest); }); console.log('Running SHA384 test'); Provable.runAndCheck(() => { - let digest = Hash.SHA384.hash([new UInt8(1), new UInt8(1), new UInt8(2)]); - Provable.log(digest); + let digest = Hash.SHA384.hash([new UInt8(1), new UInt8(2), new UInt8(3)]); + checkDigestHexConversion(digest); }); +// TODO: This test fails console.log('Running SHA512 test'); Provable.runAndCheck(() => { - let digest = Hash.SHA512.hash([new UInt8(1), new UInt8(1), new UInt8(2)]); - Provable.log(digest); + let digest = Hash.SHA512.hash([new UInt8(1), new UInt8(2), new UInt8(3)]); + checkDigestHexConversion(digest); }); -console.log('Running Poseidon test'); +console.log('Running keccack hash test'); Provable.runAndCheck(() => { - let digest = Hash.Poseidon.hash([Field(1), Field(1), Field(2)]); - Provable.log(digest); + let digest = Hash.Keccack256.hash([new UInt8(1), new UInt8(2), new UInt8(3)]); + checkDigestHexConversion(digest); }); -console.log('Running default hash test'); +console.log('Running Poseidon test'); Provable.runAndCheck(() => { - let digest = Hash.hash([Field(1), Field(1), Field(2)]); + let digest = Hash.Poseidon.hash([Field(1), Field(2), Field(3)]); Provable.log(digest); }); -console.log('Running keccack hash test'); +console.log('Running default hash test'); Provable.runAndCheck(() => { - let digest = Hash.Keccack256.hash([new UInt8(1), new UInt8(1), new UInt8(2)]); + let digest = Hash.hash([Field(1), Field(2), Field(3)]); Provable.log(digest); }); From b600b2805d546fb4bd9d2b72908133592cb6e3b9 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 28 Jun 2023 09:27:28 -0700 Subject: [PATCH 19/90] fix(int.ts): working impl of toHex for UInt8 --- src/examples/keccak.ts | 2 +- src/lib/int.ts | 14 +++----------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/examples/keccak.ts b/src/examples/keccak.ts index 818aff372..1fcf7557f 100644 --- a/src/examples/keccak.ts +++ b/src/examples/keccak.ts @@ -13,10 +13,10 @@ function checkDigestHexConversion(digest: UInt8[]) { Provable.asProver(() => { const hex = UInt8.toHex(digest); const expected = UInt8.fromHex(hex); - Provable.log(hex, digest, expected); if (equals(digest, expected)) { console.log('✅ Digest matches'); } else { + Provable.log(`hex: ${hex}\ndigest: ${digest}\nexpected:${expected}`); console.log('❌ Digest does not match'); } }); diff --git a/src/lib/int.ts b/src/lib/int.ts index bf935e03a..acc91558f 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -980,20 +980,12 @@ class UInt8 extends Struct({ static fromHex(xs: string): UInt8[] { return Snarky.sha.fieldBytesFromHex(xs).map((x) => new UInt8(Field(x))); } + static toHex(xs: UInt8[]): string { return xs .map((x) => x.value) - .map((value) => byteArrayToHex(Field.toBytes(value))) + .map((f) => Field.toBytes(f)[0].toString(16).padStart(2, '0')) + .slice(1) .join(''); } } - -// TODO: Move to more appropriate place? -function byteArrayToHex(byteArray: number[]): string { - return byteArray - .map((byte) => { - const hexValue = byte.toString(16).padStart(2, '0'); - return hexValue === '00' ? '' : hexValue; - }) - .join(''); -} From ecd62c4755b5096a62a1aad998654fce4829377d Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 28 Jun 2023 11:09:10 -0700 Subject: [PATCH 20/90] feat(random.ts): add support for generating random UInt8 values --- src/lib/testing/random.ts | 14 +++++++++++--- src/provable/field-bigint.ts | 4 +++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/lib/testing/random.ts b/src/lib/testing/random.ts index f9d191950..74ffe3e32 100644 --- a/src/lib/testing/random.ts +++ b/src/lib/testing/random.ts @@ -63,6 +63,7 @@ function sample(rng: Random, size: number) { const boolean = Random_(() => drawOneOf8() < 4); const bool = map(boolean, Bool); +const uint8 = biguintWithInvalid(8); const uint32 = biguintWithInvalid(32); const uint64 = biguintWithInvalid(64); @@ -115,6 +116,7 @@ type Generators = { const Generators: Generators = { Field: field, Bool: bool, + UInt8: uint8, UInt32: uint32, UInt64: uint64, Sign: sign, @@ -181,17 +183,21 @@ const nonNumericString = reject( string(nat(20)), (str: any) => !isNaN(str) && !isNaN(parseFloat(str)) ); -const invalidUint64Json = toString( - oneOf(uint64.invalid, nonInteger, nonNumericString) +const invalidUint8Json = toString( + oneOf(uint8.invalid, nonInteger, nonNumericString) ); const invalidUint32Json = toString( oneOf(uint32.invalid, nonInteger, nonNumericString) ); +const invalidUint64Json = toString( + oneOf(uint64.invalid, nonInteger, nonNumericString) +); // some json versions of those types let json_ = { - uint64: { ...toString(uint64), invalid: invalidUint64Json }, + uint8: { ...toString(uint8), invalid: invalidUint8Json }, uint32: { ...toString(uint32), invalid: invalidUint32Json }, + uint64: { ...toString(uint64), invalid: invalidUint64Json }, publicKey: withInvalidBase58(mapWithInvalid(publicKey, PublicKey.toBase58)), privateKey: withInvalidBase58(map(privateKey, PrivateKey.toBase58)), keypair: map(keypair, ({ privatekey, publicKey }) => ({ @@ -215,6 +221,7 @@ type JsonGenerators = { const JsonGenerators: JsonGenerators = { Field: json_.field, Bool: boolean, + UInt8: json_.uint8, UInt32: json_.uint32, UInt64: json_.uint64, Sign: withInvalidRandomString(map(sign, Sign.toJSON)), @@ -299,6 +306,7 @@ const Random = Object.assign(Random_, { dice: Object.assign(dice, { ofSize: diceOfSize() }), field, bool, + uint8, uint32, uint64, privateKey, diff --git a/src/provable/field-bigint.ts b/src/provable/field-bigint.ts index ea2d797c8..769f31dab 100644 --- a/src/provable/field-bigint.ts +++ b/src/provable/field-bigint.ts @@ -6,11 +6,12 @@ import { ProvableBigint, } from '../bindings/lib/provable-bigint.js'; -export { Field, Bool, UInt32, UInt64, Sign }; +export { Field, Bool, UInt8, UInt32, UInt64, Sign }; export { pseudoClass, sizeInBits, checkRange, checkField }; type Field = bigint; type Bool = 0n | 1n; +type UInt8 = bigint; type UInt32 = bigint; type UInt64 = bigint; @@ -99,6 +100,7 @@ function Unsigned(bits: number) { } ); } +const UInt8 = Unsigned(8); const UInt32 = Unsigned(32); const UInt64 = Unsigned(64); From 8bd3a93ee41d29c342f1e86a1f55d1489d46969d Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 28 Jun 2023 11:16:56 -0700 Subject: [PATCH 21/90] feat(int.ts): add bigint to constructor and introduce static NUM_BITS --- src/lib/int.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/lib/int.ts b/src/lib/int.ts index acc91558f..99e225e9f 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -964,13 +964,15 @@ class Int64 extends CircuitValue implements BalanceChange { class UInt8 extends Struct({ value: Field, }) { - constructor(x: number | Field) { + static NUM_BITS = 8; + + constructor(x: number | bigint | Field) { super({ value: Field(x) }); - this.value.toBits(8); // Make sure that the Field element that is exactly a byte + this.value.toBits(UInt8.NUM_BITS); // Make sure that the Field element that is exactly a byte } check() { - this.value.toBits(8); + this.value.toBits(UInt8.NUM_BITS); } fromFields(xs: Field[]): UInt8[] { From cd6a019410b30c87971b9e9e00d4ad28e66818f8 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 28 Jun 2023 11:18:03 -0700 Subject: [PATCH 22/90] feat(int.ts): add static properties and to UInt8 class --- src/lib/int.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/lib/int.ts b/src/lib/int.ts index 99e225e9f..75f28c11f 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -971,6 +971,14 @@ class UInt8 extends Struct({ this.value.toBits(UInt8.NUM_BITS); // Make sure that the Field element that is exactly a byte } + static get zero() { + return new UInt8(0); + } + + static get one() { + return new UInt8(1); + } + check() { this.value.toBits(UInt8.NUM_BITS); } From 7fed25698ddd929c54b3b5c5c93cb2db99b6a438 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 28 Jun 2023 11:19:57 -0700 Subject: [PATCH 23/90] chore(int.ts): add toString(), toBigInt(), toField(), and toJSON() methods to UInt8 class for better usability and serialization --- src/lib/int.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/lib/int.ts b/src/lib/int.ts index 75f28c11f..7d27f9518 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -979,6 +979,18 @@ class UInt8 extends Struct({ return new UInt8(1); } + toString() { + return this.value.toString(); + } + + toBigInt() { + return this.value.toBigInt(); + } + + toField() { + return this.value; + } + check() { this.value.toBits(UInt8.NUM_BITS); } @@ -998,4 +1010,8 @@ class UInt8 extends Struct({ .slice(1) .join(''); } + + static toJSON(xs: UInt8): string { + return xs.value.toString(); + } } From 8a9718970b9ba4e5dfbb7b3245e8fd08da52f022 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 28 Jun 2023 11:21:01 -0700 Subject: [PATCH 24/90] chore(int.ts): add static method MAXINT() to UInt8 class to return the maximum value of UInt8 --- src/lib/int.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/lib/int.ts b/src/lib/int.ts index 7d27f9518..c36440df1 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -1014,4 +1014,8 @@ class UInt8 extends Struct({ static toJSON(xs: UInt8): string { return xs.value.toString(); } + + static MAXINT() { + return new UInt8(255); + } } From 7176fb6ee4783bbf085d655831c36a3e402a5652 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 28 Jun 2023 11:21:59 -0700 Subject: [PATCH 25/90] refactor(int.ts): change static method to instance method --- src/lib/int.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/int.ts b/src/lib/int.ts index c36440df1..df18f68a2 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -1011,8 +1011,8 @@ class UInt8 extends Struct({ .join(''); } - static toJSON(xs: UInt8): string { - return xs.value.toString(); + toJSON(): string { + return this.value.toString(); } static MAXINT() { From 3c1bc9454a8a2a51984938d1885988e9f95614af Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 28 Jun 2023 11:25:15 -0700 Subject: [PATCH 26/90] feat(int.ts): add methods toJSON, toUInt32, and toUInt64 to the UInt8 class --- src/lib/int.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/lib/int.ts b/src/lib/int.ts index df18f68a2..35ddfb01a 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -999,6 +999,18 @@ class UInt8 extends Struct({ return xs.map((x) => new UInt8(x)); } + toJSON(): string { + return this.value.toString(); + } + + toUInt32(): UInt32 { + return new UInt32(this.value); + } + + toUInt64(): UInt64 { + return new UInt64(this.value); + } + static fromHex(xs: string): UInt8[] { return Snarky.sha.fieldBytesFromHex(xs).map((x) => new UInt8(Field(x))); } From e15f7e2b23bcd3604403395eae9c5c1551783c0f Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 28 Jun 2023 11:25:57 -0700 Subject: [PATCH 27/90] feat(int.ts): add from and private checkConstant methods --- src/lib/int.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/lib/int.ts b/src/lib/int.ts index 35ddfb01a..1eb5e7b16 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -1023,11 +1023,20 @@ class UInt8 extends Struct({ .join(''); } - toJSON(): string { - return this.value.toString(); - } - static MAXINT() { return new UInt8(255); } + + static from(x: UInt64 | UInt32 | Field | number | string | bigint) { + if (x instanceof UInt64 || x instanceof UInt32 || x instanceof UInt8) + x = x.value; + + return new this(this.checkConstant(Field(x))); + } + + private static checkConstant(x: Field) { + if (!x.isConstant()) return x; + x.toBits(UInt8.NUM_BITS); + return x; + } } From 321529b534b3bbcd7ceff4fee495cc9ee1a91117 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 28 Jun 2023 11:29:49 -0700 Subject: [PATCH 28/90] refactor(int.ts): add isConstant() method to UInt8 class for checking if the value is constant --- src/lib/int.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/lib/int.ts b/src/lib/int.ts index 1eb5e7b16..552849a45 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -1011,6 +1011,10 @@ class UInt8 extends Struct({ return new UInt64(this.value); } + isConstant() { + return this.value.isConstant(); + } + static fromHex(xs: string): UInt8[] { return Snarky.sha.fieldBytesFromHex(xs).map((x) => new UInt8(Field(x))); } From 509c5f9e0f0efe81c5422e078dea70f294eb417e Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 28 Jun 2023 11:31:08 -0700 Subject: [PATCH 29/90] refactor(int.ts): add toConstant() method to UInt8 class --- src/lib/int.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/lib/int.ts b/src/lib/int.ts index 552849a45..dbaf81ac1 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -1015,6 +1015,10 @@ class UInt8 extends Struct({ return this.value.isConstant(); } + toConstant() { + return this.value.toConstant(); + } + static fromHex(xs: string): UInt8[] { return Snarky.sha.fieldBytesFromHex(xs).map((x) => new UInt8(Field(x))); } From 325c49cc48333b6be6dbd496a8b86771890f71b3 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 28 Jun 2023 11:34:06 -0700 Subject: [PATCH 30/90] refactor(int.ts): add UInt8 to constructor --- src/lib/int.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/lib/int.ts b/src/lib/int.ts index dbaf81ac1..396354116 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -966,7 +966,9 @@ class UInt8 extends Struct({ }) { static NUM_BITS = 8; - constructor(x: number | bigint | Field) { + constructor(x: number | bigint | Field | UInt8) { + if (x instanceof UInt8) return x; + super({ value: Field(x) }); this.value.toBits(UInt8.NUM_BITS); // Make sure that the Field element that is exactly a byte } @@ -1015,10 +1017,6 @@ class UInt8 extends Struct({ return this.value.isConstant(); } - toConstant() { - return this.value.toConstant(); - } - static fromHex(xs: string): UInt8[] { return Snarky.sha.fieldBytesFromHex(xs).map((x) => new UInt8(Field(x))); } From d996d25e798d640d6405c5e76182a73d6f42ab80 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 28 Jun 2023 11:37:09 -0700 Subject: [PATCH 31/90] fix(int.ts): add support for string type in the constructor of UInt8 class to allow initializing with string values --- src/lib/int.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/int.ts b/src/lib/int.ts index 396354116..70e2464d9 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -966,7 +966,7 @@ class UInt8 extends Struct({ }) { static NUM_BITS = 8; - constructor(x: number | bigint | Field | UInt8) { + constructor(x: number | bigint | string | Field | UInt8) { if (x instanceof UInt8) return x; super({ value: Field(x) }); From 738719c19d5ae3c3008e6704136ca3ee1ccd8f6e Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 28 Jun 2023 11:37:58 -0700 Subject: [PATCH 32/90] test(keccack.unit-test.ts): start unit tests for the constructor of UInt8 class --- src/lib/keccack.unit-test.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/lib/keccack.unit-test.ts diff --git a/src/lib/keccack.unit-test.ts b/src/lib/keccack.unit-test.ts new file mode 100644 index 000000000..f8cbfa28e --- /dev/null +++ b/src/lib/keccack.unit-test.ts @@ -0,0 +1,20 @@ +import { test, Random } from './testing/property.js'; +import { UInt8 } from './int.js'; + +// Test constructor +test(Random.uint8, Random.json.uint8, (x, y, assert) => { + let z = new UInt8(x); + assert(z instanceof UInt8); + assert(z.toBigInt() === x); + assert(z.toString() === x.toString()); + assert(z.isConstant()); + + assert((z = new UInt8(x)) instanceof UInt8 && z.toBigInt() === x); + assert((z = new UInt8(z)) instanceof UInt8 && z.toBigInt() === x); + assert((z = new UInt8(z.value)) instanceof UInt8 && z.toBigInt() === x); + + z = new UInt8(y); + assert(z instanceof UInt8); + assert(z.toString() === y); + assert(z.toJSON() === y); +}); From b7bcb035f5d139e7d9682d55bf45fc050aad8fe4 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 28 Jun 2023 13:05:14 -0700 Subject: [PATCH 33/90] test(keccack.unit-test.ts): add unit test for handling numbers up to 2^8 in the UInt8 class --- src/lib/keccack.unit-test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/lib/keccack.unit-test.ts b/src/lib/keccack.unit-test.ts index f8cbfa28e..046d0b810 100644 --- a/src/lib/keccack.unit-test.ts +++ b/src/lib/keccack.unit-test.ts @@ -18,3 +18,8 @@ test(Random.uint8, Random.json.uint8, (x, y, assert) => { assert(z.toString() === y); assert(z.toJSON() === y); }); + +// handles all numbers up to 2^8 +test(Random.nat(255), (n, assert) => { + assert(new UInt8(n).toString() === String(n)); +}); From 64135dbbd0afc06299d0589baf6a202bfa711c45 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 28 Jun 2023 13:09:45 -0700 Subject: [PATCH 34/90] test(keccack.unit-test.ts): add unit tests for negative numbers and numbers greater than or equal to 2^8 to ensure proper handling and error throwing --- src/lib/keccack.unit-test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/lib/keccack.unit-test.ts b/src/lib/keccack.unit-test.ts index 046d0b810..acc6865c6 100644 --- a/src/lib/keccack.unit-test.ts +++ b/src/lib/keccack.unit-test.ts @@ -23,3 +23,9 @@ test(Random.uint8, Random.json.uint8, (x, y, assert) => { test(Random.nat(255), (n, assert) => { assert(new UInt8(n).toString() === String(n)); }); + +// throws on negative numbers +test.negative(Random.int(-10, -1), (x) => new UInt8(x)); + +// throws on numbers >= 2^8 +test.negative(Random.uint8.invalid, (x) => new UInt8(x)); From 05550d970d01fad74a8ff3d0bfe35bef2352c58e Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 28 Jun 2023 14:38:38 -0700 Subject: [PATCH 35/90] feat(keccack.unit-test.ts): add tests for digest to hex and hex to digest conversions --- src/lib/keccack.unit-test.ts | 47 ++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/lib/keccack.unit-test.ts b/src/lib/keccack.unit-test.ts index acc6865c6..99029c3e8 100644 --- a/src/lib/keccack.unit-test.ts +++ b/src/lib/keccack.unit-test.ts @@ -1,5 +1,8 @@ import { test, Random } from './testing/property.js'; import { UInt8 } from './int.js'; +import { Hash } from './hash.js'; +import { Provable } from './provable.js'; +import { expect } from 'expect'; // Test constructor test(Random.uint8, Random.json.uint8, (x, y, assert) => { @@ -29,3 +32,47 @@ test.negative(Random.int(-10, -1), (x) => new UInt8(x)); // throws on numbers >= 2^8 test.negative(Random.uint8.invalid, (x) => new UInt8(x)); + +// test digest->hex and hex->digest conversions +checkHashConversions(); +console.log('hashing digest conversions matches! 🎉'); + +function checkHashConversions() { + for (let i = 0; i < 2; i++) { + const data = Random.array(Random.uint8, Random.nat(20)) + .create()() + .map((x) => new UInt8(x)); + + Provable.runAndCheck(() => { + let digest = Hash.SHA224.hash(data); + expectDigestToEqualHex(digest); + + digest = Hash.SHA256.hash(data); + expectDigestToEqualHex(digest); + + digest = Hash.SHA384.hash(data); + expectDigestToEqualHex(digest); + + digest = Hash.SHA512.hash(data); + expectDigestToEqualHex(digest); + + digest = Hash.Keccack256.hash(data); + expectDigestToEqualHex(digest); + }); + } +} + +function expectDigestToEqualHex(digest: UInt8[]) { + Provable.asProver(() => { + const hex = UInt8.toHex(digest); + expect(equals(digest, UInt8.fromHex(hex))).toBe(true); + }); +} + +function equals(a: UInt8[], b: UInt8[]): boolean { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) + if (a[i].value.toConstant() === b[i].value.toConstant()) return false; + + return true; +} From de479249ad72e71808ab762b9d1d2087d828e04b Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 28 Jun 2023 14:41:13 -0700 Subject: [PATCH 36/90] fix(int.ts): add type checking for UInt32 and UInt64 --- src/lib/int.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/lib/int.ts b/src/lib/int.ts index 70e2464d9..2cf422a16 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -1006,11 +1006,15 @@ class UInt8 extends Struct({ } toUInt32(): UInt32 { - return new UInt32(this.value); + let uint32 = new UInt32(this.value); + UInt32.check(uint32); + return uint32; } toUInt64(): UInt64 { - return new UInt64(this.value); + let uint64 = new UInt64(this.value); + UInt64.check(uint64); + return uint64; } isConstant() { From 6c2b45d005e7cb7c4b13224d3be7bba820cb5880 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 28 Jun 2023 14:59:59 -0700 Subject: [PATCH 37/90] feat(int.ts): add arithmetic operations (add, sub, mul, div, mod) to UInt8 class and base compare --- src/lib/int.ts | 106 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/src/lib/int.ts b/src/lib/int.ts index 2cf422a16..bfbce1fb8 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -981,6 +981,108 @@ class UInt8 extends Struct({ return new UInt8(1); } + add(y: UInt8 | number) { + if (isUInt8(y)) { + return new UInt8(this.value.add(y.value)); + } + let y_ = new UInt8(y); + return new UInt8(this.value.add(y_.value)); + } + + sub(y: UInt8 | number) { + if (isUInt8(y)) { + return new UInt8(this.value.sub(y.value)); + } + let y_ = new UInt8(y); + return new UInt8(this.value.sub(y_.value)); + } + + mul(y: UInt8 | number) { + if (isUInt8(y)) { + return new UInt8(this.value.mul(y.value)); + } + let y_ = new UInt8(y); + return new UInt8(this.value.mul(y_.value)); + } + + div(y: UInt8 | number) { + if (isUInt8(y)) { + return this.divMod(y).quotient; + } + let y_ = new UInt8(y); + return this.divMod(y_).quotient; + } + + mod(y: UInt8 | number) { + if (isUInt8(y)) { + return this.divMod(y).rest; + } + let y_ = new UInt8(y); + return this.divMod(y_).rest; + } + + divMod(y: UInt8 | number) { + let x = this.value; + let y_ = new UInt8(y).value; + + if (this.value.isConstant() && y_.isConstant()) { + let xn = x.toBigInt(); + let yn = y_.toBigInt(); + let q = xn / yn; + let r = xn - q * yn; + return { + quotient: new UInt8(Field(q)), + rest: new UInt8(Field(r)), + }; + } + + y_ = y_.seal(); + + let q = Provable.witness( + Field, + () => new Field(x.toBigInt() / y_.toBigInt()) + ); + + q.rangeCheckHelper(UInt8.NUM_BITS).assertEquals(q); + + // TODO: Could be a bit more efficient + let r = x.sub(q.mul(y_)).seal(); + r.rangeCheckHelper(UInt8.NUM_BITS).assertEquals(r); + + let r_ = new UInt8(r); + let q_ = new UInt8(q); + + r_.assertLessThan(new UInt8(y_)); + + return { quotient: q_, rest: r_ }; + } + + lessThanOrEqual(y: UInt8) { + if (this.value.isConstant() && y.value.isConstant()) { + return Bool(this.value.toBigInt() <= y.value.toBigInt()); + } else { + let xMinusY = this.value.sub(y.value).seal(); + let yMinusX = xMinusY.neg(); + let xMinusYFits = xMinusY + .rangeCheckHelper(UInt8.NUM_BITS) + .equals(xMinusY); + let yMinusXFits = yMinusX + .rangeCheckHelper(UInt8.NUM_BITS) + .equals(yMinusX); + xMinusYFits.or(yMinusXFits).assertEquals(true); + // x <= y if y - x fits in 64 bits + return yMinusXFits; + } + } + + lessThan(y: UInt8) { + return this.lessThanOrEqual(y).and(this.value.equals(y.value).not()); + } + + assertLessThan(y: UInt8, message?: string) { + this.lessThan(y).assertEquals(true, message); + } + toString() { return this.value.toString(); } @@ -1050,3 +1152,7 @@ class UInt8 extends Struct({ return x; } } + +function isUInt8(x: unknown): x is UInt8 { + return x instanceof UInt8; +} From fc7597046e14d7407d2a0e0990f9b409518b86a5 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 28 Jun 2023 15:07:06 -0700 Subject: [PATCH 38/90] feat(int.ts): add assertion methods for comparing UInt8 values --- src/lib/int.ts | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/lib/int.ts b/src/lib/int.ts index bfbce1fb8..146b94000 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -1083,6 +1083,41 @@ class UInt8 extends Struct({ this.lessThan(y).assertEquals(true, message); } + assertLessThanOrEqual(y: UInt8, message?: string) { + if (this.value.isConstant() && y.value.isConstant()) { + let x0 = this.value.toBigInt(); + let y0 = y.value.toBigInt(); + if (x0 > y0) { + if (message !== undefined) throw Error(message); + throw Error(`UInt8.assertLessThanOrEqual: expected ${x0} <= ${y0}`); + } + return; + } + let yMinusX = y.value.sub(this.value).seal(); + yMinusX.rangeCheckHelper(UInt8.NUM_BITS).assertEquals(yMinusX, message); + } + + greaterThan(y: UInt8) { + return y.lessThan(this); + } + + greaterThanOrEqual(y: UInt8) { + return this.lessThan(y).not(); + } + + assertGreaterThan(y: UInt8, message?: string) { + y.assertLessThan(this, message); + } + + assertGreaterThanOrEqual(y: UInt8, message?: string) { + y.assertLessThanOrEqual(this, message); + } + + assertEquals(y: number | bigint | UInt8, message?: string) { + let y_ = new UInt8(y); + this.toField().assertEquals(y_.toField(), message); + } + toString() { return this.value.toString(); } From be58d9a1181112565351a289f1b80de3c7acb948 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Thu, 29 Jun 2023 12:06:12 -0700 Subject: [PATCH 39/90] feat(int.ts): add more unit tests for UInt8 --- src/lib/int.test.ts | 908 ++++++++++++++++++++++++++++++++++- src/lib/int.ts | 27 +- src/lib/keccack.unit-test.ts | 6 +- src/lib/testing/random.ts | 3 - 4 files changed, 906 insertions(+), 38 deletions(-) diff --git a/src/lib/int.test.ts b/src/lib/int.test.ts index e75c5a679..53599b720 100644 --- a/src/lib/int.test.ts +++ b/src/lib/int.test.ts @@ -1,28 +1,15 @@ import { - isReady, Provable, - shutdown, Int64, UInt64, UInt32, + UInt8, Field, Bool, Sign, } from 'snarkyjs'; describe('int', () => { - beforeAll(async () => { - await isReady; - }); - - afterAll(async () => { - // Use a timeout to defer the execution of `shutdown()` until Jest processes all tests. - // `shutdown()` exits the process when it's done cleanup so we want to delay it's execution until Jest is done - setTimeout(async () => { - await shutdown(); - }, 0); - }); - const NUMBERMAX = 2 ** 53 - 1; // JavaScript numbers can only safely store integers in the range -(2^53 − 1) to 2^53 − 1 describe('Int64', () => { @@ -2150,4 +2137,897 @@ describe('int', () => { }); }); }); + + describe('UInt8', () => { + const NUMBERMAX = UInt8.MAXINT().value; + + describe('Inside circuit', () => { + describe('add', () => { + it('1+1=2', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(Field(1))); + const y = Provable.witness(UInt8, () => new UInt8(Field(1))); + x.add(y).assertEquals(new UInt8(Field(2))); + }); + }).not.toThrow(); + }); + + it('100+100=200', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(Field(100))); + const y = Provable.witness(UInt8, () => new UInt8(Field(100))); + x.add(y).assertEquals(new UInt8(Field(200))); + }); + }).not.toThrow(); + }); + + it('(MAXINT/2+MAXINT/2) adds to MAXINT', () => { + const n = Field((((1n << 8n) - 2n) / 2n).toString()); + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(n)); + const y = Provable.witness(UInt8, () => new UInt8(n)); + x.add(y).add(1).assertEquals(UInt8.MAXINT()); + }); + }).not.toThrow(); + }); + + it('should throw on overflow addition', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => UInt8.MAXINT()); + const y = Provable.witness(UInt8, () => new UInt8(Field(1))); + x.add(y); + }); + }).toThrow(); + }); + }); + + describe('sub', () => { + it('1-1=0', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(Field(1))); + const y = Provable.witness(UInt8, () => new UInt8(Field(1))); + x.sub(y).assertEquals(new UInt8(Field(0))); + }); + }).not.toThrow(); + }); + + it('100-50=50', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(Field(100))); + const y = Provable.witness(UInt8, () => new UInt8(Field(50))); + x.sub(y).assertEquals(new UInt8(Field(50))); + }); + }).not.toThrow(); + }); + + it('should throw on sub if results in negative number', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(Field(0))); + const y = Provable.witness(UInt8, () => new UInt8(Field(1))); + x.sub(y); + }); + }).toThrow(); + }); + }); + + describe('mul', () => { + it('1x2=2', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(Field(1))); + const y = Provable.witness(UInt8, () => new UInt8(Field(2))); + x.mul(y).assertEquals(new UInt8(Field(2))); + }); + }).not.toThrow(); + }); + + it('1x0=0', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(Field(1))); + const y = Provable.witness(UInt8, () => new UInt8(Field(0))); + x.mul(y).assertEquals(new UInt8(Field(0))); + }); + }).not.toThrow(); + }); + + it('12x20=240', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(Field(12))); + const y = Provable.witness(UInt8, () => new UInt8(Field(20))); + x.mul(y).assertEquals(new UInt8(Field(240))); + }); + }).not.toThrow(); + }); + + it('MAXINTx1=MAXINT', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => UInt8.MAXINT()); + const y = Provable.witness(UInt8, () => new UInt8(Field(1))); + x.mul(y).assertEquals(UInt8.MAXINT()); + }); + }).not.toThrow(); + }); + + it('should throw on overflow multiplication', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => UInt8.MAXINT()); + const y = Provable.witness(UInt8, () => new UInt8(Field(2))); + x.mul(y); + }); + }).toThrow(); + }); + }); + + describe('div', () => { + it('2/1=2', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(Field(2))); + const y = Provable.witness(UInt8, () => new UInt8(Field(1))); + x.div(y).assertEquals(new UInt8(Field(2))); + }); + }).not.toThrow(); + }); + + it('0/1=0', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(Field(0))); + const y = Provable.witness(UInt8, () => new UInt8(Field(1))); + x.div(y).assertEquals(new UInt8(Field(0))); + }); + }).not.toThrow(); + }); + + it('20/10=2', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(Field(20))); + const y = Provable.witness(UInt8, () => new UInt8(Field(10))); + x.div(y).assertEquals(new UInt8(Field(2))); + }); + }).not.toThrow(); + }); + + it('MAXINT/1=MAXINT', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => UInt8.MAXINT()); + const y = Provable.witness(UInt8, () => new UInt8(Field(1))); + x.div(y).assertEquals(UInt8.MAXINT()); + }); + }).not.toThrow(); + }); + + it('should throw on division by zero', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => UInt8.MAXINT()); + const y = Provable.witness(UInt8, () => new UInt8(Field(0))); + x.div(y); + }); + }).toThrow(); + }); + }); + + describe('mod', () => { + it('1%1=0', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(Field(1))); + const y = Provable.witness(UInt8, () => new UInt8(Field(1))); + x.mod(y).assertEquals(new UInt8(Field(0))); + }); + }).not.toThrow(); + }); + + it('50%32=18', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(Field(50))); + const y = Provable.witness(UInt8, () => new UInt8(Field(32))); + x.mod(y).assertEquals(new UInt8(Field(18))); + }); + }).not.toThrow(); + }); + + it('MAXINT%7=3', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => UInt8.MAXINT()); + const y = Provable.witness(UInt8, () => new UInt8(Field(7))); + x.mod(y).assertEquals(new UInt8(Field(3))); + }); + }).not.toThrow(); + }); + + it('should throw on mod by zero', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => UInt8.MAXINT()); + const y = Provable.witness(UInt8, () => new UInt8(Field(0))); + x.mod(y).assertEquals(new UInt8(Field(1))); + }); + }).toThrow(); + }); + }); + + describe('assertLt', () => { + it('1<2=true', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(Field(1))); + const y = Provable.witness(UInt8, () => new UInt8(Field(2))); + x.assertLessThan(y); + }); + }).not.toThrow(); + }); + + it('1<1=false', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(Field(1))); + const y = Provable.witness(UInt8, () => new UInt8(Field(1))); + x.assertLessThan(y); + }); + }).toThrow(); + }); + + it('2<1=false', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(Field(2))); + const y = Provable.witness(UInt8, () => new UInt8(Field(1))); + x.assertLessThan(y); + }); + }).toThrow(); + }); + + it('10<100=true', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(Field(10))); + const y = Provable.witness(UInt8, () => new UInt8(Field(100))); + x.assertLessThan(y); + }); + }).not.toThrow(); + }); + + it('100<10=false', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(Field(100))); + const y = Provable.witness(UInt8, () => new UInt8(Field(10))); + x.assertLessThan(y); + }); + }).toThrow(); + }); + + it('MAXINT { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => UInt8.MAXINT()); + const y = Provable.witness(UInt8, () => UInt8.MAXINT()); + x.assertLessThan(y); + }); + }).toThrow(); + }); + }); + + describe('assertLessThanOrEqual', () => { + it('1<=1=true', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(Field(1))); + const y = Provable.witness(UInt8, () => new UInt8(Field(1))); + x.assertLessThanOrEqual(y); + }); + }).not.toThrow(); + }); + + it('2<=1=false', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(Field(2))); + const y = Provable.witness(UInt8, () => new UInt8(Field(1))); + x.assertLessThanOrEqual(y); + }); + }).toThrow(); + }); + + it('10<=100=true', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(Field(10))); + const y = Provable.witness(UInt8, () => new UInt8(Field(100))); + x.assertLessThanOrEqual(y); + }); + }).not.toThrow(); + }); + + it('100<=10=false', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(Field(100))); + const y = Provable.witness(UInt8, () => new UInt8(Field(10))); + x.assertLessThanOrEqual(y); + }); + }).toThrow(); + }); + + it('MAXINT<=MAXINT=true', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => UInt8.MAXINT()); + const y = Provable.witness(UInt8, () => UInt8.MAXINT()); + x.assertLessThanOrEqual(y); + }); + }).not.toThrow(); + }); + }); + + describe('assertGreaterThan', () => { + it('2>1=true', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(Field(2))); + const y = Provable.witness(UInt8, () => new UInt8(Field(1))); + x.assertGreaterThan(y); + }); + }).not.toThrow(); + }); + + it('1>1=false', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(Field(1))); + const y = Provable.witness(UInt8, () => new UInt8(Field(1))); + x.assertGreaterThan(y); + }); + }).toThrow(); + }); + + it('1>2=false', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(Field(1))); + const y = Provable.witness(UInt8, () => new UInt8(Field(2))); + x.assertGreaterThan(y); + }); + }).toThrow(); + }); + + it('100>10=true', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(Field(100))); + const y = Provable.witness(UInt8, () => new UInt8(Field(10))); + x.assertGreaterThan(y); + }); + }).not.toThrow(); + }); + + it('10>100=false', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(Field(1000))); + const y = Provable.witness(UInt8, () => new UInt8(Field(100000))); + x.assertGreaterThan(y); + }); + }).toThrow(); + }); + + it('MAXINT>MAXINT=false', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => UInt8.MAXINT()); + const y = Provable.witness(UInt8, () => UInt8.MAXINT()); + x.assertGreaterThan(y); + }); + }).toThrow(); + }); + }); + + describe('assertGreaterThanOrEqual', () => { + it('1<=1=true', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(Field(1))); + const y = Provable.witness(UInt8, () => new UInt8(Field(1))); + x.assertGreaterThanOrEqual(y); + }); + }).not.toThrow(); + }); + + it('1>=2=false', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(Field(1))); + const y = Provable.witness(UInt8, () => new UInt8(Field(2))); + x.assertGreaterThanOrEqual(y); + }); + }).toThrow(); + }); + + it('100>=10=true', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(Field(100))); + const y = Provable.witness(UInt8, () => new UInt8(Field(10))); + x.assertGreaterThanOrEqual(y); + }); + }).not.toThrow(); + }); + + it('10>=100=false', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(Field(10))); + const y = Provable.witness(UInt8, () => new UInt8(Field(100))); + x.assertGreaterThanOrEqual(y); + }); + }).toThrow(); + }); + + it('MAXINT>=MAXINT=true', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => UInt8.MAXINT()); + const y = Provable.witness(UInt8, () => UInt8.MAXINT()); + x.assertGreaterThanOrEqual(y); + }); + }).not.toThrow(); + }); + }); + + describe('from() ', () => { + describe('fromNumber()', () => { + it('should be the same as Field(1)', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => UInt8.from(1)); + const y = Provable.witness(UInt8, () => new UInt8(Field(1))); + x.assertEquals(y); + }); + }).not.toThrow(); + }); + }); + describe('fromString()', () => { + it('should be the same as Field(1)', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => UInt8.from('1')); + const y = Provable.witness(UInt8, () => new UInt8(Field(1))); + x.assertEquals(y); + }); + }).not.toThrow(); + }); + }); + }); + }); + + describe('Outside of circuit', () => { + describe('add', () => { + it('1+1=2', () => { + expect(new UInt8(Field(1)).add(1).toString()).toEqual('2'); + }); + + it('50+50=100', () => { + expect(new UInt8(Field(50)).add(50).toString()).toEqual('100'); + }); + + it('(MAXINT/2+MAXINT/2) adds to MAXINT', () => { + const value = Field((((1n << 8n) - 2n) / 2n).toString()); + expect( + new UInt8(value) + .add(new UInt8(value)) + .add(new UInt8(Field(1))) + .toString() + ).toEqual(UInt8.MAXINT().toString()); + }); + + it('should throw on overflow addition', () => { + expect(() => { + UInt8.MAXINT().add(1); + }).toThrow(); + }); + }); + + describe('sub', () => { + it('1-1=0', () => { + expect(new UInt8(Field(1)).sub(1).toString()).toEqual('0'); + }); + + it('100-50=50', () => { + expect(new UInt8(Field(100)).sub(50).toString()).toEqual('50'); + }); + + it('should throw on sub if results in negative number', () => { + expect(() => { + UInt8.from(0).sub(1); + }).toThrow(); + }); + }); + + describe('mul', () => { + it('1x2=2', () => { + expect(new UInt8(Field(1)).mul(2).toString()).toEqual('2'); + }); + + it('1x0=0', () => { + expect(new UInt8(Field(1)).mul(0).toString()).toEqual('0'); + }); + + it('12x20=240', () => { + expect(new UInt8(Field(12)).mul(20).toString()).toEqual('240'); + }); + + it('MAXINTx1=MAXINT', () => { + expect(UInt8.MAXINT().mul(1).toString()).toEqual( + UInt8.MAXINT().toString() + ); + }); + + it('should throw on overflow multiplication', () => { + expect(() => { + UInt8.MAXINT().mul(2); + }).toThrow(); + }); + }); + + describe('div', () => { + it('2/1=2', () => { + expect(new UInt8(Field(2)).div(1).toString()).toEqual('2'); + }); + + it('0/1=0', () => { + expect(new UInt32(Field(0)).div(1).toString()).toEqual('0'); + }); + + it('20/10=2', () => { + expect(new UInt8(Field(20)).div(10).toString()).toEqual('2'); + }); + + it('MAXINT/1=MAXINT', () => { + expect(UInt8.MAXINT().div(1).toString()).toEqual( + UInt8.MAXINT().toString() + ); + }); + + it('should throw on division by zero', () => { + expect(() => { + UInt8.MAXINT().div(0); + }).toThrow(); + }); + }); + + describe('mod', () => { + it('1%1=0', () => { + expect(new UInt8(Field(1)).mod(1).toString()).toEqual('0'); + }); + + it('50%32=18', () => { + expect(new UInt8(Field(50)).mod(32).toString()).toEqual('18'); + }); + + it('MAXINT%7=3', () => { + expect(UInt8.MAXINT().mod(7).toString()).toEqual('3'); + }); + + it('should throw on mod by zero', () => { + expect(() => { + UInt8.MAXINT().mod(0); + }).toThrow(); + }); + }); + + describe('lessThan', () => { + it('1<2=true', () => { + expect(new UInt8(Field(1)).lessThan(new UInt8(Field(2)))).toEqual( + Bool(true) + ); + }); + + it('1<1=false', () => { + expect(new UInt8(Field(1)).lessThan(new UInt8(Field(1)))).toEqual( + Bool(false) + ); + }); + + it('2<1=false', () => { + expect(new UInt8(Field(2)).lessThan(new UInt8(Field(1)))).toEqual( + Bool(false) + ); + }); + + it('10<100=true', () => { + expect(new UInt8(Field(10)).lessThan(new UInt8(Field(100)))).toEqual( + Bool(true) + ); + }); + + it('100<10=false', () => { + expect(new UInt8(Field(100)).lessThan(new UInt8(Field(10)))).toEqual( + Bool(false) + ); + }); + + it('MAXINT { + expect(UInt8.MAXINT().lessThan(UInt8.MAXINT())).toEqual(Bool(false)); + }); + }); + + describe('lessThanOrEqual', () => { + it('1<=1=true', () => { + expect( + new UInt8(Field(1)).lessThanOrEqual(new UInt8(Field(1))) + ).toEqual(Bool(true)); + }); + + it('2<=1=false', () => { + expect( + new UInt8(Field(2)).lessThanOrEqual(new UInt8(Field(1))) + ).toEqual(Bool(false)); + }); + + it('10<=100=true', () => { + expect( + new UInt8(Field(10)).lessThanOrEqual(new UInt8(Field(100))) + ).toEqual(Bool(true)); + }); + + it('100<=10=false', () => { + expect( + new UInt8(Field(100)).lessThanOrEqual(new UInt8(Field(10))) + ).toEqual(Bool(false)); + }); + + it('MAXINT<=MAXINT=true', () => { + expect(UInt8.MAXINT().lessThanOrEqual(UInt8.MAXINT())).toEqual( + Bool(true) + ); + }); + }); + + describe('assertLessThanOrEqual', () => { + it('1<=1=true', () => { + expect(() => { + new UInt8(Field(1)).assertLessThanOrEqual(new UInt8(Field(1))); + }).not.toThrow(); + }); + + it('2<=1=false', () => { + expect(() => { + new UInt8(Field(2)).assertLessThanOrEqual(new UInt8(Field(1))); + }).toThrow(); + }); + + it('10<=100=true', () => { + expect(() => { + new UInt8(Field(10)).assertLessThanOrEqual(new UInt8(Field(100))); + }).not.toThrow(); + }); + + it('100<=10=false', () => { + expect(() => { + new UInt8(Field(100)).assertLessThanOrEqual(new UInt8(Field(10))); + }).toThrow(); + }); + + it('MAXINT<=MAXINT=true', () => { + expect(() => { + UInt8.MAXINT().assertLessThanOrEqual(UInt8.MAXINT()); + }).not.toThrow(); + }); + }); + + describe('greaterThan', () => { + it('2>1=true', () => { + expect(new UInt8(Field(2)).greaterThan(new UInt8(Field(1)))).toEqual( + Bool(true) + ); + }); + + it('1>1=false', () => { + expect(new UInt8(Field(1)).greaterThan(new UInt8(Field(1)))).toEqual( + Bool(false) + ); + }); + + it('1>2=false', () => { + expect(new UInt8(Field(1)).greaterThan(new UInt8(Field(2)))).toEqual( + Bool(false) + ); + }); + + it('100>10=true', () => { + expect( + new UInt8(Field(100)).greaterThan(new UInt8(Field(10))) + ).toEqual(Bool(true)); + }); + + it('10>100=false', () => { + expect( + new UInt8(Field(10)).greaterThan(new UInt8(Field(100))) + ).toEqual(Bool(false)); + }); + + it('MAXINT>MAXINT=false', () => { + expect(UInt8.MAXINT().greaterThan(UInt8.MAXINT())).toEqual( + Bool(false) + ); + }); + }); + + describe('assertGreaterThan', () => { + it('1>1=false', () => { + expect(() => { + new UInt8(Field(1)).assertGreaterThan(new UInt8(Field(1))); + }).toThrow(); + }); + + it('2>1=true', () => { + expect(() => { + new UInt8(Field(2)).assertGreaterThan(new UInt8(Field(1))); + }).not.toThrow(); + }); + + it('10>100=false', () => { + expect(() => { + new UInt8(Field(10)).assertGreaterThan(new UInt8(Field(100))); + }).toThrow(); + }); + + it('100000>1000=true', () => { + expect(() => { + new UInt8(Field(100)).assertGreaterThan(new UInt8(Field(10))); + }).not.toThrow(); + }); + + it('MAXINT>MAXINT=false', () => { + expect(() => { + UInt8.MAXINT().assertGreaterThan(UInt8.MAXINT()); + }).toThrow(); + }); + }); + + describe('greaterThanOrEqual', () => { + it('2>=1=true', () => { + expect( + new UInt8(Field(2)).greaterThanOrEqual(new UInt8(Field(1))) + ).toEqual(Bool(true)); + }); + + it('1>=1=true', () => { + expect( + new UInt8(Field(1)).greaterThanOrEqual(new UInt8(Field(1))) + ).toEqual(Bool(true)); + }); + + it('1>=2=false', () => { + expect( + new UInt8(Field(1)).greaterThanOrEqual(new UInt8(Field(2))) + ).toEqual(Bool(false)); + }); + + it('100>=10=true', () => { + expect( + new UInt8(Field(100)).greaterThanOrEqual(new UInt8(Field(10))) + ).toEqual(Bool(true)); + }); + + it('10>=100=false', () => { + expect( + new UInt8(Field(10)).greaterThanOrEqual(new UInt8(Field(100))) + ).toEqual(Bool(false)); + }); + + it('MAXINT>=MAXINT=true', () => { + expect(UInt8.MAXINT().greaterThanOrEqual(UInt8.MAXINT())).toEqual( + Bool(true) + ); + }); + }); + + describe('assertGreaterThanOrEqual', () => { + it('1>=1=true', () => { + expect(() => { + new UInt8(Field(1)).assertGreaterThanOrEqual(new UInt8(Field(1))); + }).not.toThrow(); + }); + + it('2>=1=true', () => { + expect(() => { + new UInt8(Field(2)).assertGreaterThanOrEqual(new UInt8(Field(1))); + }).not.toThrow(); + }); + + it('10>=100=false', () => { + expect(() => { + new UInt8(Field(10)).assertGreaterThanOrEqual( + new UInt8(Field(100)) + ); + }).toThrow(); + }); + + it('100>=10=true', () => { + expect(() => { + new UInt8(Field(100)).assertGreaterThanOrEqual( + new UInt8(Field(10)) + ); + }).not.toThrow(); + }); + + it('MAXINT>=MAXINT=true', () => { + expect(() => { + UInt32.MAXINT().assertGreaterThanOrEqual(UInt32.MAXINT()); + }).not.toThrow(); + }); + }); + + describe('toString()', () => { + it('should be the same as Field(0)', async () => { + const x = new UInt8(Field(0)); + const y = Field(0); + expect(x.toString()).toEqual(y.toString()); + }); + it('should be the same as 2^8-1', async () => { + const x = new UInt8(Field(String(NUMBERMAX))); + const y = Field(String(NUMBERMAX)); + expect(x.toString()).toEqual(y.toString()); + }); + }); + + describe('check()', () => { + it('should pass checking a MAXINT', () => { + expect(() => { + UInt8.check(UInt8.MAXINT()); + }).not.toThrow(); + }); + + it('should throw checking over MAXINT', () => { + const x = UInt8.MAXINT(); + expect(() => { + UInt8.check(x.add(1)); + }).toThrow(); + }); + }); + + describe('from() ', () => { + describe('fromNumber()', () => { + it('should be the same as Field(1)', () => { + const x = UInt8.from(1); + expect(x.value).toEqual(new UInt32(Field(1)).value); + }); + + it('should be the same as 2^53-1', () => { + const x = UInt8.from(NUMBERMAX); + expect(x.value).toEqual(Field(String(NUMBERMAX))); + }); + }); + describe('fromString()', () => { + it('should be the same as Field(1)', () => { + const x = UInt8.from('1'); + expect(x.value).toEqual(new UInt32(Field(1)).value); + }); + + it('should be the same as 2^53-1', () => { + const x = UInt8.from(String(NUMBERMAX)); + expect(x.value).toEqual(Field(String(NUMBERMAX))); + }); + }); + }); + }); + }); }); diff --git a/src/lib/int.ts b/src/lib/int.ts index 146b94000..c8bd9338b 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -1037,17 +1037,13 @@ class UInt8 extends Struct({ } y_ = y_.seal(); - let q = Provable.witness( Field, () => new Field(x.toBigInt() / y_.toBigInt()) ); - q.rangeCheckHelper(UInt8.NUM_BITS).assertEquals(q); - // TODO: Could be a bit more efficient let r = x.sub(q.mul(y_)).seal(); - r.rangeCheckHelper(UInt8.NUM_BITS).assertEquals(r); let r_ = new UInt8(r); let q_ = new UInt8(q); @@ -1061,17 +1057,7 @@ class UInt8 extends Struct({ if (this.value.isConstant() && y.value.isConstant()) { return Bool(this.value.toBigInt() <= y.value.toBigInt()); } else { - let xMinusY = this.value.sub(y.value).seal(); - let yMinusX = xMinusY.neg(); - let xMinusYFits = xMinusY - .rangeCheckHelper(UInt8.NUM_BITS) - .equals(xMinusY); - let yMinusXFits = yMinusX - .rangeCheckHelper(UInt8.NUM_BITS) - .equals(yMinusX); - xMinusYFits.or(yMinusXFits).assertEquals(true); - // x <= y if y - x fits in 64 bits - return yMinusXFits; + return this.value.lessThanOrEqual(y.value); } } @@ -1093,8 +1079,7 @@ class UInt8 extends Struct({ } return; } - let yMinusX = y.value.sub(this.value).seal(); - yMinusX.rangeCheckHelper(UInt8.NUM_BITS).assertEquals(yMinusX, message); + return this.lessThanOrEqual(y).assertEquals(true, message); } greaterThan(y: UInt8) { @@ -1174,10 +1159,16 @@ class UInt8 extends Struct({ return new UInt8(255); } - static from(x: UInt64 | UInt32 | Field | number | string | bigint) { + static from( + x: UInt64 | UInt32 | Field | number | string | bigint | number[] + ) { if (x instanceof UInt64 || x instanceof UInt32 || x instanceof UInt8) x = x.value; + if (Array.isArray(x)) { + return new this(Field.fromBytes(x)); + } + return new this(this.checkConstant(Field(x))); } diff --git a/src/lib/keccack.unit-test.ts b/src/lib/keccack.unit-test.ts index 99029c3e8..d80908011 100644 --- a/src/lib/keccack.unit-test.ts +++ b/src/lib/keccack.unit-test.ts @@ -5,7 +5,7 @@ import { Provable } from './provable.js'; import { expect } from 'expect'; // Test constructor -test(Random.uint8, Random.json.uint8, (x, y, assert) => { +test(Random.uint8, Random.uint8, (x, y, assert) => { let z = new UInt8(x); assert(z instanceof UInt8); assert(z.toBigInt() === x); @@ -18,8 +18,8 @@ test(Random.uint8, Random.json.uint8, (x, y, assert) => { z = new UInt8(y); assert(z instanceof UInt8); - assert(z.toString() === y); - assert(z.toJSON() === y); + assert(z.toString() === y.toString()); + assert(z.toJSON() === y.toString()); }); // handles all numbers up to 2^8 diff --git a/src/lib/testing/random.ts b/src/lib/testing/random.ts index 74ffe3e32..7da60c806 100644 --- a/src/lib/testing/random.ts +++ b/src/lib/testing/random.ts @@ -116,7 +116,6 @@ type Generators = { const Generators: Generators = { Field: field, Bool: bool, - UInt8: uint8, UInt32: uint32, UInt64: uint64, Sign: sign, @@ -195,7 +194,6 @@ const invalidUint64Json = toString( // some json versions of those types let json_ = { - uint8: { ...toString(uint8), invalid: invalidUint8Json }, uint32: { ...toString(uint32), invalid: invalidUint32Json }, uint64: { ...toString(uint64), invalid: invalidUint64Json }, publicKey: withInvalidBase58(mapWithInvalid(publicKey, PublicKey.toBase58)), @@ -221,7 +219,6 @@ type JsonGenerators = { const JsonGenerators: JsonGenerators = { Field: json_.field, Bool: boolean, - UInt8: json_.uint8, UInt32: json_.uint32, UInt64: json_.uint64, Sign: withInvalidRandomString(map(sign, Sign.toJSON)), From 88c687fdda42fbc05bb30bddc545bc84b219e0a4 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Thu, 29 Jun 2023 12:35:47 -0700 Subject: [PATCH 40/90] feat(keccack.unit-test.ts): add support for provable testing --- src/lib/keccack.unit-test.ts | 74 ++++++++++++++++++++++++------------ 1 file changed, 49 insertions(+), 25 deletions(-) diff --git a/src/lib/keccack.unit-test.ts b/src/lib/keccack.unit-test.ts index d80908011..d8f8289e3 100644 --- a/src/lib/keccack.unit-test.ts +++ b/src/lib/keccack.unit-test.ts @@ -4,6 +4,8 @@ import { Hash } from './hash.js'; import { Provable } from './provable.js'; import { expect } from 'expect'; +let RandomUInt8 = Random.map(Random.uint8, (x) => UInt8.from(x)); + // Test constructor test(Random.uint8, Random.uint8, (x, y, assert) => { let z = new UInt8(x); @@ -34,45 +36,67 @@ test.negative(Random.int(-10, -1), (x) => new UInt8(x)); test.negative(Random.uint8.invalid, (x) => new UInt8(x)); // test digest->hex and hex->digest conversions -checkHashConversions(); +checkHashInCircuit(); +checkHashOutCircuit(); console.log('hashing digest conversions matches! 🎉'); -function checkHashConversions() { - for (let i = 0; i < 2; i++) { - const data = Random.array(Random.uint8, Random.nat(20)) - .create()() - .map((x) => new UInt8(x)); +// check in-circuit +function checkHashInCircuit() { + Provable.runAndCheck(() => { + let d0 = Provable.witness(UInt8, () => new UInt8(RandomUInt8.create()())); + let d1 = Provable.witness(UInt8, () => new UInt8(RandomUInt8.create()())); + let d2 = Provable.witness(UInt8, () => new UInt8(RandomUInt8.create()())); + let d3 = Provable.witness(UInt8, () => new UInt8(RandomUInt8.create()())); + let d4 = Provable.witness(UInt8, () => new UInt8(RandomUInt8.create()())); + + let data = [d0, d1, d2, d3, d4]; + checkHashConversions(data, true); + }); +} - Provable.runAndCheck(() => { - let digest = Hash.SHA224.hash(data); - expectDigestToEqualHex(digest); +// check out-of-circuit +function checkHashOutCircuit() { + let r = Random.array(RandomUInt8, Random.nat(20)).create()(); + checkHashConversions(r, false); +} - digest = Hash.SHA256.hash(data); - expectDigestToEqualHex(digest); +function checkHashConversions(data: UInt8[], provable: boolean) { + let digest = Hash.SHA224.hash(data); + expectDigestToEqualHex(digest, provable); - digest = Hash.SHA384.hash(data); - expectDigestToEqualHex(digest); + digest = Hash.SHA256.hash(data); + expectDigestToEqualHex(digest, provable); - digest = Hash.SHA512.hash(data); - expectDigestToEqualHex(digest); + digest = Hash.SHA384.hash(data); + expectDigestToEqualHex(digest, provable); - digest = Hash.Keccack256.hash(data); - expectDigestToEqualHex(digest); - }); - } + digest = Hash.SHA512.hash(data); + expectDigestToEqualHex(digest, provable); + + digest = Hash.Keccack256.hash(data); + expectDigestToEqualHex(digest, provable); } -function expectDigestToEqualHex(digest: UInt8[]) { - Provable.asProver(() => { +function expectDigestToEqualHex(digest: UInt8[], provable: boolean) { + if (provable) { + Provable.asProver(() => { + const hex = UInt8.toHex(digest); + expect(equals(digest, UInt8.fromHex(hex), provable)).toBe(true); + }); + } else { const hex = UInt8.toHex(digest); - expect(equals(digest, UInt8.fromHex(hex))).toBe(true); - }); + expect(equals(digest, UInt8.fromHex(hex), provable)).toBe(true); + } } -function equals(a: UInt8[], b: UInt8[]): boolean { +function equals(a: UInt8[], b: UInt8[], provable: boolean): boolean { if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) - if (a[i].value.toConstant() === b[i].value.toConstant()) return false; + if (provable) { + a[i].assertEquals(b[i]); + } else { + if (a[i].value.toConstant() === b[i].value.toConstant()) return false; + } return true; } From e432819af8a69022c24b9412a5f942c075ce4cc5 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Thu, 29 Jun 2023 12:37:53 -0700 Subject: [PATCH 41/90] refactor(keccak): rename keccack -> keccak --- src/examples/keccak.ts | 4 ++-- src/lib/hash.ts | 2 +- src/lib/{keccack.unit-test.ts => keccak.unit-test.ts} | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename src/lib/{keccack.unit-test.ts => keccak.unit-test.ts} (98%) diff --git a/src/examples/keccak.ts b/src/examples/keccak.ts index 1fcf7557f..799685876 100644 --- a/src/examples/keccak.ts +++ b/src/examples/keccak.ts @@ -47,9 +47,9 @@ Provable.runAndCheck(() => { checkDigestHexConversion(digest); }); -console.log('Running keccack hash test'); +console.log('Running keccak hash test'); Provable.runAndCheck(() => { - let digest = Hash.Keccack256.hash([new UInt8(1), new UInt8(2), new UInt8(3)]); + let digest = Hash.Keccak256.hash([new UInt8(1), new UInt8(2), new UInt8(3)]); checkDigestHexConversion(digest); }); diff --git a/src/lib/hash.ts b/src/lib/hash.ts index 853006b9f..a5137d169 100644 --- a/src/lib/hash.ts +++ b/src/lib/hash.ts @@ -218,5 +218,5 @@ const Hash = { SHA512: buildSHA(512, true), - Keccack256: buildSHA(256, false), + Keccak256: buildSHA(256, false), }; diff --git a/src/lib/keccack.unit-test.ts b/src/lib/keccak.unit-test.ts similarity index 98% rename from src/lib/keccack.unit-test.ts rename to src/lib/keccak.unit-test.ts index d8f8289e3..d4130ba5a 100644 --- a/src/lib/keccack.unit-test.ts +++ b/src/lib/keccak.unit-test.ts @@ -73,7 +73,7 @@ function checkHashConversions(data: UInt8[], provable: boolean) { digest = Hash.SHA512.hash(data); expectDigestToEqualHex(digest, provable); - digest = Hash.Keccack256.hash(data); + digest = Hash.Keccak256.hash(data); expectDigestToEqualHex(digest, provable); } From 71b4ee753771df7f66b9d68f23ca0cc7a6544962 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Thu, 29 Jun 2023 12:54:55 -0700 Subject: [PATCH 42/90] refactor(keccak.unit-test.ts): remove checkHashOutCircuit --- src/lib/keccak.unit-test.ts | 63 ++++++++++--------------------------- 1 file changed, 17 insertions(+), 46 deletions(-) diff --git a/src/lib/keccak.unit-test.ts b/src/lib/keccak.unit-test.ts index d4130ba5a..31616a214 100644 --- a/src/lib/keccak.unit-test.ts +++ b/src/lib/keccak.unit-test.ts @@ -37,66 +37,37 @@ test.negative(Random.uint8.invalid, (x) => new UInt8(x)); // test digest->hex and hex->digest conversions checkHashInCircuit(); -checkHashOutCircuit(); console.log('hashing digest conversions matches! 🎉'); // check in-circuit function checkHashInCircuit() { Provable.runAndCheck(() => { - let d0 = Provable.witness(UInt8, () => new UInt8(RandomUInt8.create()())); - let d1 = Provable.witness(UInt8, () => new UInt8(RandomUInt8.create()())); - let d2 = Provable.witness(UInt8, () => new UInt8(RandomUInt8.create()())); - let d3 = Provable.witness(UInt8, () => new UInt8(RandomUInt8.create()())); - let d4 = Provable.witness(UInt8, () => new UInt8(RandomUInt8.create()())); + let data = Random.array(RandomUInt8, Random.nat(32)) + .create()() + .map((x) => Provable.witness(UInt8, () => new UInt8(x))); - let data = [d0, d1, d2, d3, d4]; - checkHashConversions(data, true); + checkHashConversions(data); }); } -// check out-of-circuit -function checkHashOutCircuit() { - let r = Random.array(RandomUInt8, Random.nat(20)).create()(); - checkHashConversions(r, false); -} - -function checkHashConversions(data: UInt8[], provable: boolean) { - let digest = Hash.SHA224.hash(data); - expectDigestToEqualHex(digest, provable); - - digest = Hash.SHA256.hash(data); - expectDigestToEqualHex(digest, provable); - - digest = Hash.SHA384.hash(data); - expectDigestToEqualHex(digest, provable); - - digest = Hash.SHA512.hash(data); - expectDigestToEqualHex(digest, provable); - - digest = Hash.Keccak256.hash(data); - expectDigestToEqualHex(digest, provable); +function checkHashConversions(data: UInt8[]) { + Provable.asProver(() => { + expectDigestToEqualHex(Hash.SHA224.hash(data)); + expectDigestToEqualHex(Hash.SHA256.hash(data)); + expectDigestToEqualHex(Hash.SHA384.hash(data)); + expectDigestToEqualHex(Hash.SHA512.hash(data)); + expectDigestToEqualHex(Hash.Keccak256.hash(data)); + }); } -function expectDigestToEqualHex(digest: UInt8[], provable: boolean) { - if (provable) { - Provable.asProver(() => { - const hex = UInt8.toHex(digest); - expect(equals(digest, UInt8.fromHex(hex), provable)).toBe(true); - }); - } else { - const hex = UInt8.toHex(digest); - expect(equals(digest, UInt8.fromHex(hex), provable)).toBe(true); - } +function expectDigestToEqualHex(digest: UInt8[]) { + const hex = UInt8.toHex(digest); + expect(equals(digest, UInt8.fromHex(hex))).toBe(true); } -function equals(a: UInt8[], b: UInt8[], provable: boolean): boolean { +function equals(a: UInt8[], b: UInt8[]): boolean { if (a.length !== b.length) return false; - for (let i = 0; i < a.length; i++) - if (provable) { - a[i].assertEquals(b[i]); - } else { - if (a[i].value.toConstant() === b[i].value.toConstant()) return false; - } + for (let i = 0; i < a.length; i++) a[i].assertEquals(b[i]); return true; } From db79cd308e2d38a652eee2efd714c2e43a90e72f Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Thu, 29 Jun 2023 15:04:21 -0700 Subject: [PATCH 43/90] feat(vk_regression): add hashing to vk regression tests --- src/examples/primitive_constraint_system.ts | 47 ++++++++++++++++++++- src/examples/vk_regression.ts | 3 +- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/examples/primitive_constraint_system.ts b/src/examples/primitive_constraint_system.ts index f03bc3bbf..454fb58ff 100644 --- a/src/examples/primitive_constraint_system.ts +++ b/src/examples/primitive_constraint_system.ts @@ -1,4 +1,4 @@ -import { Field, Group, Poseidon, Provable, Scalar } from 'snarkyjs'; +import { Field, Group, Provable, Scalar, Hash, UInt8 } from 'snarkyjs'; function mock(obj: { [K: string]: (...args: any) => void }, name: string) { let methodKeys = Object.keys(obj); @@ -63,4 +63,49 @@ const GroupMock = { }, }; +const HashMock = { + SHA224() { + let xs = Array.from({ length: 32 }, (_, i) => i).map((x) => + Provable.witness(UInt8, () => UInt8.from(x)) + ); + Hash.SHA224.hash(xs); + }, + + SHA256() { + let xs = Array.from({ length: 32 }, (_, i) => i).map((x) => + Provable.witness(UInt8, () => UInt8.from(x)) + ); + Hash.SHA256.hash(xs); + }, + + SHA384() { + let xs = Array.from({ length: 32 }, (_, i) => i).map((x) => + Provable.witness(UInt8, () => UInt8.from(x)) + ); + Hash.SHA384.hash(xs); + }, + + SHA512() { + let xs = Array.from({ length: 32 }, (_, i) => i).map((x) => + Provable.witness(UInt8, () => UInt8.from(x)) + ); + Hash.SHA512.hash(xs); + }, + + Keccak256() { + let xs = Array.from({ length: 32 }, (_, i) => i).map((x) => + Provable.witness(UInt8, () => UInt8.from(x)) + ); + Hash.Keccak256.hash(xs); + }, + + Poseidon() { + let xs = Array.from({ length: 32 }, (_, i) => i).map((x) => + Provable.witness(Field, () => Field(x)) + ); + Hash.Poseidon.hash(xs); + }, +}; + export const GroupCS = mock(GroupMock, 'Group Primitive'); +export const HashCS = mock(HashMock, 'SHA Primitive'); diff --git a/src/examples/vk_regression.ts b/src/examples/vk_regression.ts index 6dbcd3137..6d301c9aa 100644 --- a/src/examples/vk_regression.ts +++ b/src/examples/vk_regression.ts @@ -3,7 +3,7 @@ import { Voting_ } from './zkapps/voting/voting.js'; import { Membership_ } from './zkapps/voting/membership.js'; import { HelloWorld } from './zkapps/hello_world/hello_world.js'; import { TokenContract, createDex } from './zkapps/dex/dex.js'; -import { GroupCS } from './primitive_constraint_system.js'; +import { GroupCS, HashCS } from './primitive_constraint_system.js'; // toggle this for quick iteration when debugging vk regressions const skipVerificationKeys = false; @@ -37,6 +37,7 @@ const ConstraintSystems: MinimumConstraintSystem[] = [ TokenContract, createDex().Dex, GroupCS, + HashCS, ]; let filePath = jsonPath ? jsonPath : './src/examples/regression_test.json'; From 36e145f57024b609a56ae4a623db8fb6333e65d9 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Thu, 29 Jun 2023 15:08:48 -0700 Subject: [PATCH 44/90] fix(keccak-test): replace usage of ctor with .from --- src/lib/int.ts | 2 +- src/lib/keccak.unit-test.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lib/int.ts b/src/lib/int.ts index c8bd9338b..382f32ca0 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -1160,7 +1160,7 @@ class UInt8 extends Struct({ } static from( - x: UInt64 | UInt32 | Field | number | string | bigint | number[] + x: UInt8 | UInt64 | UInt32 | Field | number | string | bigint | number[] ) { if (x instanceof UInt64 || x instanceof UInt32 || x instanceof UInt8) x = x.value; diff --git a/src/lib/keccak.unit-test.ts b/src/lib/keccak.unit-test.ts index 31616a214..343d1e60d 100644 --- a/src/lib/keccak.unit-test.ts +++ b/src/lib/keccak.unit-test.ts @@ -26,14 +26,14 @@ test(Random.uint8, Random.uint8, (x, y, assert) => { // handles all numbers up to 2^8 test(Random.nat(255), (n, assert) => { - assert(new UInt8(n).toString() === String(n)); + assert(UInt8.from(n).toString() === String(n)); }); // throws on negative numbers -test.negative(Random.int(-10, -1), (x) => new UInt8(x)); +test.negative(Random.int(-10, -1), (x) => UInt8.from(x)); // throws on numbers >= 2^8 -test.negative(Random.uint8.invalid, (x) => new UInt8(x)); +test.negative(Random.uint8.invalid, (x) => UInt8.from(x)); // test digest->hex and hex->digest conversions checkHashInCircuit(); @@ -44,7 +44,7 @@ function checkHashInCircuit() { Provable.runAndCheck(() => { let data = Random.array(RandomUInt8, Random.nat(32)) .create()() - .map((x) => Provable.witness(UInt8, () => new UInt8(x))); + .map((x) => Provable.witness(UInt8, () => UInt8.from(x))); checkHashConversions(data); }); From 4360afeef2b011e347d239370f18f9d450f77e6f Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Thu, 29 Jun 2023 16:12:50 -0700 Subject: [PATCH 45/90] feat(examples): start debugging hashing example --- src/examples/zkapps/hashing/hash.ts | 96 +++++++++++++++++++++++++ src/examples/zkapps/hashing/run.ts | 107 ++++++++++++++++++++++++++++ 2 files changed, 203 insertions(+) create mode 100644 src/examples/zkapps/hashing/hash.ts create mode 100644 src/examples/zkapps/hashing/run.ts diff --git a/src/examples/zkapps/hashing/hash.ts b/src/examples/zkapps/hashing/hash.ts new file mode 100644 index 000000000..2e361a347 --- /dev/null +++ b/src/examples/zkapps/hashing/hash.ts @@ -0,0 +1,96 @@ +import { + Hash, + UInt8, + Field, + SmartContract, + state, + State, + method, + PrivateKey, + Permissions, + Struct, +} from 'snarkyjs'; + +export const adminPrivateKey = PrivateKey.random(); +export const adminPublicKey = adminPrivateKey.toPublicKey(); + +let initialCommitment: Field = Field(0); + +// 32 UInts +export class HashInput extends Struct({ + data: [ + UInt8, + UInt8, + UInt8, + UInt8, + UInt8, + UInt8, + UInt8, + UInt8, + UInt8, + UInt8, + UInt8, + UInt8, + UInt8, + UInt8, + UInt8, + UInt8, + UInt8, + UInt8, + UInt8, + UInt8, + UInt8, + UInt8, + UInt8, + UInt8, + UInt8, + UInt8, + UInt8, + UInt8, + UInt8, + UInt8, + ], +}) {} + +export class HashStorage extends SmartContract { + @state(Field) commitment = State(); + + init() { + super.init(); + this.account.permissions.set({ + ...Permissions.default(), + editState: Permissions.proofOrSignature(), + }); + this.commitment.set(initialCommitment); + } + + @method SHA224(xs: HashInput) { + const shaHash = Hash.SHA224.hash(xs.data); + const commitment = Hash.hash(shaHash.map((f) => f.toField())); + this.commitment.set(commitment); + } + + @method SHA256(xs: HashInput) { + const shaHash = Hash.SHA256.hash(xs.data); + const commitment = Hash.hash(shaHash.map((f) => f.toField())); + this.commitment.set(commitment); + } + + @method SHA384(xs: HashInput) { + const shaHash = Hash.SHA384.hash(xs.data); + const commitment = Hash.hash(shaHash.map((f) => f.toField())); + this.commitment.set(commitment); + } + + @method SHA512(xs: HashInput) { + const shaHash = Hash.SHA512.hash(xs.data); + const commitment = Hash.hash(shaHash.map((f) => f.toField())); + this.commitment.set(commitment); + } + + @method Keccak256(xs: HashInput) { + const shaHash = Hash.Keccak256.hash(xs.data); + const commitment = Hash.hash(shaHash.map((f) => f.toField())); + this.commitment.set(commitment); + } +} diff --git a/src/examples/zkapps/hashing/run.ts b/src/examples/zkapps/hashing/run.ts new file mode 100644 index 000000000..47a376bc4 --- /dev/null +++ b/src/examples/zkapps/hashing/run.ts @@ -0,0 +1,107 @@ +import { HashStorage, HashInput } from './hash.js'; +import { Mina, PrivateKey, AccountUpdate, UInt8 } from 'snarkyjs'; +import { getProfiler } from '../../profiler.js'; + +const HashProfier = getProfiler('Hash'); +HashProfier.start('Hash test flow'); + +let txn; +let proofsEnabled = true; +// setup local ledger +let Local = Mina.LocalBlockchain({ proofsEnabled }); +Mina.setActiveInstance(Local); + +if (proofsEnabled) { + console.log('Proofs enabled'); + HashStorage.compile(); +} + +// test accounts that pays all the fees, and puts additional funds into the zkapp +const feePayer = Local.testAccounts[0]; + +// zkapp account +const zkAppPrivateKey = PrivateKey.random(); +const zkAppAddress = zkAppPrivateKey.toPublicKey(); +const zkAppInstance = new HashStorage(zkAppAddress); + +// 0, 1, 2, 3, ..., 32 +const hashData = new HashInput({ + data: Array.from({ length: 32 }, (_, i) => i).map((x) => UInt8.from(x)), +}); + +console.log('Deploying Hash Example....'); + +txn = await Mina.transaction(feePayer.publicKey, () => { + AccountUpdate.fundNewAccount(feePayer.publicKey); + zkAppInstance.deploy(); +}); +await txn.sign([feePayer.privateKey, zkAppPrivateKey]).send(); + +const initialState = + Mina.getAccount(zkAppAddress).zkapp?.appState?.[0].toString(); + +let currentState; + +console.log('Initial State', initialState); + +console.log(`Updating commitment from ${initialState} using SHA224 ...`); + +txn = await Mina.transaction(feePayer.publicKey, () => { + zkAppInstance.SHA224(hashData); +}); +await txn.prove(); +await txn.sign([feePayer.privateKey]).send(); + +currentState = Mina.getAccount(zkAppAddress).zkapp?.appState?.[0].toString(); + +console.log(`Current state successfully updated to ${currentState}`); + +console.log(`Updating commitment from ${initialState} using SHA256 ...`); + +txn = await Mina.transaction(feePayer.publicKey, () => { + zkAppInstance.SHA256(hashData); +}); +await txn.prove(); +await txn.sign([feePayer.privateKey]).send(); + +currentState = Mina.getAccount(zkAppAddress).zkapp?.appState?.[0].toString(); + +console.log(`Current state successfully updated to ${currentState}`); + +console.log(`Updating commitment from ${initialState} using SHA384 ...`); + +txn = await Mina.transaction(feePayer.publicKey, () => { + zkAppInstance.SHA384(hashData); +}); +await txn.prove(); +await txn.sign([feePayer.privateKey]).send(); + +currentState = Mina.getAccount(zkAppAddress).zkapp?.appState?.[0].toString(); + +console.log(`Current state successfully updated to ${currentState}`); + +console.log(`Updating commitment from ${initialState} using SHA512 ...`); + +txn = await Mina.transaction(feePayer.publicKey, () => { + zkAppInstance.SHA512(hashData); +}); +await txn.prove(); +await txn.sign([feePayer.privateKey]).send(); + +currentState = Mina.getAccount(zkAppAddress).zkapp?.appState?.[0].toString(); + +console.log(`Current state successfully updated to ${currentState}`); + +console.log(`Updating commitment from ${initialState} using Keccak256...`); + +txn = await Mina.transaction(feePayer.publicKey, () => { + zkAppInstance.Keccak256(hashData); +}); +await txn.prove(); +await txn.sign([feePayer.privateKey]).send(); + +currentState = Mina.getAccount(zkAppAddress).zkapp?.appState?.[0].toString(); + +console.log(`Current state successfully updated to ${currentState}`); + +HashProfier.stop().store(); From 5885af01807d66a67ddc76d78b5e9222ad06fb41 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Thu, 29 Jun 2023 16:21:54 -0700 Subject: [PATCH 46/90] fix(keccak.ts): fix equals function to correctly compare UInt8 arrays --- src/examples/keccak.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/examples/keccak.ts b/src/examples/keccak.ts index 799685876..243b64996 100644 --- a/src/examples/keccak.ts +++ b/src/examples/keccak.ts @@ -2,9 +2,7 @@ import { Field, Provable, Hash, UInt8 } from 'snarkyjs'; function equals(a: UInt8[], b: UInt8[]): boolean { if (a.length !== b.length) return false; - for (let i = 0; i < a.length; i++) - if (a[i].value.toConstant() === b[i].value.toConstant()) return false; - + for (let i = 0; i < a.length; i++) a[i].assertEquals(b[i]); return true; } From f86b23cbccc89b09d64e5be837d2e2fd50111bee Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Thu, 29 Jun 2023 16:23:00 -0700 Subject: [PATCH 47/90] refactor(hash.ts): remove unused code and variables for better code cleanliness and maintainability --- src/examples/zkapps/hashing/hash.ts | 4 ---- src/examples/zkapps/hashing/run.ts | 17 ----------------- src/lib/hash.ts | 4 ++-- src/lib/int.ts | 2 +- 4 files changed, 3 insertions(+), 24 deletions(-) diff --git a/src/examples/zkapps/hashing/hash.ts b/src/examples/zkapps/hashing/hash.ts index 2e361a347..308cf48bc 100644 --- a/src/examples/zkapps/hashing/hash.ts +++ b/src/examples/zkapps/hashing/hash.ts @@ -6,14 +6,10 @@ import { state, State, method, - PrivateKey, Permissions, Struct, } from 'snarkyjs'; -export const adminPrivateKey = PrivateKey.random(); -export const adminPublicKey = adminPrivateKey.toPublicKey(); - let initialCommitment: Field = Field(0); // 32 UInts diff --git a/src/examples/zkapps/hashing/run.ts b/src/examples/zkapps/hashing/run.ts index 47a376bc4..b413cd09d 100644 --- a/src/examples/zkapps/hashing/run.ts +++ b/src/examples/zkapps/hashing/run.ts @@ -30,7 +30,6 @@ const hashData = new HashInput({ }); console.log('Deploying Hash Example....'); - txn = await Mina.transaction(feePayer.publicKey, () => { AccountUpdate.fundNewAccount(feePayer.publicKey); zkAppInstance.deploy(); @@ -41,67 +40,51 @@ const initialState = Mina.getAccount(zkAppAddress).zkapp?.appState?.[0].toString(); let currentState; - console.log('Initial State', initialState); console.log(`Updating commitment from ${initialState} using SHA224 ...`); - txn = await Mina.transaction(feePayer.publicKey, () => { zkAppInstance.SHA224(hashData); }); await txn.prove(); await txn.sign([feePayer.privateKey]).send(); - currentState = Mina.getAccount(zkAppAddress).zkapp?.appState?.[0].toString(); - console.log(`Current state successfully updated to ${currentState}`); console.log(`Updating commitment from ${initialState} using SHA256 ...`); - txn = await Mina.transaction(feePayer.publicKey, () => { zkAppInstance.SHA256(hashData); }); await txn.prove(); await txn.sign([feePayer.privateKey]).send(); - currentState = Mina.getAccount(zkAppAddress).zkapp?.appState?.[0].toString(); - console.log(`Current state successfully updated to ${currentState}`); console.log(`Updating commitment from ${initialState} using SHA384 ...`); - txn = await Mina.transaction(feePayer.publicKey, () => { zkAppInstance.SHA384(hashData); }); await txn.prove(); await txn.sign([feePayer.privateKey]).send(); - currentState = Mina.getAccount(zkAppAddress).zkapp?.appState?.[0].toString(); - console.log(`Current state successfully updated to ${currentState}`); console.log(`Updating commitment from ${initialState} using SHA512 ...`); - txn = await Mina.transaction(feePayer.publicKey, () => { zkAppInstance.SHA512(hashData); }); await txn.prove(); await txn.sign([feePayer.privateKey]).send(); - currentState = Mina.getAccount(zkAppAddress).zkapp?.appState?.[0].toString(); - console.log(`Current state successfully updated to ${currentState}`); console.log(`Updating commitment from ${initialState} using Keccak256...`); - txn = await Mina.transaction(feePayer.publicKey, () => { zkAppInstance.Keccak256(hashData); }); await txn.prove(); await txn.sign([feePayer.privateKey]).send(); - currentState = Mina.getAccount(zkAppAddress).zkapp?.appState?.[0].toString(); - console.log(`Current state successfully updated to ${currentState}`); HashProfier.stop().store(); diff --git a/src/lib/hash.ts b/src/lib/hash.ts index a5137d169..cb911bf33 100644 --- a/src/lib/hash.ts +++ b/src/lib/hash.ts @@ -199,8 +199,8 @@ function buildSHA(length: 224 | 256 | 384 | 512, nist: boolean) { return { hash(message: UInt8[]): UInt8[] { return Snarky.sha - .create([0, ...message.map((f) => f.value.value)], nist, length) - .map((f) => new UInt8(Field(f))); + .create([0, ...message.map((f) => f.toField().value)], nist, length) + .map((f) => UInt8.from(Field(f))); }, }; } diff --git a/src/lib/int.ts b/src/lib/int.ts index 382f32ca0..c196e6328 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -1156,7 +1156,7 @@ class UInt8 extends Struct({ } static MAXINT() { - return new UInt8(255); + return new UInt8(1 << (this.NUM_BITS - 1)); } static from( From 068fb4de239a907c2c42c7d493f5ba96d7b7f15f Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Fri, 30 Jun 2023 12:41:55 -0700 Subject: [PATCH 48/90] feat(UInt8): refactor ctor to use .from instead --- src/lib/int.ts | 49 ++++++++++++++----------------------------------- 1 file changed, 14 insertions(+), 35 deletions(-) diff --git a/src/lib/int.ts b/src/lib/int.ts index c196e6328..f326e2ed7 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -974,56 +974,36 @@ class UInt8 extends Struct({ } static get zero() { - return new UInt8(0); + return UInt8.from(0); } static get one() { - return new UInt8(1); + return UInt8.from(1); } add(y: UInt8 | number) { - if (isUInt8(y)) { - return new UInt8(this.value.add(y.value)); - } - let y_ = new UInt8(y); - return new UInt8(this.value.add(y_.value)); + return UInt8.from(this.value.add(UInt8.from(y).value)); } sub(y: UInt8 | number) { - if (isUInt8(y)) { - return new UInt8(this.value.sub(y.value)); - } - let y_ = new UInt8(y); - return new UInt8(this.value.sub(y_.value)); + return UInt8.from(this.value.sub(UInt8.from(y).value)); } mul(y: UInt8 | number) { - if (isUInt8(y)) { - return new UInt8(this.value.mul(y.value)); - } - let y_ = new UInt8(y); - return new UInt8(this.value.mul(y_.value)); + return UInt8.from(this.value.mul(UInt8.from(y).value)); } div(y: UInt8 | number) { - if (isUInt8(y)) { - return this.divMod(y).quotient; - } - let y_ = new UInt8(y); - return this.divMod(y_).quotient; + return this.divMod(y).quotient; } mod(y: UInt8 | number) { - if (isUInt8(y)) { - return this.divMod(y).rest; - } - let y_ = new UInt8(y); - return this.divMod(y_).rest; + return this.divMod(y).rest; } divMod(y: UInt8 | number) { let x = this.value; - let y_ = new UInt8(y).value; + let y_ = UInt8.from(y).value; if (this.value.isConstant() && y_.isConstant()) { let xn = x.toBigInt(); @@ -1031,8 +1011,8 @@ class UInt8 extends Struct({ let q = xn / yn; let r = xn - q * yn; return { - quotient: new UInt8(Field(q)), - rest: new UInt8(Field(r)), + quotient: UInt8.from(Field(q)), + rest: UInt8.from(Field(r)), }; } @@ -1045,11 +1025,10 @@ class UInt8 extends Struct({ // TODO: Could be a bit more efficient let r = x.sub(q.mul(y_)).seal(); - let r_ = new UInt8(r); - let q_ = new UInt8(q); - - r_.assertLessThan(new UInt8(y_)); + let r_ = UInt8.from(r); + let q_ = UInt8.from(q); + r_.assertLessThan(UInt8.from(y_)); return { quotient: q_, rest: r_ }; } @@ -1156,7 +1135,7 @@ class UInt8 extends Struct({ } static MAXINT() { - return new UInt8(1 << (this.NUM_BITS - 1)); + return new UInt8(Field((1n << BigInt(this.NUM_BITS)) - 1n)); } static from( From b59bb9028ad3c6a0a0c938bd64896a381a537469 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Mon, 3 Jul 2023 12:17:08 -0700 Subject: [PATCH 49/90] feat(uint8): add most doc comments --- src/lib/int.ts | 406 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 375 insertions(+), 31 deletions(-) diff --git a/src/lib/int.ts b/src/lib/int.ts index f326e2ed7..260cdab18 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -961,11 +961,21 @@ class Int64 extends CircuitValue implements BalanceChange { } } +/** + * A 8 bit unsigned integer with values ranging from 0 to 255. + */ class UInt8 extends Struct({ value: Field, }) { static NUM_BITS = 8; + /** + * Coerce anything "field-like" (bigint, number, string, and {@link Field}) to a {@link UInt8}. + * The max value of a {@link UInt8} is `2^8 - 1 = 255`. + * + * + * **Warning**: Cannot overflow past 255, an error is thrown if the result is greater than 255. + */ constructor(x: number | bigint | string | Field | UInt8) { if (x instanceof UInt8) return x; @@ -973,37 +983,131 @@ class UInt8 extends Struct({ this.value.toBits(UInt8.NUM_BITS); // Make sure that the Field element that is exactly a byte } + /** + * Static method to create a {@link UInt8} with value `0`. + */ static get zero() { return UInt8.from(0); } + /** + * Static method to create a {@link UInt8} with value `1`. + */ static get one() { return UInt8.from(1); } - add(y: UInt8 | number) { - return UInt8.from(this.value.add(UInt8.from(y).value)); + /** + * Add a {@link UInt8} value to another {@link UInt8} element. + * + * @example + * ```ts + * const x = UInt8.from(3); + * const sum = x.add(UInt8.from(5)); + * + * sum.assertEquals(UInt8.from(8)); + * ``` + * + * **Warning**: This operation cannot overflow past 255, an error is thrown if the result is greater than 255. + * + * @param value - a {@link UInt8} value to add to the {@link UInt8}. + * + * @return A {@link UInt8} element that is the sum of the two values. + */ + add(value: UInt8 | number) { + return UInt8.from(this.value.add(UInt8.from(value).value)); } - sub(y: UInt8 | number) { - return UInt8.from(this.value.sub(UInt8.from(y).value)); + /** + * Subtract a {@link UInt8} value by another {@link UInt8} element. + * + * @example + * ```ts + * const x = UInt8.from(8); + * const difference = x.sub(UInt8.from(5)); + * + * difference.assertEquals(UInt8.from(3)); + * ``` + * + * @param value - a {@link UInt8} value to subtract from the {@link UInt8}. + * + * @return A {@link UInt8} element that is the difference of the two values. + */ + sub(value: UInt8 | number) { + return UInt8.from(this.value.sub(UInt8.from(value).value)); } - mul(y: UInt8 | number) { - return UInt8.from(this.value.mul(UInt8.from(y).value)); + /** + * Multiply a {@link UInt8} value by another {@link UInt8} element. + * + * @example + * ```ts + * const x = UInt8.from(3); + * const product = x.mul(UInt8.from(5)); + * + * product.assertEquals(UInt8.from(15)); + * ``` + * + * **Warning**: This operation cannot overflow past 255, an error is thrown if the result is greater than 255. + * + * @param value - a {@link UInt8} value to multiply with the {@link UInt8}. + * + * @return A {@link UInt8} element that is the product of the two values. + */ + mul(value: UInt8 | number) { + return UInt8.from(this.value.mul(UInt8.from(value).value)); } - div(y: UInt8 | number) { - return this.divMod(y).quotient; + /** + * Divide a {@link UInt8} value by another {@link UInt8} element. + * + * Proves that the denominator is non-zero, or throws a "Division by zero" error. + * + * @example + * ```ts + * const x = UInt8.from(6); + * const quotient = x.div(UInt8.from(3)); + * + * quotient.assertEquals(UInt8.from(2)); + * ``` + * + * @param value - a {@link UInt8} value to divide with the {@link UInt8}. + * + * @return A {@link UInt8} element that is the division of the two values. + */ + div(value: UInt8 | number) { + return this.divMod(value).quotient; } - mod(y: UInt8 | number) { - return this.divMod(y).rest; + /** + * Get the remainder a {@link UInt8} value of division of another {@link UInt8} element. + * + * @example + * ```ts + * const x = UInt8.from(50); + * const mod = x.mod(UInt8.from(30)); + * + * mod.assertEquals(UInt8.from(18)); + * ``` + * + * @param value - a {@link UInt8} to get the modulus with another {@link UInt8}. + * + * @return A {@link UInt8} element that is the modulus of the two values. + */ + mod(value: UInt8 | number) { + return this.divMod(value).rest; } - divMod(y: UInt8 | number) { + /** + * Get the quotient and remainder of a {@link UInt8} value divided by another {@link UInt8} element. + * + * @param value - a {@link UInt8} to get the quotient and remainder of another {@link UInt8}. + * + * @return The quotient and remainder of the two values. + */ + divMod(value: UInt8 | number) { let x = this.value; - let y_ = UInt8.from(y).value; + let y_ = UInt8.from(value).value; if (this.value.isConstant() && y_.isConstant()) { let xn = x.toBigInt(); @@ -1032,6 +1136,23 @@ class UInt8 extends Struct({ return { quotient: q_, rest: r_ }; } + /** + * Check if this {@link UInt8} is less than or equal to another {@link UInt8} value. + * Returns a {@link Bool}, which is a provable type and can be used to prove the validity of this statement. + * + * @example + * ```ts + * UInt8.from(3).lessThanOrEqual(UInt8.from(5)).assertEquals(Bool(true)); + * ``` + * + * **Warning**: Comparison methods only support UInt8 elements of size <= 8 bits in provable code. + * The method will throw if one of the inputs exceeds 8 bits. + * + * + * @param value - the {@link UInt8} value to compare with this {@link UInt8}. + * + * @return A {@link Bool} representing if this {@link UInt8} is less than or equal another {@link UInt8} value. + */ lessThanOrEqual(y: UInt8) { if (this.value.isConstant() && y.value.isConstant()) { return Bool(this.value.toBigInt() <= y.value.toBigInt()); @@ -1040,90 +1161,307 @@ class UInt8 extends Struct({ } } - lessThan(y: UInt8) { - return this.lessThanOrEqual(y).and(this.value.equals(y.value).not()); + /** + * Check if this {@link UInt8} is less than another {@link UInt8} value. + * Returns a {@link Bool}, which is a provable type and can be used prove to the validity of this statement. + * + * @example + * ```ts + * UInt8.from(2).lessThan(UInt8.from(3)).assertEquals(Bool(true)); + * ``` + * + * **Warning**: Comparison methods only support UInt8 elements of size <= 8 bits in provable code. + * The method will throw if one of the inputs exceeds 8 bits. + * + * @param value - the {@link UInt8} value to compare with this {@link UInt8}. + * + * @return A {@link Bool} representing if this {@link UInt8} is less than another {@link UInt8} value. + */ + lessThan(value: UInt8) { + return this.lessThanOrEqual(value).and( + this.value.equals(value.value).not() + ); } - assertLessThan(y: UInt8, message?: string) { - this.lessThan(y).assertEquals(true, message); + /** + * Assert that this {@link UInt8} is less than another {@link UInt8} value. + * Calling this function is equivalent to `UInt8(...).lessThan(...).assertEquals(Bool(true))`. + * See {@link UInt8.lessThan} for more details. + * + * **Important**: If an assertion fails, the code throws an error. + * + * **Warning**: Comparison methods only support UInt8 elements of size <= 8 bits in provable code. + * The method will throw if one of the inputs exceeds 8 bits. + * + * @param value - the {@link UInt8} value to compare & assert with this {@link UInt8}. + * @param message? - a string error message to print if the assertion fails, optional. + */ + assertLessThan(value: UInt8, message?: string) { + this.lessThan(value).assertEquals(true, message); } - assertLessThanOrEqual(y: UInt8, message?: string) { - if (this.value.isConstant() && y.value.isConstant()) { + /** + * Assert that this {@link UInt8} is less than or equal to another {@link UInt8} value. + * Calling this function is equivalent to `UInt8(...).lessThanOrEqual(...).assertEquals(Bool(true))`. + * See {@link UInt8.lessThanOrEqual} for more details. + * + * **Important**: If an assertion fails, the code throws an error. + * + * **Warning**: Comparison methods only support UInt8 elements of size <= 8 bits in provable code. + * The method will throw if one of the inputs exceeds 8 bits. + * + * @param value - the {@link UInt8} value to compare & assert with this {@link UInt8}. + * @param message? - a string error message to print if the assertion fails, optional. + */ + assertLessThanOrEqual(value: UInt8, message?: string) { + if (this.value.isConstant() && value.value.isConstant()) { let x0 = this.value.toBigInt(); - let y0 = y.value.toBigInt(); + let y0 = value.value.toBigInt(); if (x0 > y0) { if (message !== undefined) throw Error(message); throw Error(`UInt8.assertLessThanOrEqual: expected ${x0} <= ${y0}`); } return; } - return this.lessThanOrEqual(y).assertEquals(true, message); + return this.lessThanOrEqual(value).assertEquals(true, message); } - greaterThan(y: UInt8) { - return y.lessThan(this); + /** + * Check if this {@link UInt8} is greater than another {@link UInt8} value. + * Returns a {@link Bool}, which is a provable type and can be used to prove the validity of this statement. + * + * @example + * ```ts + * UInt8.from(5).greaterThan(UInt8.from(3)).assertEquals(Bool(true)); + * ``` + * + * **Warning**: Comparison methods currently only support Field elements of size <= 8 bits in provable code. + * The method will throw if one of the inputs exceeds 8 bits. + * + * @param value - the {@link UInt8} value to compare with this {@link UInt8}. + * + * @return A {@link Bool} representing if this {@link UInt8} is greater than another {@link UInt8} value. + */ + greaterThan(value: UInt8) { + return value.lessThan(this); } - greaterThanOrEqual(y: UInt8) { - return this.lessThan(y).not(); + /** + * Check if this {@link UInt8} is greater than or equal another {@link UInt8} value. + * Returns a {@link Bool}, which is a provable type and can be used to prove the validity of this statement. + * + * @example + * ```ts + * UInt8.from(3).greaterThanOrEqual(UInt8.from(3)).assertEquals(Bool(true)); + * ``` + * + * **Warning**: Comparison methods only support UInt8 elements of size <= 8 bits in provable code. + * The method will throw if one of the inputs exceeds 8 bits. + * + * @param value - the {@link UInt8} value to compare with this {@link Field}. + * + * @return A {@link Bool} representing if this {@link UInt8} is greater than or equal another {@link UInt8} value. + */ + greaterThanOrEqual(value: UInt8) { + return this.lessThan(value).not(); } - assertGreaterThan(y: UInt8, message?: string) { - y.assertLessThan(this, message); + /** + * Assert that this {@link UInt8} is greater than another {@link UInt8} value. + * Calling this function is equivalent to `UInt8(...).greaterThan(...).assertEquals(Bool(true))`. + * See {@link UInt8.greaterThan} for more details. + * + * **Important**: If an assertion fails, the code throws an error. + * + * **Warning**: Comparison methods only support UInt8 elements of size <= 8 bits in provable code. + * The method will throw if one of the inputs exceeds 8 bits. + * + * @param value - the {@link UInt8} value to compare & assert with this {@link UInt8}. + * @param message? - a string error message to print if the assertion fails, optional. + */ + assertGreaterThan(value: UInt8, message?: string) { + value.assertLessThan(this, message); } - assertGreaterThanOrEqual(y: UInt8, message?: string) { - y.assertLessThanOrEqual(this, message); + /** + * Assert that this {@link UInt8} is greater than or equal to another {@link UInt8} value. + * Calling this function is equivalent to `UInt8(...).greaterThanOrEqual(...).assertEquals(Bool(true))`. + * See {@link UInt8.greaterThanOrEqual} for more details. + * + * **Important**: If an assertion fails, the code throws an error. + * + * **Warning**: Comparison methods only support UInt8 elements of size <= 8 bits in provable code. + * The method will throw if one of the inputs exceeds 8 bits. + * + * @param value - the {@link UInt8} value to compare & assert with this {@link UInt8}. + * @param message? - a string error message to print if the assertion fails, optional. + */ + assertGreaterThanOrEqual(value: UInt8, message?: string) { + value.assertLessThanOrEqual(this, message); } - assertEquals(y: number | bigint | UInt8, message?: string) { - let y_ = new UInt8(y); + /** + * Assert that this {@link UInt8} is equal another {@link UInt8} value. + * + * **Important**: If an assertion fails, the code throws an error. + * + * @param value - the {@link UInt8} value to compare & assert with this {@link UInt8}. + * @param message? - a string error message to print if the assertion fails, optional. + */ + assertEquals(value: number | bigint | UInt8, message?: string) { + let y_ = new UInt8(value); this.toField().assertEquals(y_.toField(), message); } + /** + * Serialize the {@link UInt8} to a string, e.g. for printing. + * + * **Warning**: This operation does _not_ affect the circuit and can't be used to prove anything about the string representation of the {@link UInt8}. Use the operation only during debugging. + * + * @example + * ```ts + * const someUInt8 = UInt8.from(42); + * console.log(someUInt8 .toString()); + * ``` + * + * @return A string equivalent to the string representation of the {@link UInt8}. + */ toString() { return this.value.toString(); } + /** + * Serialize the {@link UInt8} to a bigint, e.g. for printing. + * + * **Warning**: This operation does _not_ affect the circuit and can't be used to prove anything about the bigint representation of the {@link UInt8}. Use the operation only during debugging. + * + * @example + * ```ts + * const someUInt8 = UInt8.from(42); + * console.log(someUInt8.toBigInt()); + * ``` + * + * @return A bigint equivalent to the bigint representation of the {@link UInt8}. + */ toBigInt() { return this.value.toBigInt(); } + /** + * Serialize the {@link UInt8} to a {@link Field}. + * + * @example + * ```ts + * const someUInt8 = UInt8.from(42); + * console.log(someUInt8.toField()); + * ``` + * + * @return A {@link Field} equivalent to the bigint representation of the {@link UInt8}. + */ toField() { return this.value; } + /** + * This function is the implementation of {@link Provable.check} in {@link UInt8} type. + * + * This function is called by {@link Provable.check} to check if the {@link UInt8} is valid. + * To check if a {@link UInt8} is valid, we need to check if the value fits in {@link UInt8.NUM_BITS} bits. + * + * @param value - the {@link UInt8} element to check. + */ check() { this.value.toBits(UInt8.NUM_BITS); } + /** + * Implementation of {@link Provable.fromFields} for the {@link UInt8} type. + * + * **Warning**: This function is designed for internal use. It is not intended to be used by a zkApp developer. + * + * @param fields - an array of {@link UInt8} serialized from {@link Field} elements. + * + * @return An array of {@link UInt8} elements of the given array. + */ fromFields(xs: Field[]): UInt8[] { return xs.map((x) => new UInt8(x)); } + /** + * Serialize the {@link UInt8} to a JSON string, e.g. for printing. + * + * **Warning**: This operation does _not_ affect the circuit and can't be used to prove anything about the JSON string representation of the {@link UInt8}. Use the operation only during debugging. + * + * @example + * ```ts + * const someUInt8 = UInt8.from(42); + * console.log(someUInt8 .toJSON()); + * ``` + * + * @return A string equivalent to the JSON representation of the {@link Field}. + */ toJSON(): string { return this.value.toString(); } + /** + * Turns a {@link UInt8} into a {@link UInt32}. + * + * @example + * ```ts + * const someUInt8 = UInt8.from(42); + * const someUInt32 = someUInt8.toUInt32(); + * ``` + * + * @return A {@link UInt32} equivalent to the {@link UInt8}. + */ toUInt32(): UInt32 { let uint32 = new UInt32(this.value); UInt32.check(uint32); return uint32; } + /** + * Turns a {@link UInt8} into a {@link UInt64}. + * + * @example + * ```ts + * const someUInt8 = UInt8.from(42); + * const someUInt64 = someUInt8.toUInt64(); + * ``` + * + * @return A {@link UInt64} equivalent to the {@link UInt8}. + * */ toUInt64(): UInt64 { let uint64 = new UInt64(this.value); UInt64.check(uint64); return uint64; } + /** + * Check whether this {@link UInt8} element is a hard-coded constant in the constraint system. + * If a {@link UInt8} is constructed outside a zkApp method, it is a constant. + * + * @example + * ```ts + * console.log(UInt8.from(42).isConstant()); // true + * ``` + * + * @example + * ```ts + * \@method myMethod(x: UInt8) { + * console.log(x.isConstant()); // false + * } + * ``` + * + * @return A `boolean` showing if this {@link UInt8} is a constant or not. + */ isConstant() { return this.value.isConstant(); } static fromHex(xs: string): UInt8[] { - return Snarky.sha.fieldBytesFromHex(xs).map((x) => new UInt8(Field(x))); + return Snarky.sha.fieldBytesFromHex(xs).map((x) => UInt8.from(Field(x))); } static toHex(xs: UInt8[]): string { @@ -1134,10 +1472,16 @@ class UInt8 extends Struct({ .join(''); } + /** + * Creates a {@link UInt8} with a value of 255. + */ static MAXINT() { return new UInt8(Field((1n << BigInt(this.NUM_BITS)) - 1n)); } + /** + * Creates a new {@link UInt8}. + */ static from( x: UInt8 | UInt64 | UInt32 | Field | number | string | bigint | number[] ) { From 2fdafc65276033c018063c1a9e1bfab5f6e72891 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Mon, 3 Jul 2023 12:24:17 -0700 Subject: [PATCH 50/90] chore(uint8: add TODO comments for more efficient range checking --- src/lib/int.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/lib/int.ts b/src/lib/int.ts index 260cdab18..0ae548bc7 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -1125,10 +1125,13 @@ class UInt8 extends Struct({ Field, () => new Field(x.toBigInt() / y_.toBigInt()) ); + // TODO: Need to range check `q` // TODO: Could be a bit more efficient let r = x.sub(q.mul(y_)).seal(); + // TODO: Need to range check `r` + let r_ = UInt8.from(r); let q_ = UInt8.from(q); @@ -1157,6 +1160,7 @@ class UInt8 extends Struct({ if (this.value.isConstant() && y.value.isConstant()) { return Bool(this.value.toBigInt() <= y.value.toBigInt()); } else { + // TODO: Need more efficient range checking return this.value.lessThanOrEqual(y.value); } } @@ -1223,6 +1227,7 @@ class UInt8 extends Struct({ } return; } + // TODO: Need more efficient range checking return this.lessThanOrEqual(value).assertEquals(true, message); } From 3e24ff7d98a4650cb2982dd382d07b149a817b05 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Mon, 3 Jul 2023 14:39:29 -0700 Subject: [PATCH 51/90] refactor(hash.ts): move fromHex and toHex from UInt8 to Hash namespace --- src/examples/keccak.ts | 4 ++-- src/lib/hash.ts | 12 ++++++++++++ src/lib/int.ts | 12 ------------ src/lib/keccak.unit-test.ts | 4 ++-- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/examples/keccak.ts b/src/examples/keccak.ts index 243b64996..7f3a26bc5 100644 --- a/src/examples/keccak.ts +++ b/src/examples/keccak.ts @@ -9,8 +9,8 @@ function equals(a: UInt8[], b: UInt8[]): boolean { function checkDigestHexConversion(digest: UInt8[]) { console.log('Checking hex->digest, digest->hex matches'); Provable.asProver(() => { - const hex = UInt8.toHex(digest); - const expected = UInt8.fromHex(hex); + const hex = Hash.toHex(digest); + const expected = Hash.fromHex(hex); if (equals(digest, expected)) { console.log('✅ Digest matches'); } else { diff --git a/src/lib/hash.ts b/src/lib/hash.ts index cb911bf33..e4018964b 100644 --- a/src/lib/hash.ts +++ b/src/lib/hash.ts @@ -219,4 +219,16 @@ const Hash = { SHA512: buildSHA(512, true), Keccak256: buildSHA(256, false), + + fromHex(xs: string): UInt8[] { + return Snarky.sha.fieldBytesFromHex(xs).map((x) => UInt8.from(Field(x))); + }, + + toHex(xs: UInt8[]): string { + return xs + .map((x) => x.value) + .map((f) => Field.toBytes(f)[0].toString(16).padStart(2, '0')) + .slice(1) + .join(''); + }, }; diff --git a/src/lib/int.ts b/src/lib/int.ts index 0ae548bc7..e8cd7ae47 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -1465,18 +1465,6 @@ class UInt8 extends Struct({ return this.value.isConstant(); } - static fromHex(xs: string): UInt8[] { - return Snarky.sha.fieldBytesFromHex(xs).map((x) => UInt8.from(Field(x))); - } - - static toHex(xs: UInt8[]): string { - return xs - .map((x) => x.value) - .map((f) => Field.toBytes(f)[0].toString(16).padStart(2, '0')) - .slice(1) - .join(''); - } - /** * Creates a {@link UInt8} with a value of 255. */ diff --git a/src/lib/keccak.unit-test.ts b/src/lib/keccak.unit-test.ts index 343d1e60d..b36ba5c9b 100644 --- a/src/lib/keccak.unit-test.ts +++ b/src/lib/keccak.unit-test.ts @@ -61,8 +61,8 @@ function checkHashConversions(data: UInt8[]) { } function expectDigestToEqualHex(digest: UInt8[]) { - const hex = UInt8.toHex(digest); - expect(equals(digest, UInt8.fromHex(hex))).toBe(true); + const hex = Hash.toHex(digest); + expect(equals(digest, Hash.fromHex(hex))).toBe(true); } function equals(a: UInt8[], b: UInt8[]): boolean { From 003c1ca0af9deed420e1772709cb17e9c8989efc Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Mon, 3 Jul 2023 16:50:43 -0700 Subject: [PATCH 52/90] feat(keccak): add ocaml unit tests --- src/lib/hash.ts | 9 +- src/lib/keccak.unit-test.ts | 208 ++++++++++++++++++++++++++++++++++++ 2 files changed, 214 insertions(+), 3 deletions(-) diff --git a/src/lib/hash.ts b/src/lib/hash.ts index e4018964b..c4a08be54 100644 --- a/src/lib/hash.ts +++ b/src/lib/hash.ts @@ -200,7 +200,8 @@ function buildSHA(length: 224 | 256 | 384 | 512, nist: boolean) { hash(message: UInt8[]): UInt8[] { return Snarky.sha .create([0, ...message.map((f) => f.toField().value)], nist, length) - .map((f) => UInt8.from(Field(f))); + .map((f) => UInt8.from(Field(f))) + .slice(1); }, }; } @@ -221,14 +222,16 @@ const Hash = { Keccak256: buildSHA(256, false), fromHex(xs: string): UInt8[] { - return Snarky.sha.fieldBytesFromHex(xs).map((x) => UInt8.from(Field(x))); + return Snarky.sha + .fieldBytesFromHex(xs) + .map((x) => UInt8.from(Field(x))) + .slice(1); }, toHex(xs: UInt8[]): string { return xs .map((x) => x.value) .map((f) => Field.toBytes(f)[0].toString(16).padStart(2, '0')) - .slice(1) .join(''); }, }; diff --git a/src/lib/keccak.unit-test.ts b/src/lib/keccak.unit-test.ts index b36ba5c9b..e86361d3e 100644 --- a/src/lib/keccak.unit-test.ts +++ b/src/lib/keccak.unit-test.ts @@ -3,6 +3,7 @@ import { UInt8 } from './int.js'; import { Hash } from './hash.js'; import { Provable } from './provable.js'; import { expect } from 'expect'; +import assert from 'assert'; let RandomUInt8 = Random.map(Random.uint8, (x) => UInt8.from(x)); @@ -35,6 +36,9 @@ test.negative(Random.int(-10, -1), (x) => UInt8.from(x)); // throws on numbers >= 2^8 test.negative(Random.uint8.invalid, (x) => UInt8.from(x)); +runHashFunctionTests(); +console.log('OCaml tests pass! 🎉'); + // test digest->hex and hex->digest conversions checkHashInCircuit(); console.log('hashing digest conversions matches! 🎉'); @@ -66,8 +70,212 @@ function expectDigestToEqualHex(digest: UInt8[]) { } function equals(a: UInt8[], b: UInt8[]): boolean { + // Provable.log('DEBUG', 'a', a); + // Provable.log('DEBUG', 'b', b); + if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) a[i].assertEquals(b[i]); return true; } + +/** + * Based off the following unit tests from the OCaml implementation: + * https://github.com/MinaProtocol/mina/blob/69d6ea4a3b7ca1690cf8f41d4598cb7484359e1d/src/lib/crypto/kimchi_backend/gadgets/keccak.ml#L646 + */ +function runHashFunctionTests() { + // Positive Tests + testExpected({ + nist: false, + length: 256, + message: '30', + expected: + '044852b2a670ade5407e78fb2863c51de9fcb96542a07186fe3aeda6bb8a116d', + }); + + testExpected({ + nist: true, + length: 512, + message: '30', + expected: + '2d44da53f305ab94b6365837b9803627ab098c41a6013694f9b468bccb9c13e95b3900365eb58924de7158a54467e984efcfdabdbcc9af9a940d49c51455b04c', + }); + + testExpected({ + nist: false, + length: 256, + message: + '4920616d20746865206f776e6572206f6620746865204e465420776974682069642058206f6e2074686520457468657265756d20636861696e', + expected: + '63858e0487687c3eeb30796a3e9307680e1b81b860b01c88ff74545c2c314e36', + }); + + testExpected({ + nist: false, + length: 256, + message: + '044852b2a670ade5407e78fb2863c51de9fcb96542a07186fe3aeda6bb8a116df9e2eaaa42d9fe9e558a9b8ef1bf366f190aacaa83bad2641ee106e9041096e42d44da53f305ab94b6365837b9803627ab098c41a6013694f9b468bccb9c13e95b3900365eb58924de7158a54467e984efcfdabdbcc9af9a940d49c51455b04c63858e0487687c3eeb30796a3e9307680e1b81b860b01c88ff74545c2c314e36', + expected: + '560deb1d387f72dba729f0bd0231ad45998dda4b53951645322cf95c7b6261d9', + }); + + testExpected({ + nist: true, + length: 256, + message: + '044852b2a670ade5407e78fb2863c51de9fcb96542a07186fe3aeda6bb8a116df9e2eaaa42d9fe9e558a9b8ef1bf366f190aacaa83bad2641ee106e9041096e42d44da53f305ab94b6365837b9803627ab098c41a6013694f9b468bccb9c13e95b3900365eb58924de7158a54467e984efcfdabdbcc9af9a940d49c51455b04c63858e0487687c3eeb30796a3e9307680e1b81b860b01c88ff74545c2c314e36', + expected: + '1784354c4bbfa5f54e5db23041089e65a807a7b970e3cfdba95e2fbe63b1c0e4', + }); + + testExpected({ + nist: false, + length: 256, + message: + '391ccf9b5de23bb86ec6b2b142adb6e9ba6bee8519e7502fb8be8959fbd2672934cc3e13b7b45bf2b8a5cb48881790a7438b4a326a0c762e31280711e6b64fcc2e3e4e631e501d398861172ea98603618b8f23b91d0208b0b992dfe7fdb298b6465adafbd45e4f88ee9dc94e06bc4232be91587f78572c169d4de4d8b95b714ea62f1fbf3c67a4', + expected: + '7d5655391ede9ca2945f32ad9696f464be8004389151ce444c89f688278f2e1d', + }); + + testExpected({ + nist: false, + length: 256, + message: + 'ff391ccf9b5de23bb86ec6b2b142adb6e9ba6bee8519e7502fb8be8959fbd2672934cc3e13b7b45bf2b8a5cb48881790a7438b4a326a0c762e31280711e6b64fcc2e3e4e631e501d398861172ea98603618b8f23b91d0208b0b992dfe7fdb298b6465adafbd45e4f88ee9dc94e06bc4232be91587f78572c169d4de4d8b95b714ea62f1fbf3c67a4', + expected: + '37694fd4ba137be747eb25a85b259af5563e0a7a3010d42bd15963ac631b9d3f', + }); + + testExpected({ + nist: false, + length: 256, + message: + '80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001', + expected: + 'bbf1f49a2cc5678aa62196d0c3108d89425b81780e1e90bcec03b4fb5f834714', + }); + + testExpected({ + nist: false, + length: 256, + message: + '80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001', + expected: + 'bbf1f49a2cc5678aa62196d0c3108d89425b81780e1e90bcec03b4fb5f834714', + }); + + testExpected({ + nist: false, + length: 256, + message: 'a2c0', + expected: + '9856642c690c036527b8274db1b6f58c0429a88d9f3b9298597645991f4f58f0', + }); + + testExpected({ + nist: false, + length: 256, + message: '0a2c', + expected: + '295b48ad49eff61c3abfd399c672232434d89a4ef3ca763b9dbebb60dbb32a8b', + }); + + testExpected({ + nist: false, + length: 256, + message: '00', + expected: + 'bc36789e7a1e281436464229828f817d6612f7b477d66591ff96a9e064bcc98a', + }); + + // Negative tests + try { + testExpected({ + nist: false, + length: 256, + message: 'a2c', + expected: + '07f02d241eeba9c909a1be75e08d9e8ac3e61d9e24fa452a6785083e1527c467', + }); + assert(false, 'Expected to throw'); + } catch (e) {} + + try { + testExpected({ + nist: true, + length: 256, + message: '0', + expected: + 'f39f4526920bb4c096e5722d64161ea0eb6dbd0b4ff0d812f31d56fb96142084', + }); + assert(false, 'Expected to throw'); + } catch (e) {} + + try { + testExpected({ + nist: true, + length: 256, + message: '30', + expected: + 'f9e2eaaa42d9fe9e558a9b8ef1bf366f190aacaa83bad2641ee106e9041096e4', + }); + assert(false, 'Expected to throw'); + } catch (e) {} + + try { + testExpected({ + nist: true, + length: 256, + message: + '4920616d20746865206f776e6572206f6620746865204e465420776974682069642058206f6e2074686520457468657265756d20636861696e', + expected: + '63858e0487687c3eeb30796a3e9307680e1b81b860b01c88ff74545c2c314e36', + }); + assert(false, 'Expected to throw'); + } catch (e) {} +} + +function testExpected({ + message, + expected, + nist = false, + length = 256, +}: { + message: string; + expected: string; + nist: boolean; + length: number; +}) { + Provable.runAndCheck(() => { + assert(message.length % 2 === 0); + + let fields = Hash.fromHex(message); + let expectedHash = Hash.fromHex(expected); + + Provable.asProver(() => { + if (nist) { + let hashed; + switch (length) { + case 224: + hashed = Hash.SHA224.hash(fields); + break; + case 256: + hashed = Hash.SHA256.hash(fields); + break; + case 384: + hashed = Hash.SHA384.hash(fields); + break; + case 512: + hashed = Hash.SHA512.hash(fields); + break; + default: + assert(false); + } + equals(hashed!, expectedHash); + } else { + let hashed = Hash.Keccak256.hash(fields); + equals(hashed, expectedHash); + } + }); + }); +} From b1c504267a4af208193d4ad2974ed1481146ae7a Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 5 Jul 2023 11:10:07 -0700 Subject: [PATCH 53/90] Revert "refactor(hash.ts): move fromHex and toHex from UInt8 to Hash namespace" This reverts commit 3e24ff7d98a4650cb2982dd382d07b149a817b05. --- src/examples/keccak.ts | 4 ++-- src/lib/hash.ts | 14 -------------- src/lib/int.ts | 14 ++++++++++++++ src/lib/keccak.unit-test.ts | 11 ++++------- 4 files changed, 20 insertions(+), 23 deletions(-) diff --git a/src/examples/keccak.ts b/src/examples/keccak.ts index 7f3a26bc5..243b64996 100644 --- a/src/examples/keccak.ts +++ b/src/examples/keccak.ts @@ -9,8 +9,8 @@ function equals(a: UInt8[], b: UInt8[]): boolean { function checkDigestHexConversion(digest: UInt8[]) { console.log('Checking hex->digest, digest->hex matches'); Provable.asProver(() => { - const hex = Hash.toHex(digest); - const expected = Hash.fromHex(hex); + const hex = UInt8.toHex(digest); + const expected = UInt8.fromHex(hex); if (equals(digest, expected)) { console.log('✅ Digest matches'); } else { diff --git a/src/lib/hash.ts b/src/lib/hash.ts index c4a08be54..d1bf656cf 100644 --- a/src/lib/hash.ts +++ b/src/lib/hash.ts @@ -220,18 +220,4 @@ const Hash = { SHA512: buildSHA(512, true), Keccak256: buildSHA(256, false), - - fromHex(xs: string): UInt8[] { - return Snarky.sha - .fieldBytesFromHex(xs) - .map((x) => UInt8.from(Field(x))) - .slice(1); - }, - - toHex(xs: UInt8[]): string { - return xs - .map((x) => x.value) - .map((f) => Field.toBytes(f)[0].toString(16).padStart(2, '0')) - .join(''); - }, }; diff --git a/src/lib/int.ts b/src/lib/int.ts index e8cd7ae47..b479411d6 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -1465,6 +1465,20 @@ class UInt8 extends Struct({ return this.value.isConstant(); } + static fromHex(xs: string): UInt8[] { + return Snarky.sha + .fieldBytesFromHex(xs) + .map((x) => UInt8.from(Field(x))) + .slice(1); + } + + static toHex(xs: UInt8[]): string { + return xs + .map((x) => x.value) + .map((f) => Field.toBytes(f)[0].toString(16).padStart(2, '0')) + .join(''); + } + /** * Creates a {@link UInt8} with a value of 255. */ diff --git a/src/lib/keccak.unit-test.ts b/src/lib/keccak.unit-test.ts index e86361d3e..c934bcf15 100644 --- a/src/lib/keccak.unit-test.ts +++ b/src/lib/keccak.unit-test.ts @@ -65,14 +65,11 @@ function checkHashConversions(data: UInt8[]) { } function expectDigestToEqualHex(digest: UInt8[]) { - const hex = Hash.toHex(digest); - expect(equals(digest, Hash.fromHex(hex))).toBe(true); + const hex = UInt8.toHex(digest); + expect(equals(digest, UInt8.fromHex(hex))).toBe(true); } function equals(a: UInt8[], b: UInt8[]): boolean { - // Provable.log('DEBUG', 'a', a); - // Provable.log('DEBUG', 'b', b); - if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) a[i].assertEquals(b[i]); @@ -249,8 +246,8 @@ function testExpected({ Provable.runAndCheck(() => { assert(message.length % 2 === 0); - let fields = Hash.fromHex(message); - let expectedHash = Hash.fromHex(expected); + let fields = UInt8.fromHex(message); + let expectedHash = UInt8.fromHex(expected); Provable.asProver(() => { if (nist) { From c0a3fa518e7018433559326102a6e405ab438d71 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 5 Jul 2023 11:18:15 -0700 Subject: [PATCH 54/90] feat(bindings): update bindings --- src/bindings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bindings b/src/bindings index a92e78311..1b4781eda 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit a92e7831122164e67e0374b8aac893d738ff6626 +Subproject commit 1b4781eda968929c514f136de66c8b8e1d8fb5bf From 553ef3a85aa5659ee076819ed84693045ae48c0a Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 5 Jul 2023 11:40:37 -0700 Subject: [PATCH 55/90] feat(uint8): add scaffold for rangeCheck it works for proofs --- src/lib/int.ts | 39 +++++++++++++++++++++++++++++++++------ src/snarky.d.ts | 2 ++ 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/lib/int.ts b/src/lib/int.ts index b479411d6..db6ef9e90 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -980,7 +980,10 @@ class UInt8 extends Struct({ if (x instanceof UInt8) return x; super({ value: Field(x) }); - this.value.toBits(UInt8.NUM_BITS); // Make sure that the Field element that is exactly a byte + + // TODO: Enable when rangeCheck works in proofs + // UInt8.#rangeCheck(this.value); + this.check(); } /** @@ -1125,12 +1128,11 @@ class UInt8 extends Struct({ Field, () => new Field(x.toBigInt() / y_.toBigInt()) ); - // TODO: Need to range check `q` + UInt8.#rangeCheck(q); // TODO: Could be a bit more efficient let r = x.sub(q.mul(y_)).seal(); - - // TODO: Need to range check `r` + UInt8.#rangeCheck(r); let r_ = UInt8.from(r); let q_ = UInt8.from(q); @@ -1160,7 +1162,17 @@ class UInt8 extends Struct({ if (this.value.isConstant() && y.value.isConstant()) { return Bool(this.value.toBigInt() <= y.value.toBigInt()); } else { - // TODO: Need more efficient range checking + // TODO: Enable when rangeCheck works in proofs + // let xMinusY = this.value.sub(y.value).seal(); + // UInt8.#rangeCheck(xMinusY); + + // let yMinusX = xMinusY.neg(); + // UInt8.#rangeCheck(yMinusX); + + // x <= y if y - x fits in 64 bits + // return yMinusX; + + // TODO: Remove this when rangeCheck works in proofs return this.value.lessThanOrEqual(y.value); } } @@ -1227,7 +1239,11 @@ class UInt8 extends Struct({ } return; } - // TODO: Need more efficient range checking + // TODO: Enable when rangeCheck works in proofs + // let yMinusX = value.value.sub(this.value).seal(); + // UInt8.#rangeCheck(yMinusX); + + // TODO: Remove this when rangeCheck works in proofs return this.lessThanOrEqual(value).assertEquals(true, message); } @@ -1376,6 +1392,8 @@ class UInt8 extends Struct({ * @param value - the {@link UInt8} element to check. */ check() { + // TODO: Enable when rangeCheck works in proofs + // UInt8.#rangeCheck(this.value); this.value.toBits(UInt8.NUM_BITS); } @@ -1507,6 +1525,15 @@ class UInt8 extends Struct({ x.toBits(UInt8.NUM_BITS); return x; } + + // TODO: rangeCheck does not prove as of yet, waiting on https://github.com/MinaProtocol/mina/pull/12524 to merge. + static #rangeCheck(x: UInt8 | Field) { + if (isUInt8(x)) x = x.value; + if (x.isConstant()) this.checkConstant(x); + + // Throws an error if the value is not in the range [0, 2^UInt8.NUM_BITS - 1] + Snarky.sha.checkBits(x.value, UInt8.NUM_BITS); + } } function isUInt8(x: unknown): x is UInt8 { diff --git a/src/snarky.d.ts b/src/snarky.d.ts index bba88fb5a..421ee2d14 100644 --- a/src/snarky.d.ts +++ b/src/snarky.d.ts @@ -252,6 +252,8 @@ declare const Snarky: { ): MlArray; fieldBytesFromHex(hex: string): MlArray; + + checkBits(value: FieldVar, bits: number): void; }; poseidon: { From bec639ac6400ebe6b57d140ce30cdc20cae3ac80 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Wed, 5 Jul 2023 12:18:39 -0700 Subject: [PATCH 56/90] chore(uint8): add rangeCheck todo comments --- src/lib/int.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/lib/int.ts b/src/lib/int.ts index db6ef9e90..cf551d479 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -980,9 +980,6 @@ class UInt8 extends Struct({ if (x instanceof UInt8) return x; super({ value: Field(x) }); - - // TODO: Enable when rangeCheck works in proofs - // UInt8.#rangeCheck(this.value); this.check(); } @@ -1128,11 +1125,15 @@ class UInt8 extends Struct({ Field, () => new Field(x.toBigInt() / y_.toBigInt()) ); - UInt8.#rangeCheck(q); + + // TODO: Enable when rangeCheck works in proofs + // UInt8.#rangeCheck(q); // TODO: Could be a bit more efficient let r = x.sub(q.mul(y_)).seal(); - UInt8.#rangeCheck(r); + + // TODO: Enable when rangeCheck works in proofs + // UInt8.#rangeCheck(r); let r_ = UInt8.from(r); let q_ = UInt8.from(q); From 77ccc7c65061a84a5b184a823ac4ac659fab01fc Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Sat, 15 Jul 2023 10:37:00 -0700 Subject: [PATCH 57/90] fix(hash.ts): use MlArray.to() instead of array literal for Snarky.sha.create() --- src/lib/hash.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/hash.ts b/src/lib/hash.ts index 4f78af1f2..347f3f919 100644 --- a/src/lib/hash.ts +++ b/src/lib/hash.ts @@ -7,6 +7,7 @@ import { MlFieldArray } from './ml/fields.js'; import { UInt8 } from './int.js'; import { Poseidon as PoseidonBigint } from '../bindings/crypto/poseidon.js'; import { assert } from './errors.js'; +import { MlArray } from './ml/base.js'; // external API export { Poseidon, TokenSymbol, Hash }; @@ -212,7 +213,7 @@ function buildSHA(length: 224 | 256 | 384 | 512, nist: boolean) { return { hash(message: UInt8[]): UInt8[] { return Snarky.sha - .create([0, ...message.map((f) => f.toField().value)], nist, length) + .create(MlArray.to(message.map((f) => f.toField().value)), nist, length) .map((f) => UInt8.from(Field(f))) .slice(1); }, From e73d5bf2a056cd8f6fe26f541005b929dcf4a1c0 Mon Sep 17 00:00:00 2001 From: Martin Minkov Date: Sat, 15 Jul 2023 10:37:14 -0700 Subject: [PATCH 58/90] chore(bindings): update bindings --- src/bindings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bindings b/src/bindings index 276f6ef96..021febf93 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit 276f6ef96b91986f10519d313722e59be79ce36f +Subproject commit 021febf9316f873b39af6f9b8582d9f7a30f6c23 From 08448a8261797b98e3b0a67ac7d5a75c4b83f9b5 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 13 Dec 2023 09:05:15 +0100 Subject: [PATCH 59/90] missing from merge --- src/lib/keccak-old.unit-test.ts | 278 ++++++++++++++++++ .../vk-regression/plain-constraint-system.ts | 48 ++- 2 files changed, 324 insertions(+), 2 deletions(-) create mode 100644 src/lib/keccak-old.unit-test.ts diff --git a/src/lib/keccak-old.unit-test.ts b/src/lib/keccak-old.unit-test.ts new file mode 100644 index 000000000..c934bcf15 --- /dev/null +++ b/src/lib/keccak-old.unit-test.ts @@ -0,0 +1,278 @@ +import { test, Random } from './testing/property.js'; +import { UInt8 } from './int.js'; +import { Hash } from './hash.js'; +import { Provable } from './provable.js'; +import { expect } from 'expect'; +import assert from 'assert'; + +let RandomUInt8 = Random.map(Random.uint8, (x) => UInt8.from(x)); + +// Test constructor +test(Random.uint8, Random.uint8, (x, y, assert) => { + let z = new UInt8(x); + assert(z instanceof UInt8); + assert(z.toBigInt() === x); + assert(z.toString() === x.toString()); + assert(z.isConstant()); + + assert((z = new UInt8(x)) instanceof UInt8 && z.toBigInt() === x); + assert((z = new UInt8(z)) instanceof UInt8 && z.toBigInt() === x); + assert((z = new UInt8(z.value)) instanceof UInt8 && z.toBigInt() === x); + + z = new UInt8(y); + assert(z instanceof UInt8); + assert(z.toString() === y.toString()); + assert(z.toJSON() === y.toString()); +}); + +// handles all numbers up to 2^8 +test(Random.nat(255), (n, assert) => { + assert(UInt8.from(n).toString() === String(n)); +}); + +// throws on negative numbers +test.negative(Random.int(-10, -1), (x) => UInt8.from(x)); + +// throws on numbers >= 2^8 +test.negative(Random.uint8.invalid, (x) => UInt8.from(x)); + +runHashFunctionTests(); +console.log('OCaml tests pass! 🎉'); + +// test digest->hex and hex->digest conversions +checkHashInCircuit(); +console.log('hashing digest conversions matches! 🎉'); + +// check in-circuit +function checkHashInCircuit() { + Provable.runAndCheck(() => { + let data = Random.array(RandomUInt8, Random.nat(32)) + .create()() + .map((x) => Provable.witness(UInt8, () => UInt8.from(x))); + + checkHashConversions(data); + }); +} + +function checkHashConversions(data: UInt8[]) { + Provable.asProver(() => { + expectDigestToEqualHex(Hash.SHA224.hash(data)); + expectDigestToEqualHex(Hash.SHA256.hash(data)); + expectDigestToEqualHex(Hash.SHA384.hash(data)); + expectDigestToEqualHex(Hash.SHA512.hash(data)); + expectDigestToEqualHex(Hash.Keccak256.hash(data)); + }); +} + +function expectDigestToEqualHex(digest: UInt8[]) { + const hex = UInt8.toHex(digest); + expect(equals(digest, UInt8.fromHex(hex))).toBe(true); +} + +function equals(a: UInt8[], b: UInt8[]): boolean { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) a[i].assertEquals(b[i]); + + return true; +} + +/** + * Based off the following unit tests from the OCaml implementation: + * https://github.com/MinaProtocol/mina/blob/69d6ea4a3b7ca1690cf8f41d4598cb7484359e1d/src/lib/crypto/kimchi_backend/gadgets/keccak.ml#L646 + */ +function runHashFunctionTests() { + // Positive Tests + testExpected({ + nist: false, + length: 256, + message: '30', + expected: + '044852b2a670ade5407e78fb2863c51de9fcb96542a07186fe3aeda6bb8a116d', + }); + + testExpected({ + nist: true, + length: 512, + message: '30', + expected: + '2d44da53f305ab94b6365837b9803627ab098c41a6013694f9b468bccb9c13e95b3900365eb58924de7158a54467e984efcfdabdbcc9af9a940d49c51455b04c', + }); + + testExpected({ + nist: false, + length: 256, + message: + '4920616d20746865206f776e6572206f6620746865204e465420776974682069642058206f6e2074686520457468657265756d20636861696e', + expected: + '63858e0487687c3eeb30796a3e9307680e1b81b860b01c88ff74545c2c314e36', + }); + + testExpected({ + nist: false, + length: 256, + message: + '044852b2a670ade5407e78fb2863c51de9fcb96542a07186fe3aeda6bb8a116df9e2eaaa42d9fe9e558a9b8ef1bf366f190aacaa83bad2641ee106e9041096e42d44da53f305ab94b6365837b9803627ab098c41a6013694f9b468bccb9c13e95b3900365eb58924de7158a54467e984efcfdabdbcc9af9a940d49c51455b04c63858e0487687c3eeb30796a3e9307680e1b81b860b01c88ff74545c2c314e36', + expected: + '560deb1d387f72dba729f0bd0231ad45998dda4b53951645322cf95c7b6261d9', + }); + + testExpected({ + nist: true, + length: 256, + message: + '044852b2a670ade5407e78fb2863c51de9fcb96542a07186fe3aeda6bb8a116df9e2eaaa42d9fe9e558a9b8ef1bf366f190aacaa83bad2641ee106e9041096e42d44da53f305ab94b6365837b9803627ab098c41a6013694f9b468bccb9c13e95b3900365eb58924de7158a54467e984efcfdabdbcc9af9a940d49c51455b04c63858e0487687c3eeb30796a3e9307680e1b81b860b01c88ff74545c2c314e36', + expected: + '1784354c4bbfa5f54e5db23041089e65a807a7b970e3cfdba95e2fbe63b1c0e4', + }); + + testExpected({ + nist: false, + length: 256, + message: + '391ccf9b5de23bb86ec6b2b142adb6e9ba6bee8519e7502fb8be8959fbd2672934cc3e13b7b45bf2b8a5cb48881790a7438b4a326a0c762e31280711e6b64fcc2e3e4e631e501d398861172ea98603618b8f23b91d0208b0b992dfe7fdb298b6465adafbd45e4f88ee9dc94e06bc4232be91587f78572c169d4de4d8b95b714ea62f1fbf3c67a4', + expected: + '7d5655391ede9ca2945f32ad9696f464be8004389151ce444c89f688278f2e1d', + }); + + testExpected({ + nist: false, + length: 256, + message: + 'ff391ccf9b5de23bb86ec6b2b142adb6e9ba6bee8519e7502fb8be8959fbd2672934cc3e13b7b45bf2b8a5cb48881790a7438b4a326a0c762e31280711e6b64fcc2e3e4e631e501d398861172ea98603618b8f23b91d0208b0b992dfe7fdb298b6465adafbd45e4f88ee9dc94e06bc4232be91587f78572c169d4de4d8b95b714ea62f1fbf3c67a4', + expected: + '37694fd4ba137be747eb25a85b259af5563e0a7a3010d42bd15963ac631b9d3f', + }); + + testExpected({ + nist: false, + length: 256, + message: + '80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001', + expected: + 'bbf1f49a2cc5678aa62196d0c3108d89425b81780e1e90bcec03b4fb5f834714', + }); + + testExpected({ + nist: false, + length: 256, + message: + '80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001', + expected: + 'bbf1f49a2cc5678aa62196d0c3108d89425b81780e1e90bcec03b4fb5f834714', + }); + + testExpected({ + nist: false, + length: 256, + message: 'a2c0', + expected: + '9856642c690c036527b8274db1b6f58c0429a88d9f3b9298597645991f4f58f0', + }); + + testExpected({ + nist: false, + length: 256, + message: '0a2c', + expected: + '295b48ad49eff61c3abfd399c672232434d89a4ef3ca763b9dbebb60dbb32a8b', + }); + + testExpected({ + nist: false, + length: 256, + message: '00', + expected: + 'bc36789e7a1e281436464229828f817d6612f7b477d66591ff96a9e064bcc98a', + }); + + // Negative tests + try { + testExpected({ + nist: false, + length: 256, + message: 'a2c', + expected: + '07f02d241eeba9c909a1be75e08d9e8ac3e61d9e24fa452a6785083e1527c467', + }); + assert(false, 'Expected to throw'); + } catch (e) {} + + try { + testExpected({ + nist: true, + length: 256, + message: '0', + expected: + 'f39f4526920bb4c096e5722d64161ea0eb6dbd0b4ff0d812f31d56fb96142084', + }); + assert(false, 'Expected to throw'); + } catch (e) {} + + try { + testExpected({ + nist: true, + length: 256, + message: '30', + expected: + 'f9e2eaaa42d9fe9e558a9b8ef1bf366f190aacaa83bad2641ee106e9041096e4', + }); + assert(false, 'Expected to throw'); + } catch (e) {} + + try { + testExpected({ + nist: true, + length: 256, + message: + '4920616d20746865206f776e6572206f6620746865204e465420776974682069642058206f6e2074686520457468657265756d20636861696e', + expected: + '63858e0487687c3eeb30796a3e9307680e1b81b860b01c88ff74545c2c314e36', + }); + assert(false, 'Expected to throw'); + } catch (e) {} +} + +function testExpected({ + message, + expected, + nist = false, + length = 256, +}: { + message: string; + expected: string; + nist: boolean; + length: number; +}) { + Provable.runAndCheck(() => { + assert(message.length % 2 === 0); + + let fields = UInt8.fromHex(message); + let expectedHash = UInt8.fromHex(expected); + + Provable.asProver(() => { + if (nist) { + let hashed; + switch (length) { + case 224: + hashed = Hash.SHA224.hash(fields); + break; + case 256: + hashed = Hash.SHA256.hash(fields); + break; + case 384: + hashed = Hash.SHA384.hash(fields); + break; + case 512: + hashed = Hash.SHA512.hash(fields); + break; + default: + assert(false); + } + equals(hashed!, expectedHash); + } else { + let hashed = Hash.Keccak256.hash(fields); + equals(hashed, expectedHash); + } + }); + }); +} diff --git a/tests/vk-regression/plain-constraint-system.ts b/tests/vk-regression/plain-constraint-system.ts index b5d0c1f44..20bf05fa1 100644 --- a/tests/vk-regression/plain-constraint-system.ts +++ b/tests/vk-regression/plain-constraint-system.ts @@ -1,6 +1,6 @@ -import { Field, Group, Gadgets, Provable, Scalar } from 'o1js'; +import { Field, Group, Gadgets, Provable, Scalar, Hash, UInt8 } from 'o1js'; -export { GroupCS, BitwiseCS }; +export { GroupCS, BitwiseCS, HashCS }; const GroupCS = constraintSystem('Group Primitive', { add() { @@ -84,6 +84,50 @@ const BitwiseCS = constraintSystem('Bitwise Primitive', { }, }); +const HashCS = constraintSystem('Hashes', { + SHA224() { + let xs = Array.from({ length: 32 }, (_, i) => i).map((x) => + Provable.witness(UInt8, () => UInt8.from(x)) + ); + Hash.SHA224.hash(xs); + }, + + SHA256() { + let xs = Array.from({ length: 32 }, (_, i) => i).map((x) => + Provable.witness(UInt8, () => UInt8.from(x)) + ); + Hash.SHA256.hash(xs); + }, + + SHA384() { + let xs = Array.from({ length: 32 }, (_, i) => i).map((x) => + Provable.witness(UInt8, () => UInt8.from(x)) + ); + Hash.SHA384.hash(xs); + }, + + SHA512() { + let xs = Array.from({ length: 32 }, (_, i) => i).map((x) => + Provable.witness(UInt8, () => UInt8.from(x)) + ); + Hash.SHA512.hash(xs); + }, + + Keccak256() { + let xs = Array.from({ length: 32 }, (_, i) => i).map((x) => + Provable.witness(UInt8, () => UInt8.from(x)) + ); + Hash.Keccak256.hash(xs); + }, + + Poseidon() { + let xs = Array.from({ length: 32 }, (_, i) => i).map((x) => + Provable.witness(Field, () => Field(x)) + ); + Hash.Poseidon.hash(xs); + }, +}); + // mock ZkProgram API for testing function constraintSystem( From 763d38f2886cefd156173141d7c17fa40d9c1a48 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 13 Dec 2023 11:10:06 +0100 Subject: [PATCH 60/90] remove some unnecessary changes --- src/examples/zkapps/hashing/hash.ts | 36 +++-------------------------- src/index.ts | 2 +- src/provable/field-bigint.ts | 4 +--- src/snarky.d.ts | 13 +---------- 4 files changed, 6 insertions(+), 49 deletions(-) diff --git a/src/examples/zkapps/hashing/hash.ts b/src/examples/zkapps/hashing/hash.ts index 308cf48bc..e593afae3 100644 --- a/src/examples/zkapps/hashing/hash.ts +++ b/src/examples/zkapps/hashing/hash.ts @@ -8,44 +8,14 @@ import { method, Permissions, Struct, -} from 'snarkyjs'; + Provable, +} from 'o1js'; let initialCommitment: Field = Field(0); // 32 UInts export class HashInput extends Struct({ - data: [ - UInt8, - UInt8, - UInt8, - UInt8, - UInt8, - UInt8, - UInt8, - UInt8, - UInt8, - UInt8, - UInt8, - UInt8, - UInt8, - UInt8, - UInt8, - UInt8, - UInt8, - UInt8, - UInt8, - UInt8, - UInt8, - UInt8, - UInt8, - UInt8, - UInt8, - UInt8, - UInt8, - UInt8, - UInt8, - UInt8, - ], + data: Provable.Array(UInt8, 32), }) {} export class HashStorage extends SmartContract { diff --git a/src/index.ts b/src/index.ts index 9a5868e59..bbd5bbc55 100644 --- a/src/index.ts +++ b/src/index.ts @@ -31,7 +31,7 @@ export { } from './lib/circuit_value.js'; export { Provable } from './lib/provable.js'; export { Circuit, Keypair, public_, circuitMain } from './lib/circuit.js'; -export { UInt8, UInt32, UInt64, Int64, Sign } from './lib/int.js'; +export { UInt32, UInt64, Int64, Sign, UInt8 } from './lib/int.js'; export { Gadgets } from './lib/gadgets/gadgets.js'; export { Types } from './bindings/mina-transaction/types.js'; diff --git a/src/provable/field-bigint.ts b/src/provable/field-bigint.ts index 06ff9fa32..c23e0e0bc 100644 --- a/src/provable/field-bigint.ts +++ b/src/provable/field-bigint.ts @@ -6,12 +6,11 @@ import { ProvableBigint, } from '../bindings/lib/provable-bigint.js'; -export { Field, Bool, UInt8, UInt32, UInt64, Sign }; +export { Field, Bool, UInt32, UInt64, Sign }; export { pseudoClass, sizeInBits, checkRange, checkField }; type Field = bigint; type Bool = 0n | 1n; -type UInt8 = bigint; type UInt32 = bigint; type UInt64 = bigint; @@ -98,7 +97,6 @@ function Unsigned(bits: number) { } ); } -const UInt8 = Unsigned(8); const UInt32 = Unsigned(32); const UInt64 = Unsigned(64); diff --git a/src/snarky.d.ts b/src/snarky.d.ts index 2a496454a..871fbeeca 100644 --- a/src/snarky.d.ts +++ b/src/snarky.d.ts @@ -525,18 +525,7 @@ declare const Snarky: { }; }; - sha: { - create( - message: MlArray, - nist: boolean, - length: number - ): MlArray; - - fieldBytesFromHex(hex: string): MlArray; - - checkBits(value: FieldVar, bits: number): void; - }; - + // TODO: implement in TS poseidon: { update( state: MlArray, From 6fd370dedc7d0bdbc75ec07deb900347daeab3bb Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 13 Dec 2023 11:48:36 +0100 Subject: [PATCH 61/90] work on uint8 and make it compile --- src/lib/int.ts | 189 ++++++++++++++--------------------------- src/lib/keccak.ts | 8 +- src/lib/util/arrays.ts | 14 +++ 3 files changed, 81 insertions(+), 130 deletions(-) create mode 100644 src/lib/util/arrays.ts diff --git a/src/lib/int.ts b/src/lib/int.ts index 6b9d46b7d..d4947493e 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -3,7 +3,9 @@ import { AnyConstructor, CircuitValue, Struct, prop } from './circuit_value.js'; import { Types } from '../bindings/mina-transaction/types.js'; import { HashInput } from './hash.js'; import { Provable } from './provable.js'; -import { Snarky } from '../snarky.js'; +import { Gadgets } from './gadgets/gadgets.js'; +import { withMessage } from './field.js'; +import { chunkString } from './util/arrays.js'; // external API export { UInt8, UInt32, UInt64, Int64, Sign }; @@ -976,28 +978,12 @@ class UInt8 extends Struct({ * Coerce anything "field-like" (bigint, number, string, and {@link Field}) to a {@link UInt8}. * The max value of a {@link UInt8} is `2^8 - 1 = 255`. * - * * **Warning**: Cannot overflow past 255, an error is thrown if the result is greater than 255. */ constructor(x: number | bigint | string | Field | UInt8) { - if (x instanceof UInt8) return x; - + if (x instanceof UInt8) x = x.value; super({ value: Field(x) }); - this.check(); - } - - /** - * Static method to create a {@link UInt8} with value `0`. - */ - static get zero() { - return UInt8.from(0); - } - - /** - * Static method to create a {@link UInt8} with value `1`. - */ - static get one() { - return UInt8.from(1); + UInt8.checkConstant(this.value); } /** @@ -1158,27 +1144,15 @@ class UInt8 extends Struct({ * The method will throw if one of the inputs exceeds 8 bits. * * - * @param value - the {@link UInt8} value to compare with this {@link UInt8}. + * @param y - the {@link UInt8} value to compare with this {@link UInt8}. * * @return A {@link Bool} representing if this {@link UInt8} is less than or equal another {@link UInt8} value. */ - lessThanOrEqual(y: UInt8) { + lessThanOrEqual(y: UInt8): Bool { if (this.value.isConstant() && y.value.isConstant()) { return Bool(this.value.toBigInt() <= y.value.toBigInt()); - } else { - // TODO: Enable when rangeCheck works in proofs - // let xMinusY = this.value.sub(y.value).seal(); - // UInt8.#rangeCheck(xMinusY); - - // let yMinusX = xMinusY.neg(); - // UInt8.#rangeCheck(yMinusX); - - // x <= y if y - x fits in 64 bits - // return yMinusX; - - // TODO: Remove this when rangeCheck works in proofs - return this.value.lessThanOrEqual(y.value); } + throw Error('Not implemented'); } /** @@ -1193,14 +1167,15 @@ class UInt8 extends Struct({ * **Warning**: Comparison methods only support UInt8 elements of size <= 8 bits in provable code. * The method will throw if one of the inputs exceeds 8 bits. * - * @param value - the {@link UInt8} value to compare with this {@link UInt8}. + * @param y - the {@link UInt8} value to compare with this {@link UInt8}. * * @return A {@link Bool} representing if this {@link UInt8} is less than another {@link UInt8} value. */ - lessThan(value: UInt8) { - return this.lessThanOrEqual(value).and( - this.value.equals(value.value).not() - ); + lessThan(y: UInt8): Bool { + if (this.value.isConstant() && y.value.isConstant()) { + return Bool(this.value.toBigInt() < y.value.toBigInt()); + } + throw Error('Not implemented'); } /** @@ -1213,11 +1188,22 @@ class UInt8 extends Struct({ * **Warning**: Comparison methods only support UInt8 elements of size <= 8 bits in provable code. * The method will throw if one of the inputs exceeds 8 bits. * - * @param value - the {@link UInt8} value to compare & assert with this {@link UInt8}. + * @param y - the {@link UInt8} value to compare & assert with this {@link UInt8}. * @param message? - a string error message to print if the assertion fails, optional. */ - assertLessThan(value: UInt8, message?: string) { - this.lessThan(value).assertEquals(true, message); + assertLessThan(y: UInt8, message?: string) { + if (this.value.isConstant() && y.value.isConstant()) { + let x0 = this.value.toBigInt(); + let y0 = y.value.toBigInt(); + if (x0 >= y0) { + if (message !== undefined) throw Error(message); + throw Error(`UInt8.assertLessThan: expected ${x0} < ${y0}`); + } + return; + } + // x < y <=> x + 1 <= y + let xPlus1 = new UInt8(this.value.add(1)); + xPlus1.assertLessThanOrEqual(y, message); } /** @@ -1230,25 +1216,26 @@ class UInt8 extends Struct({ * **Warning**: Comparison methods only support UInt8 elements of size <= 8 bits in provable code. * The method will throw if one of the inputs exceeds 8 bits. * - * @param value - the {@link UInt8} value to compare & assert with this {@link UInt8}. + * @param y - the {@link UInt8} value to compare & assert with this {@link UInt8}. * @param message? - a string error message to print if the assertion fails, optional. */ - assertLessThanOrEqual(value: UInt8, message?: string) { - if (this.value.isConstant() && value.value.isConstant()) { + assertLessThanOrEqual(y: UInt8, message?: string) { + if (this.value.isConstant() && y.value.isConstant()) { let x0 = this.value.toBigInt(); - let y0 = value.value.toBigInt(); + let y0 = y.value.toBigInt(); if (x0 > y0) { if (message !== undefined) throw Error(message); throw Error(`UInt8.assertLessThanOrEqual: expected ${x0} <= ${y0}`); } return; } - // TODO: Enable when rangeCheck works in proofs - // let yMinusX = value.value.sub(this.value).seal(); - // UInt8.#rangeCheck(yMinusX); - - // TODO: Remove this when rangeCheck works in proofs - return this.lessThanOrEqual(value).assertEquals(true, message); + try { + // x <= y <=> y - x >= 0 <=> y - x in [0, 2^8) + let yMinusX = y.value.sub(this.value).seal(); + Gadgets.rangeCheck8(yMinusX); + } catch (err) { + throw withMessage(err, message); + } } /** @@ -1263,12 +1250,12 @@ class UInt8 extends Struct({ * **Warning**: Comparison methods currently only support Field elements of size <= 8 bits in provable code. * The method will throw if one of the inputs exceeds 8 bits. * - * @param value - the {@link UInt8} value to compare with this {@link UInt8}. + * @param y - the {@link UInt8} value to compare with this {@link UInt8}. * * @return A {@link Bool} representing if this {@link UInt8} is greater than another {@link UInt8} value. */ - greaterThan(value: UInt8) { - return value.lessThan(this); + greaterThan(y: UInt8) { + return y.lessThan(this); } /** @@ -1283,12 +1270,12 @@ class UInt8 extends Struct({ * **Warning**: Comparison methods only support UInt8 elements of size <= 8 bits in provable code. * The method will throw if one of the inputs exceeds 8 bits. * - * @param value - the {@link UInt8} value to compare with this {@link Field}. + * @param y - the {@link UInt8} value to compare with this {@link Field}. * * @return A {@link Bool} representing if this {@link UInt8} is greater than or equal another {@link UInt8} value. */ - greaterThanOrEqual(value: UInt8) { - return this.lessThan(value).not(); + greaterThanOrEqual(y: UInt8) { + return this.lessThan(y).not(); } /** @@ -1301,11 +1288,11 @@ class UInt8 extends Struct({ * **Warning**: Comparison methods only support UInt8 elements of size <= 8 bits in provable code. * The method will throw if one of the inputs exceeds 8 bits. * - * @param value - the {@link UInt8} value to compare & assert with this {@link UInt8}. + * @param y - the {@link UInt8} value to compare & assert with this {@link UInt8}. * @param message? - a string error message to print if the assertion fails, optional. */ - assertGreaterThan(value: UInt8, message?: string) { - value.assertLessThan(this, message); + assertGreaterThan(y: UInt8, message?: string) { + y.assertLessThan(this, message); } /** @@ -1318,11 +1305,11 @@ class UInt8 extends Struct({ * **Warning**: Comparison methods only support UInt8 elements of size <= 8 bits in provable code. * The method will throw if one of the inputs exceeds 8 bits. * - * @param value - the {@link UInt8} value to compare & assert with this {@link UInt8}. + * @param y - the {@link UInt8} value to compare & assert with this {@link UInt8}. * @param message? - a string error message to print if the assertion fails, optional. */ - assertGreaterThanOrEqual(value: UInt8, message?: string) { - value.assertLessThanOrEqual(this, message); + assertGreaterThanOrEqual(y: UInt8, message?: string) { + y.assertLessThanOrEqual(this, message); } /** @@ -1330,11 +1317,11 @@ class UInt8 extends Struct({ * * **Important**: If an assertion fails, the code throws an error. * - * @param value - the {@link UInt8} value to compare & assert with this {@link UInt8}. + * @param y - the {@link UInt8} value to compare & assert with this {@link UInt8}. * @param message? - a string error message to print if the assertion fails, optional. */ - assertEquals(value: number | bigint | UInt8, message?: string) { - let y_ = new UInt8(value); + assertEquals(y: number | bigint | UInt8, message?: string) { + let y_ = new UInt8(y); this.toField().assertEquals(y_.toField(), message); } @@ -1395,23 +1382,8 @@ class UInt8 extends Struct({ * * @param value - the {@link UInt8} element to check. */ - check() { - // TODO: Enable when rangeCheck works in proofs - // UInt8.#rangeCheck(this.value); - this.value.toBits(UInt8.NUM_BITS); - } - - /** - * Implementation of {@link Provable.fromFields} for the {@link UInt8} type. - * - * **Warning**: This function is designed for internal use. It is not intended to be used by a zkApp developer. - * - * @param fields - an array of {@link UInt8} serialized from {@link Field} elements. - * - * @return An array of {@link UInt8} elements of the given array. - */ - fromFields(xs: Field[]): UInt8[] { - return xs.map((x) => new UInt8(x)); + static check(x: { value: Field }) { + Gadgets.rangeCheck8(x.value); } /** @@ -1443,9 +1415,7 @@ class UInt8 extends Struct({ * @return A {@link UInt32} equivalent to the {@link UInt8}. */ toUInt32(): UInt32 { - let uint32 = new UInt32(this.value); - UInt32.check(uint32); - return uint32; + return new UInt32(this.value); } /** @@ -1460,9 +1430,7 @@ class UInt8 extends Struct({ * @return A {@link UInt64} equivalent to the {@link UInt8}. * */ toUInt64(): UInt64 { - let uint64 = new UInt64(this.value); - UInt64.check(uint64); - return uint64; + return new UInt64(this.value); } /** @@ -1487,59 +1455,34 @@ class UInt8 extends Struct({ return this.value.isConstant(); } + // TODO: these might be better on a separate `Bytes` class static fromHex(xs: string): UInt8[] { - return Snarky.sha - .fieldBytesFromHex(xs) - .map((x) => UInt8.from(Field(x))) - .slice(1); + let bytes = chunkString(xs, 2).map((s) => parseInt(s, 16)); + return bytes.map(UInt8.from); } - static toHex(xs: UInt8[]): string { - return xs - .map((x) => x.value) - .map((f) => Field.toBytes(f)[0].toString(16).padStart(2, '0')) - .join(''); + return xs.map((x) => x.toBigInt().toString(16).padStart(2, '0')).join(''); } /** * Creates a {@link UInt8} with a value of 255. */ static MAXINT() { - return new UInt8(Field((1n << BigInt(this.NUM_BITS)) - 1n)); + return new UInt8(Field((1n << BigInt(UInt8.NUM_BITS)) - 1n)); } /** * Creates a new {@link UInt8}. */ - static from( - x: UInt8 | UInt64 | UInt32 | Field | number | string | bigint | number[] - ) { + static from(x: UInt8 | UInt64 | UInt32 | Field | number | bigint) { if (x instanceof UInt64 || x instanceof UInt32 || x instanceof UInt8) x = x.value; - - if (Array.isArray(x)) { - return new this(Field.fromBytes(x)); - } - - return new this(this.checkConstant(Field(x))); + return new UInt8(UInt8.checkConstant(Field(x))); } private static checkConstant(x: Field) { if (!x.isConstant()) return x; - x.toBits(UInt8.NUM_BITS); + Gadgets.rangeCheck8(x); return x; } - - // TODO: rangeCheck does not prove as of yet, waiting on https://github.com/MinaProtocol/mina/pull/12524 to merge. - static #rangeCheck(x: UInt8 | Field) { - if (isUInt8(x)) x = x.value; - if (x.isConstant()) this.checkConstant(x); - - // Throws an error if the value is not in the range [0, 2^UInt8.NUM_BITS - 1] - Snarky.sha.checkBits(x.value, UInt8.NUM_BITS); - } -} - -function isUInt8(x: unknown): x is UInt8 { - return x instanceof UInt8; } diff --git a/src/lib/keccak.ts b/src/lib/keccak.ts index 9d99e9fd4..a1e4bb872 100644 --- a/src/lib/keccak.ts +++ b/src/lib/keccak.ts @@ -3,6 +3,7 @@ import { Gadgets } from './gadgets/gadgets.js'; import { assert } from './errors.js'; import { rangeCheck8 } from './gadgets/range-check.js'; import { Provable } from './provable.js'; +import { chunk } from './util/arrays.js'; export { Keccak }; @@ -463,13 +464,6 @@ function wordsToBytes(words: Field[]): Field[] { return words.flatMap(wordToBytes); } -function chunk(array: T[], size: number): T[][] { - assert(array.length % size === 0, 'invalid input length'); - return Array.from({ length: array.length / size }, (_, i) => - array.slice(size * i, size * (i + 1)) - ); -} - // xor which avoids doing anything on 0 inputs // (but doesn't range-check the other input in that case) function xor(x: Field, y: Field): Field { diff --git a/src/lib/util/arrays.ts b/src/lib/util/arrays.ts new file mode 100644 index 000000000..2a1a913ee --- /dev/null +++ b/src/lib/util/arrays.ts @@ -0,0 +1,14 @@ +import { assert } from '../gadgets/common.js'; + +export { chunk, chunkString }; + +function chunk(array: T[], size: number): T[][] { + assert(array.length % size === 0, 'invalid input length'); + return Array.from({ length: array.length / size }, (_, i) => + array.slice(size * i, size * (i + 1)) + ); +} + +function chunkString(str: string, size: number): string[] { + return chunk([...str], size).map((c) => c.join('')); +} From 900a3449bd9379a693de073f72dcfe8f85c81d4a Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 13 Dec 2023 12:02:25 +0100 Subject: [PATCH 62/90] export Hash, rename old Hash to HashHelpers --- src/bindings | 2 +- src/index.ts | 2 +- src/lib/hash.ts | 28 +++------------------------- src/provable/poseidon-bigint.ts | 6 +++--- 4 files changed, 8 insertions(+), 30 deletions(-) diff --git a/src/bindings b/src/bindings index c09afcd2f..7946599c5 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit c09afcd2fc902abb3db7c029740d401414f76f8b +Subproject commit 7946599c5f1636576519601dbd2c20aecc90a502 diff --git a/src/index.ts b/src/index.ts index bbd5bbc55..4b896dd90 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,7 +9,7 @@ export { } from './lib/foreign-field.js'; export { createForeignCurve, ForeignCurve } from './lib/foreign-curve.js'; export { createEcdsa, EcdsaSignature } from './lib/foreign-ecdsa.js'; -export { Poseidon, TokenSymbol } from './lib/hash.js'; +export { Poseidon, TokenSymbol, Hash } from './lib/hash.js'; export { Keccak } from './lib/keccak.js'; export * from './lib/signature.js'; diff --git a/src/lib/hash.ts b/src/lib/hash.ts index bf14707f7..d986d7ea4 100644 --- a/src/lib/hash.ts +++ b/src/lib/hash.ts @@ -4,10 +4,9 @@ import { Field } from './core.js'; import { createHashHelpers } from './hash-generic.js'; import { Provable } from './provable.js'; import { MlFieldArray } from './ml/fields.js'; -import { UInt8 } from './int.js'; import { Poseidon as PoseidonBigint } from '../bindings/crypto/poseidon.js'; import { assert } from './errors.js'; -import { MlArray } from './ml/base.js'; +import { Keccak } from './keccak.js'; // external API export { Poseidon, TokenSymbol, Hash }; @@ -208,29 +207,8 @@ function toBigints(fields: Field[]) { return fields.map((x) => x.toBigInt()); } -function buildSHA(length: 224 | 256 | 384 | 512, nist: boolean) { - return { - hash(message: UInt8[]): UInt8[] { - return Snarky.sha - .create(MlArray.to(message.map((f) => f.toField().value)), nist, length) - .map((f) => UInt8.from(Field(f))) - .slice(1); - }, - }; -} - const Hash = { hash: Poseidon.hash, - - Poseidon: Poseidon, - - SHA224: buildSHA(224, true), - - SHA256: buildSHA(256, true), - - SHA384: buildSHA(384, true), - - SHA512: buildSHA(512, true), - - Keccak256: buildSHA(256, false), + Poseidon, + Keccak, }; diff --git a/src/provable/poseidon-bigint.ts b/src/provable/poseidon-bigint.ts index 2ef684d58..906ab2a2d 100644 --- a/src/provable/poseidon-bigint.ts +++ b/src/provable/poseidon-bigint.ts @@ -7,7 +7,7 @@ import { createHashHelpers } from '../lib/hash-generic.js'; export { Poseidon, - Hash, + HashHelpers, HashInput, prefixes, packToFields, @@ -20,8 +20,8 @@ export { type HashInput = GenericHashInput; const HashInput = createHashInput(); -const Hash = createHashHelpers(Field, Poseidon); -let { hashWithPrefix } = Hash; +const HashHelpers = createHashHelpers(Field, Poseidon); +let { hashWithPrefix } = HashHelpers; const HashLegacy = createHashHelpers(Field, PoseidonLegacy); From 3bbd4544dfa731423bb9da6001e11e9d166805fa Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 13 Dec 2023 12:18:57 +0100 Subject: [PATCH 63/90] add rangeCheck16 --- src/lib/gadgets/gadgets.ts | 10 ++++++++++ src/lib/gadgets/range-check.ts | 20 +++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/lib/gadgets/gadgets.ts b/src/lib/gadgets/gadgets.ts index 038646d03..27cae55bf 100644 --- a/src/lib/gadgets/gadgets.ts +++ b/src/lib/gadgets/gadgets.ts @@ -4,6 +4,7 @@ import { compactMultiRangeCheck, multiRangeCheck, + rangeCheck16, rangeCheck64, rangeCheck8, } from './range-check.js'; @@ -41,6 +42,15 @@ const Gadgets = { return rangeCheck64(x); }, + /** + * Asserts that the input value is in the range [0, 2^16). + * + * See {@link Gadgets.rangeCheck64} for analogous details and usage examples. + */ + rangeCheck16(x: Field) { + return rangeCheck16(x); + }, + /** * Asserts that the input value is in the range [0, 2^8). * diff --git a/src/lib/gadgets/range-check.ts b/src/lib/gadgets/range-check.ts index 147ef1a6a..d53d8f5e5 100644 --- a/src/lib/gadgets/range-check.ts +++ b/src/lib/gadgets/range-check.ts @@ -2,7 +2,13 @@ import { Field } from '../field.js'; import { Gates } from '../gates.js'; import { assert, bitSlice, exists, toVar, toVars } from './common.js'; -export { rangeCheck64, rangeCheck8, multiRangeCheck, compactMultiRangeCheck }; +export { + rangeCheck64, + rangeCheck8, + rangeCheck16, + multiRangeCheck, + compactMultiRangeCheck, +}; export { l, l2, l3, lMask, l2Mask }; /** @@ -208,6 +214,18 @@ function rangeCheck1Helper(inputs: { ); } +function rangeCheck16(x: Field) { + if (x.isConstant()) { + assert( + x.toBigInt() < 1n << 16n, + `rangeCheck16: expected field to fit in 8 bits, got ${x}` + ); + return; + } + // check that x fits in 16 bits + x.rangeCheckHelper(16).assertEquals(x); +} + function rangeCheck8(x: Field) { if (x.isConstant()) { assert( From 0e0480727a0ee0d5c824d49465c9f1348648d6d5 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 13 Dec 2023 12:19:14 +0100 Subject: [PATCH 64/90] fix divMod --- src/lib/int.ts | 37 ++++++++++++++----------------------- 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/src/lib/int.ts b/src/lib/int.ts index d4947493e..c5787c314 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -1090,45 +1090,36 @@ class UInt8 extends Struct({ /** * Get the quotient and remainder of a {@link UInt8} value divided by another {@link UInt8} element. * - * @param value - a {@link UInt8} to get the quotient and remainder of another {@link UInt8}. + * @param y - a {@link UInt8} to get the quotient and remainder of another {@link UInt8}. * * @return The quotient and remainder of the two values. */ - divMod(value: UInt8 | number) { + divMod(y: UInt8 | bigint | number) { let x = this.value; - let y_ = UInt8.from(value).value; + let y_ = UInt8.from(y).value.seal(); if (this.value.isConstant() && y_.isConstant()) { let xn = x.toBigInt(); let yn = y_.toBigInt(); let q = xn / yn; let r = xn - q * yn; - return { - quotient: UInt8.from(Field(q)), - rest: UInt8.from(Field(r)), - }; + return { quotient: UInt8.from(q), rest: UInt8.from(r) }; } - y_ = y_.seal(); - let q = Provable.witness( - Field, - () => new Field(x.toBigInt() / y_.toBigInt()) - ); - - // TODO: Enable when rangeCheck works in proofs - // UInt8.#rangeCheck(q); - - // TODO: Could be a bit more efficient + // prove that x === q * y + r, where 0 <= r < y + let q = Provable.witness(Field, () => Field(x.toBigInt() / y_.toBigInt())); let r = x.sub(q.mul(y_)).seal(); - // TODO: Enable when rangeCheck works in proofs - // UInt8.#rangeCheck(r); + // q, r being 16 bits is enough for them to be 8 bits, + // thanks to the === x check and the r < y check below + Gadgets.rangeCheck16(q); + Gadgets.rangeCheck16(r); - let r_ = UInt8.from(r); - let q_ = UInt8.from(q); + let rest = UInt8.from(r); + let quotient = UInt8.from(q); - r_.assertLessThan(UInt8.from(y_)); - return { quotient: q_, rest: r_ }; + rest.assertLessThan(UInt8.from(y_)); + return { quotient, rest }; } /** From 5da4f727397ee15841ed2a97d1335f06af9321af Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 13 Dec 2023 13:02:36 +0100 Subject: [PATCH 65/90] fix and polish uint8 --- src/lib/int.ts | 359 +++++++++++++++---------------------------------- 1 file changed, 108 insertions(+), 251 deletions(-) diff --git a/src/lib/int.ts b/src/lib/int.ts index c5787c314..a365b0791 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -4,7 +4,7 @@ import { Types } from '../bindings/mina-transaction/types.js'; import { HashInput } from './hash.js'; import { Provable } from './provable.js'; import { Gadgets } from './gadgets/gadgets.js'; -import { withMessage } from './field.js'; +import { FieldVar, withMessage } from './field.js'; import { chunkString } from './util/arrays.js'; // external API @@ -980,119 +980,103 @@ class UInt8 extends Struct({ * * **Warning**: Cannot overflow past 255, an error is thrown if the result is greater than 255. */ - constructor(x: number | bigint | string | Field | UInt8) { - if (x instanceof UInt8) x = x.value; + constructor(x: number | bigint | string | FieldVar | UInt8) { + if (x instanceof UInt8) x = x.value.value; super({ value: Field(x) }); UInt8.checkConstant(this.value); } /** - * Add a {@link UInt8} value to another {@link UInt8} element. + * Add a {@link UInt8} to another {@link UInt8} without allowing overflow. * * @example * ```ts * const x = UInt8.from(3); - * const sum = x.add(UInt8.from(5)); - * - * sum.assertEquals(UInt8.from(8)); + * const sum = x.add(5); + * sum.assertEquals(8); * ``` * - * **Warning**: This operation cannot overflow past 255, an error is thrown if the result is greater than 255. - * - * @param value - a {@link UInt8} value to add to the {@link UInt8}. - * - * @return A {@link UInt8} element that is the sum of the two values. + * @throws if the result is greater than 255. */ - add(value: UInt8 | number) { - return UInt8.from(this.value.add(UInt8.from(value).value)); + add(y: UInt8 | bigint | number) { + let z = this.value.add(UInt8.from(y).value); + Gadgets.rangeCheck8(z); + return UInt8.from(z); } /** - * Subtract a {@link UInt8} value by another {@link UInt8} element. + * Subtract a {@link UInt8} from another {@link UInt8} without allowing underflow. * * @example * ```ts * const x = UInt8.from(8); - * const difference = x.sub(UInt8.from(5)); - * - * difference.assertEquals(UInt8.from(3)); + * const difference = x.sub(5); + * difference.assertEquals(3); * ``` * - * @param value - a {@link UInt8} value to subtract from the {@link UInt8}. - * - * @return A {@link UInt8} element that is the difference of the two values. + * @throws if the result is less than 0. */ - sub(value: UInt8 | number) { - return UInt8.from(this.value.sub(UInt8.from(value).value)); + sub(y: UInt8 | bigint | number) { + let z = this.value.sub(UInt8.from(y).value); + Gadgets.rangeCheck8(z); + return UInt8.from(z); } /** - * Multiply a {@link UInt8} value by another {@link UInt8} element. + * Multiply a {@link UInt8} by another {@link UInt8} without allowing overflow. * * @example * ```ts * const x = UInt8.from(3); - * const product = x.mul(UInt8.from(5)); - * - * product.assertEquals(UInt8.from(15)); + * const product = x.mul(5); + * product.assertEquals(15); * ``` * - * **Warning**: This operation cannot overflow past 255, an error is thrown if the result is greater than 255. - * - * @param value - a {@link UInt8} value to multiply with the {@link UInt8}. - * - * @return A {@link UInt8} element that is the product of the two values. + * @throws if the result is greater than 255. */ - mul(value: UInt8 | number) { - return UInt8.from(this.value.mul(UInt8.from(value).value)); + mul(y: UInt8 | bigint | number) { + let z = this.value.mul(UInt8.from(y).value); + Gadgets.rangeCheck8(z); + return UInt8.from(z); } /** - * Divide a {@link UInt8} value by another {@link UInt8} element. - * - * Proves that the denominator is non-zero, or throws a "Division by zero" error. + * Divide a {@link UInt8} by another {@link UInt8}. + * This is integer division that rounds down. * * @example * ```ts - * const x = UInt8.from(6); - * const quotient = x.div(UInt8.from(3)); - * - * quotient.assertEquals(UInt8.from(2)); + * const x = UInt8.from(7); + * const quotient = x.div(2); + * quotient.assertEquals(3); * ``` - * - * @param value - a {@link UInt8} value to divide with the {@link UInt8}. - * - * @return A {@link UInt8} element that is the division of the two values. */ - div(value: UInt8 | number) { - return this.divMod(value).quotient; + div(y: UInt8 | bigint | number) { + return this.divMod(y).quotient; } /** - * Get the remainder a {@link UInt8} value of division of another {@link UInt8} element. + * Get the remainder a {@link UInt8} of division of another {@link UInt8}. * * @example * ```ts * const x = UInt8.from(50); - * const mod = x.mod(UInt8.from(30)); - * - * mod.assertEquals(UInt8.from(18)); + * const mod = x.mod(30); + * mod.assertEquals(20); * ``` - * - * @param value - a {@link UInt8} to get the modulus with another {@link UInt8}. - * - * @return A {@link UInt8} element that is the modulus of the two values. */ - mod(value: UInt8 | number) { - return this.divMod(value).rest; + mod(y: UInt8 | bigint | number) { + return this.divMod(y).remainder; } /** - * Get the quotient and remainder of a {@link UInt8} value divided by another {@link UInt8} element. + * Get the quotient and remainder of a {@link UInt8} divided by another {@link UInt8}: + * + * `x == y * q + r`, where `0 <= r < y`. * * @param y - a {@link UInt8} to get the quotient and remainder of another {@link UInt8}. * - * @return The quotient and remainder of the two values. + * @return The quotient `q` and remainder `r`. */ divMod(y: UInt8 | bigint | number) { let x = this.value; @@ -1103,7 +1087,7 @@ class UInt8 extends Struct({ let yn = y_.toBigInt(); let q = xn / yn; let r = xn - q * yn; - return { quotient: UInt8.from(q), rest: UInt8.from(r) }; + return { quotient: UInt8.from(q), remainder: UInt8.from(r) }; } // prove that x === q * y + r, where 0 <= r < y @@ -1115,77 +1099,60 @@ class UInt8 extends Struct({ Gadgets.rangeCheck16(q); Gadgets.rangeCheck16(r); - let rest = UInt8.from(r); + let remainder = UInt8.from(r); let quotient = UInt8.from(q); - rest.assertLessThan(UInt8.from(y_)); - return { quotient, rest }; + remainder.assertLessThan(y); + return { quotient, remainder }; } /** * Check if this {@link UInt8} is less than or equal to another {@link UInt8} value. - * Returns a {@link Bool}, which is a provable type and can be used to prove the validity of this statement. + * Returns a {@link Bool}. * * @example * ```ts - * UInt8.from(3).lessThanOrEqual(UInt8.from(5)).assertEquals(Bool(true)); + * UInt8.from(3).lessThanOrEqual(UInt8.from(5)); * ``` - * - * **Warning**: Comparison methods only support UInt8 elements of size <= 8 bits in provable code. - * The method will throw if one of the inputs exceeds 8 bits. - * - * - * @param y - the {@link UInt8} value to compare with this {@link UInt8}. - * - * @return A {@link Bool} representing if this {@link UInt8} is less than or equal another {@link UInt8} value. */ - lessThanOrEqual(y: UInt8): Bool { - if (this.value.isConstant() && y.value.isConstant()) { - return Bool(this.value.toBigInt() <= y.value.toBigInt()); + lessThanOrEqual(y: UInt8 | bigint | number): Bool { + let y_ = UInt8.from(y); + if (this.value.isConstant() && y_.value.isConstant()) { + return Bool(this.toBigInt() <= y_.toBigInt()); } throw Error('Not implemented'); } /** * Check if this {@link UInt8} is less than another {@link UInt8} value. - * Returns a {@link Bool}, which is a provable type and can be used prove to the validity of this statement. + * Returns a {@link Bool}. * * @example * ```ts - * UInt8.from(2).lessThan(UInt8.from(3)).assertEquals(Bool(true)); + * UInt8.from(2).lessThan(UInt8.from(3)); * ``` - * - * **Warning**: Comparison methods only support UInt8 elements of size <= 8 bits in provable code. - * The method will throw if one of the inputs exceeds 8 bits. - * - * @param y - the {@link UInt8} value to compare with this {@link UInt8}. - * - * @return A {@link Bool} representing if this {@link UInt8} is less than another {@link UInt8} value. */ - lessThan(y: UInt8): Bool { - if (this.value.isConstant() && y.value.isConstant()) { - return Bool(this.value.toBigInt() < y.value.toBigInt()); + lessThan(y: UInt8 | bigint | number): Bool { + let y_ = UInt8.from(y); + if (this.value.isConstant() && y_.value.isConstant()) { + return Bool(this.toBigInt() < y_.toBigInt()); } throw Error('Not implemented'); } /** * Assert that this {@link UInt8} is less than another {@link UInt8} value. - * Calling this function is equivalent to `UInt8(...).lessThan(...).assertEquals(Bool(true))`. - * See {@link UInt8.lessThan} for more details. * * **Important**: If an assertion fails, the code throws an error. * - * **Warning**: Comparison methods only support UInt8 elements of size <= 8 bits in provable code. - * The method will throw if one of the inputs exceeds 8 bits. - * * @param y - the {@link UInt8} value to compare & assert with this {@link UInt8}. * @param message? - a string error message to print if the assertion fails, optional. */ - assertLessThan(y: UInt8, message?: string) { - if (this.value.isConstant() && y.value.isConstant()) { - let x0 = this.value.toBigInt(); - let y0 = y.value.toBigInt(); + assertLessThan(y: UInt8 | bigint | number, message?: string) { + let y_ = UInt8.from(y); + if (this.value.isConstant() && y_.value.isConstant()) { + let x0 = this.toBigInt(); + let y0 = y_.toBigInt(); if (x0 >= y0) { if (message !== undefined) throw Error(message); throw Error(`UInt8.assertLessThan: expected ${x0} < ${y0}`); @@ -1193,27 +1160,23 @@ class UInt8 extends Struct({ return; } // x < y <=> x + 1 <= y - let xPlus1 = new UInt8(this.value.add(1)); + let xPlus1 = new UInt8(this.value.add(1).value); xPlus1.assertLessThanOrEqual(y, message); } /** * Assert that this {@link UInt8} is less than or equal to another {@link UInt8} value. - * Calling this function is equivalent to `UInt8(...).lessThanOrEqual(...).assertEquals(Bool(true))`. - * See {@link UInt8.lessThanOrEqual} for more details. * * **Important**: If an assertion fails, the code throws an error. * - * **Warning**: Comparison methods only support UInt8 elements of size <= 8 bits in provable code. - * The method will throw if one of the inputs exceeds 8 bits. - * * @param y - the {@link UInt8} value to compare & assert with this {@link UInt8}. * @param message? - a string error message to print if the assertion fails, optional. */ - assertLessThanOrEqual(y: UInt8, message?: string) { - if (this.value.isConstant() && y.value.isConstant()) { - let x0 = this.value.toBigInt(); - let y0 = y.value.toBigInt(); + assertLessThanOrEqual(y: UInt8 | bigint | number, message?: string) { + let y_ = UInt8.from(y); + if (this.value.isConstant() && y_.value.isConstant()) { + let x0 = this.toBigInt(); + let y0 = y_.toBigInt(); if (x0 > y0) { if (message !== undefined) throw Error(message); throw Error(`UInt8.assertLessThanOrEqual: expected ${x0} <= ${y0}`); @@ -1221,86 +1184,64 @@ class UInt8 extends Struct({ return; } try { - // x <= y <=> y - x >= 0 <=> y - x in [0, 2^8) - let yMinusX = y.value.sub(this.value).seal(); - Gadgets.rangeCheck8(yMinusX); + // x <= y <=> y - x >= 0 which is implied by y - x in [0, 2^16) + let yMinusX = y_.value.sub(this.value).seal(); + Gadgets.rangeCheck16(yMinusX); } catch (err) { throw withMessage(err, message); } } /** - * Check if this {@link UInt8} is greater than another {@link UInt8} value. - * Returns a {@link Bool}, which is a provable type and can be used to prove the validity of this statement. + * Check if this {@link UInt8} is greater than another {@link UInt8}. + * Returns a {@link Bool}. * * @example * ```ts - * UInt8.from(5).greaterThan(UInt8.from(3)).assertEquals(Bool(true)); + * // 5 > 3 + * UInt8.from(5).greaterThan(3); * ``` - * - * **Warning**: Comparison methods currently only support Field elements of size <= 8 bits in provable code. - * The method will throw if one of the inputs exceeds 8 bits. - * - * @param y - the {@link UInt8} value to compare with this {@link UInt8}. - * - * @return A {@link Bool} representing if this {@link UInt8} is greater than another {@link UInt8} value. */ - greaterThan(y: UInt8) { - return y.lessThan(this); + greaterThan(y: UInt8 | bigint | number) { + return UInt8.from(y).lessThan(this); } /** * Check if this {@link UInt8} is greater than or equal another {@link UInt8} value. - * Returns a {@link Bool}, which is a provable type and can be used to prove the validity of this statement. + * Returns a {@link Bool}. * * @example * ```ts - * UInt8.from(3).greaterThanOrEqual(UInt8.from(3)).assertEquals(Bool(true)); + * // 3 >= 3 + * UInt8.from(3).greaterThanOrEqual(3); * ``` - * - * **Warning**: Comparison methods only support UInt8 elements of size <= 8 bits in provable code. - * The method will throw if one of the inputs exceeds 8 bits. - * - * @param y - the {@link UInt8} value to compare with this {@link Field}. - * - * @return A {@link Bool} representing if this {@link UInt8} is greater than or equal another {@link UInt8} value. */ - greaterThanOrEqual(y: UInt8) { - return this.lessThan(y).not(); + greaterThanOrEqual(y: UInt8 | bigint | number) { + return UInt8.from(y).lessThanOrEqual(this); } /** * Assert that this {@link UInt8} is greater than another {@link UInt8} value. - * Calling this function is equivalent to `UInt8(...).greaterThan(...).assertEquals(Bool(true))`. - * See {@link UInt8.greaterThan} for more details. * * **Important**: If an assertion fails, the code throws an error. * - * **Warning**: Comparison methods only support UInt8 elements of size <= 8 bits in provable code. - * The method will throw if one of the inputs exceeds 8 bits. - * * @param y - the {@link UInt8} value to compare & assert with this {@link UInt8}. * @param message? - a string error message to print if the assertion fails, optional. */ - assertGreaterThan(y: UInt8, message?: string) { - y.assertLessThan(this, message); + assertGreaterThan(y: UInt8 | bigint | number, message?: string) { + UInt8.from(y).assertLessThan(this, message); } /** * Assert that this {@link UInt8} is greater than or equal to another {@link UInt8} value. - * Calling this function is equivalent to `UInt8(...).greaterThanOrEqual(...).assertEquals(Bool(true))`. - * See {@link UInt8.greaterThanOrEqual} for more details. * * **Important**: If an assertion fails, the code throws an error. * - * **Warning**: Comparison methods only support UInt8 elements of size <= 8 bits in provable code. - * The method will throw if one of the inputs exceeds 8 bits. - * * @param y - the {@link UInt8} value to compare & assert with this {@link UInt8}. * @param message? - a string error message to print if the assertion fails, optional. */ assertGreaterThanOrEqual(y: UInt8, message?: string) { - y.assertLessThanOrEqual(this, message); + UInt8.from(y).assertLessThanOrEqual(this, message); } /** @@ -1311,23 +1252,15 @@ class UInt8 extends Struct({ * @param y - the {@link UInt8} value to compare & assert with this {@link UInt8}. * @param message? - a string error message to print if the assertion fails, optional. */ - assertEquals(y: number | bigint | UInt8, message?: string) { - let y_ = new UInt8(y); - this.toField().assertEquals(y_.toField(), message); + assertEquals(y: UInt8 | bigint | number, message?: string) { + let y_ = UInt8.from(y); + this.value.assertEquals(y_.value, message); } /** * Serialize the {@link UInt8} to a string, e.g. for printing. * - * **Warning**: This operation does _not_ affect the circuit and can't be used to prove anything about the string representation of the {@link UInt8}. Use the operation only during debugging. - * - * @example - * ```ts - * const someUInt8 = UInt8.from(42); - * console.log(someUInt8 .toString()); - * ``` - * - * @return A string equivalent to the string representation of the {@link UInt8}. + * **Warning**: This operation is not provable. */ toString() { return this.value.toString(); @@ -1336,74 +1269,23 @@ class UInt8 extends Struct({ /** * Serialize the {@link UInt8} to a bigint, e.g. for printing. * - * **Warning**: This operation does _not_ affect the circuit and can't be used to prove anything about the bigint representation of the {@link UInt8}. Use the operation only during debugging. - * - * @example - * ```ts - * const someUInt8 = UInt8.from(42); - * console.log(someUInt8.toBigInt()); - * ``` - * - * @return A bigint equivalent to the bigint representation of the {@link UInt8}. + * **Warning**: This operation is not provable. */ toBigInt() { return this.value.toBigInt(); } /** - * Serialize the {@link UInt8} to a {@link Field}. - * - * @example - * ```ts - * const someUInt8 = UInt8.from(42); - * console.log(someUInt8.toField()); - * ``` - * - * @return A {@link Field} equivalent to the bigint representation of the {@link UInt8}. + * {@link Provable.check} for {@link UInt8}. + * Proves that the input is in the [0, 255] range. */ - toField() { - return this.value; - } - - /** - * This function is the implementation of {@link Provable.check} in {@link UInt8} type. - * - * This function is called by {@link Provable.check} to check if the {@link UInt8} is valid. - * To check if a {@link UInt8} is valid, we need to check if the value fits in {@link UInt8.NUM_BITS} bits. - * - * @param value - the {@link UInt8} element to check. - */ - static check(x: { value: Field }) { + static check(x: { value: Field } | Field) { + if (x instanceof Field) x = { value: x }; Gadgets.rangeCheck8(x.value); } - /** - * Serialize the {@link UInt8} to a JSON string, e.g. for printing. - * - * **Warning**: This operation does _not_ affect the circuit and can't be used to prove anything about the JSON string representation of the {@link UInt8}. Use the operation only during debugging. - * - * @example - * ```ts - * const someUInt8 = UInt8.from(42); - * console.log(someUInt8 .toJSON()); - * ``` - * - * @return A string equivalent to the JSON representation of the {@link Field}. - */ - toJSON(): string { - return this.value.toString(); - } - /** * Turns a {@link UInt8} into a {@link UInt32}. - * - * @example - * ```ts - * const someUInt8 = UInt8.from(42); - * const someUInt32 = someUInt8.toUInt32(); - * ``` - * - * @return A {@link UInt32} equivalent to the {@link UInt8}. */ toUInt32(): UInt32 { return new UInt32(this.value); @@ -1411,41 +1293,11 @@ class UInt8 extends Struct({ /** * Turns a {@link UInt8} into a {@link UInt64}. - * - * @example - * ```ts - * const someUInt8 = UInt8.from(42); - * const someUInt64 = someUInt8.toUInt64(); - * ``` - * - * @return A {@link UInt64} equivalent to the {@link UInt8}. - * */ + */ toUInt64(): UInt64 { return new UInt64(this.value); } - /** - * Check whether this {@link UInt8} element is a hard-coded constant in the constraint system. - * If a {@link UInt8} is constructed outside a zkApp method, it is a constant. - * - * @example - * ```ts - * console.log(UInt8.from(42).isConstant()); // true - * ``` - * - * @example - * ```ts - * \@method myMethod(x: UInt8) { - * console.log(x.isConstant()); // false - * } - * ``` - * - * @return A `boolean` showing if this {@link UInt8} is a constant or not. - */ - isConstant() { - return this.value.isConstant(); - } - // TODO: these might be better on a separate `Bytes` class static fromHex(xs: string): UInt8[] { let bytes = chunkString(xs, 2).map((s) => parseInt(s, 16)); @@ -1459,21 +1311,26 @@ class UInt8 extends Struct({ * Creates a {@link UInt8} with a value of 255. */ static MAXINT() { - return new UInt8(Field((1n << BigInt(UInt8.NUM_BITS)) - 1n)); + return new UInt8((1n << BigInt(UInt8.NUM_BITS)) - 1n); } /** * Creates a new {@link UInt8}. */ static from(x: UInt8 | UInt64 | UInt32 | Field | number | bigint) { - if (x instanceof UInt64 || x instanceof UInt32 || x instanceof UInt8) - x = x.value; - return new UInt8(UInt8.checkConstant(Field(x))); + if (x instanceof UInt8) return x; + if (x instanceof UInt64 || x instanceof UInt32 || x instanceof Field) { + // if the input could be larger than 8 bits, we have to prove that it is not + let xx = x instanceof Field ? { value: x } : x; + UInt8.check(xx); + return new UInt8(xx.value.value); + } + return new UInt8(x); } private static checkConstant(x: Field) { - if (!x.isConstant()) return x; + if (!x.isConstant()) return x.value; Gadgets.rangeCheck8(x); - return x; + return x.value; } } From 7caffae60c427d4325c7d607a709dbe27d1aa920 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 13 Dec 2023 13:41:38 +0100 Subject: [PATCH 66/90] provable bytes type --- src/lib/int.ts | 22 +++-- src/lib/provable-types/bytes.ts | 103 +++++++++++++++++++++++ src/lib/provable-types/provable-types.ts | 18 ++++ 3 files changed, 131 insertions(+), 12 deletions(-) create mode 100644 src/lib/provable-types/bytes.ts create mode 100644 src/lib/provable-types/provable-types.ts diff --git a/src/lib/int.ts b/src/lib/int.ts index a365b0791..a4cc974c4 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -5,8 +5,6 @@ import { HashInput } from './hash.js'; import { Provable } from './provable.js'; import { Gadgets } from './gadgets/gadgets.js'; import { FieldVar, withMessage } from './field.js'; -import { chunkString } from './util/arrays.js'; - // external API export { UInt8, UInt32, UInt64, Int64, Sign }; @@ -1267,7 +1265,16 @@ class UInt8 extends Struct({ } /** - * Serialize the {@link UInt8} to a bigint, e.g. for printing. + * Serialize the {@link UInt8} to a number. + * + * **Warning**: This operation is not provable. + */ + toNumber() { + return Number(this.value.toBigInt()); + } + + /** + * Serialize the {@link UInt8} to a bigint. * * **Warning**: This operation is not provable. */ @@ -1298,15 +1305,6 @@ class UInt8 extends Struct({ return new UInt64(this.value); } - // TODO: these might be better on a separate `Bytes` class - static fromHex(xs: string): UInt8[] { - let bytes = chunkString(xs, 2).map((s) => parseInt(s, 16)); - return bytes.map(UInt8.from); - } - static toHex(xs: UInt8[]): string { - return xs.map((x) => x.toBigInt().toString(16).padStart(2, '0')).join(''); - } - /** * Creates a {@link UInt8} with a value of 255. */ diff --git a/src/lib/provable-types/bytes.ts b/src/lib/provable-types/bytes.ts new file mode 100644 index 000000000..ce678970c --- /dev/null +++ b/src/lib/provable-types/bytes.ts @@ -0,0 +1,103 @@ +import { provableFromClass } from '../../bindings/lib/provable-snarky.js'; +import { ProvablePureExtended } from '../circuit_value.js'; +import { assert } from '../gadgets/common.js'; +import { chunkString } from '../util/arrays.js'; +import { Provable } from '../provable.js'; +import { UInt8 } from '../int.js'; + +export { Bytes, createBytes }; + +/** + * A provable type representing an array of bytes. + */ +class Bytes { + data: UInt8[]; + + constructor(data: UInt8[]) { + let size = (this.constructor as typeof Bytes).size; + + // assert that data is not too long + assert( + data.length < size, + `Expected at most ${size} bytes, got ${data.length}` + ); + + // pad the data with zeros + let padding = Array.from( + { length: size - data.length }, + () => new UInt8(0) + ); + this.data = data.concat(padding); + } + + /** + * Coerce the input to {@link Bytes}. + * + * Inputs smaller than `this.size` are padded with zero bytes. + */ + static from(data: (UInt8 | bigint | number)[] | Uint8Array) { + return new this([...data].map(UInt8.from)); + } + + toBytes(): Uint8Array { + return Uint8Array.from(this.data.map((x) => x.toNumber())); + } + + /** + * Create {@link Bytes} from a string. + * + * Inputs smaller than `this.size` are padded with zero bytes. + */ + static fromString(s: string) { + let bytes = new TextEncoder().encode(s); + return this.from(bytes); + } + + /** + * Create {@link Bytes} from a hex string. + * + * Inputs smaller than `this.size` are padded with zero bytes. + */ + static fromHex(xs: string): Bytes { + let bytes = chunkString(xs, 2).map((s) => parseInt(s, 16)); + return this.from(bytes); + } + + /** + * Convert {@link Bytes} to a hex string. + */ + toHex(xs: Bytes): string { + return xs.data + .map((x) => x.toBigInt().toString(16).padStart(2, '0')) + .join(''); + } + + // dynamic subclassing infra + static _size?: number; + static _provable?: ProvablePureExtended; + + /** + * The size of the {@link Bytes}. + */ + static get size() { + assert(this._size !== undefined, 'Bytes not initialized'); + return this._size; + } + + /** + * `Provable` + */ + static get provable() { + assert(this._provable !== undefined, 'Bytes not initialized'); + return this._provable; + } +} + +function createBytes(size: number): typeof Bytes { + return class Bytes_ extends Bytes { + static _size = size; + static _provable = provableFromClass(Bytes_, { + data: Provable.Array(UInt8, size), + }); + }; +} diff --git a/src/lib/provable-types/provable-types.ts b/src/lib/provable-types/provable-types.ts new file mode 100644 index 000000000..717b9b098 --- /dev/null +++ b/src/lib/provable-types/provable-types.ts @@ -0,0 +1,18 @@ +import { Bytes as InternalBytes, createBytes } from './bytes.js'; + +export { Bytes }; + +type Bytes = InternalBytes; + +/** + * A provable type representing an array of bytes. + * + * ```ts + * class Bytes32 extends Bytes(32) {} + * + * let bytes = Bytes32.fromHex('deadbeef'); + * ``` + */ +function Bytes(size: number) { + return createBytes(size); +} From fca04f80b0890d4d6bdae2ecb4178c9be128e328 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 13 Dec 2023 14:05:30 +0100 Subject: [PATCH 67/90] use bytes as keccak input --- src/lib/keccak.ts | 64 ++++++++++++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 26 deletions(-) diff --git a/src/lib/keccak.ts b/src/lib/keccak.ts index a1e4bb872..1b6f48a31 100644 --- a/src/lib/keccak.ts +++ b/src/lib/keccak.ts @@ -1,23 +1,24 @@ import { Field } from './field.js'; import { Gadgets } from './gadgets/gadgets.js'; import { assert } from './errors.js'; -import { rangeCheck8 } from './gadgets/range-check.js'; import { Provable } from './provable.js'; import { chunk } from './util/arrays.js'; +import { Bytes } from './provable-types/provable-types.js'; +import { UInt8 } from './int.js'; export { Keccak }; const Keccak = { /** TODO */ - nistSha3(len: 256 | 384 | 512, message: Field[]): Field[] { + nistSha3(len: 256 | 384 | 512, message: Bytes) { return nistSha3(len, message); }, /** TODO */ - ethereum(message: Field[]): Field[] { + ethereum(message: Bytes) { return ethereum(message); }, /** TODO */ - preNist(len: 256 | 384 | 512, message: Field[]): Field[] { + preNist(len: 256 | 384 | 512, message: Bytes) { return preNist(len, message); }, }; @@ -102,7 +103,7 @@ function bytesToPad(rate: number, length: number): number { // The padded message will start with the message argument followed by the padding rule (below) to fulfill a length that is a multiple of rate (in bytes). // If nist is true, then the padding rule is 0x06 ..0*..1. // If nist is false, then the padding rule is 10*1. -function pad(message: Field[], rate: number, nist: boolean): Field[] { +function pad(message: UInt8[], rate: number, nist: boolean): UInt8[] { // Find out desired length of the padding in bytes // If message is already rate bits, need to pad full rate again const extraBytes = bytesToPad(rate, message.length); @@ -112,8 +113,8 @@ function pad(message: Field[], rate: number, nist: boolean): Field[] { const last = 0x80n; // Create the padding vector - const pad = Array(extraBytes).fill(Field.from(0)); - pad[0] = Field.from(first); + const pad = Array(extraBytes).fill(UInt8.from(0)); + pad[0] = UInt8.from(first); pad[extraBytes - 1] = pad[extraBytes - 1].add(last); // Return the padded message @@ -324,11 +325,11 @@ function sponge( // - the 10*1 pad will take place after the message, until reaching the bit length rate. // - then, {0} pad will take place to finish the 200 bytes of the state. function hash( - message: Field[], + message: Bytes, length: number, capacity: number, nistVersion: boolean -): Field[] { +): UInt8[] { // Throw errors if used improperly assert(capacity > 0, 'capacity must be positive'); assert( @@ -346,7 +347,7 @@ function hash( const rate = KECCAK_STATE_LENGTH_WORDS - capacity; // apply padding, convert to words, and hash - const paddedBytes = pad(message, rate * BYTES_PER_WORD, nistVersion); + const paddedBytes = pad(message.data, rate * BYTES_PER_WORD, nistVersion); const padded = bytesToWords(paddedBytes); const hash = sponge(padded, length, capacity, rate); @@ -356,18 +357,20 @@ function hash( } // Gadget for NIST SHA-3 function for output lengths 256/384/512. -function nistSha3(len: 256 | 384 | 512, message: Field[]): Field[] { - return hash(message, len / 8, len / 4, true); +function nistSha3(len: 256 | 384 | 512, message: Bytes): Bytes { + let bytes = hash(message, len / 8, len / 4, true); + return BytesOfBitlength[len].from(bytes); } // Gadget for pre-NIST SHA-3 function for output lengths 256/384/512. // Note that when calling with output length 256 this is equivalent to the ethereum function -function preNist(len: 256 | 384 | 512, message: Field[]): Field[] { - return hash(message, len / 8, len / 4, false); +function preNist(len: 256 | 384 | 512, message: Bytes): Bytes { + let bytes = hash(message, len / 8, len / 4, false); + return BytesOfBitlength[len].from(bytes); } // Gadget for Keccak hash function for the parameters used in Ethereum. -function ethereum(message: Field[]): Field[] { +function ethereum(message: Bytes): Bytes { return preNist(256, message); } @@ -428,27 +431,36 @@ const State = { }, }; +// AUXILIARY TYPES + +class Bytes32 extends Bytes(32) {} +class Bytes48 extends Bytes(48) {} +class Bytes64 extends Bytes(64) {} + +const BytesOfBitlength = { + 256: Bytes32, + 384: Bytes48, + 512: Bytes64, +}; + // AUXILARY FUNCTIONS // Auxiliary functions to check the composition of 8 byte values (LE) into a 64-bit word and create constraints for it -function bytesToWord(wordBytes: Field[]): Field { - return wordBytes.reduce((acc, value, idx) => { +function bytesToWord(wordBytes: UInt8[]): Field { + return wordBytes.reduce((acc, byte, idx) => { const shift = 1n << BigInt(8 * idx); - return acc.add(value.mul(shift)); + return acc.add(byte.value.mul(shift)); }, Field.from(0)); } -function wordToBytes(word: Field): Field[] { - let bytes = Provable.witness(Provable.Array(Field, BYTES_PER_WORD), () => { +function wordToBytes(word: Field): UInt8[] { + let bytes = Provable.witness(Provable.Array(UInt8, BYTES_PER_WORD), () => { let w = word.toBigInt(); return Array.from({ length: BYTES_PER_WORD }, (_, k) => - Field.from((w >> BigInt(8 * k)) & 0xffn) + UInt8.from((w >> BigInt(8 * k)) & 0xffn) ); }); - // range-check - // TODO(jackryanservia): Use lookup argument once issue is resolved - bytes.forEach(rangeCheck8); // check decomposition bytesToWord(bytes).assertEquals(word); @@ -456,11 +468,11 @@ function wordToBytes(word: Field): Field[] { return bytes; } -function bytesToWords(bytes: Field[]): Field[] { +function bytesToWords(bytes: UInt8[]): Field[] { return chunk(bytes, BYTES_PER_WORD).map(bytesToWord); } -function wordsToBytes(words: Field[]): Field[] { +function wordsToBytes(words: Field[]): UInt8[] { return words.flatMap(wordToBytes); } From da919b70b6fbd4c513198bf45617693f19f11147 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 13 Dec 2023 15:00:10 +0100 Subject: [PATCH 68/90] tweaks to bytes type --- src/lib/provable-types/bytes.ts | 38 ++++++++++++++++-------- src/lib/provable-types/provable-types.ts | 1 + 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/lib/provable-types/bytes.ts b/src/lib/provable-types/bytes.ts index ce678970c..7cb3763d7 100644 --- a/src/lib/provable-types/bytes.ts +++ b/src/lib/provable-types/bytes.ts @@ -5,29 +5,31 @@ import { chunkString } from '../util/arrays.js'; import { Provable } from '../provable.js'; import { UInt8 } from '../int.js'; -export { Bytes, createBytes }; +export { Bytes, createBytes, FlexibleBytes }; + +type FlexibleBytes = Bytes | (UInt8 | bigint | number)[] | Uint8Array; /** * A provable type representing an array of bytes. */ class Bytes { - data: UInt8[]; + bytes: UInt8[]; - constructor(data: UInt8[]) { + constructor(bytes: UInt8[]) { let size = (this.constructor as typeof Bytes).size; // assert that data is not too long assert( - data.length < size, - `Expected at most ${size} bytes, got ${data.length}` + bytes.length < size, + `Expected at most ${size} bytes, got ${bytes.length}` ); // pad the data with zeros let padding = Array.from( - { length: size - data.length }, + { length: size - bytes.length }, () => new UInt8(0) ); - this.data = data.concat(padding); + this.bytes = bytes.concat(padding); } /** @@ -35,12 +37,17 @@ class Bytes { * * Inputs smaller than `this.size` are padded with zero bytes. */ - static from(data: (UInt8 | bigint | number)[] | Uint8Array) { + static from(data: (UInt8 | bigint | number)[] | Uint8Array | Bytes): Bytes { + if (data instanceof Bytes) return data; + if (this._size === undefined) { + let Bytes_ = createBytes(data.length); + return Bytes_.from(data); + } return new this([...data].map(UInt8.from)); } toBytes(): Uint8Array { - return Uint8Array.from(this.data.map((x) => x.toNumber())); + return Uint8Array.from(this.bytes.map((x) => x.toNumber())); } /** @@ -67,14 +74,17 @@ class Bytes { * Convert {@link Bytes} to a hex string. */ toHex(xs: Bytes): string { - return xs.data + return xs.bytes .map((x) => x.toBigInt().toString(16).padStart(2, '0')) .join(''); } // dynamic subclassing infra static _size?: number; - static _provable?: ProvablePureExtended; + static _provable?: ProvablePureExtended< + Bytes, + { bytes: { value: string }[] } + >; /** * The size of the {@link Bytes}. @@ -84,6 +94,10 @@ class Bytes { return this._size; } + get length() { + return this.bytes.length; + } + /** * `Provable` */ @@ -97,7 +111,7 @@ function createBytes(size: number): typeof Bytes { return class Bytes_ extends Bytes { static _size = size; static _provable = provableFromClass(Bytes_, { - data: Provable.Array(UInt8, size), + bytes: Provable.Array(UInt8, size), }); }; } diff --git a/src/lib/provable-types/provable-types.ts b/src/lib/provable-types/provable-types.ts index 717b9b098..1d850aa3c 100644 --- a/src/lib/provable-types/provable-types.ts +++ b/src/lib/provable-types/provable-types.ts @@ -16,3 +16,4 @@ type Bytes = InternalBytes; function Bytes(size: number) { return createBytes(size); } +Bytes.from = InternalBytes.from; From 3d8dbbd9f9695d1b15549a5cc5a5c10f628ed96e Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 13 Dec 2023 15:01:18 +0100 Subject: [PATCH 69/90] adapt bytes consumers --- src/lib/foreign-ecdsa.ts | 19 ++++++++++--------- src/lib/keccak.ts | 17 +++++++++-------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/lib/foreign-ecdsa.ts b/src/lib/foreign-ecdsa.ts index d5d941745..bccbaa77a 100644 --- a/src/lib/foreign-ecdsa.ts +++ b/src/lib/foreign-ecdsa.ts @@ -11,9 +11,10 @@ import { AlmostForeignField } from './foreign-field.js'; import { assert } from './gadgets/common.js'; import { Field3 } from './gadgets/foreign-field.js'; import { Ecdsa } from './gadgets/elliptic-curve.js'; -import { Field } from './field.js'; import { l } from './gadgets/range-check.js'; import { Keccak } from './keccak.js'; +import { Bytes } from './provable-types/provable-types.js'; +import { UInt8 } from './int.js'; // external API export { createEcdsa, EcdsaSignature }; @@ -99,7 +100,7 @@ class EcdsaSignature { * isValid.assertTrue('signature verifies'); * ``` */ - verify(message: Field[], publicKey: FlexiblePoint) { + verify(message: Bytes, publicKey: FlexiblePoint) { let msgHashBytes = Keccak.ethereum(message); let msgHash = keccakOutputToScalar(msgHashBytes, this.Constructor.Curve); return this.verifySignedHash(msgHash, publicKey); @@ -132,8 +133,7 @@ class EcdsaSignature { * Note: This method is not provable, and only takes JS bigints as input. */ static sign(message: (bigint | number)[] | Uint8Array, privateKey: bigint) { - let msgFields = [...message].map(Field.from); - let msgHashBytes = Keccak.ethereum(msgFields); + let msgHashBytes = Keccak.ethereum(message); let msgHash = keccakOutputToScalar(msgHashBytes, this.Curve); return this.signHash(msgHash.toBigInt(), privateKey); } @@ -228,7 +228,7 @@ function toObject(signature: EcdsaSignature) { * - takes a 32 bytes hash * - converts them to 3 limbs which collectively have L_n <= 256 bits */ -function keccakOutputToScalar(hash: Field[], Curve: typeof ForeignCurve) { +function keccakOutputToScalar(hash: Bytes, Curve: typeof ForeignCurve) { const L_n = Curve.Scalar.sizeInBits; // keep it simple for now, avoid dealing with dropping bits // TODO: what does "leftmost bits" mean? big-endian or little-endian? @@ -240,14 +240,15 @@ function keccakOutputToScalar(hash: Field[], Curve: typeof ForeignCurve) { // piece together into limbs // bytes are big-endian, so the first byte is the most significant assert(l === 88n); - let x2 = bytesToLimbBE(hash.slice(0, 10)); - let x1 = bytesToLimbBE(hash.slice(10, 21)); - let x0 = bytesToLimbBE(hash.slice(21, 32)); + let x2 = bytesToLimbBE(hash.bytes.slice(0, 10)); + let x1 = bytesToLimbBE(hash.bytes.slice(10, 21)); + let x0 = bytesToLimbBE(hash.bytes.slice(21, 32)); return new Curve.Scalar.AlmostReduced([x0, x1, x2]); } -function bytesToLimbBE(bytes: Field[]) { +function bytesToLimbBE(bytes_: UInt8[]) { + let bytes = bytes_.map((x) => x.value); let n = bytes.length; let limb = bytes[0]; for (let i = 1; i < n; i++) { diff --git a/src/lib/keccak.ts b/src/lib/keccak.ts index 1b6f48a31..5def838e3 100644 --- a/src/lib/keccak.ts +++ b/src/lib/keccak.ts @@ -3,23 +3,24 @@ import { Gadgets } from './gadgets/gadgets.js'; import { assert } from './errors.js'; import { Provable } from './provable.js'; import { chunk } from './util/arrays.js'; -import { Bytes } from './provable-types/provable-types.js'; +import { FlexibleBytes } from './provable-types/bytes.js'; import { UInt8 } from './int.js'; +import { Bytes } from './provable-types/provable-types.js'; export { Keccak }; const Keccak = { /** TODO */ - nistSha3(len: 256 | 384 | 512, message: Bytes) { - return nistSha3(len, message); + nistSha3(len: 256 | 384 | 512, message: FlexibleBytes) { + return nistSha3(len, Bytes.from(message)); }, /** TODO */ - ethereum(message: Bytes) { - return ethereum(message); + ethereum(message: FlexibleBytes) { + return ethereum(Bytes.from(message)); }, /** TODO */ - preNist(len: 256 | 384 | 512, message: Bytes) { - return preNist(len, message); + preNist(len: 256 | 384 | 512, message: FlexibleBytes) { + return preNist(len, Bytes.from(message)); }, }; @@ -347,7 +348,7 @@ function hash( const rate = KECCAK_STATE_LENGTH_WORDS - capacity; // apply padding, convert to words, and hash - const paddedBytes = pad(message.data, rate * BYTES_PER_WORD, nistVersion); + const paddedBytes = pad(message.bytes, rate * BYTES_PER_WORD, nistVersion); const padded = bytesToWords(paddedBytes); const hash = sponge(padded, length, capacity, rate); From 644f2790bc93a121d44fc8efa2b6bfb63f8be9b3 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 13 Dec 2023 15:06:14 +0100 Subject: [PATCH 70/90] fix import cycle --- src/index.ts | 13 +++++++++++-- src/lib/hash.ts | 17 +++++------------ 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/index.ts b/src/index.ts index 4b896dd90..3f29a8059 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,8 +9,17 @@ export { } from './lib/foreign-field.js'; export { createForeignCurve, ForeignCurve } from './lib/foreign-curve.js'; export { createEcdsa, EcdsaSignature } from './lib/foreign-ecdsa.js'; -export { Poseidon, TokenSymbol, Hash } from './lib/hash.js'; -export { Keccak } from './lib/keccak.js'; +export { TokenSymbol } from './lib/hash.js'; + +import { Poseidon } from './lib/hash.js'; +import { Keccak } from './lib/keccak.js'; + +const Hash = { + hash: Poseidon.hash, + Poseidon, + Keccak, +}; +export { Poseidon, Keccak, Hash }; export * from './lib/signature.js'; export type { diff --git a/src/lib/hash.ts b/src/lib/hash.ts index d986d7ea4..39fd99377 100644 --- a/src/lib/hash.ts +++ b/src/lib/hash.ts @@ -6,10 +6,9 @@ import { Provable } from './provable.js'; import { MlFieldArray } from './ml/fields.js'; import { Poseidon as PoseidonBigint } from '../bindings/crypto/poseidon.js'; import { assert } from './errors.js'; -import { Keccak } from './keccak.js'; // external API -export { Poseidon, TokenSymbol, Hash }; +export { Poseidon, TokenSymbol }; // internal API export { @@ -24,19 +23,19 @@ export { }; class Sponge { - private sponge: unknown; + #sponge: unknown; constructor() { let isChecked = Provable.inCheckedComputation(); - this.sponge = Snarky.poseidon.sponge.create(isChecked); + this.#sponge = Snarky.poseidon.sponge.create(isChecked); } absorb(x: Field) { - Snarky.poseidon.sponge.absorb(this.sponge, x.value); + Snarky.poseidon.sponge.absorb(this.#sponge, x.value); } squeeze(): Field { - return Field(Snarky.poseidon.sponge.squeeze(this.sponge)); + return Field(Snarky.poseidon.sponge.squeeze(this.#sponge)); } } @@ -206,9 +205,3 @@ function isConstant(fields: Field[]) { function toBigints(fields: Field[]) { return fields.map((x) => x.toBigInt()); } - -const Hash = { - hash: Poseidon.hash, - Poseidon, - Keccak, -}; From eadb423e24e91264bf94e4bb924d6569c9604b31 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 13 Dec 2023 15:06:32 +0100 Subject: [PATCH 71/90] adapt keccak unit test --- src/lib/keccak.unit-test.ts | 67 +++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 37 deletions(-) diff --git a/src/lib/keccak.unit-test.ts b/src/lib/keccak.unit-test.ts index 7caa5c166..3b1b16377 100644 --- a/src/lib/keccak.unit-test.ts +++ b/src/lib/keccak.unit-test.ts @@ -1,9 +1,7 @@ -import { Field } from './field.js'; -import { Provable } from './provable.js'; import { Keccak } from './keccak.js'; import { ZkProgram } from './proof_system.js'; import { Random } from './testing/random.js'; -import { array, equivalentAsync, fieldWithRng } from './testing/equivalent.js'; +import { equivalentAsync, spec } from './testing/equivalent.js'; import { keccak_224, keccak_256, @@ -14,6 +12,7 @@ import { sha3_384, sha3_512, } from '@noble/hashes/sha3'; +import { Bytes } from './provable-types/provable-types.js'; const RUNS = 1; @@ -32,8 +31,6 @@ const testImplementations = { }, }; -const uint = (length: number) => fieldWithRng(Random.biguint(length)); - // Choose a test length at random const digestLength = ([256, 384, 512] as const)[Math.floor(Math.random() * 4)]; @@ -46,18 +43,18 @@ const preImageLength = Math.floor(digestLength / (Math.random() * 4 + 2)); // No need to test Ethereum because it's just a special case of preNist const KeccakProgram = ZkProgram({ name: 'keccak-test', - publicInput: Provable.Array(Field, preImageLength), - publicOutput: Provable.Array(Field, digestLengthBytes), + publicInput: Bytes(preImageLength).provable, + publicOutput: Bytes(digestLengthBytes).provable, methods: { nistSha3: { privateInputs: [], - method(preImage) { + method(preImage: Bytes) { return Keccak.nistSha3(digestLength, preImage); }, }, preNist: { privateInputs: [], - method(preImage) { + method(preImage: Bytes) { return Keccak.preNist(digestLength, preImage); }, }, @@ -66,42 +63,38 @@ const KeccakProgram = ZkProgram({ await KeccakProgram.compile(); +const bytes = (length: number) => { + const Bytes_ = Bytes(length); + return spec({ + rng: Random.map(Random.bytes(length), (x) => Uint8Array.from(x)), + there: Bytes_.from, + back: (x) => x.toBytes(), + provable: Bytes_.provable, + }); +}; + // SHA-3 await equivalentAsync( { - from: [array(uint(8), preImageLength)], - to: array(uint(8), digestLengthBytes), + from: [bytes(preImageLength)], + to: bytes(digestLengthBytes), }, { runs: RUNS } -)( - (x) => { - const byteArray = new Uint8Array(x.map(Number)); - const result = testImplementations.sha3[digestLength](byteArray); - return Array.from(result).map(BigInt); - }, - async (x) => { - const proof = await KeccakProgram.nistSha3(x); - await KeccakProgram.verify(proof); - return proof.publicOutput; - } -); +)(testImplementations.sha3[digestLength], async (x) => { + const proof = await KeccakProgram.nistSha3(x); + await KeccakProgram.verify(proof); + return proof.publicOutput; +}); // PreNIST Keccak await equivalentAsync( { - from: [array(uint(8), preImageLength)], - to: array(uint(8), digestLengthBytes), + from: [bytes(preImageLength)], + to: bytes(digestLengthBytes), }, { runs: RUNS } -)( - (x) => { - const byteArray = new Uint8Array(x.map(Number)); - const result = testImplementations.preNist[digestLength](byteArray); - return Array.from(result).map(BigInt); - }, - async (x) => { - const proof = await KeccakProgram.preNist(x); - await KeccakProgram.verify(proof); - return proof.publicOutput; - } -); +)(testImplementations.preNist[digestLength], async (x) => { + const proof = await KeccakProgram.preNist(x); + await KeccakProgram.verify(proof); + return proof.publicOutput; +}); From 3ee7d192e3887ee639ae97574c6b0649bfd271d4 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 13 Dec 2023 15:15:38 +0100 Subject: [PATCH 72/90] fix length assertion --- src/lib/provable-types/bytes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/provable-types/bytes.ts b/src/lib/provable-types/bytes.ts index 7cb3763d7..8c0b08b28 100644 --- a/src/lib/provable-types/bytes.ts +++ b/src/lib/provable-types/bytes.ts @@ -20,7 +20,7 @@ class Bytes { // assert that data is not too long assert( - bytes.length < size, + bytes.length <= size, `Expected at most ${size} bytes, got ${bytes.length}` ); From 8b4344a4d3da49ef9031c4c1a86225915274f63a Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 13 Dec 2023 15:20:37 +0100 Subject: [PATCH 73/90] fix something very stupid --- src/lib/keccak.unit-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/keccak.unit-test.ts b/src/lib/keccak.unit-test.ts index 3b1b16377..7aa16f024 100644 --- a/src/lib/keccak.unit-test.ts +++ b/src/lib/keccak.unit-test.ts @@ -32,7 +32,7 @@ const testImplementations = { }; // Choose a test length at random -const digestLength = ([256, 384, 512] as const)[Math.floor(Math.random() * 4)]; +const digestLength = ([256, 384, 512] as const)[Math.floor(Math.random() * 3)]; // Digest length in bytes const digestLengthBytes = digestLength / 8; From 0a896bd7f6fc765aa4bb9b3fcb11e8583c6d5c8b Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 13 Dec 2023 15:32:47 +0100 Subject: [PATCH 74/90] add constant tests --- src/lib/keccak.unit-test.ts | 53 ++++++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/src/lib/keccak.unit-test.ts b/src/lib/keccak.unit-test.ts index 7aa16f024..454874cdc 100644 --- a/src/lib/keccak.unit-test.ts +++ b/src/lib/keccak.unit-test.ts @@ -1,7 +1,11 @@ import { Keccak } from './keccak.js'; import { ZkProgram } from './proof_system.js'; -import { Random } from './testing/random.js'; -import { equivalentAsync, spec } from './testing/equivalent.js'; +import { Random, sample } from './testing/random.js'; +import { + equivalentAsync, + equivalentProvable, + spec, +} from './testing/equivalent.js'; import { keccak_224, keccak_256, @@ -31,8 +35,41 @@ const testImplementations = { }, }; +const lengths = [256, 384, 512] as const; + +// witness construction checks + +const bytes = (length: number) => { + const Bytes_ = Bytes(length); + return spec({ + rng: Random.map(Random.bytes(length), (x) => Uint8Array.from(x)), + there: Bytes_.from, + back: (x) => x.toBytes(), + provable: Bytes_.provable, + }); +}; + +for (let length of lengths) { + let [preimageLength] = sample(Random.nat(100), 1); + console.log(`Testing ${length} with preimage length ${preimageLength}`); + let inputBytes = bytes(preimageLength); + let outputBytes = bytes(length / 8); + + equivalentProvable({ from: [inputBytes], to: outputBytes, verbose: true })( + testImplementations.sha3[length], + (x) => Keccak.nistSha3(length, x), + `sha3 ${length}` + ); + + equivalentProvable({ from: [inputBytes], to: outputBytes, verbose: true })( + testImplementations.preNist[length], + (x) => Keccak.preNist(length, x), + `keccak ${length}` + ); +} + // Choose a test length at random -const digestLength = ([256, 384, 512] as const)[Math.floor(Math.random() * 3)]; +const digestLength = lengths[Math.floor(Math.random() * 3)]; // Digest length in bytes const digestLengthBytes = digestLength / 8; @@ -63,16 +100,6 @@ const KeccakProgram = ZkProgram({ await KeccakProgram.compile(); -const bytes = (length: number) => { - const Bytes_ = Bytes(length); - return spec({ - rng: Random.map(Random.bytes(length), (x) => Uint8Array.from(x)), - there: Bytes_.from, - back: (x) => x.toBytes(), - provable: Bytes_.provable, - }); -}; - // SHA-3 await equivalentAsync( { From ca1c02e7295f7eefc31790812fed26707f96724f Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 13 Dec 2023 15:59:23 +0100 Subject: [PATCH 75/90] add verbose argument to nonprovable equivalence test --- src/lib/testing/equivalent.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/lib/testing/equivalent.ts b/src/lib/testing/equivalent.ts index 2e8384d7d..7a0f20fd7 100644 --- a/src/lib/testing/equivalent.ts +++ b/src/lib/testing/equivalent.ts @@ -122,7 +122,7 @@ function toUnion(spec: OrUnion): FromSpecUnion { function equivalent< In extends Tuple>, Out extends ToSpec ->({ from, to }: { from: In; to: Out }) { +>({ from, to, verbose }: { from: In; to: Out; verbose?: boolean }) { return function run( f1: (...args: Params1) => First, f2: (...args: Params2) => Second, @@ -130,7 +130,8 @@ function equivalent< ) { let generators = from.map((spec) => spec.rng); let assertEqual = to.assertEqual ?? deepEqual; - test(...(generators as any[]), (...args) => { + let start = performance.now(); + let nRuns = test(...(generators as any[]), (...args) => { args.pop(); let inputs = args as Params1; handleErrors( @@ -143,6 +144,14 @@ function equivalent< label ); }); + + if (verbose) { + let ms = (performance.now() - start).toFixed(1); + let runs = nRuns.toString().padStart(2, ' '); + console.log( + `${label.padEnd(20, ' ')} success on ${runs} runs in ${ms}ms.` + ); + } }; } From 0fbc626ff9a284b51099368bc70fc89f8d465fc9 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 13 Dec 2023 16:00:01 +0100 Subject: [PATCH 76/90] run keccak unit tests outside circuit for now --- src/lib/keccak.unit-test.ts | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/lib/keccak.unit-test.ts b/src/lib/keccak.unit-test.ts index 454874cdc..a811b018d 100644 --- a/src/lib/keccak.unit-test.ts +++ b/src/lib/keccak.unit-test.ts @@ -1,11 +1,7 @@ import { Keccak } from './keccak.js'; import { ZkProgram } from './proof_system.js'; import { Random, sample } from './testing/random.js'; -import { - equivalentAsync, - equivalentProvable, - spec, -} from './testing/equivalent.js'; +import { equivalent, equivalentAsync, spec } from './testing/equivalent.js'; import { keccak_224, keccak_256, @@ -37,7 +33,8 @@ const testImplementations = { const lengths = [256, 384, 512] as const; -// witness construction checks +// checks outside circuit +// TODO: fix witness generation slowness const bytes = (length: number) => { const Bytes_ = Bytes(length); @@ -55,13 +52,13 @@ for (let length of lengths) { let inputBytes = bytes(preimageLength); let outputBytes = bytes(length / 8); - equivalentProvable({ from: [inputBytes], to: outputBytes, verbose: true })( + equivalent({ from: [inputBytes], to: outputBytes, verbose: true })( testImplementations.sha3[length], (x) => Keccak.nistSha3(length, x), `sha3 ${length}` ); - equivalentProvable({ from: [inputBytes], to: outputBytes, verbose: true })( + equivalent({ from: [inputBytes], to: outputBytes, verbose: true })( testImplementations.preNist[length], (x) => Keccak.preNist(length, x), `keccak ${length}` @@ -74,12 +71,11 @@ const digestLength = lengths[Math.floor(Math.random() * 3)]; // Digest length in bytes const digestLengthBytes = digestLength / 8; -// Chose a random preimage length -const preImageLength = Math.floor(digestLength / (Math.random() * 4 + 2)); +const preImageLength = 32; // No need to test Ethereum because it's just a special case of preNist const KeccakProgram = ZkProgram({ - name: 'keccak-test', + name: `keccak-test-${digestLength}`, publicInput: Bytes(preImageLength).provable, publicOutput: Bytes(digestLengthBytes).provable, methods: { From e5810f48625d1702f738e3d5a0bb8843e6cf0fcc Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 13 Dec 2023 17:01:39 +0100 Subject: [PATCH 77/90] make old unit tests work --- src/index.ts | 14 ++------ src/lib/hashes-combined.ts | 41 ++++++++++++++++++++++ src/lib/keccak-old.unit-test.ts | 44 ++++++++++-------------- src/lib/provable-types/bytes.ts | 4 +-- src/lib/provable-types/provable-types.ts | 1 + 5 files changed, 66 insertions(+), 38 deletions(-) create mode 100644 src/lib/hashes-combined.ts diff --git a/src/index.ts b/src/index.ts index 3f29a8059..4a440bf14 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,17 +9,9 @@ export { } from './lib/foreign-field.js'; export { createForeignCurve, ForeignCurve } from './lib/foreign-curve.js'; export { createEcdsa, EcdsaSignature } from './lib/foreign-ecdsa.js'; -export { TokenSymbol } from './lib/hash.js'; - -import { Poseidon } from './lib/hash.js'; -import { Keccak } from './lib/keccak.js'; - -const Hash = { - hash: Poseidon.hash, - Poseidon, - Keccak, -}; -export { Poseidon, Keccak, Hash }; +export { Poseidon, TokenSymbol } from './lib/hash.js'; +export { Keccak } from './lib/keccak.js'; +export { Hash } from './lib/hashes-combined.js'; export * from './lib/signature.js'; export type { diff --git a/src/lib/hashes-combined.ts b/src/lib/hashes-combined.ts new file mode 100644 index 000000000..5608627e4 --- /dev/null +++ b/src/lib/hashes-combined.ts @@ -0,0 +1,41 @@ +import { Poseidon } from './hash.js'; +import { Keccak } from './keccak.js'; +import { Bytes } from './provable-types/provable-types.js'; + +export { Hash }; + +// TODO do we want this? +const Hash = { + hash: Poseidon.hash, + Poseidon, + SHA3_256: { + hash(bytes: Bytes) { + return Keccak.nistSha3(256, bytes); + }, + }, + SHA3_384: { + hash(bytes: Bytes) { + return Keccak.nistSha3(384, bytes); + }, + }, + SHA3_512: { + hash(bytes: Bytes) { + return Keccak.nistSha3(512, bytes); + }, + }, + Keccak256: { + hash(bytes: Bytes) { + return Keccak.preNist(256, bytes); + }, + }, + Keccak384: { + hash(bytes: Bytes) { + return Keccak.preNist(384, bytes); + }, + }, + Keccak512: { + hash(bytes: Bytes) { + return Keccak.preNist(512, bytes); + }, + }, +}; diff --git a/src/lib/keccak-old.unit-test.ts b/src/lib/keccak-old.unit-test.ts index c934bcf15..a39179f60 100644 --- a/src/lib/keccak-old.unit-test.ts +++ b/src/lib/keccak-old.unit-test.ts @@ -1,9 +1,10 @@ import { test, Random } from './testing/property.js'; import { UInt8 } from './int.js'; -import { Hash } from './hash.js'; +import { Hash } from './hashes-combined.js'; import { Provable } from './provable.js'; import { expect } from 'expect'; import assert from 'assert'; +import { Bytes } from './provable-types/provable-types.js'; let RandomUInt8 = Random.map(Random.uint8, (x) => UInt8.from(x)); @@ -13,16 +14,14 @@ test(Random.uint8, Random.uint8, (x, y, assert) => { assert(z instanceof UInt8); assert(z.toBigInt() === x); assert(z.toString() === x.toString()); - assert(z.isConstant()); assert((z = new UInt8(x)) instanceof UInt8 && z.toBigInt() === x); assert((z = new UInt8(z)) instanceof UInt8 && z.toBigInt() === x); - assert((z = new UInt8(z.value)) instanceof UInt8 && z.toBigInt() === x); + assert((z = new UInt8(z.value.value)) instanceof UInt8 && z.toBigInt() === x); z = new UInt8(y); assert(z instanceof UInt8); assert(z.toString() === y.toString()); - assert(z.toJSON() === y.toString()); }); // handles all numbers up to 2^8 @@ -50,29 +49,27 @@ function checkHashInCircuit() { .create()() .map((x) => Provable.witness(UInt8, () => UInt8.from(x))); - checkHashConversions(data); + checkHashConversions(Bytes.from(data)); }); } -function checkHashConversions(data: UInt8[]) { +function checkHashConversions(data: Bytes) { Provable.asProver(() => { - expectDigestToEqualHex(Hash.SHA224.hash(data)); - expectDigestToEqualHex(Hash.SHA256.hash(data)); - expectDigestToEqualHex(Hash.SHA384.hash(data)); - expectDigestToEqualHex(Hash.SHA512.hash(data)); + expectDigestToEqualHex(Hash.SHA3_256.hash(data)); + expectDigestToEqualHex(Hash.SHA3_384.hash(data)); + expectDigestToEqualHex(Hash.SHA3_512.hash(data)); expectDigestToEqualHex(Hash.Keccak256.hash(data)); }); } -function expectDigestToEqualHex(digest: UInt8[]) { - const hex = UInt8.toHex(digest); - expect(equals(digest, UInt8.fromHex(hex))).toBe(true); +function expectDigestToEqualHex(digest: Bytes) { + const hex = digest.toHex(); + expect(digest).toEqual(Bytes.fromHex(hex)); } -function equals(a: UInt8[], b: UInt8[]): boolean { +function equals(a: Bytes, b: Bytes): boolean { if (a.length !== b.length) return false; - for (let i = 0; i < a.length; i++) a[i].assertEquals(b[i]); - + for (let i = 0; i < a.length; i++) a.bytes[i].assertEquals(b.bytes[i]); return true; } @@ -246,24 +243,21 @@ function testExpected({ Provable.runAndCheck(() => { assert(message.length % 2 === 0); - let fields = UInt8.fromHex(message); - let expectedHash = UInt8.fromHex(expected); + let fields = Bytes.fromHex(message); + let expectedHash = Bytes.fromHex(expected); Provable.asProver(() => { if (nist) { - let hashed; + let hashed: Bytes; switch (length) { - case 224: - hashed = Hash.SHA224.hash(fields); - break; case 256: - hashed = Hash.SHA256.hash(fields); + hashed = Hash.SHA3_256.hash(fields); break; case 384: - hashed = Hash.SHA384.hash(fields); + hashed = Hash.SHA3_384.hash(fields); break; case 512: - hashed = Hash.SHA512.hash(fields); + hashed = Hash.SHA3_512.hash(fields); break; default: assert(false); diff --git a/src/lib/provable-types/bytes.ts b/src/lib/provable-types/bytes.ts index 8c0b08b28..ddbe6873a 100644 --- a/src/lib/provable-types/bytes.ts +++ b/src/lib/provable-types/bytes.ts @@ -73,8 +73,8 @@ class Bytes { /** * Convert {@link Bytes} to a hex string. */ - toHex(xs: Bytes): string { - return xs.bytes + toHex(): string { + return this.bytes .map((x) => x.toBigInt().toString(16).padStart(2, '0')) .join(''); } diff --git a/src/lib/provable-types/provable-types.ts b/src/lib/provable-types/provable-types.ts index 1d850aa3c..cd4c2ed2f 100644 --- a/src/lib/provable-types/provable-types.ts +++ b/src/lib/provable-types/provable-types.ts @@ -17,3 +17,4 @@ function Bytes(size: number) { return createBytes(size); } Bytes.from = InternalBytes.from; +Bytes.fromHex = InternalBytes.fromHex; From 1cb8b0be3fe56941dd054d01752a66909fbc4b1c Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 13 Dec 2023 18:02:53 +0100 Subject: [PATCH 78/90] add toFields and expose Bytes --- src/index.ts | 1 + src/lib/provable-types/bytes.ts | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/index.ts b/src/index.ts index 4a440bf14..b82033e1d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,6 +33,7 @@ export { export { Provable } from './lib/provable.js'; export { Circuit, Keypair, public_, circuitMain } from './lib/circuit.js'; export { UInt32, UInt64, Int64, Sign, UInt8 } from './lib/int.js'; +export { Bytes } from './lib/provable-types/provable-types.js'; export { Gadgets } from './lib/gadgets/gadgets.js'; export { Types } from './bindings/mina-transaction/types.js'; diff --git a/src/lib/provable-types/bytes.ts b/src/lib/provable-types/bytes.ts index ddbe6873a..992e10bd1 100644 --- a/src/lib/provable-types/bytes.ts +++ b/src/lib/provable-types/bytes.ts @@ -50,6 +50,10 @@ class Bytes { return Uint8Array.from(this.bytes.map((x) => x.toNumber())); } + toFields() { + return this.bytes.map((x) => x.value); + } + /** * Create {@link Bytes} from a string. * From 4729c8bb3e7ab232aee84789fa4eff6ce67fa8e1 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 13 Dec 2023 18:03:14 +0100 Subject: [PATCH 79/90] fix examples --- src/examples/crypto/ecdsa/ecdsa.ts | 47 ++++----------------- src/examples/crypto/ecdsa/run.ts | 4 +- src/examples/keccak.ts | 64 ----------------------------- src/examples/zkapps/hashing/hash.ts | 39 ++++++------------ src/examples/zkapps/hashing/run.ts | 23 ++--------- 5 files changed, 26 insertions(+), 151 deletions(-) delete mode 100644 src/examples/keccak.ts diff --git a/src/examples/crypto/ecdsa/ecdsa.ts b/src/examples/crypto/ecdsa/ecdsa.ts index cf2407df3..45639c41c 100644 --- a/src/examples/crypto/ecdsa/ecdsa.ts +++ b/src/examples/crypto/ecdsa/ecdsa.ts @@ -4,37 +4,34 @@ import { createEcdsa, createForeignCurve, Bool, - Struct, - Provable, - Field, Keccak, - Gadgets, + Bytes, } from 'o1js'; -export { keccakAndEcdsa, ecdsa, Secp256k1, Ecdsa, Message32 }; +export { keccakAndEcdsa, ecdsa, Secp256k1, Ecdsa, Bytes32 }; class Secp256k1 extends createForeignCurve(Crypto.CurveParams.Secp256k1) {} class Scalar extends Secp256k1.Scalar {} class Ecdsa extends createEcdsa(Secp256k1) {} -class Message32 extends Message(32) {} +class Bytes32 extends Bytes(32) {} const keccakAndEcdsa = ZkProgram({ name: 'ecdsa', - publicInput: Message32, + publicInput: Bytes32.provable, publicOutput: Bool, methods: { verifyEcdsa: { privateInputs: [Ecdsa.provable, Secp256k1.provable], - method(message: Message32, signature: Ecdsa, publicKey: Secp256k1) { - return signature.verify(message.array, publicKey); + method(message: Bytes32, signature: Ecdsa, publicKey: Secp256k1) { + return signature.verify(message, publicKey); }, }, sha3: { privateInputs: [], - method(message: Message32) { - Keccak.nistSha3(256, message.array); + method(message: Bytes32) { + Keccak.nistSha3(256, message); return Bool(true); }, }, @@ -55,31 +52,3 @@ const ecdsa = ZkProgram({ }, }, }); - -// helper: class for a message of n bytes - -function Message(lengthInBytes: number) { - return class Message extends Struct({ - array: Provable.Array(Field, lengthInBytes), - }) { - static from(message: string | Uint8Array) { - if (typeof message === 'string') { - message = new TextEncoder().encode(message); - } - let padded = new Uint8Array(32); - padded.set(message); - return new this({ array: [...padded].map(Field) }); - } - - toBytes() { - return Uint8Array.from(this.array.map((f) => Number(f))); - } - - /** - * Important: check that inputs are, in fact, bytes - */ - static check(msg: { array: Field[] }) { - msg.array.forEach(Gadgets.rangeCheck8); - } - }; -} diff --git a/src/examples/crypto/ecdsa/run.ts b/src/examples/crypto/ecdsa/run.ts index 377e18d50..2a497de37 100644 --- a/src/examples/crypto/ecdsa/run.ts +++ b/src/examples/crypto/ecdsa/run.ts @@ -1,4 +1,4 @@ -import { Secp256k1, Ecdsa, keccakAndEcdsa, Message32, ecdsa } from './ecdsa.js'; +import { Secp256k1, Ecdsa, keccakAndEcdsa, ecdsa, Bytes32 } from './ecdsa.js'; import assert from 'assert'; // create an example ecdsa signature @@ -6,7 +6,7 @@ import assert from 'assert'; let privateKey = Secp256k1.Scalar.random(); let publicKey = Secp256k1.generator.scale(privateKey); -let message = Message32.from("what's up"); +let message = Bytes32.fromString("what's up"); let signature = Ecdsa.sign(message.toBytes(), privateKey.toBigInt()); diff --git a/src/examples/keccak.ts b/src/examples/keccak.ts deleted file mode 100644 index 243b64996..000000000 --- a/src/examples/keccak.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { Field, Provable, Hash, UInt8 } from 'snarkyjs'; - -function equals(a: UInt8[], b: UInt8[]): boolean { - if (a.length !== b.length) return false; - for (let i = 0; i < a.length; i++) a[i].assertEquals(b[i]); - return true; -} - -function checkDigestHexConversion(digest: UInt8[]) { - console.log('Checking hex->digest, digest->hex matches'); - Provable.asProver(() => { - const hex = UInt8.toHex(digest); - const expected = UInt8.fromHex(hex); - if (equals(digest, expected)) { - console.log('✅ Digest matches'); - } else { - Provable.log(`hex: ${hex}\ndigest: ${digest}\nexpected:${expected}`); - console.log('❌ Digest does not match'); - } - }); -} - -console.log('Running SHA224 test'); -Provable.runAndCheck(() => { - let digest = Hash.SHA224.hash([new UInt8(1), new UInt8(2), new UInt8(3)]); - checkDigestHexConversion(digest); -}); - -console.log('Running SHA256 test'); -Provable.runAndCheck(() => { - let digest = Hash.SHA256.hash([new UInt8(1), new UInt8(2), new UInt8(3)]); - checkDigestHexConversion(digest); -}); - -console.log('Running SHA384 test'); -Provable.runAndCheck(() => { - let digest = Hash.SHA384.hash([new UInt8(1), new UInt8(2), new UInt8(3)]); - checkDigestHexConversion(digest); -}); - -// TODO: This test fails -console.log('Running SHA512 test'); -Provable.runAndCheck(() => { - let digest = Hash.SHA512.hash([new UInt8(1), new UInt8(2), new UInt8(3)]); - checkDigestHexConversion(digest); -}); - -console.log('Running keccak hash test'); -Provable.runAndCheck(() => { - let digest = Hash.Keccak256.hash([new UInt8(1), new UInt8(2), new UInt8(3)]); - checkDigestHexConversion(digest); -}); - -console.log('Running Poseidon test'); -Provable.runAndCheck(() => { - let digest = Hash.Poseidon.hash([Field(1), Field(2), Field(3)]); - Provable.log(digest); -}); - -console.log('Running default hash test'); -Provable.runAndCheck(() => { - let digest = Hash.hash([Field(1), Field(2), Field(3)]); - Provable.log(digest); -}); diff --git a/src/examples/zkapps/hashing/hash.ts b/src/examples/zkapps/hashing/hash.ts index e593afae3..9ad9947df 100644 --- a/src/examples/zkapps/hashing/hash.ts +++ b/src/examples/zkapps/hashing/hash.ts @@ -1,23 +1,16 @@ import { Hash, - UInt8, Field, SmartContract, state, State, method, Permissions, - Struct, - Provable, + Bytes, } from 'o1js'; let initialCommitment: Field = Field(0); -// 32 UInts -export class HashInput extends Struct({ - data: Provable.Array(UInt8, 32), -}) {} - export class HashStorage extends SmartContract { @state(Field) commitment = State(); @@ -30,33 +23,27 @@ export class HashStorage extends SmartContract { this.commitment.set(initialCommitment); } - @method SHA224(xs: HashInput) { - const shaHash = Hash.SHA224.hash(xs.data); - const commitment = Hash.hash(shaHash.map((f) => f.toField())); - this.commitment.set(commitment); - } - - @method SHA256(xs: HashInput) { - const shaHash = Hash.SHA256.hash(xs.data); - const commitment = Hash.hash(shaHash.map((f) => f.toField())); + @method SHA256(xs: Bytes) { + const shaHash = Hash.SHA3_256.hash(xs); + const commitment = Hash.hash(shaHash.toFields()); this.commitment.set(commitment); } - @method SHA384(xs: HashInput) { - const shaHash = Hash.SHA384.hash(xs.data); - const commitment = Hash.hash(shaHash.map((f) => f.toField())); + @method SHA384(xs: Bytes) { + const shaHash = Hash.SHA3_384.hash(xs); + const commitment = Hash.hash(shaHash.toFields()); this.commitment.set(commitment); } - @method SHA512(xs: HashInput) { - const shaHash = Hash.SHA512.hash(xs.data); - const commitment = Hash.hash(shaHash.map((f) => f.toField())); + @method SHA512(xs: Bytes) { + const shaHash = Hash.SHA3_512.hash(xs); + const commitment = Hash.hash(shaHash.toFields()); this.commitment.set(commitment); } - @method Keccak256(xs: HashInput) { - const shaHash = Hash.Keccak256.hash(xs.data); - const commitment = Hash.hash(shaHash.map((f) => f.toField())); + @method Keccak256(xs: Bytes) { + const shaHash = Hash.Keccak256.hash(xs); + const commitment = Hash.hash(shaHash.toFields()); this.commitment.set(commitment); } } diff --git a/src/examples/zkapps/hashing/run.ts b/src/examples/zkapps/hashing/run.ts index b413cd09d..9350211d6 100644 --- a/src/examples/zkapps/hashing/run.ts +++ b/src/examples/zkapps/hashing/run.ts @@ -1,9 +1,5 @@ -import { HashStorage, HashInput } from './hash.js'; -import { Mina, PrivateKey, AccountUpdate, UInt8 } from 'snarkyjs'; -import { getProfiler } from '../../profiler.js'; - -const HashProfier = getProfiler('Hash'); -HashProfier.start('Hash test flow'); +import { HashStorage } from './hash.js'; +import { Mina, PrivateKey, AccountUpdate, Bytes } from 'o1js'; let txn; let proofsEnabled = true; @@ -25,9 +21,7 @@ const zkAppAddress = zkAppPrivateKey.toPublicKey(); const zkAppInstance = new HashStorage(zkAppAddress); // 0, 1, 2, 3, ..., 32 -const hashData = new HashInput({ - data: Array.from({ length: 32 }, (_, i) => i).map((x) => UInt8.from(x)), -}); +const hashData = Bytes.from(Array.from({ length: 32 }, (_, i) => i)); console.log('Deploying Hash Example....'); txn = await Mina.transaction(feePayer.publicKey, () => { @@ -42,15 +36,6 @@ const initialState = let currentState; console.log('Initial State', initialState); -console.log(`Updating commitment from ${initialState} using SHA224 ...`); -txn = await Mina.transaction(feePayer.publicKey, () => { - zkAppInstance.SHA224(hashData); -}); -await txn.prove(); -await txn.sign([feePayer.privateKey]).send(); -currentState = Mina.getAccount(zkAppAddress).zkapp?.appState?.[0].toString(); -console.log(`Current state successfully updated to ${currentState}`); - console.log(`Updating commitment from ${initialState} using SHA256 ...`); txn = await Mina.transaction(feePayer.publicKey, () => { zkAppInstance.SHA256(hashData); @@ -86,5 +71,3 @@ await txn.prove(); await txn.sign([feePayer.privateKey]).send(); currentState = Mina.getAccount(zkAppAddress).zkapp?.appState?.[0].toString(); console.log(`Current state successfully updated to ${currentState}`); - -HashProfier.stop().store(); From 0769e6a8a32c39b2576bb4cfe0ca3aee9a9c1459 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 13 Dec 2023 18:09:08 +0100 Subject: [PATCH 80/90] fix cs example --- .../vk-regression/plain-constraint-system.ts | 34 ++++++------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/tests/vk-regression/plain-constraint-system.ts b/tests/vk-regression/plain-constraint-system.ts index 20bf05fa1..67176a9f7 100644 --- a/tests/vk-regression/plain-constraint-system.ts +++ b/tests/vk-regression/plain-constraint-system.ts @@ -1,4 +1,4 @@ -import { Field, Group, Gadgets, Provable, Scalar, Hash, UInt8 } from 'o1js'; +import { Field, Group, Gadgets, Provable, Scalar, Hash, Bytes } from 'o1js'; export { GroupCS, BitwiseCS, HashCS }; @@ -84,39 +84,27 @@ const BitwiseCS = constraintSystem('Bitwise Primitive', { }, }); -const HashCS = constraintSystem('Hashes', { - SHA224() { - let xs = Array.from({ length: 32 }, (_, i) => i).map((x) => - Provable.witness(UInt8, () => UInt8.from(x)) - ); - Hash.SHA224.hash(xs); - }, +const Bytes32 = Bytes(32); +const bytes32 = Bytes32.from([]); +const HashCS = constraintSystem('Hashes', { SHA256() { - let xs = Array.from({ length: 32 }, (_, i) => i).map((x) => - Provable.witness(UInt8, () => UInt8.from(x)) - ); - Hash.SHA256.hash(xs); + let xs = Provable.witness(Bytes32.provable, () => bytes32); + Hash.SHA3_256.hash(xs); }, SHA384() { - let xs = Array.from({ length: 32 }, (_, i) => i).map((x) => - Provable.witness(UInt8, () => UInt8.from(x)) - ); - Hash.SHA384.hash(xs); + let xs = Provable.witness(Bytes32.provable, () => bytes32); + Hash.SHA3_384.hash(xs); }, SHA512() { - let xs = Array.from({ length: 32 }, (_, i) => i).map((x) => - Provable.witness(UInt8, () => UInt8.from(x)) - ); - Hash.SHA512.hash(xs); + let xs = Provable.witness(Bytes32.provable, () => bytes32); + Hash.SHA3_512.hash(xs); }, Keccak256() { - let xs = Array.from({ length: 32 }, (_, i) => i).map((x) => - Provable.witness(UInt8, () => UInt8.from(x)) - ); + let xs = Provable.witness(Bytes32.provable, () => bytes32); Hash.Keccak256.hash(xs); }, From 231e87c10c32ef66c7b16735ce8e77f6a9c16321 Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 13 Dec 2023 18:09:32 +0100 Subject: [PATCH 81/90] vk regression --- tests/vk-regression/vk-regression.json | 29 ++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/vk-regression/vk-regression.json b/tests/vk-regression/vk-regression.json index 657960f92..1c3ae041b 100644 --- a/tests/vk-regression/vk-regression.json +++ b/tests/vk-regression/vk-regression.json @@ -202,6 +202,35 @@ "hash": "" } }, + "Hashes": { + "digest": "Hashes", + "methods": { + "SHA256": { + "rows": 14494, + "digest": "949539824d56622702d9ac048e8111e9" + }, + "SHA384": { + "rows": 14541, + "digest": "93dedf5824cab797d48e7a98c53c6bf3" + }, + "SHA512": { + "rows": 14588, + "digest": "3756008585b30a3951ed6455a7fbcdb0" + }, + "Keccak256": { + "rows": 14493, + "digest": "1ab08bd64002a0dd0a82f74df445de05" + }, + "Poseidon": { + "rows": 208, + "digest": "afa1f9920f1f657ab015c02f9b2f6c52" + } + }, + "verificationKey": { + "data": "", + "hash": "" + } + }, "ecdsa-only": { "digest": "2113edb508f10afee42dd48aec81ac7d06805d76225b0b97300501136486bb30", "methods": { From 12f0b2dd20029315311d01c4ac6b05f16e768d9c Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 13 Dec 2023 18:42:19 +0100 Subject: [PATCH 82/90] add toInput --- src/lib/int.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/lib/int.ts b/src/lib/int.ts index a4cc974c4..84a38a4b5 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -1291,6 +1291,10 @@ class UInt8 extends Struct({ Gadgets.rangeCheck8(x.value); } + static toInput(x: { value: Field }): HashInput { + return { packed: [[x.value, 8]] }; + } + /** * Turns a {@link UInt8} into a {@link UInt32}. */ From 7b305fbe5e09bf235034ba18b9983a393053bc47 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 14 Dec 2023 11:35:44 +0100 Subject: [PATCH 83/90] support bytes from string without constructing type of specific length --- src/lib/provable-types/provable-types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/provable-types/provable-types.ts b/src/lib/provable-types/provable-types.ts index cd4c2ed2f..ad11e446b 100644 --- a/src/lib/provable-types/provable-types.ts +++ b/src/lib/provable-types/provable-types.ts @@ -18,3 +18,4 @@ function Bytes(size: number) { } Bytes.from = InternalBytes.from; Bytes.fromHex = InternalBytes.fromHex; +Bytes.fromString = InternalBytes.fromString; From 9d444b9aec3f5556d7db837dab6b21a8f154dc63 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 14 Dec 2023 11:35:59 +0100 Subject: [PATCH 84/90] adapt keccak doccomments to bytes input --- src/lib/keccak.ts | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/src/lib/keccak.ts b/src/lib/keccak.ts index 76597dbe6..ac63ceecb 100644 --- a/src/lib/keccak.ts +++ b/src/lib/keccak.ts @@ -16,17 +16,17 @@ const Keccak = { * * Applies the SHA-3 hash function to a list of big-endian byte-sized {@link Field} elements, flexible to handle varying output lengths (256, 384, 512 bits) as specified. * - * The function accepts a list of byte-sized {@link Field} elements as its input. However, the input values should be range-checked externally before being passed to this function. This can be done using {@link Gadgets.rangeCheck8}. + * The function accepts {@link Bytes} as the input message, which is a type that represents a static-length list of byte-sized field elements (range-checked using {@link Gadgets.rangeCheck8}). + * Alternatively, you can pass plain `number[]` of `Uint8Array` to perform a hash outside provable code. * - * The output is ensured to conform to the chosen bit length and is a list of big-endian byte-sized {@link Field} elements, range-checked using {@link Gadgets.rangeCheck8}. + * Produces an output of {@link Bytes} that conforms to the chosen bit length. + * Both input and output bytes are big-endian. * * @param len - Desired output length in bits. Valid options: 256, 384, 512. - * @param message - Big-endian list of byte-sized {@link Field} elements representing the message to hash. - * - * _Note:_ This function does not perform internal range checking on the input, this can be done by using {@link Gadgets.rangeCheck8}. + * @param message - Big-endian {@link Bytes} representing the message to hash. * * ```ts - * let preimage = [5, 6, 19, 28, 19].map(Field); + * let preimage = Bytes.fromString("hello world"); * let digest256 = Keccak.nistSha3(256, preimage); * let digest384 = Keccak.nistSha3(384, preimage); * let digest512 = Keccak.nistSha3(512, preimage); @@ -42,17 +42,15 @@ const Keccak = { * * Primarily used in Ethereum for hashing transactions, messages, and other types of payloads. * - * The function expects an input as a list of big-endian byte-sized {@link Field} elements. However, the input should be range checked before calling this function, - * as this function does not perform internal range checking. This can be done using {@link Gadgets.rangeCheck8}. - * - * Produces an output which is a list of big-endian byte-sized {@link Field} elements and ensures output is within the specified range using {@link Gadgets.rangeCheck8}. + * The function accepts {@link Bytes} as the input message, which is a type that represents a static-length list of byte-sized field elements (range-checked using {@link Gadgets.rangeCheck8}). + * Alternatively, you can pass plain `number[]` of `Uint8Array` to perform a hash outside provable code. * - * @param message - Big-endian list of byte-sized {@link Field} elements representing the message to hash. + * Produces an output of {@link Bytes} of length 32. Both input and output bytes are big-endian. * - * _Note:_ This function does not perform internal range checking on the input, this can be done by using {@link Gadgets.rangeCheck8}. + * @param message - Big-endian {@link Bytes} representing the message to hash. * * ```ts - * let preimage = [5, 6, 19, 28, 19].map(Field); + * let preimage = Bytes.fromString("hello world"); * let digest = Keccak.ethereum(preimage); * ``` */ @@ -68,17 +66,17 @@ const Keccak = { * * The function applies the pre-NIST Keccak hash function to a list of byte-sized {@link Field} elements and is flexible to handle varying output lengths (256, 384, 512 bits) as specified. * - * {@link Keccak.preNist} accepts a list of big-endian byte-sized {@link Field} elements as its input. However, input values should be range-checked externally before being passed to this function. This can be done using {@link Gadgets.rangeCheck8}. + * {@link Keccak.preNist} accepts {@link Bytes} as the input message, which is a type that represents a static-length list of byte-sized field elements (range-checked using {@link Gadgets.rangeCheck8}). + * Alternatively, you can pass plain `number[]` of `Uint8Array` to perform a hash outside provable code. * - * The hash output is ensured to conform to the chosen bit length and is a list of big-endian byte-sized {@link Field} elements, range-checked using {@link Gadgets.rangeCheck8}. + * Produces an output of {@link Bytes} that conforms to the chosen bit length. + * Both input and output bytes are big-endian. * * @param len - Desired output length in bits. Valid options: 256, 384, 512. - * @param message - Big-endian list of byte-sized {@link Field} elements representing the message to hash. - * - * _Note:_ This function does not perform internal range checking on the input, this can be done by using {@link Gadgets.rangeCheck8}. + * @param message - Big-endian {@link Bytes} representing the message to hash. * * ```ts - * let preimage = [5, 6, 19, 28, 19].map(Field); + * let preimage = Bytes.fromString("hello world"); * let digest256 = Keccak.preNist(256, preimage); * let digest384 = Keccak.preNist(384, preimage); * let digest512= Keccak.preNist(512, preimage); From 3be3ac59d20f3746a48e3ddb7dc994bc78fd4d45 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 14 Dec 2023 12:04:12 +0100 Subject: [PATCH 85/90] Hash doccomments --- src/lib/hashes-combined.ts | 88 +++++++++++++++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/src/lib/hashes-combined.ts b/src/lib/hashes-combined.ts index 5608627e4..8440a25b3 100644 --- a/src/lib/hashes-combined.ts +++ b/src/lib/hashes-combined.ts @@ -4,36 +4,122 @@ import { Bytes } from './provable-types/provable-types.js'; export { Hash }; -// TODO do we want this? +/** + * A collection of hash functions which can be used in provable code. + */ const Hash = { + /** + * Hashes the given field elements using [Poseidon](https://eprint.iacr.org/2019/458.pdf). Alias for `Poseidon.hash()`. + * + * ```ts + * let hash = Hash.hash([a, b, c]); + * ``` + * + * **Important:** This is by far the most efficient hash function o1js has available in provable code. + * Use it by default, if no compatibility concerns require you to use a different hash function. + * + * The Poseidon implementation operates over the native [Pallas base field](https://electriccoin.co/blog/the-pasta-curves-for-halo-2-and-beyond/) + * and uses parameters generated specifically for the [Mina](https://minaprotocol.com) blockchain. + * + * We use a `rate` of 2, which means that 2 field elements are hashed per permutation. + * A permutation causes 11 rows in the constraint system. + * + * You can find the full set of Poseidon parameters [here](https://github.com/o1-labs/o1js-bindings/blob/main/crypto/constants.ts). + */ hash: Poseidon.hash, + + /** + * The [Poseidon](https://eprint.iacr.org/2019/458.pdf) hash function. + * + * See {@link Hash.hash} for details and usage examples. + */ Poseidon, + + /** + * The SHA3 hash function with an output length of 256 bits. + */ SHA3_256: { + /** + * Hashes the given bytes using SHA3-256. + * + * This is an alias for `Keccak.nistSha3(256, bytes)`.\ + * See {@link Keccak.nistSha3} for details and usage examples. + */ hash(bytes: Bytes) { return Keccak.nistSha3(256, bytes); }, }, + + /** + * The SHA3 hash function with an output length of 384 bits. + */ SHA3_384: { + /** + * Hashes the given bytes using SHA3-384. + * + * This is an alias for `Keccak.nistSha3(384, bytes)`.\ + * See {@link Keccak.nistSha3} for details and usage examples. + */ hash(bytes: Bytes) { return Keccak.nistSha3(384, bytes); }, }, + + /** + * The SHA3 hash function with an output length of 512 bits. + */ SHA3_512: { + /** + * Hashes the given bytes using SHA3-512. + * + * This is an alias for `Keccak.nistSha3(512, bytes)`.\ + * See {@link Keccak.nistSha3} for details and usage examples. + */ hash(bytes: Bytes) { return Keccak.nistSha3(512, bytes); }, }, + + /** + * The pre-NIST Keccak hash function with an output length of 256 bits. + */ Keccak256: { + /** + * Hashes the given bytes using Keccak-256. + * + * This is an alias for `Keccak.preNist(256, bytes)`.\ + * See {@link Keccak.preNist} for details and usage examples. + */ hash(bytes: Bytes) { return Keccak.preNist(256, bytes); }, }, + + /** + * The pre-NIST Keccak hash function with an output length of 384 bits. + */ Keccak384: { + /** + * Hashes the given bytes using Keccak-384. + * + * This is an alias for `Keccak.preNist(384, bytes)`.\ + * See {@link Keccak.preNist} for details and usage examples. + */ hash(bytes: Bytes) { return Keccak.preNist(384, bytes); }, }, + + /** + * The pre-NIST Keccak hash function with an output length of 512 bits. + */ Keccak512: { + /** + * Hashes the given bytes using Keccak-512. + * + * This is an alias for `Keccak.preNist(512, bytes)`.\ + * See {@link Keccak.preNist} for details and usage examples. + */ hash(bytes: Bytes) { return Keccak.preNist(512, bytes); }, From 4ad851e79143175c85c46d9c8057a21c4d65f085 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 14 Dec 2023 12:14:42 +0100 Subject: [PATCH 86/90] don't double-constrain uint8 results --- src/lib/int.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/lib/int.ts b/src/lib/int.ts index 84a38a4b5..4daf746f4 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -984,6 +984,12 @@ class UInt8 extends Struct({ UInt8.checkConstant(this.value); } + static Unsafe = { + fromField(x: Field) { + return new UInt8(x.value); + }, + }; + /** * Add a {@link UInt8} to another {@link UInt8} without allowing overflow. * @@ -999,7 +1005,7 @@ class UInt8 extends Struct({ add(y: UInt8 | bigint | number) { let z = this.value.add(UInt8.from(y).value); Gadgets.rangeCheck8(z); - return UInt8.from(z); + return UInt8.Unsafe.fromField(z); } /** @@ -1017,7 +1023,7 @@ class UInt8 extends Struct({ sub(y: UInt8 | bigint | number) { let z = this.value.sub(UInt8.from(y).value); Gadgets.rangeCheck8(z); - return UInt8.from(z); + return UInt8.Unsafe.fromField(z); } /** @@ -1035,7 +1041,7 @@ class UInt8 extends Struct({ mul(y: UInt8 | bigint | number) { let z = this.value.mul(UInt8.from(y).value); Gadgets.rangeCheck8(z); - return UInt8.from(z); + return UInt8.Unsafe.fromField(z); } /** @@ -1097,8 +1103,8 @@ class UInt8 extends Struct({ Gadgets.rangeCheck16(q); Gadgets.rangeCheck16(r); - let remainder = UInt8.from(r); - let quotient = UInt8.from(q); + let remainder = UInt8.Unsafe.fromField(r); + let quotient = UInt8.Unsafe.fromField(q); remainder.assertLessThan(y); return { quotient, remainder }; @@ -1331,8 +1337,7 @@ class UInt8 extends Struct({ } private static checkConstant(x: Field) { - if (!x.isConstant()) return x.value; + if (!x.isConstant()) return; Gadgets.rangeCheck8(x); - return x.value; } } From 7ac389a9dff77a20a7041a5841ae0562e7464314 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 14 Dec 2023 12:20:49 +0100 Subject: [PATCH 87/90] improve uint8 docs --- src/lib/int.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/lib/int.ts b/src/lib/int.ts index 4daf746f4..13c255310 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -973,18 +973,24 @@ class UInt8 extends Struct({ static NUM_BITS = 8; /** - * Coerce anything "field-like" (bigint, number, string, and {@link Field}) to a {@link UInt8}. + * Create a {@link UInt8} from a bigint or number. * The max value of a {@link UInt8} is `2^8 - 1 = 255`. * * **Warning**: Cannot overflow past 255, an error is thrown if the result is greater than 255. */ - constructor(x: number | bigint | string | FieldVar | UInt8) { + constructor(x: number | bigint | FieldVar | UInt8) { if (x instanceof UInt8) x = x.value.value; super({ value: Field(x) }); UInt8.checkConstant(this.value); } static Unsafe = { + /** + * Create a {@link UInt8} from a {@link Field} without constraining its range. + * + * **Warning**: This is unsafe, because it does not prove that the input {@link Field} actually fits in 8 bits.\ + * Only use this if you know what you are doing, otherwise use the safe {@link UInt8.from}. + */ fromField(x: Field) { return new UInt8(x.value); }, From accc779542c3c7211bee63fed77730ddbb192595 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 14 Dec 2023 12:30:11 +0100 Subject: [PATCH 88/90] fix int test --- src/lib/int.test.ts | 355 +++++++++++++++++++------------------------- 1 file changed, 154 insertions(+), 201 deletions(-) diff --git a/src/lib/int.test.ts b/src/lib/int.test.ts index 7fd38e496..1914a9fed 100644 --- a/src/lib/int.test.ts +++ b/src/lib/int.test.ts @@ -2146,9 +2146,9 @@ describe('int', () => { it('1+1=2', () => { expect(() => { Provable.runAndCheck(() => { - const x = Provable.witness(UInt8, () => new UInt8(Field(1))); - const y = Provable.witness(UInt8, () => new UInt8(Field(1))); - x.add(y).assertEquals(new UInt8(Field(2))); + const x = Provable.witness(UInt8, () => new UInt8(1)); + const y = Provable.witness(UInt8, () => new UInt8(1)); + x.add(y).assertEquals(2); }); }).not.toThrow(); }); @@ -2156,15 +2156,15 @@ describe('int', () => { it('100+100=200', () => { expect(() => { Provable.runAndCheck(() => { - const x = Provable.witness(UInt8, () => new UInt8(Field(100))); - const y = Provable.witness(UInt8, () => new UInt8(Field(100))); - x.add(y).assertEquals(new UInt8(Field(200))); + const x = Provable.witness(UInt8, () => new UInt8(100)); + const y = Provable.witness(UInt8, () => new UInt8(100)); + x.add(y).assertEquals(new UInt8(200)); }); }).not.toThrow(); }); it('(MAXINT/2+MAXINT/2) adds to MAXINT', () => { - const n = Field((((1n << 8n) - 2n) / 2n).toString()); + const n = ((1n << 8n) - 2n) / 2n; expect(() => { Provable.runAndCheck(() => { const x = Provable.witness(UInt8, () => new UInt8(n)); @@ -2178,7 +2178,7 @@ describe('int', () => { expect(() => { Provable.runAndCheck(() => { const x = Provable.witness(UInt8, () => UInt8.MAXINT()); - const y = Provable.witness(UInt8, () => new UInt8(Field(1))); + const y = Provable.witness(UInt8, () => new UInt8(1)); x.add(y); }); }).toThrow(); @@ -2189,9 +2189,9 @@ describe('int', () => { it('1-1=0', () => { expect(() => { Provable.runAndCheck(() => { - const x = Provable.witness(UInt8, () => new UInt8(Field(1))); - const y = Provable.witness(UInt8, () => new UInt8(Field(1))); - x.sub(y).assertEquals(new UInt8(Field(0))); + const x = Provable.witness(UInt8, () => new UInt8(1)); + const y = Provable.witness(UInt8, () => new UInt8(1)); + x.sub(y).assertEquals(new UInt8(0)); }); }).not.toThrow(); }); @@ -2199,9 +2199,9 @@ describe('int', () => { it('100-50=50', () => { expect(() => { Provable.runAndCheck(() => { - const x = Provable.witness(UInt8, () => new UInt8(Field(100))); - const y = Provable.witness(UInt8, () => new UInt8(Field(50))); - x.sub(y).assertEquals(new UInt8(Field(50))); + const x = Provable.witness(UInt8, () => new UInt8(100)); + const y = Provable.witness(UInt8, () => new UInt8(50)); + x.sub(y).assertEquals(new UInt8(50)); }); }).not.toThrow(); }); @@ -2209,8 +2209,8 @@ describe('int', () => { it('should throw on sub if results in negative number', () => { expect(() => { Provable.runAndCheck(() => { - const x = Provable.witness(UInt8, () => new UInt8(Field(0))); - const y = Provable.witness(UInt8, () => new UInt8(Field(1))); + const x = Provable.witness(UInt8, () => new UInt8(0)); + const y = Provable.witness(UInt8, () => new UInt8(1)); x.sub(y); }); }).toThrow(); @@ -2221,9 +2221,9 @@ describe('int', () => { it('1x2=2', () => { expect(() => { Provable.runAndCheck(() => { - const x = Provable.witness(UInt8, () => new UInt8(Field(1))); - const y = Provable.witness(UInt8, () => new UInt8(Field(2))); - x.mul(y).assertEquals(new UInt8(Field(2))); + const x = Provable.witness(UInt8, () => new UInt8(1)); + const y = Provable.witness(UInt8, () => new UInt8(2)); + x.mul(y).assertEquals(new UInt8(2)); }); }).not.toThrow(); }); @@ -2231,9 +2231,9 @@ describe('int', () => { it('1x0=0', () => { expect(() => { Provable.runAndCheck(() => { - const x = Provable.witness(UInt8, () => new UInt8(Field(1))); - const y = Provable.witness(UInt8, () => new UInt8(Field(0))); - x.mul(y).assertEquals(new UInt8(Field(0))); + const x = Provable.witness(UInt8, () => new UInt8(1)); + const y = Provable.witness(UInt8, () => new UInt8(0)); + x.mul(y).assertEquals(new UInt8(0)); }); }).not.toThrow(); }); @@ -2241,9 +2241,9 @@ describe('int', () => { it('12x20=240', () => { expect(() => { Provable.runAndCheck(() => { - const x = Provable.witness(UInt8, () => new UInt8(Field(12))); - const y = Provable.witness(UInt8, () => new UInt8(Field(20))); - x.mul(y).assertEquals(new UInt8(Field(240))); + const x = Provable.witness(UInt8, () => new UInt8(12)); + const y = Provable.witness(UInt8, () => new UInt8(20)); + x.mul(y).assertEquals(new UInt8(240)); }); }).not.toThrow(); }); @@ -2252,7 +2252,7 @@ describe('int', () => { expect(() => { Provable.runAndCheck(() => { const x = Provable.witness(UInt8, () => UInt8.MAXINT()); - const y = Provable.witness(UInt8, () => new UInt8(Field(1))); + const y = Provable.witness(UInt8, () => new UInt8(1)); x.mul(y).assertEquals(UInt8.MAXINT()); }); }).not.toThrow(); @@ -2262,7 +2262,7 @@ describe('int', () => { expect(() => { Provable.runAndCheck(() => { const x = Provable.witness(UInt8, () => UInt8.MAXINT()); - const y = Provable.witness(UInt8, () => new UInt8(Field(2))); + const y = Provable.witness(UInt8, () => new UInt8(2)); x.mul(y); }); }).toThrow(); @@ -2273,9 +2273,9 @@ describe('int', () => { it('2/1=2', () => { expect(() => { Provable.runAndCheck(() => { - const x = Provable.witness(UInt8, () => new UInt8(Field(2))); - const y = Provable.witness(UInt8, () => new UInt8(Field(1))); - x.div(y).assertEquals(new UInt8(Field(2))); + const x = Provable.witness(UInt8, () => new UInt8(2)); + const y = Provable.witness(UInt8, () => new UInt8(1)); + x.div(y).assertEquals(new UInt8(2)); }); }).not.toThrow(); }); @@ -2283,9 +2283,9 @@ describe('int', () => { it('0/1=0', () => { expect(() => { Provable.runAndCheck(() => { - const x = Provable.witness(UInt8, () => new UInt8(Field(0))); - const y = Provable.witness(UInt8, () => new UInt8(Field(1))); - x.div(y).assertEquals(new UInt8(Field(0))); + const x = Provable.witness(UInt8, () => new UInt8(0)); + const y = Provable.witness(UInt8, () => new UInt8(1)); + x.div(y).assertEquals(new UInt8(0)); }); }).not.toThrow(); }); @@ -2293,9 +2293,9 @@ describe('int', () => { it('20/10=2', () => { expect(() => { Provable.runAndCheck(() => { - const x = Provable.witness(UInt8, () => new UInt8(Field(20))); - const y = Provable.witness(UInt8, () => new UInt8(Field(10))); - x.div(y).assertEquals(new UInt8(Field(2))); + const x = Provable.witness(UInt8, () => new UInt8(20)); + const y = Provable.witness(UInt8, () => new UInt8(10)); + x.div(y).assertEquals(new UInt8(2)); }); }).not.toThrow(); }); @@ -2304,7 +2304,7 @@ describe('int', () => { expect(() => { Provable.runAndCheck(() => { const x = Provable.witness(UInt8, () => UInt8.MAXINT()); - const y = Provable.witness(UInt8, () => new UInt8(Field(1))); + const y = Provable.witness(UInt8, () => new UInt8(1)); x.div(y).assertEquals(UInt8.MAXINT()); }); }).not.toThrow(); @@ -2314,7 +2314,7 @@ describe('int', () => { expect(() => { Provable.runAndCheck(() => { const x = Provable.witness(UInt8, () => UInt8.MAXINT()); - const y = Provable.witness(UInt8, () => new UInt8(Field(0))); + const y = Provable.witness(UInt8, () => new UInt8(0)); x.div(y); }); }).toThrow(); @@ -2325,9 +2325,9 @@ describe('int', () => { it('1%1=0', () => { expect(() => { Provable.runAndCheck(() => { - const x = Provable.witness(UInt8, () => new UInt8(Field(1))); - const y = Provable.witness(UInt8, () => new UInt8(Field(1))); - x.mod(y).assertEquals(new UInt8(Field(0))); + const x = Provable.witness(UInt8, () => new UInt8(1)); + const y = Provable.witness(UInt8, () => new UInt8(1)); + x.mod(y).assertEquals(new UInt8(0)); }); }).not.toThrow(); }); @@ -2335,9 +2335,9 @@ describe('int', () => { it('50%32=18', () => { expect(() => { Provable.runAndCheck(() => { - const x = Provable.witness(UInt8, () => new UInt8(Field(50))); - const y = Provable.witness(UInt8, () => new UInt8(Field(32))); - x.mod(y).assertEquals(new UInt8(Field(18))); + const x = Provable.witness(UInt8, () => new UInt8(50)); + const y = Provable.witness(UInt8, () => new UInt8(32)); + x.mod(y).assertEquals(new UInt8(18)); }); }).not.toThrow(); }); @@ -2346,8 +2346,8 @@ describe('int', () => { expect(() => { Provable.runAndCheck(() => { const x = Provable.witness(UInt8, () => UInt8.MAXINT()); - const y = Provable.witness(UInt8, () => new UInt8(Field(7))); - x.mod(y).assertEquals(new UInt8(Field(3))); + const y = Provable.witness(UInt8, () => new UInt8(7)); + x.mod(y).assertEquals(new UInt8(3)); }); }).not.toThrow(); }); @@ -2356,8 +2356,8 @@ describe('int', () => { expect(() => { Provable.runAndCheck(() => { const x = Provable.witness(UInt8, () => UInt8.MAXINT()); - const y = Provable.witness(UInt8, () => new UInt8(Field(0))); - x.mod(y).assertEquals(new UInt8(Field(1))); + const y = Provable.witness(UInt8, () => new UInt8(0)); + x.mod(y).assertEquals(new UInt8(1)); }); }).toThrow(); }); @@ -2367,8 +2367,8 @@ describe('int', () => { it('1<2=true', () => { expect(() => { Provable.runAndCheck(() => { - const x = Provable.witness(UInt8, () => new UInt8(Field(1))); - const y = Provable.witness(UInt8, () => new UInt8(Field(2))); + const x = Provable.witness(UInt8, () => new UInt8(1)); + const y = Provable.witness(UInt8, () => new UInt8(2)); x.assertLessThan(y); }); }).not.toThrow(); @@ -2377,8 +2377,8 @@ describe('int', () => { it('1<1=false', () => { expect(() => { Provable.runAndCheck(() => { - const x = Provable.witness(UInt8, () => new UInt8(Field(1))); - const y = Provable.witness(UInt8, () => new UInt8(Field(1))); + const x = Provable.witness(UInt8, () => new UInt8(1)); + const y = Provable.witness(UInt8, () => new UInt8(1)); x.assertLessThan(y); }); }).toThrow(); @@ -2387,8 +2387,8 @@ describe('int', () => { it('2<1=false', () => { expect(() => { Provable.runAndCheck(() => { - const x = Provable.witness(UInt8, () => new UInt8(Field(2))); - const y = Provable.witness(UInt8, () => new UInt8(Field(1))); + const x = Provable.witness(UInt8, () => new UInt8(2)); + const y = Provable.witness(UInt8, () => new UInt8(1)); x.assertLessThan(y); }); }).toThrow(); @@ -2397,8 +2397,8 @@ describe('int', () => { it('10<100=true', () => { expect(() => { Provable.runAndCheck(() => { - const x = Provable.witness(UInt8, () => new UInt8(Field(10))); - const y = Provable.witness(UInt8, () => new UInt8(Field(100))); + const x = Provable.witness(UInt8, () => new UInt8(10)); + const y = Provable.witness(UInt8, () => new UInt8(100)); x.assertLessThan(y); }); }).not.toThrow(); @@ -2407,8 +2407,8 @@ describe('int', () => { it('100<10=false', () => { expect(() => { Provable.runAndCheck(() => { - const x = Provable.witness(UInt8, () => new UInt8(Field(100))); - const y = Provable.witness(UInt8, () => new UInt8(Field(10))); + const x = Provable.witness(UInt8, () => new UInt8(100)); + const y = Provable.witness(UInt8, () => new UInt8(10)); x.assertLessThan(y); }); }).toThrow(); @@ -2429,8 +2429,8 @@ describe('int', () => { it('1<=1=true', () => { expect(() => { Provable.runAndCheck(() => { - const x = Provable.witness(UInt8, () => new UInt8(Field(1))); - const y = Provable.witness(UInt8, () => new UInt8(Field(1))); + const x = Provable.witness(UInt8, () => new UInt8(1)); + const y = Provable.witness(UInt8, () => new UInt8(1)); x.assertLessThanOrEqual(y); }); }).not.toThrow(); @@ -2439,8 +2439,8 @@ describe('int', () => { it('2<=1=false', () => { expect(() => { Provable.runAndCheck(() => { - const x = Provable.witness(UInt8, () => new UInt8(Field(2))); - const y = Provable.witness(UInt8, () => new UInt8(Field(1))); + const x = Provable.witness(UInt8, () => new UInt8(2)); + const y = Provable.witness(UInt8, () => new UInt8(1)); x.assertLessThanOrEqual(y); }); }).toThrow(); @@ -2449,8 +2449,8 @@ describe('int', () => { it('10<=100=true', () => { expect(() => { Provable.runAndCheck(() => { - const x = Provable.witness(UInt8, () => new UInt8(Field(10))); - const y = Provable.witness(UInt8, () => new UInt8(Field(100))); + const x = Provable.witness(UInt8, () => new UInt8(10)); + const y = Provable.witness(UInt8, () => new UInt8(100)); x.assertLessThanOrEqual(y); }); }).not.toThrow(); @@ -2459,8 +2459,8 @@ describe('int', () => { it('100<=10=false', () => { expect(() => { Provable.runAndCheck(() => { - const x = Provable.witness(UInt8, () => new UInt8(Field(100))); - const y = Provable.witness(UInt8, () => new UInt8(Field(10))); + const x = Provable.witness(UInt8, () => new UInt8(100)); + const y = Provable.witness(UInt8, () => new UInt8(10)); x.assertLessThanOrEqual(y); }); }).toThrow(); @@ -2481,8 +2481,8 @@ describe('int', () => { it('2>1=true', () => { expect(() => { Provable.runAndCheck(() => { - const x = Provable.witness(UInt8, () => new UInt8(Field(2))); - const y = Provable.witness(UInt8, () => new UInt8(Field(1))); + const x = Provable.witness(UInt8, () => new UInt8(2)); + const y = Provable.witness(UInt8, () => new UInt8(1)); x.assertGreaterThan(y); }); }).not.toThrow(); @@ -2491,8 +2491,8 @@ describe('int', () => { it('1>1=false', () => { expect(() => { Provable.runAndCheck(() => { - const x = Provable.witness(UInt8, () => new UInt8(Field(1))); - const y = Provable.witness(UInt8, () => new UInt8(Field(1))); + const x = Provable.witness(UInt8, () => new UInt8(1)); + const y = Provable.witness(UInt8, () => new UInt8(1)); x.assertGreaterThan(y); }); }).toThrow(); @@ -2501,8 +2501,8 @@ describe('int', () => { it('1>2=false', () => { expect(() => { Provable.runAndCheck(() => { - const x = Provable.witness(UInt8, () => new UInt8(Field(1))); - const y = Provable.witness(UInt8, () => new UInt8(Field(2))); + const x = Provable.witness(UInt8, () => new UInt8(1)); + const y = Provable.witness(UInt8, () => new UInt8(2)); x.assertGreaterThan(y); }); }).toThrow(); @@ -2511,8 +2511,8 @@ describe('int', () => { it('100>10=true', () => { expect(() => { Provable.runAndCheck(() => { - const x = Provable.witness(UInt8, () => new UInt8(Field(100))); - const y = Provable.witness(UInt8, () => new UInt8(Field(10))); + const x = Provable.witness(UInt8, () => new UInt8(100)); + const y = Provable.witness(UInt8, () => new UInt8(10)); x.assertGreaterThan(y); }); }).not.toThrow(); @@ -2521,8 +2521,8 @@ describe('int', () => { it('10>100=false', () => { expect(() => { Provable.runAndCheck(() => { - const x = Provable.witness(UInt8, () => new UInt8(Field(1000))); - const y = Provable.witness(UInt8, () => new UInt8(Field(100000))); + const x = Provable.witness(UInt8, () => new UInt8(1000)); + const y = Provable.witness(UInt8, () => new UInt8(100000)); x.assertGreaterThan(y); }); }).toThrow(); @@ -2543,8 +2543,8 @@ describe('int', () => { it('1<=1=true', () => { expect(() => { Provable.runAndCheck(() => { - const x = Provable.witness(UInt8, () => new UInt8(Field(1))); - const y = Provable.witness(UInt8, () => new UInt8(Field(1))); + const x = Provable.witness(UInt8, () => new UInt8(1)); + const y = Provable.witness(UInt8, () => new UInt8(1)); x.assertGreaterThanOrEqual(y); }); }).not.toThrow(); @@ -2553,8 +2553,8 @@ describe('int', () => { it('1>=2=false', () => { expect(() => { Provable.runAndCheck(() => { - const x = Provable.witness(UInt8, () => new UInt8(Field(1))); - const y = Provable.witness(UInt8, () => new UInt8(Field(2))); + const x = Provable.witness(UInt8, () => new UInt8(1)); + const y = Provable.witness(UInt8, () => new UInt8(2)); x.assertGreaterThanOrEqual(y); }); }).toThrow(); @@ -2563,8 +2563,8 @@ describe('int', () => { it('100>=10=true', () => { expect(() => { Provable.runAndCheck(() => { - const x = Provable.witness(UInt8, () => new UInt8(Field(100))); - const y = Provable.witness(UInt8, () => new UInt8(Field(10))); + const x = Provable.witness(UInt8, () => new UInt8(100)); + const y = Provable.witness(UInt8, () => new UInt8(10)); x.assertGreaterThanOrEqual(y); }); }).not.toThrow(); @@ -2573,8 +2573,8 @@ describe('int', () => { it('10>=100=false', () => { expect(() => { Provable.runAndCheck(() => { - const x = Provable.witness(UInt8, () => new UInt8(Field(10))); - const y = Provable.witness(UInt8, () => new UInt8(Field(100))); + const x = Provable.witness(UInt8, () => new UInt8(10)); + const y = Provable.witness(UInt8, () => new UInt8(100)); x.assertGreaterThanOrEqual(y); }); }).toThrow(); @@ -2597,18 +2597,7 @@ describe('int', () => { expect(() => { Provable.runAndCheck(() => { const x = Provable.witness(UInt8, () => UInt8.from(1)); - const y = Provable.witness(UInt8, () => new UInt8(Field(1))); - x.assertEquals(y); - }); - }).not.toThrow(); - }); - }); - describe('fromString()', () => { - it('should be the same as Field(1)', () => { - expect(() => { - Provable.runAndCheck(() => { - const x = Provable.witness(UInt8, () => UInt8.from('1')); - const y = Provable.witness(UInt8, () => new UInt8(Field(1))); + const y = Provable.witness(UInt8, () => new UInt8(1)); x.assertEquals(y); }); }).not.toThrow(); @@ -2620,20 +2609,17 @@ describe('int', () => { describe('Outside of circuit', () => { describe('add', () => { it('1+1=2', () => { - expect(new UInt8(Field(1)).add(1).toString()).toEqual('2'); + expect(new UInt8(1).add(1).toString()).toEqual('2'); }); it('50+50=100', () => { - expect(new UInt8(Field(50)).add(50).toString()).toEqual('100'); + expect(new UInt8(50).add(50).toString()).toEqual('100'); }); it('(MAXINT/2+MAXINT/2) adds to MAXINT', () => { - const value = Field((((1n << 8n) - 2n) / 2n).toString()); + const value = ((1n << 8n) - 2n) / 2n; expect( - new UInt8(value) - .add(new UInt8(value)) - .add(new UInt8(Field(1))) - .toString() + new UInt8(value).add(new UInt8(value)).add(new UInt8(1)).toString() ).toEqual(UInt8.MAXINT().toString()); }); @@ -2646,11 +2632,11 @@ describe('int', () => { describe('sub', () => { it('1-1=0', () => { - expect(new UInt8(Field(1)).sub(1).toString()).toEqual('0'); + expect(new UInt8(1).sub(1).toString()).toEqual('0'); }); it('100-50=50', () => { - expect(new UInt8(Field(100)).sub(50).toString()).toEqual('50'); + expect(new UInt8(100).sub(50).toString()).toEqual('50'); }); it('should throw on sub if results in negative number', () => { @@ -2662,15 +2648,15 @@ describe('int', () => { describe('mul', () => { it('1x2=2', () => { - expect(new UInt8(Field(1)).mul(2).toString()).toEqual('2'); + expect(new UInt8(1).mul(2).toString()).toEqual('2'); }); it('1x0=0', () => { - expect(new UInt8(Field(1)).mul(0).toString()).toEqual('0'); + expect(new UInt8(1).mul(0).toString()).toEqual('0'); }); it('12x20=240', () => { - expect(new UInt8(Field(12)).mul(20).toString()).toEqual('240'); + expect(new UInt8(12).mul(20).toString()).toEqual('240'); }); it('MAXINTx1=MAXINT', () => { @@ -2688,15 +2674,15 @@ describe('int', () => { describe('div', () => { it('2/1=2', () => { - expect(new UInt8(Field(2)).div(1).toString()).toEqual('2'); + expect(new UInt8(2).div(1).toString()).toEqual('2'); }); it('0/1=0', () => { - expect(new UInt32(Field(0)).div(1).toString()).toEqual('0'); + expect(new UInt8(0).div(1).toString()).toEqual('0'); }); it('20/10=2', () => { - expect(new UInt8(Field(20)).div(10).toString()).toEqual('2'); + expect(new UInt8(20).div(10).toString()).toEqual('2'); }); it('MAXINT/1=MAXINT', () => { @@ -2714,11 +2700,11 @@ describe('int', () => { describe('mod', () => { it('1%1=0', () => { - expect(new UInt8(Field(1)).mod(1).toString()).toEqual('0'); + expect(new UInt8(1).mod(1).toString()).toEqual('0'); }); it('50%32=18', () => { - expect(new UInt8(Field(50)).mod(32).toString()).toEqual('18'); + expect(new UInt8(50).mod(32).toString()).toEqual('18'); }); it('MAXINT%7=3', () => { @@ -2734,33 +2720,23 @@ describe('int', () => { describe('lessThan', () => { it('1<2=true', () => { - expect(new UInt8(Field(1)).lessThan(new UInt8(Field(2)))).toEqual( - Bool(true) - ); + expect(new UInt8(1).lessThan(new UInt8(2))).toEqual(Bool(true)); }); it('1<1=false', () => { - expect(new UInt8(Field(1)).lessThan(new UInt8(Field(1)))).toEqual( - Bool(false) - ); + expect(new UInt8(1).lessThan(new UInt8(1))).toEqual(Bool(false)); }); it('2<1=false', () => { - expect(new UInt8(Field(2)).lessThan(new UInt8(Field(1)))).toEqual( - Bool(false) - ); + expect(new UInt8(2).lessThan(new UInt8(1))).toEqual(Bool(false)); }); it('10<100=true', () => { - expect(new UInt8(Field(10)).lessThan(new UInt8(Field(100)))).toEqual( - Bool(true) - ); + expect(new UInt8(10).lessThan(new UInt8(100))).toEqual(Bool(true)); }); it('100<10=false', () => { - expect(new UInt8(Field(100)).lessThan(new UInt8(Field(10)))).toEqual( - Bool(false) - ); + expect(new UInt8(100).lessThan(new UInt8(10))).toEqual(Bool(false)); }); it('MAXINT { @@ -2770,27 +2746,27 @@ describe('int', () => { describe('lessThanOrEqual', () => { it('1<=1=true', () => { - expect( - new UInt8(Field(1)).lessThanOrEqual(new UInt8(Field(1))) - ).toEqual(Bool(true)); + expect(new UInt8(1).lessThanOrEqual(new UInt8(1))).toEqual( + Bool(true) + ); }); it('2<=1=false', () => { - expect( - new UInt8(Field(2)).lessThanOrEqual(new UInt8(Field(1))) - ).toEqual(Bool(false)); + expect(new UInt8(2).lessThanOrEqual(new UInt8(1))).toEqual( + Bool(false) + ); }); it('10<=100=true', () => { - expect( - new UInt8(Field(10)).lessThanOrEqual(new UInt8(Field(100))) - ).toEqual(Bool(true)); + expect(new UInt8(10).lessThanOrEqual(new UInt8(100))).toEqual( + Bool(true) + ); }); it('100<=10=false', () => { - expect( - new UInt8(Field(100)).lessThanOrEqual(new UInt8(Field(10))) - ).toEqual(Bool(false)); + expect(new UInt8(100).lessThanOrEqual(new UInt8(10))).toEqual( + Bool(false) + ); }); it('MAXINT<=MAXINT=true', () => { @@ -2803,25 +2779,25 @@ describe('int', () => { describe('assertLessThanOrEqual', () => { it('1<=1=true', () => { expect(() => { - new UInt8(Field(1)).assertLessThanOrEqual(new UInt8(Field(1))); + new UInt8(1).assertLessThanOrEqual(new UInt8(1)); }).not.toThrow(); }); it('2<=1=false', () => { expect(() => { - new UInt8(Field(2)).assertLessThanOrEqual(new UInt8(Field(1))); + new UInt8(2).assertLessThanOrEqual(new UInt8(1)); }).toThrow(); }); it('10<=100=true', () => { expect(() => { - new UInt8(Field(10)).assertLessThanOrEqual(new UInt8(Field(100))); + new UInt8(10).assertLessThanOrEqual(new UInt8(100)); }).not.toThrow(); }); it('100<=10=false', () => { expect(() => { - new UInt8(Field(100)).assertLessThanOrEqual(new UInt8(Field(10))); + new UInt8(100).assertLessThanOrEqual(new UInt8(10)); }).toThrow(); }); @@ -2834,33 +2810,25 @@ describe('int', () => { describe('greaterThan', () => { it('2>1=true', () => { - expect(new UInt8(Field(2)).greaterThan(new UInt8(Field(1)))).toEqual( - Bool(true) - ); + expect(new UInt8(2).greaterThan(new UInt8(1))).toEqual(Bool(true)); }); it('1>1=false', () => { - expect(new UInt8(Field(1)).greaterThan(new UInt8(Field(1)))).toEqual( - Bool(false) - ); + expect(new UInt8(1).greaterThan(new UInt8(1))).toEqual(Bool(false)); }); it('1>2=false', () => { - expect(new UInt8(Field(1)).greaterThan(new UInt8(Field(2)))).toEqual( - Bool(false) - ); + expect(new UInt8(1).greaterThan(new UInt8(2))).toEqual(Bool(false)); }); it('100>10=true', () => { - expect( - new UInt8(Field(100)).greaterThan(new UInt8(Field(10))) - ).toEqual(Bool(true)); + expect(new UInt8(100).greaterThan(new UInt8(10))).toEqual(Bool(true)); }); it('10>100=false', () => { - expect( - new UInt8(Field(10)).greaterThan(new UInt8(Field(100))) - ).toEqual(Bool(false)); + expect(new UInt8(10).greaterThan(new UInt8(100))).toEqual( + Bool(false) + ); }); it('MAXINT>MAXINT=false', () => { @@ -2873,25 +2841,25 @@ describe('int', () => { describe('assertGreaterThan', () => { it('1>1=false', () => { expect(() => { - new UInt8(Field(1)).assertGreaterThan(new UInt8(Field(1))); + new UInt8(1).assertGreaterThan(new UInt8(1)); }).toThrow(); }); it('2>1=true', () => { expect(() => { - new UInt8(Field(2)).assertGreaterThan(new UInt8(Field(1))); + new UInt8(2).assertGreaterThan(new UInt8(1)); }).not.toThrow(); }); it('10>100=false', () => { expect(() => { - new UInt8(Field(10)).assertGreaterThan(new UInt8(Field(100))); + new UInt8(10).assertGreaterThan(new UInt8(100)); }).toThrow(); }); it('100000>1000=true', () => { expect(() => { - new UInt8(Field(100)).assertGreaterThan(new UInt8(Field(10))); + new UInt8(100).assertGreaterThan(new UInt8(10)); }).not.toThrow(); }); @@ -2904,33 +2872,33 @@ describe('int', () => { describe('greaterThanOrEqual', () => { it('2>=1=true', () => { - expect( - new UInt8(Field(2)).greaterThanOrEqual(new UInt8(Field(1))) - ).toEqual(Bool(true)); + expect(new UInt8(2).greaterThanOrEqual(new UInt8(1))).toEqual( + Bool(true) + ); }); it('1>=1=true', () => { - expect( - new UInt8(Field(1)).greaterThanOrEqual(new UInt8(Field(1))) - ).toEqual(Bool(true)); + expect(new UInt8(1).greaterThanOrEqual(new UInt8(1))).toEqual( + Bool(true) + ); }); it('1>=2=false', () => { - expect( - new UInt8(Field(1)).greaterThanOrEqual(new UInt8(Field(2))) - ).toEqual(Bool(false)); + expect(new UInt8(1).greaterThanOrEqual(new UInt8(2))).toEqual( + Bool(false) + ); }); it('100>=10=true', () => { - expect( - new UInt8(Field(100)).greaterThanOrEqual(new UInt8(Field(10))) - ).toEqual(Bool(true)); + expect(new UInt8(100).greaterThanOrEqual(new UInt8(10))).toEqual( + Bool(true) + ); }); it('10>=100=false', () => { - expect( - new UInt8(Field(10)).greaterThanOrEqual(new UInt8(Field(100))) - ).toEqual(Bool(false)); + expect(new UInt8(10).greaterThanOrEqual(new UInt8(100))).toEqual( + Bool(false) + ); }); it('MAXINT>=MAXINT=true', () => { @@ -2943,29 +2911,25 @@ describe('int', () => { describe('assertGreaterThanOrEqual', () => { it('1>=1=true', () => { expect(() => { - new UInt8(Field(1)).assertGreaterThanOrEqual(new UInt8(Field(1))); + new UInt8(1).assertGreaterThanOrEqual(new UInt8(1)); }).not.toThrow(); }); it('2>=1=true', () => { expect(() => { - new UInt8(Field(2)).assertGreaterThanOrEqual(new UInt8(Field(1))); + new UInt8(2).assertGreaterThanOrEqual(new UInt8(1)); }).not.toThrow(); }); it('10>=100=false', () => { expect(() => { - new UInt8(Field(10)).assertGreaterThanOrEqual( - new UInt8(Field(100)) - ); + new UInt8(10).assertGreaterThanOrEqual(new UInt8(100)); }).toThrow(); }); it('100>=10=true', () => { expect(() => { - new UInt8(Field(100)).assertGreaterThanOrEqual( - new UInt8(Field(10)) - ); + new UInt8(100).assertGreaterThanOrEqual(new UInt8(10)); }).not.toThrow(); }); @@ -2978,12 +2942,12 @@ describe('int', () => { describe('toString()', () => { it('should be the same as Field(0)', async () => { - const x = new UInt8(Field(0)); + const x = new UInt8(0); const y = Field(0); expect(x.toString()).toEqual(y.toString()); }); it('should be the same as 2^8-1', async () => { - const x = new UInt8(Field(String(NUMBERMAX))); + const x = new UInt8(NUMBERMAX.toBigInt()); const y = Field(String(NUMBERMAX)); expect(x.toString()).toEqual(y.toString()); }); @@ -3008,23 +2972,12 @@ describe('int', () => { describe('fromNumber()', () => { it('should be the same as Field(1)', () => { const x = UInt8.from(1); - expect(x.value).toEqual(new UInt32(Field(1)).value); + expect(x.value).toEqual(Field(1)); }); it('should be the same as 2^53-1', () => { const x = UInt8.from(NUMBERMAX); - expect(x.value).toEqual(Field(String(NUMBERMAX))); - }); - }); - describe('fromString()', () => { - it('should be the same as Field(1)', () => { - const x = UInt8.from('1'); - expect(x.value).toEqual(new UInt32(Field(1)).value); - }); - - it('should be the same as 2^53-1', () => { - const x = UInt8.from(String(NUMBERMAX)); - expect(x.value).toEqual(Field(String(NUMBERMAX))); + expect(x.value).toEqual(NUMBERMAX); }); }); }); From 7b6ace4f3d5be77440eee8451004e27323993134 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 14 Dec 2023 12:56:43 +0100 Subject: [PATCH 89/90] bytes to test utils --- src/lib/gadgets/test-utils.ts | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/lib/gadgets/test-utils.ts b/src/lib/gadgets/test-utils.ts index 309dac616..96c78866e 100644 --- a/src/lib/gadgets/test-utils.ts +++ b/src/lib/gadgets/test-utils.ts @@ -1,10 +1,17 @@ import type { FiniteField } from '../../bindings/crypto/finite_field.js'; -import { ProvableSpec } from '../testing/equivalent.js'; +import { ProvableSpec, spec } from '../testing/equivalent.js'; import { Random } from '../testing/random.js'; import { Gadgets } from './gadgets.js'; import { assert } from './common.js'; +import { Bytes } from '../provable-types/provable-types.js'; -export { foreignField, unreducedForeignField, uniformForeignField, throwError }; +export { + foreignField, + unreducedForeignField, + uniformForeignField, + bytes, + throwError, +}; const { Field3 } = Gadgets; @@ -49,6 +56,16 @@ function uniformForeignField( }; } +function bytes(length: number) { + const Bytes_ = Bytes(length); + return spec({ + rng: Random.map(Random.bytes(length), (x) => Uint8Array.from(x)), + there: Bytes_.from, + back: (x) => x.toBytes(), + provable: Bytes_.provable, + }); +} + // helper function throwError(message: string): T { From 8d52779bc144fb3edf66c178e4119851c00e1532 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 14 Dec 2023 13:02:15 +0100 Subject: [PATCH 90/90] merge old and new unit tests --- src/lib/keccak-old.unit-test.ts | 272 -------------------------------- src/lib/keccak.unit-test.ts | 168 ++++++++++++++++++-- 2 files changed, 156 insertions(+), 284 deletions(-) delete mode 100644 src/lib/keccak-old.unit-test.ts diff --git a/src/lib/keccak-old.unit-test.ts b/src/lib/keccak-old.unit-test.ts deleted file mode 100644 index a39179f60..000000000 --- a/src/lib/keccak-old.unit-test.ts +++ /dev/null @@ -1,272 +0,0 @@ -import { test, Random } from './testing/property.js'; -import { UInt8 } from './int.js'; -import { Hash } from './hashes-combined.js'; -import { Provable } from './provable.js'; -import { expect } from 'expect'; -import assert from 'assert'; -import { Bytes } from './provable-types/provable-types.js'; - -let RandomUInt8 = Random.map(Random.uint8, (x) => UInt8.from(x)); - -// Test constructor -test(Random.uint8, Random.uint8, (x, y, assert) => { - let z = new UInt8(x); - assert(z instanceof UInt8); - assert(z.toBigInt() === x); - assert(z.toString() === x.toString()); - - assert((z = new UInt8(x)) instanceof UInt8 && z.toBigInt() === x); - assert((z = new UInt8(z)) instanceof UInt8 && z.toBigInt() === x); - assert((z = new UInt8(z.value.value)) instanceof UInt8 && z.toBigInt() === x); - - z = new UInt8(y); - assert(z instanceof UInt8); - assert(z.toString() === y.toString()); -}); - -// handles all numbers up to 2^8 -test(Random.nat(255), (n, assert) => { - assert(UInt8.from(n).toString() === String(n)); -}); - -// throws on negative numbers -test.negative(Random.int(-10, -1), (x) => UInt8.from(x)); - -// throws on numbers >= 2^8 -test.negative(Random.uint8.invalid, (x) => UInt8.from(x)); - -runHashFunctionTests(); -console.log('OCaml tests pass! 🎉'); - -// test digest->hex and hex->digest conversions -checkHashInCircuit(); -console.log('hashing digest conversions matches! 🎉'); - -// check in-circuit -function checkHashInCircuit() { - Provable.runAndCheck(() => { - let data = Random.array(RandomUInt8, Random.nat(32)) - .create()() - .map((x) => Provable.witness(UInt8, () => UInt8.from(x))); - - checkHashConversions(Bytes.from(data)); - }); -} - -function checkHashConversions(data: Bytes) { - Provable.asProver(() => { - expectDigestToEqualHex(Hash.SHA3_256.hash(data)); - expectDigestToEqualHex(Hash.SHA3_384.hash(data)); - expectDigestToEqualHex(Hash.SHA3_512.hash(data)); - expectDigestToEqualHex(Hash.Keccak256.hash(data)); - }); -} - -function expectDigestToEqualHex(digest: Bytes) { - const hex = digest.toHex(); - expect(digest).toEqual(Bytes.fromHex(hex)); -} - -function equals(a: Bytes, b: Bytes): boolean { - if (a.length !== b.length) return false; - for (let i = 0; i < a.length; i++) a.bytes[i].assertEquals(b.bytes[i]); - return true; -} - -/** - * Based off the following unit tests from the OCaml implementation: - * https://github.com/MinaProtocol/mina/blob/69d6ea4a3b7ca1690cf8f41d4598cb7484359e1d/src/lib/crypto/kimchi_backend/gadgets/keccak.ml#L646 - */ -function runHashFunctionTests() { - // Positive Tests - testExpected({ - nist: false, - length: 256, - message: '30', - expected: - '044852b2a670ade5407e78fb2863c51de9fcb96542a07186fe3aeda6bb8a116d', - }); - - testExpected({ - nist: true, - length: 512, - message: '30', - expected: - '2d44da53f305ab94b6365837b9803627ab098c41a6013694f9b468bccb9c13e95b3900365eb58924de7158a54467e984efcfdabdbcc9af9a940d49c51455b04c', - }); - - testExpected({ - nist: false, - length: 256, - message: - '4920616d20746865206f776e6572206f6620746865204e465420776974682069642058206f6e2074686520457468657265756d20636861696e', - expected: - '63858e0487687c3eeb30796a3e9307680e1b81b860b01c88ff74545c2c314e36', - }); - - testExpected({ - nist: false, - length: 256, - message: - '044852b2a670ade5407e78fb2863c51de9fcb96542a07186fe3aeda6bb8a116df9e2eaaa42d9fe9e558a9b8ef1bf366f190aacaa83bad2641ee106e9041096e42d44da53f305ab94b6365837b9803627ab098c41a6013694f9b468bccb9c13e95b3900365eb58924de7158a54467e984efcfdabdbcc9af9a940d49c51455b04c63858e0487687c3eeb30796a3e9307680e1b81b860b01c88ff74545c2c314e36', - expected: - '560deb1d387f72dba729f0bd0231ad45998dda4b53951645322cf95c7b6261d9', - }); - - testExpected({ - nist: true, - length: 256, - message: - '044852b2a670ade5407e78fb2863c51de9fcb96542a07186fe3aeda6bb8a116df9e2eaaa42d9fe9e558a9b8ef1bf366f190aacaa83bad2641ee106e9041096e42d44da53f305ab94b6365837b9803627ab098c41a6013694f9b468bccb9c13e95b3900365eb58924de7158a54467e984efcfdabdbcc9af9a940d49c51455b04c63858e0487687c3eeb30796a3e9307680e1b81b860b01c88ff74545c2c314e36', - expected: - '1784354c4bbfa5f54e5db23041089e65a807a7b970e3cfdba95e2fbe63b1c0e4', - }); - - testExpected({ - nist: false, - length: 256, - message: - '391ccf9b5de23bb86ec6b2b142adb6e9ba6bee8519e7502fb8be8959fbd2672934cc3e13b7b45bf2b8a5cb48881790a7438b4a326a0c762e31280711e6b64fcc2e3e4e631e501d398861172ea98603618b8f23b91d0208b0b992dfe7fdb298b6465adafbd45e4f88ee9dc94e06bc4232be91587f78572c169d4de4d8b95b714ea62f1fbf3c67a4', - expected: - '7d5655391ede9ca2945f32ad9696f464be8004389151ce444c89f688278f2e1d', - }); - - testExpected({ - nist: false, - length: 256, - message: - 'ff391ccf9b5de23bb86ec6b2b142adb6e9ba6bee8519e7502fb8be8959fbd2672934cc3e13b7b45bf2b8a5cb48881790a7438b4a326a0c762e31280711e6b64fcc2e3e4e631e501d398861172ea98603618b8f23b91d0208b0b992dfe7fdb298b6465adafbd45e4f88ee9dc94e06bc4232be91587f78572c169d4de4d8b95b714ea62f1fbf3c67a4', - expected: - '37694fd4ba137be747eb25a85b259af5563e0a7a3010d42bd15963ac631b9d3f', - }); - - testExpected({ - nist: false, - length: 256, - message: - '80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001', - expected: - 'bbf1f49a2cc5678aa62196d0c3108d89425b81780e1e90bcec03b4fb5f834714', - }); - - testExpected({ - nist: false, - length: 256, - message: - '80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001', - expected: - 'bbf1f49a2cc5678aa62196d0c3108d89425b81780e1e90bcec03b4fb5f834714', - }); - - testExpected({ - nist: false, - length: 256, - message: 'a2c0', - expected: - '9856642c690c036527b8274db1b6f58c0429a88d9f3b9298597645991f4f58f0', - }); - - testExpected({ - nist: false, - length: 256, - message: '0a2c', - expected: - '295b48ad49eff61c3abfd399c672232434d89a4ef3ca763b9dbebb60dbb32a8b', - }); - - testExpected({ - nist: false, - length: 256, - message: '00', - expected: - 'bc36789e7a1e281436464229828f817d6612f7b477d66591ff96a9e064bcc98a', - }); - - // Negative tests - try { - testExpected({ - nist: false, - length: 256, - message: 'a2c', - expected: - '07f02d241eeba9c909a1be75e08d9e8ac3e61d9e24fa452a6785083e1527c467', - }); - assert(false, 'Expected to throw'); - } catch (e) {} - - try { - testExpected({ - nist: true, - length: 256, - message: '0', - expected: - 'f39f4526920bb4c096e5722d64161ea0eb6dbd0b4ff0d812f31d56fb96142084', - }); - assert(false, 'Expected to throw'); - } catch (e) {} - - try { - testExpected({ - nist: true, - length: 256, - message: '30', - expected: - 'f9e2eaaa42d9fe9e558a9b8ef1bf366f190aacaa83bad2641ee106e9041096e4', - }); - assert(false, 'Expected to throw'); - } catch (e) {} - - try { - testExpected({ - nist: true, - length: 256, - message: - '4920616d20746865206f776e6572206f6620746865204e465420776974682069642058206f6e2074686520457468657265756d20636861696e', - expected: - '63858e0487687c3eeb30796a3e9307680e1b81b860b01c88ff74545c2c314e36', - }); - assert(false, 'Expected to throw'); - } catch (e) {} -} - -function testExpected({ - message, - expected, - nist = false, - length = 256, -}: { - message: string; - expected: string; - nist: boolean; - length: number; -}) { - Provable.runAndCheck(() => { - assert(message.length % 2 === 0); - - let fields = Bytes.fromHex(message); - let expectedHash = Bytes.fromHex(expected); - - Provable.asProver(() => { - if (nist) { - let hashed: Bytes; - switch (length) { - case 256: - hashed = Hash.SHA3_256.hash(fields); - break; - case 384: - hashed = Hash.SHA3_384.hash(fields); - break; - case 512: - hashed = Hash.SHA3_512.hash(fields); - break; - default: - assert(false); - } - equals(hashed!, expectedHash); - } else { - let hashed = Hash.Keccak256.hash(fields); - equals(hashed, expectedHash); - } - }); - }); -} diff --git a/src/lib/keccak.unit-test.ts b/src/lib/keccak.unit-test.ts index a811b018d..2e4c160b3 100644 --- a/src/lib/keccak.unit-test.ts +++ b/src/lib/keccak.unit-test.ts @@ -1,7 +1,6 @@ import { Keccak } from './keccak.js'; import { ZkProgram } from './proof_system.js'; -import { Random, sample } from './testing/random.js'; -import { equivalent, equivalentAsync, spec } from './testing/equivalent.js'; +import { equivalent, equivalentAsync } from './testing/equivalent.js'; import { keccak_224, keccak_256, @@ -13,6 +12,10 @@ import { sha3_512, } from '@noble/hashes/sha3'; import { Bytes } from './provable-types/provable-types.js'; +import { bytes } from './gadgets/test-utils.js'; +import { UInt8 } from './int.js'; +import { test, Random, sample } from './testing/property.js'; +import { expect } from 'expect'; const RUNS = 1; @@ -33,19 +36,11 @@ const testImplementations = { const lengths = [256, 384, 512] as const; +// EQUIVALENCE TESTS AGAINST REF IMPLEMENTATION + // checks outside circuit // TODO: fix witness generation slowness -const bytes = (length: number) => { - const Bytes_ = Bytes(length); - return spec({ - rng: Random.map(Random.bytes(length), (x) => Uint8Array.from(x)), - there: Bytes_.from, - back: (x) => x.toBytes(), - provable: Bytes_.provable, - }); -}; - for (let length of lengths) { let [preimageLength] = sample(Random.nat(100), 1); console.log(`Testing ${length} with preimage length ${preimageLength}`); @@ -63,8 +58,54 @@ for (let length of lengths) { (x) => Keccak.preNist(length, x), `keccak ${length}` ); + + // bytes to hex roundtrip + equivalent({ from: [inputBytes], to: inputBytes })( + (x) => x, + (x) => Bytes.fromHex(x.toHex()), + `Bytes toHex` + ); } +// EQUIVALENCE TESTS AGAINST TEST VECTORS (at the bottom) + +for (let { nist, length, message, expected } of testVectors()) { + let Hash = nist ? Keccak.nistSha3 : Keccak.preNist; + let actual = Hash(length, Bytes.fromHex(message)); + expect(actual).toEqual(Bytes.fromHex(expected)); +} + +// MISC QUICK TESTS + +// Test constructor +test(Random.uint8, Random.uint8, (x, y, assert) => { + let z = new UInt8(x); + assert(z instanceof UInt8); + assert(z.toBigInt() === x); + assert(z.toString() === x.toString()); + + assert((z = new UInt8(x)) instanceof UInt8 && z.toBigInt() === x); + assert((z = new UInt8(z)) instanceof UInt8 && z.toBigInt() === x); + assert((z = new UInt8(z.value.value)) instanceof UInt8 && z.toBigInt() === x); + + z = new UInt8(y); + assert(z instanceof UInt8); + assert(z.toString() === y.toString()); +}); + +// handles all numbers up to 2^8 +test(Random.nat(255), (n, assert) => { + assert(UInt8.from(n).toString() === String(n)); +}); + +// throws on negative numbers +test.negative(Random.int(-10, -1), (x) => UInt8.from(x)); + +// throws on numbers >= 2^8 +test.negative(Random.uint8.invalid, (x) => UInt8.from(x)); + +// PROOF TESTS + // Choose a test length at random const digestLength = lengths[Math.floor(Math.random() * 3)]; @@ -121,3 +162,106 @@ await equivalentAsync( await KeccakProgram.verify(proof); return proof.publicOutput; }); + +// TEST VECTORS + +function testVectors(): { + nist: boolean; + length: 256 | 384 | 512; + message: string; + expected: string; +}[] { + return [ + { + nist: false, + length: 256, + message: '30', + expected: + '044852b2a670ade5407e78fb2863c51de9fcb96542a07186fe3aeda6bb8a116d', + }, + { + nist: true, + length: 512, + message: '30', + expected: + '2d44da53f305ab94b6365837b9803627ab098c41a6013694f9b468bccb9c13e95b3900365eb58924de7158a54467e984efcfdabdbcc9af9a940d49c51455b04c', + }, + { + nist: false, + length: 256, + message: + '4920616d20746865206f776e6572206f6620746865204e465420776974682069642058206f6e2074686520457468657265756d20636861696e', + expected: + '63858e0487687c3eeb30796a3e9307680e1b81b860b01c88ff74545c2c314e36', + }, + { + nist: false, + length: 256, + message: + '044852b2a670ade5407e78fb2863c51de9fcb96542a07186fe3aeda6bb8a116df9e2eaaa42d9fe9e558a9b8ef1bf366f190aacaa83bad2641ee106e9041096e42d44da53f305ab94b6365837b9803627ab098c41a6013694f9b468bccb9c13e95b3900365eb58924de7158a54467e984efcfdabdbcc9af9a940d49c51455b04c63858e0487687c3eeb30796a3e9307680e1b81b860b01c88ff74545c2c314e36', + expected: + '560deb1d387f72dba729f0bd0231ad45998dda4b53951645322cf95c7b6261d9', + }, + { + nist: true, + length: 256, + message: + '044852b2a670ade5407e78fb2863c51de9fcb96542a07186fe3aeda6bb8a116df9e2eaaa42d9fe9e558a9b8ef1bf366f190aacaa83bad2641ee106e9041096e42d44da53f305ab94b6365837b9803627ab098c41a6013694f9b468bccb9c13e95b3900365eb58924de7158a54467e984efcfdabdbcc9af9a940d49c51455b04c63858e0487687c3eeb30796a3e9307680e1b81b860b01c88ff74545c2c314e36', + expected: + '1784354c4bbfa5f54e5db23041089e65a807a7b970e3cfdba95e2fbe63b1c0e4', + }, + { + nist: false, + length: 256, + message: + '391ccf9b5de23bb86ec6b2b142adb6e9ba6bee8519e7502fb8be8959fbd2672934cc3e13b7b45bf2b8a5cb48881790a7438b4a326a0c762e31280711e6b64fcc2e3e4e631e501d398861172ea98603618b8f23b91d0208b0b992dfe7fdb298b6465adafbd45e4f88ee9dc94e06bc4232be91587f78572c169d4de4d8b95b714ea62f1fbf3c67a4', + expected: + '7d5655391ede9ca2945f32ad9696f464be8004389151ce444c89f688278f2e1d', + }, + { + nist: false, + length: 256, + message: + 'ff391ccf9b5de23bb86ec6b2b142adb6e9ba6bee8519e7502fb8be8959fbd2672934cc3e13b7b45bf2b8a5cb48881790a7438b4a326a0c762e31280711e6b64fcc2e3e4e631e501d398861172ea98603618b8f23b91d0208b0b992dfe7fdb298b6465adafbd45e4f88ee9dc94e06bc4232be91587f78572c169d4de4d8b95b714ea62f1fbf3c67a4', + expected: + '37694fd4ba137be747eb25a85b259af5563e0a7a3010d42bd15963ac631b9d3f', + }, + { + nist: false, + length: 256, + message: + '80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001', + expected: + 'bbf1f49a2cc5678aa62196d0c3108d89425b81780e1e90bcec03b4fb5f834714', + }, + { + nist: false, + length: 256, + message: + '80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001', + expected: + 'bbf1f49a2cc5678aa62196d0c3108d89425b81780e1e90bcec03b4fb5f834714', + }, + { + nist: false, + length: 256, + message: 'a2c0', + expected: + '9856642c690c036527b8274db1b6f58c0429a88d9f3b9298597645991f4f58f0', + }, + { + nist: false, + length: 256, + message: '0a2c', + expected: + '295b48ad49eff61c3abfd399c672232434d89a4ef3ca763b9dbebb60dbb32a8b', + }, + { + nist: false, + length: 256, + message: '00', + expected: + 'bc36789e7a1e281436464229828f817d6612f7b477d66591ff96a9e064bcc98a', + }, + ]; +}