From 56e7ebbb60c24ea27728ecf61f046e0d52bdeb07 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Thu, 26 Jun 2025 00:00:10 +0000 Subject: [PATCH 1/6] base58 --- src/utils/Base58.sol | 75 ++++++++++++++++++++++++++++++++++++++++++++ test/Base58.t.sol | 16 ++++++++++ 2 files changed, 91 insertions(+) create mode 100644 src/utils/Base58.sol create mode 100644 test/Base58.t.sol diff --git a/src/utils/Base58.sol b/src/utils/Base58.sol new file mode 100644 index 0000000000..64711af7d0 --- /dev/null +++ b/src/utils/Base58.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +/// @notice Library to encode strings in Base58. +/// @author Solady (https://github.com/vectorized/solady/blob/main/src/utils/Base58.sol) +/// @author Modified from OpenZeppelin (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Base58.sol) +library Base58 { + /// @dev Encodes `data` into a base58 string. + function encode(bytes memory data) internal pure returns (string memory result) { + /// @solidity memory-safe-assembly + assembly { + let l := mload(data) // `data.length`. + let b := add(data, 0x20) // Start of `data` bytes. + let z := 0 // Number of leading zero bytes in `data`. + for {} lt(byte(0, mload(add(b, z))), lt(z, l)) {} { z := add(1, z) } + + // Start the output offset by an over-estimate of the length. + let o := add(add(mload(0x40), 0x21), add(z, div(mul(sub(l, z), 8351), 6115))) + let e := o + + let limbs := o + let limbsEnd := limbs + + for { + let i := mod(l, 31) + if i { + mstore(limbsEnd, shr(shl(3, add(1, sub(31, i))), mload(b))) + limbsEnd := add(limbsEnd, 0x20) + } + } lt(i, l) { i := add(i, 31) } { + mstore(limbsEnd, shr(8, mload(add(b, i)))) + limbsEnd := add(limbsEnd, 0x20) + } + + // Use the scratch space for the lookup. We'll restore 0x40 later. + mstore(0x1f, "123456789ABCDEFGHJKLMNPQRSTUVWXY") + mstore(0x3f, "Zabcdefghijkmnopqrstuvwxyz") + + if iszero(eq(limbs, limbsEnd)) { + for {} 1 {} { + let anyNonZero := 0 + for { let i := limbs } 1 {} { + if mload(i) { + anyNonZero := 1 + break + } + i := add(i, 0x20) + if eq(i, limbsEnd) { break } + } + if iszero(anyNonZero) { break } + + let carry := 0 + for { let i := limbs } 1 {} { + let acc := add(shl(248, carry), mload(i)) + mstore(i, div(acc, 58)) + carry := mod(acc, 58) + i := add(i, 0x20) + if eq(i, limbsEnd) { break } + } + o := sub(o, 1) + mstore8(o, mload(carry)) + } + for { let i := 0 } iszero(eq(i, z)) { i := add(i, 1) } { + o := sub(o, 1) + mstore8(o, 49) // '1' in ASCII. + } + } + 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. + } + } +} diff --git a/test/Base58.t.sol b/test/Base58.t.sol new file mode 100644 index 0000000000..26531356e7 --- /dev/null +++ b/test/Base58.t.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "./utils/SoladyTest.sol"; +import {Base58} from "../src/utils/Base58.sol"; +import {LibString} from "../src/utils/LibString.sol"; + +contract Base58Test is SoladyTest { + function testBase58Encode() public { + // 0x000000000000000000001e3c8bf2dd5877fc13a2456ad7a584fe1629499985d685d6bf2e2983334225ec9dec91a445ce4f940fa4e75c3eee0436561dafe334 + bytes memory b = + hex"00001e3c8bf2dd5877fc13a2456ad7a584fe1629499985d6bf2e2983334225ec9dec91a445ce4f940fa4e75c3eee0436561dafe334ef0886895d9ce60d812a18d92ead188c4e550a9479f9e83765d603c1c0ee6b2142457b3407b2ec756faabb"; + string memory encoded = Base58.encode(b); + emit LogString(encoded); + } +} From a2dfd378e4f1e067883f9b156ef64351b2b7ee4a Mon Sep 17 00:00:00 2001 From: Vectorized Date: Thu, 26 Jun 2025 01:57:43 +0000 Subject: [PATCH 2/6] Squash --- README.md | 1 + docs/utils/base58.md | 44 +++++++++++ src/Milady.sol | 1 + src/utils/Base58.sol | 135 ++++++++++++++++++++++++------- test/Base58.t.sol | 184 +++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 329 insertions(+), 36 deletions(-) create mode 100644 docs/utils/base58.md diff --git a/README.md b/README.md index 2b763e5f59..b7719d8e85 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ tokens ├─ ERC721 — "Simple ERC721 implementation with storage hitchhiking" ├─ WETH — "Simple Wrapped Ether implementation" utils +├─ Base58 — "Library for Base58 encoding and decoding" ├─ Base64 — "Library for Base64 encoding and decoding" ├─ CallContextChecker — "Call context checker mixin" ├─ CREATE3 — "Deterministic deployments agnostic to the initialization code" diff --git a/docs/utils/base58.md b/docs/utils/base58.md new file mode 100644 index 0000000000..15fcd49c4b --- /dev/null +++ b/docs/utils/base58.md @@ -0,0 +1,44 @@ +# Base58 + +Library to encode strings in Base58. + + + + + + + + +## Custom Errors + +### Base58DecodingError() + +```solidity +error Base58DecodingError() +``` + +An unrecognized character was encountered or the carry has overflowed. + +## Encoding / Decoding + +### encode(bytes) + +```solidity +function encode(bytes memory data) + internal + pure + returns (string memory result) +``` + +Encodes `data` into a Base58 string. + +### decode(string) + +```solidity +function decode(string memory encoded) + internal + pure + returns (bytes memory result) +``` + +Decodes `encoded`, a Base58 string, into the original bytes. \ No newline at end of file diff --git a/src/Milady.sol b/src/Milady.sol index 2e14615553..60da0907ac 100644 --- a/src/Milady.sol +++ b/src/Milady.sol @@ -23,6 +23,7 @@ import "./tokens/ERC4626.sol"; import "./tokens/ERC6909.sol"; import "./tokens/ERC721.sol"; import "./tokens/WETH.sol"; +import "./utils/Base58.sol"; import "./utils/Base64.sol"; import "./utils/CREATE3.sol"; import "./utils/CallContextChecker.sol"; diff --git a/src/utils/Base58.sol b/src/utils/Base58.sol index 64711af7d0..85065b62f7 100644 --- a/src/utils/Base58.sol +++ b/src/utils/Base58.sol @@ -5,22 +5,35 @@ pragma solidity ^0.8.4; /// @author Solady (https://github.com/vectorized/solady/blob/main/src/utils/Base58.sol) /// @author Modified from OpenZeppelin (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Base58.sol) library Base58 { - /// @dev Encodes `data` into a base58 string. + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CUSTOM ERRORS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev An unrecognized character was encountered or the carry has overflowed. + error Base58DecodingError(); + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* ENCODING / DECODING */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Encodes `data` into a Base58 string. function encode(bytes memory data) internal pure returns (string memory result) { + uint256 l = data.length; + if (l == uint256(0)) return result; /// @solidity memory-safe-assembly assembly { - let l := mload(data) // `data.length`. let b := add(data, 0x20) // Start of `data` bytes. let z := 0 // Number of leading zero bytes in `data`. + // Count leading zero bytes. for {} lt(byte(0, mload(add(b, z))), lt(z, l)) {} { z := add(1, z) } // Start the output offset by an over-estimate of the length. - let o := add(add(mload(0x40), 0x21), add(z, div(mul(sub(l, z), 8351), 6115))) + let o := add(add(mload(0x40), 0x20), add(z, add(1, div(mul(sub(l, z), 8351), 6115)))) let e := o let limbs := o let limbsEnd := limbs - + // Populate the uint248 limbs. for { let i := mod(l, 31) if i { @@ -32,39 +45,39 @@ library Base58 { limbsEnd := add(limbsEnd, 0x20) } - // Use the scratch space for the lookup. We'll restore 0x40 later. + // Use the extended scratch space for the lookup. We'll restore 0x40 later. mstore(0x1f, "123456789ABCDEFGHJKLMNPQRSTUVWXY") mstore(0x3f, "Zabcdefghijkmnopqrstuvwxyz") - if iszero(eq(limbs, limbsEnd)) { - for {} 1 {} { - let anyNonZero := 0 - for { let i := limbs } 1 {} { - if mload(i) { - anyNonZero := 1 - break - } - i := add(i, 0x20) - if eq(i, limbsEnd) { break } + for {} 1 {} { + let anyNonZero := 0 + for { let i := limbs } 1 {} { + if mload(i) { + anyNonZero := 1 + break } - if iszero(anyNonZero) { break } - - let carry := 0 - for { let i := limbs } 1 {} { - let acc := add(shl(248, carry), mload(i)) - mstore(i, div(acc, 58)) - carry := mod(acc, 58) - i := add(i, 0x20) - if eq(i, limbsEnd) { break } - } - o := sub(o, 1) - mstore8(o, mload(carry)) + i := add(i, 0x20) + if eq(i, limbsEnd) { break } } - for { let i := 0 } iszero(eq(i, z)) { i := add(i, 1) } { - o := sub(o, 1) - mstore8(o, 49) // '1' in ASCII. + if iszero(anyNonZero) { break } + + let carry := 0 + for { let i := limbs } 1 {} { + let acc := add(shl(248, carry), mload(i)) + mstore(i, div(acc, 58)) + carry := mod(acc, 58) + i := add(i, 0x20) + if eq(i, limbsEnd) { break } } + o := sub(o, 1) + mstore8(o, mload(carry)) + } + // We probably can optimize this more by writing 32 bytes at a time. + for { let i := 0 } iszero(eq(i, z)) { i := add(i, 1) } { + o := sub(o, 1) + mstore8(o, 49) // '1' in ASCII. } + 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. @@ -72,4 +85,66 @@ library Base58 { 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; + if (n == uint256(0)) return result; + /// @solidity memory-safe-assembly + assembly { + let s := add(encoded, 0x20) + let z := 0 // Number of leading '1' in `data`. + // Count leading '1'. + for {} and(eq(49, byte(0, mload(add(s, z)))), lt(z, n)) {} { z := add(1, z) } + + // Start the output offset by an over-estimate of the length. + let o := add(add(mload(0x40), 0x20), add(z, add(1, div(mul(sub(n, z), 7736), 10000)))) + let e := o + let limbs := o + let limbsEnd := limbs + // Use the extended scratch space for the lookup. We'll restore 0x40 later. + mstore(0x2a, 0x30313233343536373839) + mstore(0x20, 0x1718191a1b1c1d1e1f20ffffffffffff2122232425262728292a2bff2c2d2e2f) + mstore(0x00, 0x000102030405060708ffffffffffffff090a0b0c0d0e0f10ff1112131415ff16) + + for { let j := 0 } 1 {} { + let c := sub(byte(0, mload(add(s, j))), 49) + if iszero(and(shl(c, 1), 0x3fff7ff03ffbeff01ff)) { + mstore(0x00, 0xe8fad793) // `Base58DecodingError()`. + revert(0x1c, 0x04) + } + let carry := byte(0, mload(c)) + for { let i := limbs } iszero(eq(i, limbsEnd)) { i := add(i, 0x20) } { + let acc := add(carry, mul(58, mload(i))) + mstore(i, shr(8, shl(8, acc))) + carry := shr(248, acc) + } + if carry { + if iszero(lt(carry, 58)) { + mstore(0x00, 0xe8fad793) // `Base58DecodingError()`. + revert(0x1c, 0x04) + } + mstore(limbsEnd, carry) + limbsEnd := add(limbsEnd, 0x20) + } + j := add(j, 1) + if eq(j, n) { break } + } + // Copy and compact the uint248 limbs. + for { let i := limbs } iszero(eq(i, limbsEnd)) { i := add(i, 0x20) } { + o := sub(o, 31) + mstore(sub(o, 1), mload(i)) + } + // Strip any leading zeros from the limbs. + for {} lt(byte(0, mload(o)), lt(o, e)) {} { o := add(o, 1) } + o := sub(o, z) // Move back for the leading zero bytes. + calldatacopy(o, calldatasize(), z) // Fill the leading zero bytes. + + let l := sub(e, o) // Compute the final length. + result := sub(o, 0x20) // Move back one word for the length. + mstore(result, l) // Store the length. + mstore(add(add(result, 0x20), l), 0) // Zeroize the slot after the bytes. + mstore(0x40, add(add(result, 0x40), l)) // Allocate memory. + } + } } diff --git a/test/Base58.t.sol b/test/Base58.t.sol index 26531356e7..438084191a 100644 --- a/test/Base58.t.sol +++ b/test/Base58.t.sol @@ -6,11 +6,183 @@ import {Base58} from "../src/utils/Base58.sol"; import {LibString} from "../src/utils/LibString.sol"; contract Base58Test is SoladyTest { - function testBase58Encode() public { - // 0x000000000000000000001e3c8bf2dd5877fc13a2456ad7a584fe1629499985d685d6bf2e2983334225ec9dec91a445ce4f940fa4e75c3eee0436561dafe334 - bytes memory b = - hex"00001e3c8bf2dd5877fc13a2456ad7a584fe1629499985d6bf2e2983334225ec9dec91a445ce4f940fa4e75c3eee0436561dafe334ef0886895d9ce60d812a18d92ead188c4e550a9479f9e83765d603c1c0ee6b2142457b3407b2ec756faabb"; - string memory encoded = Base58.encode(b); - emit LogString(encoded); + function testBase58EncodeDecode(bytes memory data, uint256 r) public { + if (r & 0x00f == 0) { + _brutalizeMemory(); + } + if (r & 0x0f0 == 0) { + _misalignFreeMemoryPointer(); + } + if (r & 0xf00 == 0) { + data = abi.encodePacked(new bytes(_bound(_random(), 0, 128)), data); + } + + uint256 h; + uint256 m; + /// @solidity memory-safe-assembly + assembly { + // Since `encode` writes memory backwards, we do some extra checks to ensure + // that the initial length overestimate is sufficient. + mstore(0x00, r) + mstore(0x20, "hehe") + h := keccak256(0x00, 0x40) + m := mload(0x40) + mstore(m, h) + mstore(0x40, add(m, 0x20)) + } + string memory encoded = Base58.encode(data); + /// @solidity memory-safe-assembly + assembly { + if iszero(eq(mload(m), h)) { invalid() } + } + + _checkMemory(encoded); + if (r & 0x00f000 == 0) { + _brutalizeMemory(); + } + if (r & 0x0f0000 == 0) { + _misalignFreeMemoryPointer(); + } + + /// @solidity memory-safe-assembly + assembly { + // Since `decode` writes memory backwards, we do some extra checks to ensure + // that the initial length overestimate is sufficient. + mstore(0x00, r) + mstore(0x20, "haha") + h := keccak256(0x00, 0x40) + m := mload(0x40) + mstore(m, h) + mstore(0x40, add(m, 0x20)) + } + bytes memory decoded = Base58.decode(encoded); + /// @solidity memory-safe-assembly + assembly { + if iszero(eq(mload(m), h)) { invalid() } + } + + _checkMemory(decoded); + assertEq(data, decoded); + } + + function testBase58EncodeDecode() public { + this._testBase58EncodeDecode(hex"", ""); + this._testBase58EncodeDecode(hex"0d", "E"); + this._testBase58EncodeDecode(hex"000e", "1F"); + this._testBase58EncodeDecode(hex"00f3", "15C"); + this._testBase58EncodeDecode(hex"00", "1"); + this._testBase58EncodeDecode(hex"f2", "5B"); + this._testBase58EncodeDecode(hex"0002da", "1Db"); + this._testBase58EncodeDecode(hex"0027b9", "142L"); + this._testBase58EncodeDecode(hex"00d80f", "1HSe"); + this._testBase58EncodeDecode(hex"ce", "4Z"); + this._testBase58EncodeDecode(hex"7c", "39"); + this._testBase58EncodeDecode(hex"cd0b5dfe722552f609ce", "CX9VkoSqX63kbo"); + this._testBase58EncodeDecode( + hex"00598b3dc0966af86beb7898fc9921c2fbc38a19d52dee9dfed69e3d", + "1D6w66tNCxvikkpma3BXnRnABJQojACXjHxtdJ" + ); + this._testBase58EncodeDecode( + hex"09100a2fc14628f168c2c9b980fb840857fbb9fe031013c9bf7e218d5c", + "Qs1VMdvTSeZkZ5p4e4xQaLa8J3ptpJzJAcM1Mp7" + ); + this._testBase58EncodeDecode( + hex"001d85089c34888205378be7e8f9ff5e2f", "14eRVxHMi5hh14FM9Gpd1Ua" + ); + this._testBase58EncodeDecode( + hex"0090ccbb306b1cc8f226e905623d19604fd0ad73bd80b8b4712e", + "121GLNsu9Tdp147zdSjFvJudL1pp1Qv39myF" + ); + this._testBase58EncodeDecode(hex"012ee97bcab1", "bB9gNQp"); + this._testBase58EncodeDecode( + hex"00f91f623af2d76e8ee2abdbfe5e3671373ad4736d2433397c93e08e63e9ce1830", + "1HmUGpDZUwcvX5xPMNQ9oHoMz8nKQF6EWgqno2iSXQJc7" + ); + this._testBase58EncodeDecode( + hex"00000000000000000000000000000000000000000000000000000000000000fb53beb02ab2ab6583638677b592b2b56f321d94972b38acfd6d4cd1202f77ff1fddf68b9d2c4bdb1b6ced6ef31e282e48790854ce9c0ab93435761d0f5db1e16817119e682391a23f633d9cdd6481a07585ec17d6aeca0849eab41f5895cfc4e9503f97345a364964d7e024c947ae7c238d1a4705", + "1111111111111111111111111111111QVs5qPAkBrBEtm1UXSAcNGHgA6cUYDn4oAXAxgEQ5jntH1aWoie6t7a1j2RTmP4E5uGFpWwTUj7zyeKcs7BKMXJBRXHuokJ13KmbHC6RLtAbUdobwBcjx2UjCK5rPwVBjABFvjgGAaFFwEZgnRPudGLqLqJdCx6G" + ); + this._testBase58EncodeDecode( + hex"000000000000000000000000000000000000000031f89a3264997ab236bf9c5200a5353c4e04a134ad572583a140f9c3cc7d4f3f6331716c", + "11111111111111111111P1So9spX62PHGLRTG8SgU1Lm19f6SgCozbp8Use7LdcMGYAKD" + ); + this._testBase58EncodeDecode( + hex"000000000000000000000013bc3b22341190c36a1cda2a0a1ed6f93a080447160b626b2f711c9a266bdc17622794eb9d", + "11111111111fN3TfvHm4xfWWJT7FqjTP9WXJiCoKJejeRgQmU6LcWUgJezrH2" + ); + this._testBase58EncodeDecode( + hex"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b1859e09c90d4ac3dd4c89c3c2452b4d22c92d0eee246e75d72d7209078d5230159b5e52249d5b6017e6992696028d390c61d26c0d42395072378ad89df7b94dbef0624cd0e1e091829c6292e9cf8303b43bec", + "1111111111111111111111111111111111111111111111111113sLfBS3DP3qb3hQoRZDt1DotgrCJU5N17jh4bFG6cs5KGz4Z1wqWJC6bHVbgKYXAUMVvoqFbZAzC5Rg95xvsbmhTLkuH3jbPkqiXuGcRm2wGmiom8f" + ); + this._testBase58EncodeDecode( + hex"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000322a08fd25e4a383cf093664c6a28531bdfa2e6d3a0afdddae9a58fe1074642f979d6cc1dd196e77d63fac03e9e52815bb211a760e37470b006e9682a8d432", + "11111111111111111111111111111111111111111111111111111111111EBQAEDu1wmQTkQGGtoxiwzZzxPp3q6jswQS8BoqBuKzxhmVg8ThLq8Z6HjrcBPX5BsGYWTA7sjHP9CeASeFEsj" + ); + this._testBase58EncodeDecode( + hex"00000000000000c305c2e9ca1fed1817ff8ddc60fb26b5665ce958f9cbeb3f907e6ae500d5917d24b3b30b0d9e382e9521eeb232c7f5d328f0e239cec44d21d49472727a1ec7555580c88f2776", + "11111119a4d2p5cardGk5zgtKRV5xmbugoNWw3fp8eWTq3sjbVTYr7aXiE3wGzqe5bGgusiYsBzvdPibo5BVNxaxudCXc6WkikU8xwP" + ); + this._testBase58EncodeDecode( + hex"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005fcee37b107ba13473fff385e60e48f7085c72ca9fa64af6c21a568a281161db2af9844f52867ea6048a502da3dd827158b199f1c7e330028b849e135d7f5418923e", + "11111111111111111111111111111111111111111111111111eKPzXLFs4zguFbvYgRSoJDb6mTyCPFhve4Yww4PNDs89z8Kxd72pkXqDix6FkN62WxgNfrNry43pbizFaUuzaExHCy" + ); + this._testBase58EncodeDecode( + hex"00000013dff1618a82531e334a62f0de8f17c074732abf4c59cd7c", + "1112p67UFfiuvQD92aDbWzpm1Z5EU6Q8TtpB" + ); + this._testBase58EncodeDecode( + hex"00000000000000000000003fd220658684b1ccef6552", "11111111111GpuXVbXqt99pach" + ); + this._testBase58EncodeDecode( + hex"0000000000000000000000000000000000000000000000000000e8271fe396fe0cf6bcc1f7881c9cbae4d147bbb61e947fb129177acd9887c0dca8348ffd2a385ecdac852073b60d7daf003d6e188c1841ddc5b68c3b9e3eda4c090f3567c54bd372676ee0ff9fbfede9", + "111111111111111111111111113Xj8EySrK3wEvmYt59y4b1vxzjGwPc688wZA4jXsyGNgEh7ghaP9ohvqseUcNUUCeqLxtyzWDPfngB1iHzjsEeaN14UjSeSZdCoWUSVHAYVtX2" + ); + this._testBase58EncodeDecode( + hex"0000000000000000000000000000000000000000000000000000000000000000000000003c58cebe89322eee5505a37a842fb64cd726c8a8adaa8a20c34de8eaff08373abe2f5912c912dde618fcb0b4a8a14da4f3fbc7d6004684fcedd7f133af9abe9360793485dbe855f3875e631c96d8ee775240cac29f2c8640aa9485f8c6b6410c0ac7fc83", + "1111111111111111111111111111111111113M2hXzVT7xgwnteR7ZsprbP5xBdDBWkkQHpVUJScKLca3WAfPSCUQ59d3a4zGU4P2Q5Dvgmz9LVbf1erXBmxLsha5PEhFmHDBpyGW5ZFSJswPtGRYQeVPWKpr3cbQbexUFsNpqVea" + ); + this._testBase58EncodeDecode( + hex"000000000000000000000000000000000000000000000000000000000000000060234520525c5c627553370b53eb6d76c7766490efc4dae6fd5c5940008b5110eb834a2168c9728d51840c4e571321d4f08391009a0c3785c6c6b9b14d774629995acf59bf07f88b2762b426ddd135516a24daf2", + "111111111111111111111111111111117rbt7xcf57aaNKwQwTxYHWqqtGYNiSM63bYVGhBKNExhvAubcT68EToxSShmawAr33vALSbua2s1xt9C6yPCXU5dGA8cr1B8GS6WXVCh24heoNrUbSR" + ); + this._testBase58EncodeDecode( + hex"000000000000000000000000000000000000000000000000000000000000000000003b4c61dee11d868b463c055521a78d6552daefdfdc3fc03216cd84667365c0346a2954fe099bdf4baea658b3cee9589c6d217f8b3642", + "11111111111111111111111111111111115nJEXtj9QijtXd2gqb9bgPvanHGcXLdhKX14UbmuitdejFDnZ5MiGpHALxfnMVvo4CbVKcPHhK" + ); + this._testBase58EncodeDecode( + hex"000000000000000000000000000013ba6b7a5b28ee62e23e2f037169950bfaa76a49cd560bf283cb7a76eab12b0766f61979108cd0bed77d37", + "111111111111112THktPmYvuwnWuVqbsNgWfAaR1dxCTi6zHB6ggkAidRRhwrrWmL3PGGCJ2J" + ); + this._testBase58EncodeDecode( + hex"000000000000000000000000000000000000000000000000000000000000000000000000000000e357a949bd269668402d9fe64611b55659fa5c077ed6896ba83c99f6362ff3ddfb145512bd7825d25d99b62f392d42c397191d91a85cb3aac3f5aba7a1f8c7c5098ea8d8e452eab896ce53510d58f64518509dc4c9a93f9feccb8040b18fc8065e9811e954c4c421264d528ae0817f4981a11bfe", + "1111111111111111111111111111111111111115pJo5spTW949hEVQhtDHsyhLcMfUC6gHCM9pPGYter8CD12cNuXbshZvtDZoFAjwjuCftRhweQ7TcuARE52aqNzC5He3rqs1YdEreCTYqoPuQDqufzeDjywYJQmkgwaLrDbhKcjUbxFVMertvBGeUkB7tDiSMBs" + ); + this._testBase58EncodeDecode( + hex"00000000000000000000000000000000000000af64ba6caafbbca128653eff8c51", + "111111111111111111127UskEy2WhgsTGAY4Qi4" + ); + this._testBase58EncodeDecode( + hex"0000000000000000000000000f744c6b510384801d5d10035f00b562f05c585aac1fe57f27b640096aafc1cb28a859d7ece16ee8c6813708193047aafd18c4", + "111111111111qkJMpKZLaSweHBvLEBUxhcB7AnaBv6QRC7mkQS54QEuh8PYp7pn7VfueHTsD4W1gF7px3" + ); + this._testBase58EncodeDecode( + hex"0000000000000000000000000000000000000000a4ae531a4546129e810fe716bef089bf466eed25dd729688c82481c3eea5cec22219d7dfcccb814a141dd14b98677f905c5efbc38f65040216f27042dcdf31ac81eff75985176ed0a40ae16eadca11464e40f16ee8fc2fd06ec6629d098759365df73073e4d4124b6003457367a484dc278b2e", + "11111111111111111111nqeSEB5S7wspidSeWX3LCxrjoSxFcAEE3C9LFix6tQHFWPMUWMSoFf2rStXF5JxarB4Dhxvnhbanz6mLyMerUKxU6WFag1vQMgCTLeeNHcRiu1srGtHB3YrVgoz6h2mAHyhs1ukVrGYmSmmKRty75Jx4zJ661" + ); + this._testBase58EncodeDecode( + hex"000000451c0d1e6a9b414cd8ee1d1fa7f7805de82932a48991c9edaf814215d069d5f1fef3a63f931792b2d113ce0a309d5e22a1d9ede1cca2e7e358e9d2600498f2e9a0c8159cebffa293512fe5b0f3d9971bb2a07d1b7df5f81af612141e4693147428285c21621c8e772a627ca1a9", + "1119kd1Rja2XCCMQDiRBv7EEX5upePsrmhBRHL4kkK8oBR3dSvVE4yuRtUzZKmtvWiBUD6qFk3HCoWpDapRw7WhBi3qR94ZeLkFuAZbqN4D2N4V6AgLdh2n1JwM3Z8i9quwNPHY4TNU2B2NYwZ2tons2" + ); + this._testBase58EncodeDecode( + hex"0000000000000000000000000000cc50f101efa6b062341bdc59edc70e9776728db82f5779100210d5907ebbe56673ef72e013987a297d560ad9e2229c508ce3568e5dd0f61e671e15f21521f9206a435c634b1f0d254326965d6c6eac24aec4b7fecc84b753d76d4e1bca902d662deb23a678f6cc1811", + "111111111111114xLrKJa5yzRCpMhRUMMv6WoK9hkibnv9rVbDQgxq5oWvSUv3UBJUjQ5xeFapu4ZZ4nDoi1C34c115cirJd5f1LucLojD45CuiGWNbCFQsPcQPfpkheZe1b5xWGsBPZzvZHxUaik4YAtk5sEg" + ); + } + + function _testBase58EncodeDecode(bytes memory data, string memory expectedEncoded) public { + string memory encoded = Base58.encode(data); + assertEq(expectedEncoded, encoded); + bytes memory decoded = Base58.decode(encoded); + assertEq(data, decoded); } } From ddbe3ecb74e55af10dd8cafd5095da77e0f7d414 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Thu, 26 Jun 2025 02:20:46 +0000 Subject: [PATCH 3/6] Optimize and add halmos check for the carry trick --- src/utils/Base58.sol | 8 +++----- test/Base58.t.sol | 11 +++++++++++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/utils/Base58.sol b/src/utils/Base58.sol index 85065b62f7..35d89f6800 100644 --- a/src/utils/Base58.sol +++ b/src/utils/Base58.sol @@ -9,7 +9,7 @@ library Base58 { /* CUSTOM ERRORS */ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ - /// @dev An unrecognized character was encountered or the carry has overflowed. + /// @dev An unrecognized character was encountered during decoding. error Base58DecodingError(); /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ @@ -109,6 +109,7 @@ library Base58 { for { let j := 0 } 1 {} { let c := sub(byte(0, mload(add(s, j))), 49) + // Check if the input character is valid. if iszero(and(shl(c, 1), 0x3fff7ff03ffbeff01ff)) { mstore(0x00, 0xe8fad793) // `Base58DecodingError()`. revert(0x1c, 0x04) @@ -119,11 +120,8 @@ library Base58 { mstore(i, shr(8, shl(8, acc))) carry := shr(248, acc) } + // Carry will always be < 58. if carry { - if iszero(lt(carry, 58)) { - mstore(0x00, 0xe8fad793) // `Base58DecodingError()`. - revert(0x1c, 0x04) - } mstore(limbsEnd, carry) limbsEnd := add(limbsEnd, 0x20) } diff --git a/test/Base58.t.sol b/test/Base58.t.sol index 438084191a..920623c027 100644 --- a/test/Base58.t.sol +++ b/test/Base58.t.sol @@ -185,4 +185,15 @@ contract Base58Test is SoladyTest { bytes memory decoded = Base58.decode(encoded); assertEq(data, decoded); } + + function testCarryBoundsTrick(uint248 limb, uint8 carry) public pure { + if (carry < 58) { + uint256 acc = uint256(limb) * 58 + uint256(carry); + assert((acc >> 248) < 58); + } + } + + function check_CarryBoundsTrick(uint248 limb, uint8 carry) public pure { + testCarryBoundsTrick(limb, carry); + } } From 43d9ff467492b71dfcaf3c7a71b626e2b4e36f2d Mon Sep 17 00:00:00 2001 From: Vectorized Date: Thu, 26 Jun 2025 02:33:37 +0000 Subject: [PATCH 4/6] Optimize --- src/utils/Base58.sol | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/utils/Base58.sol b/src/utils/Base58.sol index 35d89f6800..3e94de2f35 100644 --- a/src/utils/Base58.sol +++ b/src/utils/Base58.sol @@ -50,19 +50,16 @@ library Base58 { mstore(0x3f, "Zabcdefghijkmnopqrstuvwxyz") for {} 1 {} { - let anyNonZero := 0 - for { let i := limbs } 1 {} { - if mload(i) { - anyNonZero := 1 - break - } + let i := limbs + for {} 1 {} { + if mload(i) { break } i := add(i, 0x20) if eq(i, limbsEnd) { break } } - if iszero(anyNonZero) { break } + if eq(i, limbsEnd) { break } let carry := 0 - for { let i := limbs } 1 {} { + for { i := limbs } 1 {} { let acc := add(shl(248, carry), mload(i)) mstore(i, div(acc, 58)) carry := mod(acc, 58) @@ -102,6 +99,7 @@ library Base58 { let e := o let limbs := o let limbsEnd := limbs + let limbMask := shr(8, not(0)) // Use the extended scratch space for the lookup. We'll restore 0x40 later. mstore(0x2a, 0x30313233343536373839) mstore(0x20, 0x1718191a1b1c1d1e1f20ffffffffffff2122232425262728292a2bff2c2d2e2f) @@ -117,7 +115,7 @@ library Base58 { let carry := byte(0, mload(c)) for { let i := limbs } iszero(eq(i, limbsEnd)) { i := add(i, 0x20) } { let acc := add(carry, mul(58, mload(i))) - mstore(i, shr(8, shl(8, acc))) + mstore(i, and(limbMask, acc)) carry := shr(248, acc) } // Carry will always be < 58. From afb280a1a911019eeaa8f4285841bd5c7d0ec001 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Thu, 26 Jun 2025 02:38:35 +0000 Subject: [PATCH 5/6] Optimize --- src/utils/Base58.sol | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/utils/Base58.sol b/src/utils/Base58.sol index 3e94de2f35..fcbe89b4f0 100644 --- a/src/utils/Base58.sol +++ b/src/utils/Base58.sol @@ -49,6 +49,7 @@ library Base58 { mstore(0x1f, "123456789ABCDEFGHJKLMNPQRSTUVWXY") mstore(0x3f, "Zabcdefghijkmnopqrstuvwxyz") + let w := not(0) // -1. for {} 1 {} { let i := limbs for {} 1 {} { @@ -66,12 +67,12 @@ library Base58 { i := add(i, 0x20) if eq(i, limbsEnd) { break } } - o := sub(o, 1) + o := add(o, w) mstore8(o, mload(carry)) } // We probably can optimize this more by writing 32 bytes at a time. - for { let i := 0 } iszero(eq(i, z)) { i := add(i, 1) } { - o := sub(o, 1) + for {} z { z := add(z, w) } { + o := add(o, w) mstore8(o, 49) // '1' in ASCII. } From 165860ce3a2aa6a6c39e826128ea6261365eba2d Mon Sep 17 00:00:00 2001 From: Vectorized Date: Thu, 26 Jun 2025 02:39:42 +0000 Subject: [PATCH 6/6] Regen docs --- docs/utils/base58.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/utils/base58.md b/docs/utils/base58.md index 15fcd49c4b..cc55a2312c 100644 --- a/docs/utils/base58.md +++ b/docs/utils/base58.md @@ -17,7 +17,7 @@ Library to encode strings in Base58. error Base58DecodingError() ``` -An unrecognized character was encountered or the carry has overflowed. +An unrecognized character was encountered during decoding. ## Encoding / Decoding