Skip to content

Commit 88c03e7

Browse files
committed
Add Bytes.countConsecutive and Bytes.countLeading
1 parent bddf4f6 commit 88c03e7

File tree

4 files changed

+62
-29
lines changed

4 files changed

+62
-29
lines changed

.changeset/thirty-pugs-pick.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 `countLeading` and `countConsecutive`

contracts/utils/Base58.sol

Lines changed: 16 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
pragma solidity ^0.8.24;
44

5+
import {SafeCast} from "./math/SafeCast.sol";
56
import {Bytes} from "./Bytes.sol";
67

78
/**
@@ -10,6 +11,7 @@ import {Bytes} from "./Bytes.sol";
1011
* Based on the original https://github.com/storyicon/base58-solidity/commit/807428e5174e61867e4c606bdb26cba58a8c5cb1[implementation of storyicon] (MIT).
1112
*/
1213
library Base58 {
14+
using SafeCast for bool;
1315
using Bytes for bytes;
1416

1517
string internal constant _TABLE = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
@@ -24,13 +26,13 @@ library Base58 {
2426

2527
function _encode(bytes memory data) private pure returns (bytes memory) {
2628
unchecked {
27-
uint256 dataCLZ = _countLeading(data, 0x00);
28-
uint256 slotLength = dataCLZ + ((data.length - dataCLZ) * 8351) / 6115 + 1;
29+
uint256 dataCLZ = data.countLeading(0x00);
30+
uint256 length = dataCLZ + ((data.length - dataCLZ) * 8351) / 6115 + 1;
31+
bytes memory slot = new bytes(length);
2932

30-
bytes memory slot = new bytes(slotLength);
31-
uint256 end = slotLength;
33+
uint256 end = length;
3234
for (uint256 i = 0; i < data.length; i++) {
33-
uint256 ptr = slotLength;
35+
uint256 ptr = length;
3436
for (uint256 carry = _mload8i(data, i); ptr > end || carry != 0; --ptr) {
3537
carry += 256 * _mload8i(slot, ptr - 1);
3638
_mstore8i(slot, ptr - 1, uint8(carry % 58));
@@ -39,18 +41,14 @@ library Base58 {
3941
end = ptr;
4042
}
4143

42-
uint256 slotCLZ = _countLeading(slot, 0x00);
43-
uint256 resultLength = slotLength + dataCLZ - slotCLZ;
44+
uint256 slotCLZ = slot.countLeading(0x00);
45+
length -= slotCLZ - dataCLZ;
46+
slot.splice(slotCLZ - dataCLZ);
4447

4548
bytes memory cache = bytes(_TABLE);
46-
for (uint256 i = 0; i < resultLength; ++i) {
47-
uint256 idx = _mload8i(slot, i + slotCLZ - dataCLZ);
48-
bytes1 c = _mload8(cache, idx);
49-
_mstore8(slot, i, c);
50-
}
51-
52-
assembly ("memory-safe") {
53-
mstore(slot, resultLength)
49+
for (uint256 i = 0; i < length; ++i) {
50+
// equivalent to `slot[i] = TABLE[slot[i]];`
51+
_mstore8(slot, i, _mload8(cache, _mload8i(slot, i)));
5452
}
5553

5654
return slot;
@@ -88,13 +86,9 @@ library Base58 {
8886
mask = 4;
8987
}
9088

91-
uint256 dataCLZ = _countLeading(data, 0x31);
92-
for (uint256 msb = dataCLZ; msb < binu.length; ++msb) {
93-
if (_mload8(binu, msb) != 0x00) {
94-
return binu.slice(msb - dataCLZ, ptr);
95-
}
96-
}
97-
return binu.slice(0, ptr);
89+
uint256 dataCLZ = data.countLeading(0x31);
90+
uint256 msb = binu.countConsecutive(dataCLZ, 0x00);
91+
return binu.splice(msb * (dataCLZ + msb < binu.length).toUint(), ptr);
9892
}
9993
}
10094

@@ -125,11 +119,4 @@ library Base58 {
125119
mstore8(add(add(buffer, 0x20), offset), value)
126120
}
127121
}
128-
129-
function _countLeading(bytes memory buffer, bytes1 el) private pure returns (uint256) {
130-
uint256 length = buffer.length;
131-
uint256 i = 0;
132-
while (i < length && _mload8(buffer, i) == el) ++i;
133-
return i;
134-
}
135122
}

contracts/utils/Bytes.sol

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,39 @@ library Bytes {
6868
}
6969
}
7070

71+
/**
72+
* @dev Count number of occurrences of `search` at the beginning of `buffer`.
73+
*/
74+
function countLeading(bytes memory buffer, bytes1 search) public pure returns (uint256) {
75+
return countConsecutive(buffer, 0, search);
76+
}
77+
78+
/**
79+
* @dev Count number of occurrences of `search` in `buffer`, starting from position `offset`.
80+
*/
81+
function countConsecutive(bytes memory buffer, uint256 offset, bytes1 search) public pure returns (uint256 i) {
82+
assembly ("memory-safe") {
83+
let chunk
84+
let length := sub(mload(buffer), offset)
85+
for {
86+
i := 0
87+
} lt(i, length) {
88+
i := add(i, 1)
89+
} {
90+
// every 32 bytes, load a new chunk
91+
if iszero(mod(i, 0x20)) {
92+
chunk := mload(add(buffer, add(0x20, add(offset, i))))
93+
}
94+
// if the first byte of the chunk does not match the search element, exit
95+
if shr(248, xor(chunk, search)) {
96+
break
97+
}
98+
// shift chunk
99+
chunk := shl(8, chunk)
100+
}
101+
}
102+
}
103+
71104
/**
72105
* @dev Copies the content of `buffer`, from `start` (included) to the end of `buffer` into a new bytes object in
73106
* memory.

test/utils/Base58.t.sol

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ contract Base58Test is Test {
1010
assertEq(Base58.decode(Base58.encode("")), "");
1111
}
1212

13+
function testEncodeDecodeZeros() external pure {
14+
bytes memory zeros = hex"0000000000000000";
15+
assertEq(Base58.decode(Base58.encode(zeros)), zeros);
16+
17+
bytes memory almostZeros = hex"00000000a400000000";
18+
assertEq(Base58.decode(Base58.encode(almostZeros)), almostZeros);
19+
}
20+
1321
function testEncodeDecode(bytes memory input) external pure {
1422
assertEq(Base58.decode(Base58.encode(input)), input);
1523
}

0 commit comments

Comments
 (0)