Skip to content

Commit d66a0ce

Browse files
ernestognwAmxx
andauthored
Add new clz(bytes) and clz(uint256) functions (#5725)
Co-authored-by: Hadrien Croubois <[email protected]>
1 parent e8a3e62 commit d66a0ce

File tree

8 files changed

+199
-1
lines changed

8 files changed

+199
-1
lines changed

.changeset/fast-beans-pull.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 a `clz` function to count the leading zero bits in a `bytes` buffer.

.changeset/whole-cats-find.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+
`Math`: Add a `clz` function to count the leading zero bits in a `uint256` value.

contracts/utils/Bytes.sol

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ library Bytes {
128128
return buffer;
129129
}
130130

131-
/*
131+
/**
132132
* @dev Returns true if the two byte buffers are equal.
133133
*/
134134
function equal(bytes memory a, bytes memory b) internal pure returns (bool) {
@@ -187,6 +187,20 @@ library Bytes {
187187
return (value >> 8) | (value << 8);
188188
}
189189

190+
/**
191+
* @dev Counts the number of leading zero bits a bytes array. Returns `8 * buffer.length`
192+
* if the buffer is all zeros.
193+
*/
194+
function clz(bytes memory buffer) internal pure returns (uint256) {
195+
for (uint256 i = 0; i < buffer.length; i += 32) {
196+
bytes32 chunk = _unsafeReadBytesOffset(buffer, i);
197+
if (chunk != bytes32(0)) {
198+
return Math.min(8 * i + Math.clz(uint256(chunk)), 8 * buffer.length);
199+
}
200+
}
201+
return 8 * buffer.length;
202+
}
203+
190204
/**
191205
* @dev Reads a bytes32 from a bytes array without bounds checking.
192206
*

contracts/utils/math/Math.sol

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -746,4 +746,11 @@ library Math {
746746
function unsignedRoundsUp(Rounding rounding) internal pure returns (bool) {
747747
return uint8(rounding) % 2 == 1;
748748
}
749+
750+
/**
751+
* @dev Counts the number of leading zero bits in a uint256.
752+
*/
753+
function clz(uint256 x) internal pure returns (uint256) {
754+
return ternary(x == 0, 256, 255 - log2(x));
755+
}
749756
}

test/utils/Bytes.t.sol

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,34 @@ contract BytesTest is Test {
196196
assertEq(Bytes.reverseBytes2(_dirtyBytes2(Bytes.reverseBytes2(value))), value);
197197
}
198198

199+
// CLZ (Count Leading Zeros)
200+
function testClz(bytes memory buffer) public pure {
201+
uint256 result = Bytes.clz(buffer);
202+
203+
// index and offset of the first non zero bit
204+
uint256 index = result / 8;
205+
uint256 offset = result % 8;
206+
207+
// Result should never exceed buffer length
208+
assertLe(index, buffer.length);
209+
210+
// All bytes before index position must be zero
211+
for (uint256 i = 0; i < index; ++i) {
212+
assertEq(buffer[i], 0);
213+
}
214+
215+
// If index < buffer.length, byte at index position must be non-zero
216+
if (index < buffer.length) {
217+
// bit at position offset must be non zero
218+
bytes1 singleBitMask = bytes1(0x80) >> offset;
219+
assertEq(buffer[index] & singleBitMask, singleBitMask);
220+
221+
// all bits before offset must be zero
222+
bytes1 multiBitsMask = bytes1(0xff) << (8 - offset);
223+
assertEq(buffer[index] & multiBitsMask, 0);
224+
}
225+
}
226+
199227
// Helpers
200228
function _dirtyBytes16(bytes16 value) private pure returns (bytes16 dirty) {
201229
assembly ("memory-safe") {

test/utils/Bytes.test.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,93 @@ describe('Bytes', function () {
112112
});
113113
});
114114

115+
describe('clz bytes', function () {
116+
it('empty buffer', async function () {
117+
await expect(this.mock.$clz('0x')).to.eventually.equal(0);
118+
});
119+
120+
it('single zero byte', async function () {
121+
await expect(this.mock.$clz('0x00')).to.eventually.equal(8);
122+
});
123+
124+
it('single non-zero byte', async function () {
125+
await expect(this.mock.$clz('0x01')).to.eventually.equal(7);
126+
await expect(this.mock.$clz('0xff')).to.eventually.equal(0);
127+
});
128+
129+
it('multiple leading zeros', async function () {
130+
await expect(this.mock.$clz('0x0000000001')).to.eventually.equal(39);
131+
await expect(
132+
this.mock.$clz('0x0000000000000000000000000000000000000000000000000000000000000001'),
133+
).to.eventually.equal(255);
134+
});
135+
136+
it('all zeros of various lengths', async function () {
137+
await expect(this.mock.$clz('0x00000000')).to.eventually.equal(32);
138+
await expect(
139+
this.mock.$clz('0x0000000000000000000000000000000000000000000000000000000000000000'),
140+
).to.eventually.equal(256);
141+
142+
// Complete chunks
143+
await expect(this.mock.$clz('0x' + '00'.repeat(32) + '01')).to.eventually.equal(263); // 32*8+7
144+
await expect(this.mock.$clz('0x' + '00'.repeat(64) + '01')).to.eventually.equal(519); // 64*8+7
145+
146+
// Partial last chunk
147+
await expect(this.mock.$clz('0x' + '00'.repeat(33) + '01')).to.eventually.equal(271); // 33*8+7
148+
await expect(this.mock.$clz('0x' + '00'.repeat(34) + '01')).to.eventually.equal(279); // 34*8+7
149+
await expect(this.mock.$clz('0x' + '00'.repeat(40) + '01' + '00'.repeat(9))).to.eventually.equal(327); // 40*8+7
150+
await expect(this.mock.$clz('0x' + '00'.repeat(50))).to.eventually.equal(400); // 50*8
151+
152+
// First byte of each chunk non-zero
153+
await expect(this.mock.$clz('0x80' + '00'.repeat(31))).to.eventually.equal(0);
154+
await expect(this.mock.$clz('0x01' + '00'.repeat(31))).to.eventually.equal(7);
155+
await expect(this.mock.$clz('0x' + '00'.repeat(32) + '80' + '00'.repeat(31))).to.eventually.equal(256); // 32*8
156+
await expect(this.mock.$clz('0x' + '00'.repeat(32) + '01' + '00'.repeat(31))).to.eventually.equal(263); // 32*8+7
157+
158+
// Last byte of each chunk non-zero
159+
await expect(this.mock.$clz('0x' + '00'.repeat(31) + '01')).to.eventually.equal(255); // 31*8+7
160+
await expect(this.mock.$clz('0x' + '00'.repeat(63) + '01')).to.eventually.equal(511); // 63*8+7
161+
162+
// Middle byte of each chunk non-zero
163+
await expect(this.mock.$clz('0x' + '00'.repeat(16) + '01' + '00'.repeat(15))).to.eventually.equal(135); // 16*8+7
164+
await expect(this.mock.$clz('0x' + '00'.repeat(32) + '01' + '00'.repeat(31))).to.eventually.equal(263); // 32*8+7
165+
await expect(this.mock.$clz('0x' + '00'.repeat(48) + '01' + '00'.repeat(47))).to.eventually.equal(391); // 48*8+7
166+
await expect(this.mock.$clz('0x' + '00'.repeat(64) + '01' + '00'.repeat(63))).to.eventually.equal(519); // 64*8+7
167+
});
168+
});
169+
170+
describe('equal', function () {
171+
it('identical buffers', async function () {
172+
await expect(this.mock.$equal(lorem, lorem)).to.eventually.be.true;
173+
});
174+
175+
it('same content', async function () {
176+
const copy = new Uint8Array(lorem);
177+
await expect(this.mock.$equal(lorem, copy)).to.eventually.be.true;
178+
});
179+
180+
it('different content', async function () {
181+
const different = ethers.toUtf8Bytes('Different content');
182+
await expect(this.mock.$equal(lorem, different)).to.eventually.be.false;
183+
});
184+
185+
it('different lengths', async function () {
186+
const shorter = lorem.slice(0, 10);
187+
await expect(this.mock.$equal(lorem, shorter)).to.eventually.be.false;
188+
});
189+
190+
it('empty buffers', async function () {
191+
const empty1 = new Uint8Array(0);
192+
const empty2 = new Uint8Array(0);
193+
await expect(this.mock.$equal(empty1, empty2)).to.eventually.be.true;
194+
});
195+
196+
it('one empty one not', async function () {
197+
const empty = new Uint8Array(0);
198+
await expect(this.mock.$equal(lorem, empty)).to.eventually.be.false;
199+
});
200+
});
201+
115202
describe('reverseBits', function () {
116203
describe('reverseBytes32', function () {
117204
it('reverses bytes correctly', async function () {

test/utils/math/Math.t.sol

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,25 @@ contract MathTest is Test {
308308
}
309309
}
310310

311+
function testSymbolicCountLeadingZeroes(uint256 x) public pure {
312+
uint256 result = Math.clz(x);
313+
314+
if (x == 0) {
315+
assertEq(result, 256);
316+
} else {
317+
// result in [0, 255]
318+
assertLe(result, 255);
319+
320+
// bit at position offset must be non zero
321+
uint256 singleBitMask = uint256(1) << (255 - result);
322+
assertEq(x & singleBitMask, singleBitMask);
323+
324+
// all bits before offset must be zero
325+
uint256 multiBitsMask = type(uint256).max << (256 - result);
326+
assertEq(x & multiBitsMask, 0);
327+
}
328+
}
329+
311330
// Helpers
312331
function _asRounding(uint8 r) private pure returns (Math.Rounding) {
313332
vm.assume(r < uint8(type(Math.Rounding).max));

test/utils/math/Math.test.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -710,4 +710,37 @@ describe('Math', function () {
710710
});
711711
});
712712
});
713+
714+
describe('clz', function () {
715+
it('zero value', async function () {
716+
await expect(this.mock.$clz(0)).to.eventually.equal(256);
717+
});
718+
719+
it('small values', async function () {
720+
await expect(this.mock.$clz(1)).to.eventually.equal(255);
721+
await expect(this.mock.$clz(255)).to.eventually.equal(248);
722+
});
723+
724+
it('larger values', async function () {
725+
await expect(this.mock.$clz(256)).to.eventually.equal(247);
726+
await expect(this.mock.$clz(0xff00)).to.eventually.equal(240);
727+
await expect(this.mock.$clz(0x10000)).to.eventually.equal(239);
728+
});
729+
730+
it('max value', async function () {
731+
await expect(this.mock.$clz(ethers.MaxUint256)).to.eventually.equal(0);
732+
});
733+
734+
it('specific patterns', async function () {
735+
await expect(
736+
this.mock.$clz('0x0000000000000000000000000000000000000000000000000000000000000100'),
737+
).to.eventually.equal(247);
738+
await expect(
739+
this.mock.$clz('0x0000000000000000000000000000000000000000000000000000000000010000'),
740+
).to.eventually.equal(239);
741+
await expect(
742+
this.mock.$clz('0x0000000000000000000000000000000000000000000000000000000001000000'),
743+
).to.eventually.equal(231);
744+
});
745+
});
713746
});

0 commit comments

Comments
 (0)