From 42c79f1416f29a76c2c55fc7335a65d382d0b8b6 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sun, 8 Jun 2025 19:07:31 -0600 Subject: [PATCH 01/18] Add new equal, nibbles and countLeadingZeroes functions --- .changeset/khaki-hats-leave.md | 5 ++ .changeset/ten-steaks-try.md | 5 ++ .changeset/whole-cats-find.md | 5 ++ contracts/utils/Bytes.sol | 29 +++++++++++ contracts/utils/Strings.sol | 3 +- test/utils/Bytes.t.sol | 77 ++++++++++++++++++++++++++++ test/utils/Bytes.test.js | 94 ++++++++++++++++++++++++++++++++++ 7 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 .changeset/khaki-hats-leave.md create mode 100644 .changeset/ten-steaks-try.md create mode 100644 .changeset/whole-cats-find.md create mode 100644 test/utils/Bytes.t.sol diff --git a/.changeset/khaki-hats-leave.md b/.changeset/khaki-hats-leave.md new file mode 100644 index 00000000000..021df0ff083 --- /dev/null +++ b/.changeset/khaki-hats-leave.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Bytes`: Add a `nibbles` function to split each byte into two nibbles. diff --git a/.changeset/ten-steaks-try.md b/.changeset/ten-steaks-try.md new file mode 100644 index 00000000000..a734f5fdb45 --- /dev/null +++ b/.changeset/ten-steaks-try.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Bytes`: Add an `equal` function to compare byte buffers. diff --git a/.changeset/whole-cats-find.md b/.changeset/whole-cats-find.md new file mode 100644 index 00000000000..e170da3dc63 --- /dev/null +++ b/.changeset/whole-cats-find.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Bytes`: Add a `countLeadingZeroes` function to count the leading zero bytes in a `uint256` value. diff --git a/contracts/utils/Bytes.sol b/contracts/utils/Bytes.sol index 1234b845513..a7ff88a4982 100644 --- a/contracts/utils/Bytes.sol +++ b/contracts/utils/Bytes.sol @@ -99,6 +99,35 @@ library Bytes { return result; } + /// @dev Split each byte in `value` into two nibbles (4 bits each). + function nibbles(bytes memory value) internal pure returns (bytes memory) { + uint256 length = value.length; + bytes memory nibbles_ = new bytes(length * 2); + for (uint256 i = 0; i < length; i++) { + (nibbles_[i * 2], nibbles_[i * 2 + 1]) = (value[i] & 0xf0, value[i] & 0x0f); + } + return nibbles_; + } + + /** + * @dev Returns true if the two byte buffers are equal. + */ + function equal(bytes memory a, bytes memory b) internal pure returns (bool) { + return a.length == b.length && keccak256(a) == keccak256(b); + } + + /// @dev Counts the number of leading zero bytes in a uint256. + function countLeadingZeroes(uint256 x) internal pure returns (uint256) { + if (x == 0) return 32; // All 32 bytes are zero + uint256 r = 0; + if (x > 0xffffffffffffffffffffffffffffffff) r = 128; // Upper 128 bits + if ((x >> r) > 0xffffffffffffffff) r |= 64; // Next 64 bits + if ((x >> r) > 0xffffffff) r |= 32; // Next 32 bits + if ((x >> r) > 0xffff) r |= 16; // Next 16 bits + if ((x >> r) > 0xff) r |= 8; // Next 8 bits + return 31 ^ (r >> 3); // Convert to leading zero bytes count + } + /** * @dev Reads a bytes32 from a bytes array without bounds checking. * diff --git a/contracts/utils/Strings.sol b/contracts/utils/Strings.sol index 4cc597646f2..a865bfbc785 100644 --- a/contracts/utils/Strings.sol +++ b/contracts/utils/Strings.sol @@ -6,6 +6,7 @@ pragma solidity ^0.8.20; import {Math} from "./math/Math.sol"; import {SafeCast} from "./math/SafeCast.sol"; import {SignedMath} from "./math/SignedMath.sol"; +import {Bytes} from "./Bytes.sol"; /** * @dev String operations. @@ -132,7 +133,7 @@ library Strings { * @dev Returns true if the two strings are equal. */ function equal(string memory a, string memory b) internal pure returns (bool) { - return bytes(a).length == bytes(b).length && keccak256(bytes(a)) == keccak256(bytes(b)); + return Bytes.equal(bytes(a), bytes(b)); } /** diff --git a/test/utils/Bytes.t.sol b/test/utils/Bytes.t.sol new file mode 100644 index 00000000000..e473ec4ff6a --- /dev/null +++ b/test/utils/Bytes.t.sol @@ -0,0 +1,77 @@ +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {Bytes} from "@openzeppelin/contracts/utils/Bytes.sol"; + +contract BytesTest is Test { + function testIndexOf(bytes memory buffer, bytes1 s) public pure { + testIndexOf(buffer, s, 0); + } + + function testIndexOf(bytes memory buffer, bytes1 s, uint256 pos) public pure { + uint256 result = Bytes.indexOf(buffer, s, pos); + + // Should not be found before result + for (uint256 i = pos; result != type(uint256).max && i < result; i++) assertNotEq(buffer[i], s); + if (result != type(uint256).max) assertEq(buffer[result], s); + } + + function testLastIndexOf(bytes memory buffer, bytes1 s) public pure { + testLastIndexOf(buffer, s, 0); + } + + function testLastIndexOf(bytes memory buffer, bytes1 s, uint256 pos) public pure { + pos = bound(pos, 0, buffer.length); + uint256 result = Bytes.lastIndexOf(buffer, s, pos); + + // Should not be found before result + for (uint256 i = pos; result != type(uint256).max && i < result; i++) assertNotEq(buffer[i], s); + if (result != type(uint256).max) assertEq(buffer[result], s); + } + + function testSlice(bytes memory buffer, uint256 start) public pure { + testSlice(buffer, start, buffer.length); + } + + function testSlice(bytes memory buffer, uint256 start, uint256 end) public pure { + bytes memory result = Bytes.slice(buffer, start, end); + uint256 sanitizedEnd = Math.min(end, buffer.length); + uint256 sanitizedStart = Math.min(start, sanitizedEnd); + assertEq(result.length, sanitizedEnd - sanitizedStart); + for (uint256 i = 0; i < result.length; i++) assertEq(result[i], buffer[sanitizedStart + i]); + } + + function testNibbles(bytes memory value) public pure { + bytes memory result = Bytes.nibbles(value); + assertEq(result.length, value.length * 2); + for (uint256 i = 0; i < value.length; i++) { + bytes1 originalByte = value[i]; + bytes1 highNibble = result[i * 2]; + bytes1 lowNibble = result[i * 2 + 1]; + + assertEq(highNibble, originalByte & 0xf0); + assertEq(lowNibble, originalByte & 0x0f); + } + } + + function testSymbolicEqual(bytes memory a, bytes memory b) public pure { + assertEq(Bytes.equal(a, b), Bytes.equal(a, b)); + } + + function testSymbolicCountLeadingZeroes(uint256 x) public pure { + uint256 result = Bytes.countLeadingZeroes(x); + assertLe(result, 32); // [0, 32] + + if (x != 0) { + uint256 firstNonZeroBytePos = 32 - result - 1; + uint256 byteValue = (x >> (firstNonZeroBytePos * 8)) & 0xff; + assertNotEq(byteValue, 0); + + // x != 0 implies result < 32 + // most significant byte should be non-zero + uint256 msbValue = (x >> (248 - result * 8)) & 0xff; + assertNotEq(msbValue, 0); + } + } +} diff --git a/test/utils/Bytes.test.js b/test/utils/Bytes.test.js index 52a1ae95e77..a496d0518f7 100644 --- a/test/utils/Bytes.test.js +++ b/test/utils/Bytes.test.js @@ -85,4 +85,98 @@ describe('Bytes', function () { } }); }); + + describe('nibbles', function () { + it('converts single byte', async function () { + await expect(this.mock.$nibbles('0xab')).to.eventually.equal('0xa00b'); + }); + + it('converts multiple bytes', async function () { + await expect(this.mock.$nibbles('0x1234')).to.eventually.equal('0x10023004'); + }); + + it('handles empty bytes', async function () { + await expect(this.mock.$nibbles('0x')).to.eventually.equal('0x'); + }); + + it('converts lorem text', async function () { + const result = await this.mock.$nibbles(lorem); + expect(ethers.dataLength(result)).to.equal(lorem.length * 2); + + // Check nibble extraction for first few bytes + for (let i = 0; i < Math.min(lorem.length, 5); i++) { + const originalByte = lorem[i]; + const highNibble = ethers.dataSlice(result, i * 2, i * 2 + 1); + const lowNibble = ethers.dataSlice(result, i * 2 + 1, i * 2 + 2); + + expect(highNibble).to.equal(ethers.toBeHex(originalByte & 0xf0, 1)); + expect(lowNibble).to.equal(ethers.toBeHex(originalByte & 0x0f, 1)); + } + }); + }); + + describe('equal', function () { + it('identical arrays', async function () { + await expect(this.mock.$equal(lorem, lorem)).to.eventually.be.true; + }); + + it('same content', async function () { + const copy = new Uint8Array(lorem); + await expect(this.mock.$equal(lorem, copy)).to.eventually.be.true; + }); + + it('different content', async function () { + const different = ethers.toUtf8Bytes('Different content'); + await expect(this.mock.$equal(lorem, different)).to.eventually.be.false; + }); + + it('different lengths', async function () { + const shorter = lorem.slice(0, 10); + await expect(this.mock.$equal(lorem, shorter)).to.eventually.be.false; + }); + + it('empty arrays', async function () { + const empty1 = new Uint8Array(0); + const empty2 = new Uint8Array(0); + await expect(this.mock.$equal(empty1, empty2)).to.eventually.be.true; + }); + + it('one empty one not', async function () { + const empty = new Uint8Array(0); + await expect(this.mock.$equal(lorem, empty)).to.eventually.be.false; + }); + }); + + describe('countLeadingZeroes', function () { + it('zero value', async function () { + await expect(this.mock.$countLeadingZeroes(0)).to.eventually.equal(32); + }); + + it('small values', async function () { + await expect(this.mock.$countLeadingZeroes(1)).to.eventually.equal(31); + await expect(this.mock.$countLeadingZeroes(255)).to.eventually.equal(31); + }); + + it('larger values', async function () { + await expect(this.mock.$countLeadingZeroes(256)).to.eventually.equal(30); + await expect(this.mock.$countLeadingZeroes(0xff00)).to.eventually.equal(30); + await expect(this.mock.$countLeadingZeroes(0x10000)).to.eventually.equal(29); + }); + + it('max value', async function () { + await expect(this.mock.$countLeadingZeroes(ethers.MaxUint256)).to.eventually.equal(0); + }); + + it('specific patterns', async function () { + await expect( + this.mock.$countLeadingZeroes('0x0000000000000000000000000000000000000000000000000000000000000100'), + ).to.eventually.equal(30); + await expect( + this.mock.$countLeadingZeroes('0x0000000000000000000000000000000000000000000000000000000000010000'), + ).to.eventually.equal(29); + await expect( + this.mock.$countLeadingZeroes('0x0000000000000000000000000000000000000000000000000000000001000000'), + ).to.eventually.equal(28); + }); + }); }); From 5754ab890369b0667293263b404ecd780808bca9 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sun, 8 Jun 2025 19:08:02 -0600 Subject: [PATCH 02/18] Rename countLeadingZeroes to clz --- .changeset/whole-cats-find.md | 2 +- contracts/utils/Bytes.sol | 2 +- test/utils/Bytes.t.sol | 2 +- test/utils/Bytes.test.js | 22 +++++++++++----------- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.changeset/whole-cats-find.md b/.changeset/whole-cats-find.md index e170da3dc63..e5ba8df6e5d 100644 --- a/.changeset/whole-cats-find.md +++ b/.changeset/whole-cats-find.md @@ -2,4 +2,4 @@ 'openzeppelin-solidity': minor --- -`Bytes`: Add a `countLeadingZeroes` function to count the leading zero bytes in a `uint256` value. +`Bytes`: Add a `clz` function to count the leading zero bytes in a `uint256` value. diff --git a/contracts/utils/Bytes.sol b/contracts/utils/Bytes.sol index a7ff88a4982..633a9cc913b 100644 --- a/contracts/utils/Bytes.sol +++ b/contracts/utils/Bytes.sol @@ -117,7 +117,7 @@ library Bytes { } /// @dev Counts the number of leading zero bytes in a uint256. - function countLeadingZeroes(uint256 x) internal pure returns (uint256) { + function clz(uint256 x) internal pure returns (uint256) { if (x == 0) return 32; // All 32 bytes are zero uint256 r = 0; if (x > 0xffffffffffffffffffffffffffffffff) r = 128; // Upper 128 bits diff --git a/test/utils/Bytes.t.sol b/test/utils/Bytes.t.sol index e473ec4ff6a..73c63b70bb3 100644 --- a/test/utils/Bytes.t.sol +++ b/test/utils/Bytes.t.sol @@ -60,7 +60,7 @@ contract BytesTest is Test { } function testSymbolicCountLeadingZeroes(uint256 x) public pure { - uint256 result = Bytes.countLeadingZeroes(x); + uint256 result = Bytes.clz(x); assertLe(result, 32); // [0, 32] if (x != 0) { diff --git a/test/utils/Bytes.test.js b/test/utils/Bytes.test.js index a496d0518f7..05ba530d94d 100644 --- a/test/utils/Bytes.test.js +++ b/test/utils/Bytes.test.js @@ -147,35 +147,35 @@ describe('Bytes', function () { }); }); - describe('countLeadingZeroes', function () { + describe('clz', function () { it('zero value', async function () { - await expect(this.mock.$countLeadingZeroes(0)).to.eventually.equal(32); + await expect(this.mock.$clz(0)).to.eventually.equal(32); }); it('small values', async function () { - await expect(this.mock.$countLeadingZeroes(1)).to.eventually.equal(31); - await expect(this.mock.$countLeadingZeroes(255)).to.eventually.equal(31); + await expect(this.mock.$clz(1)).to.eventually.equal(31); + await expect(this.mock.$clz(255)).to.eventually.equal(31); }); it('larger values', async function () { - await expect(this.mock.$countLeadingZeroes(256)).to.eventually.equal(30); - await expect(this.mock.$countLeadingZeroes(0xff00)).to.eventually.equal(30); - await expect(this.mock.$countLeadingZeroes(0x10000)).to.eventually.equal(29); + await expect(this.mock.$clz(256)).to.eventually.equal(30); + await expect(this.mock.$clz(0xff00)).to.eventually.equal(30); + await expect(this.mock.$clz(0x10000)).to.eventually.equal(29); }); it('max value', async function () { - await expect(this.mock.$countLeadingZeroes(ethers.MaxUint256)).to.eventually.equal(0); + await expect(this.mock.$clz(ethers.MaxUint256)).to.eventually.equal(0); }); it('specific patterns', async function () { await expect( - this.mock.$countLeadingZeroes('0x0000000000000000000000000000000000000000000000000000000000000100'), + this.mock.$clz('0x0000000000000000000000000000000000000000000000000000000000000100'), ).to.eventually.equal(30); await expect( - this.mock.$countLeadingZeroes('0x0000000000000000000000000000000000000000000000000000000000010000'), + this.mock.$clz('0x0000000000000000000000000000000000000000000000000000000000010000'), ).to.eventually.equal(29); await expect( - this.mock.$countLeadingZeroes('0x0000000000000000000000000000000000000000000000000000000001000000'), + this.mock.$clz('0x0000000000000000000000000000000000000000000000000000000001000000'), ).to.eventually.equal(28); }); }); From 5a44b1116541436c68a0d973be8ec3d28d3965f8 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Wed, 9 Jul 2025 11:49:26 -0600 Subject: [PATCH 03/18] up --- contracts/utils/Bytes.sol | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/contracts/utils/Bytes.sol b/contracts/utils/Bytes.sol index 633a9cc913b..7528416eae0 100644 --- a/contracts/utils/Bytes.sol +++ b/contracts/utils/Bytes.sol @@ -116,16 +116,8 @@ library Bytes { return a.length == b.length && keccak256(a) == keccak256(b); } - /// @dev Counts the number of leading zero bytes in a uint256. function clz(uint256 x) internal pure returns (uint256) { - if (x == 0) return 32; // All 32 bytes are zero - uint256 r = 0; - if (x > 0xffffffffffffffffffffffffffffffff) r = 128; // Upper 128 bits - if ((x >> r) > 0xffffffffffffffff) r |= 64; // Next 64 bits - if ((x >> r) > 0xffffffff) r |= 32; // Next 32 bits - if ((x >> r) > 0xffff) r |= 16; // Next 16 bits - if ((x >> r) > 0xff) r |= 8; // Next 8 bits - return 31 ^ (r >> 3); // Convert to leading zero bytes count + return Math.ternary(x == 0, 32, 31 - Math.log256(x)); } /** From d6db2d7a47708341a10612d34b83e91bda8d687e Mon Sep 17 00:00:00 2001 From: ernestognw Date: Wed, 9 Jul 2025 11:49:51 -0600 Subject: [PATCH 04/18] Document --- contracts/utils/Bytes.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/utils/Bytes.sol b/contracts/utils/Bytes.sol index 7528416eae0..0650dc0d42b 100644 --- a/contracts/utils/Bytes.sol +++ b/contracts/utils/Bytes.sol @@ -116,6 +116,7 @@ library Bytes { return a.length == b.length && keccak256(a) == keccak256(b); } + /// @dev Counts the number of leading zeros in a uint256. function clz(uint256 x) internal pure returns (uint256) { return Math.ternary(x == 0, 32, 31 - Math.log256(x)); } From 2cf0acfb39ddccf29dd68db5e2e03dea9a1edd54 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sat, 12 Jul 2025 14:46:15 -0600 Subject: [PATCH 05/18] up --- test/utils/Bytes.t.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/utils/Bytes.t.sol b/test/utils/Bytes.t.sol index c019db14ef3..7963112e6b4 100644 --- a/test/utils/Bytes.t.sol +++ b/test/utils/Bytes.t.sol @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: MIT + pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; From 1dfee419be6e8d911c4cb616568adbcb3745600c Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 21 Jul 2025 14:01:18 -0600 Subject: [PATCH 06/18] up --- contracts/utils/Bytes.sol | 7 ------- test/utils/Bytes.t.sol | 29 ----------------------------- 2 files changed, 36 deletions(-) diff --git a/contracts/utils/Bytes.sol b/contracts/utils/Bytes.sol index 85b6b88ac67..c6c2eaa0eab 100644 --- a/contracts/utils/Bytes.sol +++ b/contracts/utils/Bytes.sol @@ -150,13 +150,6 @@ library Bytes { return buffer; } - /* - * @dev Returns true if the two byte buffers are equal. - */ - function equal(bytes memory a, bytes memory b) internal pure returns (bool) { - return a.length == b.length && keccak256(a) == keccak256(b); - } - /** * @dev Reverses the byte order of a bytes32 value, converting between little-endian and big-endian. * Inspired in https://graphics.stanford.edu/~seander/bithacks.html#ReverseParallel[Reverse Parallel] diff --git a/test/utils/Bytes.t.sol b/test/utils/Bytes.t.sol index d80f0fcb0dd..7ca7ab28335 100644 --- a/test/utils/Bytes.t.sol +++ b/test/utils/Bytes.t.sol @@ -9,10 +9,6 @@ import {Bytes} from "@openzeppelin/contracts/utils/Bytes.sol"; contract BytesTest is Test { using Bytes for bytes; - function testSymbolicEqual(bytes memory a) public pure { - assertTrue(Bytes.equal(a, a)); - } - // INDEX OF function testIndexOf(bytes memory buffer, bytes1 s) public pure { uint256 result = Bytes.indexOf(buffer, s); @@ -155,31 +151,6 @@ contract BytesTest is Test { } } - function testIndexOf(bytes memory buffer, bytes1 s) public pure { - testIndexOf(buffer, s, 0); - } - - function testIndexOf(bytes memory buffer, bytes1 s, uint256 pos) public pure { - uint256 result = Bytes.indexOf(buffer, s, pos); - - // Should not be found before result - for (uint256 i = pos; result != type(uint256).max && i < result; i++) assertNotEq(buffer[i], s); - if (result != type(uint256).max) assertEq(buffer[result], s); - } - - function testLastIndexOf(bytes memory buffer, bytes1 s) public pure { - testLastIndexOf(buffer, s, 0); - } - - function testLastIndexOf(bytes memory buffer, bytes1 s, uint256 pos) public pure { - pos = bound(pos, 0, buffer.length); - uint256 result = Bytes.lastIndexOf(buffer, s, pos); - - // Should not be found before result - for (uint256 i = pos; result != type(uint256).max && i < result; i++) assertNotEq(buffer[i], s); - if (result != type(uint256).max) assertEq(buffer[result], s); - } - function testNibbles(bytes memory value) public pure { bytes memory result = Bytes.nibbles(value); assertEq(result.length, value.length * 2); From ab00bb1a0f450fa7f7c4ee317b65bc9061ceaef8 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 21 Jul 2025 14:40:16 -0600 Subject: [PATCH 07/18] up --- contracts/utils/Bytes.sol | 5 ----- contracts/utils/math/Math.sol | 7 +++++++ test/utils/Bytes.t.sol | 16 ---------------- test/utils/math/Math.t.sol | 16 ++++++++++++++++ 4 files changed, 23 insertions(+), 21 deletions(-) diff --git a/contracts/utils/Bytes.sol b/contracts/utils/Bytes.sol index c6c2eaa0eab..e3f6ed35ba5 100644 --- a/contracts/utils/Bytes.sol +++ b/contracts/utils/Bytes.sol @@ -115,11 +115,6 @@ library Bytes { return a.length == b.length && keccak256(a) == keccak256(b); } - /// @dev Counts the number of leading zeros in a uint256. - function clz(uint256 x) internal pure returns (uint256) { - return Math.ternary(x == 0, 32, 31 - Math.log256(x)); - } - /** * @dev Moves the content of `buffer`, from `start` (included) to the end of `buffer` to the start of that buffer. * diff --git a/contracts/utils/math/Math.sol b/contracts/utils/math/Math.sol index f0d608a2dea..c3daa647ded 100644 --- a/contracts/utils/math/Math.sol +++ b/contracts/utils/math/Math.sol @@ -746,4 +746,11 @@ library Math { function unsignedRoundsUp(Rounding rounding) internal pure returns (bool) { return uint8(rounding) % 2 == 1; } + + /** + * @dev Counts the number of leading zeros in a uint256. + */ + function clz(uint256 x) internal pure returns (uint256) { + return ternary(x == 0, 32, 31 - log256(x)); + } } diff --git a/test/utils/Bytes.t.sol b/test/utils/Bytes.t.sol index 7ca7ab28335..a6c2e687a64 100644 --- a/test/utils/Bytes.t.sol +++ b/test/utils/Bytes.t.sol @@ -168,22 +168,6 @@ contract BytesTest is Test { assertEq(Bytes.equal(a, b), Bytes.equal(a, b)); } - function testSymbolicCountLeadingZeroes(uint256 x) public pure { - uint256 result = Bytes.clz(x); - assertLe(result, 32); // [0, 32] - - if (x != 0) { - uint256 firstNonZeroBytePos = 32 - result - 1; - uint256 byteValue = (x >> (firstNonZeroBytePos * 8)) & 0xff; - assertNotEq(byteValue, 0); - - // x != 0 implies result < 32 - // most significant byte should be non-zero - uint256 msbValue = (x >> (248 - result * 8)) & 0xff; - assertNotEq(msbValue, 0); - } - } - // REVERSE BITS function testSymbolicReverseBytes32(bytes32 value) public pure { assertEq(Bytes.reverseBytes32(Bytes.reverseBytes32(value)), value); diff --git a/test/utils/math/Math.t.sol b/test/utils/math/Math.t.sol index 3c83febe9df..20d59bfc992 100644 --- a/test/utils/math/Math.t.sol +++ b/test/utils/math/Math.t.sol @@ -308,6 +308,22 @@ contract MathTest is Test { } } + function testSymbolicCountLeadingZeroes(uint256 x) public pure { + uint256 result = Math.clz(x); + assertLe(result, 32); // [0, 32] + + if (x != 0) { + uint256 firstNonZeroBytePos = 32 - result - 1; + uint256 byteValue = (x >> (firstNonZeroBytePos * 8)) & 0xff; + assertNotEq(byteValue, 0); + + // x != 0 implies result < 32 + // most significant byte should be non-zero + uint256 msbValue = (x >> (248 - result * 8)) & 0xff; + assertNotEq(msbValue, 0); + } + } + // Helpers function _asRounding(uint8 r) private pure returns (Math.Rounding) { vm.assume(r < uint8(type(Math.Rounding).max)); From e6092ae71d810c73ffefd70723405c2ef1a13b93 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 21 Jul 2025 15:05:21 -0600 Subject: [PATCH 08/18] Add clz(bytes) --- contracts/utils/Bytes.sol | 26 +++++++++++++++ test/utils/Bytes.t.sol | 41 ++++++++++++++++++++++++ test/utils/Bytes.test.js | 62 ++++++++++++++++++++++++------------ test/utils/math/Math.test.js | 33 +++++++++++++++++++ 4 files changed, 141 insertions(+), 21 deletions(-) diff --git a/contracts/utils/Bytes.sol b/contracts/utils/Bytes.sol index e3f6ed35ba5..aed377fcf13 100644 --- a/contracts/utils/Bytes.sol +++ b/contracts/utils/Bytes.sol @@ -197,6 +197,32 @@ library Bytes { return (value >> 8) | (value << 8); } + /** + * @dev Counts the number of leading zeros in a bytes array. Returns `buffer.length` + * if the buffer is all zeros. + */ + function clz(bytes memory buffer) internal pure returns (uint256) { + for (uint256 i = 0; i < buffer.length; i += 32) { + uint256 value = uint256(_unsafeReadBytesOffset(buffer, i)); + unchecked { + // Mask out bytes beyond buffer length + // left is buffer.length at most, can't overflow at realistic size + uint256 left = buffer.length - i; + if (left < 32) { + // left is less than 32, can't overflow + uint256 shift = (32 - left) * 8; + value = (value >> shift) << shift; // Clear the lower bits for the last iteration + } + } + if (value != 0) { + uint256 leadingZeros = Math.clz(value); + return Math.min(i + leadingZeros, buffer.length); + } + } + + return buffer.length; + } + /** * @dev Reads a bytes32 from a bytes array without bounds checking. * diff --git a/test/utils/Bytes.t.sol b/test/utils/Bytes.t.sol index a6c2e687a64..be280ec1234 100644 --- a/test/utils/Bytes.t.sol +++ b/test/utils/Bytes.t.sol @@ -209,6 +209,47 @@ contract BytesTest is Test { assertEq(Bytes.reverseBytes2(_dirtyBytes2(Bytes.reverseBytes2(value))), value); } + // CLZ (Count Leading Zeros) + function testClz(bytes memory buffer) public pure { + uint256 result = Bytes.clz(buffer); + + // Result should never exceed buffer length + assertLe(result, buffer.length); + + if (buffer.length == 0) { + // Empty buffer should return 0 + assertEq(result, 0); + } else if (result == buffer.length) { + // If result equals buffer length, all bytes must be zero + for (uint256 i = 0; i < buffer.length; ++i) { + assertEq(buffer[i], 0); + } + } else { + // If result < buffer.length, byte at result position must be non-zero + assertNotEq(buffer[result], 0); + + // All bytes before result position must be zero + for (uint256 i = 0; i < result; ++i) { + assertEq(buffer[i], 0); + } + } + } + + function testClzConsistentWithByteByByteCount(bytes memory buffer) public pure { + uint256 result = Bytes.clz(buffer); + + // Manually count leading zeros byte by byte + uint256 manualCount = 0; + for (uint256 i = 0; i < buffer.length; ++i) { + if (buffer[i] != 0) { + break; + } + manualCount++; + } + + assertEq(result, manualCount); + } + // Helpers function _dirtyBytes16(bytes16 value) private pure returns (bytes16 dirty) { assembly ("memory-safe") { diff --git a/test/utils/Bytes.test.js b/test/utils/Bytes.test.js index 4ea253ece66..390712d968c 100644 --- a/test/utils/Bytes.test.js +++ b/test/utils/Bytes.test.js @@ -173,36 +173,56 @@ describe('Bytes', function () { }); }); - describe('clz', function () { - it('zero value', async function () { - await expect(this.mock.$clz(0)).to.eventually.equal(32); + describe('clz bytes', function () { + it('empty buffer', async function () { + await expect(this.mock['$clz(bytes)']('0x')).to.eventually.equal(0); }); - it('small values', async function () { - await expect(this.mock.$clz(1)).to.eventually.equal(31); - await expect(this.mock.$clz(255)).to.eventually.equal(31); + it('single zero byte', async function () { + await expect(this.mock['$clz(bytes)']('0x00')).to.eventually.equal(1); }); - it('larger values', async function () { - await expect(this.mock.$clz(256)).to.eventually.equal(30); - await expect(this.mock.$clz(0xff00)).to.eventually.equal(30); - await expect(this.mock.$clz(0x10000)).to.eventually.equal(29); + it('single non-zero byte', async function () { + await expect(this.mock['$clz(bytes)']('0x01')).to.eventually.equal(0); + await expect(this.mock['$clz(bytes)']('0xff')).to.eventually.equal(0); }); - it('max value', async function () { - await expect(this.mock.$clz(ethers.MaxUint256)).to.eventually.equal(0); + it('multiple leading zeros', async function () { + await expect(this.mock['$clz(bytes)']('0x0000000001')).to.eventually.equal(4); + await expect( + this.mock['$clz(bytes)']('0x0000000000000000000000000000000000000000000000000000000000000001'), + ).to.eventually.equal(31); }); - it('specific patterns', async function () { - await expect( - this.mock.$clz('0x0000000000000000000000000000000000000000000000000000000000000100'), - ).to.eventually.equal(30); - await expect( - this.mock.$clz('0x0000000000000000000000000000000000000000000000000000000000010000'), - ).to.eventually.equal(29); + it('all zeros of various lengths', async function () { + await expect(this.mock['$clz(bytes)']('0x00000000')).to.eventually.equal(4); await expect( - this.mock.$clz('0x0000000000000000000000000000000000000000000000000000000001000000'), - ).to.eventually.equal(28); + this.mock['$clz(bytes)']('0x0000000000000000000000000000000000000000000000000000000000000000'), + ).to.eventually.equal(32); + + // Complete chunks + await expect(this.mock['$clz(bytes)']('0x' + '00'.repeat(32) + '01')).to.eventually.equal(32); + await expect(this.mock['$clz(bytes)']('0x' + '00'.repeat(64) + '01')).to.eventually.equal(64); + + // Partial last chunk + await expect(this.mock['$clz(bytes)']('0x' + '00'.repeat(33) + '01')).to.eventually.equal(33); + await expect(this.mock['$clz(bytes)']('0x' + '00'.repeat(34) + '01')).to.eventually.equal(34); + await expect(this.mock['$clz(bytes)']('0x' + '00'.repeat(40) + '01' + '00'.repeat(9))).to.eventually.equal(40); + await expect(this.mock['$clz(bytes)']('0x' + '00'.repeat(50))).to.eventually.equal(50); + + // First byte of each chunk non-zero + await expect(this.mock['$clz(bytes)']('0x01' + '00'.repeat(31))).to.eventually.equal(0); + await expect(this.mock['$clz(bytes)']('0x' + '00'.repeat(32) + '01' + '00'.repeat(31))).to.eventually.equal(32); + + // Last byte of each chunk non-zero + await expect(this.mock['$clz(bytes)']('0x' + '00'.repeat(31) + '01')).to.eventually.equal(31); + await expect(this.mock['$clz(bytes)']('0x' + '00'.repeat(63) + '01')).to.eventually.equal(63); + + // Middle byte of each chunk non-zero + await expect(this.mock['$clz(bytes)']('0x' + '00'.repeat(16) + '01' + '00'.repeat(15))).to.eventually.equal(16); + await expect(this.mock['$clz(bytes)']('0x' + '00'.repeat(32) + '01' + '00'.repeat(31))).to.eventually.equal(32); + await expect(this.mock['$clz(bytes)']('0x' + '00'.repeat(48) + '01' + '00'.repeat(47))).to.eventually.equal(48); + await expect(this.mock['$clz(bytes)']('0x' + '00'.repeat(64) + '01' + '00'.repeat(63))).to.eventually.equal(64); }); }); diff --git a/test/utils/math/Math.test.js b/test/utils/math/Math.test.js index 6a09938148a..fe7149b1615 100644 --- a/test/utils/math/Math.test.js +++ b/test/utils/math/Math.test.js @@ -710,4 +710,37 @@ describe('Math', function () { }); }); }); + + describe('clz', function () { + it('zero value', async function () { + await expect(this.mock.$clz(0)).to.eventually.equal(32); + }); + + it('small values', async function () { + await expect(this.mock.$clz(1)).to.eventually.equal(31); + await expect(this.mock.$clz(255)).to.eventually.equal(31); + }); + + it('larger values', async function () { + await expect(this.mock.$clz(256)).to.eventually.equal(30); + await expect(this.mock.$clz(0xff00)).to.eventually.equal(30); + await expect(this.mock.$clz(0x10000)).to.eventually.equal(29); + }); + + it('max value', async function () { + await expect(this.mock.$clz(ethers.MaxUint256)).to.eventually.equal(0); + }); + + it('specific patterns', async function () { + await expect( + this.mock.$clz('0x0000000000000000000000000000000000000000000000000000000000000100'), + ).to.eventually.equal(30); + await expect( + this.mock.$clz('0x0000000000000000000000000000000000000000000000000000000000010000'), + ).to.eventually.equal(29); + await expect( + this.mock.$clz('0x0000000000000000000000000000000000000000000000000000000001000000'), + ).to.eventually.equal(28); + }); + }); }); From ede80e927460436dba8289e3d4782aecff62ff56 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 21 Jul 2025 15:18:14 -0600 Subject: [PATCH 09/18] up --- contracts/utils/Bytes.sol | 14 ++++----- test/utils/Bytes.test.js | 64 +++++++++++++++++++-------------------- 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/contracts/utils/Bytes.sol b/contracts/utils/Bytes.sol index aed377fcf13..1e810d6416a 100644 --- a/contracts/utils/Bytes.sol +++ b/contracts/utils/Bytes.sol @@ -108,13 +108,6 @@ library Bytes { return nibbles_; } - /** - * @dev Returns true if the two byte buffers are equal. - */ - function equal(bytes memory a, bytes memory b) internal pure returns (bool) { - return a.length == b.length && keccak256(a) == keccak256(b); - } - /** * @dev Moves the content of `buffer`, from `start` (included) to the end of `buffer` to the start of that buffer. * @@ -145,6 +138,13 @@ library Bytes { return buffer; } + /** + * @dev Returns true if the two byte buffers are equal. + */ + function equal(bytes memory a, bytes memory b) internal pure returns (bool) { + return a.length == b.length && keccak256(a) == keccak256(b); + } + /** * @dev Reverses the byte order of a bytes32 value, converting between little-endian and big-endian. * Inspired in https://graphics.stanford.edu/~seander/bithacks.html#ReverseParallel[Reverse Parallel] diff --git a/test/utils/Bytes.test.js b/test/utils/Bytes.test.js index 390712d968c..f7e4a89b6f5 100644 --- a/test/utils/Bytes.test.js +++ b/test/utils/Bytes.test.js @@ -141,38 +141,6 @@ describe('Bytes', function () { }); }); - describe('equal', function () { - it('identical arrays', async function () { - await expect(this.mock.$equal(lorem, lorem)).to.eventually.be.true; - }); - - it('same content', async function () { - const copy = new Uint8Array(lorem); - await expect(this.mock.$equal(lorem, copy)).to.eventually.be.true; - }); - - it('different content', async function () { - const different = ethers.toUtf8Bytes('Different content'); - await expect(this.mock.$equal(lorem, different)).to.eventually.be.false; - }); - - it('different lengths', async function () { - const shorter = lorem.slice(0, 10); - await expect(this.mock.$equal(lorem, shorter)).to.eventually.be.false; - }); - - it('empty arrays', async function () { - const empty1 = new Uint8Array(0); - const empty2 = new Uint8Array(0); - await expect(this.mock.$equal(empty1, empty2)).to.eventually.be.true; - }); - - it('one empty one not', async function () { - const empty = new Uint8Array(0); - await expect(this.mock.$equal(lorem, empty)).to.eventually.be.false; - }); - }); - describe('clz bytes', function () { it('empty buffer', async function () { await expect(this.mock['$clz(bytes)']('0x')).to.eventually.equal(0); @@ -226,6 +194,38 @@ describe('Bytes', function () { }); }); + describe('equal', function () { + it('identical buffers', async function () { + await expect(this.mock.$equal(lorem, lorem)).to.eventually.be.true; + }); + + it('same content', async function () { + const copy = new Uint8Array(lorem); + await expect(this.mock.$equal(lorem, copy)).to.eventually.be.true; + }); + + it('different content', async function () { + const different = ethers.toUtf8Bytes('Different content'); + await expect(this.mock.$equal(lorem, different)).to.eventually.be.false; + }); + + it('different lengths', async function () { + const shorter = lorem.slice(0, 10); + await expect(this.mock.$equal(lorem, shorter)).to.eventually.be.false; + }); + + it('empty buffers', async function () { + const empty1 = new Uint8Array(0); + const empty2 = new Uint8Array(0); + await expect(this.mock.$equal(empty1, empty2)).to.eventually.be.true; + }); + + it('one empty one not', async function () { + const empty = new Uint8Array(0); + await expect(this.mock.$equal(lorem, empty)).to.eventually.be.false; + }); + }); + describe('reverseBits', function () { describe('reverseBytes32', function () { it('reverses bytes correctly', async function () { From d65d7a529fbf5dbe6141ff299d7ff4c8a4a2e052 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 21 Jul 2025 15:21:05 -0600 Subject: [PATCH 10/18] Minimize diff --- test/utils/Bytes.t.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/utils/Bytes.t.sol b/test/utils/Bytes.t.sol index be280ec1234..0e949f9960b 100644 --- a/test/utils/Bytes.t.sol +++ b/test/utils/Bytes.t.sol @@ -9,6 +9,10 @@ import {Bytes} from "@openzeppelin/contracts/utils/Bytes.sol"; contract BytesTest is Test { using Bytes for bytes; + function testSymbolicEqual(bytes memory a) public pure { + assertTrue(Bytes.equal(a, a)); + } + // INDEX OF function testIndexOf(bytes memory buffer, bytes1 s) public pure { uint256 result = Bytes.indexOf(buffer, s); @@ -164,10 +168,6 @@ contract BytesTest is Test { } } - function testSymbolicEqual(bytes memory a, bytes memory b) public pure { - assertEq(Bytes.equal(a, b), Bytes.equal(a, b)); - } - // REVERSE BITS function testSymbolicReverseBytes32(bytes32 value) public pure { assertEq(Bytes.reverseBytes32(Bytes.reverseBytes32(value)), value); From fb91c859e89c47967d1bbc35eac93a56b36c5897 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Tue, 22 Jul 2025 13:32:19 -0600 Subject: [PATCH 11/18] up --- contracts/utils/Bytes.sol | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/contracts/utils/Bytes.sol b/contracts/utils/Bytes.sol index 1e810d6416a..c3875a9b9ca 100644 --- a/contracts/utils/Bytes.sol +++ b/contracts/utils/Bytes.sol @@ -103,7 +103,10 @@ library Bytes { uint256 length = value.length; bytes memory nibbles_ = new bytes(length * 2); for (uint256 i = 0; i < length; i++) { - (nibbles_[i * 2], nibbles_[i * 2 + 1]) = (value[i] & 0xf0, value[i] & 0x0f); + unchecked { + // Bounded to the array length, can't overflow realistically + (nibbles_[i * 2], nibbles_[i * 2 + 1]) = (value[i] & 0xf0, value[i] & 0x0f); + } } return nibbles_; } From c74934691225b4546cbc104f197db552ad874853 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 22 Jul 2025 23:42:04 +0200 Subject: [PATCH 12/18] Change CLZ behavior to count zero bits instead of zero bytes --- contracts/utils/Bytes.sol | 20 +++----------- contracts/utils/math/Math.sol | 4 +-- test/utils/Bytes.t.sol | 45 +++++++++++-------------------- test/utils/Bytes.test.js | 50 ++++++++++++++++++----------------- 4 files changed, 48 insertions(+), 71 deletions(-) diff --git a/contracts/utils/Bytes.sol b/contracts/utils/Bytes.sol index c3875a9b9ca..4c466f99aba 100644 --- a/contracts/utils/Bytes.sol +++ b/contracts/utils/Bytes.sol @@ -206,24 +206,12 @@ library Bytes { */ function clz(bytes memory buffer) internal pure returns (uint256) { for (uint256 i = 0; i < buffer.length; i += 32) { - uint256 value = uint256(_unsafeReadBytesOffset(buffer, i)); - unchecked { - // Mask out bytes beyond buffer length - // left is buffer.length at most, can't overflow at realistic size - uint256 left = buffer.length - i; - if (left < 32) { - // left is less than 32, can't overflow - uint256 shift = (32 - left) * 8; - value = (value >> shift) << shift; // Clear the lower bits for the last iteration - } - } - if (value != 0) { - uint256 leadingZeros = Math.clz(value); - return Math.min(i + leadingZeros, buffer.length); + bytes32 chunk = _unsafeReadBytesOffset(buffer, i); + if (chunk != bytes32(0)) { + return Math.min(8 * i + Math.clz(uint256(chunk)), 8 * buffer.length); } } - - return buffer.length; + return 8 * buffer.length; } /** diff --git a/contracts/utils/math/Math.sol b/contracts/utils/math/Math.sol index c3daa647ded..668774c6aa4 100644 --- a/contracts/utils/math/Math.sol +++ b/contracts/utils/math/Math.sol @@ -748,9 +748,9 @@ library Math { } /** - * @dev Counts the number of leading zeros in a uint256. + * @dev Counts the number of leading zeros bits in a uint256. */ function clz(uint256 x) internal pure returns (uint256) { - return ternary(x == 0, 32, 31 - log256(x)); + return ternary(x == 0, 256, 255 - log2(x)); } } diff --git a/test/utils/Bytes.t.sol b/test/utils/Bytes.t.sol index 0e949f9960b..d6eb2fc2c1c 100644 --- a/test/utils/Bytes.t.sol +++ b/test/utils/Bytes.t.sol @@ -213,41 +213,28 @@ contract BytesTest is Test { function testClz(bytes memory buffer) public pure { uint256 result = Bytes.clz(buffer); - // Result should never exceed buffer length - assertLe(result, buffer.length); + // index and offset of the first non zero bit + uint256 index = result / 8; + uint256 offset = result % 8; - if (buffer.length == 0) { - // Empty buffer should return 0 - assertEq(result, 0); - } else if (result == buffer.length) { - // If result equals buffer length, all bytes must be zero - for (uint256 i = 0; i < buffer.length; ++i) { - assertEq(buffer[i], 0); - } - } else { - // If result < buffer.length, byte at result position must be non-zero - assertNotEq(buffer[result], 0); + // Result should never exceed buffer length + assertLe(index, buffer.length); - // All bytes before result position must be zero - for (uint256 i = 0; i < result; ++i) { - assertEq(buffer[i], 0); - } + // All bytes before index position must be zero + for (uint256 i = 0; i < index; ++i) { + assertEq(buffer[i], 0); } - } - function testClzConsistentWithByteByByteCount(bytes memory buffer) public pure { - uint256 result = Bytes.clz(buffer); + // If index < buffer.length, byte at index position must be non-zero + if (index < buffer.length) { + // bit at position offset must be non zero + bytes1 singleBitMask = bytes1(0x80) >> offset; + assertEq(buffer[index] & singleBitMask, singleBitMask); - // Manually count leading zeros byte by byte - uint256 manualCount = 0; - for (uint256 i = 0; i < buffer.length; ++i) { - if (buffer[i] != 0) { - break; - } - manualCount++; + // all bits before offset must be zero + bytes1 multiBitsMask = bytes1(0xff) << (8 - offset); + assertEq(buffer[index] & multiBitsMask, 0); } - - assertEq(result, manualCount); } // Helpers diff --git a/test/utils/Bytes.test.js b/test/utils/Bytes.test.js index f7e4a89b6f5..48df5bf14db 100644 --- a/test/utils/Bytes.test.js +++ b/test/utils/Bytes.test.js @@ -143,54 +143,56 @@ describe('Bytes', function () { describe('clz bytes', function () { it('empty buffer', async function () { - await expect(this.mock['$clz(bytes)']('0x')).to.eventually.equal(0); + await expect(this.mock.$clz('0x')).to.eventually.equal(0); }); it('single zero byte', async function () { - await expect(this.mock['$clz(bytes)']('0x00')).to.eventually.equal(1); + await expect(this.mock.$clz('0x00')).to.eventually.equal(8); }); it('single non-zero byte', async function () { - await expect(this.mock['$clz(bytes)']('0x01')).to.eventually.equal(0); - await expect(this.mock['$clz(bytes)']('0xff')).to.eventually.equal(0); + await expect(this.mock.$clz('0x01')).to.eventually.equal(7); + await expect(this.mock.$clz('0xff')).to.eventually.equal(0); }); it('multiple leading zeros', async function () { - await expect(this.mock['$clz(bytes)']('0x0000000001')).to.eventually.equal(4); + await expect(this.mock.$clz('0x0000000001')).to.eventually.equal(39); await expect( - this.mock['$clz(bytes)']('0x0000000000000000000000000000000000000000000000000000000000000001'), - ).to.eventually.equal(31); + this.mock.$clz('0x0000000000000000000000000000000000000000000000000000000000000001'), + ).to.eventually.equal(255); }); it('all zeros of various lengths', async function () { - await expect(this.mock['$clz(bytes)']('0x00000000')).to.eventually.equal(4); + await expect(this.mock.$clz('0x00000000')).to.eventually.equal(32); await expect( - this.mock['$clz(bytes)']('0x0000000000000000000000000000000000000000000000000000000000000000'), - ).to.eventually.equal(32); + this.mock.$clz('0x0000000000000000000000000000000000000000000000000000000000000000'), + ).to.eventually.equal(256); // Complete chunks - await expect(this.mock['$clz(bytes)']('0x' + '00'.repeat(32) + '01')).to.eventually.equal(32); - await expect(this.mock['$clz(bytes)']('0x' + '00'.repeat(64) + '01')).to.eventually.equal(64); + await expect(this.mock.$clz('0x' + '00'.repeat(32) + '01')).to.eventually.equal(263); // 32*8+7 + await expect(this.mock.$clz('0x' + '00'.repeat(64) + '01')).to.eventually.equal(519); // 64*8+7 // Partial last chunk - await expect(this.mock['$clz(bytes)']('0x' + '00'.repeat(33) + '01')).to.eventually.equal(33); - await expect(this.mock['$clz(bytes)']('0x' + '00'.repeat(34) + '01')).to.eventually.equal(34); - await expect(this.mock['$clz(bytes)']('0x' + '00'.repeat(40) + '01' + '00'.repeat(9))).to.eventually.equal(40); - await expect(this.mock['$clz(bytes)']('0x' + '00'.repeat(50))).to.eventually.equal(50); + await expect(this.mock.$clz('0x' + '00'.repeat(33) + '01')).to.eventually.equal(271); // 33*8+7 + await expect(this.mock.$clz('0x' + '00'.repeat(34) + '01')).to.eventually.equal(279); // 34*8+7 + await expect(this.mock.$clz('0x' + '00'.repeat(40) + '01' + '00'.repeat(9))).to.eventually.equal(327); // 40*8+7 + await expect(this.mock.$clz('0x' + '00'.repeat(50))).to.eventually.equal(400); // 50*8 // First byte of each chunk non-zero - await expect(this.mock['$clz(bytes)']('0x01' + '00'.repeat(31))).to.eventually.equal(0); - await expect(this.mock['$clz(bytes)']('0x' + '00'.repeat(32) + '01' + '00'.repeat(31))).to.eventually.equal(32); + await expect(this.mock.$clz('0x80' + '00'.repeat(31))).to.eventually.equal(0); + await expect(this.mock.$clz('0x01' + '00'.repeat(31))).to.eventually.equal(7); + await expect(this.mock.$clz('0x' + '00'.repeat(32) + '80' + '00'.repeat(31))).to.eventually.equal(256); // 32*8 + await expect(this.mock.$clz('0x' + '00'.repeat(32) + '01' + '00'.repeat(31))).to.eventually.equal(263); // 32*8+7 // Last byte of each chunk non-zero - await expect(this.mock['$clz(bytes)']('0x' + '00'.repeat(31) + '01')).to.eventually.equal(31); - await expect(this.mock['$clz(bytes)']('0x' + '00'.repeat(63) + '01')).to.eventually.equal(63); + await expect(this.mock.$clz('0x' + '00'.repeat(31) + '01')).to.eventually.equal(255); // 31*8+7 + await expect(this.mock.$clz('0x' + '00'.repeat(63) + '01')).to.eventually.equal(511); // 63*8+7 // Middle byte of each chunk non-zero - await expect(this.mock['$clz(bytes)']('0x' + '00'.repeat(16) + '01' + '00'.repeat(15))).to.eventually.equal(16); - await expect(this.mock['$clz(bytes)']('0x' + '00'.repeat(32) + '01' + '00'.repeat(31))).to.eventually.equal(32); - await expect(this.mock['$clz(bytes)']('0x' + '00'.repeat(48) + '01' + '00'.repeat(47))).to.eventually.equal(48); - await expect(this.mock['$clz(bytes)']('0x' + '00'.repeat(64) + '01' + '00'.repeat(63))).to.eventually.equal(64); + await expect(this.mock.$clz('0x' + '00'.repeat(16) + '01' + '00'.repeat(15))).to.eventually.equal(135); // 16*8+7 + await expect(this.mock.$clz('0x' + '00'.repeat(32) + '01' + '00'.repeat(31))).to.eventually.equal(263); // 32*8+7 + await expect(this.mock.$clz('0x' + '00'.repeat(48) + '01' + '00'.repeat(47))).to.eventually.equal(391); // 48*8+7 + await expect(this.mock.$clz('0x' + '00'.repeat(64) + '01' + '00'.repeat(63))).to.eventually.equal(519); // 64*8+7 }); }); From 12c87ddda855dc385a235c97aed4f56f2ff41679 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 22 Jul 2025 23:44:37 +0200 Subject: [PATCH 13/18] Update Bytes.sol --- contracts/utils/Bytes.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/utils/Bytes.sol b/contracts/utils/Bytes.sol index 4c466f99aba..ae1a38f01b5 100644 --- a/contracts/utils/Bytes.sol +++ b/contracts/utils/Bytes.sol @@ -201,7 +201,7 @@ library Bytes { } /** - * @dev Counts the number of leading zeros in a bytes array. Returns `buffer.length` + * @dev Counts the number of leading zeros in bits a bytes array. Returns `8 * buffer.length` * if the buffer is all zeros. */ function clz(bytes memory buffer) internal pure returns (uint256) { From 740a056abe11c442c2179c97476379eeeab3be69 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 22 Jul 2025 23:53:51 +0200 Subject: [PATCH 14/18] fix Math tests --- test/utils/math/Math.t.sol | 21 ++++++++++++--------- test/utils/math/Math.test.js | 18 +++++++++--------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/test/utils/math/Math.t.sol b/test/utils/math/Math.t.sol index 20d59bfc992..b25db92de8d 100644 --- a/test/utils/math/Math.t.sol +++ b/test/utils/math/Math.t.sol @@ -310,17 +310,20 @@ contract MathTest is Test { function testSymbolicCountLeadingZeroes(uint256 x) public pure { uint256 result = Math.clz(x); - assertLe(result, 32); // [0, 32] - if (x != 0) { - uint256 firstNonZeroBytePos = 32 - result - 1; - uint256 byteValue = (x >> (firstNonZeroBytePos * 8)) & 0xff; - assertNotEq(byteValue, 0); + if (x == 0) { + assertEq(result, 256); + } else { + // result in [0, 255] + assertLe(result, 255); + + // bit at position offset must be non zero + uint256 singleBitMask = uint256(1) << (255 - result); + assertEq(x & singleBitMask, singleBitMask); - // x != 0 implies result < 32 - // most significant byte should be non-zero - uint256 msbValue = (x >> (248 - result * 8)) & 0xff; - assertNotEq(msbValue, 0); + // all bits before offset must be zero + uint256 multiBitsMask = type(uint256).max << (256 - result); + assertEq(x & multiBitsMask, 0); } } diff --git a/test/utils/math/Math.test.js b/test/utils/math/Math.test.js index fe7149b1615..507a3e9cec0 100644 --- a/test/utils/math/Math.test.js +++ b/test/utils/math/Math.test.js @@ -713,18 +713,18 @@ describe('Math', function () { describe('clz', function () { it('zero value', async function () { - await expect(this.mock.$clz(0)).to.eventually.equal(32); + await expect(this.mock.$clz(0)).to.eventually.equal(256); }); it('small values', async function () { - await expect(this.mock.$clz(1)).to.eventually.equal(31); - await expect(this.mock.$clz(255)).to.eventually.equal(31); + await expect(this.mock.$clz(1)).to.eventually.equal(255); + await expect(this.mock.$clz(255)).to.eventually.equal(248); }); it('larger values', async function () { - await expect(this.mock.$clz(256)).to.eventually.equal(30); - await expect(this.mock.$clz(0xff00)).to.eventually.equal(30); - await expect(this.mock.$clz(0x10000)).to.eventually.equal(29); + await expect(this.mock.$clz(256)).to.eventually.equal(247); + await expect(this.mock.$clz(0xff00)).to.eventually.equal(240); + await expect(this.mock.$clz(0x10000)).to.eventually.equal(239); }); it('max value', async function () { @@ -734,13 +734,13 @@ describe('Math', function () { it('specific patterns', async function () { await expect( this.mock.$clz('0x0000000000000000000000000000000000000000000000000000000000000100'), - ).to.eventually.equal(30); + ).to.eventually.equal(247); await expect( this.mock.$clz('0x0000000000000000000000000000000000000000000000000000000000010000'), - ).to.eventually.equal(29); + ).to.eventually.equal(239); await expect( this.mock.$clz('0x0000000000000000000000000000000000000000000000000000000001000000'), - ).to.eventually.equal(28); + ).to.eventually.equal(231); }); }); }); From 34e0783d20c74d2c5564c453eb9d934f3a6adbb0 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Thu, 24 Jul 2025 09:35:09 -0600 Subject: [PATCH 15/18] Remove nibbles --- .changeset/khaki-hats-leave.md | 5 ----- contracts/utils/Bytes.sol | 13 ------------- test/utils/Bytes.t.sol | 13 ------------- test/utils/Bytes.test.js | 29 ----------------------------- 4 files changed, 60 deletions(-) delete mode 100644 .changeset/khaki-hats-leave.md diff --git a/.changeset/khaki-hats-leave.md b/.changeset/khaki-hats-leave.md deleted file mode 100644 index 021df0ff083..00000000000 --- a/.changeset/khaki-hats-leave.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`Bytes`: Add a `nibbles` function to split each byte into two nibbles. diff --git a/contracts/utils/Bytes.sol b/contracts/utils/Bytes.sol index ae1a38f01b5..a29ae931e09 100644 --- a/contracts/utils/Bytes.sol +++ b/contracts/utils/Bytes.sol @@ -98,19 +98,6 @@ library Bytes { return result; } - /// @dev Split each byte in `value` into two nibbles (4 bits each). - function nibbles(bytes memory value) internal pure returns (bytes memory) { - uint256 length = value.length; - bytes memory nibbles_ = new bytes(length * 2); - for (uint256 i = 0; i < length; i++) { - unchecked { - // Bounded to the array length, can't overflow realistically - (nibbles_[i * 2], nibbles_[i * 2 + 1]) = (value[i] & 0xf0, value[i] & 0x0f); - } - } - return nibbles_; - } - /** * @dev Moves the content of `buffer`, from `start` (included) to the end of `buffer` to the start of that buffer. * diff --git a/test/utils/Bytes.t.sol b/test/utils/Bytes.t.sol index d6eb2fc2c1c..9fdcd47c2d1 100644 --- a/test/utils/Bytes.t.sol +++ b/test/utils/Bytes.t.sol @@ -155,19 +155,6 @@ contract BytesTest is Test { } } - function testNibbles(bytes memory value) public pure { - bytes memory result = Bytes.nibbles(value); - assertEq(result.length, value.length * 2); - for (uint256 i = 0; i < value.length; i++) { - bytes1 originalByte = value[i]; - bytes1 highNibble = result[i * 2]; - bytes1 lowNibble = result[i * 2 + 1]; - - assertEq(highNibble, originalByte & 0xf0); - assertEq(lowNibble, originalByte & 0x0f); - } - } - // REVERSE BITS function testSymbolicReverseBytes32(bytes32 value) public pure { assertEq(Bytes.reverseBytes32(Bytes.reverseBytes32(value)), value); diff --git a/test/utils/Bytes.test.js b/test/utils/Bytes.test.js index 48df5bf14db..38c2d626e17 100644 --- a/test/utils/Bytes.test.js +++ b/test/utils/Bytes.test.js @@ -112,35 +112,6 @@ describe('Bytes', function () { }); }); - describe('nibbles', function () { - it('converts single byte', async function () { - await expect(this.mock.$nibbles('0xab')).to.eventually.equal('0xa00b'); - }); - - it('converts multiple bytes', async function () { - await expect(this.mock.$nibbles('0x1234')).to.eventually.equal('0x10023004'); - }); - - it('handles empty bytes', async function () { - await expect(this.mock.$nibbles('0x')).to.eventually.equal('0x'); - }); - - it('converts lorem text', async function () { - const result = await this.mock.$nibbles(lorem); - expect(ethers.dataLength(result)).to.equal(lorem.length * 2); - - // Check nibble extraction for first few bytes - for (let i = 0; i < Math.min(lorem.length, 5); i++) { - const originalByte = lorem[i]; - const highNibble = ethers.dataSlice(result, i * 2, i * 2 + 1); - const lowNibble = ethers.dataSlice(result, i * 2 + 1, i * 2 + 2); - - expect(highNibble).to.equal(ethers.toBeHex(originalByte & 0xf0, 1)); - expect(lowNibble).to.equal(ethers.toBeHex(originalByte & 0x0f, 1)); - } - }); - }); - describe('clz bytes', function () { it('empty buffer', async function () { await expect(this.mock.$clz('0x')).to.eventually.equal(0); From 93a4eedaa6543dd926cabda9db740f15db36c5e0 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Thu, 24 Jul 2025 09:39:19 -0600 Subject: [PATCH 16/18] Update changesets --- .changeset/fast-beans-pull.md | 5 +++++ .changeset/whole-cats-find.md | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/fast-beans-pull.md diff --git a/.changeset/fast-beans-pull.md b/.changeset/fast-beans-pull.md new file mode 100644 index 00000000000..df80408d25b --- /dev/null +++ b/.changeset/fast-beans-pull.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Bytes`: Add a `clz` function to count the leading zero bits in a `bytes` buffer. diff --git a/.changeset/whole-cats-find.md b/.changeset/whole-cats-find.md index e5ba8df6e5d..cdc0b75ca72 100644 --- a/.changeset/whole-cats-find.md +++ b/.changeset/whole-cats-find.md @@ -2,4 +2,4 @@ 'openzeppelin-solidity': minor --- -`Bytes`: Add a `clz` function to count the leading zero bytes in a `uint256` value. +`Math`: Add a `clz` function to count the leading zero bits in a `uint256` value. From 82ca12bbb5aa3e617f9660eda3095636d6c083f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Thu, 24 Jul 2025 09:41:18 -0600 Subject: [PATCH 17/18] Update contracts/utils/math/Math.sol --- contracts/utils/math/Math.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/utils/math/Math.sol b/contracts/utils/math/Math.sol index 668774c6aa4..bd52cba644e 100644 --- a/contracts/utils/math/Math.sol +++ b/contracts/utils/math/Math.sol @@ -748,7 +748,7 @@ library Math { } /** - * @dev Counts the number of leading zeros bits in a uint256. + * @dev Counts the number of leading zero bits in a uint256. */ function clz(uint256 x) internal pure returns (uint256) { return ternary(x == 0, 256, 255 - log2(x)); From 56da3b57f35787d895f4fece78aa614fbaacfd1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Fri, 25 Jul 2025 07:50:36 -0600 Subject: [PATCH 18/18] Update contracts/utils/Bytes.sol Co-authored-by: Hadrien Croubois --- contracts/utils/Bytes.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/utils/Bytes.sol b/contracts/utils/Bytes.sol index a29ae931e09..2fabf4872db 100644 --- a/contracts/utils/Bytes.sol +++ b/contracts/utils/Bytes.sol @@ -188,7 +188,7 @@ library Bytes { } /** - * @dev Counts the number of leading zeros in bits a bytes array. Returns `8 * buffer.length` + * @dev Counts the number of leading zero bits a bytes array. Returns `8 * buffer.length` * if the buffer is all zeros. */ function clz(bytes memory buffer) internal pure returns (uint256) {