diff --git a/.gitignore b/.gitignore index 2a32ed65..43df403f 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,5 @@ coverage *~ *temp + +.act.env diff --git a/contracts/package.json b/contracts/package.json index d05b662f..43e43a4f 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -30,6 +30,7 @@ "compact:security": "compact-compiler --dir security", "compact:token": "compact-compiler --dir token", "compact:utils": "compact-compiler --dir utils", + "compact:math": "compact-compiler --dir math", "build": "compact-builder", "test": "compact-compiler --skip-zk && vitest run", "types": "tsc -p tsconfig.json --noEmit", diff --git a/contracts/src/math/Bytes8.compact b/contracts/src/math/Bytes8.compact new file mode 100644 index 00000000..34555fe4 --- /dev/null +++ b/contracts/src/math/Bytes8.compact @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Midnight Apps Contracts v0.0.1-alpha.0 (math/Bytes8.compact) + +pragma language_version >= 0.21.0; + +/** + * @title Bytes8 module + * @description Canonical implementation for converting 8 bytes (as Vector or Bytes) to Uint<64> + * using little-endian (LE) byte ordering. + * + * @remarks + * Byte ordering convention (little-endian): + * - Element 0 is the least significant byte, element 7 is the most significant. + * - Works with Vector<8, Uint<8>> (e.g. from Uint64.toUnpackedBytes, or slices of Bytes32). + * + * Supported Operations: + * - Conversions: + * - pack(vec): Converts Vector<8, Uint<8>> to Bytes<8>. + * - unpack(bytes): Converts Bytes<8> to Vector<8, Uint<8>>. + * - toUint64(vec): Converts Vector<8, Uint<8>> to Uint<64>. + * - toUint64(bytes): Converts Bytes<8> to Uint<64>. + */ +module Bytes8 { + import { pack, unpack } from Pack<8> prefix Pack8_; + + //////////////////////////////////////////////////////////////// + // Conversions + //////////////////////////////////////////////////////////////// + + /** + * @title pack circuit + * @description Packs a Vector<8, Uint<8>> into a Bytes<8>. + * + * @remarks + * This circuit converts an 8-element vector of bytes to an 8-byte array + * using little-endian byte ordering. + * + * @circuitInfo k=12, rows=2412 + * + * @param {Vector<8, Uint<8>>} vec - The vector of 8 bytes to convert. + * + * @returns {Bytes<8>} - The 8-byte array. + */ + export circuit pack(vec: Vector<8, Uint<8>>): Bytes<8> { + return Pack8_pack(vec); + } + + /** + * @title unpack circuit + * @description Unpacks a Bytes<8> into a Vector<8, Uint<8>>. + * + * @remarks + * This circuit converts an 8-byte array to an 8-element vector of bytes + * using little-endian byte ordering. + * + * @circuitInfo k=12, rows=2486 + * + * @param {Bytes<8>} bytes - The 8-byte array to convert. + * + * @returns {Vector<8, Uint<8>>} - The 8-element vector of bytes. + */ + export circuit unpack(bytes: Bytes<8>): Vector<8, Uint<8>> { + return Pack8_unpack(bytes); + } + + /** + * @title _vector8ToUint64 internal circuit + * @description Converts a Vector<8, Uint<8>> to a Uint<64> using pure arithmetic. + * + * @remarks + * This circuit converts an 8-element vector of bytes to a 64-bit unsigned integer + * using little-endian byte ordering (element 0 is the LSB, element 7 is the MSB). + * + * @circuitInfo k=8, rows=193 + * + * @param {Vector<8, Uint<8>>} vec - The vector of 8 bytes to convert. + * + * @returns {Uint<64>} - The 64-bit unsigned integer. + */ + pure circuit _toUint64(vec: Vector<8, Uint<8>>): Uint<64> { + return vec[0] + + vec[1] * 0x100 + + vec[2] * 0x10000 + + vec[3] * 0x1000000 + + vec[4] * 0x100000000 + + vec[5] * 0x10000000000 + + vec[6] * 0x1000000000000 + + vec[7] * 0x100000000000000; + } + + /** + * @title toUint64 circuit (from Vector) + * @description Converts a Vector<8, Uint<8>> to a Uint<64> using pure arithmetic. + * + * @remarks + * This circuit converts an 8-element vector of bytes to a 64-bit unsigned integer + * using little-endian byte ordering (element 0 is the LSB, element 7 is the MSB). + * + * @circuitInfo k=8, rows=193 + * + * @param {Vector<8, Uint<8>>} vec - The vector of 8 bytes to convert. + * + * @returns {Uint<64>} - The 64-bit unsigned integer. + */ + export pure circuit toUint64(vec: Vector<8, Uint<8>>): Uint<64> { + return _toUint64(vec); + } + + /** + * @title toUint64 circuit (from Bytes) + * @description Converts a Bytes<8> to a Uint<64>. + * + * @remarks + * This circuit converts an 8-byte array to a 64-bit unsigned integer. + * The conversion is done by first unpacking to a vector via `unpack`, + * then converting to Uint<64>. + * + * @circuitInfo k=12, rows=2433 + * + * @param {Bytes<8>} bytes - The 8-byte array to convert. + * + * @returns {Uint<64>} - The 64-bit unsigned integer. + */ + export circuit toUint64(bytes: Bytes<8>): Uint<64> { + return _toUint64(unpack(bytes)); + } +} diff --git a/contracts/src/math/Pack.compact b/contracts/src/math/Pack.compact new file mode 100644 index 00000000..c17f1741 --- /dev/null +++ b/contracts/src/math/Pack.compact @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Midnight Apps Contracts v0.0.1-alpha.0 (math/Pack.compact) + +pragma language_version >= 0.21.0; + +/** + * @title Pack module + * @description Generic module for packing and unpacking between Vector> and Bytes + * for any byte length N. Uses little-endian byte ordering (element 0 is the LSB). + * + * @remarks + * This module has no dependencies and exists so that BytesN and UintN modules can import + * pack/unpack without creating cyclic dependencies (e.g. Bytes32 needs Uint256_lt for + * comparisons while Uint256 needs pack for toBytes). + * + * Instantiate with a size parameter, e.g. Pack<8>, Pack<16>, Pack<32>. + * + * Supported Operations: + * - pack(vec): Converts Vector> to Bytes. + * - unpack(bytes): Converts Bytes to Vector> (uses witness, then verifies in-circuit). + */ +module Pack<#N> { + /** + * @title wit_unpackBytes witness + * @description Unpacks Bytes into Vector> off-chain. Implementation is supplied + * in TypeScript; the unpack circuit verifies the result by re-packing and asserting equality. + * + * @param bytes - The byte array to unpack. + * @returns A vector of N bytes where element 0 is the LSB. + */ + witness wit_unpackBytes(bytes: Bytes): Vector>; + + /** + * @title pack circuit + * @description Packs a Vector> into Bytes. + * + * @remarks + * This pure circuit converts an N-element vector of bytes to an N-byte array + * using little-endian byte ordering. + * + * @circuitInfo depends on N: + * - Pack<8>: k=12, rows=2412 + * - Pack<16>: k=13, rows=5118 + * - Pack<32>: k=14, rows=10231 + * + * @param vec - The vector of N bytes to convert. + * @returns The byte array of length N. + */ + export pure circuit pack(vec: Vector>): Bytes { + return Bytes[...vec]; + } + + /** + * @title unpack circuit + * @description Unpacks Bytes into a Vector>. + * + * @remarks + * Calls the wit_unpackBytes witness off-chain, then verifies in-circuit that + * pack(vec) equals the input bytes. + * + * @circuitInfo depends on N: + * - Pack<8>: k=12, rows=2486 + * - Pack<16>: k=13, rows=5262 + * - Pack<32>: k=14, rows=10521 + * + * @param bytes - The byte array to unpack. + * @returns The vector of N bytes (element 0 is the LSB). + */ + export circuit unpack(bytes: Bytes): Vector> { + const vec = wit_unpackBytes(bytes); + assert(pack(vec) == bytes, "Pack: unpack verification failed"); + return vec; + } +} diff --git a/contracts/src/math/Types.compact b/contracts/src/math/Types.compact new file mode 100644 index 00000000..90ba200a --- /dev/null +++ b/contracts/src/math/Types.compact @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Midnight Apps Contracts v0.0.1-alpha.0 (math/types/Types.compact) + +pragma language_version >= 0.20.0; + +/** + * @title Types + * @dev Shared type definitions for U128 and U256 structs, used by Uint128 and Uint256 modules. + * + * These types exist in a separate module to avoid cyclic dependencies between Uint128 and + * Uint256. U256 is used as a return type for Uint128 operations that may overflow 128 bits + * (e.g. addition, multiplication), while U256 itself is composed of two U128 values. + * + * Import chain: + * - Types has no external dependencies (base types) + * - Uint128 imports U128 and U256 from Types + * - Uint256 imports U128 and U256 from Types + */ +module Types { + /** + * @description A struct representing a 128-bit unsigned integer as two 64-bit parts. + * The value is computed as: high * 2^64 + low + */ + export struct U128 { + /** + * @description The least significant 64 bits (bits 0-63) of the 128-bit number + */ + low: Uint<64>, + /** + * @description The most significant 64 bits (bits 64-127) of the 128-bit number + */ + high: Uint<64> + } + + /** + * @description A struct representing a 256-bit unsigned integer as two 128-bit parts. + * The value is computed as: high * 2^128 + low + */ + export struct U256 { + /** + * @description The least significant 128 bits (bits 0-127) of the 256-bit number + */ + low: U128, + /** + * @description The most significant 128 bits (bits 128-255) of the 256-bit number + */ + high: U128 + } +} diff --git a/contracts/src/math/Uint64.compact b/contracts/src/math/Uint64.compact new file mode 100644 index 00000000..397ea42a --- /dev/null +++ b/contracts/src/math/Uint64.compact @@ -0,0 +1,538 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Midnight Apps Contracts v0.0.1-alpha.0 (math/Uint64.compact) + +pragma language_version >= 0.20.0; + +/** + * @title Uint64 + * @description A utility module providing mathematical operations for unsigned integers. Functions operate on + * `Uint<64>` values in the range [0, 2^64 - 1]. The module supports arithmetic, division, square root, + * and utility functions with overflow and underflow checks where applicable. + * + * @remarks + * Supported Operations: + * - Constants: + * - DivResultU64: Struct with quotient and remainder from division. + * - MAX_UINT8(): Returns 2^8 - 1 (255). + * - MAX_UINT16(): Returns 2^16 - 1 (65,535). + * - MAX_UINT32(): Returns 2^32 - 1 (4,294,967,295). + * - MAX_UINT64(): Returns 2^64 - 1 (18,446,744,073,709,551,615). + * - Conversions: + * - toBytes(value): Converts Uint<64> to Bytes<8> (little-endian) + * - toUnpackedBytes(value): Converts Uint<64> to Vector<8, Uint<8>> (little-endian) + * - Arithmetic: + * - add(a, b): Adds two `Uint<64>` numbers, returning a `Uint<128>` result to handle potential overflow. + * - addChecked(a, b): Adds two `Uint<64>` numbers with overflow checking, returning a `Uint<64>` result. + * - sub(a, b): Subtracts one `Uint<64>` number from another, checking for underflow. + * - mul(a, b): Multiplies two `Uint<64>` numbers, returning a `Uint<128>` result. + * - mulChecked(a, b): Multiplies two `Uint<64>` numbers with overflow checking, returning a `Uint<64>` result. + * - Division: + * - div(a, b): Computes the quotient of dividing one `Uint<64>` number by another. + * - rem(a, b): Computes the remainder of dividing one `Uint<64>` number by another. + * - divRem(a, b): Computes both quotient and remainder of a division. + * - Square Root: + * - sqrt(radicand): Computes the floor of the square root of a `Uint<64>` number. + * - Utility: + * - isMultiple(a, b): Checks if one `Uint<64>` number is a multiple of another. + * - min(a, b): Returns the smaller of two `Uint<64>` numbers. + * - max(a, b): Returns the larger of two `Uint<64>` numbers. + */ +module Uint64 { + import { toUint64, pack } from Bytes8 prefix Bytes8_; + + //////////////////////////////////////////////////////////////// + // Constants + //////////////////////////////////////////////////////////////// + + /** + * @dev Struct containing the quotient and remainder from a division operation + */ + export struct DivResultU64 { + /** + * @dev The quotient result from the division operation + */ + quotient: Uint<64>, + /** + * @dev The remainder result from the division operation + */ + remainder: Uint<64> + } + + /** + * @description Computes division of two Uint<64> values locally (off-chain). + * + * @param a The number to divide. + * @param b The number to divide by. + * @returns DivResultU64 The quotient and remainder of the division. + */ + witness wit_divUint64(a: Uint<64>, b: Uint<64>): DivResultU64; + + /** + * @description Computes the square root of a Uint<64> value locally (off-chain). + * + * @param radicand The number to compute the square root of. + * @returns Uint<32> The square root of radicand. + */ + witness wit_sqrtUint64(radicand: Uint<64>): Uint<32>; + + /** + * @title wit_uint64ToUnpackedBytes witness + * @description Converts a Uint<64> into 8 individual bytes off-chain. + */ + witness wit_uint64ToUnpackedBytes(value: Uint<64>): Vector<8, Uint<8>>; + + /** + * @title MAX_UINT8 circuit + * @description Returns the maximum value for an 8-bit unsigned integer (2^8 - 1). + * + * @circuitInfo k=5, rows=25 + * + * @returns {Uint<8>} The value 255 (0xFF). + */ + export pure circuit MAX_UINT8(): Uint<8> { + return 0xFF; + } + + /** + * @title MAX_UINT16 circuit + * @description Returns the maximum value for a 16-bit unsigned integer (2^16 - 1). + * + * @circuitInfo k=5, rows=25 + * + * @returns {Uint<16>} The value 65,535 (0xFFFF). + */ + export pure circuit MAX_UINT16(): Uint<16> { + return 0xFFFF; + } + + /** + * @title MAX_UINT32 circuit + * @description Returns the maximum value for a 32-bit unsigned integer (2^32 - 1). + * + * @circuitInfo k=5, rows=25 + * + * @returns {Uint<32>} The value 4,294,967,295 (0xFFFFFFFF). + */ + export pure circuit MAX_UINT32(): Uint<32> { + return 0xFFFFFFFF; + } + + /** + * @title MAX_UINT64 circuit + * @description Returns the maximum value for a 64-bit unsigned integer (2^64 - 1). + * + * @circuitInfo k=5, rows=25 + * + * @returns {Uint<64>} The value 18,446,744,073,709,551,615 (0xFFFFFFFFFFFFFFFF). + */ + export pure circuit MAX_UINT64(): Uint<64> { + return 0xFFFFFFFFFFFFFFFF; + } + + //////////////////////////////////////////////////////////////// + // Conversions + //////////////////////////////////////////////////////////////// + + /** + * @title toBytes circuit + * @description Converts a Uint<64> to a Bytes<8> using little-endian byte ordering. + * + * @remarks + * This circuit converts a 64-bit unsigned integer to an 8-byte array. + * The conversion is done by first converting to unpacked bytes via `toUnpackedBytes`, + * then packing to `Bytes<8>` via `Bytes8.pack`. + * + * @circuitInfo k=12, rows=2433 + * + * @param {Uint<64>} value - The 64-bit value to convert. + * + * @throws {Error} "Uint64: toUnpackedBytes verification failed" if witness verification fails. + * + * @returns {Bytes<8>} - The 8-byte array representation. + */ + export circuit toBytes(value: Uint<64>): Bytes<8> { + const vec = toUnpackedBytes(value); + return Bytes8_pack(vec); + } + + /** + * @title toUnpackedBytes circuit + * @description Converts a Uint<64> to individual bytes as a Vector<8, Uint<8>>. + * + * @remarks + * This circuit converts a 64-bit unsigned integer into a vector of 8 individual bytes + * using little-endian byte ordering. The conversion is performed off-chain via a witness, + * then verified on-chain by packing the bytes back and checking equality. + * + * Uses `Bytes8_toUint64` for the packing verification. + * + * @circuitInfo k=9, rows=267 + * + * @param {Uint<64>} value - The 64-bit value to convert. + * + * @throws {Error} "Uint64: toUnpackedBytes verification failed" if the witness result doesn't match. + * + * @returns {Vector<8, Uint<8>>} - The vector of 8 individual bytes [b0, b1, b2, b3, b4, b5, b6, b7]. + */ + export circuit toUnpackedBytes(value: Uint<64>): Vector<8, Uint<8>> { + // Get bytes from witness as its cheaper + const vec = wit_uint64ToUnpackedBytes(value); + + // Verify by packing back using arithmetic + const reconstructed = Bytes8_toUint64(vec); + assert(reconstructed == value, "Uint64: toUnpackedBytes verification failed"); + + return vec; + } + + //////////////////////////////////////////////////////////////// + // Arithmetic + //////////////////////////////////////////////////////////////// + + /** + * @title Add circuit + * @description Adds two `Uint<64>` numbers, returning a `Uint<128>` result to accommodate potential overflow. + * + * @remarks + * Requirements: + * - `a` and `b` must be valid `Uint<64>` values in range [0, 2^64 - 1]. + * + * @circuitInfo k=8, rows=177 + * + * @param {Uint<64>} a - The first unsigned 64-bit integer. + * @param {Uint<64>} b - The second unsigned 64-bit integer. + * + * @returns {Uint<128>} The sum of `a` and `b` as a `Uint<128>` value. + */ + export pure circuit add(a: Uint<64>, b: Uint<64>): Uint<128> { + return a + b; + } + + /** + * @title Add Checked circuit + * @description Adds two `Uint<64>` numbers with overflow checking, returning a `Uint<64>` result. + * + * @remarks + * Requirements: + * - `a` and `b` must be valid `Uint<64>` values in range [0, 2^64 - 1]. + * - The sum of `a` and `b` must not exceed `MAX_UINT64` (2^64 - 1). + * + * @circuitInfo k=9, rows=298 + * + * @param {Uint<64>} a - The first unsigned 64-bit integer. + * @param {Uint<64>} b - The second unsigned 64-bit integer. + * + * @throws {Error} "Uint64: addition overflow" if `a + b > MAX_UINT64`. + * + * @returns {Uint<64>} The sum of `a` and `b` as a `Uint<64>` value. + */ + export pure circuit addChecked(a: Uint<64>, b: Uint<64>): Uint<64> { + const sum: Uint<128> = a + b; + assert(sum <= MAX_UINT64(), "Uint64: addition overflow"); + return sum as Uint<64>; + } + + /** + * @title Subtract circuit + * @description Subtracts `b` from `a`, checking for underflow to ensure the result is non-negative. + * + * @remarks + * Requirements: + * - `a` must be greater than or equal to `b` to prevent underflow. + * - Both inputs must be valid `Uint<64>` values. + * + * @circuitInfo k=9, rows=212 + * + * @param {Uint<64>} a - The unsigned 64-bit integer to subtract from (minuend). + * @param {Uint<64>} b - The unsigned 64-bit integer to subtract (subtrahend). + * + * @throws {Error} "Uint64: subtraction underflow" if `b > a`. + * + * @returns {Uint<64>} The difference `a - b` as a `Uint<64>` value. + */ + export pure circuit sub(a: Uint<64>, b: Uint<64>): Uint<64> { + assert(a >= b, "Uint64: subtraction underflow"); + return a - b; + } + + /** + * @title Multiply circuit + * @description Multiplies two `Uint<64>` values, returning a `Uint<128>` result to handle large products. + * + * @remarks + * Requirements: + * - `a` and `b` must be valid `Uint<64>` values. + * - Result is returned as `Uint<128>` to handle potential overflow. + * + * @circuitInfo k=8, rows=177 + * + * @param {Uint<64>} a - The first unsigned 64-bit integer (multiplicand). + * @param {Uint<64>} b - The second unsigned 64-bit integer (multiplier). + * + * @returns {Uint<128>} The product of `a` and `b` as a `Uint<128>` value. + */ + export pure circuit mul(a: Uint<64>, b: Uint<64>): Uint<128> { + return a * b; + } + + /** + * @title Multiply Checked circuit + * @description Multiplies two `Uint<64>` values with overflow checking, returning a `Uint<64>` result. + * + * @remarks + * Requirements: + * - `a` and `b` must be valid `Uint<64>` values in range [0, 2^64 - 1]. + * - The product of `a` and `b` must not exceed `MAX_UINT64` (2^64 - 1). + * + * @circuitInfo k=9, rows=298 + * + * @param {Uint<64>} a - The first unsigned 64-bit integer (multiplicand). + * @param {Uint<64>} b - The second unsigned 64-bit integer (multiplier). + * + * @throws {Error} "Uint64: multiplication overflow" if `a * b > MAX_UINT64`. + * + * @returns {Uint<64>} The product of `a` and `b` as a `Uint<64>` value. + */ + export pure circuit mulChecked(a: Uint<64>, b: Uint<64>): Uint<64> { + const product: Uint<128> = a * b; + assert(product <= MAX_UINT64(), "Uint64: multiplication overflow"); + return product as Uint<64>; + } + + //////////////////////////////////////////////////////////////// + // Division + //////////////////////////////////////////////////////////////// + + /** + * @title Internal Division circuit + * @description Internal circuit to divide a Uint<64> number by another, returning quotient and remainder. + * + * @remarks + * This circuit computes the quotient and remainder of dividing a 64-bit unsigned integer a by another b, + * both represented as Uint<64> values in [0, 2^64 - 1]. It returns a DivResultU64 struct containing the + * quotient and remainder, satisfying a = quotient * b + remainder, where 0 <= remainder < b. + * + * Mathematical Steps: + * 1. Check for division by zero. + * 2. Division Computation: + * - Compute result = (quotient, remainder) using wit_divUint64, where quotient = floor(a / b) + * and remainder = a mod b. + * 3. Verification: + * - Assert remainder < b, ensuring 0 <= remainder < b. + * - Assert quotient * b + remainder = a, ensuring correctness. + * 4. Result: + * - Return DivResultU64 { quotient, remainder }. + * + * Requirements: + * - `b` must not be zero. + * - `remainder` must be less than `b`. + * - `quotient * b + remainder` must equal `a`. + * + * @param {Uint<64>} a - The Uint<64> value to divide (dividend). + * @param {Uint<64>} b - The Uint<64> value to divide by (divisor). + * + * @throws {Error} "Uint64: division by zero" if b is zero. + * @throws {Error} "Uint64: remainder error" if remainder is not less than b. + * @throws {Error} "Uint64: division invalid" if quotient * b + remainder does not equal a. + * + * @returns {DivResultU64} A struct containing the quotient and remainder as Uint<64> values. + */ + circuit _div(a: Uint<64>, b: Uint<64>): DivResultU64 { + assert(b != 0, "Uint64: division by zero"); + + const result = wit_divUint64(a, b); + assert(result.remainder < b, "Uint64: remainder error"); + assert((result.quotient * b + result.remainder) as Uint<64> == a, "Uint64: division invalid"); + return result; + } + + /** + * @title Division circuit + * @description Divides a `Uint<64>` number `a` by `b`, returning the quotient. + * + * @remarks + * Requirements: + * - `b` must not be zero. + * - Uses internal `_div` circuit for computation. + * + * @circuitInfo k=9, rows=411 + * + * @param {Uint<64>} a - The unsigned 64-bit integer to divide (dividend). + * @param {Uint<64>} b - The unsigned 64-bit integer to divide by (divisor). + * + * @throws {Error} "Uint64: division by zero" if `b` is zero. + * @throws {Error} "Uint64: remainder error" if the division result is invalid. + * @throws {Error} "Uint64: division invalid" if the division result is invalid. + * + * @returns {Uint<64>} The quotient of `a` divided by `b` as a `Uint<64>` value. + */ + export circuit div(a: Uint<64>, b: Uint<64>): Uint<64> { + return _div(a, b).quotient; + } + + /** + * @title Remainder circuit + * @description Computes the remainder of dividing a `Uint<64>` number `a` by `b`. + * + * @remarks + * Requirements: + * - `b` must not be zero. + * - Uses internal `_div` circuit for computation. + * + * @circuitInfo k=9, rows=411 + * + * @param {Uint<64>} a - The unsigned 64-bit integer to divide (dividend). + * @param {Uint<64>} b - The unsigned 64-bit integer to divide by (divisor). + * + * @throws {Error} "Uint64: division by zero" if `b` is zero. + * @throws {Error} "Uint64: remainder error" if the division result is invalid. + * @throws {Error} "Uint64: division invalid" if the division result is invalid. + * + * @returns {Uint<64>} The remainder of `a` divided by `b` as a `Uint<64>` value. + */ + export circuit rem(a: Uint<64>, b: Uint<64>): Uint<64> { + return _div(a, b).remainder; + } + + /** + * @title Division with Remainder circuit + * @description Divides a Uint<64> number by another, returning both quotient and remainder. + * + * @remarks + * Requirements: + * - `b` must not be zero. + * - `remainder` must be less than `b`. + * - `quotient * b + remainder` must equal `a`. + * + * @circuitInfo k=9, rows=432 + * + * @param {Uint<64>} a - The Uint<64> value to divide (dividend). + * @param {Uint<64>} b - The Uint<64> value to divide by (divisor). + * + * @throws {Error} "Uint64: division by zero" if b is zero. + * @throws {Error} "Uint64: remainder error" if remainder is not less than b. + * @throws {Error} "Uint64: division invalid" if quotient * b + remainder does not equal a. + * + * @returns {DivResultU64} A struct containing the quotient and remainder as Uint<64> values. + */ + export circuit divRem(a: Uint<64>, b: Uint<64>): DivResultU64 { + return _div(a, b); + } + + //////////////////////////////////////////////////////////////// + // Square Root + //////////////////////////////////////////////////////////////// + + /** + * @title Square Root circuit + * @description Computes the floor of the square root of a Uint<64> value. + * + * @remarks + * This circuit calculates the floor of the square root R = floor(sqrt(N)) of a 64-bit unsigned integer + * N, provided as a Uint<64> value in [0, 2^64 - 1]. The result is a Uint<32> value R in [0, 2^32 - 1], + * such that R^2 <= N < (R + 1)^2. It uses a witness-based approach for the general case and includes + * special cases for common inputs to optimize performance. + * + * Mathematical Steps: + * 1. General Case Computation: + * - Compute R = floor(sqrt(N)) using sqrtLocally, where R is in [0, 2^32 - 1]. + * 2. Root Verification: + * - Compute rootSquare = R * R using mul. + * - Assert rootSquare <= N, ensuring R^2 <= N. + * 3. Next Value Verification: + * - Compute next = R + 1, where next is in [1, 2^32]. + * - Compute nextSquare = next * next using mul. + * - Assert nextSquare > N, ensuring (R + 1)^2 > N. + * 4. Result: + * - Return R as Uint<32>. + * + * Requirements: + * - `radicand` must be a valid `Uint<64>` value. + * - `rootSquare` must be less than or equal to `radicand`. + * - `nextSquare` must be greater than `radicand`. + * + * @circuitInfo k=9, rows=242 + * + * @param {Uint<64>} radicand - The Uint<64> value to compute the square root of. + * + * @throws {Error} "Uint64: sqrt overestimate" If R^2 > radicand. + * @throws {Error} "Uint64: sqrt underestimate" If (R + 1)^2 <= radicand. + * + * @returns {Uint<32>} The floor of the square root of radicand. + */ + export circuit sqrt(radicand: Uint<64>): Uint<32> { + const root = wit_sqrtUint64(radicand); + const rootSquare = mul(root, root); + assert(rootSquare <= radicand, "Uint64: sqrt overestimate"); + + const next = root + 1; + const nextSquare = mul(next, next); + assert(nextSquare > radicand, "Uint64: sqrt underestimate"); + + return root; + } + + //////////////////////////////////////////////////////////////// + // Utilities + //////////////////////////////////////////////////////////////// + + /** + * @title Is Multiple circuit + * @description Checks if a `Uint<64>` number is a multiple of another. + * + * @remarks + * Requirements: + * - `b` must not be zero. + * - Uses `rem` circuit to check if remainder is zero. + * + * @circuitInfo k=9, rows=413 + * + * @param {Uint<64>} a - The unsigned 64-bit integer to check. + * @param {Uint<64>} b - The unsigned 64-bit integer divisor. + * + * @throws {Error} "Uint64: division by zero" if `b` is zero. + * + * @returns {Boolean} `true` if `a` is a multiple of `b`, `false` otherwise. + */ + export circuit isMultiple(a: Uint<64>, b: Uint<64>): Boolean { + return rem(a, b) == 0; + } + + /** + * @title Min circuit + * @description Returns the minimum of two Uint<64> values. + * + * @remarks + * Requirements: + * - `a` and `b` must be valid `Uint<64>` values. + * + * @circuitInfo k=9, rows=208 + * + * @param {Uint<64>} a - The first unsigned 64-bit integer. + * @param {Uint<64>} b - The second unsigned 64-bit integer. + * + * @returns {Uint<64>} The smaller of `a` and `b` as a `Uint<64>` value. + */ + export pure circuit min(a: Uint<64>, b: Uint<64>): Uint<64> { + return a < b ? a : b; + } + + /** + * @title Max circuit + * @description Returns the maximum of two Uint<64> values. + * + * @remarks + * Requirements: + * - `a` and `b` must be valid `Uint<64>` values. + * + * @circuitInfo k=9, rows=208 + * + * @param {Uint<64>} a - The first unsigned 64-bit integer. + * @param {Uint<64>} b - The second unsigned 64-bit integer. + * + * @returns {Uint<64>} The larger of `a` and `b` as a `Uint<64>` value. + */ + export pure circuit max(a: Uint<64>, b: Uint<64>): Uint<64> { + return a > b ? a : b; + } +} diff --git a/contracts/src/math/test/Bytes8.test.ts b/contracts/src/math/test/Bytes8.test.ts new file mode 100644 index 00000000..09ca712f --- /dev/null +++ b/contracts/src/math/test/Bytes8.test.ts @@ -0,0 +1,196 @@ +import { Bytes8Simulator } from '@src/math/test/mocks/Bytes8Simulator.js'; +import { MAX_UINT64 } from '@src/math/utils/consts.js'; +import { beforeEach, describe, expect, test } from 'vitest'; + +let bytes8Simulator: Bytes8Simulator; + +const setup = () => { + bytes8Simulator = new Bytes8Simulator(); +}; + +type Bytes8 = [bigint, bigint, bigint, bigint, bigint, bigint, bigint, bigint]; + +const bytes = (...values: number[]): Bytes8 => { + const a = [...values]; + while (a.length < 8) a.push(0); + return a.map((x) => BigInt(x)) as Bytes8; +}; + +describe('Bytes8', () => { + beforeEach(setup); + + describe('conversions', () => { + describe('pack', () => { + test('should convert zero vector to zero bytes', () => { + const result = bytes8Simulator.pack(bytes(0, 0, 0, 0, 0, 0, 0, 0)); + expect(result).toEqual(new Uint8Array(8).fill(0)); + }); + + test('should match vector elements as bytes', () => { + const v = bytes(1, 2, 3, 4, 5, 6, 7, 8); + const result = bytes8Simulator.pack(v); + expect(result.length).toBe(8); + for (let i = 0; i < 8; i++) { + expect(result[i]).toBe(Number(v[i])); + } + }); + + test('should roundtrip with vectorToUint64', () => { + const v = bytes(0xef, 0xcd, 0xab, 0x89, 0x67, 0x45, 0x23, 0x01); + const asU64 = bytes8Simulator.vectorToUint64(v); + const backBytes = bytes8Simulator.pack(v); + expect(asU64).toBe(0x0123456789abcdefn); + const fromBack = Array.from(backBytes).reduce( + (acc, b, i) => acc + (BigInt(b) << (8n * BigInt(i))), + 0n, + ); + expect(fromBack).toBe(asU64); + }); + }); + + describe('unpack', () => { + test('should unpack bytes to vector matching pack roundtrip', () => { + const v = bytes(0xef, 0xcd, 0xab, 0x89, 0x67, 0x45, 0x23, 0x01); + const packed = bytes8Simulator.pack(v); + const unpacked = bytes8Simulator.unpack(packed); + expect(unpacked).toEqual(v); + }); + + test('should unpack zero bytes to zero vector', () => { + const packed = new Uint8Array(8).fill(0); + const unpacked = bytes8Simulator.unpack(packed); + expect(unpacked).toEqual(bytes(0, 0, 0, 0, 0, 0, 0, 0)); + }); + + test('should fail when witness returns pack(vec) != bytes', () => { + bytes8Simulator.overrideWitness( + 'wit_unpackBytes', + (context, _bytes) => [ + context.privateState, + bytes(0, 0, 0, 0, 0, 0, 0, 0), + ], + ); + const packed = new Uint8Array(8); + packed[0] = 1; + expect(() => bytes8Simulator.unpack(packed)).toThrow( + 'failed assert: Pack: unpack verification failed', + ); + }); + }); + + describe('vectorToUint64', () => { + test('should convert zero bytes to zero', () => { + expect( + bytes8Simulator.vectorToUint64(bytes(0, 0, 0, 0, 0, 0, 0, 0)), + ).toBe(0n); + }); + + test('should place single byte at b0', () => { + expect(bytes8Simulator.vectorToUint64(bytes(0xab))).toBe(0xabn); + }); + + test('should place single byte at b1 through b7', () => { + expect(bytes8Simulator.vectorToUint64(bytes(0, 1))).toBe(0x100n); + expect(bytes8Simulator.vectorToUint64(bytes(0, 0, 1))).toBe(0x10000n); + expect(bytes8Simulator.vectorToUint64(bytes(0, 0, 0, 1))).toBe( + 0x1000000n, + ); + expect(bytes8Simulator.vectorToUint64(bytes(0, 0, 0, 0, 1))).toBe( + 0x100000000n, + ); + expect(bytes8Simulator.vectorToUint64(bytes(0, 0, 0, 0, 0, 1))).toBe( + 0x10000000000n, + ); + expect(bytes8Simulator.vectorToUint64(bytes(0, 0, 0, 0, 0, 0, 1))).toBe( + 0x1000000000000n, + ); + expect( + bytes8Simulator.vectorToUint64(bytes(0, 0, 0, 0, 0, 0, 0, 1)), + ).toBe(0x100000000000000n); + }); + + test('should convert MAX_UINT64 all-0xFF bytes', () => { + const allFF: Bytes8 = [255n, 255n, 255n, 255n, 255n, 255n, 255n, 255n]; + expect(bytes8Simulator.vectorToUint64(allFF)).toBe(MAX_UINT64); + }); + + test('should convert arbitrary multi-byte value', () => { + const b = bytes(0xef, 0xcd, 0xab, 0x89, 0x67, 0x45, 0x23, 0x01); + expect(bytes8Simulator.vectorToUint64(b)).toBe(0x0123456789abcdefn); + }); + }); + + describe('bytesToUint64', () => { + test('should convert zero bytes to zero', () => { + const packed = new Uint8Array(8).fill(0); + expect(bytes8Simulator.bytesToUint64(packed)).toBe(0n); + }); + + test('should place single byte at b0', () => { + const packed = bytes8Simulator.pack(bytes(0xab)); + expect(bytes8Simulator.bytesToUint64(packed)).toBe(0xabn); + }); + + test('should place single byte at b1 through b7', () => { + expect( + bytes8Simulator.bytesToUint64(bytes8Simulator.pack(bytes(0, 1))), + ).toBe(0x100n); + expect( + bytes8Simulator.bytesToUint64(bytes8Simulator.pack(bytes(0, 0, 1))), + ).toBe(0x10000n); + expect( + bytes8Simulator.bytesToUint64( + bytes8Simulator.pack(bytes(0, 0, 0, 1)), + ), + ).toBe(0x1000000n); + expect( + bytes8Simulator.bytesToUint64( + bytes8Simulator.pack(bytes(0, 0, 0, 0, 1)), + ), + ).toBe(0x100000000n); + expect( + bytes8Simulator.bytesToUint64( + bytes8Simulator.pack(bytes(0, 0, 0, 0, 0, 1)), + ), + ).toBe(0x10000000000n); + expect( + bytes8Simulator.bytesToUint64( + bytes8Simulator.pack(bytes(0, 0, 0, 0, 0, 0, 1)), + ), + ).toBe(0x1000000000000n); + expect( + bytes8Simulator.bytesToUint64( + bytes8Simulator.pack(bytes(0, 0, 0, 0, 0, 0, 0, 1)), + ), + ).toBe(0x100000000000000n); + }); + + test('should convert MAX_UINT64 all-0xFF bytes', () => { + const allFF: Bytes8 = [255n, 255n, 255n, 255n, 255n, 255n, 255n, 255n]; + const packed = bytes8Simulator.pack(allFF); + expect(bytes8Simulator.bytesToUint64(packed)).toBe(MAX_UINT64); + }); + + test('should convert arbitrary multi-byte value', () => { + const b = bytes(0xef, 0xcd, 0xab, 0x89, 0x67, 0x45, 0x23, 0x01); + const packed = bytes8Simulator.pack(b); + expect(bytes8Simulator.bytesToUint64(packed)).toBe(0x0123456789abcdefn); + }); + + test('should fail when witness returns pack(vec) != bytes', () => { + bytes8Simulator.overrideWitness( + 'wit_unpackBytes', + (context, _bytes) => [ + context.privateState, + bytes(0, 0, 0, 0, 0, 0, 0, 0), + ], + ); + const packed = new Uint8Array(8); + packed[0] = 1; + expect(() => bytes8Simulator.bytesToUint64(packed)).toThrow( + 'failed assert: Pack: unpack verification failed', + ); + }); + }); + }); +}); diff --git a/contracts/src/math/test/Pack.test.ts b/contracts/src/math/test/Pack.test.ts new file mode 100644 index 00000000..94b412a8 --- /dev/null +++ b/contracts/src/math/test/Pack.test.ts @@ -0,0 +1,178 @@ +import { PackSimulator } from '@src/math/test/mocks/PackSimulator.js'; +import { beforeEach, describe, expect, test } from 'vitest'; + +let packSimulator: PackSimulator; + +const setup = () => { + packSimulator = new PackSimulator(); +}; + +type Vec8 = [bigint, bigint, bigint, bigint, bigint, bigint, bigint, bigint]; + +const vec8 = (...values: number[]): Vec8 => { + const a = [...values]; + while (a.length < 8) a.push(0); + return a.map((x) => BigInt(x)) as Vec8; +}; + +const vec16 = (value: bigint): bigint[] => { + const vec = new Array(16).fill(0n); + for (let i = 0; i < 16; i++) { + vec[i] = (value >> (8n * BigInt(i))) & 0xffn; + } + return vec; +}; + +const vec32 = (value: bigint): bigint[] => { + const vec = new Array(32).fill(0n); + for (let i = 0; i < 32; i++) { + vec[i] = (value >> (8n * BigInt(i))) & 0xffn; + } + return vec; +}; + +describe('Pack', () => { + beforeEach(setup); + + describe('Bytes8', () => { + describe('pack8', () => { + test('should convert zero vector to zero bytes', () => { + const result = packSimulator.pack8(vec8(0, 0, 0, 0, 0, 0, 0, 0)); + expect(result).toEqual(new Uint8Array(8).fill(0)); + }); + + test('should match vector elements as bytes', () => { + const v = vec8(1, 2, 3, 4, 5, 6, 7, 8); + const result = packSimulator.pack8(v); + expect(result.length).toBe(8); + for (let i = 0; i < 8; i++) { + expect(result[i]).toBe(Number(v[i])); + } + }); + }); + + describe('unpack8', () => { + test('should roundtrip: unpack8(pack8(vec)) equals vec', () => { + const v = vec8(0xef, 0xcd, 0xab, 0x89, 0x67, 0x45, 0x23, 0x01); + const packed = packSimulator.pack8(v); + const unpacked = packSimulator.unpack8(packed); + expect(unpacked).toEqual(v); + }); + + test('should unpack zero bytes to zero vector', () => { + const packed = new Uint8Array(8).fill(0); + const unpacked = packSimulator.unpack8(packed); + expect(unpacked).toEqual(vec8(0, 0, 0, 0, 0, 0, 0, 0)); + }); + + test('should fail when witness returns pack(vec) != bytes', () => { + packSimulator.overrideWitness( + 'wit_unpackBytes', + (_context: unknown, _bytes: Uint8Array) => [ + {}, + vec8(0, 0, 0, 0, 0, 0, 0, 0), + ], + ); + const packed = new Uint8Array(8); + packed[0] = 1; + expect(() => packSimulator.unpack8(packed)).toThrow( + 'failed assert: Pack: unpack verification failed', + ); + }); + }); + }); + + describe('Bytes16', () => { + describe('pack16', () => { + test('should convert zero vector to zero bytes', () => { + const result = packSimulator.pack16(vec16(0n)); + expect(result).toEqual(new Uint8Array(16).fill(0)); + }); + + test('should match vector elements as bytes', () => { + const value = 0x0123456789abcdefn; + const v = vec16(value); + const result = packSimulator.pack16(v); + expect(result.length).toBe(16); + for (let i = 0; i < 16; i++) { + expect(result[i]).toBe(Number(v[i])); + } + }); + }); + + describe('unpack16', () => { + test('should roundtrip: unpack16(pack16(vec)) equals vec', () => { + const value = 0x0123456789abcdefn; + const v = vec16(value); + const packed = packSimulator.pack16(v); + const unpacked = packSimulator.unpack16(packed); + expect(unpacked).toEqual(v); + }); + + test('should unpack zero bytes to zero vector', () => { + const packed = new Uint8Array(16).fill(0); + const unpacked = packSimulator.unpack16(packed); + expect(unpacked).toEqual(vec16(0n)); + }); + + test('should fail when witness returns pack(vec) != bytes', () => { + packSimulator.overrideWitness( + 'wit_unpackBytes', + (_context: unknown, _bytes: Uint8Array) => [{}, vec16(0n)], + ); + const packed = new Uint8Array(16); + packed[0] = 1; + expect(() => packSimulator.unpack16(packed)).toThrow( + 'failed assert: Pack: unpack verification failed', + ); + }); + }); + }); + + describe('Bytes32', () => { + describe('pack32', () => { + test('should convert zero vector to zero bytes', () => { + const result = packSimulator.pack32(vec32(0n)); + expect(result).toEqual(new Uint8Array(32).fill(0)); + }); + + test('should match vector elements as bytes', () => { + const value = 1n + (2n << 64n) + (3n << 128n) + (4n << 192n); + const v = vec32(value); + const result = packSimulator.pack32(v); + expect(result.length).toBe(32); + for (let i = 0; i < 32; i++) { + expect(result[i]).toBe(Number(v[i])); + } + }); + }); + + describe('unpack32', () => { + test('should roundtrip: unpack32(pack32(vec)) equals vec', () => { + const value = 1n + (2n << 64n) + (3n << 128n) + (4n << 192n); + const v = vec32(value); + const packed = packSimulator.pack32(v); + const unpacked = packSimulator.unpack32(packed); + expect(unpacked).toEqual(v); + }); + + test('should unpack zero bytes to zero vector', () => { + const packed = new Uint8Array(32).fill(0); + const unpacked = packSimulator.unpack32(packed); + expect(unpacked).toEqual(vec32(0n)); + }); + + test('should fail when witness returns pack(vec) != bytes', () => { + packSimulator.overrideWitness( + 'wit_unpackBytes', + (_context: unknown, _bytes: Uint8Array) => [{}, vec32(0n)], + ); + const packed = new Uint8Array(32); + packed[0] = 1; + expect(() => packSimulator.unpack32(packed)).toThrow( + 'failed assert: Pack: unpack verification failed', + ); + }); + }); + }); +}); diff --git a/contracts/src/math/test/Uint64.test.ts b/contracts/src/math/test/Uint64.test.ts new file mode 100644 index 00000000..26c0ded1 --- /dev/null +++ b/contracts/src/math/test/Uint64.test.ts @@ -0,0 +1,598 @@ +import { Uint64Simulator } from '@src/math/test/mocks/Uint64Simulator.js'; +import { MAX_UINT32, MAX_UINT64 } from '@src/math/utils/consts.js'; +import { beforeEach, describe, expect, test } from 'vitest'; + +let uint64Simulator: Uint64Simulator; + +const setup = () => { + uint64Simulator = new Uint64Simulator(); +}; + +describe('Uint64', () => { + beforeEach(setup); + + describe('constants', () => { + describe('MAX_UINT8', () => { + test('should return 255', () => { + expect(uint64Simulator.MAX_UINT8()).toBe(0xffn); + }); + }); + + describe('MAX_UINT16', () => { + test('should return 65535', () => { + expect(uint64Simulator.MAX_UINT16()).toBe(0xffffn); + }); + }); + + describe('MAX_UINT32', () => { + test('should return 4294967295', () => { + expect(uint64Simulator.MAX_UINT32()).toBe(0xffffffffn); + }); + }); + + describe('MAX_UINT64', () => { + test('should return 18446744073709551615', () => { + expect(uint64Simulator.MAX_UINT64()).toBe(0xffffffffffffffffn); + }); + }); + }); + + describe('conversions', () => { + describe('toBytes', () => { + test('should convert zero to zero bytes', () => { + const bytes = uint64Simulator.toBytes(0n); + expect(bytes).toEqual(new Uint8Array(8).fill(0)); + }); + + test('should convert small value correctly', () => { + const bytes = uint64Simulator.toBytes(123n); + expect(bytes[0]).toBe(123); + expect(bytes.slice(1)).toEqual(new Uint8Array(7).fill(0)); + }); + + test('should convert MAX_UINT64 to all-0xFF bytes', () => { + const bytes = uint64Simulator.toBytes(MAX_UINT64); + expect(bytes).toEqual(new Uint8Array(8).fill(255)); + }); + + test('should match outputs between toUnpackedBytes and toBytes', () => { + const value = 0x0123456789abcdefn; + const vec = uint64Simulator.toUnpackedBytes(value); + const bytes = uint64Simulator.toBytes(value); + for (let i = 0; i < 8; i++) { + expect(Number(vec[i])).toBe(bytes[i]); + } + }); + + test('should fail when witness returns Bytes8_toUint64(vec) != value', () => { + uint64Simulator.overrideWitness( + 'wit_uint64ToUnpackedBytes', + (context, _value) => [ + context.privateState, + [0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n], + ], + ); + expect(() => uint64Simulator.toBytes(123n)).toThrow( + 'failed assert: Uint64: toUnpackedBytes verification failed', + ); + }); + }); + + describe('toUnpackedBytes', () => { + test('should convert zero to all-zero vector', () => { + const vec = uint64Simulator.toUnpackedBytes(0n); + expect(vec).toEqual([0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n]); + }); + + test('should convert small value correctly', () => { + const vec = uint64Simulator.toUnpackedBytes(0x01_02_03n); + expect(vec[0]).toBe(3n); + expect(vec[1]).toBe(2n); + expect(vec[2]).toBe(1n); + expect(vec.slice(3)).toEqual([0n, 0n, 0n, 0n, 0n]); + }); + + test('should convert MAX_UINT64 to all-0xFF vector', () => { + const vec = uint64Simulator.toUnpackedBytes(MAX_UINT64); + expect(vec).toEqual([255n, 255n, 255n, 255n, 255n, 255n, 255n, 255n]); + }); + + test('should place single byte at each position', () => { + expect(uint64Simulator.toUnpackedBytes(1n)[0]).toBe(1n); + expect(uint64Simulator.toUnpackedBytes(0x100n)[1]).toBe(1n); + expect(uint64Simulator.toUnpackedBytes(0x10000n)[2]).toBe(1n); + expect(uint64Simulator.toUnpackedBytes(0x1000000n)[3]).toBe(1n); + expect(uint64Simulator.toUnpackedBytes(0x100000000n)[4]).toBe(1n); + expect(uint64Simulator.toUnpackedBytes(0x10000000000n)[5]).toBe(1n); + expect(uint64Simulator.toUnpackedBytes(0x1000000000000n)[6]).toBe(1n); + expect(uint64Simulator.toUnpackedBytes(0x100000000000000n)[7]).toBe(1n); + }); + + test('should fail when witness returns Bytes8_toUint64(vec) != value', () => { + uint64Simulator.overrideWitness( + 'wit_uint64ToUnpackedBytes', + (context, _value) => [ + context.privateState, + [0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n], + ], + ); + expect(() => uint64Simulator.toUnpackedBytes(123n)).toThrow( + 'failed assert: Uint64: toUnpackedBytes verification failed', + ); + }); + }); + }); + + describe('arithmetic', () => { + describe('Add', () => { + test('should add two numbers', () => { + expect(uint64Simulator.add(5n, 3n)).toBe(8n); + }); + + test('should not overflow', () => { + expect(uint64Simulator.add(MAX_UINT64, MAX_UINT64)).toBe( + MAX_UINT64 * 2n, + ); + }); + }); + + describe('AddChecked', () => { + test('should add two small numbers', () => { + expect(uint64Simulator.addChecked(5n, 3n)).toBe(8n); + }); + + test('should add zero', () => { + expect(uint64Simulator.addChecked(5n, 0n)).toBe(5n); + expect(uint64Simulator.addChecked(0n, 5n)).toBe(5n); + }); + + test('should add at boundary without overflow', () => { + expect(uint64Simulator.addChecked(MAX_UINT64 - 1n, 1n)).toBe( + MAX_UINT64, + ); + expect(uint64Simulator.addChecked(1n, MAX_UINT64 - 1n)).toBe( + MAX_UINT64, + ); + }); + + test('should fail on overflow', () => { + expect(() => uint64Simulator.addChecked(MAX_UINT64, 1n)).toThrowError( + 'failed assert: Uint64: addition overflow', + ); + }); + + test('should fail on large overflow', () => { + expect(() => + uint64Simulator.addChecked(MAX_UINT64, MAX_UINT64), + ).toThrowError('failed assert: Uint64: addition overflow'); + }); + + test('should handle half max values without overflow', () => { + const halfMax = MAX_UINT64 / 2n; + expect(uint64Simulator.addChecked(halfMax, halfMax)).toBe(halfMax * 2n); + }); + }); + + describe('Sub', () => { + test('should subtract two numbers', () => { + expect(uint64Simulator.sub(10n, 4n)).toBe(6n); + }); + + test('should subtract zero', () => { + expect(uint64Simulator.sub(5n, 0n)).toBe(5n); + expect(uint64Simulator.sub(0n, 0n)).toBe(0n); + }); + + test('should subtract from zero', () => { + expect(() => uint64Simulator.sub(0n, 5n)).toThrowError( + 'failed assert: Uint64: subtraction underflow', + ); + }); + + test('should subtract max Uint<64> minus 1', () => { + expect(uint64Simulator.sub(MAX_UINT64, 1n)).toBe(MAX_UINT64 - 1n); + }); + + test('should subtract max Uint<64> minus itself', () => { + expect(uint64Simulator.sub(MAX_UINT64, MAX_UINT64)).toBe(0n); + }); + + test('should fail on underflow with small numbers', () => { + expect(() => uint64Simulator.sub(3n, 5n)).toThrowError( + 'failed assert: Uint64: subtraction underflow', + ); + }); + + test('should fail on underflow with large numbers', () => { + expect(() => + uint64Simulator.sub(MAX_UINT64 - 10n, MAX_UINT64), + ).toThrowError('failed assert: Uint64: subtraction underflow'); + }); + }); + + describe('Mul', () => { + test('should multiply two numbers', () => { + expect(uint64Simulator.mul(4n, 3n)).toBe(12n); + }); + + test('should handle max Uint<64> times 1', () => { + expect(uint64Simulator.mul(MAX_UINT64, 1n)).toBe(MAX_UINT64); + }); + + test('should handle max Uint<64> times max Uint<64> without overflow', () => { + expect(uint64Simulator.mul(MAX_UINT64, MAX_UINT64)).toBe( + MAX_UINT64 * MAX_UINT64, + ); + }); + }); + + describe('MulChecked', () => { + test('should multiply two small numbers', () => { + expect(uint64Simulator.mulChecked(4n, 3n)).toBe(12n); + }); + + test('should multiply by zero', () => { + expect(uint64Simulator.mulChecked(5n, 0n)).toBe(0n); + expect(uint64Simulator.mulChecked(0n, 5n)).toBe(0n); + }); + + test('should multiply by one', () => { + expect(uint64Simulator.mulChecked(MAX_UINT64, 1n)).toBe(MAX_UINT64); + expect(uint64Simulator.mulChecked(1n, MAX_UINT64)).toBe(MAX_UINT64); + }); + + test('should multiply at boundary without overflow', () => { + // sqrt(MAX_UINT64) ≈ 4294967295, so 4294967295 * 4294967295 should be within range + const sqrtMax = MAX_UINT32; + expect(uint64Simulator.mulChecked(sqrtMax, sqrtMax)).toBe( + sqrtMax * sqrtMax, + ); + }); + + test('should fail on overflow', () => { + expect(() => uint64Simulator.mulChecked(MAX_UINT64, 2n)).toThrowError( + 'failed assert: Uint64: multiplication overflow', + ); + }); + + test('should fail on large overflow', () => { + expect(() => + uint64Simulator.mulChecked(MAX_UINT64, MAX_UINT64), + ).toThrowError('failed assert: Uint64: multiplication overflow'); + }); + + test('should fail when product exceeds MAX_UINT64', () => { + // MAX_UINT32 + 1 = 2^32, and (2^32)^2 = 2^64 which overflows + const sqrtMaxPlusOne = MAX_UINT32 + 1n; + expect(() => + uint64Simulator.mulChecked(sqrtMaxPlusOne, sqrtMaxPlusOne), + ).toThrowError('failed assert: Uint64: multiplication overflow'); + }); + }); + }); + + describe('division', () => { + describe('div', () => { + test('should divide small numbers', () => { + expect(uint64Simulator.div(10n, 3n)).toBe(3n); + }); + + test('should handle dividend is zero', () => { + expect(uint64Simulator.div(0n, 5n)).toBe(0n); + }); + + test('should handle divisor is one', () => { + expect(uint64Simulator.div(10n, 1n)).toBe(10n); + }); + + test('should handle dividend equals divisor', () => { + expect(uint64Simulator.div(5n, 5n)).toBe(1n); + }); + + test('should handle dividend less than divisor', () => { + expect(uint64Simulator.div(3n, 5n)).toBe(0n); + }); + + test('should handle large division', () => { + expect(uint64Simulator.div(MAX_UINT64, 2n)).toBe(MAX_UINT64 / 2n); + }); + + test('should fail on division by zero', () => { + expect(() => uint64Simulator.div(5n, 0n)).toThrowError( + 'failed assert: Uint64: division by zero', + ); + }); + + test('should fail when remainder >= divisor', () => { + uint64Simulator.overrideWitness('wit_divUint64', (context) => [ + context.privateState, + { quotient: 1n, remainder: 10n }, + ]); + expect(() => uint64Simulator.div(10n, 5n)).toThrow( + 'failed assert: Uint64: remainder error', + ); + }); + + test('should fail when quotient * b + remainder != a', () => { + uint64Simulator.overrideWitness('wit_divUint64', (context) => [ + context.privateState, + { quotient: 1n, remainder: 1n }, + ]); + expect(() => uint64Simulator.div(10n, 5n)).toThrow( + 'failed assert: Uint64: division invalid', + ); + }); + }); + + describe('rem', () => { + test('should compute remainder of small numbers', () => { + expect(uint64Simulator.rem(10n, 3n)).toBe(1n); + }); + + test('should handle dividend is zero', () => { + expect(uint64Simulator.rem(0n, 5n)).toBe(0n); + }); + + test('should handle divisor is one', () => { + expect(uint64Simulator.rem(10n, 1n)).toBe(0n); + }); + + test('should handle dividend equals divisor', () => { + expect(uint64Simulator.rem(5n, 5n)).toBe(0n); + }); + + test('should handle dividend less than divisor', () => { + expect(uint64Simulator.rem(3n, 5n)).toBe(3n); + }); + + test('should compute remainder of max U64 by 2', () => { + expect(uint64Simulator.rem(MAX_UINT64, 2n)).toBe(1n); + }); + + test('should handle zero remainder', () => { + expect(uint64Simulator.rem(6n, 3n)).toBe(0n); + }); + + test('should fail on division by zero', () => { + expect(() => uint64Simulator.rem(5n, 0n)).toThrowError( + 'failed assert: Uint64: division by zero', + ); + }); + + test('should fail when remainder >= divisor', () => { + uint64Simulator.overrideWitness('wit_divUint64', (context) => [ + context.privateState, + { quotient: 1n, remainder: 5n }, + ]); + expect(() => uint64Simulator.rem(10n, 5n)).toThrow( + 'failed assert: Uint64: remainder error', + ); + }); + + test('should fail when quotient * b + remainder != a', () => { + uint64Simulator.overrideWitness('wit_divUint64', (context) => [ + context.privateState, + { quotient: 0n, remainder: 2n }, + ]); + expect(() => uint64Simulator.rem(10n, 5n)).toThrow( + 'failed assert: Uint64: division invalid', + ); + }); + }); + + describe('divRem', () => { + test('should compute quotient and remainder of small numbers', () => { + const result = uint64Simulator.divRem(10n, 3n); + expect(result.quotient).toBe(3n); + expect(result.remainder).toBe(1n); + }); + + test('should handle dividend is zero', () => { + const result = uint64Simulator.divRem(0n, 5n); + expect(result.quotient).toBe(0n); + expect(result.remainder).toBe(0n); + }); + + test('should handle divisor is one', () => { + const result = uint64Simulator.divRem(10n, 1n); + expect(result.quotient).toBe(10n); + expect(result.remainder).toBe(0n); + }); + + test('should handle dividend equals divisor', () => { + const result = uint64Simulator.divRem(5n, 5n); + expect(result.quotient).toBe(1n); + expect(result.remainder).toBe(0n); + }); + + test('should handle dividend less than divisor', () => { + const result = uint64Simulator.divRem(3n, 5n); + expect(result.quotient).toBe(0n); + expect(result.remainder).toBe(3n); + }); + + test('should compute quotient and remainder of max U64 by 2', () => { + const result = uint64Simulator.divRem(MAX_UINT64, 2n); + expect(result.quotient).toBe(MAX_UINT64 / 2n); + expect(result.remainder).toBe(1n); + }); + + test('should handle zero remainder', () => { + const result = uint64Simulator.divRem(6n, 3n); + expect(result.quotient).toBe(2n); + expect(result.remainder).toBe(0n); + }); + + test('should fail on division by zero', () => { + expect(() => uint64Simulator.divRem(5n, 0n)).toThrowError( + 'failed assert: Uint64: division by zero', + ); + }); + + test('should fail when remainder >= divisor', () => { + uint64Simulator.overrideWitness('wit_divUint64', (context) => [ + context.privateState, + { quotient: 1n, remainder: 5n }, + ]); + expect(() => uint64Simulator.divRem(10n, 5n)).toThrow( + 'failed assert: Uint64: remainder error', + ); + }); + + test('should fail when quotient * b + remainder != a', () => { + uint64Simulator.overrideWitness('wit_divUint64', (context) => [ + context.privateState, + { quotient: 2n, remainder: 0n }, + ]); + expect(() => uint64Simulator.divRem(11n, 5n)).toThrow( + 'failed assert: Uint64: division invalid', + ); // 2*5 + 0 = 10 ≠ 11 + }); + + test('should fail when remainder >= divisor (duplicate)', () => { + uint64Simulator.overrideWitness('wit_divUint64', (context) => [ + context.privateState, + { quotient: 1n, remainder: 10n }, + ]); + expect(() => uint64Simulator.divRem(10n, 5n)).toThrow( + 'failed assert: Uint64: remainder error', + ); + }); + }); + }); + + describe('square root', () => { + describe('Sqrt', () => { + test('should compute square root of small perfect squares', () => { + expect(uint64Simulator.sqrt(4n)).toBe(2n); + expect(uint64Simulator.sqrt(9n)).toBe(3n); + expect(uint64Simulator.sqrt(16n)).toBe(4n); + expect(uint64Simulator.sqrt(25n)).toBe(5n); + expect(uint64Simulator.sqrt(100n)).toBe(10n); + }); + + test('should compute square root of small imperfect squares', () => { + expect(uint64Simulator.sqrt(2n)).toBe(1n); // floor(sqrt(2)) ≈ 1.414 + expect(uint64Simulator.sqrt(3n)).toBe(1n); // floor(sqrt(3)) ≈ 1.732 + expect(uint64Simulator.sqrt(5n)).toBe(2n); // floor(sqrt(5)) ≈ 2.236 + expect(uint64Simulator.sqrt(8n)).toBe(2n); // floor(sqrt(8)) ≈ 2.828 + expect(uint64Simulator.sqrt(99n)).toBe(9n); // floor(sqrt(99)) ≈ 9.95 + }); + + test('should compute square root of large perfect squares', () => { + expect(uint64Simulator.sqrt(10000n)).toBe(100n); + expect(uint64Simulator.sqrt(1000000n)).toBe(1000n); + expect(uint64Simulator.sqrt(100000000n)).toBe(10000n); + }); + + test('should compute square root of large imperfect squares', () => { + expect(uint64Simulator.sqrt(101n)).toBe(10n); // floor(sqrt(101)) ≈ 10.05 + expect(uint64Simulator.sqrt(999999n)).toBe(999n); // floor(sqrt(999999)) ≈ 999.9995 + expect(uint64Simulator.sqrt(100000001n)).toBe(10000n); // floor(sqrt(100000001)) ≈ 10000.00005 + }); + + test('should handle powers of 2', () => { + expect(uint64Simulator.sqrt(2n ** 32n)).toBe(65536n); // sqrt(2^32) = 2^16 + expect(uint64Simulator.sqrt(MAX_UINT64)).toBe(4294967295n); // sqrt(2^64 - 1) ≈ 2^32 - 1 + }); + + test('should fail if number exceeds MAX_64', () => { + expect(() => uint64Simulator.sqrt(MAX_UINT64 + 1n)).toThrow( + 'expected value of type Uint<0..18446744073709551616> but received 18446744073709551616', + ); + }); + + test('should handle zero', () => { + expect(uint64Simulator.sqrt(0n)).toBe(0n); + }); + + test('should handle 1', () => { + expect(uint64Simulator.sqrt(1n)).toBe(1n); + }); + + test('should handle max Uint<64>', () => { + expect(uint64Simulator.sqrt(MAX_UINT64)).toBe(MAX_UINT32); // floor(sqrt(2^64 - 1)) = 2^32 - 1 + }); + + test('should fail with overestimated root', () => { + uint64Simulator.overrideWitness('wit_sqrtUint64', (context) => [ + context.privateState, + 5n, + ]); + expect(() => uint64Simulator.sqrt(10n)).toThrow( + 'failed assert: Uint64: sqrt overestimate', + ); + }); + + test('should fail with underestimated root', () => { + uint64Simulator.overrideWitness('wit_sqrtUint64', (context) => [ + context.privateState, + 3n, + ]); + expect(() => uint64Simulator.sqrt(16n)).toThrow( + 'failed assert: Uint64: sqrt underestimate', + ); + }); + }); + }); + + describe('utilities', () => { + describe('IsMultiple', () => { + test('should check if multiple', () => { + expect(uint64Simulator.isMultiple(6n, 3n)).toBe(true); + }); + + test('should fail on zero divisor', () => { + expect(() => uint64Simulator.isMultiple(5n, 0n)).toThrowError( + 'failed assert: Uint64: division by zero', + ); + }); + + test('should check max Uint<64> is multiple of 1', () => { + expect(uint64Simulator.isMultiple(MAX_UINT64, 1n)).toBe(true); + }); + + test('should detect a failed case', () => { + expect(uint64Simulator.isMultiple(7n, 3n)).toBe(false); + }); + + test('should fail when witness returns quotient * b + remainder != a', () => { + uint64Simulator.overrideWitness('wit_divUint64', (context) => [ + context.privateState, + { quotient: 1n, remainder: 1n }, + ]); + expect(() => uint64Simulator.isMultiple(6n, 3n)).toThrow( + 'failed assert: Uint64: division invalid', + ); + }); + }); + + describe('Min', () => { + test('should return minimum', () => { + expect(uint64Simulator.min(5n, 3n)).toBe(3n); + }); + + test('should handle equal values', () => { + expect(uint64Simulator.min(4n, 4n)).toBe(4n); + }); + + test('should handle max Uint<64> and smaller value', () => { + expect(uint64Simulator.min(MAX_UINT64, 1n)).toBe(1n); + }); + }); + + describe('Max', () => { + test('should return maximum', () => { + expect(uint64Simulator.max(5n, 3n)).toBe(5n); + }); + + test('should handle equal values', () => { + expect(uint64Simulator.max(4n, 4n)).toBe(4n); + }); + + test('should handle max Uint<64> and smaller value', () => { + expect(uint64Simulator.max(MAX_UINT64, 1n)).toBe(MAX_UINT64); + }); + }); + }); +}); diff --git a/contracts/src/math/test/mocks/Bytes8Simulator.ts b/contracts/src/math/test/mocks/Bytes8Simulator.ts new file mode 100644 index 00000000..1bcd7a6f --- /dev/null +++ b/contracts/src/math/test/mocks/Bytes8Simulator.ts @@ -0,0 +1,80 @@ +import type { Witnesses } from '@artifacts/Bytes8.mock/contract/index.js'; +import { Contract, ledger } from '@artifacts/Bytes8.mock/contract/index.js'; +import { + type BaseSimulatorOptions, + createSimulator, +} from '@openzeppelin-compact/contracts-simulator'; +import { wit_unpackBytes } from '@src/math/witnesses/wit_unpackBytes.js'; + +export type Bytes8PrivateState = Record; + +export const Bytes8Witnesses = (): Witnesses => ({ + wit_unpackBytes(_context, bytes) { + return [{}, wit_unpackBytes(bytes)]; + }, +}); + +/** + * Base simulator for Bytes8 mock contract + */ +const Bytes8SimulatorBase = createSimulator< + Bytes8PrivateState, + ReturnType, + ReturnType, + Contract, + readonly [] +>({ + contractFactory: (witnesses) => new Contract(witnesses), + defaultPrivateState: () => ({}), + contractArgs: () => [], + ledgerExtractor: (state) => ledger(state), + witnessesFactory: () => Bytes8Witnesses(), +}); + +/** + * @description A simulator implementation for testing Bytes8 conversion operations. + */ +export class Bytes8Simulator extends Bytes8SimulatorBase { + constructor( + options: BaseSimulatorOptions< + Bytes8PrivateState, + ReturnType + > = {}, + ) { + super([], options); + } + + //////////////////////////////////////////////////////////////// + // Conversions + //////////////////////////////////////////////////////////////// + + public pack( + vec: [bigint, bigint, bigint, bigint, bigint, bigint, bigint, bigint], + ): Uint8Array { + return this.circuits.impure.pack(vec); + } + public unpack( + bytes: Uint8Array, + ): [bigint, bigint, bigint, bigint, bigint, bigint, bigint, bigint] { + return this.circuits.impure.unpack(bytes) as [ + bigint, + bigint, + bigint, + bigint, + bigint, + bigint, + bigint, + bigint, + ]; + } + + public vectorToUint64( + vec: [bigint, bigint, bigint, bigint, bigint, bigint, bigint, bigint], + ): bigint { + return this.circuits.impure.vectorToUint64(vec); + } + + public bytesToUint64(bytes: Uint8Array): bigint { + return this.circuits.impure.bytesToUint64(bytes); + } +} diff --git a/contracts/src/math/test/mocks/PackSimulator.ts b/contracts/src/math/test/mocks/PackSimulator.ts new file mode 100644 index 00000000..bf42dc17 --- /dev/null +++ b/contracts/src/math/test/mocks/PackSimulator.ts @@ -0,0 +1,89 @@ +import type { Witnesses } from '@artifacts/Pack.mock/contract/index.js'; +import { Contract, ledger } from '@artifacts/Pack.mock/contract/index.js'; +import { + type BaseSimulatorOptions, + createSimulator, +} from '@openzeppelin-compact/contracts-simulator'; +import { wit_unpackBytes } from '@src/math/witnesses/wit_unpackBytes.js'; + +export type PackPrivateState = Record; + +export const PackWitnesses = (): Witnesses => ({ + wit_unpackBytes(_context: unknown, bytes: Uint8Array) { + return [{}, wit_unpackBytes(bytes)]; + }, +}); + +/** + * Base simulator for Pack mock contract + */ +const PackSimulatorBase = createSimulator< + PackPrivateState, + ReturnType, + ReturnType, + Contract, + readonly [] +>({ + contractFactory: (witnesses) => new Contract(witnesses), + defaultPrivateState: () => ({}), + contractArgs: () => [], + ledgerExtractor: (state) => ledger(state), + witnessesFactory: () => PackWitnesses(), +}); + +/** + * @description A simulator implementation for testing Pack conversion operations (N=8, 16, 32). + */ +export class PackSimulator extends PackSimulatorBase { + constructor( + options: BaseSimulatorOptions< + PackPrivateState, + ReturnType + > = {}, + ) { + super([], options); + } + + /** Packs Vector<8, Uint<8>> into Bytes<8>. */ + public pack8( + vec: [bigint, bigint, bigint, bigint, bigint, bigint, bigint, bigint], + ): Uint8Array { + return this.circuits.impure.pack8(vec); + } + + /** Unpacks Bytes<8> into Vector<8, Uint<8>>. */ + public unpack8( + bytes: Uint8Array, + ): [bigint, bigint, bigint, bigint, bigint, bigint, bigint, bigint] { + return this.circuits.impure.unpack8(bytes) as [ + bigint, + bigint, + bigint, + bigint, + bigint, + bigint, + bigint, + bigint, + ]; + } + + /** Packs Vector<16, Uint<8>> into Bytes<16>. */ + public pack16(vec: bigint[]): Uint8Array { + return this.circuits.impure.pack16(vec); + } + + /** Unpacks Bytes<16> into Vector<16, Uint<8>>. */ + public unpack16(bytes: Uint8Array): bigint[] { + return this.circuits.impure.unpack16(bytes); + } + + /** Packs Vector<32, Uint<8>> into Bytes<32>. */ + public pack32(vec: bigint[]): Uint8Array { + return this.circuits.impure.pack32(vec); + } + + /** Unpacks Bytes<32> into Vector<32, Uint<8>>. */ + public unpack32(bytes: Uint8Array): bigint[] { + return this.circuits.impure.unpack32(bytes); + } +} diff --git a/contracts/src/math/test/mocks/Uint64Simulator.ts b/contracts/src/math/test/mocks/Uint64Simulator.ts new file mode 100644 index 00000000..25040455 --- /dev/null +++ b/contracts/src/math/test/mocks/Uint64Simulator.ts @@ -0,0 +1,166 @@ +import type { + DivResultU64, + Witnesses, +} from '@artifacts/Uint64.mock/contract/index.js'; +import { Contract, ledger } from '@artifacts/Uint64.mock/contract/index.js'; +import { + type BaseSimulatorOptions, + createSimulator, +} from '@openzeppelin-compact/contracts-simulator'; +import { wit_divUint64 } from '@src/math/witnesses/wit_divUint64.js'; +import { wit_sqrtUint64 } from '@src/math/witnesses/wit_sqrtUint64.js'; +import { wit_uint64ToUnpackedBytes } from '@src/math/witnesses/wit_uint64ToUnpackedBytes.js'; + +export type Uint64PrivateState = Record; + +export const Uint64Witnesses = (): Witnesses => ({ + wit_sqrtUint64(_context, radicand) { + return [{}, wit_sqrtUint64(radicand)]; + }, + + wit_divUint64(_context, dividend, divisor) { + return [{}, wit_divUint64(dividend, divisor)]; + }, + + wit_uint64ToUnpackedBytes(_context, value) { + return [{}, wit_uint64ToUnpackedBytes(value)]; + }, +}); + +/** + * Base simulator for Uint64 mock contract + */ +const Uint64SimulatorBase = createSimulator< + Uint64PrivateState, + ReturnType, + ReturnType, + Contract, + readonly [] +>({ + contractFactory: (witnesses) => new Contract(witnesses), + defaultPrivateState: () => ({}), + contractArgs: () => [], + ledgerExtractor: (state) => ledger(state), + witnessesFactory: () => Uint64Witnesses(), +}); + +/** + * @description A simulator implementation for testing Uint64 math operations. + */ +export class Uint64Simulator extends Uint64SimulatorBase { + constructor( + options: BaseSimulatorOptions< + Uint64PrivateState, + ReturnType + > = {}, + ) { + super([], options); + } + + //////////////////////////////////////////////////////////////// + // Constants + //////////////////////////////////////////////////////////////// + + public MAX_UINT8(): bigint { + return this.circuits.impure.MAX_UINT8(); + } + + public MAX_UINT16(): bigint { + return this.circuits.impure.MAX_UINT16(); + } + + public MAX_UINT32(): bigint { + return this.circuits.impure.MAX_UINT32(); + } + + public MAX_UINT64(): bigint { + return this.circuits.impure.MAX_UINT64(); + } + + //////////////////////////////////////////////////////////////// + // Conversions + //////////////////////////////////////////////////////////////// + + public toBytes(value: bigint): Uint8Array { + return this.circuits.impure.toBytes(value); + } + + public toUnpackedBytes( + value: bigint, + ): [bigint, bigint, bigint, bigint, bigint, bigint, bigint, bigint] { + return this.circuits.impure.toUnpackedBytes(value) as [ + bigint, + bigint, + bigint, + bigint, + bigint, + bigint, + bigint, + bigint, + ]; + } + + //////////////////////////////////////////////////////////////// + // Arithmetic + //////////////////////////////////////////////////////////////// + + public add(a: bigint, b: bigint): bigint { + return this.circuits.impure.add(a, b); + } + + public addChecked(a: bigint, b: bigint): bigint { + return this.circuits.impure.addChecked(a, b); + } + + public sub(a: bigint, b: bigint): bigint { + return this.circuits.impure.sub(a, b); + } + + public mul(a: bigint, b: bigint): bigint { + return this.circuits.impure.mul(a, b); + } + + public mulChecked(a: bigint, b: bigint): bigint { + return this.circuits.impure.mulChecked(a, b); + } + + //////////////////////////////////////////////////////////////// + // Division + //////////////////////////////////////////////////////////////// + + public div(a: bigint, b: bigint): bigint { + return this.circuits.impure.div(a, b); + } + + public rem(a: bigint, b: bigint): bigint { + return this.circuits.impure.rem(a, b); + } + + public divRem(a: bigint, b: bigint): DivResultU64 { + return this.circuits.impure.divRem(a, b); + } + + //////////////////////////////////////////////////////////////// + // Square Root + //////////////////////////////////////////////////////////////// + + public sqrt(radical: bigint): bigint { + return this.circuits.impure.sqrt(radical); + } + + //////////////////////////////////////////////////////////////// + // Utilities + //////////////////////////////////////////////////////////////// + + public isMultiple(a: bigint, b: bigint): boolean { + return this.circuits.impure.isMultiple(a, b); + } + + public min(a: bigint, b: bigint): bigint { + return this.circuits.impure.min(a, b); + } + + public max(a: bigint, b: bigint): bigint { + return this.circuits.impure.max(a, b); + } +} diff --git a/contracts/src/math/test/mocks/contracts/Bytes8.mock.compact b/contracts/src/math/test/mocks/contracts/Bytes8.mock.compact new file mode 100644 index 00000000..e8d758a5 --- /dev/null +++ b/contracts/src/math/test/mocks/contracts/Bytes8.mock.compact @@ -0,0 +1,33 @@ +pragma language_version >= 0.20.0; + +import CompactStandardLibrary; + +import "../../../Bytes8" prefix Bytes8_; + +// Helper for test suite: hardcoded to true in every circuit to enable circuit metadata reporting; +// only increases circuit size by 3 rows per exposed circuit. +ledger toImpure: Boolean; + +//////////////////////////////////////////////////////////////// +// Conversions +//////////////////////////////////////////////////////////////// + +export circuit pack(vec: Vector<8, Uint<8>>): Bytes<8> { + toImpure = true; + return Bytes8_pack(vec); +} + +export circuit unpack(bytes: Bytes<8>): Vector<8, Uint<8>> { + toImpure = true; + return disclose(Bytes8_unpack(bytes)); +} + +export circuit vectorToUint64(vec: Vector<8, Uint<8>>): Uint<64> { + toImpure = true; + return Bytes8_toUint64(vec); +} + +export circuit bytesToUint64(bytes: Bytes<8>): Uint<64> { + toImpure = true; + return disclose(Bytes8_toUint64(bytes)); +} diff --git a/contracts/src/math/test/mocks/contracts/Pack.mock.compact b/contracts/src/math/test/mocks/contracts/Pack.mock.compact new file mode 100644 index 00000000..baf84662 --- /dev/null +++ b/contracts/src/math/test/mocks/contracts/Pack.mock.compact @@ -0,0 +1,55 @@ +pragma language_version >= 0.20.0; + +import CompactStandardLibrary; + +import "../../../Pack"<8> prefix Pack8_; + +import "../../../Pack"<16> prefix Pack16_; + +import "../../../Pack"<32> prefix Pack32_; + +// Helper for test suite: hardcoded to true in every circuit to enable circuit metadata reporting; +// only increases circuit size by 3 rows per exposed circuit. +ledger toImpure: Boolean; + +//////////////////////////////////////////////////////////////// +// N = 8 +//////////////////////////////////////////////////////////////// + +export circuit pack8(vec: Vector<8, Uint<8>>): Bytes<8> { + toImpure = true; + return Pack8_pack(vec); +} + +export circuit unpack8(bytes: Bytes<8>): Vector<8, Uint<8>> { + toImpure = true; + return disclose(Pack8_unpack(bytes)); +} + +//////////////////////////////////////////////////////////////// +// N = 16 +//////////////////////////////////////////////////////////////// + +export circuit pack16(vec: Vector<16, Uint<8>>): Bytes<16> { + toImpure = true; + return Pack16_pack(vec); +} + +export circuit unpack16(bytes: Bytes<16>): Vector<16, Uint<8>> { + toImpure = true; + return disclose(Pack16_unpack(bytes)); +} + +//////////////////////////////////////////////////////////////// +// N = 32 +//////////////////////////////////////////////////////////////// + +export circuit pack32(vec: Vector<32, Uint<8>>): Bytes<32> { + toImpure = true; + return Pack32_pack(vec); +} + +export circuit unpack32(bytes: Bytes<32>): Vector<32, Uint<8>> { + toImpure = true; + return disclose(Pack32_unpack(bytes)); +} diff --git a/contracts/src/math/test/mocks/contracts/Uint64.mock.compact b/contracts/src/math/test/mocks/contracts/Uint64.mock.compact new file mode 100644 index 00000000..161048cc --- /dev/null +++ b/contracts/src/math/test/mocks/contracts/Uint64.mock.compact @@ -0,0 +1,127 @@ +pragma language_version >= 0.20.0; + +import CompactStandardLibrary; + +import { DivResultU64 } from "../../../Uint64"; + +import "../../../Uint64" prefix Uint64_; + +export { DivResultU64 }; + +// Helper for test suite: hardcoded to true in every circuit to enable circuit metadata reporting; +// only increases circuit size by 3 rows per exposed circuit. +ledger toImpure: Boolean; + +//////////////////////////////////////////////////////////////// +// Constants +//////////////////////////////////////////////////////////////// + +export circuit MAX_UINT8(): Uint<8> { + toImpure = true; + return Uint64_MAX_UINT8(); +} + +export circuit MAX_UINT16(): Uint<16> { + toImpure = true; + return Uint64_MAX_UINT16(); +} + +export circuit MAX_UINT32(): Uint<32> { + toImpure = true; + return Uint64_MAX_UINT32(); +} + +export circuit MAX_UINT64(): Uint<64> { + toImpure = true; + return Uint64_MAX_UINT64(); +} + +//////////////////////////////////////////////////////////////// +// Conversions +//////////////////////////////////////////////////////////////// + +export circuit toBytes(value: Uint<64>): Bytes<8> { + toImpure = true; + return disclose(Uint64_toBytes(value)); +} + +export circuit toUnpackedBytes(value: Uint<64>): Vector<8, Uint<8>> { + toImpure = true; + return disclose(Uint64_toUnpackedBytes(value)); +} + +//////////////////////////////////////////////////////////////// +// Arithmetic +//////////////////////////////////////////////////////////////// + +export circuit add(a: Uint<64>, b: Uint<64>): Uint<128> { + toImpure = true; + return Uint64_add(a, b); +} + +export circuit addChecked(a: Uint<64>, b: Uint<64>): Uint<64> { + toImpure = true; + return Uint64_addChecked(a, b); +} + +export circuit sub(a: Uint<64>, b: Uint<64>): Uint<64> { + toImpure = true; + return Uint64_sub(a, b); +} + +export circuit mul(a: Uint<64>, b: Uint<64>): Uint<128> { + toImpure = true; + return Uint64_mul(a, b); +} + +export circuit mulChecked(a: Uint<64>, b: Uint<64>): Uint<64> { + toImpure = true; + return Uint64_mulChecked(a, b); +} + +//////////////////////////////////////////////////////////////// +// Division +//////////////////////////////////////////////////////////////// + +export circuit div(a: Uint<64>, b: Uint<64>): Uint<64> { + toImpure = true; + return disclose(Uint64_div(a, b)); +} + +export circuit rem(a: Uint<64>, b: Uint<64>): Uint<64> { + toImpure = true; + return disclose(Uint64_rem(a, b)); +} + +export circuit divRem(a: Uint<64>, b: Uint<64>): DivResultU64 { + toImpure = true; + return disclose(Uint64_divRem(a, b)); +} + +//////////////////////////////////////////////////////////////// +// Square Root +//////////////////////////////////////////////////////////////// + +export circuit sqrt(radical: Uint<64>): Uint<32> { + toImpure = true; + return disclose(Uint64_sqrt(radical)); +} + +//////////////////////////////////////////////////////////////// +// Utilities +//////////////////////////////////////////////////////////////// + +export circuit isMultiple(value: Uint<64>, b: Uint<64>): Boolean { + toImpure = true; + return disclose(Uint64_isMultiple(value, b)); +} + +export circuit min(a: Uint<64>, b: Uint<64>): Uint<64> { + toImpure = true; + return Uint64_min(a, b); +} + +export circuit max(a: Uint<64>, b: Uint<64>): Uint<64> { + toImpure = true; + return Uint64_max(a, b); +} diff --git a/contracts/src/math/types/index.ts b/contracts/src/math/types/index.ts new file mode 100644 index 00000000..bf193255 --- /dev/null +++ b/contracts/src/math/types/index.ts @@ -0,0 +1,39 @@ +/** + * @description Represents a 128-bit unsigned integer as two 64-bit components. + */ +export type U128 = { + low: bigint; + high: bigint; +}; + +/** + * @description Represents a 256-bit unsigned integer as two U128 components. + */ +export type U256 = { + low: U128; + high: U128; +}; + +/** + * @description Division result for 64-bit operations. + */ +export type DivResultU64 = { + quotient: bigint; + remainder: bigint; +}; + +/** + * @description Division result for 128-bit operations (U128 struct). + */ +export type DivResultU128 = { + quotient: U128; + remainder: U128; +}; + +/** + * @description Division result for 256-bit operations (U256 struct). + */ +export type DivResultU256 = { + quotient: U256; + remainder: U256; +}; diff --git a/contracts/src/math/utils/consts.test.ts b/contracts/src/math/utils/consts.test.ts new file mode 100644 index 00000000..8df4c18f --- /dev/null +++ b/contracts/src/math/utils/consts.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, test } from 'vitest'; +import { + MAX_UINT8, + MAX_UINT16, + MAX_UINT32, + MAX_UINT64, + MAX_UINT128, + MAX_UINT256, +} from './consts.js'; + +describe('Constants', () => { + test('MAX_U8 should be 2^8 - 1', () => { + expect(MAX_UINT8).toBe(2n ** 8n - 1n); + expect(MAX_UINT8).toBe(255n); + }); + + test('MAX_U16 should be 2^16 - 1', () => { + expect(MAX_UINT16).toBe(2n ** 16n - 1n); + expect(MAX_UINT16).toBe(65535n); + }); + + test('MAX_U32 should be 2^32 - 1', () => { + expect(MAX_UINT32).toBe(2n ** 32n - 1n); + expect(MAX_UINT32).toBe(4294967295n); + }); + + test('MAX_U64 should be 2^64 - 1', () => { + expect(MAX_UINT64).toBe(2n ** 64n - 1n); + expect(MAX_UINT64).toBe(18446744073709551615n); + }); + + test('MAX_U128 should be 2^128 - 1', () => { + expect(MAX_UINT128).toBe(2n ** 128n - 1n); + expect(MAX_UINT128).toBe(340282366920938463463374607431768211455n); + }); + + test('MAX_U256 should be 2^256 - 1', () => { + expect(MAX_UINT256).toBe(2n ** 256n - 1n); + expect(MAX_UINT256).toBe( + 115792089237316195423570985008687907853269984665640564039457584007913129639935n, + ); + }); +}); diff --git a/contracts/src/math/utils/consts.ts b/contracts/src/math/utils/consts.ts new file mode 100644 index 00000000..41f23994 --- /dev/null +++ b/contracts/src/math/utils/consts.ts @@ -0,0 +1,6 @@ +export const MAX_UINT8 = 2n ** 8n - 1n; +export const MAX_UINT16 = 2n ** 16n - 1n; +export const MAX_UINT32 = 2n ** 32n - 1n; +export const MAX_UINT64 = 2n ** 64n - 1n; +export const MAX_UINT128 = 2n ** 128n - 1n; +export const MAX_UINT256 = 2n ** 256n - 1n; diff --git a/contracts/src/math/utils/sqrtBigint.test.ts b/contracts/src/math/utils/sqrtBigint.test.ts new file mode 100644 index 00000000..a765b42b --- /dev/null +++ b/contracts/src/math/utils/sqrtBigint.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, test } from 'vitest'; +import { sqrtBigint } from './sqrtBigint.js'; + +describe('sqrtBigint() function', () => { + test('should handle zero', () => { + expect(sqrtBigint(0n)).toBe(0n); + }); + + test('should handle 1', () => { + expect(sqrtBigint(1n)).toBe(1n); + }); + + test('should handle small perfect squares', () => { + expect(sqrtBigint(4n)).toBe(2n); + expect(sqrtBigint(9n)).toBe(3n); + expect(sqrtBigint(16n)).toBe(4n); + expect(sqrtBigint(25n)).toBe(5n); + expect(sqrtBigint(100n)).toBe(10n); + }); + + test('should handle small non-perfect squares', () => { + expect(sqrtBigint(2n)).toBe(1n); // floor(sqrtBigint(2)) ≈ 1.414 + expect(sqrtBigint(3n)).toBe(1n); // floor(sqrtBigint(3)) ≈ 1.732 + expect(sqrtBigint(5n)).toBe(2n); // floor(sqrtBigint(5)) ≈ 2.236 + expect(sqrtBigint(8n)).toBe(2n); // floor(sqrtBigint(8)) ≈ 2.828 + expect(sqrtBigint(99n)).toBe(9n); // floor(sqrtBigint(99)) ≈ 9.95 + }); + + test('should handle large perfect squares', () => { + expect(sqrtBigint(10000n)).toBe(100n); + expect(sqrtBigint(1000000n)).toBe(1000n); + expect(sqrtBigint(100000000n)).toBe(10000n); + }); + + test('should handle large non-perfect squares', () => { + expect(sqrtBigint(101n)).toBe(10n); // floor(sqrtBigint(101)) ≈ 10.05 + expect(sqrtBigint(999999n)).toBe(999n); // floor(sqrtBigint(999999)) ≈ 999.9995 + expect(sqrtBigint(100000001n)).toBe(10000n); // floor(sqrtBigint(100000001)) ≈ 10000.00005 + }); + + test('should handle powers of 2', () => { + expect(sqrtBigint(2n ** 32n)).toBe(2n ** 16n); // sqrtBigint(2^32) = 2^16 + expect(sqrtBigint(2n ** 64n)).toBe(2n ** 32n); // sqrtBigint(2^64) = 2^32 + expect(sqrtBigint(2n ** 128n)).toBe(2n ** 64n); // sqrtBigint(2^128) = 2^64 + }); + + test('should handle max Uint<64>', () => { + const maxU64 = 2n ** 64n - 1n; // 18446744073709551615 + expect(sqrtBigint(maxU64)).toBe(4294967295n); // floor(sqrtBigint(2^64 - 1)) = 2^32 - 1 + }); + + test('should handle max Uint<128>', () => { + const maxU128 = 2n ** 128n - 1n; // 340282366920938463463374607431768211455 + expect(sqrtBigint(maxU128)).toBe(18446744073709551615n); // floor(sqrtBigint(2^128 - 1)) = 2^64 - 1 + }); + + test('should throw on negative numbers', () => { + expect(() => sqrtBigint(-1n)).toThrowError( + 'square root of negative numbers is not supported', + ); + expect(() => sqrtBigint(-100n)).toThrowError( + 'square root of negative numbers is not supported', + ); + expect(() => sqrtBigint(-(2n ** 128n))).toThrowError( + 'square root of negative numbers is not supported', + ); + }); +}); diff --git a/contracts/src/math/utils/sqrtBigint.ts b/contracts/src/math/utils/sqrtBigint.ts new file mode 100644 index 00000000..258cce18 --- /dev/null +++ b/contracts/src/math/utils/sqrtBigint.ts @@ -0,0 +1,41 @@ +/** + * @description Computes the square root of a non-negative bigint using the Newton-Raphson method. + * This implementation avoids floating-point precision issues inherent in Math.sqrt + * by performing all calculations with bigint arithmetic, ensuring accuracy for large numbers. + * + * @param value - The non-negative bigint to compute the square root of. + * @returns The floor of the square root of the input value as a bigint. + * @throws Will throw an error if the input value is negative. + * @source Adapted from https://stackoverflow.com/a/53684036 + */ +export function sqrtBigint(value: bigint): bigint { + if (value < 0n) { + throw new Error('square root of negative numbers is not supported'); + } + + if (value < 2n) { + return value; + } + + function newtonIteration(n: bigint, x0: bigint): bigint { + const x1 = (n / x0 + x0) >> 1n; + if (x0 === x1 || x0 === x1 - 1n) { + return x0; + } + return newtonIteration(n, x1); + } + + let root = newtonIteration(value, 1n); + + // Ensure we return floor(sqrt(value)) + const rootSquare = root * root; + if (rootSquare > value) { + // Adjust downward if x^2 overshoots + root = root - 1n; + } else if (rootSquare < value && (root + 1n) * (root + 1n) <= value) { + // Adjust upward if (x + 1)^2 is still <= value (e.g., for 4n) + root = root + 1n; + } + + return root; +} diff --git a/contracts/src/math/witnesses/wit_divUint64.ts b/contracts/src/math/witnesses/wit_divUint64.ts new file mode 100644 index 00000000..24914460 --- /dev/null +++ b/contracts/src/math/witnesses/wit_divUint64.ts @@ -0,0 +1,16 @@ +import type { DivResultU64 } from '../types/index.js'; + +/** + * @description Computes the quotient and remainder of dividing two 64-bit unsigned integers. + * @param dividend - The dividend. + * @param divisor - The divisor. + * @returns An object containing the quotient and remainder. + */ +export const wit_divUint64 = ( + dividend: bigint, + divisor: bigint, +): DivResultU64 => { + const quotient = dividend / divisor; + const remainder = dividend % divisor; + return { quotient, remainder }; +}; diff --git a/contracts/src/math/witnesses/wit_sqrtUint64.ts b/contracts/src/math/witnesses/wit_sqrtUint64.ts new file mode 100644 index 00000000..b8779f73 --- /dev/null +++ b/contracts/src/math/witnesses/wit_sqrtUint64.ts @@ -0,0 +1,10 @@ +import { sqrtBigint } from '../utils/sqrtBigint.js'; + +/** + * @description Computes the square root of a 64-bit unsigned integer. + * @param radicand - The value to compute the square root of. + * @returns The floor of the square root as a 32-bit result. + */ +export const wit_sqrtUint64 = (radicand: bigint): bigint => { + return sqrtBigint(radicand); +}; diff --git a/contracts/src/math/witnesses/wit_uint64ToUnpackedBytes.ts b/contracts/src/math/witnesses/wit_uint64ToUnpackedBytes.ts new file mode 100644 index 00000000..c32f7c28 --- /dev/null +++ b/contracts/src/math/witnesses/wit_uint64ToUnpackedBytes.ts @@ -0,0 +1,20 @@ +/** + * @description Unpacks a 64-bit unsigned integer into 8 bytes (little-endian). + * This is the witness for Uint64.toUnpackedBytes. + * @param value - The 64-bit value to unpack. + * @returns A vector of 8 bytes [b0, b1, b2, b3, b4, b5, b6, b7] where b0 is the LSB. + */ +export const wit_uint64ToUnpackedBytes = ( + value: bigint, +): [bigint, bigint, bigint, bigint, bigint, bigint, bigint, bigint] => { + const mask = 0xffn; + const b0 = value & mask; + const b1 = (value >> 8n) & mask; + const b2 = (value >> 16n) & mask; + const b3 = (value >> 24n) & mask; + const b4 = (value >> 32n) & mask; + const b5 = (value >> 40n) & mask; + const b6 = (value >> 48n) & mask; + const b7 = (value >> 56n) & mask; + return [b0, b1, b2, b3, b4, b5, b6, b7]; +}; diff --git a/contracts/src/math/witnesses/wit_unpackBytes.ts b/contracts/src/math/witnesses/wit_unpackBytes.ts new file mode 100644 index 00000000..4103b0b9 --- /dev/null +++ b/contracts/src/math/witnesses/wit_unpackBytes.ts @@ -0,0 +1,8 @@ +/** + * @description Unpacks a byte array into a vector of bytes (little-endian). + * Used by Pack.unpack for any N (e.g. Pack mock and tests). + * @param bytes - The byte array to unpack. + * @returns A vector of bytes where element 0 is the LSB. + */ +export const wit_unpackBytes = (bytes: Uint8Array): bigint[] => + Array.from(bytes, (b) => BigInt(b)); diff --git a/contracts/tsconfig.json b/contracts/tsconfig.json index 56f85e46..8b98c928 100644 --- a/contracts/tsconfig.json +++ b/contracts/tsconfig.json @@ -1,10 +1,11 @@ { "extends": "@tsconfig/node24/tsconfig.json", "include": [ - "src/**/witnesses/**/*.ts" + "src/**/*.ts" ], "exclude": ["src/archive/"], "compilerOptions": { + "baseUrl": ".", "rootDir": "src", "outDir": "dist", "declaration": true, @@ -13,5 +14,9 @@ "noImplicitAny": true, "isolatedModules": true, "resolveJsonModule": true, + "paths": { + "@artifacts/*": ["./artifacts/*"], + "@src/*": ["./src/*"] + } } } \ No newline at end of file diff --git a/contracts/vitest.config.ts b/contracts/vitest.config.ts index 3e71d7a7..bd2c49fb 100644 --- a/contracts/vitest.config.ts +++ b/contracts/vitest.config.ts @@ -1,6 +1,13 @@ +import { resolve } from 'path'; import { configDefaults, defineConfig } from 'vitest/config'; export default defineConfig({ + resolve: { + alias: { + '@artifacts': resolve(import.meta.dirname, 'artifacts'), + '@src': resolve(import.meta.dirname, 'src'), + }, + }, test: { globals: true, environment: 'node',