-
Notifications
You must be signed in to change notification settings - Fork 12.1k
Add Base58 library #5762
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Add Base58 library #5762
Changes from 20 commits
5a400eb
65292d5
99a1835
bddf4f6
88c03e7
a3c4667
41b586b
c6d6bdd
48bf13b
eebd51e
296a87e
8c94acc
d09ebfa
a25bd11
a4ce8c8
7474f2a
bef2e4f
ce1c5ad
c33e933
855a1c6
ec641c7
7429bcc
45edb76
20f3611
8e60a99
dd8e895
45f04b4
da84743
c80f693
f7ac27d
2696cd8
1736f38
8652d20
8098fb2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'openzeppelin-solidity': minor | ||
--- | ||
|
||
`Bytes`: Add `splice(bytes,uint256)` and `splice(bytes,uint256,uint256)`, two "in place" variants of the existing slice functions |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'openzeppelin-solidity': minor | ||
--- | ||
|
||
`Base58`: Add a library for encoding and decoding bytes buffers into base58 strings. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'openzeppelin-solidity': minor | ||
--- | ||
|
||
`Bytes`: Add `countLeading` and `countConsecutive` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
// SPDX-License-Identifier: MIT | ||
|
||
pragma solidity ^0.8.26; | ||
|
||
import {SafeCast} from "./math/SafeCast.sol"; | ||
import {Bytes} from "./Bytes.sol"; | ||
|
||
/** | ||
* @dev Provides a set of functions to operate with Base58 strings. | ||
* | ||
* Based on https://github.com/storyicon/base58-solidity/commit/807428e5174e61867e4c606bdb26cba58a8c5cb1[storyicon's implementation] (MIT). | ||
*/ | ||
library Base58 { | ||
using SafeCast for bool; | ||
using Bytes for bytes; | ||
|
||
error InvalidBase56Digit(uint8); | ||
|
||
/** | ||
* @dev Base58 encoding & decoding tables | ||
* See sections 2 of https://datatracker.ietf.org/doc/html/draft-msporny-base58-03 | ||
*/ | ||
bytes internal constant _TABLE = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; | ||
bytes internal constant _LOOKUP_TABLE = | ||
hex"000102030405060708ffffffffffffff090a0b0c0d0e0f10ff1112131415ff161718191a1b1c1d1e1f20ffffffffffff2122232425262728292a2bff2c2d2e2f30313233343536373839"; | ||
|
||
/** | ||
* @dev Encode a `bytes` buffer as a Base58 `string`. | ||
*/ | ||
function encode(bytes memory data) internal pure returns (string memory) { | ||
return string(_encode(data)); | ||
} | ||
|
||
/** | ||
* @dev Decode a Base58 `string` into a `bytes` buffer. | ||
*/ | ||
function decode(string memory data) internal pure returns (bytes memory) { | ||
return _decode(bytes(data)); | ||
} | ||
|
||
function _encode(bytes memory data) private pure returns (bytes memory encoded) { | ||
// For reference, solidity implementation | ||
// unchecked { | ||
// uint256 dataLeadingZeros = data.countLeading(0x00); | ||
// uint256 length = dataLeadingZeros + ((data.length - dataLeadingZeros) * 8351) / 6115 + 1; | ||
// encoded = new bytes(length); | ||
// uint256 end = length; | ||
// for (uint256 i = 0; i < data.length; ++i) { | ||
// uint256 ptr = length; | ||
// for (uint256 carry = uint8(data[i]); ptr > end || carry != 0; --ptr) { | ||
// carry += 256 * uint8(encoded[ptr - 1]); | ||
// encoded[ptr - 1] = bytes1(uint8(carry % 58)); | ||
// carry /= 58; | ||
// } | ||
// end = ptr; | ||
// } | ||
// uint256 encodedCLZ = encoded.countLeading(0x00); | ||
// length -= encodedCLZ - dataLeadingZeros; | ||
// encoded.splice(encodedCLZ - dataLeadingZeros); | ||
// for (uint256 i = 0; i < length; ++i) { | ||
// encoded[i] = _TABLE[uint8(encoded[i])]; | ||
// } | ||
// } | ||
|
||
// Assembly is ~50% cheaper for buffers of size 32. | ||
assembly ("memory-safe") { | ||
function clzBytes(ptr, length) -> i { | ||
for { | ||
i := 0 | ||
} and(iszero(byte(0, mload(add(ptr, i)))), lt(i, length)) { | ||
i := add(i, 1) | ||
} {} | ||
} | ||
|
||
encoded := mload(0x40) | ||
let dataLength := mload(data) | ||
|
||
// Count number of zero bytes at the beginning of `data`. These are encoded using the same number of '1's | ||
// at then beginning of the encoded string. | ||
let dataLeadingZeros := clzBytes(add(data, 0x20), dataLength) | ||
|
||
// Initial encoding length: 100% of zero bytes (zero prefix) + 138% of non zero bytes + 1 | ||
let slotLength := add(add(div(mul(sub(dataLength, dataLeadingZeros), 138), 100), dataLeadingZeros), 1) | ||
|
||
// Zero the encoded buffer | ||
for { | ||
let i := 0 | ||
} lt(i, slotLength) { | ||
i := add(i, 0x20) | ||
} { | ||
mstore(add(add(encoded, 0x20), i), 0) | ||
} | ||
Amxx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// Build the "slots" | ||
for { | ||
let i := 0 | ||
let end := slotLength | ||
} lt(i, dataLength) { | ||
i := add(i, 1) | ||
} { | ||
let ptr := slotLength | ||
for { | ||
let carry := byte(0, mload(add(add(data, 0x20), i))) | ||
} or(carry, lt(end, ptr)) { | ||
ptr := sub(ptr, 1) | ||
carry := div(carry, 58) | ||
} { | ||
carry := add(carry, mul(256, byte(0, mload(add(add(encoded, 0x1f), ptr))))) | ||
mstore8(add(add(encoded, 0x1f), ptr), mod(carry, 58)) | ||
} | ||
end := ptr | ||
} | ||
|
||
// Count number of zero bytes at the beginning of slots. This is a pointer to the first non zero slot that | ||
// contains the base58 data. This base58 data span over `slotLength-slotLeadingZeros` bytes. | ||
let slotLeadingZeros := clzBytes(add(encoded, 0x20), slotLength) | ||
|
||
// Update length: `slotLength-slotLeadingZeros` of non-zero data plus `dataLeadingZeros` of zero prefix. | ||
let offset := sub(slotLeadingZeros, dataLeadingZeros) | ||
let encodedLength := sub(slotLength, offset) | ||
|
||
// Store the encoding table. This overlaps with the FMP that we are going to reset later anyway. | ||
mstore(0x1f, "123456789ABCDEFGHJKLMNPQRSTUVWXY") | ||
mstore(0x3f, "Zabcdefghijkmnopqrstuvwxyz") | ||
|
||
// For each slot, use the table to obtain the corresponding base58 "digit". | ||
for { | ||
let i := 0 | ||
} lt(i, encodedLength) { | ||
i := add(i, 1) | ||
} { | ||
mstore8(add(add(encoded, 0x20), i), mload(byte(0, mload(add(add(encoded, 0x20), add(offset, i)))))) | ||
} | ||
|
||
// Store length and allocate (reserve) memory | ||
mstore(encoded, encodedLength) | ||
mstore(0x40, add(add(encoded, 0x20), encodedLength)) | ||
} | ||
} | ||
|
||
function _decode(bytes memory data) private pure returns (bytes memory) { | ||
unchecked { | ||
uint256 b58Length = data.length; | ||
|
||
uint256 size = 2 * ((b58Length * 8351) / 6115 + 1); | ||
bytes memory binu = new bytes(size); | ||
|
||
bytes memory cache = _LOOKUP_TABLE; | ||
uint256 outiLength = (b58Length + 3) / 4; | ||
// Note: allocating uint32[] would be enough, but solidity doesn't pack memory. | ||
uint256[] memory outi = new uint256[](outiLength); | ||
for (uint256 i = 0; i < data.length; ++i) { | ||
// get b58 char | ||
uint8 chr = uint8(data[i]); | ||
require(chr > 48 && chr < 123, InvalidBase56Digit(chr)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 48 and 123 are derived from the minimum and maximum values taken by b58 chars, see https://github.com/OpenZeppelin/openzeppelin-contracts/pull/5762/files#r2160061084 |
||
|
||
// decode b58 char | ||
uint256 carry = uint8(cache[chr - 49]); | ||
require(carry < 58, InvalidBase56Digit(chr)); | ||
|
||
for (uint256 j = outiLength; j > 0; --j) { | ||
uint256 value = carry + 58 * outi[j - 1]; | ||
carry = value >> 32; | ||
outi[j - 1] = value & 0xffffffff; | ||
} | ||
} | ||
|
||
uint256 ptr = 0; | ||
uint256 mask = ((b58Length - 1) % 4) + 1; | ||
for (uint256 j = 0; j < outiLength; ++j) { | ||
while (mask > 0) { | ||
--mask; | ||
binu[ptr] = bytes1(uint8(outi[j] >> (8 * mask))); | ||
++ptr; | ||
} | ||
mask = 4; | ||
} | ||
|
||
uint256 dataLeadingZeros = data.countLeading(0x31); | ||
uint256 msb = binu.countConsecutive(dataLeadingZeros, 0x00); | ||
return binu.splice(msb * (dataLeadingZeros + msb < binu.length).toUint(), ptr); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -68,6 +68,28 @@ library Bytes { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* @dev Count number of occurrences of `search` at the beginning of `buffer`. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
function countLeading(bytes memory buffer, bytes1 search) internal pure returns (uint256) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return countConsecutive(buffer, 0, search); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* @dev Count number of occurrences of `search` in `buffer`, starting from position `offset`. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
function countConsecutive(bytes memory buffer, uint256 offset, bytes1 search) internal pure returns (uint256 i) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
uint256 length = Math.saturatingSub(buffer.length, offset); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
assembly ("memory-safe") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
for { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
let ptr := add(add(buffer, 0x20), offset) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
i := 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} and(iszero(shr(248, xor(mload(add(ptr, i)), search))), lt(i, length)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
i := add(i, 1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+71
to
+92
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: incorrect byte comparison and potential OOB read in
Fix by reading only when - assembly ("memory-safe") {
- for {
- let ptr := add(add(buffer, 0x20), offset)
- i := 0
- } and(iszero(shr(248, xor(mload(add(ptr, i)), search))), lt(i, length)) {
- i := add(i, 1)
- } {}
- }
+ assembly ("memory-safe") {
+ let ptr := add(add(buffer, 0x20), offset)
+ // Iterate while in-bounds; break on first non-match.
+ for { i := 0 } lt(i, length) { i := add(i, 1) } {
+ if iszero(eq(byte(0, mload(add(ptr, i))), search))) { break }
+ }
+ } 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note: replacing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. . You're absolutely correct! Here's the analysis:
So my suggested fix The original code's However, I notice there's a syntax error in my suggestion - missing closing parenthesis. Here's the corrected version: assembly ("memory-safe") {
let ptr := add(add(buffer, 0x20), offset)
// Iterate while in-bounds; break on first non-match.
for { i := 0 } lt(i, length) { i := add(i, 1) } {
if iszero(eq(byte(0, mload(add(ptr, i))), search)) { break }
}
} The key insight is that both |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* @dev Copies the content of `buffer`, from `start` (included) to the end of `buffer` into a new bytes object in | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* memory. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -99,6 +121,35 @@ library Bytes { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return result; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* @dev In place slice: moves the content of `buffer`, from `start` (included) to the end of `buffer` to the start of that buffer. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* NOTE: This function modifies the provided buffer in place. If you need to preserve the original buffer, use {slice} instead | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
function splice(bytes memory buffer, uint256 start) internal pure returns (bytes memory) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return splice(buffer, start, buffer.length); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* @dev In place slice: moves the content of `buffer`, from `start` (included) to end (excluded) to the start of that buffer. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* NOTE: This function modifies the provided buffer in place. If you need to preserve the original buffer, use {slice} instead | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
function splice(bytes memory buffer, uint256 start, uint256 end) internal pure returns (bytes memory) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// sanitize | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
uint256 length = buffer.length; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
end = Math.min(end, length); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
start = Math.min(start, end); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// allocate and copy | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
assembly ("memory-safe") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
mcopy(add(buffer, 0x20), add(add(buffer, 0x20), start), sub(end, start)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
mstore(buffer, sub(end, start)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return buffer; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* @dev Reads a bytes32 from a bytes array without bounds checking. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
// SPDX-License-Identifier: MIT | ||
|
||
pragma solidity ^0.8.26; | ||
|
||
import {Test} from "forge-std/Test.sol"; | ||
import {Base58} from "@openzeppelin/contracts/utils/Base58.sol"; | ||
|
||
contract Base58Test is Test { | ||
function testEncodeDecodeEmpty() external pure { | ||
assertEq(Base58.decode(Base58.encode("")), ""); | ||
} | ||
Amxx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
function testEncodeDecodeZeros() external pure { | ||
bytes memory zeros = hex"0000000000000000"; | ||
assertEq(Base58.decode(Base58.encode(zeros)), zeros); | ||
|
||
bytes memory almostZeros = hex"00000000a400000000"; | ||
assertEq(Base58.decode(Base58.encode(almostZeros)), almostZeros); | ||
} | ||
|
||
function testEncodeDecode(bytes memory input) external pure { | ||
assertEq(Base58.decode(Base58.encode(input)), input); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For anyone curious, here is how you build these lookup tables: