Skip to content

Commit 5def3f7

Browse files
ernestognwAmxx
andauthored
Add reverseBits operations to Bytes.sol (#5724)
Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com>
1 parent 21cd7e8 commit 5def3f7

File tree

5 files changed

+257
-0
lines changed

5 files changed

+257
-0
lines changed

.changeset/major-feet-write.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': minor
3+
---
4+
5+
`Bytes`: Add `reverseBytes32`, `reverseBytes16`, `reverseBytes8`, `reverseBytes4`, and `reverseBytes2` functions to reverse byte order for converting between little-endian and big-endian representations.

contracts/utils/Bytes.sol

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,58 @@ library Bytes {
9999
return result;
100100
}
101101

102+
/**
103+
* @dev Reverses the byte order of a bytes32 value, converting between little-endian and big-endian.
104+
* Inspired in https://graphics.stanford.edu/~seander/bithacks.html#ReverseParallel[Reverse Parallel]
105+
*/
106+
function reverseBytes32(bytes32 value) internal pure returns (bytes32) {
107+
value = // swap bytes
108+
((value >> 8) & 0x00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF) |
109+
((value & 0x00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF) << 8);
110+
value = // swap 2-byte long pairs
111+
((value >> 16) & 0x0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF) |
112+
((value & 0x0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF) << 16);
113+
value = // swap 4-byte long pairs
114+
((value >> 32) & 0x00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF) |
115+
((value & 0x00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF) << 32);
116+
value = // swap 8-byte long pairs
117+
((value >> 64) & 0x0000000000000000FFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF) |
118+
((value & 0x0000000000000000FFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF) << 64);
119+
return (value >> 128) | (value << 128); // swap 16-byte long pairs
120+
}
121+
122+
/// @dev Same as {reverseBytes32} but optimized for 128-bit values.
123+
function reverseBytes16(bytes16 value) internal pure returns (bytes16) {
124+
value = // swap bytes
125+
((value & 0xFF00FF00FF00FF00FF00FF00FF00FF00) >> 8) |
126+
((value & 0x00FF00FF00FF00FF00FF00FF00FF00FF) << 8);
127+
value = // swap 2-byte long pairs
128+
((value & 0xFFFF0000FFFF0000FFFF0000FFFF0000) >> 16) |
129+
((value & 0x0000FFFF0000FFFF0000FFFF0000FFFF) << 16);
130+
value = // swap 4-byte long pairs
131+
((value & 0xFFFFFFFF00000000FFFFFFFF00000000) >> 32) |
132+
((value & 0x00000000FFFFFFFF00000000FFFFFFFF) << 32);
133+
return (value >> 64) | (value << 64); // swap 8-byte long pairs
134+
}
135+
136+
/// @dev Same as {reverseBytes32} but optimized for 64-bit values.
137+
function reverseBytes8(bytes8 value) internal pure returns (bytes8) {
138+
value = ((value & 0xFF00FF00FF00FF00) >> 8) | ((value & 0x00FF00FF00FF00FF) << 8); // swap bytes
139+
value = ((value & 0xFFFF0000FFFF0000) >> 16) | ((value & 0x0000FFFF0000FFFF) << 16); // swap 2-byte long pairs
140+
return (value >> 32) | (value << 32); // swap 4-byte long pairs
141+
}
142+
143+
/// @dev Same as {reverseBytes32} but optimized for 32-bit values.
144+
function reverseBytes4(bytes4 value) internal pure returns (bytes4) {
145+
value = ((value & 0xFF00FF00) >> 8) | ((value & 0x00FF00FF) << 8); // swap bytes
146+
return (value >> 16) | (value << 16); // swap 2-byte long pairs
147+
}
148+
149+
/// @dev Same as {reverseBytes32} but optimized for 16-bit values.
150+
function reverseBytes2(bytes2 value) internal pure returns (bytes2) {
151+
return (value >> 8) | (value << 8);
152+
}
153+
102154
/**
103155
* @dev Reads a bytes32 from a bytes array without bounds checking.
104156
*

test/helpers/constants.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
module.exports = {
2+
MAX_UINT16: 2n ** 16n - 1n,
23
MAX_UINT32: 2n ** 32n - 1n,
34
MAX_UINT48: 2n ** 48n - 1n,
45
MAX_UINT64: 2n ** 64n - 1n,
6+
MAX_UINT128: 2n ** 128n - 1n,
57
};

test/utils/Bytes.t.sol

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {Test} from "forge-std/Test.sol";
6+
import {Bytes} from "@openzeppelin/contracts/utils/Bytes.sol";
7+
8+
contract BytesTest is Test {
9+
// REVERSE BITS
10+
function testSymbolicReverseBytes32(bytes32 value) public pure {
11+
assertEq(Bytes.reverseBytes32(Bytes.reverseBytes32(value)), value);
12+
}
13+
14+
function testSymbolicReverseBytes16(bytes16 value) public pure {
15+
assertEq(Bytes.reverseBytes16(Bytes.reverseBytes16(value)), value);
16+
}
17+
18+
function testSymbolicReverseBytes16Dirty(bytes16 value) public pure {
19+
assertEq(Bytes.reverseBytes16(Bytes.reverseBytes16(_dirtyBytes16(value))), value);
20+
assertEq(Bytes.reverseBytes16(_dirtyBytes16(Bytes.reverseBytes16(value))), value);
21+
}
22+
23+
function testSymbolicReverseBytes8(bytes8 value) public pure {
24+
assertEq(Bytes.reverseBytes8(Bytes.reverseBytes8(value)), value);
25+
}
26+
27+
function testSymbolicReverseBytes8Dirty(bytes8 value) public pure {
28+
assertEq(Bytes.reverseBytes8(Bytes.reverseBytes8(_dirtyBytes8(value))), value);
29+
assertEq(Bytes.reverseBytes8(_dirtyBytes8(Bytes.reverseBytes8(value))), value);
30+
}
31+
32+
function testSymbolicReverseBytes4(bytes4 value) public pure {
33+
assertEq(Bytes.reverseBytes4(Bytes.reverseBytes4(value)), value);
34+
}
35+
36+
function testSymbolicReverseBytes4Dirty(bytes4 value) public pure {
37+
assertEq(Bytes.reverseBytes4(Bytes.reverseBytes4(_dirtyBytes4(value))), value);
38+
assertEq(Bytes.reverseBytes4(_dirtyBytes4(Bytes.reverseBytes4(value))), value);
39+
}
40+
41+
function testSymbolicReverseBytes2(bytes2 value) public pure {
42+
assertEq(Bytes.reverseBytes2(Bytes.reverseBytes2(value)), value);
43+
}
44+
45+
function testSymbolicReverseBytes2Dirty(bytes2 value) public pure {
46+
assertEq(Bytes.reverseBytes2(Bytes.reverseBytes2(_dirtyBytes2(value))), value);
47+
assertEq(Bytes.reverseBytes2(_dirtyBytes2(Bytes.reverseBytes2(value))), value);
48+
}
49+
50+
// Helpers
51+
function _dirtyBytes16(bytes16 value) private pure returns (bytes16 dirty) {
52+
assembly ("memory-safe") {
53+
dirty := or(value, shr(128, not(0)))
54+
}
55+
}
56+
57+
function _dirtyBytes8(bytes8 value) private pure returns (bytes8 dirty) {
58+
assembly ("memory-safe") {
59+
dirty := or(value, shr(192, not(0)))
60+
}
61+
}
62+
63+
function _dirtyBytes4(bytes4 value) private pure returns (bytes4 dirty) {
64+
assembly ("memory-safe") {
65+
dirty := or(value, shr(224, not(0)))
66+
}
67+
}
68+
69+
function _dirtyBytes2(bytes2 value) private pure returns (bytes2 dirty) {
70+
assembly ("memory-safe") {
71+
dirty := or(value, shr(240, not(0)))
72+
}
73+
}
74+
}

test/utils/Bytes.test.js

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
const { ethers } = require('hardhat');
22
const { expect } = require('chai');
33
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
4+
const { MAX_UINT128, MAX_UINT64, MAX_UINT32, MAX_UINT16 } = require('../helpers/constants');
5+
6+
// Helper functions for fixed bytes types
7+
const bytes32 = value => ethers.toBeHex(value, 32);
8+
const bytes16 = value => ethers.toBeHex(value, 16);
9+
const bytes8 = value => ethers.toBeHex(value, 8);
10+
const bytes4 = value => ethers.toBeHex(value, 4);
11+
const bytes2 = value => ethers.toBeHex(value, 2);
412

513
async function fixture() {
614
const mock = await ethers.deployContract('$Bytes');
@@ -85,4 +93,120 @@ describe('Bytes', function () {
8593
}
8694
});
8795
});
96+
97+
describe('reverseBits', function () {
98+
describe('reverseBytes32', function () {
99+
it('reverses bytes correctly', async function () {
100+
await expect(this.mock.$reverseBytes32(bytes32(0))).to.eventually.equal(bytes32(0));
101+
await expect(this.mock.$reverseBytes32(bytes32(ethers.MaxUint256))).to.eventually.equal(
102+
bytes32(ethers.MaxUint256),
103+
);
104+
105+
// Test complex pattern that clearly shows byte reversal
106+
await expect(
107+
this.mock.$reverseBytes32('0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'),
108+
).to.eventually.equal('0xefcdab8967452301efcdab8967452301efcdab8967452301efcdab8967452301');
109+
});
110+
111+
it('double reverse returns original', async function () {
112+
const values = [0n, 1n, 0x12345678n, ethers.MaxUint256];
113+
for (const value of values) {
114+
const reversed = await this.mock.$reverseBytes32(bytes32(value));
115+
await expect(this.mock.$reverseBytes32(reversed)).to.eventually.equal(bytes32(value));
116+
}
117+
});
118+
});
119+
120+
describe('reverseBytes16', function () {
121+
it('reverses bytes correctly', async function () {
122+
await expect(this.mock.$reverseBytes16(bytes16(0))).to.eventually.equal(bytes16(0));
123+
await expect(this.mock.$reverseBytes16(bytes16(MAX_UINT128))).to.eventually.equal(bytes16(MAX_UINT128));
124+
125+
// Test complex pattern that clearly shows byte reversal
126+
await expect(this.mock.$reverseBytes16('0x0123456789abcdef0123456789abcdef')).to.eventually.equal(
127+
'0xefcdab8967452301efcdab8967452301',
128+
);
129+
});
130+
131+
it('double reverse returns original', async function () {
132+
const values = [0n, 1n, 0x12345678n, MAX_UINT128];
133+
for (const value of values) {
134+
const reversed = await this.mock.$reverseBytes16(bytes16(value));
135+
// Cast back to uint128 for comparison since function returns uint256
136+
await expect(this.mock.$reverseBytes16(reversed)).to.eventually.equal(bytes16(value & MAX_UINT128));
137+
}
138+
});
139+
});
140+
141+
describe('reverseBytes8', function () {
142+
it('reverses bytes correctly', async function () {
143+
await expect(this.mock.$reverseBytes8(bytes8(0))).to.eventually.equal(bytes8(0));
144+
await expect(this.mock.$reverseBytes8(bytes8(MAX_UINT64))).to.eventually.equal(bytes8(MAX_UINT64));
145+
146+
// Test known pattern: 0x123456789ABCDEF0 -> 0xF0DEBC9A78563412
147+
await expect(this.mock.$reverseBytes8('0x123456789abcdef0')).to.eventually.equal('0xf0debc9a78563412');
148+
});
149+
150+
it('double reverse returns original', async function () {
151+
const values = [0n, 1n, 0x12345678n, MAX_UINT64];
152+
for (const value of values) {
153+
const reversed = await this.mock.$reverseBytes8(bytes8(value));
154+
// Cast back to uint64 for comparison since function returns uint256
155+
await expect(this.mock.$reverseBytes8(reversed)).to.eventually.equal(bytes8(value & MAX_UINT64));
156+
}
157+
});
158+
});
159+
160+
describe('reverseBytes4', function () {
161+
it('reverses bytes correctly', async function () {
162+
await expect(this.mock.$reverseBytes4(bytes4(0))).to.eventually.equal(bytes4(0));
163+
await expect(this.mock.$reverseBytes4(bytes4(MAX_UINT32))).to.eventually.equal(bytes4(MAX_UINT32));
164+
165+
// Test known pattern: 0x12345678 -> 0x78563412
166+
await expect(this.mock.$reverseBytes4(bytes4(0x12345678))).to.eventually.equal(bytes4(0x78563412));
167+
});
168+
169+
it('double reverse returns original', async function () {
170+
const values = [0n, 1n, 0x12345678n, MAX_UINT32];
171+
for (const value of values) {
172+
const reversed = await this.mock.$reverseBytes4(bytes4(value));
173+
// Cast back to uint32 for comparison since function returns uint256
174+
await expect(this.mock.$reverseBytes4(reversed)).to.eventually.equal(bytes4(value & MAX_UINT32));
175+
}
176+
});
177+
});
178+
179+
describe('reverseBytes2', function () {
180+
it('reverses bytes correctly', async function () {
181+
await expect(this.mock.$reverseBytes2(bytes2(0))).to.eventually.equal(bytes2(0));
182+
await expect(this.mock.$reverseBytes2(bytes2(MAX_UINT16))).to.eventually.equal(bytes2(MAX_UINT16));
183+
184+
// Test known pattern: 0x1234 -> 0x3412
185+
await expect(this.mock.$reverseBytes2(bytes2(0x1234))).to.eventually.equal(bytes2(0x3412));
186+
});
187+
188+
it('double reverse returns original', async function () {
189+
const values = [0n, 1n, 0x1234n, MAX_UINT16];
190+
for (const value of values) {
191+
const reversed = await this.mock.$reverseBytes2(bytes2(value));
192+
// Cast back to uint16 for comparison since function returns uint256
193+
await expect(this.mock.$reverseBytes2(reversed)).to.eventually.equal(bytes2(value & MAX_UINT16));
194+
}
195+
});
196+
});
197+
198+
describe('edge cases', function () {
199+
it('handles single byte values', async function () {
200+
await expect(this.mock.$reverseBytes2(bytes2(0x00ff))).to.eventually.equal(bytes2(0xff00));
201+
await expect(this.mock.$reverseBytes4(bytes4(0x000000ff))).to.eventually.equal(bytes4(0xff000000));
202+
});
203+
204+
it('handles alternating patterns', async function () {
205+
await expect(this.mock.$reverseBytes2(bytes2(0xaaaa))).to.eventually.equal(bytes2(0xaaaa));
206+
await expect(this.mock.$reverseBytes2(bytes2(0x5555))).to.eventually.equal(bytes2(0x5555));
207+
await expect(this.mock.$reverseBytes4(bytes4(0xaaaaaaaa))).to.eventually.equal(bytes4(0xaaaaaaaa));
208+
await expect(this.mock.$reverseBytes4(bytes4(0x55555555))).to.eventually.equal(bytes4(0x55555555));
209+
});
210+
});
211+
});
88212
});

0 commit comments

Comments
 (0)