From e613ae2fefba17efc3c25a86b1345feed219964b Mon Sep 17 00:00:00 2001 From: Luis Mastrangelo Date: Wed, 6 Dec 2023 15:20:32 -0300 Subject: [PATCH] :sparkles: Encode IPFS hashes in metadata using `bs58` 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 --- src/metadata.ts | 77 +++++++++++++++++++- test/metadata.test.ts | 36 ++++++---- test/utils/solc.ts | 26 +++---- types/solc.d.ts | 161 ++++++++++++++++++++++++++++++------------ 4 files changed, 227 insertions(+), 73 deletions(-) diff --git a/src/metadata.ts b/src/metadata.ts index 478d54eb..323c5770 100644 --- a/src/metadata.ts +++ b/src/metadata.ts @@ -1,3 +1,5 @@ +import { fromHexString } from './opcode'; + const BZZR0 = '627a7a7230'; const BZZR1 = '627a7a7231'; const IPFS = '69706673'; @@ -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; + } } /** @@ -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' + ), ]; } } @@ -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} 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 Usage. + * ```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); + } +} diff --git a/test/metadata.test.ts b/test/metadata.test.ts index 08a11861..3fbbabd1 100644 --- a/test/metadata.test.ts +++ b/test/metadata.test.ts @@ -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); @@ -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; - - 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( diff --git a/test/utils/solc.ts b/test/utils/solc.ts index 04317081..65e43481 100644 --- a/test/utils/solc.ts +++ b/test/utils/solc.ts @@ -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; @@ -28,24 +28,24 @@ 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) => void; if (context !== undefined) { @@ -53,8 +53,9 @@ export function compile( 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, '_') @@ -97,9 +98,10 @@ 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( @@ -107,7 +109,7 @@ export function forVersion( compile_: ( content: string, context: Mocha.Context, - optimizer?: SolcInputSettings['optimizer'] + optimizer?: SolcInput['settings']['optimizer'] ) => ReturnType, fallback: 'fallback' | 'function', version: Version @@ -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'; @@ -177,6 +179,4 @@ export async function mochaGlobalSetup() { } } } - - // console.info(); } diff --git a/types/solc.d.ts b/types/solc.d.ts index 5e026e5d..b6655fe2 100644 --- a/types/solc.d.ts +++ b/types/solc.d.ts @@ -1,57 +1,120 @@ declare module 'solc' { /** * https://docs.soliditylang.org/en/latest/using-the-compiler.html#input-description + * + * To be used with https://github.com/ethereum/solc-js. */ - interface SolcInputSettings { - /** - * Optional: Optimizer settings - */ - optimizer?: { + interface SolcInput { + language: 'Solidity'; + sources: { + 'source.sol': { + content: string; + }; + }; + settings: { /** - * Disabled by default. - * NOTE: enabled=false still leaves some optimizations on. See comments below. - * WARNING: Before version 0.8.6 omitting the 'enabled' key was not equivalent to setting - * it to false and would actually disable all the optimizations. + * Optional: Optimizer settings */ - enabled?: boolean; + optimizer?: + | undefined + | { + /** + * Disabled by default. + * NOTE: enabled=false still leaves some optimizations on. See comments below. + * WARNING: Before version 0.8.6 omitting the 'enabled' key was not equivalent to setting + * it to false and would actually disable all the optimizations. + */ + enabled?: boolean; + /** + * Switch optimizer components on or off in detail. + * The "enabled" switch above provides two defaults which can be tweaked here. + * If "details" is given, "enabled" can be omitted. + */ + details?: { + /** + * The peephole optimizer is always on if no details are given, + * use details to switch it off. + */ + peephole?: boolean; + /** + * The inliner is always off if no details are given, + * use details to switch it on. + */ + inliner?: boolean; + /** + * The unused jumpdest remover is always on if no details are given, + * use details to switch it off. + */ + jumpdestRemover?: boolean; + /** + * Sometimes re-orders literals in commutative operations. + */ + orderLiterals?: boolean; + /** + * Removes duplicate code blocks. + */ + deduplicate?: boolean; + /** + * Common subexpression elimination, this is the most complicated step but + * can also provide the largest gain. + */ + cse?: boolean; + /** + * Optimize representation of literal numbers and strings in code. + */ + constantOptimizer?: boolean; + }; + }; + /** - * Switch optimizer components on or off in detail. - * The "enabled" switch above provides two defaults which can be tweaked here. - * If "details" is given, "enabled" can be omitted. + * The following can be used to select desired outputs based + * on file and contract names. + * If this field is omitted, then the compiler loads and does type checking, + * but will not generate any outputs apart from errors. + * The first level key is the file name and the second level key is the contract name. + * An empty contract name is used for outputs that are not tied to a contract + * but to the whole source file like the AST. + * A star as contract name refers to all contracts in the file. + * Similarly, a star as a file name matches all files. + * To select all outputs the compiler can possibly generate, use + * "outputSelection: { "*": { "*": [ "*" ], "": [ "*" ] } }" + * but note that this might slow down the compilation process needlessly. + * + * The available output types are as follows: + * + * File level (needs empty string as contract name): + * ast - AST of all source files + * + * Contract level (needs the contract name or "*"): + * abi - ABI + * devdoc - Developer documentation (natspec) + * userdoc - User documentation (natspec) + * metadata - Metadata + * ir - Yul intermediate representation of the code before optimization + * irAst - AST of Yul intermediate representation of the code before optimization + * irOptimized - Intermediate representation after optimization + * irOptimizedAst - AST of intermediate representation after optimization + * storageLayout - Slots, offsets and types of the contract's state variables. + * evm.assembly - New assembly format + * evm.legacyAssembly - Old-style assembly format in JSON + * evm.bytecode.functionDebugData - Debugging information at function level + * evm.bytecode.object - Bytecode object + * evm.bytecode.opcodes - Opcodes list + * evm.bytecode.sourceMap - Source mapping (useful for debugging) + * evm.bytecode.linkReferences - Link references (if unlinked object) + * evm.bytecode.generatedSources - Sources generated by the compiler + * evm.deployedBytecode* - Deployed bytecode (has all the options that evm.bytecode has) + * evm.deployedBytecode.immutableReferences - Map from AST ids to bytecode ranges that reference immutables + * evm.methodIdentifiers - The list of function hashes + * evm.gasEstimates - Function gas estimates + * + * Note that using `evm`, `evm.bytecode`, etc. will select every + * target part of that output. Additionally, `*` can be used as a wildcard to request everything. */ - details?: { - /** - * The peephole optimizer is always on if no details are given, - * use details to switch it off. - */ - peephole?: boolean; - /** - * The inliner is always off if no details are given, - * use details to switch it on. - */ - inliner?: boolean; - /** - * The unused jumpdest remover is always on if no details are given, - * use details to switch it off. - */ - jumpdestRemover?: boolean; - /** - * Sometimes re-orders literals in commutative operations. - */ - orderLiterals?: boolean; - /** - * Removes duplicate code blocks. - */ - deduplicate?: boolean; - /** - * Common subexpression elimination, this is the most complicated step but - * can also provide the largest gain. - */ - cse?: boolean; - /** - * Optimize representation of literal numbers and strings in code. - */ - constantOptimizer?: boolean; + outputSelection: { + '*': { + '*': string[]; + }; }; }; } @@ -76,6 +139,12 @@ declare module 'solc' { * See https://docs.soliditylang.org/en/develop/abi-spec.html */ abi: ABI; + + /** + * See the Metadata Output documentation (serialised JSON string) + */ + metadata: string; + /** * EVM-related outputs. */