-
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 30 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
d0ece81
3974f6d
59b2866
66ef584
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,207 @@ | ||||||||||
// SPDX-License-Identifier: MIT | ||||||||||
|
||||||||||
pragma solidity ^0.8.20; | ||||||||||
|
||||||||||
/** | ||||||||||
* @dev Provides a set of functions to operate with Base58 strings. | ||||||||||
* | ||||||||||
* Initially based on https://github.com/storyicon/base58-solidity/commit/807428e5174e61867e4c606bdb26cba58a8c5cb1[storyicon's implementation] (MIT). | ||||||||||
* Based on the updated and improved https://github.com/Vectorized/solady/blob/main/src/utils/Base58.sol[Vectorized version] (MIT). | ||||||||||
*/ | ||||||||||
library Base58 { | ||||||||||
error InvalidBase58Digit(bytes1); | ||||||||||
|
||||||||||
/** | ||||||||||
* @dev Encode a `bytes` buffer as a Base58 `string`. | ||||||||||
*/ | ||||||||||
function encode(bytes memory input) internal pure returns (string memory) { | ||||||||||
return string(_encode(input)); | ||||||||||
} | ||||||||||
|
||||||||||
/** | ||||||||||
* @dev Decode a Base58 `string` into a `bytes` buffer. | ||||||||||
*/ | ||||||||||
function decode(string memory input) internal pure returns (bytes memory) { | ||||||||||
return _decode(bytes(input)); | ||||||||||
} | ||||||||||
|
||||||||||
function _encode(bytes memory input) private pure returns (bytes memory output) { | ||||||||||
uint256 inputLength = input.length; | ||||||||||
if (inputLength == 0) return ""; | ||||||||||
|
||||||||||
assembly ("memory-safe") { | ||||||||||
// Count number of zero bytes at the beginning of `input`. These are encoded using the same number of '1's | ||||||||||
// at then beginning of the encoded string. | ||||||||||
Amxx marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
let inputLeadingZeros := 0 | ||||||||||
for {} lt(byte(0, mload(add(add(input, 0x20), inputLeadingZeros))), lt(inputLeadingZeros, inputLength)) {} { | ||||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
inputLeadingZeros := add(inputLeadingZeros, 1) | ||||||||||
} | ||||||||||
|
||||||||||
// Start the output offset by an over-estimate of the length. | ||||||||||
let outputLengthEstim := add(inputLeadingZeros, div(mul(sub(inputLength, inputLeadingZeros), 8351), 6115)) | ||||||||||
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. FYI: |
||||||||||
|
||||||||||
// This is going to be our "scratch" workspace. Be leave enough room on the left to store length + encoded input. | ||||||||||
Amxx marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
let scratch := add(mload(0x40), add(outputLengthEstim, 0x21)) | ||||||||||
|
||||||||||
// Cut the input buffer in section (limbs) of 31 bytes (248 bits). Store in scratch. | ||||||||||
let ptr := scratch | ||||||||||
for { | ||||||||||
// first section is possibly smaller than 31 bytes | ||||||||||
let i := mod(inputLength, 31) | ||||||||||
// unfold first loop, with a different shift. | ||||||||||
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.
Suggested change
|
||||||||||
if i { | ||||||||||
mstore(ptr, shr(mul(sub(32, i), 8), mload(add(input, 0x20)))) | ||||||||||
ptr := add(ptr, 0x20) | ||||||||||
} | ||||||||||
} lt(i, inputLength) { | ||||||||||
ptr := add(ptr, 0x20) // next limb | ||||||||||
i := add(i, 31) // move in buffer | ||||||||||
} { | ||||||||||
// Load 32 bytes from the input buffer and shift to only keep the 31 leftmost. | ||||||||||
Amxx marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
mstore(ptr, shr(8, mload(add(add(input, 0x20), i)))) | ||||||||||
} | ||||||||||
|
||||||||||
// Store the encoding table. This overlaps with the FMP that we are going to reset later anyway. | ||||||||||
// See sections 2 of https://inputtracker.ietf.org/doc/html/draft-msporny-base58-03 | ||||||||||
mstore(0x1f, "123456789ABCDEFGHJKLMNPQRSTUVWXY") | ||||||||||
mstore(0x3f, "Zabcdefghijkmnopqrstuvwxyz") | ||||||||||
|
||||||||||
// Encoding the "input" part of the result. | ||||||||||
// `output` point the the left part of the encoded string. we start from scratch, which means we have | ||||||||||
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.
Suggested change
|
||||||||||
// outputLengthEstim bytes to work with before hitting the FMP | ||||||||||
for { | ||||||||||
output := scratch | ||||||||||
} 1 {} { | ||||||||||
// check if there are non-zero limbs remaining | ||||||||||
let i := scratch | ||||||||||
for {} and(iszero(mload(i)), lt(i, ptr)) { | ||||||||||
i := add(i, 0x20) | ||||||||||
} {} | ||||||||||
if eq(i, ptr) { | ||||||||||
break | ||||||||||
} | ||||||||||
|
||||||||||
// base 58 arithmetic on the 248bits limbs | ||||||||||
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.
Suggested change
|
||||||||||
let carry := 0 | ||||||||||
for { | ||||||||||
i := scratch | ||||||||||
} lt(i, ptr) { | ||||||||||
i := add(i, 0x20) | ||||||||||
} { | ||||||||||
let acc := add(shl(248, carry), mload(i)) | ||||||||||
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.
Suggested change
|
||||||||||
mstore(i, div(acc, 58)) | ||||||||||
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.
Suggested change
|
||||||||||
carry := mod(acc, 58) | ||||||||||
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.
Suggested change
|
||||||||||
} | ||||||||||
|
||||||||||
// encode carry using base58 table, and add it to the output | ||||||||||
output := sub(output, 1) | ||||||||||
mstore8(output, mload(carry)) | ||||||||||
} | ||||||||||
|
||||||||||
// Write the input leading zeros at the left of the encoded. | ||||||||||
// This will spill to the left into the "length" of the buffer. | ||||||||||
for { | ||||||||||
let i := 0 | ||||||||||
} lt(i, inputLeadingZeros) {} { | ||||||||||
i := add(i, 0x20) | ||||||||||
mstore(sub(output, i), "11111111111111111111111111111111") | ||||||||||
} | ||||||||||
|
||||||||||
// Move output pointer to account for inputLeadingZeros | ||||||||||
output := sub(output, add(inputLeadingZeros, 0x20)) | ||||||||||
|
||||||||||
// Store length and allocate (reserve) memory up to scratch. | ||||||||||
mstore(output, sub(scratch, add(output, 0x20))) | ||||||||||
mstore(0x40, scratch) | ||||||||||
} | ||||||||||
} | ||||||||||
|
||||||||||
function _decode(bytes memory input) private pure returns (bytes memory output) { | ||||||||||
bytes4 errorSelector = InvalidBase58Digit.selector; | ||||||||||
|
||||||||||
uint256 inputLength = input.length; | ||||||||||
if (inputLength == 0) return ""; | ||||||||||
|
||||||||||
/// @solidity memory-safe-assembly | ||||||||||
assembly { | ||||||||||
let inputLeadingZeros := 0 // Number of leading '1' in `input`. | ||||||||||
// Count leading zeros. In base58, zeros are represented using '1' (chr(49)). | ||||||||||
for {} and( | ||||||||||
eq(byte(0, mload(add(add(input, 0x20), inputLeadingZeros))), 49), | ||||||||||
lt(inputLeadingZeros, inputLength) | ||||||||||
) {} { | ||||||||||
inputLeadingZeros := add(inputLeadingZeros, 1) | ||||||||||
} | ||||||||||
|
||||||||||
// Start the output offset by an over-estimate of the length. | ||||||||||
let outputLengthEstim := add(inputLeadingZeros, div(mul(sub(inputLength, inputLeadingZeros), 6115), 8351)) | ||||||||||
|
||||||||||
// This is going to be our "scratch" workspace. Be leave enough room on the left to store length + encoded input. | ||||||||||
let scratch := add(mload(0x40), add(outputLengthEstim, 0x21)) | ||||||||||
|
||||||||||
// Store the decoding table. This overlaps with the FMP that we are going to reset later anyway. | ||||||||||
mstore(0x2a, 0x30313233343536373839) | ||||||||||
mstore(0x20, 0x1718191a1b1c1d1e1f20ffffffffffff2122232425262728292a2bff2c2d2e2f) | ||||||||||
mstore(0x00, 0x000102030405060708ffffffffffffff090a0b0c0d0e0f10ff1112131415ff16) | ||||||||||
|
||||||||||
// Decode each char of the input string, and stored that in section (limbs) of 31 bytes. Store in scratch. | ||||||||||
let ptr := scratch | ||||||||||
let mask := shr(8, not(0)) | ||||||||||
for { | ||||||||||
let j := 0 | ||||||||||
} lt(j, inputLength) { | ||||||||||
j := add(j, 1) | ||||||||||
} { | ||||||||||
// for each char, decode it ... | ||||||||||
let c := sub(byte(0, mload(add(add(input, 0x20), j))), 49) | ||||||||||
// slither-disable-next-line incorrect-shift | ||||||||||
if iszero(and(shl(c, 1), 0x3fff7ff03ffbeff01ff)) { | ||||||||||
mstore(0, errorSelector) | ||||||||||
mstore(4, shl(248, add(c, 49))) | ||||||||||
revert(0, 0x24) | ||||||||||
} | ||||||||||
let carry := byte(0, mload(c)) | ||||||||||
|
||||||||||
// ... and add it to the limbs starting a `scratch` | ||||||||||
for { | ||||||||||
let i := scratch | ||||||||||
} lt(i, ptr) { | ||||||||||
i := add(i, 0x20) | ||||||||||
} { | ||||||||||
let acc := add(carry, mul(58, mload(i))) | ||||||||||
mstore(i, and(mask, acc)) | ||||||||||
carry := shr(248, acc) | ||||||||||
} | ||||||||||
// If the char just read result in a leftover carry, extend the limbs with the new value | ||||||||||
if carry { | ||||||||||
mstore(ptr, carry) | ||||||||||
ptr := add(ptr, 0x20) | ||||||||||
} | ||||||||||
} | ||||||||||
|
||||||||||
// Copy and compact the uint248 limbs + remove any zeros at the beginning. | ||||||||||
output := scratch | ||||||||||
for { | ||||||||||
let i := scratch | ||||||||||
} lt(i, ptr) { | ||||||||||
i := add(i, 0x20) | ||||||||||
} { | ||||||||||
output := sub(output, 31) | ||||||||||
mstore(sub(output, 1), mload(i)) | ||||||||||
} | ||||||||||
for {} lt(byte(0, mload(output)), lt(output, scratch)) {} { | ||||||||||
output := add(output, 1) | ||||||||||
} | ||||||||||
|
||||||||||
// Add the zeros that were encoded in the input (prefix '1's) | ||||||||||
calldatacopy(sub(output, inputLeadingZeros), calldatasize(), inputLeadingZeros) | ||||||||||
|
||||||||||
// Move output pointer to account for inputLeadingZeros | ||||||||||
output := sub(output, add(inputLeadingZeros, 0x20)) | ||||||||||
|
||||||||||
// Store length and allocate (reserve) memory up to scratch. | ||||||||||
mstore(output, sub(scratch, add(output, 0x20))) | ||||||||||
mstore(0x40, scratch) | ||||||||||
} | ||||||||||
} | ||||||||||
} |
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); | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.
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.
"character" or "value" fits better here