From ab43534479557b4d4e11c772c375adf57d453369 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Thu, 26 Jun 2025 08:49:58 +0000 Subject: [PATCH 1/3] Base58 bytes32 specialization --- docs/utils/base58.md | 22 +++++++++++++++++++++ src/utils/Base58.sol | 46 ++++++++++++++++++++++++++++++++++++++++++++ test/Base58.t.sol | 32 ++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+) diff --git a/docs/utils/base58.md b/docs/utils/base58.md index cc55a2312c..5b52d351b3 100644 --- a/docs/utils/base58.md +++ b/docs/utils/base58.md @@ -32,6 +32,17 @@ function encode(bytes memory data) Encodes `data` into a Base58 string. +### encodeWord(bytes32) + +```solidity +function encodeWord(bytes32 data) + internal + pure + returns (string memory result) +``` + +Encodes `data` into a Base58 string. + ### decode(string) ```solidity @@ -41,4 +52,15 @@ function decode(string memory encoded) returns (bytes memory result) ``` +Decodes `encoded`, a Base58 string, into the original bytes. + +### decodeWord(string) + +```solidity +function decodeWord(string memory encoded) + internal + pure + returns (bytes32 result) +``` + Decodes `encoded`, a Base58 string, into the original bytes. \ No newline at end of file diff --git a/src/utils/Base58.sol b/src/utils/Base58.sol index 3ae1a6f23b..9383691d59 100644 --- a/src/utils/Base58.sol +++ b/src/utils/Base58.sol @@ -80,6 +80,37 @@ library Base58 { } } + /// @dev Encodes `data` into a Base58 string. + function encodeWord(bytes32 data) internal pure returns (string memory result) { + /// @solidity memory-safe-assembly + assembly { + let o := add(mload(0x40), 0x4c) // 32 for word, 44 for maximum possible length. + let e := o + + // Use the extended scratch space for the lookup. We'll restore 0x40 later. + mstore(0x1f, "123456789ABCDEFGHJKLMNPQRSTUVWXY") + mstore(0x3f, "Zabcdefghijkmnopqrstuvwxyz") + + let w := not(0) // -1. + let z := shl(5, iszero(data)) // Number of leading zeroes in `data`. + if iszero(z) { + for { let v := data } v { v := div(v, 58) } { + o := add(o, w) + mstore8(o, mload(mod(v, 58))) + } + for {} iszero(byte(z, data)) { z := add(z, 1) } {} // Just loop, `z` is often tiny. + } + if z { mstore(sub(o, 0x20), mul(div(w, 0xff), 49)) } // '1111...1111' in ASCII. + o := sub(o, z) + + let n := sub(e, o) // Compute the final length. + result := sub(o, 0x20) // Move back one word for the length. + mstore(result, n) // Store the length. + mstore(add(add(result, 0x20), n), 0) // Zeroize the slot after the bytes. + mstore(0x40, add(add(result, 0x40), n)) // Allocate memory. + } + } + /// @dev Decodes `encoded`, a Base58 string, into the original bytes. function decode(string memory encoded) internal pure returns (bytes memory result) { uint256 n = bytes(encoded).length; @@ -140,4 +171,19 @@ library Base58 { mstore(0x40, add(add(result, 0x40), l)) // Allocate memory. } } + + /// @dev Decodes `encoded`, a Base58 string, into the original bytes. + function decodeWord(string memory encoded) internal pure returns (bytes32 result) { + // Specializing and optimizing this is left as an exercise to the frontrunner. + bytes memory t = decode(encoded); + /// @solidity memory-safe-assembly + assembly { + let n := mload(t) + if iszero(lt(n, 0x21)) { + mstore(0x00, 0xe8fad793) // `Base58DecodingError()`. + revert(0x1c, 0x04) + } + result := mload(add(t, n)) + } + } } diff --git a/test/Base58.t.sol b/test/Base58.t.sol index 920623c027..e77616c849 100644 --- a/test/Base58.t.sol +++ b/test/Base58.t.sol @@ -6,6 +6,27 @@ import {Base58} from "../src/utils/Base58.sol"; import {LibString} from "../src/utils/LibString.sol"; contract Base58Test is SoladyTest { + function testBase58DecodeRevertsIfInvalidCharacter(bytes1 c) public { + if (isValidBase58Character(c)) { + this.base58DecodeRevertsIfInvalidCharacter(c); + } else { + vm.expectRevert(Base58.Base58DecodingError.selector); + this.base58DecodeRevertsIfInvalidCharacter(c); + } + } + + function isValidBase58Character(bytes1 c) internal pure returns (bool) { + bytes memory allowed = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + for (uint256 i; i < allowed.length; ++i) { + if (allowed[i] == c) return true; + } + return false; + } + + function base58DecodeRevertsIfInvalidCharacter(bytes1 c) public { + emit LogBytes(Base58.decode(string(abi.encodePacked(c)))); + } + function testBase58EncodeDecode(bytes memory data, uint256 r) public { if (r & 0x00f == 0) { _brutalizeMemory(); @@ -196,4 +217,15 @@ contract Base58Test is SoladyTest { function check_CarryBoundsTrick(uint248 limb, uint8 carry) public pure { testCarryBoundsTrick(limb, carry); } + + function testEncodeWordDifferential(bytes32 word) public { + string memory expected = Base58.encode(abi.encodePacked(word)); + string memory computed = Base58.encodeWord(word); + assertEq(computed, expected); + } + + function testEncodeDecodeWord(bytes32 word) public { + string memory encoded = Base58.encodeWord(word); + assertEq(Base58.decodeWord(encoded), word); + } } From 2666dffb5e2ab7f595085c3be0f6292c4edb3887 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Thu, 26 Jun 2025 08:51:16 +0000 Subject: [PATCH 2/3] Regen docs --- docs/utils/base58.md | 4 ++-- src/utils/Base58.sol | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/utils/base58.md b/docs/utils/base58.md index 5b52d351b3..7e1d096c63 100644 --- a/docs/utils/base58.md +++ b/docs/utils/base58.md @@ -41,7 +41,7 @@ function encodeWord(bytes32 data) returns (string memory result) ``` -Encodes `data` into a Base58 string. +Encodes the `data` word into a Base58 string. ### decode(string) @@ -63,4 +63,4 @@ function decodeWord(string memory encoded) returns (bytes32 result) ``` -Decodes `encoded`, a Base58 string, into the original bytes. \ No newline at end of file +Decodes `encoded`, a Base58 string, into the original word. \ No newline at end of file diff --git a/src/utils/Base58.sol b/src/utils/Base58.sol index 9383691d59..268fbaf0e9 100644 --- a/src/utils/Base58.sol +++ b/src/utils/Base58.sol @@ -80,7 +80,7 @@ library Base58 { } } - /// @dev Encodes `data` into a Base58 string. + /// @dev Encodes the `data` word into a Base58 string. function encodeWord(bytes32 data) internal pure returns (string memory result) { /// @solidity memory-safe-assembly assembly { @@ -172,7 +172,7 @@ library Base58 { } } - /// @dev Decodes `encoded`, a Base58 string, into the original bytes. + /// @dev Decodes `encoded`, a Base58 string, into the original word. function decodeWord(string memory encoded) internal pure returns (bytes32 result) { // Specializing and optimizing this is left as an exercise to the frontrunner. bytes memory t = decode(encoded); From ab4a61e8b37b686adb8f4fa948c70c23df300391 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Thu, 26 Jun 2025 08:54:59 +0000 Subject: [PATCH 3/3] Edit comment --- src/utils/Base58.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/Base58.sol b/src/utils/Base58.sol index 268fbaf0e9..3cb8886ded 100644 --- a/src/utils/Base58.sol +++ b/src/utils/Base58.sol @@ -174,7 +174,7 @@ library Base58 { /// @dev Decodes `encoded`, a Base58 string, into the original word. function decodeWord(string memory encoded) internal pure returns (bytes32 result) { - // Specializing and optimizing this is left as an exercise to the frontrunner. + // Specializing and optimizing this for bytes32 is left as an exercise to the reader. bytes memory t = decode(encoded); /// @solidity memory-safe-assembly assembly {