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 `reverseBytes32`, `reverseBytes16`, `reverseBytes8`, `reverseBytes4`, and `reverseBytes2` 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 bytes32 value, converting between little-endian and big-endian.
* Inspired in https://graphics.stanford.edu/~seander/bithacks.html#ReverseParallel[Reverse Parallel]
*/
function reverseBytes32(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 {reverseBytes32} but optimized for 128-bit values.
function reverseBytes16(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 {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 {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 {reverseBytes32} but optimized for 16-bit values.
function reverseBytes2(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,
};
74 changes: 74 additions & 0 deletions test/utils/Bytes.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {Test} from "forge-std/Test.sol";
import {Bytes} from "@openzeppelin/contracts/utils/Bytes.sol";

contract BytesTest is Test {
// REVERSE BITS
function testSymbolicReverseBytes32(bytes32 value) public pure {
assertEq(Bytes.reverseBytes32(Bytes.reverseBytes32(value)), value);
}

function testSymbolicReverseBytes16(bytes16 value) public pure {
assertEq(Bytes.reverseBytes16(Bytes.reverseBytes16(value)), value);
}

function testSymbolicReverseBytes16Dirty(bytes16 value) public pure {
assertEq(Bytes.reverseBytes16(Bytes.reverseBytes16(_dirtyBytes16(value))), value);
assertEq(Bytes.reverseBytes16(_dirtyBytes16(Bytes.reverseBytes16(value))), value);
}

function testSymbolicReverseBytes8(bytes8 value) public pure {
assertEq(Bytes.reverseBytes8(Bytes.reverseBytes8(value)), value);
}

function testSymbolicReverseBytes8Dirty(bytes8 value) public pure {
assertEq(Bytes.reverseBytes8(Bytes.reverseBytes8(_dirtyBytes8(value))), value);
assertEq(Bytes.reverseBytes8(_dirtyBytes8(Bytes.reverseBytes8(value))), value);
}

function testSymbolicReverseBytes4(bytes4 value) public pure {
assertEq(Bytes.reverseBytes4(Bytes.reverseBytes4(value)), value);
}

function testSymbolicReverseBytes4Dirty(bytes4 value) public pure {
assertEq(Bytes.reverseBytes4(Bytes.reverseBytes4(_dirtyBytes4(value))), value);
assertEq(Bytes.reverseBytes4(_dirtyBytes4(Bytes.reverseBytes4(value))), value);
}

function testSymbolicReverseBytes2(bytes2 value) public pure {
assertEq(Bytes.reverseBytes2(Bytes.reverseBytes2(value)), value);
}

function testSymbolicReverseBytes2Dirty(bytes2 value) public pure {
assertEq(Bytes.reverseBytes2(Bytes.reverseBytes2(_dirtyBytes2(value))), value);
assertEq(Bytes.reverseBytes2(_dirtyBytes2(Bytes.reverseBytes2(value))), value);
}

// Helpers
function _dirtyBytes16(bytes16 value) private pure returns (bytes16 dirty) {
assembly ("memory-safe") {
dirty := or(value, shr(128, not(0)))
}
}

function _dirtyBytes8(bytes8 value) private pure returns (bytes8 dirty) {
assembly ("memory-safe") {
dirty := or(value, shr(192, not(0)))
}
}

function _dirtyBytes4(bytes4 value) private pure returns (bytes4 dirty) {
assembly ("memory-safe") {
dirty := or(value, shr(224, not(0)))
}
}

function _dirtyBytes2(bytes2 value) private pure returns (bytes2 dirty) {
assembly ("memory-safe") {
dirty := or(value, shr(240, not(0)))
}
}
}
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('reverseBytes32', function () {
it('reverses bytes correctly', async function () {
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.$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.$reverseBytes32(bytes32(value));
await expect(this.mock.$reverseBytes32(reversed)).to.eventually.equal(bytes32(value));
}
});
});

describe('reverseBytes16', function () {
it('reverses bytes correctly', async function () {
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.$reverseBytes16('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.$reverseBytes16(bytes16(value));
// Cast back to uint128 for comparison since function returns uint256
await expect(this.mock.$reverseBytes16(reversed)).to.eventually.equal(bytes16(value & MAX_UINT128));
}
});
});

describe('reverseBytes8', function () {
it('reverses bytes correctly', async function () {
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.$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.$reverseBytes8(bytes8(value));
// Cast back to uint64 for comparison since function returns uint256
await expect(this.mock.$reverseBytes8(reversed)).to.eventually.equal(bytes8(value & MAX_UINT64));
}
});
});

describe('reverseBytes4', function () {
it('reverses bytes correctly', async function () {
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.$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.$reverseBytes4(bytes4(value));
// Cast back to uint32 for comparison since function returns uint256
await expect(this.mock.$reverseBytes4(reversed)).to.eventually.equal(bytes4(value & MAX_UINT32));
}
});
});

describe('reverseBytes2', function () {
it('reverses bytes correctly', async function () {
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.$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.$reverseBytes2(bytes2(value));
// Cast back to uint16 for comparison since function returns uint256
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.$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.$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));
});
});
});
});