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:
Bence Háromi
2025-10-08 18:40:06 +01:00
committed by GitHub
parent 62dd548718
commit abe9d839d2
16 changed files with 869 additions and 607 deletions

View File

@@ -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;
}
}

View File

@@ -14,7 +14,6 @@
"files": [
"DKIMRegistry.sol",
"UserOverrideableDKIMRegistry.sol",
"CircuitUtils.sol",
"/utils",
"/interfaces"
],

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View 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)));
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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)));
}
}

View 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)));
}
}

View File

@@ -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));
}
}

View 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));
}
}

View 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);
}
}

View 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;
}
}

View 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;
}
}