Skip to content

Commit f47355a

Browse files
committed
add bytes32 fast-encode
1 parent 855a1c6 commit f47355a

File tree

2 files changed

+61
-2
lines changed

2 files changed

+61
-2
lines changed

contracts/utils/Base58.sol

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,55 @@ library Base58 {
3131
return string(_encode(data));
3232
}
3333

34+
/**
35+
* @dev Encode a `bytes32` as a Base58 `string`. This is an alternative to {encode-bytes} that is significantly
36+
* faster when the input is known to be 32 bytes long.
37+
*
38+
* This is usefull to encode Solana addresses.
39+
*/
40+
function encode(bytes32 data) internal pure returns (string memory) {
41+
return string(_encode(data, 44));
42+
}
43+
3444
/**
3545
* @dev Decode a Base58 `string` into a `bytes` buffer.
46+
*
47+
* NOTE: This function reverts if the input string contains invalid characters.
3648
*/
3749
function decode(string memory data) internal pure returns (bytes memory) {
3850
return _decode(bytes(data));
3951
}
4052

53+
/**
54+
* @dev Internal encoding function that processes `bytes32` input. This is way faster than the generic (`bytes`)
55+
* version, but is limited to 32 bytes of input (equivalent to 44 char outputs).
56+
*/
57+
function _encode(bytes32 input, uint256 length) private pure returns (bytes memory encoded) {
58+
assembly ("memory-safe") {
59+
encoded := mload(0x40)
60+
61+
// Store the encoding table. This overlaps with the FMP that we are going to reset later anyway.
62+
mstore(0x1f, "123456789ABCDEFGHJKLMNPQRSTUVWXY")
63+
mstore(0x3f, "Zabcdefghijkmnopqrstuvwxyz")
64+
65+
for {
66+
let i := length
67+
} gt(i, 0) {
68+
i := sub(i, 1)
69+
} {
70+
mstore8(add(add(encoded, 0x1f), i), mload(mod(input, 58)))
71+
input := div(input, 58)
72+
}
73+
74+
mstore(encoded, length)
75+
mstore(0x40, add(add(encoded, 0x20), length))
76+
}
77+
}
78+
79+
/**
80+
* @dev Internal encoding function that processes `bytes` input of arbitrary length and returns another (newly
81+
* allocated) bytes object that contains the encoded base58 string.
82+
*/
4183
function _encode(bytes memory data) private pure returns (bytes memory encoded) {
4284
// For reference, solidity implementation
4385
// unchecked {
@@ -138,6 +180,12 @@ library Base58 {
138180
}
139181
}
140182

183+
/**
184+
* @dev Internal decoding function that processes `bytes` input containing a base58 encoded string of arbitrary
185+
* length and returns another (newly allocated) bytes object that contains the decoded buffer.
186+
*
187+
* NOTE: This function reverts if the input string contains invalid characters.
188+
*/
141189
function _decode(bytes memory data) private pure returns (bytes memory) {
142190
unchecked {
143191
uint256 b58Length = data.length;

test/utils/Base58.test.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,23 @@ describe('Base58', function () {
2424
const hex = ethers.hexlify(buffer);
2525
const b58 = ethers.encodeBase58(buffer);
2626

27-
await expect(this.mock.$encode(hex)).to.eventually.equal(b58);
27+
await expect(this.mock.$encode(ethers.Typed.bytes(hex))).to.eventually.equal(b58);
2828
await expect(this.mock.$decode(b58)).to.eventually.equal(hex);
2929
},
3030
);
3131
});
3232

33+
describe('encode bytes32 (solana addresses)', function () {
34+
for (const buffer of Array.from({ length: 100 }).map(() => ethers.randomBytes(32))) {
35+
const hex = ethers.hexlify(buffer);
36+
const b58 = ethers.encodeBase58(hex).padStart(44, '1');
37+
38+
it(hex, async function () {
39+
await expect(this.mock.$encode(ethers.Typed.bytes32(hex))).to.eventually.equal(b58, hex + ' ' + b58);
40+
});
41+
}
42+
});
43+
3344
// Tests case from section 5 of the (no longer active) Base58 Encoding Scheme RFC
3445
// https://datatracker.ietf.org/doc/html/draft-msporny-base58-03
3546
describe('test vectors', function () {
@@ -45,7 +56,7 @@ describe('Base58', function () {
4556
const buffer = (ethers.isHexString(raw) ? ethers.getBytes : ethers.toUtf8Bytes)(raw);
4657
const hex = ethers.hexlify(buffer);
4758

48-
await expect(this.mock.$encode(hex)).to.eventually.equal(b58);
59+
await expect(this.mock.$encode(ethers.Typed.bytes(hex))).to.eventually.equal(b58);
4960
await expect(this.mock.$decode(b58)).to.eventually.equal(hex);
5061
});
5162
});

0 commit comments

Comments
 (0)