-
Notifications
You must be signed in to change notification settings - Fork 12.4k
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
Add Base58 library #5762
Changes from 44 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
8b1ae97
bb3aaf3
ac75789
930a03f
c3a4e76
f372c20
377ec53
aeaa73c
107328f
e3268cf
fe288ea
8e32c79
0c488da
23221e1
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 | ||
| --- | ||
|
|
||
| `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,214 @@ | ||||||||||||
| // 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). | ||||||||||||
ernestognw marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||
| */ | ||||||||||||
| library Base58 { | ||||||||||||
| error InvalidBase58Char(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 the beginning of the encoded string. | ||||||||||||
| 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
ernestognw marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
| inputLeadingZeros := add(inputLeadingZeros, 1) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // Start the output offset by an over-estimate of the length. | ||||||||||||
| // This is an estimation of the length ratio between bytes (base 256) and base58 | ||||||||||||
| // 9886 / 7239 = 1.36565824008841 > 1.365658237309761 = Math.log(256) / Math.log(58) | ||||||||||||
ernestognw marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||
| let outputLengthEstim := add(inputLeadingZeros, div(mul(sub(inputLength, inputLeadingZeros), 9886), 7239)) | ||||||||||||
ernestognw marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
|
|
||||||||||||
| // This is going to be our "scratch" workspace. We leave enough room after FMP to later store length + encoded output. | ||||||||||||
ernestognw marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||
| let scratch := add(mload(0x40), add(outputLengthEstim, 0x21)) | ||||||||||||
Amxx marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
|
|
||||||||||||
| // 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. | ||||||||||||
|
||||||||||||
| // unfold first loop, with a different shift. | |
| // If inputLength is not a multiple of 31, unfold the first loop: | |
| // Load the first i bytes from the input, right align them, and store them in scratch (at ptr) in a dedicated 32 bytes space. |
ernestognw marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
ernestognw marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
Outdated
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.
| // base 58 arithmetic on the 248bits limbs | |
| // base 58 arithmetic on the 248bits limbs: | |
| // we compute the remainder modulo 58 of the value encoded in the limbs, | |
| // all while dividing the value encoded in the limbs by 58 |
Outdated
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.
| let acc := add(shl(248, carry), mload(i)) | |
| // Load the next limb and add the carry (limbs are 248bits objects) | |
| let acc := add(shl(248, carry), mload(i)) |
Outdated
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.
| let acc := add(shl(248, carry), mload(i)) | |
| let numerator := add(shl(248, carry), mload(i)) |
Outdated
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.
| mstore(i, div(acc, 58)) | |
| // store the updated limb (divided by 58) | |
| mstore(i, div(acc, 58)) |
Outdated
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.
| carry := mod(acc, 58) | |
| // carry the remainder (modulo 58) | |
| carry := mod(acc, 58) |
Outdated
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.
| carry := mod(acc, 58) | |
| remainder := mod(acc, 58) |
I would call it remainder here
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.
Not sure what you mean by numerator / denominator. To me these for fractions, but there is no fraction here, is thee ?
carry really feels like the right word to me. Its what remains of the operation and gets carried to the next step, like in fulladders or other similar operations.
acc is for accululator. We might find a better name, but I'm really nkot convinced by numerator
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.
In elementary arithmetic, a carry is a digit that is transferred from one column of digits to another column of more significant digits.
From wikipedia. I was confused by the word carry since I expected it to be going to somewhere more significant (not less).
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.
This is the inverse of add and mul.
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.
I think I see what you mean. We are doing a division, so you think or "partial" remainder at each step. That makes sens.
Amxx marked this conversation as resolved.
Show resolved
Hide resolved
| 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(hex"")), hex""); | ||
| } | ||
|
|
||
| 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); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| const { ethers } = require('hardhat'); | ||
| const { expect } = require('chai'); | ||
| const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); | ||
|
|
||
| async function fixture() { | ||
| const mock = await ethers.deployContract('$Base58'); | ||
| return { mock }; | ||
| } | ||
|
|
||
| describe('Base58', function () { | ||
| beforeEach(async function () { | ||
| Object.assign(this, await loadFixture(fixture)); | ||
| }); | ||
|
|
||
| describe('base58', function () { | ||
| describe('encode/decode random buffers', function () { | ||
| // length 512 runs out of gas. | ||
| // this checks are very slow when running coverage, causing CI to timeout. | ||
| for (const length of [0, 1, 2, 3, 4, 32, 42, 128, 384]) | ||
| it( | ||
| [length > 32 && '[skip-on-coverage]', `buffer of length ${length}`].filter(Boolean).join(' '), | ||
| async function () { | ||
| const buffer = ethers.randomBytes(length); | ||
| const hex = ethers.hexlify(buffer); | ||
| const b58 = ethers.encodeBase58(buffer); | ||
|
|
||
| await expect(this.mock.$encode(hex)).to.eventually.equal(b58); | ||
| await expect(this.mock.$decode(b58)).to.eventually.equal(hex); | ||
| }, | ||
| ); | ||
| }); | ||
|
|
||
| // Tests case from section 5 of the (no longer active) Base58 Encoding Scheme RFC | ||
| // https://datatracker.ietf.org/doc/html/draft-msporny-base58-03 | ||
| describe('test vectors', function () { | ||
| for (const { raw, b58 } of [ | ||
| { raw: 'Hello World!', b58: '2NEpo7TZRRrLZSi2U' }, | ||
| { | ||
| raw: 'The quick brown fox jumps over the lazy dog.', | ||
| b58: 'USm3fpXnKG5EUBx2ndxBDMPVciP5hGey2Jh4NDv6gmeo1LkMeiKrLJUUBk6Z', | ||
| }, | ||
| { raw: '0x0000287fb4cd', b58: '11233QC4' }, | ||
| ]) | ||
| it(raw, async function () { | ||
| const buffer = (ethers.isHexString(raw) ? ethers.getBytes : ethers.toUtf8Bytes)(raw); | ||
| const hex = ethers.hexlify(buffer); | ||
|
|
||
| await expect(this.mock.$encode(hex)).to.eventually.equal(b58); | ||
| await expect(this.mock.$decode(b58)).to.eventually.equal(hex); | ||
| }); | ||
| }); | ||
|
|
||
| describe('decode invalid format', function () { | ||
| for (const chr of ['I', '-', '~']) | ||
| it(`Invalid base58 char ${chr}`, async function () { | ||
| const getHexCode = str => ethers.hexlify(ethers.toUtf8Bytes(str)); | ||
| const helper = { interface: ethers.Interface.from(['error InvalidBase58Char(bytes1)']) }; | ||
|
|
||
| await expect(this.mock.$decode(`VYRWKp${chr}pnN7`)) | ||
| .to.be.revertedWithCustomError(helper, 'InvalidBase58Char') | ||
| .withArgs(getHexCode(chr)); | ||
| }); | ||
| }); | ||
| }); | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.