diff --git a/contracts/mocks/Stateless.sol b/contracts/mocks/Stateless.sol index fd0cf398eb0..bc346351edd 100644 --- a/contracts/mocks/Stateless.sol +++ b/contracts/mocks/Stateless.sol @@ -31,9 +31,12 @@ import {ERC4337Utils} from "../account/utils/draft-ERC4337Utils.sol"; import {ERC7579Utils} from "../account/utils/draft-ERC7579Utils.sol"; import {ERC7913P256Verifier} from "../utils/cryptography/verifiers/ERC7913P256Verifier.sol"; import {ERC7913RSAVerifier} from "../utils/cryptography/verifiers/ERC7913RSAVerifier.sol"; +import {FastLZ} from "../utils/compression/FastLZ.sol"; import {Heap} from "../utils/structs/Heap.sol"; import {InteroperableAddress} from "../utils/draft-InteroperableAddress.sol"; +import {LZ4} from "../utils/compression/LZ4.sol"; import {Math} from "../utils/math/Math.sol"; +import {Memory} from "../utils/Memory.sol"; import {MerkleProof} from "../utils/cryptography/MerkleProof.sol"; import {MessageHashUtils} from "../utils/cryptography/MessageHashUtils.sol"; import {Nonces} from "../utils/Nonces.sol"; @@ -47,9 +50,9 @@ import {SafeERC20} from "../token/ERC20/utils/SafeERC20.sol"; import {ShortStrings} from "../utils/ShortStrings.sol"; import {SignatureChecker} from "../utils/cryptography/SignatureChecker.sol"; import {SignedMath} from "../utils/math/SignedMath.sol"; +import {Snappy} from "../utils/compression/Snappy.sol"; import {StorageSlot} from "../utils/StorageSlot.sol"; import {Strings} from "../utils/Strings.sol"; -import {Memory} from "../utils/Memory.sol"; import {Time} from "../utils/types/Time.sol"; contract Dummy1234 {} diff --git a/contracts/utils/compression/FastLZ.sol b/contracts/utils/compression/FastLZ.sol new file mode 100644 index 00000000000..5bcec878425 --- /dev/null +++ b/contracts/utils/compression/FastLZ.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +/** + * @dev Library for decompressing data using FastLZ. + * + * See https://ariya.github.io/FastLZ/ + */ +library FastLZ { + /** + * @dev FastLZ level 1 decompression. + * + * Based on the reference implementation available here: + * https://github.com/ariya/FastLZ?tab=readme-ov-file#decompressor-reference-implementation + */ + function decompress(bytes memory input) internal pure returns (bytes memory output) { + assembly ("memory-safe") { + let inputPtr := add(input, 0x20) + let inputEnd := add(add(input, 0x20), mload(input)) + + // Use new memory allocate at the FMP + output := mload(0x40) + let outputPtr := add(output, 0x20) + + for {} lt(inputPtr, inputEnd) {} { + let chunk := mload(inputPtr) + let first := byte(0, chunk) + let type_ := shr(5, first) + + switch type_ + case 0 { + mstore(outputPtr, mload(add(inputPtr, 1))) + inputPtr := add(inputPtr, add(2, first)) + outputPtr := add(outputPtr, add(1, first)) + } + case 7 { + let len := add(9, byte(1, chunk)) + for { + let i := 0 + let ofs := add(add(shl(8, and(first, 31)), byte(2, chunk)), 1) + let ref := sub(outputPtr, ofs) + let step := xor(len, mul(lt(ofs, len), xor(ofs, len))) + } lt(i, len) { + i := add(i, step) + } { + mcopy(add(outputPtr, i), add(ref, i), step) + } + inputPtr := add(inputPtr, 3) + outputPtr := add(outputPtr, len) + } + default { + let len := add(2, type_) + for { + let i := 0 + let ofs := add(add(shl(8, and(first, 31)), byte(1, chunk)), 1) + let ref := sub(outputPtr, ofs) + let step := xor(len, mul(lt(ofs, len), xor(ofs, len))) + } lt(i, len) { + i := add(i, step) + } { + mcopy(add(outputPtr, i), add(ref, i), step) + } + inputPtr := add(inputPtr, 2) + outputPtr := add(outputPtr, len) + } + } + if iszero(eq(inputPtr, inputEnd)) { + revert(0, 0) + } + + mstore(output, sub(outputPtr, add(output, 0x20))) + mstore(0x40, outputPtr) + } + } + + function decompressCalldata(bytes calldata input) internal pure returns (bytes memory output) { + assembly ("memory-safe") { + let inputPtr := input.offset + let inputEnd := add(input.offset, input.length) + + // Use new memory allocate at the FMP + output := mload(0x40) + let outputPtr := add(output, 0x20) + + for {} lt(inputPtr, inputEnd) {} { + let chunk := calldataload(inputPtr) + let first := byte(0, chunk) + let type_ := shr(5, first) + + switch type_ + case 0 { + mstore(outputPtr, calldataload(add(inputPtr, 1))) + inputPtr := add(inputPtr, add(2, first)) + outputPtr := add(outputPtr, add(1, first)) + } + case 7 { + let len := add(9, byte(1, chunk)) + for { + let i := 0 + let ofs := add(add(shl(8, and(first, 31)), byte(2, chunk)), 1) + let ref := sub(outputPtr, ofs) + let step := xor(len, mul(lt(ofs, len), xor(ofs, len))) + } lt(i, len) { + i := add(i, step) + } { + mcopy(add(outputPtr, i), add(ref, i), step) + } + inputPtr := add(inputPtr, 3) + outputPtr := add(outputPtr, len) + } + default { + let len := add(2, type_) + for { + let i := 0 + let ofs := add(add(shl(8, and(first, 31)), byte(1, chunk)), 1) + let ref := sub(outputPtr, ofs) + let step := xor(len, mul(lt(ofs, len), xor(ofs, len))) + } lt(i, len) { + i := add(i, step) + } { + mcopy(add(outputPtr, i), add(ref, i), step) + } + inputPtr := add(inputPtr, 2) + outputPtr := add(outputPtr, len) + } + } + if iszero(eq(inputPtr, inputEnd)) { + revert(0, 0) + } + + mstore(output, sub(outputPtr, add(output, 0x20))) + mstore(0x40, outputPtr) + } + } +} diff --git a/contracts/utils/compression/LZ4.sol b/contracts/utils/compression/LZ4.sol new file mode 100644 index 00000000000..f1a73b8a63c --- /dev/null +++ b/contracts/utils/compression/LZ4.sol @@ -0,0 +1,293 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +/** + * @dev Library for decompressing data using LZ4. + * + * See https://lz4.org/ + */ +library LZ4 { + // Compression format parameters/constants. + uint256 private constant MIN_MATCH = 4; + // Frame constants + uint32 private constant MAGIC_NUM = 0x04224d18; // reversed endianness (first 4 bytes of the frame) + // Frame descriptor flags + uint8 private constant FD_CONTENT_SIZE = 0x08; + uint8 private constant FD_BLOCK_CHKSUM = 0x10; + uint8 private constant FD_VERSION_MASK = 0xC0; + uint8 private constant FD_VERSION = 0x40; + // Block sizes + uint32 private constant BS_UNCOMPRESSED = 0x80000000; + uint8 private constant BS_SHIFT = 0x04; + uint8 private constant BS_MASK = 0x07; + + error InvalidMagicNumber(); + error InvalidVersion(); + error DecodingFailure(); + + /** + * @dev Implementation of LZ4's decompress function. + * + * See https://github.com/Benzinga/lz4js/blob/master/lz4.js + */ + function decompress(bytes memory input) internal pure returns (bytes memory output) { + bytes4 invalidMagicNumberCode = InvalidMagicNumber.selector; + bytes4 invalidVersionCode = InvalidVersion.selector; + bytes4 decodingFailureCode = DecodingFailure.selector; + + assembly ("memory-safe") { + function assert(b, e) { + if iszero(b) { + mstore(0, e) + revert(0, 4) + } + } + // input buffer + let inputPtr := add(input, 0x20) + let inputEnd := add(inputPtr, mload(input)) + // output buffer + output := mload(0x40) + let outputPtr := add(output, 0x20) + // ========================================== decompress frame =========================================== + // Get header (size must be at least 7 / 15 depending on useContentSize ) + // [ magic (4) ++ descriptor (1) ++ bsIds (1) ++ contentsize (0 or 8) ++ ??? (1) ] + let header := mload(inputPtr) + // read magic number (first 4 bytes, realigned right) + assert(eq(shr(224, header), MAGIC_NUM), invalidMagicNumberCode) + // read descriptor and check version + let descriptor := byte(4, header) + assert(eq(and(descriptor, FD_VERSION_MASK), FD_VERSION), invalidVersionCode) + // read flags + let useBlockSum := eq(and(descriptor, FD_BLOCK_CHKSUM), FD_BLOCK_CHKSUM) + let useContentSize := eq(and(descriptor, FD_CONTENT_SIZE), FD_CONTENT_SIZE) + // read block size + let bsIdx := and(shr(BS_SHIFT, byte(5, header)), BS_MASK) + assert(and(gt(bsIdx, 3), lt(bsIdx, 8)), decodingFailureCode) + // move forward 7 or 15 bytes depending on "useContentSize" + inputPtr := add(inputPtr, add(7, mul(useContentSize, 8))) + // read blocks + for {} 1 {} { + let chunk := mload(inputPtr) + // read block length (32 bits = 4 bytes reverse endianness) + let blockLength := or( + or(byte(0, chunk), shl(8, byte(1, chunk))), + or(shl(16, byte(2, chunk)), shl(24, byte(3, chunk))) + ) + inputPtr := add(inputPtr, 4) + // empty block means we are done with decoding + if iszero(blockLength) { + break + } + // read block checksum if "useBlockSum" (from chunk) ? + if useBlockSum { + inputPtr := add(inputPtr, 4) + } + // check if block is compressed + switch iszero(and(blockLength, BS_UNCOMPRESSED)) + // uncompressed block case + case 0 { + // mask off the 'uncompressed' bit + blockLength := and(blockLength, not(BS_UNCOMPRESSED)) + // copy uncompressed data to the output buffer + mcopy(outputPtr, inputPtr, blockLength) + inputPtr := add(inputPtr, blockLength) + outputPtr := add(outputPtr, blockLength) + } + // compressed block case + case 1 { + let blockEnd := add(inputPtr, blockLength) + for {} lt(inputPtr, blockEnd) {} { + let token := byte(0, mload(inputPtr)) + inputPtr := add(inputPtr, 1) + // literals to copy + let literalLength := shr(4, token) + if literalLength { + // Parse length. + if eq(literalLength, 0xf) { + for {} 1 {} { + let count := byte(0, mload(inputPtr)) + inputPtr := add(inputPtr, 1) + literalLength := add(literalLength, count) + if lt(count, 0xff) { + break + } + } + } + mcopy(outputPtr, inputPtr, literalLength) + inputPtr := add(inputPtr, literalLength) + outputPtr := add(outputPtr, literalLength) + } + // if we are done reading the block, break the switch (continue the loop) + if iszero(lt(inputPtr, blockEnd)) { + break + } + // read offset (32 bits = 4 bytes reverse endianness) + chunk := mload(inputPtr) + let offset := or(byte(0, chunk), shl(8, byte(1, chunk))) + inputPtr := add(inputPtr, 2) + // parse length of the copy section + let copyLength := and(token, 0xf) + if eq(copyLength, 0xf) { + for {} 1 {} { + let count := byte(0, mload(inputPtr)) + inputPtr := add(inputPtr, 1) + copyLength := add(copyLength, count) + if lt(count, 0xff) { + break + } + } + } + copyLength := add(copyLength, MIN_MATCH) + // do the copy + for { + let ptr := outputPtr + let end := add(outputPtr, copyLength) + let step := xor(offset, mul(lt(copyLength, offset), xor(copyLength, offset))) // min(copyLength, offset) + } lt(ptr, end) { + ptr := add(ptr, step) + } { + mcopy(ptr, sub(ptr, offset), step) + } + outputPtr := add(outputPtr, copyLength) + } + assert(eq(inputPtr, blockEnd), decodingFailureCode) + } + } + assert(eq(inputPtr, inputEnd), decodingFailureCode) + // allocate used memory + mstore(output, sub(outputPtr, add(output, 0x20))) + mstore(0x40, outputPtr) + } + } + + function decompressCalldata(bytes calldata input) internal pure returns (bytes memory output) { + bytes4 invalidMagicNumberCode = InvalidMagicNumber.selector; + bytes4 invalidVersionCode = InvalidVersion.selector; + bytes4 decodingFailureCode = DecodingFailure.selector; + + assembly ("memory-safe") { + function assert(b, e) { + if iszero(b) { + mstore(0, e) + revert(0, 4) + } + } + // input buffer + let inputPtr := input.offset + let inputEnd := add(inputPtr, input.length) + // output buffer + output := mload(0x40) + let outputPtr := add(output, 0x20) + // ========================================== decompress frame =========================================== + // Get header (size must be at least 7 / 15 depending on useContentSize ) + // [ magic (4) ++ descriptor (1) ++ bsIds (1) ++ contentsize (0 or 8) ++ ??? (1) ] + let header := calldataload(inputPtr) + // read magic number (first 4 bytes, realigned right) + assert(eq(shr(224, header), MAGIC_NUM), invalidMagicNumberCode) + // read descriptor and check version + let descriptor := byte(4, header) + assert(eq(and(descriptor, FD_VERSION_MASK), FD_VERSION), invalidVersionCode) + // read flags + let useBlockSum := eq(and(descriptor, FD_BLOCK_CHKSUM), FD_BLOCK_CHKSUM) + let useContentSize := eq(and(descriptor, FD_CONTENT_SIZE), FD_CONTENT_SIZE) + // read block size + let bsIdx := and(shr(BS_SHIFT, byte(5, header)), BS_MASK) + assert(and(gt(bsIdx, 3), lt(bsIdx, 8)), decodingFailureCode) + // move forward 7 or 15 bytes depending on "useContentSize" + inputPtr := add(inputPtr, add(7, mul(useContentSize, 8))) + // read blocks + for {} 1 {} { + let chunk := calldataload(inputPtr) + // read block length (32 bits = 4 bytes reverse endianness) + let blockLength := or( + or(byte(0, chunk), shl(8, byte(1, chunk))), + or(shl(16, byte(2, chunk)), shl(24, byte(3, chunk))) + ) + inputPtr := add(inputPtr, 4) + // empty block means we are done with decoding + if iszero(blockLength) { + break + } + // read block checksum if "useBlockSum" (from chunk) ? + if useBlockSum { + inputPtr := add(inputPtr, 4) + } + // check if block is compressed + switch iszero(and(blockLength, BS_UNCOMPRESSED)) + // uncompressed block case + case 0 { + // mask off the 'uncompressed' bit + blockLength := and(blockLength, not(BS_UNCOMPRESSED)) + // copy uncompressed data to the output buffer + calldatacopy(outputPtr, inputPtr, blockLength) + inputPtr := add(inputPtr, blockLength) + outputPtr := add(outputPtr, blockLength) + } + // compressed block case + case 1 { + let blockEnd := add(inputPtr, blockLength) + for {} lt(inputPtr, blockEnd) {} { + let token := byte(0, calldataload(inputPtr)) + inputPtr := add(inputPtr, 1) + // literals to copy + let literalLength := shr(4, token) + if literalLength { + // Parse length. + if eq(literalLength, 0xf) { + for {} 1 {} { + let count := byte(0, calldataload(inputPtr)) + inputPtr := add(inputPtr, 1) + literalLength := add(literalLength, count) + if lt(count, 0xff) { + break + } + } + } + calldatacopy(outputPtr, inputPtr, literalLength) + inputPtr := add(inputPtr, literalLength) + outputPtr := add(outputPtr, literalLength) + } + // if we are done reading the block, break the switch (continue the loop) + if iszero(lt(inputPtr, blockEnd)) { + break + } + // read offset (32 bits = 4 bytes reverse endianness) + chunk := calldataload(inputPtr) + let offset := or(byte(0, chunk), shl(8, byte(1, chunk))) + inputPtr := add(inputPtr, 2) + // parse length of the copy section + let copyLength := and(token, 0xf) + if eq(copyLength, 0xf) { + for {} 1 {} { + let count := byte(0, calldataload(inputPtr)) + inputPtr := add(inputPtr, 1) + copyLength := add(copyLength, count) + if lt(count, 0xff) { + break + } + } + } + copyLength := add(copyLength, MIN_MATCH) + // do the copy + for { + let ptr := outputPtr + let end := add(outputPtr, copyLength) + let step := xor(offset, mul(lt(copyLength, offset), xor(copyLength, offset))) // min(copyLength, offset) + } lt(ptr, end) { + ptr := add(ptr, step) + } { + mcopy(ptr, sub(ptr, offset), step) + } + outputPtr := add(outputPtr, copyLength) + } + assert(eq(inputPtr, blockEnd), decodingFailureCode) + } + } + assert(eq(inputPtr, inputEnd), decodingFailureCode) + // allocate used memory + mstore(output, sub(outputPtr, add(output, 0x20))) + mstore(0x40, outputPtr) + } + } +} diff --git a/contracts/utils/compression/Snappy.sol b/contracts/utils/compression/Snappy.sol new file mode 100644 index 00000000000..10aa644f50d --- /dev/null +++ b/contracts/utils/compression/Snappy.sol @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +/** + * @dev Library for decompressing data using Snappy. + * + * See https://github.com/google/snappy + */ +library Snappy { + error DecodingFailure(); + + /** + * @dev Implementation of Snappy's uncompress function. + * + * Based on https://github.com/zhipeng-jia/snappyjs/blob/v0.7.0/snappy_decompressor.js[snappyjs javascript implementation]. + */ + function uncompress(bytes memory input) internal pure returns (bytes memory output) { + assembly ("memory-safe") { + // input buffer bounds + let inputBegin := add(input, 0x20) + let inputEnd := add(inputBegin, mload(input)) + // input traversal pointer + let inputPtr := inputBegin + // read length of the decompressed buffer + let outputLength := 0 + for {} lt(inputPtr, inputEnd) { + inputPtr := add(inputPtr, 1) + } { + let c := byte(0, mload(inputPtr)) + outputLength := add(outputLength, shl(mul(7, sub(inputPtr, inputBegin)), and(c, 0x7f))) + if iszero(and(c, 0x80)) { + break + } + } + inputPtr := add(inputPtr, 1) + // allocated output buffer + output := mload(0x40) + let outputPtr := add(output, 0x20) + mstore(output, outputLength) + mstore(0x40, add(outputPtr, outputLength)) + // decompress input buffer into output buffer + for { + let len, offset + } lt(inputPtr, inputEnd) {} { + // get next (compressed) word -- used as a cache for further reads + let w := mload(inputPtr) + inputPtr := add(inputPtr, 1) + let c := byte(0, w) + // consider different cases based on the lower 2 bits of c + // - 0: literal + // - 1,2,3: offset copy + switch and(c, 0x3) + case 0 { + len := add(shr(2, c), 1) + if gt(len, 60) { + let smallLen := sub(len, 60) + len := or(or(byte(1, w), shl(8, byte(2, w))), or(shl(16, byte(3, w)), shl(24, byte(4, w)))) + len := add(and(len, shr(sub(256, mul(8, smallLen)), not(0))), 1) + inputPtr := add(inputPtr, smallLen) + } + mcopy(outputPtr, inputPtr, len) + inputPtr := add(inputPtr, len) + outputPtr := add(outputPtr, len) + // continue to skip the offset copy logic that is shared by the other 3 cases + continue + } + case 1 { + len := add(and(shr(2, c), 0x7), 4) + offset := add(byte(1, w), shl(8, shr(5, c))) + inputPtr := add(inputPtr, 1) + } + case 2 { + len := add(shr(2, c), 1) + offset := add(byte(1, w), shl(8, byte(2, w))) + inputPtr := add(inputPtr, 2) + } + case 3 { + len := add(shr(2, c), 1) + offset := add(add(byte(1, w), shl(8, byte(2, w))), add(shl(16, byte(3, w)), shl(24, byte(4, w)))) + inputPtr := add(inputPtr, 4) + } + // copying in will not work if the offset is larger than the len being copied, so we compute + // `step = Math.min(len, offset)` and use it for the memory copy in chunks + for { + let ptr := outputPtr + let end := add(outputPtr, len) + let step := xor(offset, mul(lt(len, offset), xor(len, offset))) // min(len, offset) + } lt(ptr, end) { + ptr := add(ptr, step) + } { + mcopy(ptr, sub(ptr, offset), step) + } + outputPtr := add(outputPtr, len) + } + // sanity check, we did not read more than the input and FMP is at the right location + if iszero(and(eq(inputPtr, inputEnd), eq(outputPtr, mload(0x40)))) { + revert(0, 0) + } + } + } + + /// @dev Variant of {uncompress} that takes a buffer from calldata. + function uncompressCalldata(bytes calldata input) internal pure returns (bytes memory output) { + assembly ("memory-safe") { + // input buffer bounds + let inputBegin := input.offset + let inputEnd := add(inputBegin, input.length) + // input traversal pointer + let inputPtr := inputBegin + // read length of the decompressed buffer + let outputLength := 0 + for {} lt(inputPtr, inputEnd) { + inputPtr := add(inputPtr, 1) + } { + let c := byte(0, calldataload(inputPtr)) + outputLength := add(outputLength, shl(mul(7, sub(inputPtr, inputBegin)), and(c, 0x7f))) + if iszero(and(c, 0x80)) { + break + } + } + inputPtr := add(inputPtr, 1) + // allocated output buffer + output := mload(0x40) + let outputPtr := add(output, 0x20) + mstore(output, outputLength) + mstore(0x40, add(outputPtr, outputLength)) + // decompress input buffer into output buffer + for { + let len, offset + } lt(inputPtr, inputEnd) {} { + // get next (compressed) word -- used as a cache for further reads + let w := calldataload(inputPtr) + inputPtr := add(inputPtr, 1) + let c := byte(0, w) + // consider different cases based on the lower 2 bits of c + // - 0: literal + // - 1, 2, 3: offset copy + switch and(c, 0x3) + case 0 { + len := add(shr(2, c), 1) + if gt(len, 60) { + let smallLen := sub(len, 60) + len := or(or(byte(1, w), shl(8, byte(2, w))), or(shl(16, byte(3, w)), shl(24, byte(4, w)))) + len := add(and(len, shr(sub(256, mul(8, smallLen)), not(0))), 1) + inputPtr := add(inputPtr, smallLen) + } + // copy len bytes from input to output in chunks of 32 bytes + calldatacopy(outputPtr, inputPtr, len) + inputPtr := add(inputPtr, len) + outputPtr := add(outputPtr, len) + // continue to skip the offset copy logic that is shared by the other 3 cases + continue + } + case 1 { + len := add(and(shr(2, c), 0x7), 4) + offset := add(byte(1, w), shl(8, shr(5, c))) + inputPtr := add(inputPtr, 1) + } + case 2 { + len := add(shr(2, c), 1) + offset := add(byte(1, w), shl(8, byte(2, w))) + inputPtr := add(inputPtr, 2) + } + case 3 { + len := add(shr(2, c), 1) + len := add(shr(2, c), 1) + offset := add(add(byte(1, w), shl(8, byte(2, w))), add(shl(16, byte(3, w)), shl(24, byte(4, w)))) + inputPtr := add(inputPtr, 4) + } + // copying in will not work if the offset is larger than the len being copied, so we compute + // `step = Math.min(len, offset)` and use it for the memory copy in chunks + for { + let ptr := outputPtr + let end := add(outputPtr, len) + let step := xor(offset, mul(lt(len, offset), xor(len, offset))) // min(len, offset) + } lt(ptr, end) { + ptr := add(ptr, step) + } { + mcopy(ptr, sub(ptr, offset), step) + } + outputPtr := add(outputPtr, len) + } + // sanity check, we did not read more than the input and FMP is at the right location + if iszero(and(eq(inputPtr, inputEnd), eq(outputPtr, mload(0x40)))) { + revert(0, 0) + } + } + } +} diff --git a/package-lock.json b/package-lock.json index 072b6072d59..d1769492028 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,12 +36,15 @@ "interoperable-addresses": "^0.1.3", "lint-staged": "^16.0.0", "lodash.startcase": "^4.4.0", + "lz4js": "^0.2.0", "micromatch": "^4.0.2", "p-limit": "^6.0.0", "prettier": "^3.0.0", "prettier-plugin-solidity": "^2.0.0", "rimraf": "^6.0.0", "semver": "^7.3.5", + "snappy": "^7.3.0", + "solady": "^0.1.24", "solhint": "^6.0.0", "solhint-plugin-openzeppelin": "file:scripts/solhint-custom", "solidity-ast": "^0.4.50", @@ -425,6 +428,40 @@ "node": ">=0.1.90" } }, + "node_modules/@emnapi/core": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz", + "integrity": "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.0.4", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz", + "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz", + "integrity": "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", @@ -1639,6 +1676,325 @@ "node": ">=16.0.0" } }, + "node_modules/@napi-rs/snappy-android-arm-eabi": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-android-arm-eabi/-/snappy-android-arm-eabi-7.3.0.tgz", + "integrity": "sha512-KgD+8wNtS4/w7JqpiyONTwPAF74mhLnH7up+Ke4l5+jS9WFC1UexFsp+g62ugCIAmsdWfWKIpNTPpxkV7yrdYw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-android-arm64": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-android-arm64/-/snappy-android-arm64-7.3.0.tgz", + "integrity": "sha512-HUX9icvbWPgBDNwdxrFt1+lDfOmVsxAOMnJ+jzSYFDp2EclQjxAhPUV/rSRtS23xakHZgi6aBiJ+aY1p9Z/Fkg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-darwin-arm64": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-darwin-arm64/-/snappy-darwin-arm64-7.3.0.tgz", + "integrity": "sha512-shU1IOgMJRBLxqNvuqRvIr3lP8i2q/UJiwHfkipN05wzjlLN327ej0E98TarsfDgn8SDX92ovsc6zVfb4bhHbg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-darwin-x64": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-darwin-x64/-/snappy-darwin-x64-7.3.0.tgz", + "integrity": "sha512-j/gaU69biRWyYq46DocHYBLjJezQhLNEjaWW3J7Y/dbil3P/+iCkvJkA0uHnIG2KXJk/QSyu2kzuy6YnMSk9Qw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-freebsd-x64": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-freebsd-x64/-/snappy-freebsd-x64-7.3.0.tgz", + "integrity": "sha512-JnZDZDoWrUO2E8hmiU6oyrD34xazB8CVoZYCsHmkF5Yrl/5jvr/0BG3jd4SQ10wAdhQDHVwS8iaEB6GXdQxZGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-linux-arm-gnueabihf": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-arm-gnueabihf/-/snappy-linux-arm-gnueabihf-7.3.0.tgz", + "integrity": "sha512-qjEItUn91kuR6oLhXrLXSiUSB1LEz9L4lHCidmGmcIvvdM9FrceS0ny8seYuiU5fa6pZTwTO/6O3nQFQlgI+tg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-linux-arm64-gnu": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-arm64-gnu/-/snappy-linux-arm64-gnu-7.3.0.tgz", + "integrity": "sha512-+v3IjUgphndbzRQ/PXoEDyORW3X4xfMPBAoQfziW1g+f+7nms+PCobWoAbGMneIGf6QP4KuLsv3clVa9+noW9g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-linux-arm64-musl": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-arm64-musl/-/snappy-linux-arm64-musl-7.3.0.tgz", + "integrity": "sha512-8XFJIYXYrmesQ4m8xHX7jfon7HFdxY1n3eykfQaAWrKLpLMQL2x+caS6So7wpRFJI/2NyYWiY7FWYTAuMDbFVw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-linux-arm64-ohos": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-arm64-ohos/-/snappy-linux-arm64-ohos-7.3.0.tgz", + "integrity": "sha512-PlKHoHxeiNOQakbdMoQ0nF6GOKC52wHNCJ0Ii/JJHF8YfPIOv/BRZBEY32b8IidGj9RMq8vCa/ftBvNjIYdF5Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-linux-ppc64-gnu": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-ppc64-gnu/-/snappy-linux-ppc64-gnu-7.3.0.tgz", + "integrity": "sha512-OJTssYn5nZC6UX9O85z4/Mtwr4ya09P0ebatb7IVNRN8GKcnkZbDOZX4HosSNNAllVQgn3n18gP45RSbtXzcGQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-linux-riscv64-gnu": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-riscv64-gnu/-/snappy-linux-riscv64-gnu-7.3.0.tgz", + "integrity": "sha512-3O0QwzFtUm9sxDVBPjYgvAeHDw+omNJdtvamS60Yn87UZMk8V50vgDDr5NKcksmyfgdftdZfR/bFqDYCucTQuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-linux-s390x-gnu": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-s390x-gnu/-/snappy-linux-s390x-gnu-7.3.0.tgz", + "integrity": "sha512-hGgPn0M1tX4gUsJ/FieLsmKQgiI5p/TOlxh9WDqtIp52k9m965V9lRTEQrlUL//CndlEz3m6bk0PVgd4klNF8w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-linux-x64-gnu": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-x64-gnu/-/snappy-linux-x64-gnu-7.3.0.tgz", + "integrity": "sha512-pBDHRUOJqC/6ZG7zy068pTItGeF4gJwO9zcZVgt951WXCvn7cIEjHhZSRim2ud8IoeH0Oumew1QYxZIxOFACJg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-linux-x64-musl": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-x64-musl/-/snappy-linux-x64-musl-7.3.0.tgz", + "integrity": "sha512-S+lWMBIvNYbTqGx1V2ndmcXyzIqsBTCBiU0cgtWxDNMTea5LCC749k+xxcCms/D0vwEe98qYkqAbs8IIEh1hKA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-wasm32-wasi": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-wasm32-wasi/-/snappy-wasm32-wasi-7.3.0.tgz", + "integrity": "sha512-8K6OVGRzJ21BVXCYmbOa0wZrpalOWT8k/v8PbFHWcLLlghPEMJYGuGMlkzTd+VgKYjkjX1qBLaDA51+UMRWPug==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.12" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@napi-rs/snappy-win32-arm64-msvc": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-win32-arm64-msvc/-/snappy-win32-arm64-msvc-7.3.0.tgz", + "integrity": "sha512-TzBl/dmHt+8JGZrv0eQ1j7dL6XuSor7K/1EZ2hwX1vxe/rdygZfIlMJlYbUuEqiK2ZJD1AVuc5WhqyQADu0t+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-win32-ia32-msvc": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-win32-ia32-msvc/-/snappy-win32-ia32-msvc-7.3.0.tgz", + "integrity": "sha512-UVIIzEcxSo4TKNs8PGrfAjCHKlbxCljnQFjYPEwv8O9YaNpQoSy6pZU4SdO5aB4SRbqhSoqtkJlaKFBiHLa8Zw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-win32-x64-msvc": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-win32-x64-msvc/-/snappy-win32-x64-msvc-7.3.0.tgz", + "integrity": "sha512-4vI5r2QY0NRIe0l/Vsgs+cqZDIFqtsqwSJVVBuCfcf1G39AXQ7g1bstdxnE4s6MHh/Xis7h2vsajYNnQh6xX4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, "node_modules/@noble/ciphers": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", @@ -2422,6 +2778,17 @@ "node": ">=14.16" } }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", + "integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/bn.js": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.6.tgz", @@ -7283,6 +7650,13 @@ "node": "20 || >=22" } }, + "node_modules/lz4js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/lz4js/-/lz4js-0.2.0.tgz", + "integrity": "sha512-gY2Ia9Lm7Ep8qMiuGRhvUq0Q7qUereeldZPP1PMEJxPtEWHJLqw9pgX68oHajBH0nzJK4MaZEA/YNV3jT8u8Bg==", + "dev": true, + "license": "ISC" + }, "node_modules/markdown-table": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz", @@ -9339,6 +9713,47 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/snappy": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/snappy/-/snappy-7.3.0.tgz", + "integrity": "sha512-Qd1XxFO71HOOA6RxWkO5yHcYrQyBZOqGFKv99DD75bS34I3J6HEudlO4Lo617B6A6fJJ87YS5oYY9NZXxlXGkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/snappy-android-arm-eabi": "7.3.0", + "@napi-rs/snappy-android-arm64": "7.3.0", + "@napi-rs/snappy-darwin-arm64": "7.3.0", + "@napi-rs/snappy-darwin-x64": "7.3.0", + "@napi-rs/snappy-freebsd-x64": "7.3.0", + "@napi-rs/snappy-linux-arm-gnueabihf": "7.3.0", + "@napi-rs/snappy-linux-arm64-gnu": "7.3.0", + "@napi-rs/snappy-linux-arm64-musl": "7.3.0", + "@napi-rs/snappy-linux-arm64-ohos": "7.3.0", + "@napi-rs/snappy-linux-ppc64-gnu": "7.3.0", + "@napi-rs/snappy-linux-riscv64-gnu": "7.3.0", + "@napi-rs/snappy-linux-s390x-gnu": "7.3.0", + "@napi-rs/snappy-linux-x64-gnu": "7.3.0", + "@napi-rs/snappy-linux-x64-musl": "7.3.0", + "@napi-rs/snappy-wasm32-wasi": "7.3.0", + "@napi-rs/snappy-win32-arm64-msvc": "7.3.0", + "@napi-rs/snappy-win32-ia32-msvc": "7.3.0", + "@napi-rs/snappy-win32-x64-msvc": "7.3.0" + } + }, + "node_modules/solady": { + "version": "0.1.24", + "resolved": "https://registry.npmjs.org/solady/-/solady-0.1.24.tgz", + "integrity": "sha512-uFTtYane4KMn2Tbth+7f8svTOrQ5+SUksFyTA9Vqwffwxak7OduHZxBYxSz34foBGnbsKtleM2FbgiAP1NYB1A==", + "dev": true, + "license": "MIT" + }, "node_modules/solc": { "version": "0.8.26", "resolved": "https://registry.npmjs.org/solc/-/solc-0.8.26.tgz", diff --git a/package.json b/package.json index 65def77e53e..100fde02b63 100644 --- a/package.json +++ b/package.json @@ -79,12 +79,15 @@ "interoperable-addresses": "^0.1.3", "lint-staged": "^16.0.0", "lodash.startcase": "^4.4.0", + "lz4js": "^0.2.0", "micromatch": "^4.0.2", "p-limit": "^6.0.0", "prettier": "^3.0.0", "prettier-plugin-solidity": "^2.0.0", "rimraf": "^6.0.0", "semver": "^7.3.5", + "snappy": "^7.3.0", + "solady": "^0.1.24", "solhint": "^6.0.0", "solhint-plugin-openzeppelin": "file:scripts/solhint-custom", "solidity-ast": "^0.4.50", diff --git a/test/utils/compression/FastLZ.t.sol b/test/utils/compression/FastLZ.t.sol new file mode 100644 index 00000000000..1df946003c3 --- /dev/null +++ b/test/utils/compression/FastLZ.t.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {FastLZ} from "@openzeppelin/contracts/utils/compression/FastLZ.sol"; +import {LibZip} from "solady/src/utils/LibZip.sol"; + +contract FastLZTest is Test { + function testEncodeDecode(bytes memory input) external pure { + assertEq(FastLZ.decompress(LibZip.flzCompress(input)), input); + } + + function testEncodeDecodeCalldata(bytes memory input) external view { + assertEq(this.decompressCalldata(LibZip.flzCompress(input)), input); + } + + function decompressCalldata(bytes calldata input) external pure returns (bytes memory) { + return FastLZ.decompress(input); + } +} diff --git a/test/utils/compression/FastLZ.test.js b/test/utils/compression/FastLZ.test.js new file mode 100644 index 00000000000..3859941c1f5 --- /dev/null +++ b/test/utils/compression/FastLZ.test.js @@ -0,0 +1,64 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +const { LibZip } = require('solady/js/solady'); + +async function fixture() { + const mock = await ethers.deployContract('$FastLZ'); + return { mock }; +} + +// From https://github.com/google/snappy/blob/main/snappy_unittest.cc +const unittests = [ + '', + 'a', + 'ab', + 'abc', + 'aaaaaaa' + 'b'.repeat(16) + 'aaaaa' + 'abc', + 'aaaaaaa' + 'b'.repeat(256) + 'aaaaa' + 'abc', + 'aaaaaaa' + 'b'.repeat(2047) + 'aaaaa' + 'abc', + 'aaaaaaa' + 'b'.repeat(65536) + 'aaaaa' + 'abc', + 'abcaaaaaaa' + 'b'.repeat(65536) + 'aaaaa' + 'abc', + 'abcabcabcabcabcabcab', + 'abcabcabcabcabcabcab0123456789ABCDEF', + 'abcabcabcabcabcabcabcabcabcabcabcabc', + 'abcabcabcabcabcabcabcabcabcabcabcabc0123456789ABCDEF', +]; + +describe('FastLZ', function () { + before(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + describe('decompress', function () { + for (const [i, str] of Object.entries(unittests)) { + it(`Google's unit tests #${i}: length ${str.length}`, async function () { + this.input = str; + }); + } + + it('Lorem ipsum...', async function () { + this.input = + '\ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas ligula urna, bibendum sagittis eleifend non, rutrum sit amet lectus. Donec eu pellentesque dolor, varius lobortis erat. In viverra diam in nunc porta, at pretium orci hendrerit. Duis suscipit lacus eu sodales imperdiet. Donec rhoncus tincidunt sem sed laoreet. Suspendisse potenti. Suspendisse a dictum diam, a porttitor augue. Praesent sodales quis nisi sed auctor. Nullam efficitur est eros, a tincidunt velit faucibus consequat. Praesent urna leo, imperdiet ut mi eu, pellentesque mattis ante. Suspendisse cursus lacus ac urna egestas, vitae ultricies ante porttitor. In sed risus vitae nunc faucibus tristique.\ +Quisque aliquet bibendum augue, et tristique lorem pellentesque quis. Nulla rhoncus erat sed velit luctus, in cursus neque suscipit. Quisque sit amet mauris nec enim congue sagittis eu nec diam. Quisque a enim a leo aliquam vestibulum a ut risus. In hendrerit cursus nisl, et porttitor dolor volutpat non. Donec rhoncus, nisl ut blandit porta, libero felis vulputate ante, et pharetra ex risus et enim. Vestibulum eu ultricies ipsum, quis auctor odio. Morbi ornare metus nec purus elementum, eu interdum magna dapibus. Aliquam odio ipsum, semper in nisl tristique, fermentum porta risus. Curabitur facilisis felis a molestie dignissim. Pellentesque aliquet sagittis sodales. Fusce at dignissim mi. Nulla a tempus quam.\ +Nam et egestas quam. Aliquam bibendum iaculis mauris a sagittis. Suspendisse tincidunt, magna vitae scelerisque pharetra, orci nisi venenatis est, sit amet consequat ligula dolor eu felis. Nulla suscipit eleifend augue, et commodo elit lobortis eget. Integer pharetra commodo metus, at accumsan arcu porttitor sed. Ut eu nulla sit amet diam imperdiet fermentum id in erat. Curabitur at neque ornare neque dictum malesuada a nec enim. Ut ac aliquam mauris, eu pretium urna. Donec vitae leo eros. Phasellus et purus rhoncus, accumsan ligula vel, sagittis lectus. Mauris sed lectus elementum, porta nisl eget, convallis ligula. Aenean pellentesque arcu ac lacus scelerisque sollicitudin. Nunc vitae enim egestas, sollicitudin ipsum vulputate, fringilla urna. Aenean eget libero sollicitudin, sagittis lorem in, convallis nibh.\ +Cras cursus luctus malesuada. Sed dictum, sem feugiat placerat placerat, nisl neque blandit enim, quis semper mauris augue quis lacus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Pellentesque dignissim quis est et auctor. Etiam porttitor facilisis nibh eget luctus. Etiam at congue neque. Donec a odio varius, rhoncus metus ac, bibendum est. Nullam nisl tortor, egestas id quam sed, hendrerit lobortis diam. Phasellus eros sapien, hendrerit nec ex nec, convallis ullamcorper nibh. Integer tempor hendrerit auctor. Duis ut orci iaculis, tincidunt dui eget, faucibus magna. Pellentesque sit amet eros ac nibh pulvinar volutpat. In ligula felis, hendrerit non congue finibus, tincidunt a nibh. Morbi suscipit dui orci, eget volutpat odio malesuada in.\ +Nullam eget pharetra mauris. Cras nec ultricies mi. Suspendisse sit amet ligula lectus. Vestibulum commodo massa nec turpis viverra, nec tempor velit convallis. Etiam egestas quam ut justo rhoncus porta. Morbi viverra mi dui, mattis feugiat neque pulvinar laoreet. Curabitur pulvinar mi vitae nisi sodales tristique. Nunc vulputate maximus ante ac venenatis.\ +'; + }); + + it('Random buffer', async function () { + this.input = ethers.randomBytes(4096); + }); + + afterEach(async function () { + const raw = ethers.isBytesLike(this.input) ? this.input : ethers.toUtf8Bytes(this.input); + const hex = ethers.hexlify(raw); + const compressed = LibZip.flzCompress(hex); + await expect(this.mock.$decompress(compressed)).to.eventually.equal(hex); + await expect(this.mock.$decompressCalldata(compressed)).to.eventually.equal(hex); + }); + }); +}); diff --git a/test/utils/compression/LZ4.test.js b/test/utils/compression/LZ4.test.js new file mode 100644 index 00000000000..370cbd8d371 --- /dev/null +++ b/test/utils/compression/LZ4.test.js @@ -0,0 +1,64 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +const lz4js = require('lz4js'); + +async function fixture() { + const mock = await ethers.deployContract('$LZ4'); + return { mock }; +} + +// From https://github.com/google/snappy/blob/main/snappy_unittest.cc +const unittests = [ + '', + 'a', + 'ab', + 'abc', + 'aaaaaaa' + 'b'.repeat(16) + 'aaaaa' + 'abc', + 'aaaaaaa' + 'b'.repeat(256) + 'aaaaa' + 'abc', + 'aaaaaaa' + 'b'.repeat(2047) + 'aaaaa' + 'abc', + 'aaaaaaa' + 'b'.repeat(65536) + 'aaaaa' + 'abc', + 'abcaaaaaaa' + 'b'.repeat(65536) + 'aaaaa' + 'abc', + 'abcabcabcabcabcabcab', + 'abcabcabcabcabcabcab0123456789ABCDEF', + 'abcabcabcabcabcabcabcabcabcabcabcabc', + 'abcabcabcabcabcabcabcabcabcabcabcabc0123456789ABCDEF', +]; + +describe('LZ4', function () { + before(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + describe('decompress', function () { + for (const [i, str] of Object.entries(unittests)) { + it(`Google's unit tests #${i}: length ${str.length}`, async function () { + this.input = str; + }); + } + + it('Lorem ipsum...', async function () { + this.input = + '\ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas ligula urna, bibendum sagittis eleifend non, rutrum sit amet lectus. Donec eu pellentesque dolor, varius lobortis erat. In viverra diam in nunc porta, at pretium orci hendrerit. Duis suscipit lacus eu sodales imperdiet. Donec rhoncus tincidunt sem sed laoreet. Suspendisse potenti. Suspendisse a dictum diam, a porttitor augue. Praesent sodales quis nisi sed auctor. Nullam efficitur est eros, a tincidunt velit faucibus consequat. Praesent urna leo, imperdiet ut mi eu, pellentesque mattis ante. Suspendisse cursus lacus ac urna egestas, vitae ultricies ante porttitor. In sed risus vitae nunc faucibus tristique.\ +Quisque aliquet bibendum augue, et tristique lorem pellentesque quis. Nulla rhoncus erat sed velit luctus, in cursus neque suscipit. Quisque sit amet mauris nec enim congue sagittis eu nec diam. Quisque a enim a leo aliquam vestibulum a ut risus. In hendrerit cursus nisl, et porttitor dolor volutpat non. Donec rhoncus, nisl ut blandit porta, libero felis vulputate ante, et pharetra ex risus et enim. Vestibulum eu ultricies ipsum, quis auctor odio. Morbi ornare metus nec purus elementum, eu interdum magna dapibus. Aliquam odio ipsum, semper in nisl tristique, fermentum porta risus. Curabitur facilisis felis a molestie dignissim. Pellentesque aliquet sagittis sodales. Fusce at dignissim mi. Nulla a tempus quam.\ +Nam et egestas quam. Aliquam bibendum iaculis mauris a sagittis. Suspendisse tincidunt, magna vitae scelerisque pharetra, orci nisi venenatis est, sit amet consequat ligula dolor eu felis. Nulla suscipit eleifend augue, et commodo elit lobortis eget. Integer pharetra commodo metus, at accumsan arcu porttitor sed. Ut eu nulla sit amet diam imperdiet fermentum id in erat. Curabitur at neque ornare neque dictum malesuada a nec enim. Ut ac aliquam mauris, eu pretium urna. Donec vitae leo eros. Phasellus et purus rhoncus, accumsan ligula vel, sagittis lectus. Mauris sed lectus elementum, porta nisl eget, convallis ligula. Aenean pellentesque arcu ac lacus scelerisque sollicitudin. Nunc vitae enim egestas, sollicitudin ipsum vulputate, fringilla urna. Aenean eget libero sollicitudin, sagittis lorem in, convallis nibh.\ +Cras cursus luctus malesuada. Sed dictum, sem feugiat placerat placerat, nisl neque blandit enim, quis semper mauris augue quis lacus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Pellentesque dignissim quis est et auctor. Etiam porttitor facilisis nibh eget luctus. Etiam at congue neque. Donec a odio varius, rhoncus metus ac, bibendum est. Nullam nisl tortor, egestas id quam sed, hendrerit lobortis diam. Phasellus eros sapien, hendrerit nec ex nec, convallis ullamcorper nibh. Integer tempor hendrerit auctor. Duis ut orci iaculis, tincidunt dui eget, faucibus magna. Pellentesque sit amet eros ac nibh pulvinar volutpat. In ligula felis, hendrerit non congue finibus, tincidunt a nibh. Morbi suscipit dui orci, eget volutpat odio malesuada in.\ +Nullam eget pharetra mauris. Cras nec ultricies mi. Suspendisse sit amet ligula lectus. Vestibulum commodo massa nec turpis viverra, nec tempor velit convallis. Etiam egestas quam ut justo rhoncus porta. Morbi viverra mi dui, mattis feugiat neque pulvinar laoreet. Curabitur pulvinar mi vitae nisi sodales tristique. Nunc vulputate maximus ante ac venenatis.\ +'; + }); + + it('Random buffer', async function () { + this.input = ethers.randomBytes(4096); + }); + + afterEach(async function () { + const raw = ethers.isBytesLike(this.input) ? this.input : ethers.toUtf8Bytes(this.input); + const hex = ethers.hexlify(raw); + const compressed = lz4js.compress(raw); + await expect(this.mock.$decompress(compressed)).to.eventually.equal(hex); + await expect(this.mock.$decompressCalldata(compressed)).to.eventually.equal(hex); + }); + }); +}); diff --git a/test/utils/compression/Snappy.test.js b/test/utils/compression/Snappy.test.js new file mode 100644 index 00000000000..fea3c1d51ff --- /dev/null +++ b/test/utils/compression/Snappy.test.js @@ -0,0 +1,64 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +const snappy = require('snappy'); + +async function fixture() { + const mock = await ethers.deployContract('$Snappy'); + return { mock }; +} + +// From https://github.com/google/snappy/blob/main/snappy_unittest.cc +const unittests = [ + '', + 'a', + 'ab', + 'abc', + 'aaaaaaa' + 'b'.repeat(16) + 'aaaaa' + 'abc', + 'aaaaaaa' + 'b'.repeat(256) + 'aaaaa' + 'abc', + 'aaaaaaa' + 'b'.repeat(2047) + 'aaaaa' + 'abc', + 'aaaaaaa' + 'b'.repeat(65536) + 'aaaaa' + 'abc', + 'abcaaaaaaa' + 'b'.repeat(65536) + 'aaaaa' + 'abc', + 'abcabcabcabcabcabcab', + 'abcabcabcabcabcabcab0123456789ABCDEF', + 'abcabcabcabcabcabcabcabcabcabcabcabc', + 'abcabcabcabcabcabcabcabcabcabcabcabc0123456789ABCDEF', +]; + +describe('Snappy', function () { + before(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + describe('uncompress', function () { + for (const [i, str] of Object.entries(unittests)) { + it(`Google's unit tests #${i}: length ${str.length}`, async function () { + this.input = str; + }); + } + + it('Lorem ipsum...', async function () { + this.input = + '\ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas ligula urna, bibendum sagittis eleifend non, rutrum sit amet lectus. Donec eu pellentesque dolor, varius lobortis erat. In viverra diam in nunc porta, at pretium orci hendrerit. Duis suscipit lacus eu sodales imperdiet. Donec rhoncus tincidunt sem sed laoreet. Suspendisse potenti. Suspendisse a dictum diam, a porttitor augue. Praesent sodales quis nisi sed auctor. Nullam efficitur est eros, a tincidunt velit faucibus consequat. Praesent urna leo, imperdiet ut mi eu, pellentesque mattis ante. Suspendisse cursus lacus ac urna egestas, vitae ultricies ante porttitor. In sed risus vitae nunc faucibus tristique.\ +Quisque aliquet bibendum augue, et tristique lorem pellentesque quis. Nulla rhoncus erat sed velit luctus, in cursus neque suscipit. Quisque sit amet mauris nec enim congue sagittis eu nec diam. Quisque a enim a leo aliquam vestibulum a ut risus. In hendrerit cursus nisl, et porttitor dolor volutpat non. Donec rhoncus, nisl ut blandit porta, libero felis vulputate ante, et pharetra ex risus et enim. Vestibulum eu ultricies ipsum, quis auctor odio. Morbi ornare metus nec purus elementum, eu interdum magna dapibus. Aliquam odio ipsum, semper in nisl tristique, fermentum porta risus. Curabitur facilisis felis a molestie dignissim. Pellentesque aliquet sagittis sodales. Fusce at dignissim mi. Nulla a tempus quam.\ +Nam et egestas quam. Aliquam bibendum iaculis mauris a sagittis. Suspendisse tincidunt, magna vitae scelerisque pharetra, orci nisi venenatis est, sit amet consequat ligula dolor eu felis. Nulla suscipit eleifend augue, et commodo elit lobortis eget. Integer pharetra commodo metus, at accumsan arcu porttitor sed. Ut eu nulla sit amet diam imperdiet fermentum id in erat. Curabitur at neque ornare neque dictum malesuada a nec enim. Ut ac aliquam mauris, eu pretium urna. Donec vitae leo eros. Phasellus et purus rhoncus, accumsan ligula vel, sagittis lectus. Mauris sed lectus elementum, porta nisl eget, convallis ligula. Aenean pellentesque arcu ac lacus scelerisque sollicitudin. Nunc vitae enim egestas, sollicitudin ipsum vulputate, fringilla urna. Aenean eget libero sollicitudin, sagittis lorem in, convallis nibh.\ +Cras cursus luctus malesuada. Sed dictum, sem feugiat placerat placerat, nisl neque blandit enim, quis semper mauris augue quis lacus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Pellentesque dignissim quis est et auctor. Etiam porttitor facilisis nibh eget luctus. Etiam at congue neque. Donec a odio varius, rhoncus metus ac, bibendum est. Nullam nisl tortor, egestas id quam sed, hendrerit lobortis diam. Phasellus eros sapien, hendrerit nec ex nec, convallis ullamcorper nibh. Integer tempor hendrerit auctor. Duis ut orci iaculis, tincidunt dui eget, faucibus magna. Pellentesque sit amet eros ac nibh pulvinar volutpat. In ligula felis, hendrerit non congue finibus, tincidunt a nibh. Morbi suscipit dui orci, eget volutpat odio malesuada in.\ +Nullam eget pharetra mauris. Cras nec ultricies mi. Suspendisse sit amet ligula lectus. Vestibulum commodo massa nec turpis viverra, nec tempor velit convallis. Etiam egestas quam ut justo rhoncus porta. Morbi viverra mi dui, mattis feugiat neque pulvinar laoreet. Curabitur pulvinar mi vitae nisi sodales tristique. Nunc vulputate maximus ante ac venenatis.\ +'; + }); + + it('Random buffer', async function () { + this.input = ethers.randomBytes(4096); + }); + + afterEach(async function () { + const raw = ethers.isBytesLike(this.input) ? this.input : ethers.toUtf8Bytes(this.input); + const hex = ethers.hexlify(raw); + const compressed = snappy.compressSync(raw); + await expect(this.mock.$uncompress(compressed)).to.eventually.equal(hex); + await expect(this.mock.$uncompressCalldata(compressed)).to.eventually.equal(hex); + }); + }); +}); diff --git a/test/utils/compression/gas.test.js b/test/utils/compression/gas.test.js new file mode 100644 index 00000000000..697a3734a77 --- /dev/null +++ b/test/utils/compression/gas.test.js @@ -0,0 +1,86 @@ +const { ethers } = require('hardhat'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +const lz4js = require('lz4js'); +const snappy = require('snappy'); +const { LibZip } = require('solady/js/solady'); +const { min } = require('../../helpers/math'); + +async function fixture() { + const fastlz = await ethers.deployContract('$FastLZ'); + const lz4 = await ethers.deployContract('$LZ4'); + const snappy = await ethers.deployContract('$Snappy'); + return { fastlz, lz4, snappy }; +} + +// From https://github.com/google/snappy/blob/main/snappy_unittest.cc +const unittests = [ + '', + 'a', + 'ab', + 'abc', + 'aaaaaaa' + 'b'.repeat(16) + 'aaaaa' + 'abc', + 'aaaaaaa' + 'b'.repeat(256) + 'aaaaa' + 'abc', + 'aaaaaaa' + 'b'.repeat(2047) + 'aaaaa' + 'abc', + 'aaaaaaa' + 'b'.repeat(65536) + 'aaaaa' + 'abc', + 'abcaaaaaaa' + 'b'.repeat(65536) + 'aaaaa' + 'abc', + 'abcabcabcabcabcabcab', + 'abcabcabcabcabcabcab0123456789ABCDEF', + 'abcabcabcabcabcabcabcabcabcabcabcabc', + 'abcabcabcabcabcabcabcabcabcabcabcabc0123456789ABCDEF', +]; + +describe('Gas comparaison of decompression algorithms', function () { + before(async function () { + Object.assign(this, await loadFixture(fixture)); + this.results = []; + }); + + describe('decompress', function () { + for (const [i, str] of Object.entries(unittests)) { + it(`Google's unit tests #${i}: length ${str.length}`, async function () { + this.input = str; + }); + } + + it('Lorem ipsum...', async function () { + this.input = + '\ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas ligula urna, bibendum sagittis eleifend non, rutrum sit amet lectus. Donec eu pellentesque dolor, varius lobortis erat. In viverra diam in nunc porta, at pretium orci hendrerit. Duis suscipit lacus eu sodales imperdiet. Donec rhoncus tincidunt sem sed laoreet. Suspendisse potenti. Suspendisse a dictum diam, a porttitor augue. Praesent sodales quis nisi sed auctor. Nullam efficitur est eros, a tincidunt velit faucibus consequat. Praesent urna leo, imperdiet ut mi eu, pellentesque mattis ante. Suspendisse cursus lacus ac urna egestas, vitae ultricies ante porttitor. In sed risus vitae nunc faucibus tristique.\ +Quisque aliquet bibendum augue, et tristique lorem pellentesque quis. Nulla rhoncus erat sed velit luctus, in cursus neque suscipit. Quisque sit amet mauris nec enim congue sagittis eu nec diam. Quisque a enim a leo aliquam vestibulum a ut risus. In hendrerit cursus nisl, et porttitor dolor volutpat non. Donec rhoncus, nisl ut blandit porta, libero felis vulputate ante, et pharetra ex risus et enim. Vestibulum eu ultricies ipsum, quis auctor odio. Morbi ornare metus nec purus elementum, eu interdum magna dapibus. Aliquam odio ipsum, semper in nisl tristique, fermentum porta risus. Curabitur facilisis felis a molestie dignissim. Pellentesque aliquet sagittis sodales. Fusce at dignissim mi. Nulla a tempus quam.\ +Nam et egestas quam. Aliquam bibendum iaculis mauris a sagittis. Suspendisse tincidunt, magna vitae scelerisque pharetra, orci nisi venenatis est, sit amet consequat ligula dolor eu felis. Nulla suscipit eleifend augue, et commodo elit lobortis eget. Integer pharetra commodo metus, at accumsan arcu porttitor sed. Ut eu nulla sit amet diam imperdiet fermentum id in erat. Curabitur at neque ornare neque dictum malesuada a nec enim. Ut ac aliquam mauris, eu pretium urna. Donec vitae leo eros. Phasellus et purus rhoncus, accumsan ligula vel, sagittis lectus. Mauris sed lectus elementum, porta nisl eget, convallis ligula. Aenean pellentesque arcu ac lacus scelerisque sollicitudin. Nunc vitae enim egestas, sollicitudin ipsum vulputate, fringilla urna. Aenean eget libero sollicitudin, sagittis lorem in, convallis nibh.\ +Cras cursus luctus malesuada. Sed dictum, sem feugiat placerat placerat, nisl neque blandit enim, quis semper mauris augue quis lacus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Pellentesque dignissim quis est et auctor. Etiam porttitor facilisis nibh eget luctus. Etiam at congue neque. Donec a odio varius, rhoncus metus ac, bibendum est. Nullam nisl tortor, egestas id quam sed, hendrerit lobortis diam. Phasellus eros sapien, hendrerit nec ex nec, convallis ullamcorper nibh. Integer tempor hendrerit auctor. Duis ut orci iaculis, tincidunt dui eget, faucibus magna. Pellentesque sit amet eros ac nibh pulvinar volutpat. In ligula felis, hendrerit non congue finibus, tincidunt a nibh. Morbi suscipit dui orci, eget volutpat odio malesuada in.\ +Nullam eget pharetra mauris. Cras nec ultricies mi. Suspendisse sit amet ligula lectus. Vestibulum commodo massa nec turpis viverra, nec tempor velit convallis. Etiam egestas quam ut justo rhoncus porta. Morbi viverra mi dui, mattis feugiat neque pulvinar laoreet. Curabitur pulvinar mi vitae nisi sodales tristique. Nunc vulputate maximus ante ac venenatis.\ +'; + }); + + it('Random buffer', async function () { + this.input = ethers.randomBytes(4096); + }); + + afterEach(async function () { + const raw = ethers.isBytesLike(this.input) ? this.input : ethers.toUtf8Bytes(this.input); + const hex = ethers.hexlify(raw); + + const compressedFastlz = LibZip.flzCompress(hex); + const compressedSnappy = snappy.compressSync(raw); + const compressedLz4 = lz4js.compress(raw); + + const gasUsedFastlz = await this.fastlz.$decompress.estimateGas(compressedFastlz).then(Number); + const gasUsedSnappy = await this.snappy.$uncompress.estimateGas(compressedSnappy).then(Number); + const gasUsedLz4 = await this.lz4.$decompress.estimateGas(compressedLz4).then(Number); + const lowest = min(gasUsedFastlz, gasUsedSnappy, gasUsedLz4); + + this.results.push({ + lowest, + extraGasUsedPercentageFastlz: `+${((100 * (gasUsedFastlz - lowest)) / lowest).toFixed(2)}%`, + extraGasUsedPercentageSnappy: `+${((100 * (gasUsedSnappy - lowest)) / lowest).toFixed(2)}%`, + extraGasUsedPercentageLz4: `+${((100 * (gasUsedLz4 - lowest)) / lowest).toFixed(2)}%`, + }); + }); + + after(async function () { + console.table(this.results); + }); + }); +});