Skip to content

Commit 65292d5

Browse files
committed
Add Base58 library
1 parent 6079eb3 commit 65292d5

File tree

5 files changed

+183
-1
lines changed

5 files changed

+183
-1
lines changed

.changeset/loose-lamps-bake.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+
`Base58`: Add a library for encoding and decoding bytes buffers into base58 strings.

contracts/utils/Base58.sol

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.24;
4+
5+
import {Bytes} from "./Bytes.sol";
6+
7+
/**
8+
* @dev Provides a set of functions to operate with Base58 strings.
9+
*
10+
* Based on the original https://github.com/storyicon/base58-solidity/commit/807428e5174e61867e4c606bdb26cba58a8c5cb1[implementation of storyicon] (MIT).
11+
*/
12+
library Base58 {
13+
using Bytes for bytes;
14+
15+
string internal constant _TABLE = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
16+
17+
function encode(bytes memory data) internal pure returns (string memory) {
18+
return string(_encode(data));
19+
}
20+
21+
function decode(string memory data) internal pure returns (bytes memory) {
22+
return _decode(bytes(data));
23+
}
24+
25+
function _encode(bytes memory data) private pure returns (bytes memory) {
26+
unchecked {
27+
uint256 dataCLZ = _countLeading(data, 0x00);
28+
uint256 slotLength = dataCLZ + ((data.length - dataCLZ) * 8351) / 6115 + 1;
29+
30+
bytes memory slot = new bytes(slotLength);
31+
uint256 end = slotLength;
32+
for (uint256 i = 0; i < data.length; i++) {
33+
uint256 ptr = slotLength;
34+
for (uint256 carry = _mload8i(data, i); ptr > end || carry != 0; --ptr) {
35+
carry += 256 * _mload8i(slot, ptr - 1);
36+
_mstore8i(slot, ptr - 1, uint8(carry % 58));
37+
carry /= 58;
38+
}
39+
end = ptr;
40+
}
41+
42+
uint256 slotCLZ = _countLeading(slot, 0x00);
43+
uint256 resultLength = slotLength + dataCLZ - slotCLZ;
44+
45+
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)
54+
}
55+
56+
return slot;
57+
}
58+
}
59+
60+
function _decode(bytes memory data) private pure returns (bytes memory) {
61+
unchecked {
62+
uint256 b58Length = data.length;
63+
64+
uint256 size = 2 * ((b58Length * 8351) / 6115 + 1);
65+
bytes memory binu = new bytes(size);
66+
67+
bytes memory cache = bytes(_TABLE);
68+
uint32[] memory outi = new uint32[]((b58Length + 3) / 4);
69+
for (uint256 i = 0; i < data.length; i++) {
70+
bytes1 r = _mload8(data, i);
71+
uint256 c = cache.indexOf(r); // can we avoid the loop here ?
72+
require(c != type(uint256).max, "invalid base58 digit");
73+
for (uint256 k = outi.length; k > 0; --k) {
74+
uint256 t = uint64(outi[k - 1]) * 58 + c;
75+
c = t >> 32;
76+
outi[k - 1] = uint32(t & 0xffffffff);
77+
}
78+
}
79+
80+
uint256 ptr = 0;
81+
uint256 mask = ((b58Length - 1) % 4) + 1;
82+
for (uint256 j = 0; j < outi.length; ++j) {
83+
while (mask > 0) {
84+
--mask;
85+
_mstore8(binu, ptr, bytes1(uint8(outi[j] >> (8 * mask))));
86+
ptr++;
87+
}
88+
mask = 4;
89+
}
90+
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);
98+
}
99+
}
100+
101+
function _mload8(bytes memory buffer, uint256 offset) private pure returns (bytes1 value) {
102+
// This is not memory safe in the general case, but all calls to this private function are within bounds.
103+
assembly ("memory-safe") {
104+
value := mload(add(add(buffer, 0x20), offset))
105+
}
106+
}
107+
108+
function _mload8i(bytes memory buffer, uint256 offset) private pure returns (uint8 value) {
109+
// This is not memory safe in the general case, but all calls to this private function are within bounds.
110+
assembly ("memory-safe") {
111+
value := shr(248, mload(add(add(buffer, 0x20), offset)))
112+
}
113+
}
114+
115+
function _mstore8(bytes memory buffer, uint256 offset, bytes1 value) private pure {
116+
// This is not memory safe in the general case, but all calls to this private function are within bounds.
117+
assembly ("memory-safe") {
118+
mstore8(add(add(buffer, 0x20), offset), shr(248, value))
119+
}
120+
}
121+
122+
function _mstore8i(bytes memory buffer, uint256 offset, uint8 value) private pure {
123+
// This is not memory safe in the general case, but all calls to this private function are within bounds.
124+
assembly ("memory-safe") {
125+
mstore8(add(add(buffer, 0x20), offset), value)
126+
}
127+
}
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+
}
135+
}

test/utils/Base58.t.sol

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {Test} from "forge-std/Test.sol";
6+
import {Base58} from "@openzeppelin/contracts/utils/Base58.sol";
7+
8+
contract Base58Test is Test {
9+
function testEncodeDecodeEmpty() external pure {
10+
assertEq(Base58.decode(Base58.encode("")), "");
11+
}
12+
13+
function testEncodeDecode(bytes memory input) external pure {
14+
assertEq(Base58.decode(Base58.encode(input)), input);
15+
}
16+
}

test/utils/Base58.test.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
const { ethers } = require('hardhat');
2+
const { expect } = require('chai');
3+
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
4+
5+
async function fixture() {
6+
const mock = await ethers.deployContract('$Base58');
7+
return { mock };
8+
}
9+
10+
describe('Base58', function () {
11+
beforeEach(async function () {
12+
Object.assign(this, await loadFixture(fixture));
13+
});
14+
15+
describe('base58', function () {
16+
for (const length of [0, 1, 2, 3, 4, 32, 42, 128, 384]) // 512 runs out of gas
17+
it(`Encode/Decode buffer of length ${length}`, async function () {
18+
const buffer = ethers.randomBytes(length);
19+
const hex = ethers.hexlify(buffer);
20+
const b58 = ethers.encodeBase58(buffer);
21+
22+
expect(await this.mock.$encode(hex)).to.equal(b58);
23+
expect(await this.mock.$decode(b58)).to.equal(hex);
24+
});
25+
});
26+
});

test/utils/Base64.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ async function fixture() {
1111
return { mock };
1212
}
1313

14-
describe('Strings', function () {
14+
describe('Base64', function () {
1515
beforeEach(async function () {
1616
Object.assign(this, await loadFixture(fixture));
1717
});

0 commit comments

Comments
 (0)