Skip to content

Commit

Permalink
✨ Encode IPFS hashes in metadata using bs58
Browse files Browse the repository at this point in the history
Embedded lightweight https://github.com/pur3miish/base58-js encoder.
Moreover, introduce the following
- Augment `solc` module definition with input description
- Include `metadata` in compilation output to enable hashes test
- Use more appropiate `UNLICENSED` license identifier
  • Loading branch information
acuarica committed Dec 6, 2023
1 parent 90909d2 commit e613ae2
Show file tree
Hide file tree
Showing 4 changed files with 227 additions and 73 deletions.
77 changes: 76 additions & 1 deletion src/metadata.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { fromHexString } from './opcode';

Check failure on line 1 in src/metadata.ts

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 18)

Module '"./opcode"' has no exported member 'fromHexString'.

Check failure on line 1 in src/metadata.ts

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, 20)

Module '"./opcode"' has no exported member 'fromHexString'.

const BZZR0 = '627a7a7230';
const BZZR1 = '627a7a7231';
const IPFS = '69706673';
Expand Down Expand Up @@ -33,6 +35,11 @@ export class Metadata {
get url(): string {
return `${this.protocol}://${this.hash}`;
}

get minor(): number | undefined {
const field = /^0\.(\d+)\./.exec(this.solc)?.[1];
return field ? parseInt(field) : undefined;
}
}

/**
Expand All @@ -50,7 +57,11 @@ export function stripMetadataHash(bytecode: string): [string, Metadata | undefin
if (match && match[1]) {
return [
bytecode.substring(0, match.index),
new Metadata(protocol, match[1], match[2] ? convertVersion(match[2]) : '<0.5.9'),
new Metadata(
protocol,
protocol === 'ipfs' ? bs58.toBase58(match[1]) : match[1],
match[2] ? convertVersion(match[2]) : '<0.5.9'
),
];
}
}
Expand All @@ -67,3 +78,67 @@ export function stripMetadataHash(bytecode: string): [string, Metadata | undefin
return `${slice(0)}.${slice(2)}.${slice(4)}`;
}
}

// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace bs58 {
/**
* Base58 characters include numbers `123456789`, uppercase `ABCDEFGHJKLMNPQRSTUVWXYZ` and lowercase `abcdefghijkmnopqrstuvwxyz`.
*/
const chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';

/**
* Generates a mapping between base58 and ascii.
* @returns {Array<Number>} mapping between ascii and base58.
*/
const create_base58_map = (): number[] => {
const base58M = Array(256).fill(-1) as number[];
for (let i = 0; i < chars.length; ++i) {
base58M[chars.charCodeAt(i)] = i;
}

return base58M;
};

const base58Map = create_base58_map();

/**
* Converts a Uint8Array into a base58 string.
* @param uint8array Unsigned integer array.
* @returns { import("./base58_chars.mjs").base58_chars } base58 string representation of the binary array.
* @example <caption>Usage.</caption>
* ```js
* const str = binary_to_base58([15, 239, 64])
* console.log(str)
* ```
* Logged output will be 6MRy.
*/
export function toBase58(uint8array2: string): string {
const uint8array: Uint8Array = fromHexString(uint8array2, 0);

const result = [];

for (const byte of uint8array) {
let carry = byte;
for (let j = 0; j < result.length; ++j) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const x = (base58Map[result[j]] << 8) + carry;
result[j] = chars.charCodeAt(x % 58);
carry = (x / 58) | 0;
}
while (carry) {
result.push(chars.charCodeAt(carry % 58));
carry = (carry / 58) | 0;
}
}

for (const byte of uint8array) {
if (byte) break;
else result.push('1'.charCodeAt(0));
}

result.reverse();

return String.fromCharCode(...result);
}
}
36 changes: 23 additions & 13 deletions test/metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Metadata, stripMetadataHash } from 'sevm';

import { forVersion } from './utils/solc';

