Skip to content

Add reverseBits operations to Bytes.sol #5724

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jul 10, 2025
Merged
5 changes: 5 additions & 0 deletions .changeset/major-feet-write.md
Original file line number Diff line number Diff line change
@@ -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.
52 changes: 52 additions & 0 deletions contracts/utils/math/Math.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
2 changes: 2 additions & 0 deletions test/helpers/constants.js
Original file line number Diff line number Diff line change
@@ -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,
};
21 changes: 21 additions & 0 deletions test/utils/math/Math.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
115 changes: 115 additions & 0 deletions test/utils/math/Math.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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);
});
});
});
});