From f25da218d7b40878a61f6feb09f39c7fb06433f5 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Thu, 23 Jan 2025 18:43:10 +0530 Subject: [PATCH 1/4] feat: add LibPrefixLengthEncodedByteArray with tests --- .../lib/LibPrefixLengthEncodedByteArray.sol | 74 +++++++++++++ .../LibPrefixLengthEncodedByteArray.t.sol | 104 ++++++++++++++++++ 2 files changed, 178 insertions(+) create mode 100644 foundry/src/lib/LibPrefixLengthEncodedByteArray.sol create mode 100644 foundry/test/LibPrefixLengthEncodedByteArray.t.sol diff --git a/foundry/src/lib/LibPrefixLengthEncodedByteArray.sol b/foundry/src/lib/LibPrefixLengthEncodedByteArray.sol new file mode 100644 index 0000000..f8053b4 --- /dev/null +++ b/foundry/src/lib/LibPrefixLengthEncodedByteArray.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +error LibPrefixLengthEncodedByteArray__InvalidEncoding(); + +library LibPrefixLengthEncodedByteArray { + /** + * @dev Pop the first element of an array and returns it with the remaining data. + */ + function next(bytes calldata encoded) + internal + pure + returns (bytes calldata elem, bytes calldata res) + { + // Handle empty input + if (encoded.length == 0) { + return (encoded[:0], encoded[:0]); + } + + // Ensure we have at least 2 bytes for length prefix + if (encoded.length < 2) revert LibPrefixLengthEncodedByteArray__InvalidEncoding(); + // Extract the length prefix (first 2 bytes) + uint16 length = uint16(bytes2(encoded[:2])); + + // Check if length is valid + if (2 + length > encoded.length) revert LibPrefixLengthEncodedByteArray__InvalidEncoding(); + + // Extract the element (after length prefix) + elem = encoded[2:2+length]; + + // Extract the remaining data + res = encoded[2+length:]; + + return (elem, res); + } + + /** + * @dev Gets the size of the encoded array. + */ + function size(bytes calldata encoded) internal pure returns (uint256 s) { + uint256 offset = 0; + + while (offset < encoded.length) { + // Ensure we have at least 2 bytes for length prefix + if (offset + 2 > encoded.length) revert LibPrefixLengthEncodedByteArray__InvalidEncoding(); + + uint16 length = uint16(bytes2(encoded[offset:offset + 2])); + + // Check if length is valid + if (offset + 2 + length > encoded.length) revert LibPrefixLengthEncodedByteArray__InvalidEncoding(); + + offset += length + 2; + s++; + } + } + + /** + * @dev Cast an encoded array into a Solidity array. + */ + function toArray(bytes calldata encoded) + internal + pure + returns (bytes[] memory arr) + { + bytes calldata elem; + uint256 idx = 0; + arr = new bytes[](LibPrefixLengthEncodedByteArray.size(encoded)); + while (encoded.length > 0) { + (elem, encoded) = LibPrefixLengthEncodedByteArray.next(encoded); + arr[idx] = elem; + idx++; + } + } +} \ No newline at end of file diff --git a/foundry/test/LibPrefixLengthEncodedByteArray.t.sol b/foundry/test/LibPrefixLengthEncodedByteArray.t.sol new file mode 100644 index 0000000..7ea6d79 --- /dev/null +++ b/foundry/test/LibPrefixLengthEncodedByteArray.t.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Test} from "forge-std/Test.sol"; +import {LibPrefixLengthEncodedByteArray} from "../src/lib/LibPrefixLengthEncodedByteArray.sol"; + +contract LibPrefixLengthEncodedByteArrayTest is Test { + using LibPrefixLengthEncodedByteArray for bytes; + + function testNextEmpty() public { + bytes memory encoded = ""; + (bytes memory elem, bytes memory remaining) = this.next(encoded); + assertEq(elem.length, 0); + assertEq(remaining.length, 0); + } + + function testNextSingleElement() public { + // Create encoded data: length prefix (0003) followed by "ABC" + bytes memory encoded = hex"0003414243"; + (bytes memory elem, bytes memory remaining) = this.next(encoded); + + assertEq(elem.length, 3); + assertEq(elem, hex"414243"); // "ABC" + assertEq(remaining.length, 0); + } + + function testNextMultipleElements() public { + // Encoded data: [0003]ABC[0002]DE + bytes memory encoded = hex"000341424300024445"; + + // First next() + (bytes memory elem1, bytes memory remaining1) = this.next(encoded); + assertEq(elem1, hex"414243"); // "ABC" + assertEq(remaining1, hex"00024445"); + + // Second next() + (bytes memory elem2, bytes memory remaining2) = this.next(remaining1); + assertEq(elem2, hex"4445"); // "DE" + assertEq(remaining2.length, 0); + } + + function testSize() public { + bytes memory empty = ""; + assertEq(this.size(empty), 0); + + bytes memory single = hex"0003414243"; + assertEq(this.size(single), 1); + + bytes memory multiple = hex"0003414243000244450001FF"; + assertEq(this.size(multiple), 3); + } + + function testFailInvalidLength() public { + // Length prefix larger than remaining data + bytes memory invalid = hex"0004414243"; + this.next(invalid); + } + + function testFailIncompletePrefix() public { + // Only 1 byte instead of 2 bytes prefix + bytes memory invalid = hex"01"; + this.next(invalid); + } + + function testLargeElement() public { + // Test with a large but manageable size (1000 bytes) + bytes memory large = new bytes(1002); // 2 bytes prefix + 1000 bytes data + large[0] = bytes1(uint8(0x03)); // 03 + large[1] = bytes1(uint8(0xe8)); // E8 (1000 in hex) + + // Fill data bytes + for (uint i = 2; i < large.length; i++) { + large[i] = bytes1(uint8(0x01)); + } + + (bytes memory elem, bytes memory remaining) = this.next(large); + assertEq(elem.length, 1000); + assertEq(remaining.length, 0); + } + + function testSizeWithLargeElements() public { + // Two elements: 1000 bytes + 500 bytes + bytes memory data = new bytes(1504); // 1000 + 2 + 500 + 2 + + // First element (1000 bytes) + data[0] = bytes1(uint8(0x03)); // 03 + data[1] = bytes1(uint8(0xe8)); // E8 (1000 in hex) + + // Second element (500 bytes) + data[1002] = bytes1(uint8(0x01)); // 01 + data[1003] = bytes1(uint8(0xf4)); // F4 (500 in hex) + + assertEq(this.size(data), 2); + } + + function next(bytes calldata data) external pure returns (bytes memory, bytes memory) { + return data.next(); + } + + function size(bytes calldata data) external pure returns (uint256) { + return data.size(); + } + +} From ae662d002608c97b8e350241c5992b3659753c76 Mon Sep 17 00:00:00 2001 From: royvardhan Date: Thu, 23 Jan 2025 18:50:55 +0530 Subject: [PATCH 2/4] feat: keep assembly --- .../lib/LibPrefixLengthEncodedByteArray.sol | 67 +++++++++---------- 1 file changed, 33 insertions(+), 34 deletions(-) diff --git a/foundry/src/lib/LibPrefixLengthEncodedByteArray.sol b/foundry/src/lib/LibPrefixLengthEncodedByteArray.sol index f8053b4..6692b32 100644 --- a/foundry/src/lib/LibPrefixLengthEncodedByteArray.sol +++ b/foundry/src/lib/LibPrefixLengthEncodedByteArray.sol @@ -1,8 +1,17 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0; -error LibPrefixLengthEncodedByteArray__InvalidEncoding(); - +/** + * @title Propellerheads PrefixLengthEncoded Byte Array Library + * @author PropellerHeads Developers + * @dev Provide a gas efficient encoding for bytes array. + * + * Array of bytes are encoded as a single bytes like this : + * 16 bits ??bits 16bits ??bits ... + * [length(elem1)] [elem1] [length(elem2)] [elem2] ... + * + * This is much more efficient and compact than default solidity encoding. + */ library LibPrefixLengthEncodedByteArray { /** * @dev Pop the first element of an array and returns it with the remaining data. @@ -12,45 +21,35 @@ library LibPrefixLengthEncodedByteArray { pure returns (bytes calldata elem, bytes calldata res) { - // Handle empty input - if (encoded.length == 0) { - return (encoded[:0], encoded[:0]); + assembly { + switch iszero(encoded.length) + case 1 { + elem.offset := 0 + elem.length := 0 + res.offset := 0 + res.length := 0 + } + default { + let l := shr(240, calldataload(encoded.offset)) + elem.offset := add(encoded.offset, 2) + elem.length := l + res.offset := add(elem.offset, l) + res.length := sub(sub(encoded.length, l), 2) + } } - - // Ensure we have at least 2 bytes for length prefix - if (encoded.length < 2) revert LibPrefixLengthEncodedByteArray__InvalidEncoding(); - // Extract the length prefix (first 2 bytes) - uint16 length = uint16(bytes2(encoded[:2])); - - // Check if length is valid - if (2 + length > encoded.length) revert LibPrefixLengthEncodedByteArray__InvalidEncoding(); - - // Extract the element (after length prefix) - elem = encoded[2:2+length]; - - // Extract the remaining data - res = encoded[2+length:]; - - return (elem, res); } /** * @dev Gets the size of the encoded array. */ function size(bytes calldata encoded) internal pure returns (uint256 s) { - uint256 offset = 0; - - while (offset < encoded.length) { - // Ensure we have at least 2 bytes for length prefix - if (offset + 2 > encoded.length) revert LibPrefixLengthEncodedByteArray__InvalidEncoding(); - - uint16 length = uint16(bytes2(encoded[offset:offset + 2])); - - // Check if length is valid - if (offset + 2 + length > encoded.length) revert LibPrefixLengthEncodedByteArray__InvalidEncoding(); - - offset += length + 2; - s++; + assembly { + let offset := encoded.offset + let end := add(encoded.offset, encoded.length) + for {} lt(offset, end) {} { + offset := add(offset, add(shr(240, calldataload(offset)), 2)) + s := add(s, 1) + } } } From ef2600b7f8e7ae45ad30636121264c6c540961be Mon Sep 17 00:00:00 2001 From: royvardhan Date: Thu, 23 Jan 2025 19:03:42 +0530 Subject: [PATCH 3/4] chore: fmt and slither --- .../lib/LibPrefixLengthEncodedByteArray.sol | 4 +-- .../LibPrefixLengthEncodedByteArray.t.sol | 36 ++++++++++--------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/foundry/src/lib/LibPrefixLengthEncodedByteArray.sol b/foundry/src/lib/LibPrefixLengthEncodedByteArray.sol index 6692b32..b5773fd 100644 --- a/foundry/src/lib/LibPrefixLengthEncodedByteArray.sol +++ b/foundry/src/lib/LibPrefixLengthEncodedByteArray.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.0; +pragma solidity ^0.8.28; /** * @title Propellerheads PrefixLengthEncoded Byte Array Library @@ -70,4 +70,4 @@ library LibPrefixLengthEncodedByteArray { idx++; } } -} \ No newline at end of file +} diff --git a/foundry/test/LibPrefixLengthEncodedByteArray.t.sol b/foundry/test/LibPrefixLengthEncodedByteArray.t.sol index 7ea6d79..63f1f36 100644 --- a/foundry/test/LibPrefixLengthEncodedByteArray.t.sol +++ b/foundry/test/LibPrefixLengthEncodedByteArray.t.sol @@ -2,12 +2,13 @@ pragma solidity ^0.8.0; import {Test} from "forge-std/Test.sol"; -import {LibPrefixLengthEncodedByteArray} from "../src/lib/LibPrefixLengthEncodedByteArray.sol"; +import {LibPrefixLengthEncodedByteArray} from + "../src/lib/LibPrefixLengthEncodedByteArray.sol"; contract LibPrefixLengthEncodedByteArrayTest is Test { using LibPrefixLengthEncodedByteArray for bytes; - function testNextEmpty() public { + function testNextEmpty() public { bytes memory encoded = ""; (bytes memory elem, bytes memory remaining) = this.next(encoded); assertEq(elem.length, 0); @@ -18,7 +19,7 @@ contract LibPrefixLengthEncodedByteArrayTest is Test { // Create encoded data: length prefix (0003) followed by "ABC" bytes memory encoded = hex"0003414243"; (bytes memory elem, bytes memory remaining) = this.next(encoded); - + assertEq(elem.length, 3); assertEq(elem, hex"414243"); // "ABC" assertEq(remaining.length, 0); @@ -27,7 +28,7 @@ contract LibPrefixLengthEncodedByteArrayTest is Test { function testNextMultipleElements() public { // Encoded data: [0003]ABC[0002]DE bytes memory encoded = hex"000341424300024445"; - + // First next() (bytes memory elem1, bytes memory remaining1) = this.next(encoded); assertEq(elem1, hex"414243"); // "ABC" @@ -53,13 +54,13 @@ contract LibPrefixLengthEncodedByteArrayTest is Test { function testFailInvalidLength() public { // Length prefix larger than remaining data bytes memory invalid = hex"0004414243"; - this.next(invalid); + (bytes memory elem, bytes memory remaining) = this.next(invalid); } function testFailIncompletePrefix() public { // Only 1 byte instead of 2 bytes prefix bytes memory invalid = hex"01"; - this.next(invalid); + (bytes memory elem, bytes memory remaining) = this.next(invalid); } function testLargeElement() public { @@ -67,12 +68,12 @@ contract LibPrefixLengthEncodedByteArrayTest is Test { bytes memory large = new bytes(1002); // 2 bytes prefix + 1000 bytes data large[0] = bytes1(uint8(0x03)); // 03 large[1] = bytes1(uint8(0xe8)); // E8 (1000 in hex) - + // Fill data bytes - for (uint i = 2; i < large.length; i++) { + for (uint256 i = 2; i < large.length; i++) { large[i] = bytes1(uint8(0x01)); } - + (bytes memory elem, bytes memory remaining) = this.next(large); assertEq(elem.length, 1000); assertEq(remaining.length, 0); @@ -81,11 +82,11 @@ contract LibPrefixLengthEncodedByteArrayTest is Test { function testSizeWithLargeElements() public { // Two elements: 1000 bytes + 500 bytes bytes memory data = new bytes(1504); // 1000 + 2 + 500 + 2 - + // First element (1000 bytes) data[0] = bytes1(uint8(0x03)); // 03 data[1] = bytes1(uint8(0xe8)); // E8 (1000 in hex) - + // Second element (500 bytes) data[1002] = bytes1(uint8(0x01)); // 01 data[1003] = bytes1(uint8(0xf4)); // F4 (500 in hex) @@ -93,12 +94,15 @@ contract LibPrefixLengthEncodedByteArrayTest is Test { assertEq(this.size(data), 2); } - function next(bytes calldata data) external pure returns (bytes memory, bytes memory) { - return data.next(); + function next(bytes calldata data) + external + pure + returns (bytes memory elem, bytes memory remaining) + { + (elem, remaining) = data.next(); } - function size(bytes calldata data) external pure returns (uint256) { - return data.size(); + function size(bytes calldata data) external pure returns (uint256 s) { + s = data.size(); } - } From e6f3fc7004629930b63be07febcf2d4d50a2b4de Mon Sep 17 00:00:00 2001 From: royvardhan Date: Thu, 23 Jan 2025 19:14:56 +0530 Subject: [PATCH 4/4] chore: move to lib/bytes --- .../{src/lib => lib/bytes}/LibPrefixLengthEncodedByteArray.sol | 0 foundry/test/LibPrefixLengthEncodedByteArray.t.sol | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename foundry/{src/lib => lib/bytes}/LibPrefixLengthEncodedByteArray.sol (100%) diff --git a/foundry/src/lib/LibPrefixLengthEncodedByteArray.sol b/foundry/lib/bytes/LibPrefixLengthEncodedByteArray.sol similarity index 100% rename from foundry/src/lib/LibPrefixLengthEncodedByteArray.sol rename to foundry/lib/bytes/LibPrefixLengthEncodedByteArray.sol diff --git a/foundry/test/LibPrefixLengthEncodedByteArray.t.sol b/foundry/test/LibPrefixLengthEncodedByteArray.t.sol index 63f1f36..c06eb47 100644 --- a/foundry/test/LibPrefixLengthEncodedByteArray.t.sol +++ b/foundry/test/LibPrefixLengthEncodedByteArray.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.0; import {Test} from "forge-std/Test.sol"; import {LibPrefixLengthEncodedByteArray} from - "../src/lib/LibPrefixLengthEncodedByteArray.sol"; + "../lib/bytes/LibPrefixLengthEncodedByteArray.sol"; contract LibPrefixLengthEncodedByteArrayTest is Test { using LibPrefixLengthEncodedByteArray for bytes;