Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add string utils to have packsize improvements from zkp2p #125

Merged
merged 2 commits into from
Oct 26, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
278 changes: 278 additions & 0 deletions packages/contracts/utils/StringUtils.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.6;

// https://github.com/nalinbhardwaj/ethdosnumber/blob/main/ethdos-contracts/src/HexStrings.sol
library StringUtils {
bytes16 internal constant ALPHABET = "0123456789abcdef";
uint256 internal constant DEFAULT_PACK_SIZE = 31;

/// @notice Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length.
/// @dev Credit to Open Zeppelin under MIT license https://github.com/OpenZeppelin/openzeppelin-contracts/blob/243adff49ce1700e0ecb99fe522fb16cff1d1ddc/contracts/utils/Strings.sol#L55
function toHexString(uint256 value, uint256 length) internal pure returns (string memory) {
bytes memory buffer = new bytes(2 * length + 2);
buffer[0] = "0";
buffer[1] = "x";
for (uint256 i = 2 * length + 1; i > 1; --i) {
buffer[i] = ALPHABET[value & 0xf];
value >>= 4;
}
require(value == 0, "Strings: hex length insufficient");
return string(buffer);
}

function toHexStringNoPrefix(uint256 value, uint256 length) internal pure returns (string memory) {
bytes memory buffer = new bytes(2 * length);
for (uint256 i = buffer.length; i > 0; i--) {
buffer[i - 1] = ALPHABET[value & 0xf];
value >>= 4;
}
return string(buffer);
}

function toString(uint256 value) internal pure returns (string memory) {
return toString(abi.encodePacked(value));
}

function toString(bytes32 value) internal pure returns (string memory) {
return toString(abi.encodePacked(value));
}

function toString(address account) internal pure returns (string memory) {
return toString(abi.encodePacked(account));
}

function stringEq(string memory a, string memory b) internal pure returns (bool) {
return keccak256(abi.encodePacked(a)) == keccak256(abi.encodePacked(b));
}

function toString(bytes memory data) internal pure returns (string memory) {
bytes memory alphabet = "0123456789abcdef";

bytes memory str = new bytes(2 + data.length * 2);
str[0] = "0";
str[1] = "x";
for (uint256 i = 0; i < data.length; i++) {
str[2 + i * 2] = alphabet[uint256(uint8(data[i] >> 4))];
str[3 + i * 2] = alphabet[uint256(uint8(data[i] & 0x0f))];
}
return string(str);
}

// 1 packed byte = packSize (usually 31) normal bytes, all in one 255/256-bit value
// Note that this is not 32 due to the field modulus of circom
function convertPackedByteToString(uint256 packedByte, uint256 packSize)
internal
pure
returns (string memory extractedString)
{
uint256[] memory packedBytes = new uint256[](1);
packedBytes[0] = packedByte;
return convertPackedBytesToString(packedBytes, packSize, packSize);
}

// Note: This convenience function removes the max string length check, which may cause misalignment with the circom
// If using this, then the circom needs to rangecheck packed length in the circuit itself
// This defaults to 31 bytes per packed byte
function convertPackedBytesToString(uint256[] memory packedBytes)
internal
pure
returns (string memory extractedString)
{
return convertPackedBytesToString(packedBytes, packedBytes.length * DEFAULT_PACK_SIZE, DEFAULT_PACK_SIZE);
}

// Unpacks uint256s into bytes and then extracts the non-zero characters
// Only extracts contiguous non-zero characters and ensures theres only 1 such state
// Note that unpackedLen may be more than packedBytes.length * 8 since there may be 0s
// signals is the total number of signals (i.e. bytes) packed into the packedBytes. it defaults to packedBytes.length * packSize
function convertPackedBytesToString(uint256[] memory packedBytes, uint256 signals, uint256 packSize)
internal
pure
returns (string memory extractedString)
{
uint8 state = 0;
// bytes: 0 0 0 0 y u s h _ g 0 0 0
// state: 0 0 0 0 1 1 1 1 1 1 2 2 2
bytes memory nonzeroBytesArray = new bytes(packedBytes.length * packSize);
uint256 nonzeroBytesArrayIndex = 0;
for (uint16 i = 0; i < packedBytes.length; i++) {
uint256 packedByte = packedBytes[i];
uint8[] memory unpackedBytes = new uint8[](packSize);
for (uint256 j = 0; j < packSize; j++) {
unpackedBytes[j] = uint8(packedByte >> (j * 8));
}
for (uint256 j = 0; j < packSize; j++) {
uint256 unpackedByte = unpackedBytes[j]; //unpackedBytes[j];
if (unpackedByte != 0) {
nonzeroBytesArray[nonzeroBytesArrayIndex] = bytes1(uint8(unpackedByte));
nonzeroBytesArrayIndex++;
if (state % 2 == 0) {
state += 1;
}
} else {
if (state % 2 == 1) {
state += 1;
}
}
packedByte = packedByte >> 8;
}
}
// TODO: You might want to assert that the state is exactly 1 or 2
// If not, that means empty bytse have been removed from the middle and things have been concatenated.
// We removed due to some tests failing, but this is not ideal and the require should be uncommented as soon as tests pass with it.

// require(state == 1 || state == 2, "Invalid final state of packed bytes in email; more than two non-zero regions found!");
require(state >= 1, "No packed bytes found! Invalid final state of packed bytes in email; value is likely 0!");
require(nonzeroBytesArrayIndex <= signals, "Packed bytes more than allowed max number of signals!");
string memory returnValue = removeTrailingZeros(string(nonzeroBytesArray));
return returnValue;
// Have to end at the end of the email -- state cannot be 1 since there should be an email footer
}

function bytes32ToString(bytes32 input) internal pure returns (string memory) {
uint256 i;
for (i = 0; i < 32 && input[i] != 0; i++) {}
bytes memory resultBytes = new bytes(i);
for (i = 0; i < 32 && input[i] != 0; i++) {
resultBytes[i] = input[i];
}
return string(resultBytes);
}

// sliceArray is used to slice an array of uint256s from start-end into a new array of uint256s
function sliceArray(uint256[] memory input, uint256 start, uint256 end) internal pure returns (uint256[] memory) {
require(start <= end && end <= input.length, "Invalid slice indices");
uint256[] memory result = new uint256[](end - start);
for (uint256 i = start; i < end; i++) {
result[i - start] = input[i];
}
return result;
}

// stringToUint is used to convert a string like "45" to a uint256 4
function stringToUint(string memory s) internal pure returns (uint256) {
bytes memory b = bytes(s);
uint256 result = 0;
for (uint256 i = 0; i < b.length; i++) {
if (b[i] >= 0x30 && b[i] <= 0x39) {
result = result * 10 + (uint256(uint8(b[i])) - 48);
}

// TODO: Currently truncates decimals
if (b[i] == 0x2E) {
return result;
}
}
return result;
}

// getDomainFromEmail is used to extract the domain from an email i.e. the part after the @
function getDomainFromEmail(string memory fromEmail) internal pure returns (string memory) {
bytes memory emailBytes = bytes(fromEmail);
uint256 atIndex;
for (uint256 i = 0; i < emailBytes.length; i++) {
if (emailBytes[i] == "@") {
atIndex = i;
break;
}
}

bytes memory domainBytes = new bytes(emailBytes.length - atIndex - 1);
for (uint256 j = 0; j < domainBytes.length; j++) {
domainBytes[j] = emailBytes[atIndex + 1 + j];
}
return bytes32ToString(bytes32(bytes(domainBytes)));
}

function removeTrailingZeros(string memory input) public pure returns (string memory) {
bytes memory inputBytes = bytes(input);
uint256 endIndex = inputBytes.length;

for (uint256 i = 0; i < inputBytes.length; i++) {
if (inputBytes[i] == 0) {
endIndex = i;
break;
}
}

bytes memory resultBytes = new bytes(endIndex);
for (uint256 i = 0; i < endIndex; i++) {
resultBytes[i] = inputBytes[i];
}

return string(resultBytes);
}

// Upper/lower string utils from https://github.com/willitscale/solidity-util/blob/master/lib/Strings.sol
/**
* Upper
*
* Converts all the values of a string to their corresponding upper case
* value.
*
* @param _base When being used for a data type this is the extended object
* otherwise this is the string base to convert to upper case
* @return string
*/
function upper(string memory _base) public pure returns (string memory) {
bytes memory _baseBytes = bytes(_base);
for (uint256 i = 0; i < _baseBytes.length; i++) {
_baseBytes[i] = _upper(_baseBytes[i]);
}
return string(_baseBytes);
}

/**
* Lower
*
* Converts all the values of a string to their corresponding lower case
* value.
*
* @param _base When being used for a data type this is the extended object
* otherwise this is the string base to convert to lower case
* @return string
*/
function lower(string memory _base) public pure returns (string memory) {
bytes memory _baseBytes = bytes(_base);
for (uint256 i = 0; i < _baseBytes.length; i++) {
_baseBytes[i] = _lower(_baseBytes[i]);
}
return string(_baseBytes);
}

/**
* Upper
*
* Convert an alphabetic character to upper case and return the original
* value when not alphabetic
*
* @param _b1 The byte to be converted to upper case
* @return bytes1 The converted value if the passed value was alphabetic
* and in a lower case otherwise returns the original value
*/
function _upper(bytes1 _b1) private pure returns (bytes1) {
if (_b1 >= 0x61 && _b1 <= 0x7A) {
return bytes1(uint8(_b1) - 32);
}

return _b1;
}

/**
* Lower
*
* Convert an alphabetic character to lower case and return the original
* value when not alphabetic
*
* @param _b1 The byte to be converted to lower case
* @return bytes1 The converted value if the passed value was alphabetic
* and in a upper case otherwise returns the original value
*/
function _lower(bytes1 _b1) private pure returns (bytes1) {
if (_b1 >= 0x41 && _b1 <= 0x5A) {
return bytes1(uint8(_b1) + 32);
}

return _b1;
}
}