Skip to content

Commit 066ccf5

Browse files
authored
⚡️ Base58 decodeWord (#1445)
1 parent 82fa779 commit 066ccf5

3 files changed

Lines changed: 77 additions & 9 deletions

File tree

docs/utils/base58.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Library to encode strings in Base58.
1717
error Base58DecodingError()
1818
```
1919

20-
An unrecognized character was encountered during decoding.
20+
An unrecognized character or overflow was encountered during decoding.
2121

2222
## Encoding / Decoding
2323

src/utils/Base58.sol

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ library Base58 {
99
/* CUSTOM ERRORS */
1010
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
1111

12-
/// @dev An unrecognized character was encountered during decoding.
12+
/// @dev An unrecognized character or overflow was encountered during decoding.
1313
error Base58DecodingError();
1414

1515
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
@@ -174,16 +174,32 @@ library Base58 {
174174

175175
/// @dev Decodes `encoded`, a Base58 string, into the original word.
176176
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);
177+
uint256 n = bytes(encoded).length;
178+
if (n == uint256(0)) return result;
179179
/// @solidity memory-safe-assembly
180180
assembly {
181-
let n := mload(t)
182-
if iszero(lt(n, 0x21)) {
183-
mstore(0x00, 0xe8fad793) // `Base58DecodingError()`.
184-
revert(0x1c, 0x04)
181+
let m := mload(0x40) // Cache the free memory pointer.
182+
let s := add(encoded, 0x20)
183+
let t := add(1, div(not(0), 58)) // Overflow threshold for multiplication.
184+
// Use the extended scratch space for the lookup. We'll restore 0x40 later.
185+
mstore(0x2a, 0x30313233343536373839)
186+
mstore(0x20, 0x1718191a1b1c1d1e1f20ffffffffffff2122232425262728292a2bff2c2d2e2f)
187+
mstore(0x00, 0x000102030405060708ffffffffffffff090a0b0c0d0e0f10ff1112131415ff16)
188+
189+
for { let j := 0 } 1 {} {
190+
let c := sub(byte(0, mload(add(s, j))), 49)
191+
let p := mul(result, 58)
192+
let acc := add(byte(0, mload(c)), p)
193+
// Check if the input character is valid.
194+
if iszero(and(0x3fff7ff03ffbeff01ff, shl(c, lt(lt(acc, p), lt(result, t))))) {
195+
mstore(0x00, 0xe8fad793) // `Base58DecodingError()`.
196+
revert(0x1c, 0x04)
197+
}
198+
result := acc
199+
j := add(j, 1)
200+
if eq(j, n) { break }
185201
}
186-
result := mload(add(t, n))
202+
mstore(0x40, m) // Restore the free memory pointer.
187203
}
188204
}
189205
}

test/Base58.t.sol

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,4 +228,56 @@ contract Base58Test is SoladyTest {
228228
string memory encoded = Base58.encodeWord(word);
229229
assertEq(Base58.decodeWord(encoded), word);
230230
}
231+
232+
function testDecodeWordDifferential(bytes32 word) public {
233+
string memory encoded = Base58.encodeWord(word);
234+
bytes32 expected = _decodeWordOriginal(encoded);
235+
bytes32 computed = Base58.decodeWord(encoded);
236+
_checkMemory();
237+
assertEq(computed, expected);
238+
}
239+
240+
function testDecodeWordOverflowsReverts() public {
241+
bytes32 expected = bytes32(type(uint256).max);
242+
assertEq(this.decodeWord("JEKNVnkbo3jma5nREBBJCDoXFVeKkD56V3xKrvRmWxFG"), expected);
243+
assertEq(this.decodeWord("1JEKNVnkbo3jma5nREBBJCDoXFVeKkD56V3xKrvRmWxFG"), expected);
244+
assertEq(this.decodeWord("11JEKNVnkbo3jma5nREBBJCDoXFVeKkD56V3xKrvRmWxFG"), expected);
245+
246+
vm.expectRevert(Base58.Base58DecodingError.selector);
247+
this.decodeWord("JEKNVnkbo3jma5nREBBJCDoXFVeKkD56V3xKrvRmWxFH");
248+
vm.expectRevert(Base58.Base58DecodingError.selector);
249+
this.decodeWord("JEKNVnkbo3jma5nREBBJCDoXFVeKkD56V3xKrvRmWxFJ");
250+
}
251+
252+
function testDecodeWordInvalidCharacterReverts() public {
253+
vm.expectRevert(Base58.Base58DecodingError.selector);
254+
this.decodeWord("JEKNVnkbo3jma5nREBBJCDoXFVeKkD56V3xKrvRmWxFH@");
255+
}
256+
257+
function decodeWord(string memory encoded) public pure returns (bytes32) {
258+
return Base58.decodeWord(encoded);
259+
}
260+
261+
function _decodeWordOriginal(string memory encoded) internal pure returns (bytes32 result) {
262+
bytes memory t = Base58.decode(encoded);
263+
/// @solidity memory-safe-assembly
264+
assembly {
265+
let n := mload(t)
266+
if iszero(lt(n, 0x21)) {
267+
mstore(0x00, 0xe8fad793) // `Base58DecodingError()`.
268+
revert(0x1c, 0x04)
269+
}
270+
result := mload(add(t, n))
271+
}
272+
}
273+
274+
function testDecodeWordGas() public {
275+
bytes32 expected = bytes32(type(uint256).max);
276+
assertEq(Base58.decodeWord("JEKNVnkbo3jma5nREBBJCDoXFVeKkD56V3xKrvRmWxFG"), expected);
277+
}
278+
279+
function testDecodeGas() public {
280+
bytes memory expected = abi.encodePacked(type(uint256).max);
281+
assertEq(Base58.decode("JEKNVnkbo3jma5nREBBJCDoXFVeKkD56V3xKrvRmWxFG"), expected);
282+
}
231283
}

0 commit comments

Comments
 (0)