Skip to content

Commit 82fa779

Browse files
✨ Base58 bytes32 specialization (#1443)
Co-authored-by: shuhuiluo <107524008+shuhuiluo@users.noreply.github.com>
1 parent 2b505ab commit 82fa779

3 files changed

Lines changed: 101 additions & 1 deletion

File tree

docs/utils/base58.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,17 @@ function encode(bytes memory data)
3232

3333
Encodes `data` into a Base58 string.
3434

35+
### encodeWord(bytes32)
36+
37+
```solidity
38+
function encodeWord(bytes32 data)
39+
internal
40+
pure
41+
returns (string memory result)
42+
```
43+
44+
Encodes the `data` word into a Base58 string.
45+
3546
### decode(string)
3647

3748
```solidity
@@ -41,4 +52,15 @@ function decode(string memory encoded)
4152
returns (bytes memory result)
4253
```
4354

44-
Decodes `encoded`, a Base58 string, into the original bytes.
55+
Decodes `encoded`, a Base58 string, into the original bytes.
56+
57+
### decodeWord(string)
58+
59+
```solidity
60+
function decodeWord(string memory encoded)
61+
internal
62+
pure
63+
returns (bytes32 result)
64+
```
65+
66+
Decodes `encoded`, a Base58 string, into the original word.

src/utils/Base58.sol

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,37 @@ library Base58 {
8080
}
8181
}
8282

83+
/// @dev Encodes the `data` word into a Base58 string.
84+
function encodeWord(bytes32 data) internal pure returns (string memory result) {
85+
/// @solidity memory-safe-assembly
86+
assembly {
87+
let o := add(mload(0x40), 0x4c) // 32 for word, 44 for maximum possible length.
88+
let e := o
89+
90+
// Use the extended scratch space for the lookup. We'll restore 0x40 later.
91+
mstore(0x1f, "123456789ABCDEFGHJKLMNPQRSTUVWXY")
92+
mstore(0x3f, "Zabcdefghijkmnopqrstuvwxyz")
93+
94+
let w := not(0) // -1.
95+
let z := shl(5, iszero(data)) // Number of leading zeroes in `data`.
96+
if iszero(z) {
97+
for { let v := data } v { v := div(v, 58) } {
98+
o := add(o, w)
99+
mstore8(o, mload(mod(v, 58)))
100+
}
101+
for {} iszero(byte(z, data)) { z := add(z, 1) } {} // Just loop, `z` is often tiny.
102+
}
103+
if z { mstore(sub(o, 0x20), mul(div(w, 0xff), 49)) } // '1111...1111' in ASCII.
104+
o := sub(o, z)
105+
106+
let n := sub(e, o) // Compute the final length.
107+
result := sub(o, 0x20) // Move back one word for the length.
108+
mstore(result, n) // Store the length.
109+
mstore(add(add(result, 0x20), n), 0) // Zeroize the slot after the bytes.
110+
mstore(0x40, add(add(result, 0x40), n)) // Allocate memory.
111+
}
112+
}
113+
83114
/// @dev Decodes `encoded`, a Base58 string, into the original bytes.
84115
function decode(string memory encoded) internal pure returns (bytes memory result) {
85116
uint256 n = bytes(encoded).length;
@@ -140,4 +171,19 @@ library Base58 {
140171
mstore(0x40, add(add(result, 0x40), l)) // Allocate memory.
141172
}
142173
}
174+
175+
/// @dev Decodes `encoded`, a Base58 string, into the original word.
176+
function decodeWord(string memory encoded) internal pure returns (bytes32 result) {
177+
// Specializing and optimizing this for bytes32 is left as an exercise to the reader.
178+
bytes memory t = decode(encoded);
179+
/// @solidity memory-safe-assembly
180+
assembly {
181+
let n := mload(t)
182+
if iszero(lt(n, 0x21)) {
183+
mstore(0x00, 0xe8fad793) // `Base58DecodingError()`.
184+
revert(0x1c, 0x04)
185+
}
186+
result := mload(add(t, n))
187+
}
188+
}
143189
}

test/Base58.t.sol

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,27 @@ import {Base58} from "../src/utils/Base58.sol";
66
import {LibString} from "../src/utils/LibString.sol";
77

88
contract Base58Test is SoladyTest {
9+
function testBase58DecodeRevertsIfInvalidCharacter(bytes1 c) public {
10+
if (isValidBase58Character(c)) {
11+
this.base58DecodeRevertsIfInvalidCharacter(c);
12+
} else {
13+
vm.expectRevert(Base58.Base58DecodingError.selector);
14+
this.base58DecodeRevertsIfInvalidCharacter(c);
15+
}
16+
}
17+
18+
function isValidBase58Character(bytes1 c) internal pure returns (bool) {
19+
bytes memory allowed = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
20+
for (uint256 i; i < allowed.length; ++i) {
21+
if (allowed[i] == c) return true;
22+
}
23+
return false;
24+
}
25+
26+
function base58DecodeRevertsIfInvalidCharacter(bytes1 c) public {
27+
emit LogBytes(Base58.decode(string(abi.encodePacked(c))));
28+
}
29+
930
function testBase58EncodeDecode(bytes memory data, uint256 r) public {
1031
if (r & 0x00f == 0) {
1132
_brutalizeMemory();
@@ -196,4 +217,15 @@ contract Base58Test is SoladyTest {
196217
function check_CarryBoundsTrick(uint248 limb, uint8 carry) public pure {
197218
testCarryBoundsTrick(limb, carry);
198219
}
220+
221+
function testEncodeWordDifferential(bytes32 word) public {
222+
string memory expected = Base58.encode(abi.encodePacked(word));
223+
string memory computed = Base58.encodeWord(word);
224+
assertEq(computed, expected);
225+
}
226+
227+
function testEncodeDecodeWord(bytes32 word) public {
228+
string memory encoded = Base58.encodeWord(word);
229+
assertEq(Base58.decodeWord(encoded), word);
230+
}
199231
}

0 commit comments

Comments
 (0)