mirror of
https://github.com/zkemail/zk-email-verify.git
synced 2026-01-09 13:38:03 -05:00
feat: added noir utils and renamed circuit utils to circom utils (#287)
* feat: updated circuit utils to use bytes32 fields * feat: circuit utils renamed to circom utils * feat: noir utils * chore: removed unused functions * chore: efficiency fix * chore: moved back pack unpack header hash to ens repo * chore: noir utils bytes instead of strings * chore: renamed circom utils functions * chore: removed circom utils offset for unpacking * fix: calldata to memory * fix: unpackBool
This commit is contained in:
@@ -1,236 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import { Bytes } from "@openzeppelin/contracts/utils/Bytes.sol";
|
||||
|
||||
/**
|
||||
* @title CircuitUtils
|
||||
* @notice Library for ZK circuit-related utilities including field element packing and proof processing
|
||||
* @dev This library provides functions for converting between byte arrays and field elements
|
||||
* and other utilities needed for zero-knowledge proof circuit compatibility.
|
||||
*/
|
||||
library CircuitUtils {
|
||||
using Bytes for bytes;
|
||||
|
||||
/**
|
||||
* @notice Error thrown when the public signals array length is not exactly 60
|
||||
* @dev The ZK circuit expects exactly 60 public signals for verification
|
||||
*/
|
||||
error InvalidPubSignalsLength();
|
||||
|
||||
/**
|
||||
* @notice Error thrown when the command length is invalid
|
||||
* @dev The command should have the expected format and length
|
||||
*/
|
||||
error InvalidCommandLength();
|
||||
|
||||
/**
|
||||
* @notice Error thrown when the data length is greater than the padded size
|
||||
* @dev The data should have the expected format and length
|
||||
*/
|
||||
error InvalidDataLength();
|
||||
|
||||
/**
|
||||
* @notice Packs byte arrays into field elements for ZK circuit compatibility
|
||||
* @param _bytes The byte array to pack into field elements
|
||||
* @param _paddedSize The target size after padding (must be larger than or equal to _bytes.length)
|
||||
* @return An array of field elements containing the packed byte data
|
||||
* @dev This function packs bytes into field elements by:
|
||||
* 1. Determining how many field elements are needed (31 bytes per field element)
|
||||
* 2. Packing bytes in little-endian order within each field element
|
||||
* 3. Padding with zeros if the input is shorter than _paddedSize
|
||||
* 4. Ensuring the resulting field elements are compatible with ZK circuits
|
||||
*
|
||||
* Each field element can contain up to 31 bytes to ensure the result stays below
|
||||
* the BN128 curve order. Bytes are packed as: byte0 + (byte1 << 8) + (byte2 << 16) + ...
|
||||
*/
|
||||
function packBytes2Fields(bytes memory _bytes, uint256 _paddedSize) internal pure returns (uint256[] memory) {
|
||||
if (_bytes.length > _paddedSize) revert InvalidDataLength();
|
||||
|
||||
uint256 remain = _paddedSize % 31;
|
||||
uint256 numFields = (_paddedSize - remain) / 31;
|
||||
if (remain > 0) {
|
||||
numFields += 1;
|
||||
}
|
||||
uint256[] memory fields = new uint256[](numFields);
|
||||
uint256 idx = 0;
|
||||
uint256 byteVal = 0;
|
||||
for (uint256 i = 0; i < numFields; i++) {
|
||||
for (uint256 j = 0; j < 31; j++) {
|
||||
idx = i * 31 + j;
|
||||
if (idx >= _paddedSize) {
|
||||
break;
|
||||
}
|
||||
if (idx >= _bytes.length) {
|
||||
byteVal = 0;
|
||||
} else {
|
||||
byteVal = uint256(uint8(_bytes[idx]));
|
||||
}
|
||||
if (j == 0) {
|
||||
fields[i] = byteVal;
|
||||
} else {
|
||||
fields[i] += (byteVal << (8 * j));
|
||||
}
|
||||
}
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Packs a string into field elements for ZK circuit compatibility
|
||||
* @param _string The string to pack
|
||||
* @param paddedSize The target size after padding
|
||||
* @return fields The packed field elements
|
||||
*/
|
||||
function packString(string memory _string, uint256 paddedSize) internal pure returns (uint256[] memory fields) {
|
||||
fields = packBytes2Fields(bytes(_string), paddedSize);
|
||||
return fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Packs a bytes32 value into a single field element
|
||||
* @param _bytes32 The bytes32 value to pack
|
||||
* @return fields The packed field element
|
||||
*/
|
||||
function packBytes32(bytes32 _bytes32) internal pure returns (uint256[] memory fields) {
|
||||
fields = new uint256[](1);
|
||||
fields[0] = uint256(_bytes32);
|
||||
return fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Packs a boolean value into a single field element
|
||||
* @param b The boolean value to pack
|
||||
* @return fields The packed field element
|
||||
*/
|
||||
function packBool(bool b) internal pure returns (uint256[] memory fields) {
|
||||
fields = new uint256[](1);
|
||||
fields[0] = b ? 1 : 0;
|
||||
return fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Packs a uint256 value into a single field element
|
||||
* @param _uint256 The uint256 value to pack
|
||||
* @return fields The packed field element
|
||||
*/
|
||||
function packUint256(uint256 _uint256) internal pure returns (uint256[] memory fields) {
|
||||
fields = new uint256[](1);
|
||||
fields[0] = _uint256;
|
||||
return fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Unpacks field elements back to bytes
|
||||
* @param _pucSignals Array of public signals
|
||||
* @param _startIndex Starting index in pubSignals
|
||||
* @param _paddedSize Original padded size of the bytes
|
||||
* @return The unpacked bytes
|
||||
*/
|
||||
function unpackFields2Bytes(
|
||||
uint256[] calldata _pucSignals,
|
||||
uint256 _startIndex,
|
||||
uint256 _paddedSize
|
||||
)
|
||||
internal
|
||||
pure
|
||||
returns (bytes memory)
|
||||
{
|
||||
uint256 remain = _paddedSize % 31;
|
||||
uint256 numFields = (_paddedSize - remain) / 31;
|
||||
if (remain > 0) {
|
||||
numFields += 1;
|
||||
}
|
||||
|
||||
bytes memory result = new bytes(_paddedSize);
|
||||
uint256 resultIndex = 0;
|
||||
|
||||
for (uint256 i = 0; i < numFields; i++) {
|
||||
uint256 field = _pucSignals[_startIndex + i];
|
||||
for (uint256 j = 0; j < 31 && resultIndex < _paddedSize; j++) {
|
||||
result[resultIndex] = bytes1(uint8(field & 0xFF));
|
||||
field = field >> 8;
|
||||
resultIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
// Trim trailing zeros
|
||||
uint256 actualLength = 0;
|
||||
for (uint256 i = 0; i < result.length; i++) {
|
||||
if (result[i] != 0) {
|
||||
actualLength = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return result.slice(0, actualLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Unpacks field elements to a string
|
||||
* @param pubSignals Array of public signals
|
||||
* @param startIndex Starting index in pubSignals
|
||||
* @param paddedSize Original padded size of the string
|
||||
* @return The unpacked string
|
||||
*/
|
||||
function unpackString(
|
||||
uint256[] calldata pubSignals,
|
||||
uint256 startIndex,
|
||||
uint256 paddedSize
|
||||
)
|
||||
internal
|
||||
pure
|
||||
returns (string memory)
|
||||
{
|
||||
return string(unpackFields2Bytes(pubSignals, startIndex, paddedSize));
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Unpacks a bytes32 value from public signals
|
||||
* @param pubSignals Array of public signals
|
||||
* @param startIndex Starting index in pubSignals
|
||||
* @return The unpacked bytes32 value
|
||||
*/
|
||||
function unpackBytes32(uint256[] calldata pubSignals, uint256 startIndex) internal pure returns (bytes32) {
|
||||
return bytes32(pubSignals[startIndex]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Unpacks a uint256 value from public signals
|
||||
* @param pubSignals Array of public signals
|
||||
* @param startIndex Starting index in pubSignals
|
||||
* @return The unpacked uint256 value
|
||||
*/
|
||||
function unpackUint256(uint256[] calldata pubSignals, uint256 startIndex) internal pure returns (uint256) {
|
||||
return pubSignals[startIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Unpacks a boolean value from public signals
|
||||
* @param pubSignals Array of public signals
|
||||
* @param startIndex Starting index in pubSignals
|
||||
* @return The unpacked boolean value
|
||||
*/
|
||||
function unpackBool(uint256[] calldata pubSignals, uint256 startIndex) internal pure returns (bool) {
|
||||
return pubSignals[startIndex] == 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Flattens multiple arrays of field elements into a single array
|
||||
* @param inputs The arrays of field elements to flatten
|
||||
* @param outLength The length of the flattened array
|
||||
* @return out The flattened array
|
||||
*/
|
||||
function flattenFields(uint256[][] memory inputs, uint256 outLength) internal pure returns (uint256[] memory out) {
|
||||
out = new uint256[](outLength);
|
||||
uint256 k = 0;
|
||||
for (uint256 i = 0; i < inputs.length; i++) {
|
||||
uint256[] memory arr = inputs[i];
|
||||
for (uint256 j = 0; j < arr.length; j++) {
|
||||
if (k >= outLength) revert InvalidPubSignalsLength();
|
||||
out[k++] = arr[j];
|
||||
}
|
||||
}
|
||||
if (k != outLength) revert InvalidPubSignalsLength();
|
||||
return out;
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,6 @@
|
||||
"files": [
|
||||
"DKIMRegistry.sol",
|
||||
"UserOverrideableDKIMRegistry.sol",
|
||||
"CircuitUtils.sol",
|
||||
"/utils",
|
||||
"/interfaces"
|
||||
],
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { Test } from "forge-std/Test.sol";
|
||||
import { CircuitUtils } from "../../CircuitUtils.sol";
|
||||
import { CircuitUtilsHelper } from "./_CircuitUtilsHelper.sol";
|
||||
|
||||
contract FlattenFieldsTest is Test {
|
||||
CircuitUtilsHelper private _helper;
|
||||
|
||||
function setUp() public {
|
||||
_helper = new CircuitUtilsHelper();
|
||||
}
|
||||
|
||||
function test_expectRevert_tooManyElements() public {
|
||||
uint256[][] memory inputs = new uint256[][](2);
|
||||
inputs[0] = new uint256[](30);
|
||||
inputs[1] = new uint256[](31);
|
||||
for (uint256 i = 0; i < 30; i++) {
|
||||
inputs[0][i] = i + 1;
|
||||
}
|
||||
for (uint256 i = 0; i < 31; i++) {
|
||||
inputs[1][i] = i + 31;
|
||||
}
|
||||
vm.expectRevert(CircuitUtils.InvalidPubSignalsLength.selector);
|
||||
_helper.callFlattenFields(inputs, 60);
|
||||
}
|
||||
|
||||
function test_expectRevert_tooFewElements() public {
|
||||
uint256[][] memory inputs = new uint256[][](2);
|
||||
inputs[0] = new uint256[](30);
|
||||
inputs[1] = new uint256[](29);
|
||||
for (uint256 i = 0; i < 30; i++) {
|
||||
inputs[0][i] = i + 1;
|
||||
}
|
||||
for (uint256 i = 0; i < 29; i++) {
|
||||
inputs[1][i] = i + 31;
|
||||
}
|
||||
vm.expectRevert(CircuitUtils.InvalidPubSignalsLength.selector);
|
||||
_helper.callFlattenFields(inputs, 60);
|
||||
}
|
||||
|
||||
function test_zeroArrays() public {
|
||||
uint256[][] memory inputs = new uint256[][](0);
|
||||
vm.expectRevert(CircuitUtils.InvalidPubSignalsLength.selector);
|
||||
_helper.callFlattenFields(inputs, 60);
|
||||
}
|
||||
|
||||
function test_singleArray() public view {
|
||||
uint256[][] memory inputs = new uint256[][](1);
|
||||
inputs[0] = new uint256[](60);
|
||||
for (uint256 i = 0; i < 60; i++) {
|
||||
inputs[0][i] = i + 1;
|
||||
}
|
||||
uint256[] memory result = _helper.callFlattenFields(inputs, 60);
|
||||
for (uint256 i = 0; i < 60; i++) {
|
||||
assertEq(result[i], i + 1);
|
||||
}
|
||||
}
|
||||
|
||||
function test_multipleArrays() public view {
|
||||
uint256[][] memory inputs = new uint256[][](3);
|
||||
inputs[0] = new uint256[](20);
|
||||
inputs[1] = new uint256[](20);
|
||||
inputs[2] = new uint256[](20);
|
||||
for (uint256 i = 0; i < 20; i++) {
|
||||
inputs[0][i] = i + 1;
|
||||
inputs[1][i] = i + 21;
|
||||
inputs[2][i] = i + 41;
|
||||
}
|
||||
uint256[] memory result = _helper.callFlattenFields(inputs, 60);
|
||||
for (uint256 i = 0; i < 20; i++) {
|
||||
assertEq(result[i], i + 1);
|
||||
assertEq(result[i + 20], i + 21);
|
||||
assertEq(result[i + 40], i + 41);
|
||||
}
|
||||
}
|
||||
|
||||
function test_manySmallArrays() public view {
|
||||
uint256[][] memory inputs = new uint256[][](60);
|
||||
for (uint256 i = 0; i < 60; i++) {
|
||||
inputs[i] = new uint256[](1);
|
||||
inputs[i][0] = i + 1;
|
||||
}
|
||||
uint256[] memory result = _helper.callFlattenFields(inputs, 60);
|
||||
for (uint256 i = 0; i < 60; i++) {
|
||||
assertEq(result[i], i + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { Test } from "forge-std/Test.sol";
|
||||
import { CircuitUtils } from "../../CircuitUtils.sol";
|
||||
import { CircuitUtilsHelper } from "./_CircuitUtilsHelper.sol";
|
||||
|
||||
contract PackBytes2FieldsTest is Test {
|
||||
CircuitUtilsHelper private _helper;
|
||||
|
||||
function setUp() public {
|
||||
_helper = new CircuitUtilsHelper();
|
||||
}
|
||||
|
||||
function test_emptyBytes() public view {
|
||||
bytes memory emptyBytes = "";
|
||||
uint256[] memory fields = _helper.callPackBytes2Fields(emptyBytes, 0);
|
||||
assertEq(fields.length, 0);
|
||||
}
|
||||
|
||||
function test_singleByte() public view {
|
||||
bytes memory singleByte = hex"41";
|
||||
uint256[] memory fields = _helper.callPackBytes2Fields(singleByte, 1);
|
||||
assertEq(fields.length, 1);
|
||||
assertEq(uint8(fields[0]), 0x41);
|
||||
}
|
||||
|
||||
function test_exactly31Bytes() public view {
|
||||
bytes memory data = new bytes(31);
|
||||
for (uint256 i = 0; i < 31; i++) {
|
||||
data[i] = bytes1(uint8(i + 1));
|
||||
}
|
||||
uint256[] memory fields = _helper.callPackBytes2Fields(data, 31);
|
||||
assertEq(fields.length, 1);
|
||||
uint256 expected = 0;
|
||||
for (uint256 i = 0; i < 31; i++) {
|
||||
expected += uint256(uint8(data[i])) << (8 * i);
|
||||
}
|
||||
assertEq(fields[0], expected);
|
||||
}
|
||||
|
||||
function test_32Bytes() public view {
|
||||
bytes memory data = new bytes(32);
|
||||
for (uint256 i = 0; i < 32; i++) {
|
||||
data[i] = bytes1(uint8(i + 1));
|
||||
}
|
||||
uint256[] memory fields = _helper.callPackBytes2Fields(data, 32);
|
||||
assertEq(fields.length, 2);
|
||||
uint256 expectedFirst = 0;
|
||||
for (uint256 i = 0; i < 31; i++) {
|
||||
expectedFirst += uint256(uint8(data[i])) << (8 * i);
|
||||
}
|
||||
assertEq(fields[0], expectedFirst);
|
||||
assertEq(fields[1], uint256(uint8(data[31])));
|
||||
}
|
||||
|
||||
function test_withPadding() public view {
|
||||
bytes memory data = hex"414243";
|
||||
uint256[] memory fields = _helper.callPackBytes2Fields(data, 10);
|
||||
assertEq(fields.length, 1);
|
||||
uint256 expected = 0x41 + (0x42 << 8) + (0x43 << 16);
|
||||
assertEq(fields[0], expected);
|
||||
}
|
||||
|
||||
function test_exactFieldBoundaries() public view {
|
||||
bytes memory data = new bytes(62);
|
||||
for (uint256 i = 0; i < 62; i++) {
|
||||
data[i] = bytes1(uint8(i + 1));
|
||||
}
|
||||
uint256[] memory fields = _helper.callPackBytes2Fields(data, 62);
|
||||
assertEq(fields.length, 2);
|
||||
uint256 expectedFirst = 0;
|
||||
for (uint256 i = 0; i < 31; i++) {
|
||||
expectedFirst += uint256(uint8(data[i])) << (8 * i);
|
||||
}
|
||||
assertEq(fields[0], expectedFirst);
|
||||
uint256 expectedSecond = 0;
|
||||
for (uint256 i = 31; i < 62; i++) {
|
||||
expectedSecond += uint256(uint8(data[i])) << (8 * (i - 31));
|
||||
}
|
||||
assertEq(fields[1], expectedSecond);
|
||||
}
|
||||
|
||||
function test_allZeros() public view {
|
||||
bytes memory data = new bytes(31);
|
||||
uint256[] memory fields = _helper.callPackBytes2Fields(data, 31);
|
||||
assertEq(fields.length, 1);
|
||||
assertEq(fields[0], 0);
|
||||
}
|
||||
|
||||
function test_maxByteValues() public view {
|
||||
bytes memory data = new bytes(31);
|
||||
for (uint256 i = 0; i < 31; i++) {
|
||||
data[i] = 0xFF;
|
||||
}
|
||||
uint256[] memory fields = _helper.callPackBytes2Fields(data, 31);
|
||||
assertEq(fields.length, 1);
|
||||
uint256 expected = 0;
|
||||
for (uint256 i = 0; i < 31; i++) {
|
||||
expected += 0xFF << (8 * i);
|
||||
}
|
||||
assertEq(fields[0], expected);
|
||||
}
|
||||
|
||||
function test_realisticString() public view {
|
||||
bytes memory data = "gmail.com";
|
||||
uint256[] memory fields = _helper.callPackBytes2Fields(data, 255);
|
||||
assertEq(fields.length, 9);
|
||||
uint256 expected = 0;
|
||||
for (uint256 i = 0; i < data.length; i++) {
|
||||
expected += uint256(uint8(data[i])) << (8 * i);
|
||||
}
|
||||
assertEq(fields[0], expected);
|
||||
for (uint256 i = 1; i < 9; i++) {
|
||||
assertEq(fields[i], 0);
|
||||
}
|
||||
}
|
||||
|
||||
function test_paddedSizeSmallerThanData() public {
|
||||
bytes memory data = "This is a longer string that should revert";
|
||||
vm.expectRevert(CircuitUtils.InvalidDataLength.selector);
|
||||
_helper.callPackBytes2Fields(data, 10);
|
||||
}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { Test } from "forge-std/Test.sol";
|
||||
import { CircuitUtilsHelper } from "./_CircuitUtilsHelper.sol";
|
||||
|
||||
contract UnpackFields2BytesTest is Test {
|
||||
CircuitUtilsHelper private _helper;
|
||||
|
||||
function setUp() public {
|
||||
_helper = new CircuitUtilsHelper();
|
||||
}
|
||||
|
||||
function test_emptyFields() public view {
|
||||
uint256[] memory fields = new uint256[](0);
|
||||
bytes memory result = _helper.callUnpackFields2Bytes(fields, 0, 0);
|
||||
assertEq(result.length, 0);
|
||||
}
|
||||
|
||||
function test_singleFieldSingleByte() public view {
|
||||
uint256[] memory fields = new uint256[](1);
|
||||
fields[0] = 0x41;
|
||||
bytes memory result = _helper.callUnpackFields2Bytes(fields, 0, 1);
|
||||
assertEq(result.length, 1);
|
||||
assertEq(uint8(result[0]), 0x41);
|
||||
}
|
||||
|
||||
function test_singleFieldMultipleBytes() public view {
|
||||
uint256[] memory fields = new uint256[](1);
|
||||
fields[0] = 0x41 + (0x42 << 8) + (0x43 << 16);
|
||||
bytes memory result = _helper.callUnpackFields2Bytes(fields, 0, 3);
|
||||
assertEq(result.length, 3);
|
||||
assertEq(uint8(result[0]), 0x41);
|
||||
assertEq(uint8(result[1]), 0x42);
|
||||
assertEq(uint8(result[2]), 0x43);
|
||||
}
|
||||
|
||||
function test_multipleFields() public view {
|
||||
uint256[] memory fields = new uint256[](2);
|
||||
fields[0] = 0x41 + (0x42 << 8) + (0x43 << 16);
|
||||
fields[1] = 0x44 + (0x45 << 8) + (0x46 << 16);
|
||||
bytes memory result = _helper.callUnpackFields2Bytes(fields, 0, 6);
|
||||
// Only the first 3 bytes are non-zero, the rest are zeros and will be trimmed
|
||||
assertEq(result.length, 3);
|
||||
assertEq(uint8(result[0]), 0x41);
|
||||
assertEq(uint8(result[1]), 0x42);
|
||||
assertEq(uint8(result[2]), 0x43);
|
||||
}
|
||||
|
||||
function test_trimTrailingZeros() public view {
|
||||
uint256[] memory fields = new uint256[](1);
|
||||
fields[0] = 0x41 + (0x42 << 8) + (0x00 << 16);
|
||||
bytes memory result = _helper.callUnpackFields2Bytes(fields, 0, 3);
|
||||
assertEq(result.length, 2);
|
||||
assertEq(uint8(result[0]), 0x41);
|
||||
assertEq(uint8(result[1]), 0x42);
|
||||
}
|
||||
|
||||
function test_zerosInMiddle() public view {
|
||||
uint256[] memory fields = new uint256[](1);
|
||||
fields[0] = 0x41 + (0x00 << 8) + (0x43 << 16);
|
||||
bytes memory result = _helper.callUnpackFields2Bytes(fields, 0, 3);
|
||||
assertEq(result.length, 3);
|
||||
assertEq(uint8(result[0]), 0x41);
|
||||
assertEq(uint8(result[1]), 0x00);
|
||||
assertEq(uint8(result[2]), 0x43);
|
||||
}
|
||||
|
||||
function test_withOffset() public view {
|
||||
uint256[] memory fields = new uint256[](3);
|
||||
fields[0] = 0x11 + (0x12 << 8);
|
||||
fields[1] = 0x21 + (0x22 << 8) + (0x23 << 16);
|
||||
fields[2] = 0x31 + (0x32 << 8);
|
||||
bytes memory result = _helper.callUnpackFields2Bytes(fields, 1, 3);
|
||||
assertEq(result.length, 3);
|
||||
assertEq(uint8(result[0]), 0x21);
|
||||
assertEq(uint8(result[1]), 0x22);
|
||||
assertEq(uint8(result[2]), 0x23);
|
||||
}
|
||||
|
||||
function test_moreFieldsThanAvailable() public view {
|
||||
uint256[] memory fields = new uint256[](1);
|
||||
fields[0] = 0x41 + (0x42 << 8);
|
||||
bytes memory result = _helper.callUnpackFields2Bytes(fields, 0, 4);
|
||||
assertEq(result.length, 2);
|
||||
assertEq(uint8(result[0]), 0x41);
|
||||
assertEq(uint8(result[1]), 0x42);
|
||||
}
|
||||
|
||||
function test_allZeros() public view {
|
||||
uint256[] memory fields = new uint256[](1);
|
||||
fields[0] = 0;
|
||||
bytes memory result = _helper.callUnpackFields2Bytes(fields, 0, 31);
|
||||
assertEq(result.length, 0);
|
||||
}
|
||||
|
||||
function test_maxFieldValue() public view {
|
||||
uint256[] memory fields = new uint256[](1);
|
||||
fields[0] = 0;
|
||||
for (uint256 i = 0; i < 31; i++) {
|
||||
fields[0] += 0xFF << (8 * i);
|
||||
}
|
||||
bytes memory result = _helper.callUnpackFields2Bytes(fields, 0, 31);
|
||||
assertEq(result.length, 31);
|
||||
for (uint256 i = 0; i < 31; i++) {
|
||||
assertEq(uint8(result[i]), 0xFF);
|
||||
}
|
||||
}
|
||||
|
||||
function test_multipleFieldsWithPadding() public view {
|
||||
uint256[] memory fields = new uint256[](2);
|
||||
fields[0] = 0x41 + (0x42 << 8) + (0x43 << 16);
|
||||
fields[1] = 0x44 + (0x45 << 8);
|
||||
bytes memory result = _helper.callUnpackFields2Bytes(fields, 0, 5);
|
||||
// Only the first 3 bytes are non-zero, the rest are zeros and will be trimmed
|
||||
assertEq(result.length, 3);
|
||||
assertEq(uint8(result[0]), 0x41);
|
||||
assertEq(uint8(result[1]), 0x42);
|
||||
assertEq(uint8(result[2]), 0x43);
|
||||
}
|
||||
|
||||
function test_partialFieldUnpack() public view {
|
||||
uint256[] memory fields = new uint256[](1);
|
||||
fields[0] = 0x41 + (0x42 << 8) + (0x43 << 16) + (0x44 << 24);
|
||||
bytes memory result = _helper.callUnpackFields2Bytes(fields, 0, 2);
|
||||
assertEq(result.length, 2);
|
||||
assertEq(uint8(result[0]), 0x41);
|
||||
assertEq(uint8(result[1]), 0x42);
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import { CircuitUtils } from "../../CircuitUtils.sol";
|
||||
|
||||
contract CircuitUtilsHelper {
|
||||
function callFlattenFields(uint256[][] memory inputs, uint256 outLength) external pure returns (uint256[] memory) {
|
||||
return CircuitUtils.flattenFields(inputs, outLength);
|
||||
}
|
||||
|
||||
function callPackBytes2Fields(bytes memory data, uint256 paddedSize) external pure returns (uint256[] memory) {
|
||||
return CircuitUtils.packBytes2Fields(data, paddedSize);
|
||||
}
|
||||
|
||||
function callUnpackFields2Bytes(
|
||||
uint256[] calldata fields,
|
||||
uint256 startIndex,
|
||||
uint256 paddedSize
|
||||
)
|
||||
external
|
||||
pure
|
||||
returns (bytes memory)
|
||||
{
|
||||
return CircuitUtils.unpackFields2Bytes(fields, startIndex, paddedSize);
|
||||
}
|
||||
}
|
||||
160
packages/contracts/test/utils/CircomUtils/PackFieldsArray.t.sol
Normal file
160
packages/contracts/test/utils/CircomUtils/PackFieldsArray.t.sol
Normal file
@@ -0,0 +1,160 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import {Test} from "forge-std/Test.sol";
|
||||
import {CircomUtils} from "../../../utils/CircomUtils.sol";
|
||||
import {CircomUtilsHelper} from "./_CircomUtilsHelper.sol";
|
||||
|
||||
contract PackBytes2FieldsTest is Test {
|
||||
CircomUtilsHelper private _helper;
|
||||
|
||||
function setUp() public {
|
||||
_helper = new CircomUtilsHelper();
|
||||
}
|
||||
|
||||
function test_revertsWhen_paddedSizeSmallerThanData() public {
|
||||
bytes memory input = "This is a longer string that should revert";
|
||||
uint256 paddedSize = 10;
|
||||
|
||||
vm.expectRevert(CircomUtils.InvalidDataLength.selector);
|
||||
_helper.callPackFieldsArray(input, paddedSize);
|
||||
}
|
||||
|
||||
function test_emptyBytes() public view {
|
||||
bytes memory input = "";
|
||||
uint256 paddedSize = 0;
|
||||
|
||||
bytes32[] memory expected = new bytes32[](0);
|
||||
|
||||
bytes32[] memory fields = _helper.callPackFieldsArray(input, paddedSize);
|
||||
_assertEq(fields, expected);
|
||||
}
|
||||
|
||||
function test_singleByte() public view {
|
||||
bytes memory input = bytes("A");
|
||||
uint256 paddedSize = 1;
|
||||
|
||||
bytes32[] memory expected = new bytes32[](1);
|
||||
expected[0] = bytes32(uint256(0x41));
|
||||
|
||||
bytes32[] memory fields = _helper.callPackFieldsArray(input, paddedSize);
|
||||
_assertEq(fields, expected);
|
||||
}
|
||||
|
||||
function test_31Bytes() public view {
|
||||
bytes memory input = new bytes(31);
|
||||
uint256 paddedSize = 31;
|
||||
|
||||
bytes32[] memory expected = new bytes32[](1);
|
||||
expected[0] = bytes32(uint256(0));
|
||||
|
||||
bytes32[] memory fields = _helper.callPackFieldsArray(input, paddedSize);
|
||||
_assertEq(fields, expected);
|
||||
}
|
||||
|
||||
function test_32Bytes() public view {
|
||||
bytes memory input = new bytes(32);
|
||||
for (uint256 i = 0; i < 32; i++) {
|
||||
input[i] = bytes1(uint8(i + 1));
|
||||
}
|
||||
uint256 paddedSize = 32;
|
||||
|
||||
bytes32[] memory expected = new bytes32[](2);
|
||||
for (uint256 i = 0; i < 31; i++) {
|
||||
expected[0] = bytes32(uint256(expected[0]) + (uint256(uint8(input[i])) << (8 * i)));
|
||||
}
|
||||
expected[1] = bytes32(uint256(uint8(input[31])));
|
||||
|
||||
bytes32[] memory fields = _helper.callPackFieldsArray(input, paddedSize);
|
||||
_assertEq(fields, expected);
|
||||
}
|
||||
|
||||
function test_withPadding() public view {
|
||||
bytes memory input = bytes("ABC");
|
||||
uint256 paddedSize = 10;
|
||||
|
||||
bytes32[] memory expected = new bytes32[](1);
|
||||
expected[0] = bytes32(
|
||||
uint256(
|
||||
0x41 // A
|
||||
+ (0x42 << 8) // B
|
||||
+ (0x43 << 16) // C
|
||||
)
|
||||
);
|
||||
|
||||
bytes32[] memory fields = _helper.callPackFieldsArray(input, paddedSize);
|
||||
_assertEq(fields, expected);
|
||||
}
|
||||
|
||||
function test_exactFieldBoundaries() public view {
|
||||
bytes memory input = new bytes(62);
|
||||
for (uint256 i = 0; i < 62; i++) {
|
||||
input[i] = bytes1(uint8(i + 1));
|
||||
}
|
||||
uint256 paddedSize = 62;
|
||||
|
||||
bytes32[] memory expected = new bytes32[](2);
|
||||
for (uint256 i = 0; i < 31; i++) {
|
||||
expected[0] = bytes32(uint256(expected[0]) + (uint256(uint8(input[i])) << (8 * i)));
|
||||
}
|
||||
for (uint256 i = 31; i < 62; i++) {
|
||||
expected[1] = bytes32(uint256(expected[1]) + (uint256(uint8(input[i])) << (8 * (i - 31))));
|
||||
}
|
||||
|
||||
bytes32[] memory fields = _helper.callPackFieldsArray(input, paddedSize);
|
||||
_assertEq(fields, expected);
|
||||
}
|
||||
|
||||
function test_allZeros() public view {
|
||||
bytes memory input = new bytes(31);
|
||||
uint256 paddedSize = 31;
|
||||
|
||||
bytes32[] memory expected = new bytes32[](1);
|
||||
|
||||
bytes32[] memory fields = _helper.callPackFieldsArray(input, paddedSize);
|
||||
_assertEq(fields, expected);
|
||||
}
|
||||
|
||||
function test_maxByteValues() public view {
|
||||
bytes memory input = new bytes(31);
|
||||
for (uint256 i = 0; i < 31; i++) {
|
||||
input[i] = 0xFF;
|
||||
}
|
||||
uint256 paddedSize = 31;
|
||||
|
||||
bytes32[] memory expected = new bytes32[](1);
|
||||
for (uint256 i = 0; i < 31; i++) {
|
||||
expected[0] = bytes32(uint256(expected[0]) + (0xFF << (8 * i)));
|
||||
}
|
||||
|
||||
bytes32[] memory fields = _helper.callPackFieldsArray(input, paddedSize);
|
||||
_assertEq(fields, expected);
|
||||
}
|
||||
|
||||
function test_realisticString() public view {
|
||||
bytes memory input = "gmail.com";
|
||||
uint256 paddedSize = 255;
|
||||
|
||||
bytes32[] memory expected = new bytes32[](9);
|
||||
expected[0] = bytes32(
|
||||
uint256(
|
||||
0x67 // g
|
||||
+ (0x6d << 8) // m
|
||||
+ (0x61 << 16) // a
|
||||
+ (0x69 << 24) // i
|
||||
+ (0x6c << 32) // l
|
||||
+ (0x2e << 40) // .
|
||||
+ (0x63 << 48) // c
|
||||
+ (0x6f << 56) // o
|
||||
+ (0x6d << 64) // m
|
||||
)
|
||||
);
|
||||
|
||||
bytes32[] memory fields = _helper.callPackFieldsArray(input, paddedSize);
|
||||
_assertEq(fields, expected);
|
||||
}
|
||||
|
||||
function _assertEq(bytes32[] memory fields, bytes32[] memory expected) internal pure {
|
||||
assertEq(keccak256(abi.encode(fields)), keccak256(abi.encode(expected)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import {Test} from "forge-std/Test.sol";
|
||||
import {CircomUtilsHelper} from "./_CircomUtilsHelper.sol";
|
||||
|
||||
contract UnpackFields2BytesTest is Test {
|
||||
CircomUtilsHelper private _helper;
|
||||
|
||||
function setUp() public {
|
||||
_helper = new CircomUtilsHelper();
|
||||
}
|
||||
|
||||
function test_emptyFields() public view {
|
||||
bytes32[] memory fields = new bytes32[](0);
|
||||
bytes memory result = _helper.callUnpackFieldsArray(fields, 0);
|
||||
assertEq(result.length, 0);
|
||||
}
|
||||
|
||||
function test_singleFieldSingleByte() public view {
|
||||
bytes32[] memory fields = new bytes32[](1);
|
||||
fields[0] = bytes32(uint256(0x41));
|
||||
bytes memory result = _helper.callUnpackFieldsArray(fields, 1);
|
||||
assertEq(result.length, 1);
|
||||
assertEq(uint8(result[0]), 0x41);
|
||||
}
|
||||
|
||||
function test_singleFieldMultipleBytes() public view {
|
||||
bytes32[] memory fields = new bytes32[](1);
|
||||
fields[0] = bytes32(uint256(0x41 + (0x42 << 8) + (0x43 << 16)));
|
||||
bytes memory result = _helper.callUnpackFieldsArray(fields, 3);
|
||||
assertEq(result.length, 3);
|
||||
assertEq(uint8(result[0]), 0x41);
|
||||
assertEq(uint8(result[1]), 0x42);
|
||||
assertEq(uint8(result[2]), 0x43);
|
||||
}
|
||||
|
||||
function test_multipleFields() public view {
|
||||
bytes32[] memory fields = new bytes32[](2);
|
||||
fields[0] = bytes32(uint256(0x41 + (0x42 << 8) + (0x43 << 16)));
|
||||
fields[1] = bytes32(uint256(0x44 + (0x45 << 8) + (0x46 << 16)));
|
||||
bytes memory result = _helper.callUnpackFieldsArray(fields, 6);
|
||||
// Only the first 3 bytes are non-zero, the rest are zeros and will be trimmed
|
||||
assertEq(result.length, 3);
|
||||
assertEq(uint8(result[0]), 0x41);
|
||||
assertEq(uint8(result[1]), 0x42);
|
||||
assertEq(uint8(result[2]), 0x43);
|
||||
}
|
||||
|
||||
function test_trimTrailingZeros() public view {
|
||||
bytes32[] memory fields = new bytes32[](1);
|
||||
fields[0] = bytes32(uint256(0x41 + (0x42 << 8) + (0x00 << 16)));
|
||||
bytes memory result = _helper.callUnpackFieldsArray(fields, 3);
|
||||
assertEq(result.length, 2);
|
||||
assertEq(uint8(result[0]), 0x41);
|
||||
assertEq(uint8(result[1]), 0x42);
|
||||
}
|
||||
|
||||
function test_zerosInMiddle() public view {
|
||||
bytes32[] memory fields = new bytes32[](1);
|
||||
fields[0] = bytes32(uint256(0x41 + (0x00 << 8) + (0x43 << 16)));
|
||||
bytes memory result = _helper.callUnpackFieldsArray(fields, 3);
|
||||
assertEq(result.length, 3);
|
||||
assertEq(uint8(result[0]), 0x41);
|
||||
assertEq(uint8(result[1]), 0x00);
|
||||
assertEq(uint8(result[2]), 0x43);
|
||||
}
|
||||
|
||||
function test_moreFieldsThanAvailable() public view {
|
||||
bytes32[] memory fields = new bytes32[](1);
|
||||
fields[0] = bytes32(uint256(0x41 + (0x42 << 8)));
|
||||
bytes memory result = _helper.callUnpackFieldsArray(fields, 4);
|
||||
assertEq(result.length, 2);
|
||||
assertEq(uint8(result[0]), 0x41);
|
||||
assertEq(uint8(result[1]), 0x42);
|
||||
}
|
||||
|
||||
function test_allZeros() public view {
|
||||
bytes32[] memory fields = new bytes32[](1);
|
||||
fields[0] = bytes32(uint256(0));
|
||||
bytes memory result = _helper.callUnpackFieldsArray(fields, 31);
|
||||
assertEq(result.length, 0);
|
||||
}
|
||||
|
||||
function test_maxFieldValue() public view {
|
||||
bytes32[] memory fields = new bytes32[](1);
|
||||
uint256 fieldValue = 0;
|
||||
for (uint256 i = 0; i < 31; i++) {
|
||||
fieldValue += 0xFF << (8 * i);
|
||||
}
|
||||
fields[0] = bytes32(fieldValue);
|
||||
bytes memory result = _helper.callUnpackFieldsArray(fields, 31);
|
||||
assertEq(result.length, 31);
|
||||
for (uint256 i = 0; i < 31; i++) {
|
||||
assertEq(uint8(result[i]), 0xFF);
|
||||
}
|
||||
}
|
||||
|
||||
function test_multipleFieldsWithPadding() public view {
|
||||
bytes32[] memory fields = new bytes32[](2);
|
||||
fields[0] = bytes32(uint256(0x41 + (0x42 << 8) + (0x43 << 16)));
|
||||
fields[1] = bytes32(uint256(0x44 + (0x45 << 8)));
|
||||
bytes memory result = _helper.callUnpackFieldsArray(fields, 5);
|
||||
// Only the first 3 bytes are non-zero, the rest are zeros and will be trimmed
|
||||
assertEq(result.length, 3);
|
||||
assertEq(uint8(result[0]), 0x41);
|
||||
assertEq(uint8(result[1]), 0x42);
|
||||
assertEq(uint8(result[2]), 0x43);
|
||||
}
|
||||
|
||||
function test_partialFieldUnpack() public view {
|
||||
bytes32[] memory fields = new bytes32[](1);
|
||||
fields[0] = bytes32(uint256(0x41 + (0x42 << 8) + (0x43 << 16) + (0x44 << 24)));
|
||||
bytes memory result = _helper.callUnpackFieldsArray(fields, 2);
|
||||
assertEq(result.length, 2);
|
||||
assertEq(uint8(result[0]), 0x41);
|
||||
assertEq(uint8(result[1]), 0x42);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import {CircomUtils} from "../../../utils/CircomUtils.sol";
|
||||
|
||||
contract CircomUtilsHelper {
|
||||
function callPackFieldsArray(bytes memory input, uint256 paddedSize) external pure returns (bytes32[] memory) {
|
||||
return CircomUtils.packFieldsArray(input, paddedSize);
|
||||
}
|
||||
|
||||
function callUnpackFieldsArray(bytes32[] calldata fields, uint256 paddedSize)
|
||||
external
|
||||
pure
|
||||
returns (bytes memory)
|
||||
{
|
||||
return CircomUtils.unpackFieldsArray(fields, paddedSize);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import {Test} from "forge-std/Test.sol";
|
||||
import {NoirUtilsHelper} from "./_NoirUtilsHelper.sol";
|
||||
|
||||
contract PackBoundedVecU8Test is Test {
|
||||
NoirUtilsHelper private _helper;
|
||||
|
||||
function setUp() public {
|
||||
_helper = new NoirUtilsHelper();
|
||||
}
|
||||
|
||||
function test_revertsWhen_InputLengthEqualsNumFields() public {
|
||||
// 6 length data
|
||||
bytes memory input = bytes("abcdef");
|
||||
// data needs 6 slots + 1 length slot, so this should revert
|
||||
uint256 numFields = 6;
|
||||
|
||||
vm.expectRevert();
|
||||
_helper.callPackBoundedVecU8(input, numFields);
|
||||
}
|
||||
|
||||
function test_revertsWhen_InputTooLong() public {
|
||||
// 7 length data
|
||||
bytes memory input = bytes("toolong");
|
||||
// data needs 7 slots + 1 length slot, so this should revert
|
||||
uint256 numFields = 6;
|
||||
|
||||
vm.expectRevert();
|
||||
_helper.callPackBoundedVecU8(input, numFields);
|
||||
}
|
||||
|
||||
function test_correctlyPacks() public view {
|
||||
bytes memory input = bytes("hello");
|
||||
uint256 numFields = 8;
|
||||
|
||||
// 8 slots: 5 data slots + 2 unused slots + 1 length slot
|
||||
bytes32[] memory expected = new bytes32[](8);
|
||||
// 5 data slots
|
||||
expected[0] = bytes32(uint256(uint8(bytes1("h"))));
|
||||
expected[1] = bytes32(uint256(uint8(bytes1("e"))));
|
||||
expected[2] = bytes32(uint256(uint8(bytes1("l"))));
|
||||
expected[3] = bytes32(uint256(uint8(bytes1("l"))));
|
||||
expected[4] = bytes32(uint256(uint8(bytes1("o"))));
|
||||
// 2 unused slots
|
||||
expected[5] = bytes32(0);
|
||||
expected[6] = bytes32(0);
|
||||
// 1 length slot
|
||||
expected[7] = bytes32(uint256(5));
|
||||
|
||||
bytes32[] memory packed = _helper.callPackBoundedVecU8(input, numFields);
|
||||
_assertEq(packed, expected);
|
||||
}
|
||||
|
||||
function _assertEq(bytes32[] memory packed, bytes32[] memory expected) internal pure {
|
||||
assertEq(keccak256(abi.encode(packed)), keccak256(abi.encode(expected)));
|
||||
}
|
||||
}
|
||||
106
packages/contracts/test/utils/NoirUtils/PackFieldsArray.t.sol
Normal file
106
packages/contracts/test/utils/NoirUtils/PackFieldsArray.t.sol
Normal file
@@ -0,0 +1,106 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import {Test} from "forge-std/Test.sol";
|
||||
import {NoirUtilsHelper} from "./_NoirUtilsHelper.sol";
|
||||
|
||||
contract PackFieldsArrayTest is Test {
|
||||
NoirUtilsHelper private _helper;
|
||||
|
||||
function setUp() public {
|
||||
_helper = new NoirUtilsHelper();
|
||||
}
|
||||
|
||||
function test_revertsWhen_inputTooLong() public {
|
||||
// 32 bytes of data
|
||||
bytes memory input = new bytes(32);
|
||||
// 1 field can fit only 31 bytes, so this should revert
|
||||
uint256 numFields = 1;
|
||||
|
||||
vm.expectRevert();
|
||||
_helper.callPackFieldsArray(input, numFields);
|
||||
}
|
||||
|
||||
function test_emptyBytes() public view {
|
||||
bytes memory input = "";
|
||||
uint256 numFields = 1;
|
||||
|
||||
bytes32[] memory expected = new bytes32[](1);
|
||||
expected[0] = bytes32(uint256(0));
|
||||
|
||||
bytes32[] memory fields = _helper.callPackFieldsArray(input, numFields);
|
||||
_assertEq(fields, expected);
|
||||
}
|
||||
|
||||
function test_singleByte() public view {
|
||||
bytes memory input = bytes("A");
|
||||
uint256 numFields = 1;
|
||||
|
||||
bytes32[] memory expected = new bytes32[](1);
|
||||
expected[0] = bytes32(uint256(0x41));
|
||||
|
||||
bytes32[] memory fields = _helper.callPackFieldsArray(input, numFields);
|
||||
_assertEq(fields, expected);
|
||||
}
|
||||
|
||||
function test_31Bytes() public view {
|
||||
bytes memory input = new bytes(31);
|
||||
// 1 field can fit exactly 31 bytes
|
||||
uint256 numFields = 1;
|
||||
|
||||
bytes32[] memory expected = new bytes32[](1);
|
||||
expected[0] = bytes32(uint256(0));
|
||||
|
||||
bytes32[] memory fields = _helper.callPackFieldsArray(input, numFields);
|
||||
_assertEq(fields, expected);
|
||||
}
|
||||
|
||||
function test_bytesWithUnusedSlots() public view {
|
||||
bytes memory input = bytes("ABC");
|
||||
uint256 numFields = 3;
|
||||
|
||||
// 3 slots: 1 data slot + 2 unused slots
|
||||
bytes32[] memory expected = new bytes32[](3);
|
||||
// 1 data slot
|
||||
expected[0] = bytes32(
|
||||
uint256(
|
||||
0x41 // A
|
||||
+ (0x42 << 8) // B
|
||||
+ (0x43 << 16) // C
|
||||
)
|
||||
);
|
||||
// 2 unused slots
|
||||
expected[1] = bytes32(uint256(0));
|
||||
expected[2] = bytes32(uint256(0));
|
||||
|
||||
bytes32[] memory fields = _helper.callPackFieldsArray(input, numFields);
|
||||
_assertEq(fields, expected);
|
||||
}
|
||||
|
||||
function test_realisticString() public view {
|
||||
bytes memory input = bytes("gmail.com");
|
||||
uint256 numFields = 1;
|
||||
|
||||
bytes32[] memory expected = new bytes32[](1);
|
||||
expected[0] = bytes32(
|
||||
uint256(
|
||||
0x67 // g
|
||||
+ (0x6d << 8) // m
|
||||
+ (0x61 << 16) // a
|
||||
+ (0x69 << 24) // i
|
||||
+ (0x6c << 32) // l
|
||||
+ (0x2e << 40) // .
|
||||
+ (0x63 << 48) // c
|
||||
+ (0x6f << 56) // o
|
||||
+ (0x6d << 64) // m
|
||||
)
|
||||
);
|
||||
|
||||
bytes32[] memory fields = _helper.callPackFieldsArray(input, numFields);
|
||||
_assertEq(fields, expected);
|
||||
}
|
||||
|
||||
function _assertEq(bytes32[] memory fields, bytes32[] memory expected) internal pure {
|
||||
assertEq(keccak256(abi.encode(fields)), keccak256(abi.encode(expected)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import {Test} from "forge-std/Test.sol";
|
||||
import {NoirUtilsHelper} from "./_NoirUtilsHelper.sol";
|
||||
|
||||
contract UnpackBoundedVecU8Test is Test {
|
||||
NoirUtilsHelper private _helper;
|
||||
|
||||
function setUp() public {
|
||||
_helper = new NoirUtilsHelper();
|
||||
}
|
||||
|
||||
function test_correctlyUnpacks() public view {
|
||||
// 8 slots: 5 data slots + 2 unused slots + 1 length slot
|
||||
bytes32[] memory input = new bytes32[](8);
|
||||
// 5 data slots
|
||||
input[0] = bytes32(uint256(uint8(bytes1("h"))));
|
||||
input[1] = bytes32(uint256(uint8(bytes1("e"))));
|
||||
input[2] = bytes32(uint256(uint8(bytes1("l"))));
|
||||
input[3] = bytes32(uint256(uint8(bytes1("l"))));
|
||||
input[4] = bytes32(uint256(uint8(bytes1("o"))));
|
||||
// 2 unused slots
|
||||
input[5] = bytes32(0);
|
||||
input[6] = bytes32(0);
|
||||
// 1 length slot
|
||||
input[7] = bytes32(uint256(5));
|
||||
|
||||
bytes memory expected = bytes("hello");
|
||||
|
||||
bytes memory result = _helper.callUnpackBoundedVecU8(input);
|
||||
assertEq(keccak256(result), keccak256(expected));
|
||||
}
|
||||
}
|
||||
121
packages/contracts/test/utils/NoirUtils/UnpackFieldsArray.t.sol
Normal file
121
packages/contracts/test/utils/NoirUtils/UnpackFieldsArray.t.sol
Normal file
@@ -0,0 +1,121 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import {Test} from "forge-std/Test.sol";
|
||||
import {NoirUtilsHelper} from "./_NoirUtilsHelper.sol";
|
||||
|
||||
contract UnpackFieldsArrayTest is Test {
|
||||
NoirUtilsHelper private _helper;
|
||||
|
||||
function setUp() public {
|
||||
_helper = new NoirUtilsHelper();
|
||||
}
|
||||
|
||||
function test_emptyBytes() public view {
|
||||
bytes32[] memory inputFields = new bytes32[](1);
|
||||
inputFields[0] = bytes32(0);
|
||||
|
||||
bytes memory expected = bytes("");
|
||||
|
||||
bytes memory result = _helper.callUnpackFieldsArray(inputFields);
|
||||
assertEq(keccak256(result), keccak256(expected));
|
||||
}
|
||||
|
||||
function test_singleByte() public view {
|
||||
bytes32[] memory inputFields = new bytes32[](1);
|
||||
inputFields[0] = bytes32(uint256(0x41));
|
||||
|
||||
bytes memory expected = bytes("A");
|
||||
|
||||
bytes memory result = _helper.callUnpackFieldsArray(inputFields);
|
||||
assertEq(keccak256(result), keccak256(expected));
|
||||
}
|
||||
|
||||
function test_31Bytes() public view {
|
||||
// we want a 31 bytes length bytes that fits in 1 field
|
||||
bytes32[] memory inputFields = new bytes32[](1);
|
||||
// the field will hold an empty byte and 31 "A" bytes
|
||||
bytes memory data = new bytes(32);
|
||||
// 31 "A" bytes starting from the second byte
|
||||
for (uint256 i = 1; i < 32; i++) {
|
||||
data[i] = bytes1("A");
|
||||
}
|
||||
inputFields[0] = bytes32(data);
|
||||
|
||||
// expected bytes is 31 "A" bytes
|
||||
bytes memory expected = bytes("");
|
||||
for (uint256 i = 0; i < 31; i++) {
|
||||
expected = bytes.concat(expected, bytes("A"));
|
||||
}
|
||||
|
||||
bytes memory result = _helper.callUnpackFieldsArray(inputFields);
|
||||
assertEq(keccak256(result), keccak256(expected));
|
||||
}
|
||||
|
||||
function test_32Bytes() public view {
|
||||
// we want a 32 bytes length bytes with "A" in each byte that only fits in 2 fields
|
||||
bytes32[] memory inputFields = new bytes32[](2);
|
||||
|
||||
// first field will hold an empty byte and 31 "A" bytes
|
||||
bytes memory data = new bytes(32);
|
||||
// 31 "A" bytes starting from the second byte
|
||||
for (uint256 i = 1; i < 32; i++) {
|
||||
data[i] = bytes1("A");
|
||||
}
|
||||
inputFields[0] = bytes32(data);
|
||||
|
||||
// second field will hold the last "A" byte
|
||||
inputFields[1] = bytes32(uint256(uint8(bytes1("A"))));
|
||||
|
||||
// expected bytes is 32 "A" bytes
|
||||
bytes memory expected = bytes("");
|
||||
for (uint256 i = 0; i < 32; i++) {
|
||||
expected = bytes.concat(expected, bytes("A"));
|
||||
}
|
||||
|
||||
bytes memory result = _helper.callUnpackFieldsArray(inputFields);
|
||||
assertEq(keccak256(result), keccak256(expected));
|
||||
}
|
||||
|
||||
function test_bytesWithUnusedSlots() public view {
|
||||
bytes32[] memory inputFields = new bytes32[](3);
|
||||
// 1 data slot
|
||||
inputFields[0] = bytes32(
|
||||
uint256(
|
||||
0x41 // A
|
||||
+ (0x42 << 8) // B
|
||||
+ (0x43 << 16) // C
|
||||
)
|
||||
);
|
||||
// 2 unused slots
|
||||
inputFields[1] = bytes32(0);
|
||||
inputFields[2] = bytes32(0);
|
||||
|
||||
bytes memory expected = bytes("ABC");
|
||||
|
||||
bytes memory result = _helper.callUnpackFieldsArray(inputFields);
|
||||
assertEq(keccak256(result), keccak256(expected));
|
||||
}
|
||||
|
||||
function test_realisticString() public view {
|
||||
bytes32[] memory inputFields = new bytes32[](1);
|
||||
inputFields[0] = bytes32(
|
||||
uint256(
|
||||
0x67 // g
|
||||
+ (0x6d << 8) // m
|
||||
+ (0x61 << 16) // a
|
||||
+ (0x69 << 24) // i
|
||||
+ (0x6c << 32) // l
|
||||
+ (0x2e << 40) // .
|
||||
+ (0x63 << 48) // c
|
||||
+ (0x6f << 56) // o
|
||||
+ (0x6d << 64) // m
|
||||
)
|
||||
);
|
||||
|
||||
bytes memory expected = bytes("gmail.com");
|
||||
|
||||
bytes memory result = _helper.callUnpackFieldsArray(inputFields);
|
||||
assertEq(keccak256(result), keccak256(expected));
|
||||
}
|
||||
}
|
||||
26
packages/contracts/test/utils/NoirUtils/_NoirUtilsHelper.sol
Normal file
26
packages/contracts/test/utils/NoirUtils/_NoirUtilsHelper.sol
Normal file
@@ -0,0 +1,26 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import {NoirUtils} from "../../../utils/NoirUtils.sol";
|
||||
|
||||
contract NoirUtilsHelper {
|
||||
// PACK FUNCTIONS
|
||||
|
||||
function callPackBoundedVecU8(bytes memory input, uint256 numFields) external pure returns (bytes32[] memory) {
|
||||
return NoirUtils.packBoundedVecU8(input, numFields);
|
||||
}
|
||||
|
||||
function callPackFieldsArray(bytes memory input, uint256 numFields) external pure returns (bytes32[] memory) {
|
||||
return NoirUtils.packFieldsArray(input, numFields);
|
||||
}
|
||||
|
||||
// UNPACK FUNCTIONS
|
||||
|
||||
function callUnpackBoundedVecU8(bytes32[] memory fields) external pure returns (bytes memory) {
|
||||
return NoirUtils.unpackBoundedVecU8(fields);
|
||||
}
|
||||
|
||||
function callUnpackFieldsArray(bytes32[] memory fields) external pure returns (bytes memory) {
|
||||
return NoirUtils.unpackFieldsArray(fields);
|
||||
}
|
||||
}
|
||||
132
packages/contracts/utils/CircomUtils.sol
Normal file
132
packages/contracts/utils/CircomUtils.sol
Normal file
@@ -0,0 +1,132 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
import {Bytes} from "@openzeppelin/contracts/utils/Bytes.sol";
|
||||
|
||||
/**
|
||||
* @title CircomUtils
|
||||
* @notice Library for ZK circuit-related utilities including field element packing and proof processing
|
||||
* @dev This library provides functions for converting between byte arrays and field elements
|
||||
* and other utilities needed for zero-knowledge proof circuit compatibility.
|
||||
*/
|
||||
library CircomUtils {
|
||||
using Bytes for bytes;
|
||||
|
||||
/**
|
||||
* @notice Error thrown when the public inputs array length is not exactly 60
|
||||
* @dev The ZK circuit expects exactly 60 public inputs for verification
|
||||
*/
|
||||
error InvalidPublicInputsLength();
|
||||
|
||||
/**
|
||||
* @notice Error thrown when the data length is greater than the padded size
|
||||
* @dev The data should have the expected format and length
|
||||
*/
|
||||
error InvalidDataLength();
|
||||
|
||||
/**
|
||||
* @notice Packs byte arrays into field elements for ZK circuit compatibility
|
||||
* @param input The byte array to pack into field elements
|
||||
* @param paddedSize The target size after padding (must be larger than or equal to _bytes.length)
|
||||
* @return fields An array of field elements containing the packed byte data
|
||||
* @dev This function packs bytes into field elements by:
|
||||
* 1. Determining how many field elements are needed (31 bytes per field element)
|
||||
* 2. Packing bytes in little-endian order within each field element
|
||||
* 3. Padding with zeros if the input is shorter than paddedSize
|
||||
* 4. Ensuring the resulting field elements are compatible with ZK circuits
|
||||
*
|
||||
* Each field element can contain up to 31 bytes to ensure the result stays below
|
||||
* the BN128 curve order. Bytes are packed as: byte0 + (byte1 << 8) + (byte2 << 16) + ...
|
||||
*/
|
||||
function packFieldsArray(bytes memory input, uint256 paddedSize) internal pure returns (bytes32[] memory fields) {
|
||||
if (input.length > paddedSize) revert InvalidDataLength();
|
||||
|
||||
uint256 remain = paddedSize % 31;
|
||||
uint256 numFields = (paddedSize - remain) / 31;
|
||||
if (remain > 0) {
|
||||
numFields += 1;
|
||||
}
|
||||
fields = new bytes32[](numFields);
|
||||
uint256 idx = 0;
|
||||
uint256 byteVal = 0;
|
||||
for (uint256 i = 0; i < numFields; i++) {
|
||||
for (uint256 j = 0; j < 31; j++) {
|
||||
idx = i * 31 + j;
|
||||
if (idx >= paddedSize) {
|
||||
break;
|
||||
}
|
||||
if (idx >= input.length) {
|
||||
byteVal = 0;
|
||||
} else {
|
||||
byteVal = uint256(uint8(input[idx]));
|
||||
}
|
||||
if (j == 0) {
|
||||
fields[i] = bytes32(byteVal);
|
||||
} else {
|
||||
fields[i] = bytes32(uint256(fields[i]) + (byteVal << (8 * j)));
|
||||
}
|
||||
}
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Packs a boolean value into a single field element
|
||||
* @param input The boolean value to pack
|
||||
* @return fields The packed field element
|
||||
*/
|
||||
function packBool(bool input) internal pure returns (bytes32[] memory fields) {
|
||||
fields = new bytes32[](1);
|
||||
fields[0] = input ? bytes32(uint256(1)) : bytes32(uint256(0));
|
||||
return fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Unpacks field elements back to bytes
|
||||
* @param fields Array of field elements
|
||||
* @param paddedSize Original padded size of the bytes
|
||||
* @return result The unpacked bytes
|
||||
*/
|
||||
function unpackFieldsArray(bytes32[] memory fields, uint256 paddedSize)
|
||||
internal
|
||||
pure
|
||||
returns (bytes memory result)
|
||||
{
|
||||
uint256 remain = paddedSize % 31;
|
||||
uint256 numFields = (paddedSize - remain) / 31;
|
||||
if (remain > 0) {
|
||||
numFields += 1;
|
||||
}
|
||||
|
||||
result = new bytes(paddedSize);
|
||||
uint256 resultIndex = 0;
|
||||
|
||||
for (uint256 i = 0; i < numFields; i++) {
|
||||
uint256 field = uint256(fields[i]);
|
||||
for (uint256 j = 0; j < 31 && resultIndex < paddedSize; j++) {
|
||||
result[resultIndex] = bytes1(uint8(field & 0xFF));
|
||||
field = field >> 8;
|
||||
resultIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
// Trim trailing zeros
|
||||
uint256 actualLength = 0;
|
||||
for (uint256 i = 0; i < result.length; i++) {
|
||||
if (result[i] != 0) {
|
||||
actualLength = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return result.slice(0, actualLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Unpacks a boolean value from public inputs
|
||||
* @param fields Array of field elements
|
||||
* @return result The unpacked boolean value
|
||||
*/
|
||||
function unpackBool(bytes32[] memory fields) internal pure returns (bool result) {
|
||||
return uint256(fields[0]) == 1;
|
||||
}
|
||||
}
|
||||
94
packages/contracts/utils/NoirUtils.sol
Normal file
94
packages/contracts/utils/NoirUtils.sol
Normal file
@@ -0,0 +1,94 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.30;
|
||||
|
||||
uint256 constant FIELD_BYTES = 31;
|
||||
|
||||
library NoirUtils {
|
||||
error InvalidLength();
|
||||
|
||||
function packBoundedVecU8(bytes memory input, uint256 numFields) internal pure returns (bytes32[] memory) {
|
||||
// numFields includes the length field, therefore length should be less than numFields
|
||||
if (input.length >= numFields) revert InvalidLength();
|
||||
|
||||
bytes32[] memory result = new bytes32[](numFields);
|
||||
|
||||
// First fields are the data
|
||||
for (uint256 i = 0; i < input.length; i++) {
|
||||
result[i] = bytes32(uint256(uint8(input[i])));
|
||||
}
|
||||
// Other fields are empty
|
||||
|
||||
// Last element is the length
|
||||
result[numFields - 1] = bytes32(input.length);
|
||||
return result;
|
||||
}
|
||||
|
||||
function packFieldsArray(bytes memory input, uint256 numFields) internal pure returns (bytes32[] memory) {
|
||||
if (input.length > numFields * FIELD_BYTES) revert InvalidLength();
|
||||
|
||||
bytes32[] memory fieldElements = new bytes32[](numFields);
|
||||
|
||||
for (uint256 i = 0; i < numFields; i++) {
|
||||
uint256 start = i * FIELD_BYTES;
|
||||
uint256 field = 0;
|
||||
|
||||
for (uint256 j = 0; j < FIELD_BYTES; j++) {
|
||||
if (start + j < input.length) {
|
||||
// LSB first
|
||||
field |= uint256(uint8(input[start + j])) << (8 * j);
|
||||
} else {
|
||||
// Padding with 0x00 (already zeroed by default)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
fieldElements[i] = bytes32(field);
|
||||
}
|
||||
|
||||
return fieldElements;
|
||||
}
|
||||
|
||||
function unpackBoundedVecU8(bytes32[] memory fields) internal pure returns (bytes memory) {
|
||||
// BoundedVec stores the length of the array in the last element
|
||||
uint256 length = uint256(fields[fields.length - 1]);
|
||||
// Create a new bytes array of the correct length
|
||||
bytes memory result = new bytes(length);
|
||||
for (uint256 i = 0; i < length; i++) {
|
||||
// u8 is 8 bits, so we need to take the least-significant byte of each bytes32
|
||||
result[i] = bytes1(uint8(uint256(fields[i])));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function unpackFieldsArray(bytes32[] memory fields) internal pure returns (bytes memory) {
|
||||
uint256 totalBytes = fields.length * FIELD_BYTES;
|
||||
bytes memory result = new bytes(totalBytes);
|
||||
uint256 resultIndex = 0;
|
||||
|
||||
for (uint256 i = 0; i < fields.length; i++) {
|
||||
uint256 field = uint256(fields[i]); // Convert bytes32 to uint256
|
||||
|
||||
// Extract FIELD_BYTES bytes in little-endian order (LSB first)
|
||||
for (uint256 j = 0; j < FIELD_BYTES && resultIndex < totalBytes; j++) {
|
||||
result[resultIndex++] = bytes1(uint8(field));
|
||||
field >>= 8;
|
||||
}
|
||||
}
|
||||
|
||||
// Trim trailing 0x00 bytes (preserve internal 0x00s)
|
||||
uint256 actualLength = 0;
|
||||
for (uint256 i = 0; i < result.length; i++) {
|
||||
if (result[i] != 0) {
|
||||
actualLength = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Create trimmed byte array
|
||||
bytes memory trimmed = new bytes(actualLength);
|
||||
for (uint256 i = 0; i < actualLength; i++) {
|
||||
trimmed[i] = result[i];
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user