describe('metadata', function () {
describe('::metadata', function () {
it(`should return original bytecode when no metadata`, function () {
const originalCode = '01020304';
const [code, metadata] = stripMetadataHash(originalCode);
Expand All @@ -13,27 +13,37 @@ describe('metadata', function () {
expect(metadata).to.be.undefined;
});

/**
* IPFS hashes need to be computed manually in order to avoid adding
* [`ipfs-core`](https://github.com/ipfs/js-ipfs#install-as-an-application-developer)
* as a dependency.
* This is a bloated and deprecated package.
*
* To get the IPFS hash of each contract run
*
* ```sh
* cat $ARTIFACT | jq -r .metadata | tr -d '\n' | ipfs add --quiet --only-hash
* ```
*
* where `$ARTIFACT` refers to the output of solc compiler.
*/
const HASHES = {
'0.5.5': [
'bzzr',
'5d2a7f7236dd33784730b31e6c9e23b977b5c78b67e6f3ae1e476d4bad674583',
'886590b34f4504f97d0869b9d2210fb027f1057978e99c5a955fd1ea6ab603e9',
'<0.5.9',
],
'0.5.17': ['bzzr', 'bc000e08756c8a39ecf14e345b9b9cd73096befd4310ba9be5acd60f965e9117'],
'0.6.12': ['ipfs', '122022dc709c3afc7cabde0eb1cb8305f420c0cee343e32fef5905ba12a6c80275cc'],
'0.7.6': ['ipfs', '122097825c4aec6dd5baee935bb6c6efdfef43e6eccaf6e57b3c9776f4dc1fc98796'],
'0.8.16': ['ipfs', '1220d4c87f86f0fbd16c75f71f84f3fbae12b409812214fc9572eb31de27b071e944'],
'0.5.17': ['bzzr', '99edd4d2083be1b43f60e5f50ceb4ef57a6b968f18f236047a97a0eb54036a99'],
'0.6.12': ['ipfs', 'QmR2wMAiGogVWTxtXh1AVNboWSHNggtn9jYzG2zLXi836A'],
'0.7.6': ['ipfs', 'QmaRBmmGGny5mjFjSJcbvcQLsJMRsbcSB4QoEcxFu9mxhB'],
'0.8.16': ['ipfs', 'QmcshgdTcz3T2rD8BgPAw2njvp2WsCCcsi6qh9VQhJhwLZ'],
} as const;

forVersion((compile, _fallback, version) => {
let output: ReturnType<typeof compile>;

before(function () {
output = compile('contract Test {}', this);
});

it('should get metadata for deployed bytecode', function () {
const [, metadata] = stripMetadataHash(output.bytecode);
const { bytecode } = compile('contract Test {}', this);

const [, metadata] = stripMetadataHash(bytecode);

const [protocol, hash, expectedVersion] = HASHES[version];
expect(metadata).to.be.deep.equal(
Expand Down
26 changes: 13 additions & 13 deletions test/utils/solc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import c from 'ansi-colors';
import type { Runnable, Suite } from 'mocha';

import wrapper from 'solc/wrapper';
import type { ABI, SolcInputSettings, SolcOutput } from 'solc';
import type { ABI, SolcInput, SolcOutput } from 'solc';

export const VERSIONS = ['0.5.5', '0.5.17', '0.6.12', '0.7.6', '0.8.16'] as const;

Expand All @@ -28,33 +28,34 @@ export function compile(
content: string,
version: Version,
context?: Mocha.Context,
optimizer?: SolcInputSettings['optimizer']
): { bytecode: string; abi: ABI } {
optimizer?: SolcInput['settings']['optimizer']
): { bytecode: string; abi: ABI; metadata: string } {
const input = JSON.stringify({
language: 'Solidity',
sources: {
'source.sol': {
content: `// SPDX-License-Identifier: MIT\npragma solidity ${version};\n${content}`,
content: `// SPDX-License-Identifier: UNLICENSED\npragma solidity ${version};\n${content}`,
},
},
settings: {
optimizer,
outputSelection: {
'*': {
'*': ['abi', 'evm.deployedBytecode'],
'*': ['abi', 'metadata', 'evm.deployedBytecode'],
},
},
},
});
} satisfies SolcInput);

let writeCacheFn: (output: ReturnType<typeof compile>) => void;
if (context !== undefined) {
const title = (test: Runnable | Suite | undefined): string =>
test ? title(test.parent) + '.' + test.title : '';
const fileName = title(context.test)
.replace(/^../, '')
.replace('solc-', '')
.replace(`solc-v${version}.`, '')
.replace(/`/g, '')
.replace(/^::/, '')
.replace(/::/g, '.')
.replace(/ /g, '-')
.replace(/[:^'()]/g, '_')
Expand Down Expand Up @@ -97,17 +98,18 @@ export function compile(

const bytecode = contract.evm.deployedBytecode.object;
const abi = contract.abi;
writeCacheFn({ bytecode, abi });
const metadata = contract.metadata;
writeCacheFn({ bytecode, abi, metadata });

return { bytecode, abi };
return { bytecode, abi, metadata };
}

export function forVersion(
fn: (
compile_: (
content: string,
context: Mocha.Context,
optimizer?: SolcInputSettings['optimizer']
optimizer?: SolcInput['settings']['optimizer']
) => ReturnType<typeof compile>,
fallback: 'fallback' | 'function',
version: Version
Expand Down Expand Up @@ -143,7 +145,7 @@ export async function mochaGlobalSetup() {
type Releases = { [key: string]: string };

mkdirSync('.solc', { recursive: true });
process.stdout.write('solc setup ');
process.stdout.write(c.magenta('> setup solc-js compilers '));

const releases = await (async function () {
const path = './.solc/releases.json';
Expand Down Expand Up @@ -177,6 +179,4 @@ export async function mochaGlobalSetup() {
}
}
}

// console.info();
}
Loading

0 comments on commit e613ae2

Please sign in to comment.