From bdd2cf116eda32365aa1679c147331599dbb4967 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sun, 8 Jun 2025 19:02:49 -0600 Subject: [PATCH 01/13] Add operations to Math.sol --- .changeset/major-feet-write.md | 5 ++ contracts/utils/math/Math.sol | 52 +++++++++++++++ test/helpers/constants.js | 2 + test/utils/math/Math.t.sol | 21 ++++++ test/utils/math/Math.test.js | 115 +++++++++++++++++++++++++++++++++ 5 files changed, 195 insertions(+) create mode 100644 .changeset/major-feet-write.md diff --git a/.changeset/major-feet-write.md b/.changeset/major-feet-write.md new file mode 100644 index 00000000000..da2966f00cd --- /dev/null +++ b/.changeset/major-feet-write.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Math`: Add `reverseBitsUint256`, `reverseBitsUint128`, `reverseBitsUint64`, `reverseBitsUint32`, and `reverseBits16` functions to reverse byte order for converting between little-endian and big-endian representations. diff --git a/contracts/utils/math/Math.sol b/contracts/utils/math/Math.sol index f0d608a2dea..12546593ffe 100644 --- a/contracts/utils/math/Math.sol +++ b/contracts/utils/math/Math.sol @@ -740,6 +740,58 @@ library Math { } } + /** + * @dev Reverses the byte order of a uint256 value, converting between little-endian and big-endian. + * Inspired in https://graphics.stanford.edu/~seander/bithacks.html#ReverseParallel[Reverse Parallel] + */ + function reverseBitsUint256(uint256 value) internal pure returns (uint256) { + value = // swap bytes + ((value >> 8) & 0x00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF) | + ((value & 0x00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF) << 8); + value = // swap 2-byte long pairs + ((value >> 16) & 0x0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF) | + ((value & 0x0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF) << 16); + value = // swap 4-byte long pairs + ((value >> 32) & 0x00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF) | + ((value & 0x00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF) << 32); + value = // swap 8-byte long pairs + ((value >> 64) & 0x0000000000000000FFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF) | + ((value & 0x0000000000000000FFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF) << 64); + return (value >> 128) | (value << 128); // swap 16-byte long pairs + } + + /// @dev Same as {reverseBitsUint256} but optimized for 128-bit values. + function reverseBitsUint128(uint128 value) internal pure returns (uint256) { + value = // swap bytes + ((value & 0xFF00FF00FF00FF00FF00FF00FF00FF00) >> 8) | + ((value & 0x00FF00FF00FF00FF00FF00FF00FF00FF) << 8); + value = // swap 2-byte long pairs + ((value & 0xFFFF0000FFFF0000FFFF0000FFFF0000) >> 16) | + ((value & 0x0000FFFF0000FFFF0000FFFF0000FFFF) << 16); + value = // swap 4-byte long pairs + ((value & 0xFFFFFFFF00000000FFFFFFFF00000000) >> 32) | + ((value & 0x00000000FFFFFFFF00000000FFFFFFFF) << 32); + return (value >> 64) | (value << 64); // swap 8-byte long pairs + } + + /// @dev Same as {reverseBitsUint256} but optimized for 64-bit values. + function reverseBitsUint64(uint64 value) internal pure returns (uint256) { + value = ((value & 0xFF00FF00FF00FF00) >> 8) | ((value & 0x00FF00FF00FF00FF) << 8); // swap bytes + value = ((value & 0xFFFF0000FFFF0000) >> 16) | ((value & 0x0000FFFF0000FFFF) << 16); // swap 2-byte long pairs + return (value >> 32) | (value << 32); // swap 4-byte long pairs + } + + /// @dev Same as {reverseBitsUint256} but optimized for 32-bit values. + function reverseBitsUint32(uint32 value) internal pure returns (uint256) { + value = ((value & 0xFF00FF00) >> 8) | ((value & 0x00FF00FF) << 8); // swap bytes + return (value >> 16) | (value << 16); // swap 2-byte long pairs + } + + /// @dev Same as {reverseBitsUint256} but optimized for 16-bit values. + function reverseBits16(uint16 value) internal pure returns (uint256) { + return (value >> 8) | (value << 8); + } + /** * @dev Returns whether a provided rounding mode is considered rounding up for unsigned integers. */ diff --git a/test/helpers/constants.js b/test/helpers/constants.js index eb9b43e5549..d08c3ec0455 100644 --- a/test/helpers/constants.js +++ b/test/helpers/constants.js @@ -1,5 +1,7 @@ module.exports = { + MAX_UINT16: 2n ** 16n - 1n, MAX_UINT32: 2n ** 32n - 1n, MAX_UINT48: 2n ** 48n - 1n, MAX_UINT64: 2n ** 64n - 1n, + MAX_UINT128: 2n ** 128n - 1n, }; diff --git a/test/utils/math/Math.t.sol b/test/utils/math/Math.t.sol index 3c83febe9df..9f501b0e367 100644 --- a/test/utils/math/Math.t.sol +++ b/test/utils/math/Math.t.sol @@ -308,6 +308,27 @@ contract MathTest is Test { } } + // REVERSE BITS + function testSymbolicReverseBitsUint256(uint256 value) public pure { + assertEq(Math.reverseBitsUint256(Math.reverseBitsUint256(value)), value); + } + + function testSymbolicReverseBitsUint128(uint128 value) public pure { + assertEq(Math.reverseBitsUint128(uint128(Math.reverseBitsUint128(value))), value); + } + + function testSymbolicReverseBitsUint64(uint64 value) public pure { + assertEq(Math.reverseBitsUint64(uint64(Math.reverseBitsUint64(value))), value); + } + + function testSymbolicReverseBitsUint32(uint32 value) public pure { + assertEq(Math.reverseBitsUint32(uint32(Math.reverseBitsUint32(value))), value); + } + + function testSymbolicReverseBits16(uint16 value) public pure { + assertEq(Math.reverseBits16(uint16(Math.reverseBits16(value))), value); + } + // Helpers function _asRounding(uint8 r) private pure returns (Math.Rounding) { vm.assume(r < uint8(type(Math.Rounding).max)); diff --git a/test/utils/math/Math.test.js b/test/utils/math/Math.test.js index 6a09938148a..ce1abdd8a09 100644 --- a/test/utils/math/Math.test.js +++ b/test/utils/math/Math.test.js @@ -7,6 +7,7 @@ const { Rounding } = require('../../helpers/enums'); const { min, max, modExp } = require('../../helpers/math'); const { generators } = require('../../helpers/random'); const { product, range } = require('../../helpers/iterate'); +const { MAX_UINT128, MAX_UINT64, MAX_UINT32, MAX_UINT16 } = require('../../helpers/constants'); const RoundingDown = [Rounding.Floor, Rounding.Trunc]; const RoundingUp = [Rounding.Ceil, Rounding.Expand]; @@ -710,4 +711,118 @@ describe('Math', function () { }); }); }); + + describe('reverseBits', function () { + describe('reverseBitsUint256', function () { + it('reverses bytes correctly', async function () { + await expect(this.mock.$reverseBitsUint256(0)).to.eventually.equal(0n); + await expect(this.mock.$reverseBitsUint256(ethers.MaxUint256)).to.eventually.equal(ethers.MaxUint256); + + // Test simple pattern + await expect( + this.mock.$reverseBitsUint256('0x0000000000000000000000000000000000000000000000000000000000000001'), + ).to.eventually.equal('0x0100000000000000000000000000000000000000000000000000000000000000'); + }); + + it('double reverse returns original', async function () { + const values = [0n, 1n, 0x12345678n, ethers.MaxUint256]; + for (const value of values) { + const reversed = await this.mock.$reverseBitsUint256(value); + await expect(this.mock.$reverseBitsUint256(reversed)).to.eventually.equal(value); + } + }); + }); + + describe('reverseBitsUint128', function () { + it('reverses bytes correctly', async function () { + await expect(this.mock.$reverseBitsUint128(0)).to.eventually.equal(0n); + await expect(this.mock.$reverseBitsUint128(MAX_UINT128)).to.eventually.equal(MAX_UINT128); + + // Test simple pattern + await expect(this.mock.$reverseBitsUint128('0x00000000000000000000000000000001')).to.eventually.equal( + '0x01000000000000000000000000000000', + ); + }); + + it('double reverse returns original', async function () { + const values = [0n, 1n, 0x12345678n, MAX_UINT128]; + for (const value of values) { + const reversed = await this.mock.$reverseBitsUint128(value); + // Cast back to uint128 for comparison since function returns uint256 + await expect(this.mock.$reverseBitsUint128(reversed & MAX_UINT128)).to.eventually.equal(value); + } + }); + }); + + describe('reverseBitsUint64', function () { + it('reverses bytes correctly', async function () { + await expect(this.mock.$reverseBitsUint64(0)).to.eventually.equal(0n); + await expect(this.mock.$reverseBitsUint64(MAX_UINT64)).to.eventually.equal(MAX_UINT64); + + // Test known pattern: 0x123456789ABCDEF0 -> 0xF0DEBC9A78563412 + await expect(this.mock.$reverseBitsUint64('0x123456789ABCDEF0')).to.eventually.equal('0xF0DEBC9A78563412'); + }); + + it('double reverse returns original', async function () { + const values = [0n, 1n, 0x12345678n, MAX_UINT64]; + for (const value of values) { + const reversed = await this.mock.$reverseBitsUint64(value); + // Cast back to uint64 for comparison since function returns uint256 + await expect(this.mock.$reverseBitsUint64(reversed & MAX_UINT64)).to.eventually.equal(value); + } + }); + }); + + describe('reverseBitsUint32', function () { + it('reverses bytes correctly', async function () { + await expect(this.mock.$reverseBitsUint32(0)).to.eventually.equal(0n); + await expect(this.mock.$reverseBitsUint32(MAX_UINT32)).to.eventually.equal(MAX_UINT32); + + // Test known pattern: 0x12345678 -> 0x78563412 + await expect(this.mock.$reverseBitsUint32(0x12345678)).to.eventually.equal(0x78563412); + }); + + it('double reverse returns original', async function () { + const values = [0n, 1n, 0x12345678n, MAX_UINT32]; + for (const value of values) { + const reversed = await this.mock.$reverseBitsUint32(value); + // Cast back to uint32 for comparison since function returns uint256 + await expect(this.mock.$reverseBitsUint32(reversed & MAX_UINT32)).to.eventually.equal(value); + } + }); + }); + + describe('reverseBits16', function () { + it('reverses bytes correctly', async function () { + await expect(this.mock.$reverseBits16(0)).to.eventually.equal(0n); + await expect(this.mock.$reverseBits16(MAX_UINT16)).to.eventually.equal(MAX_UINT16); + + // Test known pattern: 0x1234 -> 0x3412 + await expect(this.mock.$reverseBits16(0x1234)).to.eventually.equal(0x3412); + }); + + it('double reverse returns original', async function () { + const values = [0n, 1n, 0x1234n, MAX_UINT16]; + for (const value of values) { + const reversed = await this.mock.$reverseBits16(value); + // Cast back to uint16 for comparison since function returns uint256 + await expect(this.mock.$reverseBits16(reversed & MAX_UINT16)).to.eventually.equal(value); + } + }); + }); + + describe('edge cases', function () { + it('handles single byte values', async function () { + await expect(this.mock.$reverseBits16(0x00ff)).to.eventually.equal(0xff00); + await expect(this.mock.$reverseBitsUint32(0x000000ff)).to.eventually.equal(0xff000000); + }); + + it('handles alternating patterns', async function () { + await expect(this.mock.$reverseBits16(0xaaaa)).to.eventually.equal(0xaaaa); + await expect(this.mock.$reverseBits16(0x5555)).to.eventually.equal(0x5555); + await expect(this.mock.$reverseBitsUint32(0xaaaaaaaa)).to.eventually.equal(0xaaaaaaaa); + await expect(this.mock.$reverseBitsUint32(0x55555555)).to.eventually.equal(0x55555555); + }); + }); + }); }); From 33857183a08d86d23dc8b2a961b260f5a8232896 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Wed, 9 Jul 2025 11:04:04 -0600 Subject: [PATCH 02/13] Rename to in Math library and update corresponding tests for consistency --- contracts/utils/math/Math.sol | 2 +- test/utils/math/Math.t.sol | 4 ++-- test/utils/math/Math.test.js | 16 ++++++++-------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/contracts/utils/math/Math.sol b/contracts/utils/math/Math.sol index 12546593ffe..d328509f31f 100644 --- a/contracts/utils/math/Math.sol +++ b/contracts/utils/math/Math.sol @@ -788,7 +788,7 @@ library Math { } /// @dev Same as {reverseBitsUint256} but optimized for 16-bit values. - function reverseBits16(uint16 value) internal pure returns (uint256) { + function reverseBitsUint16(uint16 value) internal pure returns (uint256) { return (value >> 8) | (value << 8); } diff --git a/test/utils/math/Math.t.sol b/test/utils/math/Math.t.sol index 9f501b0e367..cea4b901dee 100644 --- a/test/utils/math/Math.t.sol +++ b/test/utils/math/Math.t.sol @@ -325,8 +325,8 @@ contract MathTest is Test { assertEq(Math.reverseBitsUint32(uint32(Math.reverseBitsUint32(value))), value); } - function testSymbolicReverseBits16(uint16 value) public pure { - assertEq(Math.reverseBits16(uint16(Math.reverseBits16(value))), value); + function testSymbolicreverseBitsUint16(uint16 value) public pure { + assertEq(Math.reverseBitsUint16(uint16(Math.reverseBitsUint16(value))), value); } // Helpers diff --git a/test/utils/math/Math.test.js b/test/utils/math/Math.test.js index ce1abdd8a09..80166c59dc7 100644 --- a/test/utils/math/Math.test.js +++ b/test/utils/math/Math.test.js @@ -794,32 +794,32 @@ describe('Math', function () { describe('reverseBits16', function () { it('reverses bytes correctly', async function () { - await expect(this.mock.$reverseBits16(0)).to.eventually.equal(0n); - await expect(this.mock.$reverseBits16(MAX_UINT16)).to.eventually.equal(MAX_UINT16); + await expect(this.mock.$reverseBitsUint16(0)).to.eventually.equal(0n); + await expect(this.mock.$reverseBitsUint16(MAX_UINT16)).to.eventually.equal(MAX_UINT16); // Test known pattern: 0x1234 -> 0x3412 - await expect(this.mock.$reverseBits16(0x1234)).to.eventually.equal(0x3412); + await expect(this.mock.$reverseBitsUint16(0x1234)).to.eventually.equal(0x3412); }); it('double reverse returns original', async function () { const values = [0n, 1n, 0x1234n, MAX_UINT16]; for (const value of values) { - const reversed = await this.mock.$reverseBits16(value); + const reversed = await this.mock.$reverseBitsUint16(value); // Cast back to uint16 for comparison since function returns uint256 - await expect(this.mock.$reverseBits16(reversed & MAX_UINT16)).to.eventually.equal(value); + await expect(this.mock.$reverseBitsUint16(reversed & MAX_UINT16)).to.eventually.equal(value); } }); }); describe('edge cases', function () { it('handles single byte values', async function () { - await expect(this.mock.$reverseBits16(0x00ff)).to.eventually.equal(0xff00); + await expect(this.mock.$reverseBitsUint16(0x00ff)).to.eventually.equal(0xff00); await expect(this.mock.$reverseBitsUint32(0x000000ff)).to.eventually.equal(0xff000000); }); it('handles alternating patterns', async function () { - await expect(this.mock.$reverseBits16(0xaaaa)).to.eventually.equal(0xaaaa); - await expect(this.mock.$reverseBits16(0x5555)).to.eventually.equal(0x5555); + await expect(this.mock.$reverseBitsUint16(0xaaaa)).to.eventually.equal(0xaaaa); + await expect(this.mock.$reverseBitsUint16(0x5555)).to.eventually.equal(0x5555); await expect(this.mock.$reverseBitsUint32(0xaaaaaaaa)).to.eventually.equal(0xaaaaaaaa); await expect(this.mock.$reverseBitsUint32(0x55555555)).to.eventually.equal(0x55555555); }); From 40d7922684b065c544e6432301b53bfd4054373f Mon Sep 17 00:00:00 2001 From: ernestognw Date: Wed, 9 Jul 2025 11:08:05 -0600 Subject: [PATCH 03/13] Update return types of reverseBits functions to match their respective bit sizes --- contracts/utils/math/Math.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/utils/math/Math.sol b/contracts/utils/math/Math.sol index d328509f31f..5c25022d917 100644 --- a/contracts/utils/math/Math.sol +++ b/contracts/utils/math/Math.sol @@ -761,7 +761,7 @@ library Math { } /// @dev Same as {reverseBitsUint256} but optimized for 128-bit values. - function reverseBitsUint128(uint128 value) internal pure returns (uint256) { + function reverseBitsUint128(uint128 value) internal pure returns (uint128) { value = // swap bytes ((value & 0xFF00FF00FF00FF00FF00FF00FF00FF00) >> 8) | ((value & 0x00FF00FF00FF00FF00FF00FF00FF00FF) << 8); @@ -775,20 +775,20 @@ library Math { } /// @dev Same as {reverseBitsUint256} but optimized for 64-bit values. - function reverseBitsUint64(uint64 value) internal pure returns (uint256) { + function reverseBitsUint64(uint64 value) internal pure returns (uint64) { value = ((value & 0xFF00FF00FF00FF00) >> 8) | ((value & 0x00FF00FF00FF00FF) << 8); // swap bytes value = ((value & 0xFFFF0000FFFF0000) >> 16) | ((value & 0x0000FFFF0000FFFF) << 16); // swap 2-byte long pairs return (value >> 32) | (value << 32); // swap 4-byte long pairs } /// @dev Same as {reverseBitsUint256} but optimized for 32-bit values. - function reverseBitsUint32(uint32 value) internal pure returns (uint256) { + function reverseBitsUint32(uint32 value) internal pure returns (uint32) { value = ((value & 0xFF00FF00) >> 8) | ((value & 0x00FF00FF) << 8); // swap bytes return (value >> 16) | (value << 16); // swap 2-byte long pairs } /// @dev Same as {reverseBitsUint256} but optimized for 16-bit values. - function reverseBitsUint16(uint16 value) internal pure returns (uint256) { + function reverseBitsUint16(uint16 value) internal pure returns (uint16) { return (value >> 8) | (value << 8); } From 89860bc87f0618eac29109103f801a4880ec7085 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Wed, 9 Jul 2025 11:21:40 -0600 Subject: [PATCH 04/13] Refactor reverseBits functions in to use fixed-size byte types --- .changeset/major-feet-write.md | 2 +- contracts/utils/math/Math.sol | 18 ++++---- test/utils/math/Math.t.sol | 20 ++++----- test/utils/math/Math.test.js | 79 +++++++++++++++++++--------------- 4 files changed, 64 insertions(+), 55 deletions(-) diff --git a/.changeset/major-feet-write.md b/.changeset/major-feet-write.md index da2966f00cd..f175b7cc14b 100644 --- a/.changeset/major-feet-write.md +++ b/.changeset/major-feet-write.md @@ -2,4 +2,4 @@ 'openzeppelin-solidity': minor --- -`Math`: Add `reverseBitsUint256`, `reverseBitsUint128`, `reverseBitsUint64`, `reverseBitsUint32`, and `reverseBits16` functions to reverse byte order for converting between little-endian and big-endian representations. +`Math`: Add `reverseBits256`, `reverseBits128`, `reverseBits64`, `reverseBits32`, and `reverseBits16` functions to reverse byte order for converting between little-endian and big-endian representations. diff --git a/contracts/utils/math/Math.sol b/contracts/utils/math/Math.sol index 5c25022d917..3d49b50778e 100644 --- a/contracts/utils/math/Math.sol +++ b/contracts/utils/math/Math.sol @@ -744,7 +744,7 @@ library Math { * @dev Reverses the byte order of a uint256 value, converting between little-endian and big-endian. * Inspired in https://graphics.stanford.edu/~seander/bithacks.html#ReverseParallel[Reverse Parallel] */ - function reverseBitsUint256(uint256 value) internal pure returns (uint256) { + function reverseBits256(bytes32 value) internal pure returns (bytes32) { value = // swap bytes ((value >> 8) & 0x00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF) | ((value & 0x00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF) << 8); @@ -760,8 +760,8 @@ library Math { return (value >> 128) | (value << 128); // swap 16-byte long pairs } - /// @dev Same as {reverseBitsUint256} but optimized for 128-bit values. - function reverseBitsUint128(uint128 value) internal pure returns (uint128) { + /// @dev Same as {reverseBits256} but optimized for 128-bit values. + function reverseBits128(bytes16 value) internal pure returns (bytes16) { value = // swap bytes ((value & 0xFF00FF00FF00FF00FF00FF00FF00FF00) >> 8) | ((value & 0x00FF00FF00FF00FF00FF00FF00FF00FF) << 8); @@ -774,21 +774,21 @@ library Math { return (value >> 64) | (value << 64); // swap 8-byte long pairs } - /// @dev Same as {reverseBitsUint256} but optimized for 64-bit values. - function reverseBitsUint64(uint64 value) internal pure returns (uint64) { + /// @dev Same as {reverseBits256} but optimized for 64-bit values. + function reverseBits64(bytes8 value) internal pure returns (bytes8) { value = ((value & 0xFF00FF00FF00FF00) >> 8) | ((value & 0x00FF00FF00FF00FF) << 8); // swap bytes value = ((value & 0xFFFF0000FFFF0000) >> 16) | ((value & 0x0000FFFF0000FFFF) << 16); // swap 2-byte long pairs return (value >> 32) | (value << 32); // swap 4-byte long pairs } - /// @dev Same as {reverseBitsUint256} but optimized for 32-bit values. - function reverseBitsUint32(uint32 value) internal pure returns (uint32) { + /// @dev Same as {reverseBits256} but optimized for 32-bit values. + function reverseBits32(bytes4 value) internal pure returns (bytes4) { value = ((value & 0xFF00FF00) >> 8) | ((value & 0x00FF00FF) << 8); // swap bytes return (value >> 16) | (value << 16); // swap 2-byte long pairs } - /// @dev Same as {reverseBitsUint256} but optimized for 16-bit values. - function reverseBitsUint16(uint16 value) internal pure returns (uint16) { + /// @dev Same as {reverseBits256} but optimized for 16-bit values. + function reverseBits16(bytes2 value) internal pure returns (bytes2) { return (value >> 8) | (value << 8); } diff --git a/test/utils/math/Math.t.sol b/test/utils/math/Math.t.sol index cea4b901dee..47f74930556 100644 --- a/test/utils/math/Math.t.sol +++ b/test/utils/math/Math.t.sol @@ -309,24 +309,24 @@ contract MathTest is Test { } // REVERSE BITS - function testSymbolicReverseBitsUint256(uint256 value) public pure { - assertEq(Math.reverseBitsUint256(Math.reverseBitsUint256(value)), value); + function testSymbolicReverseBits256(bytes32 value) public pure { + assertEq(Math.reverseBits256(Math.reverseBits256(value)), value); } - function testSymbolicReverseBitsUint128(uint128 value) public pure { - assertEq(Math.reverseBitsUint128(uint128(Math.reverseBitsUint128(value))), value); + function testSymbolicReverseBits128(bytes16 value) public pure { + assertEq(Math.reverseBits128(Math.reverseBits128(value)), value); } - function testSymbolicReverseBitsUint64(uint64 value) public pure { - assertEq(Math.reverseBitsUint64(uint64(Math.reverseBitsUint64(value))), value); + function testSymbolicReverseBits64(bytes8 value) public pure { + assertEq(Math.reverseBits64(Math.reverseBits64(value)), value); } - function testSymbolicReverseBitsUint32(uint32 value) public pure { - assertEq(Math.reverseBitsUint32(uint32(Math.reverseBitsUint32(value))), value); + function testSymbolicReverseBits32(bytes4 value) public pure { + assertEq(Math.reverseBits32(Math.reverseBits32(value)), value); } - function testSymbolicreverseBitsUint16(uint16 value) public pure { - assertEq(Math.reverseBitsUint16(uint16(Math.reverseBitsUint16(value))), value); + function testSymbolicReverseBits16(bytes2 value) public pure { + assertEq(Math.reverseBits16(Math.reverseBits16(value)), value); } // Helpers diff --git a/test/utils/math/Math.test.js b/test/utils/math/Math.test.js index 80166c59dc7..3283015db01 100644 --- a/test/utils/math/Math.test.js +++ b/test/utils/math/Math.test.js @@ -17,6 +17,13 @@ const uint256 = value => ethers.Typed.uint256(value); bytes.zero = '0x'; uint256.zero = 0n; +// Helper functions for fixed bytes types +const bytes32 = value => ethers.toBeHex(value, 32); +const bytes16 = value => ethers.toBeHex(value, 16); +const bytes8 = value => ethers.toBeHex(value, 8); +const bytes4 = value => ethers.toBeHex(value, 4); +const bytes2 = value => ethers.toBeHex(value, 2); + const testCommutative = (fn, lhs, rhs, expected, ...extra) => Promise.all([ expect(fn(lhs, rhs, ...extra)).to.eventually.deep.equal(expected), @@ -713,33 +720,35 @@ describe('Math', function () { }); describe('reverseBits', function () { - describe('reverseBitsUint256', function () { + describe('reverseBits256', function () { it('reverses bytes correctly', async function () { - await expect(this.mock.$reverseBitsUint256(0)).to.eventually.equal(0n); - await expect(this.mock.$reverseBitsUint256(ethers.MaxUint256)).to.eventually.equal(ethers.MaxUint256); + await expect(this.mock.$reverseBits256(bytes32(0))).to.eventually.equal(bytes32(0)); + await expect(this.mock.$reverseBits256(bytes32(ethers.MaxUint256))).to.eventually.equal( + bytes32(ethers.MaxUint256), + ); // Test simple pattern await expect( - this.mock.$reverseBitsUint256('0x0000000000000000000000000000000000000000000000000000000000000001'), + this.mock.$reverseBits256('0x0000000000000000000000000000000000000000000000000000000000000001'), ).to.eventually.equal('0x0100000000000000000000000000000000000000000000000000000000000000'); }); it('double reverse returns original', async function () { const values = [0n, 1n, 0x12345678n, ethers.MaxUint256]; for (const value of values) { - const reversed = await this.mock.$reverseBitsUint256(value); - await expect(this.mock.$reverseBitsUint256(reversed)).to.eventually.equal(value); + const reversed = await this.mock.$reverseBits256(bytes32(value)); + await expect(this.mock.$reverseBits256(reversed)).to.eventually.equal(bytes32(value)); } }); }); - describe('reverseBitsUint128', function () { + describe('reverseBits128', function () { it('reverses bytes correctly', async function () { - await expect(this.mock.$reverseBitsUint128(0)).to.eventually.equal(0n); - await expect(this.mock.$reverseBitsUint128(MAX_UINT128)).to.eventually.equal(MAX_UINT128); + await expect(this.mock.$reverseBits128(bytes16(0))).to.eventually.equal(bytes16(0)); + await expect(this.mock.$reverseBits128(bytes16(MAX_UINT128))).to.eventually.equal(bytes16(MAX_UINT128)); // Test simple pattern - await expect(this.mock.$reverseBitsUint128('0x00000000000000000000000000000001')).to.eventually.equal( + await expect(this.mock.$reverseBits128('0x00000000000000000000000000000001')).to.eventually.equal( '0x01000000000000000000000000000000', ); }); @@ -747,81 +756,81 @@ describe('Math', function () { it('double reverse returns original', async function () { const values = [0n, 1n, 0x12345678n, MAX_UINT128]; for (const value of values) { - const reversed = await this.mock.$reverseBitsUint128(value); + const reversed = await this.mock.$reverseBits128(bytes16(value)); // Cast back to uint128 for comparison since function returns uint256 - await expect(this.mock.$reverseBitsUint128(reversed & MAX_UINT128)).to.eventually.equal(value); + await expect(this.mock.$reverseBits128(reversed)).to.eventually.equal(bytes16(value & MAX_UINT128)); } }); }); - describe('reverseBitsUint64', function () { + describe('reverseBits64', function () { it('reverses bytes correctly', async function () { - await expect(this.mock.$reverseBitsUint64(0)).to.eventually.equal(0n); - await expect(this.mock.$reverseBitsUint64(MAX_UINT64)).to.eventually.equal(MAX_UINT64); + await expect(this.mock.$reverseBits64(bytes8(0))).to.eventually.equal(bytes8(0)); + await expect(this.mock.$reverseBits64(bytes8(MAX_UINT64))).to.eventually.equal(bytes8(MAX_UINT64)); // Test known pattern: 0x123456789ABCDEF0 -> 0xF0DEBC9A78563412 - await expect(this.mock.$reverseBitsUint64('0x123456789ABCDEF0')).to.eventually.equal('0xF0DEBC9A78563412'); + await expect(this.mock.$reverseBits64('0x123456789ABCDEF0')).to.eventually.equal('0xf0debc9a78563412'); }); it('double reverse returns original', async function () { const values = [0n, 1n, 0x12345678n, MAX_UINT64]; for (const value of values) { - const reversed = await this.mock.$reverseBitsUint64(value); + const reversed = await this.mock.$reverseBits64(bytes8(value)); // Cast back to uint64 for comparison since function returns uint256 - await expect(this.mock.$reverseBitsUint64(reversed & MAX_UINT64)).to.eventually.equal(value); + await expect(this.mock.$reverseBits64(reversed)).to.eventually.equal(bytes8(value & MAX_UINT64)); } }); }); - describe('reverseBitsUint32', function () { + describe('reverseBits32', function () { it('reverses bytes correctly', async function () { - await expect(this.mock.$reverseBitsUint32(0)).to.eventually.equal(0n); - await expect(this.mock.$reverseBitsUint32(MAX_UINT32)).to.eventually.equal(MAX_UINT32); + await expect(this.mock.$reverseBits32(bytes4(0))).to.eventually.equal(bytes4(0)); + await expect(this.mock.$reverseBits32(bytes4(MAX_UINT32))).to.eventually.equal(bytes4(MAX_UINT32)); // Test known pattern: 0x12345678 -> 0x78563412 - await expect(this.mock.$reverseBitsUint32(0x12345678)).to.eventually.equal(0x78563412); + await expect(this.mock.$reverseBits32(bytes4(0x12345678))).to.eventually.equal(bytes4(0x78563412)); }); it('double reverse returns original', async function () { const values = [0n, 1n, 0x12345678n, MAX_UINT32]; for (const value of values) { - const reversed = await this.mock.$reverseBitsUint32(value); + const reversed = await this.mock.$reverseBits32(bytes4(value)); // Cast back to uint32 for comparison since function returns uint256 - await expect(this.mock.$reverseBitsUint32(reversed & MAX_UINT32)).to.eventually.equal(value); + await expect(this.mock.$reverseBits32(reversed)).to.eventually.equal(bytes4(value & MAX_UINT32)); } }); }); describe('reverseBits16', function () { it('reverses bytes correctly', async function () { - await expect(this.mock.$reverseBitsUint16(0)).to.eventually.equal(0n); - await expect(this.mock.$reverseBitsUint16(MAX_UINT16)).to.eventually.equal(MAX_UINT16); + await expect(this.mock.$reverseBits16(bytes2(0))).to.eventually.equal(bytes2(0)); + await expect(this.mock.$reverseBits16(bytes2(MAX_UINT16))).to.eventually.equal(bytes2(MAX_UINT16)); // Test known pattern: 0x1234 -> 0x3412 - await expect(this.mock.$reverseBitsUint16(0x1234)).to.eventually.equal(0x3412); + await expect(this.mock.$reverseBits16(bytes2(0x1234))).to.eventually.equal(bytes2(0x3412)); }); it('double reverse returns original', async function () { const values = [0n, 1n, 0x1234n, MAX_UINT16]; for (const value of values) { - const reversed = await this.mock.$reverseBitsUint16(value); + const reversed = await this.mock.$reverseBits16(bytes2(value)); // Cast back to uint16 for comparison since function returns uint256 - await expect(this.mock.$reverseBitsUint16(reversed & MAX_UINT16)).to.eventually.equal(value); + await expect(this.mock.$reverseBits16(reversed)).to.eventually.equal(bytes2(value & MAX_UINT16)); } }); }); describe('edge cases', function () { it('handles single byte values', async function () { - await expect(this.mock.$reverseBitsUint16(0x00ff)).to.eventually.equal(0xff00); - await expect(this.mock.$reverseBitsUint32(0x000000ff)).to.eventually.equal(0xff000000); + await expect(this.mock.$reverseBits16(bytes2(0x00ff))).to.eventually.equal(bytes2(0xff00)); + await expect(this.mock.$reverseBits32(bytes4(0x000000ff))).to.eventually.equal(bytes4(0xff000000)); }); it('handles alternating patterns', async function () { - await expect(this.mock.$reverseBitsUint16(0xaaaa)).to.eventually.equal(0xaaaa); - await expect(this.mock.$reverseBitsUint16(0x5555)).to.eventually.equal(0x5555); - await expect(this.mock.$reverseBitsUint32(0xaaaaaaaa)).to.eventually.equal(0xaaaaaaaa); - await expect(this.mock.$reverseBitsUint32(0x55555555)).to.eventually.equal(0x55555555); + await expect(this.mock.$reverseBits16(bytes2(0xaaaa))).to.eventually.equal(bytes2(0xaaaa)); + await expect(this.mock.$reverseBits16(bytes2(0x5555))).to.eventually.equal(bytes2(0x5555)); + await expect(this.mock.$reverseBits32(bytes4(0xaaaaaaaa))).to.eventually.equal(bytes4(0xaaaaaaaa)); + await expect(this.mock.$reverseBits32(bytes4(0x55555555))).to.eventually.equal(bytes4(0x55555555)); }); }); }); From 9b58730aa21682f15e5d4b409f6e936116c34f6d Mon Sep 17 00:00:00 2001 From: ernestognw Date: Wed, 9 Jul 2025 11:32:34 -0600 Subject: [PATCH 05/13] Test nits --- test/utils/math/Math.t.sol | 52 ++++++++++++++++++++++++++++++++++++ test/utils/math/Math.test.js | 14 +++++----- 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/test/utils/math/Math.t.sol b/test/utils/math/Math.t.sol index 47f74930556..3ba58b06959 100644 --- a/test/utils/math/Math.t.sol +++ b/test/utils/math/Math.t.sol @@ -317,19 +317,71 @@ contract MathTest is Test { assertEq(Math.reverseBits128(Math.reverseBits128(value)), value); } + function testSymbolicReverseBits128Dirty(bytes16 value) public pure { + bytes16 dirty = _dirtyBytes128(value); + assertEq(Math.reverseBits128(Math.reverseBits128(dirty)), value); + } + function testSymbolicReverseBits64(bytes8 value) public pure { assertEq(Math.reverseBits64(Math.reverseBits64(value)), value); } + function testSymbolicReverseBits64Dirty(bytes8 value) public pure { + bytes8 dirty = _dirtyBytes64(value); + assertEq(Math.reverseBits64(Math.reverseBits64(dirty)), value); + } + function testSymbolicReverseBits32(bytes4 value) public pure { assertEq(Math.reverseBits32(Math.reverseBits32(value)), value); } + function testSymbolicReverseBits32Dirty(bytes4 value) public pure { + bytes4 dirty = _dirtyBytes32(value); + assertEq(Math.reverseBits32(Math.reverseBits32(dirty)), value); + } + function testSymbolicReverseBits16(bytes2 value) public pure { assertEq(Math.reverseBits16(Math.reverseBits16(value)), value); } + function testSymbolicReverseBits16Dirty(bytes2 value) public pure { + bytes2 dirty = _dirtyBytes16(value); + assertEq(Math.reverseBits16(Math.reverseBits16(dirty)), value); + } + // Helpers + function _dirtyBytes128(bytes16 value) private pure returns (bytes16) { + bytes16 dirty = value; + assembly ("memory-safe") { + dirty := or(dirty, shr(128, not(0))) + } + return dirty; + } + + function _dirtyBytes64(bytes8 value) private pure returns (bytes8) { + bytes8 dirty = value; + assembly ("memory-safe") { + dirty := or(dirty, shr(64, not(0))) + } + return dirty; + } + + function _dirtyBytes32(bytes4 value) private pure returns (bytes4) { + bytes4 dirty = value; + assembly ("memory-safe") { + dirty := or(dirty, shr(32, not(0))) + } + return dirty; + } + + function _dirtyBytes16(bytes2 value) private pure returns (bytes2) { + bytes2 dirty = value; + assembly ("memory-safe") { + dirty := or(dirty, shr(16, not(0))) + } + return dirty; + } + function _asRounding(uint8 r) private pure returns (Math.Rounding) { vm.assume(r < uint8(type(Math.Rounding).max)); return Math.Rounding(r); diff --git a/test/utils/math/Math.test.js b/test/utils/math/Math.test.js index 3283015db01..4cd0eb7a44d 100644 --- a/test/utils/math/Math.test.js +++ b/test/utils/math/Math.test.js @@ -727,10 +727,10 @@ describe('Math', function () { bytes32(ethers.MaxUint256), ); - // Test simple pattern + // Test complex pattern that clearly shows byte reversal await expect( - this.mock.$reverseBits256('0x0000000000000000000000000000000000000000000000000000000000000001'), - ).to.eventually.equal('0x0100000000000000000000000000000000000000000000000000000000000000'); + this.mock.$reverseBits256('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'), + ).to.eventually.equal('0xefcdab8967452301efcdab8967452301efcdab8967452301efcdab8967452301'); }); it('double reverse returns original', async function () { @@ -747,9 +747,9 @@ describe('Math', function () { await expect(this.mock.$reverseBits128(bytes16(0))).to.eventually.equal(bytes16(0)); await expect(this.mock.$reverseBits128(bytes16(MAX_UINT128))).to.eventually.equal(bytes16(MAX_UINT128)); - // Test simple pattern - await expect(this.mock.$reverseBits128('0x00000000000000000000000000000001')).to.eventually.equal( - '0x01000000000000000000000000000000', + // Test complex pattern that clearly shows byte reversal + await expect(this.mock.$reverseBits128('0x0123456789abcdef0123456789abcdef')).to.eventually.equal( + '0xefcdab8967452301efcdab8967452301', ); }); @@ -769,7 +769,7 @@ describe('Math', function () { await expect(this.mock.$reverseBits64(bytes8(MAX_UINT64))).to.eventually.equal(bytes8(MAX_UINT64)); // Test known pattern: 0x123456789ABCDEF0 -> 0xF0DEBC9A78563412 - await expect(this.mock.$reverseBits64('0x123456789ABCDEF0')).to.eventually.equal('0xf0debc9a78563412'); + await expect(this.mock.$reverseBits64('0x123456789abcdef0')).to.eventually.equal('0xf0debc9a78563412'); }); it('double reverse returns original', async function () { From 77ffa8ce90c3c9796beaadea5c6300ebec2d0475 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Wed, 9 Jul 2025 11:33:24 -0600 Subject: [PATCH 06/13] Simplify --- test/utils/math/Math.t.sol | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/test/utils/math/Math.t.sol b/test/utils/math/Math.t.sol index 3ba58b06959..fdcb4111d42 100644 --- a/test/utils/math/Math.t.sol +++ b/test/utils/math/Math.t.sol @@ -318,8 +318,7 @@ contract MathTest is Test { } function testSymbolicReverseBits128Dirty(bytes16 value) public pure { - bytes16 dirty = _dirtyBytes128(value); - assertEq(Math.reverseBits128(Math.reverseBits128(dirty)), value); + assertEq(Math.reverseBits128(Math.reverseBits128(_dirtyBytes128(value))), value); } function testSymbolicReverseBits64(bytes8 value) public pure { @@ -327,8 +326,7 @@ contract MathTest is Test { } function testSymbolicReverseBits64Dirty(bytes8 value) public pure { - bytes8 dirty = _dirtyBytes64(value); - assertEq(Math.reverseBits64(Math.reverseBits64(dirty)), value); + assertEq(Math.reverseBits64(Math.reverseBits64(_dirtyBytes64(value))), value); } function testSymbolicReverseBits32(bytes4 value) public pure { @@ -336,8 +334,7 @@ contract MathTest is Test { } function testSymbolicReverseBits32Dirty(bytes4 value) public pure { - bytes4 dirty = _dirtyBytes32(value); - assertEq(Math.reverseBits32(Math.reverseBits32(dirty)), value); + assertEq(Math.reverseBits32(Math.reverseBits32(_dirtyBytes32(value))), value); } function testSymbolicReverseBits16(bytes2 value) public pure { @@ -345,8 +342,7 @@ contract MathTest is Test { } function testSymbolicReverseBits16Dirty(bytes2 value) public pure { - bytes2 dirty = _dirtyBytes16(value); - assertEq(Math.reverseBits16(Math.reverseBits16(dirty)), value); + assertEq(Math.reverseBits16(Math.reverseBits16(_dirtyBytes16(value))), value); } // Helpers From ce91c8098f99b8181c3dcdcb5975fb0f45b4e116 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Wed, 9 Jul 2025 11:34:07 -0600 Subject: [PATCH 07/13] up --- test/utils/math/Math.t.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/utils/math/Math.t.sol b/test/utils/math/Math.t.sol index fdcb4111d42..c0ac024ed05 100644 --- a/test/utils/math/Math.t.sol +++ b/test/utils/math/Math.t.sol @@ -357,7 +357,7 @@ contract MathTest is Test { function _dirtyBytes64(bytes8 value) private pure returns (bytes8) { bytes8 dirty = value; assembly ("memory-safe") { - dirty := or(dirty, shr(64, not(0))) + dirty := or(dirty, shr(192, not(0))) } return dirty; } @@ -365,7 +365,7 @@ contract MathTest is Test { function _dirtyBytes32(bytes4 value) private pure returns (bytes4) { bytes4 dirty = value; assembly ("memory-safe") { - dirty := or(dirty, shr(32, not(0))) + dirty := or(dirty, shr(224, not(0))) } return dirty; } @@ -373,7 +373,7 @@ contract MathTest is Test { function _dirtyBytes16(bytes2 value) private pure returns (bytes2) { bytes2 dirty = value; assembly ("memory-safe") { - dirty := or(dirty, shr(16, not(0))) + dirty := or(dirty, shr(240, not(0))) } return dirty; } From b3e3adde2cbaa45f9961e8f589cd8901c694f50e Mon Sep 17 00:00:00 2001 From: ernestognw Date: Wed, 9 Jul 2025 11:40:08 -0600 Subject: [PATCH 08/13] Move reverse functions to Bytes.sol --- .changeset/major-feet-write.md | 2 +- contracts/utils/Bytes.sol | 52 ++++++++++++++ contracts/utils/math/Math.sol | 52 -------------- test/utils/Bytes.test.js | 124 +++++++++++++++++++++++++++++++++ test/utils/math/Bytes.t.sol | 79 +++++++++++++++++++++ test/utils/math/Math.t.sol | 69 ------------------ test/utils/math/Math.test.js | 124 --------------------------------- 7 files changed, 256 insertions(+), 246 deletions(-) create mode 100644 test/utils/math/Bytes.t.sol diff --git a/.changeset/major-feet-write.md b/.changeset/major-feet-write.md index f175b7cc14b..81897219735 100644 --- a/.changeset/major-feet-write.md +++ b/.changeset/major-feet-write.md @@ -2,4 +2,4 @@ 'openzeppelin-solidity': minor --- -`Math`: Add `reverseBits256`, `reverseBits128`, `reverseBits64`, `reverseBits32`, and `reverseBits16` functions to reverse byte order for converting between little-endian and big-endian representations. +`Bytes`: Add `reverseBits256`, `reverseBits128`, `reverseBits64`, `reverseBits32`, and `reverseBits16` functions to reverse byte order for converting between little-endian and big-endian representations. diff --git a/contracts/utils/Bytes.sol b/contracts/utils/Bytes.sol index 1234b845513..fa024cc2f2a 100644 --- a/contracts/utils/Bytes.sol +++ b/contracts/utils/Bytes.sol @@ -99,6 +99,58 @@ library Bytes { return result; } + /** + * @dev Reverses the byte order of a uint256 value, converting between little-endian and big-endian. + * Inspired in https://graphics.stanford.edu/~seander/bithacks.html#ReverseParallel[Reverse Parallel] + */ + function reverseBits256(bytes32 value) internal pure returns (bytes32) { + value = // swap bytes + ((value >> 8) & 0x00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF) | + ((value & 0x00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF) << 8); + value = // swap 2-byte long pairs + ((value >> 16) & 0x0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF) | + ((value & 0x0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF) << 16); + value = // swap 4-byte long pairs + ((value >> 32) & 0x00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF) | + ((value & 0x00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF) << 32); + value = // swap 8-byte long pairs + ((value >> 64) & 0x0000000000000000FFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF) | + ((value & 0x0000000000000000FFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF) << 64); + return (value >> 128) | (value << 128); // swap 16-byte long pairs + } + + /// @dev Same as {reverseBits256} but optimized for 128-bit values. + function reverseBits128(bytes16 value) internal pure returns (bytes16) { + value = // swap bytes + ((value & 0xFF00FF00FF00FF00FF00FF00FF00FF00) >> 8) | + ((value & 0x00FF00FF00FF00FF00FF00FF00FF00FF) << 8); + value = // swap 2-byte long pairs + ((value & 0xFFFF0000FFFF0000FFFF0000FFFF0000) >> 16) | + ((value & 0x0000FFFF0000FFFF0000FFFF0000FFFF) << 16); + value = // swap 4-byte long pairs + ((value & 0xFFFFFFFF00000000FFFFFFFF00000000) >> 32) | + ((value & 0x00000000FFFFFFFF00000000FFFFFFFF) << 32); + return (value >> 64) | (value << 64); // swap 8-byte long pairs + } + + /// @dev Same as {reverseBits256} but optimized for 64-bit values. + function reverseBits64(bytes8 value) internal pure returns (bytes8) { + value = ((value & 0xFF00FF00FF00FF00) >> 8) | ((value & 0x00FF00FF00FF00FF) << 8); // swap bytes + value = ((value & 0xFFFF0000FFFF0000) >> 16) | ((value & 0x0000FFFF0000FFFF) << 16); // swap 2-byte long pairs + return (value >> 32) | (value << 32); // swap 4-byte long pairs + } + + /// @dev Same as {reverseBits256} but optimized for 32-bit values. + function reverseBits32(bytes4 value) internal pure returns (bytes4) { + value = ((value & 0xFF00FF00) >> 8) | ((value & 0x00FF00FF) << 8); // swap bytes + return (value >> 16) | (value << 16); // swap 2-byte long pairs + } + + /// @dev Same as {reverseBits256} but optimized for 16-bit values. + function reverseBits16(bytes2 value) internal pure returns (bytes2) { + return (value >> 8) | (value << 8); + } + /** * @dev Reads a bytes32 from a bytes array without bounds checking. * diff --git a/contracts/utils/math/Math.sol b/contracts/utils/math/Math.sol index 3d49b50778e..f0d608a2dea 100644 --- a/contracts/utils/math/Math.sol +++ b/contracts/utils/math/Math.sol @@ -740,58 +740,6 @@ library Math { } } - /** - * @dev Reverses the byte order of a uint256 value, converting between little-endian and big-endian. - * Inspired in https://graphics.stanford.edu/~seander/bithacks.html#ReverseParallel[Reverse Parallel] - */ - function reverseBits256(bytes32 value) internal pure returns (bytes32) { - value = // swap bytes - ((value >> 8) & 0x00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF) | - ((value & 0x00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF) << 8); - value = // swap 2-byte long pairs - ((value >> 16) & 0x0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF) | - ((value & 0x0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF) << 16); - value = // swap 4-byte long pairs - ((value >> 32) & 0x00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF) | - ((value & 0x00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF) << 32); - value = // swap 8-byte long pairs - ((value >> 64) & 0x0000000000000000FFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF) | - ((value & 0x0000000000000000FFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF) << 64); - return (value >> 128) | (value << 128); // swap 16-byte long pairs - } - - /// @dev Same as {reverseBits256} but optimized for 128-bit values. - function reverseBits128(bytes16 value) internal pure returns (bytes16) { - value = // swap bytes - ((value & 0xFF00FF00FF00FF00FF00FF00FF00FF00) >> 8) | - ((value & 0x00FF00FF00FF00FF00FF00FF00FF00FF) << 8); - value = // swap 2-byte long pairs - ((value & 0xFFFF0000FFFF0000FFFF0000FFFF0000) >> 16) | - ((value & 0x0000FFFF0000FFFF0000FFFF0000FFFF) << 16); - value = // swap 4-byte long pairs - ((value & 0xFFFFFFFF00000000FFFFFFFF00000000) >> 32) | - ((value & 0x00000000FFFFFFFF00000000FFFFFFFF) << 32); - return (value >> 64) | (value << 64); // swap 8-byte long pairs - } - - /// @dev Same as {reverseBits256} but optimized for 64-bit values. - function reverseBits64(bytes8 value) internal pure returns (bytes8) { - value = ((value & 0xFF00FF00FF00FF00) >> 8) | ((value & 0x00FF00FF00FF00FF) << 8); // swap bytes - value = ((value & 0xFFFF0000FFFF0000) >> 16) | ((value & 0x0000FFFF0000FFFF) << 16); // swap 2-byte long pairs - return (value >> 32) | (value << 32); // swap 4-byte long pairs - } - - /// @dev Same as {reverseBits256} but optimized for 32-bit values. - function reverseBits32(bytes4 value) internal pure returns (bytes4) { - value = ((value & 0xFF00FF00) >> 8) | ((value & 0x00FF00FF) << 8); // swap bytes - return (value >> 16) | (value << 16); // swap 2-byte long pairs - } - - /// @dev Same as {reverseBits256} but optimized for 16-bit values. - function reverseBits16(bytes2 value) internal pure returns (bytes2) { - return (value >> 8) | (value << 8); - } - /** * @dev Returns whether a provided rounding mode is considered rounding up for unsigned integers. */ diff --git a/test/utils/Bytes.test.js b/test/utils/Bytes.test.js index 52a1ae95e77..1728f2213bb 100644 --- a/test/utils/Bytes.test.js +++ b/test/utils/Bytes.test.js @@ -1,6 +1,14 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { MAX_UINT128, MAX_UINT64, MAX_UINT32, MAX_UINT16 } = require('../helpers/constants'); + +// Helper functions for fixed bytes types +const bytes32 = value => ethers.toBeHex(value, 32); +const bytes16 = value => ethers.toBeHex(value, 16); +const bytes8 = value => ethers.toBeHex(value, 8); +const bytes4 = value => ethers.toBeHex(value, 4); +const bytes2 = value => ethers.toBeHex(value, 2); async function fixture() { const mock = await ethers.deployContract('$Bytes'); @@ -85,4 +93,120 @@ describe('Bytes', function () { } }); }); + + describe('reverseBits', function () { + describe('reverseBits256', function () { + it('reverses bytes correctly', async function () { + await expect(this.mock.$reverseBits256(bytes32(0))).to.eventually.equal(bytes32(0)); + await expect(this.mock.$reverseBits256(bytes32(ethers.MaxUint256))).to.eventually.equal( + bytes32(ethers.MaxUint256), + ); + + // Test complex pattern that clearly shows byte reversal + await expect( + this.mock.$reverseBits256('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'), + ).to.eventually.equal('0xefcdab8967452301efcdab8967452301efcdab8967452301efcdab8967452301'); + }); + + it('double reverse returns original', async function () { + const values = [0n, 1n, 0x12345678n, ethers.MaxUint256]; + for (const value of values) { + const reversed = await this.mock.$reverseBits256(bytes32(value)); + await expect(this.mock.$reverseBits256(reversed)).to.eventually.equal(bytes32(value)); + } + }); + }); + + describe('reverseBits128', function () { + it('reverses bytes correctly', async function () { + await expect(this.mock.$reverseBits128(bytes16(0))).to.eventually.equal(bytes16(0)); + await expect(this.mock.$reverseBits128(bytes16(MAX_UINT128))).to.eventually.equal(bytes16(MAX_UINT128)); + + // Test complex pattern that clearly shows byte reversal + await expect(this.mock.$reverseBits128('0x0123456789abcdef0123456789abcdef')).to.eventually.equal( + '0xefcdab8967452301efcdab8967452301', + ); + }); + + it('double reverse returns original', async function () { + const values = [0n, 1n, 0x12345678n, MAX_UINT128]; + for (const value of values) { + const reversed = await this.mock.$reverseBits128(bytes16(value)); + // Cast back to uint128 for comparison since function returns uint256 + await expect(this.mock.$reverseBits128(reversed)).to.eventually.equal(bytes16(value & MAX_UINT128)); + } + }); + }); + + describe('reverseBits64', function () { + it('reverses bytes correctly', async function () { + await expect(this.mock.$reverseBits64(bytes8(0))).to.eventually.equal(bytes8(0)); + await expect(this.mock.$reverseBits64(bytes8(MAX_UINT64))).to.eventually.equal(bytes8(MAX_UINT64)); + + // Test known pattern: 0x123456789ABCDEF0 -> 0xF0DEBC9A78563412 + await expect(this.mock.$reverseBits64('0x123456789abcdef0')).to.eventually.equal('0xf0debc9a78563412'); + }); + + it('double reverse returns original', async function () { + const values = [0n, 1n, 0x12345678n, MAX_UINT64]; + for (const value of values) { + const reversed = await this.mock.$reverseBits64(bytes8(value)); + // Cast back to uint64 for comparison since function returns uint256 + await expect(this.mock.$reverseBits64(reversed)).to.eventually.equal(bytes8(value & MAX_UINT64)); + } + }); + }); + + describe('reverseBits32', function () { + it('reverses bytes correctly', async function () { + await expect(this.mock.$reverseBits32(bytes4(0))).to.eventually.equal(bytes4(0)); + await expect(this.mock.$reverseBits32(bytes4(MAX_UINT32))).to.eventually.equal(bytes4(MAX_UINT32)); + + // Test known pattern: 0x12345678 -> 0x78563412 + await expect(this.mock.$reverseBits32(bytes4(0x12345678))).to.eventually.equal(bytes4(0x78563412)); + }); + + it('double reverse returns original', async function () { + const values = [0n, 1n, 0x12345678n, MAX_UINT32]; + for (const value of values) { + const reversed = await this.mock.$reverseBits32(bytes4(value)); + // Cast back to uint32 for comparison since function returns uint256 + await expect(this.mock.$reverseBits32(reversed)).to.eventually.equal(bytes4(value & MAX_UINT32)); + } + }); + }); + + describe('reverseBits16', function () { + it('reverses bytes correctly', async function () { + await expect(this.mock.$reverseBits16(bytes2(0))).to.eventually.equal(bytes2(0)); + await expect(this.mock.$reverseBits16(bytes2(MAX_UINT16))).to.eventually.equal(bytes2(MAX_UINT16)); + + // Test known pattern: 0x1234 -> 0x3412 + await expect(this.mock.$reverseBits16(bytes2(0x1234))).to.eventually.equal(bytes2(0x3412)); + }); + + it('double reverse returns original', async function () { + const values = [0n, 1n, 0x1234n, MAX_UINT16]; + for (const value of values) { + const reversed = await this.mock.$reverseBits16(bytes2(value)); + // Cast back to uint16 for comparison since function returns uint256 + await expect(this.mock.$reverseBits16(reversed)).to.eventually.equal(bytes2(value & MAX_UINT16)); + } + }); + }); + + describe('edge cases', function () { + it('handles single byte values', async function () { + await expect(this.mock.$reverseBits16(bytes2(0x00ff))).to.eventually.equal(bytes2(0xff00)); + await expect(this.mock.$reverseBits32(bytes4(0x000000ff))).to.eventually.equal(bytes4(0xff000000)); + }); + + it('handles alternating patterns', async function () { + await expect(this.mock.$reverseBits16(bytes2(0xaaaa))).to.eventually.equal(bytes2(0xaaaa)); + await expect(this.mock.$reverseBits16(bytes2(0x5555))).to.eventually.equal(bytes2(0x5555)); + await expect(this.mock.$reverseBits32(bytes4(0xaaaaaaaa))).to.eventually.equal(bytes4(0xaaaaaaaa)); + await expect(this.mock.$reverseBits32(bytes4(0x55555555))).to.eventually.equal(bytes4(0x55555555)); + }); + }); + }); }); diff --git a/test/utils/math/Bytes.t.sol b/test/utils/math/Bytes.t.sol new file mode 100644 index 00000000000..7b4f5acd87d --- /dev/null +++ b/test/utils/math/Bytes.t.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Test, stdError} from "forge-std/Test.sol"; + +import {Bytes} from "@openzeppelin/contracts/utils/Bytes.sol"; + +contract BytesTest is Test { + // REVERSE BITS + function testSymbolicReverseBits256(bytes32 value) public pure { + assertEq(Bytes.reverseBits256(Bytes.reverseBits256(value)), value); + } + + function testSymbolicReverseBits128(bytes16 value) public pure { + assertEq(Bytes.reverseBits128(Bytes.reverseBits128(value)), value); + } + + function testSymbolicReverseBits128Dirty(bytes16 value) public pure { + assertEq(Bytes.reverseBits128(Bytes.reverseBits128(_dirtyBytes128(value))), value); + } + + function testSymbolicReverseBits64(bytes8 value) public pure { + assertEq(Bytes.reverseBits64(Bytes.reverseBits64(value)), value); + } + + function testSymbolicReverseBits64Dirty(bytes8 value) public pure { + assertEq(Bytes.reverseBits64(Bytes.reverseBits64(_dirtyBytes64(value))), value); + } + + function testSymbolicReverseBits32(bytes4 value) public pure { + assertEq(Bytes.reverseBits32(Bytes.reverseBits32(value)), value); + } + + function testSymbolicReverseBits32Dirty(bytes4 value) public pure { + assertEq(Bytes.reverseBits32(Bytes.reverseBits32(_dirtyBytes32(value))), value); + } + + function testSymbolicReverseBits16(bytes2 value) public pure { + assertEq(Bytes.reverseBits16(Bytes.reverseBits16(value)), value); + } + + function testSymbolicReverseBits16Dirty(bytes2 value) public pure { + assertEq(Bytes.reverseBits16(Bytes.reverseBits16(_dirtyBytes16(value))), value); + } + + // Helpers + function _dirtyBytes128(bytes16 value) private pure returns (bytes16) { + bytes16 dirty = value; + assembly ("memory-safe") { + dirty := or(dirty, shr(128, not(0))) + } + return dirty; + } + + function _dirtyBytes64(bytes8 value) private pure returns (bytes8) { + bytes8 dirty = value; + assembly ("memory-safe") { + dirty := or(dirty, shr(192, not(0))) + } + return dirty; + } + + function _dirtyBytes32(bytes4 value) private pure returns (bytes4) { + bytes4 dirty = value; + assembly ("memory-safe") { + dirty := or(dirty, shr(224, not(0))) + } + return dirty; + } + + function _dirtyBytes16(bytes2 value) private pure returns (bytes2) { + bytes2 dirty = value; + assembly ("memory-safe") { + dirty := or(dirty, shr(240, not(0))) + } + return dirty; + } +} diff --git a/test/utils/math/Math.t.sol b/test/utils/math/Math.t.sol index c0ac024ed05..3c83febe9df 100644 --- a/test/utils/math/Math.t.sol +++ b/test/utils/math/Math.t.sol @@ -308,76 +308,7 @@ contract MathTest is Test { } } - // REVERSE BITS - function testSymbolicReverseBits256(bytes32 value) public pure { - assertEq(Math.reverseBits256(Math.reverseBits256(value)), value); - } - - function testSymbolicReverseBits128(bytes16 value) public pure { - assertEq(Math.reverseBits128(Math.reverseBits128(value)), value); - } - - function testSymbolicReverseBits128Dirty(bytes16 value) public pure { - assertEq(Math.reverseBits128(Math.reverseBits128(_dirtyBytes128(value))), value); - } - - function testSymbolicReverseBits64(bytes8 value) public pure { - assertEq(Math.reverseBits64(Math.reverseBits64(value)), value); - } - - function testSymbolicReverseBits64Dirty(bytes8 value) public pure { - assertEq(Math.reverseBits64(Math.reverseBits64(_dirtyBytes64(value))), value); - } - - function testSymbolicReverseBits32(bytes4 value) public pure { - assertEq(Math.reverseBits32(Math.reverseBits32(value)), value); - } - - function testSymbolicReverseBits32Dirty(bytes4 value) public pure { - assertEq(Math.reverseBits32(Math.reverseBits32(_dirtyBytes32(value))), value); - } - - function testSymbolicReverseBits16(bytes2 value) public pure { - assertEq(Math.reverseBits16(Math.reverseBits16(value)), value); - } - - function testSymbolicReverseBits16Dirty(bytes2 value) public pure { - assertEq(Math.reverseBits16(Math.reverseBits16(_dirtyBytes16(value))), value); - } - // Helpers - function _dirtyBytes128(bytes16 value) private pure returns (bytes16) { - bytes16 dirty = value; - assembly ("memory-safe") { - dirty := or(dirty, shr(128, not(0))) - } - return dirty; - } - - function _dirtyBytes64(bytes8 value) private pure returns (bytes8) { - bytes8 dirty = value; - assembly ("memory-safe") { - dirty := or(dirty, shr(192, not(0))) - } - return dirty; - } - - function _dirtyBytes32(bytes4 value) private pure returns (bytes4) { - bytes4 dirty = value; - assembly ("memory-safe") { - dirty := or(dirty, shr(224, not(0))) - } - return dirty; - } - - function _dirtyBytes16(bytes2 value) private pure returns (bytes2) { - bytes2 dirty = value; - assembly ("memory-safe") { - dirty := or(dirty, shr(240, not(0))) - } - return dirty; - } - function _asRounding(uint8 r) private pure returns (Math.Rounding) { vm.assume(r < uint8(type(Math.Rounding).max)); return Math.Rounding(r); diff --git a/test/utils/math/Math.test.js b/test/utils/math/Math.test.js index 4cd0eb7a44d..6a09938148a 100644 --- a/test/utils/math/Math.test.js +++ b/test/utils/math/Math.test.js @@ -7,7 +7,6 @@ const { Rounding } = require('../../helpers/enums'); const { min, max, modExp } = require('../../helpers/math'); const { generators } = require('../../helpers/random'); const { product, range } = require('../../helpers/iterate'); -const { MAX_UINT128, MAX_UINT64, MAX_UINT32, MAX_UINT16 } = require('../../helpers/constants'); const RoundingDown = [Rounding.Floor, Rounding.Trunc]; const RoundingUp = [Rounding.Ceil, Rounding.Expand]; @@ -17,13 +16,6 @@ const uint256 = value => ethers.Typed.uint256(value); bytes.zero = '0x'; uint256.zero = 0n; -// Helper functions for fixed bytes types -const bytes32 = value => ethers.toBeHex(value, 32); -const bytes16 = value => ethers.toBeHex(value, 16); -const bytes8 = value => ethers.toBeHex(value, 8); -const bytes4 = value => ethers.toBeHex(value, 4); -const bytes2 = value => ethers.toBeHex(value, 2); - const testCommutative = (fn, lhs, rhs, expected, ...extra) => Promise.all([ expect(fn(lhs, rhs, ...extra)).to.eventually.deep.equal(expected), @@ -718,120 +710,4 @@ describe('Math', function () { }); }); }); - - describe('reverseBits', function () { - describe('reverseBits256', function () { - it('reverses bytes correctly', async function () { - await expect(this.mock.$reverseBits256(bytes32(0))).to.eventually.equal(bytes32(0)); - await expect(this.mock.$reverseBits256(bytes32(ethers.MaxUint256))).to.eventually.equal( - bytes32(ethers.MaxUint256), - ); - - // Test complex pattern that clearly shows byte reversal - await expect( - this.mock.$reverseBits256('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'), - ).to.eventually.equal('0xefcdab8967452301efcdab8967452301efcdab8967452301efcdab8967452301'); - }); - - it('double reverse returns original', async function () { - const values = [0n, 1n, 0x12345678n, ethers.MaxUint256]; - for (const value of values) { - const reversed = await this.mock.$reverseBits256(bytes32(value)); - await expect(this.mock.$reverseBits256(reversed)).to.eventually.equal(bytes32(value)); - } - }); - }); - - describe('reverseBits128', function () { - it('reverses bytes correctly', async function () { - await expect(this.mock.$reverseBits128(bytes16(0))).to.eventually.equal(bytes16(0)); - await expect(this.mock.$reverseBits128(bytes16(MAX_UINT128))).to.eventually.equal(bytes16(MAX_UINT128)); - - // Test complex pattern that clearly shows byte reversal - await expect(this.mock.$reverseBits128('0x0123456789abcdef0123456789abcdef')).to.eventually.equal( - '0xefcdab8967452301efcdab8967452301', - ); - }); - - it('double reverse returns original', async function () { - const values = [0n, 1n, 0x12345678n, MAX_UINT128]; - for (const value of values) { - const reversed = await this.mock.$reverseBits128(bytes16(value)); - // Cast back to uint128 for comparison since function returns uint256 - await expect(this.mock.$reverseBits128(reversed)).to.eventually.equal(bytes16(value & MAX_UINT128)); - } - }); - }); - - describe('reverseBits64', function () { - it('reverses bytes correctly', async function () { - await expect(this.mock.$reverseBits64(bytes8(0))).to.eventually.equal(bytes8(0)); - await expect(this.mock.$reverseBits64(bytes8(MAX_UINT64))).to.eventually.equal(bytes8(MAX_UINT64)); - - // Test known pattern: 0x123456789ABCDEF0 -> 0xF0DEBC9A78563412 - await expect(this.mock.$reverseBits64('0x123456789abcdef0')).to.eventually.equal('0xf0debc9a78563412'); - }); - - it('double reverse returns original', async function () { - const values = [0n, 1n, 0x12345678n, MAX_UINT64]; - for (const value of values) { - const reversed = await this.mock.$reverseBits64(bytes8(value)); - // Cast back to uint64 for comparison since function returns uint256 - await expect(this.mock.$reverseBits64(reversed)).to.eventually.equal(bytes8(value & MAX_UINT64)); - } - }); - }); - - describe('reverseBits32', function () { - it('reverses bytes correctly', async function () { - await expect(this.mock.$reverseBits32(bytes4(0))).to.eventually.equal(bytes4(0)); - await expect(this.mock.$reverseBits32(bytes4(MAX_UINT32))).to.eventually.equal(bytes4(MAX_UINT32)); - - // Test known pattern: 0x12345678 -> 0x78563412 - await expect(this.mock.$reverseBits32(bytes4(0x12345678))).to.eventually.equal(bytes4(0x78563412)); - }); - - it('double reverse returns original', async function () { - const values = [0n, 1n, 0x12345678n, MAX_UINT32]; - for (const value of values) { - const reversed = await this.mock.$reverseBits32(bytes4(value)); - // Cast back to uint32 for comparison since function returns uint256 - await expect(this.mock.$reverseBits32(reversed)).to.eventually.equal(bytes4(value & MAX_UINT32)); - } - }); - }); - - describe('reverseBits16', function () { - it('reverses bytes correctly', async function () { - await expect(this.mock.$reverseBits16(bytes2(0))).to.eventually.equal(bytes2(0)); - await expect(this.mock.$reverseBits16(bytes2(MAX_UINT16))).to.eventually.equal(bytes2(MAX_UINT16)); - - // Test known pattern: 0x1234 -> 0x3412 - await expect(this.mock.$reverseBits16(bytes2(0x1234))).to.eventually.equal(bytes2(0x3412)); - }); - - it('double reverse returns original', async function () { - const values = [0n, 1n, 0x1234n, MAX_UINT16]; - for (const value of values) { - const reversed = await this.mock.$reverseBits16(bytes2(value)); - // Cast back to uint16 for comparison since function returns uint256 - await expect(this.mock.$reverseBits16(reversed)).to.eventually.equal(bytes2(value & MAX_UINT16)); - } - }); - }); - - describe('edge cases', function () { - it('handles single byte values', async function () { - await expect(this.mock.$reverseBits16(bytes2(0x00ff))).to.eventually.equal(bytes2(0xff00)); - await expect(this.mock.$reverseBits32(bytes4(0x000000ff))).to.eventually.equal(bytes4(0xff000000)); - }); - - it('handles alternating patterns', async function () { - await expect(this.mock.$reverseBits16(bytes2(0xaaaa))).to.eventually.equal(bytes2(0xaaaa)); - await expect(this.mock.$reverseBits16(bytes2(0x5555))).to.eventually.equal(bytes2(0x5555)); - await expect(this.mock.$reverseBits32(bytes4(0xaaaaaaaa))).to.eventually.equal(bytes4(0xaaaaaaaa)); - await expect(this.mock.$reverseBits32(bytes4(0x55555555))).to.eventually.equal(bytes4(0x55555555)); - }); - }); - }); }); From 2f3107cd44082b49a1811a4a3ea8b267a4ee0fc0 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Wed, 9 Jul 2025 11:40:43 -0600 Subject: [PATCH 09/13] Move Bytes.t.sol --- test/utils/{math => }/Bytes.t.sol | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/utils/{math => }/Bytes.t.sol (100%) diff --git a/test/utils/math/Bytes.t.sol b/test/utils/Bytes.t.sol similarity index 100% rename from test/utils/math/Bytes.t.sol rename to test/utils/Bytes.t.sol From a50d8b9de3d5c097e13a0a20167da7e6a73e08d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Wed, 9 Jul 2025 13:21:59 -0600 Subject: [PATCH 10/13] 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 fa024cc2f2a..1c81a93bf50 100644 --- a/contracts/utils/Bytes.sol +++ b/contracts/utils/Bytes.sol @@ -100,7 +100,7 @@ library Bytes { } /** - * @dev Reverses the byte order of a uint256 value, converting between little-endian and big-endian. + * @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] */ function reverseBits256(bytes32 value) internal pure returns (bytes32) { From 4957157f3a4e705ea0b8f9cdfd4ca64b4284288d Mon Sep 17 00:00:00 2001 From: ernestognw Date: Wed, 9 Jul 2025 14:15:10 -0600 Subject: [PATCH 11/13] Rename reverseBits functions to reverseBytes --- .changeset/major-feet-write.md | 2 +- contracts/utils/Bytes.sol | 18 ++++----- test/utils/Bytes.t.sol | 36 ++++++++--------- test/utils/Bytes.test.js | 72 +++++++++++++++++----------------- 4 files changed, 64 insertions(+), 64 deletions(-) diff --git a/.changeset/major-feet-write.md b/.changeset/major-feet-write.md index 81897219735..ebae3abef8f 100644 --- a/.changeset/major-feet-write.md +++ b/.changeset/major-feet-write.md @@ -2,4 +2,4 @@ 'openzeppelin-solidity': minor --- -`Bytes`: Add `reverseBits256`, `reverseBits128`, `reverseBits64`, `reverseBits32`, and `reverseBits16` functions to reverse byte order for converting between little-endian and big-endian representations. +`Bytes`: Add `reverseBytes32`, `reverseBytes16`, `reverseBytes8`, `reverseBytes4`, and `reverseBytes2` functions to reverse byte order for converting between little-endian and big-endian representations. diff --git a/contracts/utils/Bytes.sol b/contracts/utils/Bytes.sol index 1c81a93bf50..1f38bce88e0 100644 --- a/contracts/utils/Bytes.sol +++ b/contracts/utils/Bytes.sol @@ -103,7 +103,7 @@ library Bytes { * @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] */ - function reverseBits256(bytes32 value) internal pure returns (bytes32) { + function reverseBytes32(bytes32 value) internal pure returns (bytes32) { value = // swap bytes ((value >> 8) & 0x00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF) | ((value & 0x00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF) << 8); @@ -119,8 +119,8 @@ library Bytes { return (value >> 128) | (value << 128); // swap 16-byte long pairs } - /// @dev Same as {reverseBits256} but optimized for 128-bit values. - function reverseBits128(bytes16 value) internal pure returns (bytes16) { + /// @dev Same as {reverseBytes32} but optimized for 128-bit values. + function reverseBytes16(bytes16 value) internal pure returns (bytes16) { value = // swap bytes ((value & 0xFF00FF00FF00FF00FF00FF00FF00FF00) >> 8) | ((value & 0x00FF00FF00FF00FF00FF00FF00FF00FF) << 8); @@ -133,21 +133,21 @@ library Bytes { return (value >> 64) | (value << 64); // swap 8-byte long pairs } - /// @dev Same as {reverseBits256} but optimized for 64-bit values. - function reverseBits64(bytes8 value) internal pure returns (bytes8) { + /// @dev Same as {reverseBytes32} but optimized for 64-bit values. + function reverseBytes8(bytes8 value) internal pure returns (bytes8) { value = ((value & 0xFF00FF00FF00FF00) >> 8) | ((value & 0x00FF00FF00FF00FF) << 8); // swap bytes value = ((value & 0xFFFF0000FFFF0000) >> 16) | ((value & 0x0000FFFF0000FFFF) << 16); // swap 2-byte long pairs return (value >> 32) | (value << 32); // swap 4-byte long pairs } - /// @dev Same as {reverseBits256} but optimized for 32-bit values. - function reverseBits32(bytes4 value) internal pure returns (bytes4) { + /// @dev Same as {reverseBytes32} but optimized for 32-bit values. + function reverseBytes4(bytes4 value) internal pure returns (bytes4) { value = ((value & 0xFF00FF00) >> 8) | ((value & 0x00FF00FF) << 8); // swap bytes return (value >> 16) | (value << 16); // swap 2-byte long pairs } - /// @dev Same as {reverseBits256} but optimized for 16-bit values. - function reverseBits16(bytes2 value) internal pure returns (bytes2) { + /// @dev Same as {reverseBytes32} but optimized for 16-bit values. + function reverseBytes2(bytes2 value) internal pure returns (bytes2) { return (value >> 8) | (value << 8); } diff --git a/test/utils/Bytes.t.sol b/test/utils/Bytes.t.sol index 7b4f5acd87d..b476b92e806 100644 --- a/test/utils/Bytes.t.sol +++ b/test/utils/Bytes.t.sol @@ -8,40 +8,40 @@ import {Bytes} from "@openzeppelin/contracts/utils/Bytes.sol"; contract BytesTest is Test { // REVERSE BITS - function testSymbolicReverseBits256(bytes32 value) public pure { - assertEq(Bytes.reverseBits256(Bytes.reverseBits256(value)), value); + function testSymbolicReverseBytes32(bytes32 value) public pure { + assertEq(Bytes.reverseBytes32(Bytes.reverseBytes32(value)), value); } - function testSymbolicReverseBits128(bytes16 value) public pure { - assertEq(Bytes.reverseBits128(Bytes.reverseBits128(value)), value); + function testSymbolicReverseBytes16(bytes16 value) public pure { + assertEq(Bytes.reverseBytes16(Bytes.reverseBytes16(value)), value); } - function testSymbolicReverseBits128Dirty(bytes16 value) public pure { - assertEq(Bytes.reverseBits128(Bytes.reverseBits128(_dirtyBytes128(value))), value); + function testSymbolicReverseBytes16Dirty(bytes16 value) public pure { + assertEq(Bytes.reverseBytes16(Bytes.reverseBytes16(_dirtyBytes128(value))), value); } - function testSymbolicReverseBits64(bytes8 value) public pure { - assertEq(Bytes.reverseBits64(Bytes.reverseBits64(value)), value); + function testSymbolicReverseBytes8(bytes8 value) public pure { + assertEq(Bytes.reverseBytes8(Bytes.reverseBytes8(value)), value); } - function testSymbolicReverseBits64Dirty(bytes8 value) public pure { - assertEq(Bytes.reverseBits64(Bytes.reverseBits64(_dirtyBytes64(value))), value); + function testSymbolicReverseBytes8Dirty(bytes8 value) public pure { + assertEq(Bytes.reverseBytes8(Bytes.reverseBytes8(_dirtyBytes64(value))), value); } - function testSymbolicReverseBits32(bytes4 value) public pure { - assertEq(Bytes.reverseBits32(Bytes.reverseBits32(value)), value); + function testSymbolicReverseBytes4(bytes4 value) public pure { + assertEq(Bytes.reverseBytes4(Bytes.reverseBytes4(value)), value); } - function testSymbolicReverseBits32Dirty(bytes4 value) public pure { - assertEq(Bytes.reverseBits32(Bytes.reverseBits32(_dirtyBytes32(value))), value); + function testSymbolicReverseBytes4Dirty(bytes4 value) public pure { + assertEq(Bytes.reverseBytes4(Bytes.reverseBytes4(_dirtyBytes32(value))), value); } - function testSymbolicReverseBits16(bytes2 value) public pure { - assertEq(Bytes.reverseBits16(Bytes.reverseBits16(value)), value); + function testSymbolicReverseBytes2(bytes2 value) public pure { + assertEq(Bytes.reverseBytes2(Bytes.reverseBytes2(value)), value); } - function testSymbolicReverseBits16Dirty(bytes2 value) public pure { - assertEq(Bytes.reverseBits16(Bytes.reverseBits16(_dirtyBytes16(value))), value); + function testSymbolicReverseBytes2Dirty(bytes2 value) public pure { + assertEq(Bytes.reverseBytes2(Bytes.reverseBytes2(_dirtyBytes16(value))), value); } // Helpers diff --git a/test/utils/Bytes.test.js b/test/utils/Bytes.test.js index 1728f2213bb..def131e12b7 100644 --- a/test/utils/Bytes.test.js +++ b/test/utils/Bytes.test.js @@ -95,35 +95,35 @@ describe('Bytes', function () { }); describe('reverseBits', function () { - describe('reverseBits256', function () { + describe('reverseBytes32', function () { it('reverses bytes correctly', async function () { - await expect(this.mock.$reverseBits256(bytes32(0))).to.eventually.equal(bytes32(0)); - await expect(this.mock.$reverseBits256(bytes32(ethers.MaxUint256))).to.eventually.equal( + await expect(this.mock.$reverseBytes32(bytes32(0))).to.eventually.equal(bytes32(0)); + await expect(this.mock.$reverseBytes32(bytes32(ethers.MaxUint256))).to.eventually.equal( bytes32(ethers.MaxUint256), ); // Test complex pattern that clearly shows byte reversal await expect( - this.mock.$reverseBits256('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'), + this.mock.$reverseBytes32('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'), ).to.eventually.equal('0xefcdab8967452301efcdab8967452301efcdab8967452301efcdab8967452301'); }); it('double reverse returns original', async function () { const values = [0n, 1n, 0x12345678n, ethers.MaxUint256]; for (const value of values) { - const reversed = await this.mock.$reverseBits256(bytes32(value)); - await expect(this.mock.$reverseBits256(reversed)).to.eventually.equal(bytes32(value)); + const reversed = await this.mock.$reverseBytes32(bytes32(value)); + await expect(this.mock.$reverseBytes32(reversed)).to.eventually.equal(bytes32(value)); } }); }); - describe('reverseBits128', function () { + describe('reverseBytes16', function () { it('reverses bytes correctly', async function () { - await expect(this.mock.$reverseBits128(bytes16(0))).to.eventually.equal(bytes16(0)); - await expect(this.mock.$reverseBits128(bytes16(MAX_UINT128))).to.eventually.equal(bytes16(MAX_UINT128)); + await expect(this.mock.$reverseBytes16(bytes16(0))).to.eventually.equal(bytes16(0)); + await expect(this.mock.$reverseBytes16(bytes16(MAX_UINT128))).to.eventually.equal(bytes16(MAX_UINT128)); // Test complex pattern that clearly shows byte reversal - await expect(this.mock.$reverseBits128('0x0123456789abcdef0123456789abcdef')).to.eventually.equal( + await expect(this.mock.$reverseBytes16('0x0123456789abcdef0123456789abcdef')).to.eventually.equal( '0xefcdab8967452301efcdab8967452301', ); }); @@ -131,81 +131,81 @@ describe('Bytes', function () { it('double reverse returns original', async function () { const values = [0n, 1n, 0x12345678n, MAX_UINT128]; for (const value of values) { - const reversed = await this.mock.$reverseBits128(bytes16(value)); + const reversed = await this.mock.$reverseBytes16(bytes16(value)); // Cast back to uint128 for comparison since function returns uint256 - await expect(this.mock.$reverseBits128(reversed)).to.eventually.equal(bytes16(value & MAX_UINT128)); + await expect(this.mock.$reverseBytes16(reversed)).to.eventually.equal(bytes16(value & MAX_UINT128)); } }); }); - describe('reverseBits64', function () { + describe('reverseBytes8', function () { it('reverses bytes correctly', async function () { - await expect(this.mock.$reverseBits64(bytes8(0))).to.eventually.equal(bytes8(0)); - await expect(this.mock.$reverseBits64(bytes8(MAX_UINT64))).to.eventually.equal(bytes8(MAX_UINT64)); + await expect(this.mock.$reverseBytes8(bytes8(0))).to.eventually.equal(bytes8(0)); + await expect(this.mock.$reverseBytes8(bytes8(MAX_UINT64))).to.eventually.equal(bytes8(MAX_UINT64)); // Test known pattern: 0x123456789ABCDEF0 -> 0xF0DEBC9A78563412 - await expect(this.mock.$reverseBits64('0x123456789abcdef0')).to.eventually.equal('0xf0debc9a78563412'); + await expect(this.mock.$reverseBytes8('0x123456789abcdef0')).to.eventually.equal('0xf0debc9a78563412'); }); it('double reverse returns original', async function () { const values = [0n, 1n, 0x12345678n, MAX_UINT64]; for (const value of values) { - const reversed = await this.mock.$reverseBits64(bytes8(value)); + const reversed = await this.mock.$reverseBytes8(bytes8(value)); // Cast back to uint64 for comparison since function returns uint256 - await expect(this.mock.$reverseBits64(reversed)).to.eventually.equal(bytes8(value & MAX_UINT64)); + await expect(this.mock.$reverseBytes8(reversed)).to.eventually.equal(bytes8(value & MAX_UINT64)); } }); }); - describe('reverseBits32', function () { + describe('reverseBytes4', function () { it('reverses bytes correctly', async function () { - await expect(this.mock.$reverseBits32(bytes4(0))).to.eventually.equal(bytes4(0)); - await expect(this.mock.$reverseBits32(bytes4(MAX_UINT32))).to.eventually.equal(bytes4(MAX_UINT32)); + await expect(this.mock.$reverseBytes4(bytes4(0))).to.eventually.equal(bytes4(0)); + await expect(this.mock.$reverseBytes4(bytes4(MAX_UINT32))).to.eventually.equal(bytes4(MAX_UINT32)); // Test known pattern: 0x12345678 -> 0x78563412 - await expect(this.mock.$reverseBits32(bytes4(0x12345678))).to.eventually.equal(bytes4(0x78563412)); + await expect(this.mock.$reverseBytes4(bytes4(0x12345678))).to.eventually.equal(bytes4(0x78563412)); }); it('double reverse returns original', async function () { const values = [0n, 1n, 0x12345678n, MAX_UINT32]; for (const value of values) { - const reversed = await this.mock.$reverseBits32(bytes4(value)); + const reversed = await this.mock.$reverseBytes4(bytes4(value)); // Cast back to uint32 for comparison since function returns uint256 - await expect(this.mock.$reverseBits32(reversed)).to.eventually.equal(bytes4(value & MAX_UINT32)); + await expect(this.mock.$reverseBytes4(reversed)).to.eventually.equal(bytes4(value & MAX_UINT32)); } }); }); - describe('reverseBits16', function () { + describe('reverseBytes2', function () { it('reverses bytes correctly', async function () { - await expect(this.mock.$reverseBits16(bytes2(0))).to.eventually.equal(bytes2(0)); - await expect(this.mock.$reverseBits16(bytes2(MAX_UINT16))).to.eventually.equal(bytes2(MAX_UINT16)); + await expect(this.mock.$reverseBytes2(bytes2(0))).to.eventually.equal(bytes2(0)); + await expect(this.mock.$reverseBytes2(bytes2(MAX_UINT16))).to.eventually.equal(bytes2(MAX_UINT16)); // Test known pattern: 0x1234 -> 0x3412 - await expect(this.mock.$reverseBits16(bytes2(0x1234))).to.eventually.equal(bytes2(0x3412)); + await expect(this.mock.$reverseBytes2(bytes2(0x1234))).to.eventually.equal(bytes2(0x3412)); }); it('double reverse returns original', async function () { const values = [0n, 1n, 0x1234n, MAX_UINT16]; for (const value of values) { - const reversed = await this.mock.$reverseBits16(bytes2(value)); + const reversed = await this.mock.$reverseBytes2(bytes2(value)); // Cast back to uint16 for comparison since function returns uint256 - await expect(this.mock.$reverseBits16(reversed)).to.eventually.equal(bytes2(value & MAX_UINT16)); + await expect(this.mock.$reverseBytes2(reversed)).to.eventually.equal(bytes2(value & MAX_UINT16)); } }); }); describe('edge cases', function () { it('handles single byte values', async function () { - await expect(this.mock.$reverseBits16(bytes2(0x00ff))).to.eventually.equal(bytes2(0xff00)); - await expect(this.mock.$reverseBits32(bytes4(0x000000ff))).to.eventually.equal(bytes4(0xff000000)); + await expect(this.mock.$reverseBytes2(bytes2(0x00ff))).to.eventually.equal(bytes2(0xff00)); + await expect(this.mock.$reverseBytes4(bytes4(0x000000ff))).to.eventually.equal(bytes4(0xff000000)); }); it('handles alternating patterns', async function () { - await expect(this.mock.$reverseBits16(bytes2(0xaaaa))).to.eventually.equal(bytes2(0xaaaa)); - await expect(this.mock.$reverseBits16(bytes2(0x5555))).to.eventually.equal(bytes2(0x5555)); - await expect(this.mock.$reverseBits32(bytes4(0xaaaaaaaa))).to.eventually.equal(bytes4(0xaaaaaaaa)); - await expect(this.mock.$reverseBits32(bytes4(0x55555555))).to.eventually.equal(bytes4(0x55555555)); + await expect(this.mock.$reverseBytes2(bytes2(0xaaaa))).to.eventually.equal(bytes2(0xaaaa)); + await expect(this.mock.$reverseBytes2(bytes2(0x5555))).to.eventually.equal(bytes2(0x5555)); + await expect(this.mock.$reverseBytes4(bytes4(0xaaaaaaaa))).to.eventually.equal(bytes4(0xaaaaaaaa)); + await expect(this.mock.$reverseBytes4(bytes4(0x55555555))).to.eventually.equal(bytes4(0x55555555)); }); }); }); From ebcb2e569a33b5b0d970bff04ede7c916075f103 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 10 Jul 2025 13:55:36 +0200 Subject: [PATCH 12/13] Update Bytes.t.sol --- test/utils/Bytes.t.sol | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/test/utils/Bytes.t.sol b/test/utils/Bytes.t.sol index b476b92e806..d64e7182131 100644 --- a/test/utils/Bytes.t.sol +++ b/test/utils/Bytes.t.sol @@ -17,7 +17,8 @@ contract BytesTest is Test { } function testSymbolicReverseBytes16Dirty(bytes16 value) public pure { - assertEq(Bytes.reverseBytes16(Bytes.reverseBytes16(_dirtyBytes128(value))), value); + assertEq(Bytes.reverseBytes16(Bytes.reverseBytes16(_dirtyBytes16(value))), value); + assertEq(Bytes.reverseBytes16(_dirtyBytes16(Bytes.reverseBytes16(value))), value); } function testSymbolicReverseBytes8(bytes8 value) public pure { @@ -25,7 +26,8 @@ contract BytesTest is Test { } function testSymbolicReverseBytes8Dirty(bytes8 value) public pure { - assertEq(Bytes.reverseBytes8(Bytes.reverseBytes8(_dirtyBytes64(value))), value); + assertEq(Bytes.reverseBytes8(Bytes.reverseBytes8(_dirtyBytes8(value))), value); + assertEq(Bytes.reverseBytes8(_dirtyBytes8(Bytes.reverseBytes8(value))), value); } function testSymbolicReverseBytes4(bytes4 value) public pure { @@ -33,7 +35,8 @@ contract BytesTest is Test { } function testSymbolicReverseBytes4Dirty(bytes4 value) public pure { - assertEq(Bytes.reverseBytes4(Bytes.reverseBytes4(_dirtyBytes32(value))), value); + assertEq(Bytes.reverseBytes4(Bytes.reverseBytes4(_dirtyBytes4(value))), value); + assertEq(Bytes.reverseBytes4(_dirtyBytes4(Bytes.reverseBytes4(value))), value); } function testSymbolicReverseBytes2(bytes2 value) public pure { @@ -41,39 +44,32 @@ contract BytesTest is Test { } function testSymbolicReverseBytes2Dirty(bytes2 value) public pure { - assertEq(Bytes.reverseBytes2(Bytes.reverseBytes2(_dirtyBytes16(value))), value); + assertEq(Bytes.reverseBytes2(Bytes.reverseBytes2(_dirtyBytes2(value))), value); + assertEq(Bytes.reverseBytes2(_dirtyBytes2(Bytes.reverseBytes2(value))), value); } // Helpers - function _dirtyBytes128(bytes16 value) private pure returns (bytes16) { - bytes16 dirty = value; + function _dirtyBytes16(bytes16 value) private pure returns (bytes16 dirty) { assembly ("memory-safe") { - dirty := or(dirty, shr(128, not(0))) + dirty := or(value, shr(128, not(0))) } - return dirty; } - function _dirtyBytes64(bytes8 value) private pure returns (bytes8) { - bytes8 dirty = value; + function _dirtyBytes8(bytes8 value) private pure returns (bytes8 dirty) { assembly ("memory-safe") { - dirty := or(dirty, shr(192, not(0))) + dirty := or(value, shr(192, not(0))) } - return dirty; } - function _dirtyBytes32(bytes4 value) private pure returns (bytes4) { - bytes4 dirty = value; + function _dirtyBytes4(bytes4 value) private pure returns (bytes4 dirty) { assembly ("memory-safe") { - dirty := or(dirty, shr(224, not(0))) + dirty := or(value, shr(224, not(0))) } - return dirty; } - function _dirtyBytes16(bytes2 value) private pure returns (bytes2) { - bytes2 dirty = value; + function _dirtyBytes2(bytes2 value) private pure returns (bytes2 dirty) { assembly ("memory-safe") { - dirty := or(dirty, shr(240, not(0))) + dirty := or(value, shr(240, not(0))) } - return dirty; } } From fff6ad26ba3d708d0436829578e52e62ae415008 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 10 Jul 2025 13:56:18 +0200 Subject: [PATCH 13/13] Update Bytes.t.sol --- test/utils/Bytes.t.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/utils/Bytes.t.sol b/test/utils/Bytes.t.sol index d64e7182131..20d5cb0014f 100644 --- a/test/utils/Bytes.t.sol +++ b/test/utils/Bytes.t.sol @@ -2,8 +2,7 @@ pragma solidity ^0.8.20; -import {Test, stdError} from "forge-std/Test.sol"; - +import {Test} from "forge-std/Test.sol"; import {Bytes} from "@openzeppelin/contracts/utils/Bytes.sol"; contract BytesTest is Test {