Skip to content

Commit 41b586b

Browse files
committed
efficient decoding
1 parent a3c4667 commit 41b586b

File tree

2 files changed

+43
-21
lines changed

2 files changed

+43
-21
lines changed

contracts/utils/Base58.sol

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ library Base58 {
1414
using SafeCast for bool;
1515
using Bytes for bytes;
1616

17-
string internal constant _TABLE = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
17+
error InvalidBase56Digit(uint8);
18+
19+
bytes internal constant _TABLE = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
20+
bytes internal constant _LOOKUP_TABLE =
21+
hex"000102030405060708ffffffffffffff090a0b0c0d0e0f10ff1112131415ff161718191a1b1c1d1e1f20ffffffffffff2122232425262728292a2bff2c2d2e2f30313233343536373839";
1822

1923
function encode(bytes memory data) internal pure returns (string memory) {
2024
return string(_encode(data));
@@ -45,7 +49,7 @@ library Base58 {
4549
length -= slotCLZ - dataCLZ;
4650
slot.splice(slotCLZ - dataCLZ);
4751

48-
bytes memory cache = bytes(_TABLE);
52+
bytes memory cache = _TABLE;
4953
for (uint256 i = 0; i < length; ++i) {
5054
// equivalent to `slot[i] = TABLE[slot[i]];`
5155
_mstore8(slot, i, _mload8(cache, _mload8i(slot, i)));
@@ -62,22 +66,29 @@ library Base58 {
6266
uint256 size = 2 * ((b58Length * 8351) / 6115 + 1);
6367
bytes memory binu = new bytes(size);
6468

65-
bytes memory cache = bytes(_TABLE);
66-
uint32[] memory outi = new uint32[]((b58Length + 3) / 4);
67-
for (uint256 i = 0; i < data.length; i++) {
68-
bytes1 r = _mload8(data, i);
69-
uint256 c = cache.indexOf(r); // can we avoid the loop here ?
70-
require(c != type(uint256).max, "invalid base58 digit");
71-
for (uint256 k = outi.length; k > 0; --k) {
72-
uint256 t = uint64(outi[k - 1]) * 58 + c;
73-
c = t >> 32;
74-
outi[k - 1] = uint32(t & 0xffffffff);
69+
bytes memory cache = _LOOKUP_TABLE;
70+
uint256 outiLength = (b58Length + 3) / 4;
71+
// Note: allocating uint32[] would be enough, but solidity doesn't pack memory.
72+
uint256[] memory outi = new uint256[](outiLength);
73+
for (uint256 i = 0; i < data.length; ++i) {
74+
// get b58 char
75+
uint8 chr = _mload8i(data, i);
76+
require(chr > 48 && chr < 123, InvalidBase56Digit(chr));
77+
78+
// decode b58 char
79+
uint256 carry = _mload8i(cache, chr - 49);
80+
require(carry < 58, InvalidBase56Digit(chr));
81+
82+
for (uint256 j = outiLength; j > 0; --j) {
83+
uint256 value = carry + 58 * outi[j - 1];
84+
carry = value >> 32;
85+
outi[j - 1] = value & 0xffffffff;
7586
}
7687
}
7788

7889
uint256 ptr = 0;
7990
uint256 mask = ((b58Length - 1) % 4) + 1;
80-
for (uint256 j = 0; j < outi.length; ++j) {
91+
for (uint256 j = 0; j < outiLength; ++j) {
8192
while (mask > 0) {
8293
--mask;
8394
_mstore8(binu, ptr, bytes1(uint8(outi[j] >> (8 * mask))));

test/utils/Base58.test.js

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,25 @@ describe('Base58', function () {
1313
});
1414

1515
describe('base58', function () {
16-
for (const length of [0, 1, 2, 3, 4, 32, 42, 128, 384]) // 512 runs out of gas
17-
it(`Encode/Decode buffer of length ${length}`, async function () {
18-
const buffer = ethers.randomBytes(length);
19-
const hex = ethers.hexlify(buffer);
20-
const b58 = ethers.encodeBase58(buffer);
16+
describe('encode/decode', function () {
17+
for (const length of [0, 1, 2, 3, 4, 32, 42, 128, 384]) // 512 runs out of gas
18+
it(`buffer of length ${length}`, async function () {
19+
const buffer = ethers.randomBytes(length);
20+
const hex = ethers.hexlify(buffer);
21+
const b58 = ethers.encodeBase58(buffer);
2122

22-
expect(await this.mock.$encode(hex)).to.equal(b58);
23-
expect(await this.mock.$decode(b58)).to.equal(hex);
24-
});
23+
expect(await this.mock.$encode(hex)).to.equal(b58);
24+
expect(await this.mock.$decode(b58)).to.equal(hex);
25+
});
26+
});
27+
28+
describe('decode invalid format', function () {
29+
for (const chr of ['I', '-', '~'])
30+
it(`Invalid base58 char ${chr}`, async function () {
31+
await expect(this.mock.$decode(`VYRWKp${chr}pnN7`))
32+
.to.be.revertedWithCustomError(this.mock, 'InvalidBase56Digit')
33+
.withArgs(chr.codePointAt(0));
34+
});
35+
});
2536
});
2637
});

0 commit comments

Comments
 (0)