diff --git a/src/bindings b/src/bindings index c09afcd2fc..7946599c5f 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit c09afcd2fc902abb3db7c029740d401414f76f8b +Subproject commit 7946599c5f1636576519601dbd2c20aecc90a502 diff --git a/src/examples/crypto/ecdsa/ecdsa.ts b/src/examples/crypto/ecdsa/ecdsa.ts index cf2407df35..45639c41cb 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 377e18d50a..2a497de373 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/zkapps/hashing/hash.ts b/src/examples/zkapps/hashing/hash.ts new file mode 100644 index 0000000000..9ad9947dfa --- /dev/null +++ b/src/examples/zkapps/hashing/hash.ts @@ -0,0 +1,49 @@ +import { + Hash, + Field, + SmartContract, + state, + State, + method, + Permissions, + Bytes, +} from 'o1js'; + +let initialCommitment: Field = Field(0); + +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 SHA256(xs: Bytes) { + const shaHash = Hash.SHA3_256.hash(xs); + const commitment = Hash.hash(shaHash.toFields()); + this.commitment.set(commitment); + } + + @method SHA384(xs: Bytes) { + const shaHash = Hash.SHA3_384.hash(xs); + const commitment = Hash.hash(shaHash.toFields()); + this.commitment.set(commitment); + } + + @method SHA512(xs: Bytes) { + const shaHash = Hash.SHA3_512.hash(xs); + const commitment = Hash.hash(shaHash.toFields()); + this.commitment.set(commitment); + } + + @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 new file mode 100644 index 0000000000..9350211d68 --- /dev/null +++ b/src/examples/zkapps/hashing/run.ts @@ -0,0 +1,73 @@ +import { HashStorage } from './hash.js'; +import { Mina, PrivateKey, AccountUpdate, Bytes } from 'o1js'; + +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 = Bytes.from(Array.from({ length: 32 }, (_, i) => i)); + +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 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}`); diff --git a/src/index.ts b/src/index.ts index 9f63303560..b82033e1dd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,7 @@ export { createForeignCurve, ForeignCurve } from './lib/foreign-curve.js'; export { createEcdsa, EcdsaSignature } from './lib/foreign-ecdsa.js'; 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 { @@ -31,7 +32,8 @@ 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 { 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/foreign-ecdsa.ts b/src/lib/foreign-ecdsa.ts index d5d941745c..bccbaa77ab 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/gadgets/gadgets.ts b/src/lib/gadgets/gadgets.ts index 038646d03d..27cae55bf7 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 147ef1a6a4..d53d8f5e51 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( diff --git a/src/lib/gadgets/test-utils.ts b/src/lib/gadgets/test-utils.ts index 309dac6161..96c78866e3 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 { diff --git a/src/lib/hash.ts b/src/lib/hash.ts index 2b1064f7a7..39fd993774 100644 --- a/src/lib/hash.ts +++ b/src/lib/hash.ts @@ -13,7 +13,7 @@ export { Poseidon, TokenSymbol }; // internal API export { HashInput, - Hash, + HashHelpers, emptyHashWithPrefix, hashWithPrefix, salt, @@ -23,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)); } } @@ -105,8 +105,8 @@ function hashConstant(input: Field[]) { return Field(PoseidonBigint.hash(toBigints(input))); } -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) { diff --git a/src/lib/hashes-combined.ts b/src/lib/hashes-combined.ts new file mode 100644 index 0000000000..8440a25b32 --- /dev/null +++ b/src/lib/hashes-combined.ts @@ -0,0 +1,127 @@ +import { Poseidon } from './hash.js'; +import { Keccak } from './keccak.js'; +import { Bytes } from './provable-types/provable-types.js'; + +export { Hash }; + +/** + * 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); + }, + }, +}; diff --git a/src/lib/int.test.ts b/src/lib/int.test.ts index cc93726496..1914a9fede 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 'o1js'; 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,850 @@ 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(1)); + const y = Provable.witness(UInt8, () => new UInt8(1)); + x.add(y).assertEquals(2); + }); + }).not.toThrow(); + }); + + it('100+100=200', () => { + expect(() => { + Provable.runAndCheck(() => { + 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 = ((1n << 8n) - 2n) / 2n; + 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(1)); + x.add(y); + }); + }).toThrow(); + }); + }); + + describe('sub', () => { + it('1-1=0', () => { + expect(() => { + Provable.runAndCheck(() => { + 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(); + }); + + it('100-50=50', () => { + expect(() => { + Provable.runAndCheck(() => { + 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(); + }); + + it('should throw on sub if results in negative number', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(0)); + const y = Provable.witness(UInt8, () => new UInt8(1)); + x.sub(y); + }); + }).toThrow(); + }); + }); + + describe('mul', () => { + it('1x2=2', () => { + expect(() => { + Provable.runAndCheck(() => { + 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(); + }); + + it('1x0=0', () => { + expect(() => { + Provable.runAndCheck(() => { + 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(); + }); + + it('12x20=240', () => { + expect(() => { + Provable.runAndCheck(() => { + 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(); + }); + + it('MAXINTx1=MAXINT', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => UInt8.MAXINT()); + const y = Provable.witness(UInt8, () => new UInt8(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(2)); + x.mul(y); + }); + }).toThrow(); + }); + }); + + describe('div', () => { + it('2/1=2', () => { + expect(() => { + Provable.runAndCheck(() => { + 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(); + }); + + it('0/1=0', () => { + expect(() => { + Provable.runAndCheck(() => { + 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(); + }); + + it('20/10=2', () => { + expect(() => { + Provable.runAndCheck(() => { + 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(); + }); + + it('MAXINT/1=MAXINT', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => UInt8.MAXINT()); + const y = Provable.witness(UInt8, () => new UInt8(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(0)); + x.div(y); + }); + }).toThrow(); + }); + }); + + describe('mod', () => { + it('1%1=0', () => { + expect(() => { + Provable.runAndCheck(() => { + 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(); + }); + + it('50%32=18', () => { + expect(() => { + Provable.runAndCheck(() => { + 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(); + }); + + it('MAXINT%7=3', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => UInt8.MAXINT()); + const y = Provable.witness(UInt8, () => new UInt8(7)); + x.mod(y).assertEquals(new UInt8(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(0)); + x.mod(y).assertEquals(new UInt8(1)); + }); + }).toThrow(); + }); + }); + + describe('assertLt', () => { + it('1<2=true', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(1)); + const y = Provable.witness(UInt8, () => new UInt8(2)); + x.assertLessThan(y); + }); + }).not.toThrow(); + }); + + it('1<1=false', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(1)); + const y = Provable.witness(UInt8, () => new UInt8(1)); + x.assertLessThan(y); + }); + }).toThrow(); + }); + + it('2<1=false', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(2)); + const y = Provable.witness(UInt8, () => new UInt8(1)); + x.assertLessThan(y); + }); + }).toThrow(); + }); + + it('10<100=true', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(10)); + const y = Provable.witness(UInt8, () => new UInt8(100)); + x.assertLessThan(y); + }); + }).not.toThrow(); + }); + + it('100<10=false', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(100)); + const y = Provable.witness(UInt8, () => new UInt8(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(1)); + const y = Provable.witness(UInt8, () => new UInt8(1)); + x.assertLessThanOrEqual(y); + }); + }).not.toThrow(); + }); + + it('2<=1=false', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(2)); + const y = Provable.witness(UInt8, () => new UInt8(1)); + x.assertLessThanOrEqual(y); + }); + }).toThrow(); + }); + + it('10<=100=true', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(10)); + const y = Provable.witness(UInt8, () => new UInt8(100)); + x.assertLessThanOrEqual(y); + }); + }).not.toThrow(); + }); + + it('100<=10=false', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(100)); + const y = Provable.witness(UInt8, () => new UInt8(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(2)); + const y = Provable.witness(UInt8, () => new UInt8(1)); + x.assertGreaterThan(y); + }); + }).not.toThrow(); + }); + + it('1>1=false', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(1)); + const y = Provable.witness(UInt8, () => new UInt8(1)); + x.assertGreaterThan(y); + }); + }).toThrow(); + }); + + it('1>2=false', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(1)); + const y = Provable.witness(UInt8, () => new UInt8(2)); + x.assertGreaterThan(y); + }); + }).toThrow(); + }); + + it('100>10=true', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(100)); + const y = Provable.witness(UInt8, () => new UInt8(10)); + x.assertGreaterThan(y); + }); + }).not.toThrow(); + }); + + it('10>100=false', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(1000)); + const y = Provable.witness(UInt8, () => new UInt8(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(1)); + const y = Provable.witness(UInt8, () => new UInt8(1)); + x.assertGreaterThanOrEqual(y); + }); + }).not.toThrow(); + }); + + it('1>=2=false', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(1)); + const y = Provable.witness(UInt8, () => new UInt8(2)); + x.assertGreaterThanOrEqual(y); + }); + }).toThrow(); + }); + + it('100>=10=true', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(100)); + const y = Provable.witness(UInt8, () => new UInt8(10)); + x.assertGreaterThanOrEqual(y); + }); + }).not.toThrow(); + }); + + it('10>=100=false', () => { + expect(() => { + Provable.runAndCheck(() => { + const x = Provable.witness(UInt8, () => new UInt8(10)); + const y = Provable.witness(UInt8, () => new UInt8(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(1)); + x.assertEquals(y); + }); + }).not.toThrow(); + }); + }); + }); + }); + + describe('Outside of circuit', () => { + describe('add', () => { + it('1+1=2', () => { + expect(new UInt8(1).add(1).toString()).toEqual('2'); + }); + + it('50+50=100', () => { + expect(new UInt8(50).add(50).toString()).toEqual('100'); + }); + + it('(MAXINT/2+MAXINT/2) adds to MAXINT', () => { + const value = ((1n << 8n) - 2n) / 2n; + expect( + new UInt8(value).add(new UInt8(value)).add(new UInt8(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(1).sub(1).toString()).toEqual('0'); + }); + + it('100-50=50', () => { + expect(new UInt8(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(1).mul(2).toString()).toEqual('2'); + }); + + it('1x0=0', () => { + expect(new UInt8(1).mul(0).toString()).toEqual('0'); + }); + + it('12x20=240', () => { + expect(new UInt8(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(2).div(1).toString()).toEqual('2'); + }); + + it('0/1=0', () => { + expect(new UInt8(0).div(1).toString()).toEqual('0'); + }); + + it('20/10=2', () => { + expect(new UInt8(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(1).mod(1).toString()).toEqual('0'); + }); + + it('50%32=18', () => { + expect(new UInt8(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(1).lessThan(new UInt8(2))).toEqual(Bool(true)); + }); + + it('1<1=false', () => { + expect(new UInt8(1).lessThan(new UInt8(1))).toEqual(Bool(false)); + }); + + it('2<1=false', () => { + expect(new UInt8(2).lessThan(new UInt8(1))).toEqual(Bool(false)); + }); + + it('10<100=true', () => { + expect(new UInt8(10).lessThan(new UInt8(100))).toEqual(Bool(true)); + }); + + it('100<10=false', () => { + expect(new UInt8(100).lessThan(new UInt8(10))).toEqual(Bool(false)); + }); + + it('MAXINT { + expect(UInt8.MAXINT().lessThan(UInt8.MAXINT())).toEqual(Bool(false)); + }); + }); + + describe('lessThanOrEqual', () => { + it('1<=1=true', () => { + expect(new UInt8(1).lessThanOrEqual(new UInt8(1))).toEqual( + Bool(true) + ); + }); + + it('2<=1=false', () => { + expect(new UInt8(2).lessThanOrEqual(new UInt8(1))).toEqual( + Bool(false) + ); + }); + + it('10<=100=true', () => { + expect(new UInt8(10).lessThanOrEqual(new UInt8(100))).toEqual( + Bool(true) + ); + }); + + it('100<=10=false', () => { + expect(new UInt8(100).lessThanOrEqual(new UInt8(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(1).assertLessThanOrEqual(new UInt8(1)); + }).not.toThrow(); + }); + + it('2<=1=false', () => { + expect(() => { + new UInt8(2).assertLessThanOrEqual(new UInt8(1)); + }).toThrow(); + }); + + it('10<=100=true', () => { + expect(() => { + new UInt8(10).assertLessThanOrEqual(new UInt8(100)); + }).not.toThrow(); + }); + + it('100<=10=false', () => { + expect(() => { + new UInt8(100).assertLessThanOrEqual(new UInt8(10)); + }).toThrow(); + }); + + it('MAXINT<=MAXINT=true', () => { + expect(() => { + UInt8.MAXINT().assertLessThanOrEqual(UInt8.MAXINT()); + }).not.toThrow(); + }); + }); + + describe('greaterThan', () => { + it('2>1=true', () => { + expect(new UInt8(2).greaterThan(new UInt8(1))).toEqual(Bool(true)); + }); + + it('1>1=false', () => { + expect(new UInt8(1).greaterThan(new UInt8(1))).toEqual(Bool(false)); + }); + + it('1>2=false', () => { + expect(new UInt8(1).greaterThan(new UInt8(2))).toEqual(Bool(false)); + }); + + it('100>10=true', () => { + expect(new UInt8(100).greaterThan(new UInt8(10))).toEqual(Bool(true)); + }); + + it('10>100=false', () => { + expect(new UInt8(10).greaterThan(new UInt8(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(1).assertGreaterThan(new UInt8(1)); + }).toThrow(); + }); + + it('2>1=true', () => { + expect(() => { + new UInt8(2).assertGreaterThan(new UInt8(1)); + }).not.toThrow(); + }); + + it('10>100=false', () => { + expect(() => { + new UInt8(10).assertGreaterThan(new UInt8(100)); + }).toThrow(); + }); + + it('100000>1000=true', () => { + expect(() => { + new UInt8(100).assertGreaterThan(new UInt8(10)); + }).not.toThrow(); + }); + + it('MAXINT>MAXINT=false', () => { + expect(() => { + UInt8.MAXINT().assertGreaterThan(UInt8.MAXINT()); + }).toThrow(); + }); + }); + + describe('greaterThanOrEqual', () => { + it('2>=1=true', () => { + expect(new UInt8(2).greaterThanOrEqual(new UInt8(1))).toEqual( + Bool(true) + ); + }); + + it('1>=1=true', () => { + expect(new UInt8(1).greaterThanOrEqual(new UInt8(1))).toEqual( + Bool(true) + ); + }); + + it('1>=2=false', () => { + expect(new UInt8(1).greaterThanOrEqual(new UInt8(2))).toEqual( + Bool(false) + ); + }); + + it('100>=10=true', () => { + expect(new UInt8(100).greaterThanOrEqual(new UInt8(10))).toEqual( + Bool(true) + ); + }); + + it('10>=100=false', () => { + expect(new UInt8(10).greaterThanOrEqual(new UInt8(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(1).assertGreaterThanOrEqual(new UInt8(1)); + }).not.toThrow(); + }); + + it('2>=1=true', () => { + expect(() => { + new UInt8(2).assertGreaterThanOrEqual(new UInt8(1)); + }).not.toThrow(); + }); + + it('10>=100=false', () => { + expect(() => { + new UInt8(10).assertGreaterThanOrEqual(new UInt8(100)); + }).toThrow(); + }); + + it('100>=10=true', () => { + expect(() => { + new UInt8(100).assertGreaterThanOrEqual(new UInt8(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(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(NUMBERMAX.toBigInt()); + 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(Field(1)); + }); + + it('should be the same as 2^53-1', () => { + const x = UInt8.from(NUMBERMAX); + expect(x.value).toEqual(NUMBERMAX); + }); + }); + }); + }); + }); }); diff --git a/src/lib/int.ts b/src/lib/int.ts index 45ca4e4011..13c2553101 100644 --- a/src/lib/int.ts +++ b/src/lib/int.ts @@ -1,11 +1,12 @@ 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'; - +import { Gadgets } from './gadgets/gadgets.js'; +import { FieldVar, withMessage } from './field.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. @@ -962,3 +963,387 @@ class Int64 extends CircuitValue implements BalanceChange { return this.sgn.isPositive(); } } + +/** + * A 8 bit unsigned integer with values ranging from 0 to 255. + */ +class UInt8 extends Struct({ + value: Field, +}) { + static NUM_BITS = 8; + + /** + * 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 | 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); + }, + }; + + /** + * Add a {@link UInt8} to another {@link UInt8} without allowing overflow. + * + * @example + * ```ts + * const x = UInt8.from(3); + * const sum = x.add(5); + * sum.assertEquals(8); + * ``` + * + * @throws if the result is greater than 255. + */ + add(y: UInt8 | bigint | number) { + let z = this.value.add(UInt8.from(y).value); + Gadgets.rangeCheck8(z); + return UInt8.Unsafe.fromField(z); + } + + /** + * Subtract a {@link UInt8} from another {@link UInt8} without allowing underflow. + * + * @example + * ```ts + * const x = UInt8.from(8); + * const difference = x.sub(5); + * difference.assertEquals(3); + * ``` + * + * @throws if the result is less than 0. + */ + sub(y: UInt8 | bigint | number) { + let z = this.value.sub(UInt8.from(y).value); + Gadgets.rangeCheck8(z); + return UInt8.Unsafe.fromField(z); + } + + /** + * Multiply a {@link UInt8} by another {@link UInt8} without allowing overflow. + * + * @example + * ```ts + * const x = UInt8.from(3); + * const product = x.mul(5); + * product.assertEquals(15); + * ``` + * + * @throws if the result is greater than 255. + */ + mul(y: UInt8 | bigint | number) { + let z = this.value.mul(UInt8.from(y).value); + Gadgets.rangeCheck8(z); + return UInt8.Unsafe.fromField(z); + } + + /** + * Divide a {@link UInt8} by another {@link UInt8}. + * This is integer division that rounds down. + * + * @example + * ```ts + * const x = UInt8.from(7); + * const quotient = x.div(2); + * quotient.assertEquals(3); + * ``` + */ + div(y: UInt8 | bigint | number) { + return this.divMod(y).quotient; + } + + /** + * Get the remainder a {@link UInt8} of division of another {@link UInt8}. + * + * @example + * ```ts + * const x = UInt8.from(50); + * const mod = x.mod(30); + * mod.assertEquals(20); + * ``` + */ + mod(y: UInt8 | bigint | number) { + return this.divMod(y).remainder; + } + + /** + * 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 `q` and remainder `r`. + */ + divMod(y: UInt8 | bigint | number) { + let x = this.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(q), remainder: UInt8.from(r) }; + } + + // 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(); + + // 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 remainder = UInt8.Unsafe.fromField(r); + let quotient = UInt8.Unsafe.fromField(q); + + 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}. + * + * @example + * ```ts + * UInt8.from(3).lessThanOrEqual(UInt8.from(5)); + * ``` + */ + 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}. + * + * @example + * ```ts + * UInt8.from(2).lessThan(UInt8.from(3)); + * ``` + */ + 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. + * + * **Important**: If an assertion fails, the code throws an error. + * + * @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 | 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}`); + } + return; + } + // x < y <=> x + 1 <= y + 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. + * + * **Important**: If an assertion fails, the code throws an error. + * + * @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 | 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}`); + } + return; + } + try { + // 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}. + * Returns a {@link Bool}. + * + * @example + * ```ts + * // 5 > 3 + * UInt8.from(5).greaterThan(3); + * ``` + */ + 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}. + * + * @example + * ```ts + * // 3 >= 3 + * UInt8.from(3).greaterThanOrEqual(3); + * ``` + */ + greaterThanOrEqual(y: UInt8 | bigint | number) { + return UInt8.from(y).lessThanOrEqual(this); + } + + /** + * Assert that this {@link UInt8} is greater than another {@link UInt8} value. + * + * **Important**: If an assertion fails, the code throws an error. + * + * @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 | 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. + * + * **Important**: If an assertion fails, the code throws an error. + * + * @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) { + UInt8.from(y).assertLessThanOrEqual(this, message); + } + + /** + * Assert that this {@link UInt8} is equal another {@link UInt8} value. + * + * **Important**: If an assertion fails, the code throws an error. + * + * @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: 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 is not provable. + */ + toString() { + return this.value.toString(); + } + + /** + * 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. + */ + toBigInt() { + return this.value.toBigInt(); + } + + /** + * {@link Provable.check} for {@link UInt8}. + * Proves that the input is in the [0, 255] range. + */ + static check(x: { value: Field } | Field) { + if (x instanceof Field) x = { value: x }; + Gadgets.rangeCheck8(x.value); + } + + static toInput(x: { value: Field }): HashInput { + return { packed: [[x.value, 8]] }; + } + + /** + * Turns a {@link UInt8} into a {@link UInt32}. + */ + toUInt32(): UInt32 { + return new UInt32(this.value); + } + + /** + * Turns a {@link UInt8} into a {@link UInt64}. + */ + toUInt64(): UInt64 { + return new UInt64(this.value); + } + + /** + * Creates a {@link UInt8} with a value of 255. + */ + static MAXINT() { + 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 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; + Gadgets.rangeCheck8(x); + } +} diff --git a/src/lib/keccak.ts b/src/lib/keccak.ts index d43ee8fc28..ac63ceecb1 100644 --- a/src/lib/keccak.ts +++ b/src/lib/keccak.ts @@ -1,8 +1,11 @@ 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 { FlexibleBytes } from './provable-types/bytes.js'; +import { UInt8 } from './int.js'; +import { Bytes } from './provable-types/provable-types.js'; export { Keccak }; @@ -13,25 +16,25 @@ 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); * ``` * */ - nistSha3(len: 256 | 384 | 512, message: Field[]): Field[] { - return nistSha3(len, message); + nistSha3(len: 256 | 384 | 512, message: FlexibleBytes) { + return nistSha3(len, Bytes.from(message)); }, /** * Ethereum-Compatible Keccak-256 Hash Function. @@ -39,22 +42,20 @@ 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); * ``` */ - ethereum(message: Field[]): Field[] { - return ethereum(message); + ethereum(message: FlexibleBytes) { + return ethereum(Bytes.from(message)); }, /** * Implementation of [pre-NIST Keccak](https://keccak.team/keccak.html) hash function. @@ -65,25 +66,25 @@ 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); * ``` * */ - preNist(len: 256 | 384 | 512, message: Field[]): Field[] { - return preNist(len, message); + preNist(len: 256 | 384 | 512, message: FlexibleBytes) { + return preNist(len, Bytes.from(message)); }, }; @@ -167,7 +168,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); @@ -177,8 +178,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 @@ -389,11 +390,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( @@ -411,7 +412,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.bytes, rate * BYTES_PER_WORD, nistVersion); const padded = bytesToWords(paddedBytes); const hash = sponge(padded, length, capacity, rate); @@ -421,18 +422,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); } @@ -493,27 +496,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); @@ -521,21 +533,14 @@ 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); } -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/keccak.unit-test.ts b/src/lib/keccak.unit-test.ts index 7caa5c166a..2e4c160b3f 100644 --- a/src/lib/keccak.unit-test.ts +++ b/src/lib/keccak.unit-test.ts @@ -1,9 +1,6 @@ -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 { equivalent, equivalentAsync } from './testing/equivalent.js'; import { keccak_224, keccak_256, @@ -14,6 +11,11 @@ import { sha3_384, 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; @@ -32,32 +34,101 @@ const testImplementations = { }, }; -const uint = (length: number) => fieldWithRng(Random.biguint(length)); +const lengths = [256, 384, 512] as const; + +// EQUIVALENCE TESTS AGAINST REF IMPLEMENTATION + +// checks outside circuit +// TODO: fix witness generation slowness + +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); + + equivalent({ from: [inputBytes], to: outputBytes, verbose: true })( + testImplementations.sha3[length], + (x) => Keccak.nistSha3(length, x), + `sha3 ${length}` + ); + + equivalent({ from: [inputBytes], to: outputBytes, verbose: true })( + testImplementations.preNist[length], + (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 = ([256, 384, 512] as const)[Math.floor(Math.random() * 4)]; +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', - publicInput: Provable.Array(Field, preImageLength), - publicOutput: Provable.Array(Field, digestLengthBytes), + name: `keccak-test-${digestLength}`, + 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); }, }, @@ -69,39 +140,128 @@ await KeccakProgram.compile(); // 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; +}); + +// 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', + }, + ]; +} diff --git a/src/lib/provable-types/bytes.ts b/src/lib/provable-types/bytes.ts new file mode 100644 index 0000000000..992e10bd1f --- /dev/null +++ b/src/lib/provable-types/bytes.ts @@ -0,0 +1,121 @@ +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, FlexibleBytes }; + +type FlexibleBytes = Bytes | (UInt8 | bigint | number)[] | Uint8Array; + +/** + * A provable type representing an array of bytes. + */ +class Bytes { + bytes: UInt8[]; + + constructor(bytes: UInt8[]) { + let size = (this.constructor as typeof Bytes).size; + + // assert that data is not too long + assert( + bytes.length <= size, + `Expected at most ${size} bytes, got ${bytes.length}` + ); + + // pad the data with zeros + let padding = Array.from( + { length: size - bytes.length }, + () => new UInt8(0) + ); + this.bytes = bytes.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 | 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.bytes.map((x) => x.toNumber())); + } + + toFields() { + return this.bytes.map((x) => x.value); + } + + /** + * 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(): string { + return this.bytes + .map((x) => x.toBigInt().toString(16).padStart(2, '0')) + .join(''); + } + + // dynamic subclassing infra + static _size?: number; + static _provable?: ProvablePureExtended< + Bytes, + { bytes: { value: string }[] } + >; + + /** + * The size of the {@link Bytes}. + */ + static get size() { + assert(this._size !== undefined, 'Bytes not initialized'); + return this._size; + } + + get length() { + return this.bytes.length; + } + + /** + * `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_, { + bytes: 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 0000000000..ad11e446b0 --- /dev/null +++ b/src/lib/provable-types/provable-types.ts @@ -0,0 +1,21 @@ +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); +} +Bytes.from = InternalBytes.from; +Bytes.fromHex = InternalBytes.fromHex; +Bytes.fromString = InternalBytes.fromString; diff --git a/src/lib/testing/equivalent.ts b/src/lib/testing/equivalent.ts index 2e8384d7da..7a0f20fd7d 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.` + ); + } }; } diff --git a/src/lib/testing/random.ts b/src/lib/testing/random.ts index 2880ec51f8..8dd41ac38f 100644 --- a/src/lib/testing/random.ts +++ b/src/lib/testing/random.ts @@ -66,6 +66,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); const byte = Random_(drawRandomByte); @@ -187,17 +188,20 @@ 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 }, 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 }) => ({ @@ -310,6 +314,7 @@ const Random = Object.assign(Random_, { field, otherField: fieldWithInvalid, bool, + uint8, uint32, uint64, biguint: biguintWithInvalid, diff --git a/src/lib/util/arrays.ts b/src/lib/util/arrays.ts new file mode 100644 index 0000000000..2a1a913eef --- /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('')); +} diff --git a/src/provable/poseidon-bigint.ts b/src/provable/poseidon-bigint.ts index 2ef684d585..906ab2a2d5 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); diff --git a/src/snarky.d.ts b/src/snarky.d.ts index 343d714c46..871fbeecaf 100644 --- a/src/snarky.d.ts +++ b/src/snarky.d.ts @@ -525,6 +525,7 @@ declare const Snarky: { }; }; + // TODO: implement in TS poseidon: { update( state: MlArray, diff --git a/tests/vk-regression/plain-constraint-system.ts b/tests/vk-regression/plain-constraint-system.ts index b5d0c1f447..67176a9f75 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, Bytes } from 'o1js'; -export { GroupCS, BitwiseCS }; +export { GroupCS, BitwiseCS, HashCS }; const GroupCS = constraintSystem('Group Primitive', { add() { @@ -84,6 +84,38 @@ const BitwiseCS = constraintSystem('Bitwise Primitive', { }, }); +const Bytes32 = Bytes(32); +const bytes32 = Bytes32.from([]); + +const HashCS = constraintSystem('Hashes', { + SHA256() { + let xs = Provable.witness(Bytes32.provable, () => bytes32); + Hash.SHA3_256.hash(xs); + }, + + SHA384() { + let xs = Provable.witness(Bytes32.provable, () => bytes32); + Hash.SHA3_384.hash(xs); + }, + + SHA512() { + let xs = Provable.witness(Bytes32.provable, () => bytes32); + Hash.SHA3_512.hash(xs); + }, + + Keccak256() { + let xs = Provable.witness(Bytes32.provable, () => bytes32); + 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( diff --git a/tests/vk-regression/vk-regression.json b/tests/vk-regression/vk-regression.json index 657960f923..1c3ae041bf 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": { diff --git a/tests/vk-regression/vk-regression.ts b/tests/vk-regression/vk-regression.ts index c23433b6dc..9c63c7e38e 100644 --- a/tests/vk-regression/vk-regression.ts +++ b/tests/vk-regression/vk-regression.ts @@ -7,7 +7,7 @@ import { ecdsa, keccakAndEcdsa, } from '../../src/examples/crypto/ecdsa/ecdsa.js'; -import { GroupCS, BitwiseCS } from './plain-constraint-system.js'; +import { GroupCS, BitwiseCS, HashCS } from './plain-constraint-system.js'; // toggle this for quick iteration when debugging vk regressions const skipVerificationKeys = false; @@ -42,6 +42,7 @@ const ConstraintSystems: MinimumConstraintSystem[] = [ createDex().Dex, GroupCS, BitwiseCS, + HashCS, ecdsa, keccakAndEcdsa, ];