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
---

`Bytes`: Add `reverseBits256`, `reverseBits128`, `reverseBits64`, `reverseBits32`, and `reverseBits16` functions to reverse byte order for converting between little-endian and big-endian representations.
52 changes: 52 additions & 0 deletions contracts/utils/Bytes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
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,
};
79 changes: 79 additions & 0 deletions test/utils/Bytes.t.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
124 changes: 124 additions & 0 deletions test/utils/Bytes.test.js
Original file line number Diff line number Diff line change
@@ -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');
Expand Down Expand Up @@ -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));
});
});
});
});