diff --git a/packages/rollup-dev-tools/src/tools/transpiler/transpiler.ts b/packages/rollup-dev-tools/src/tools/transpiler/transpiler.ts index 35d36d5d4beb..bd484d7e5b08 100644 --- a/packages/rollup-dev-tools/src/tools/transpiler/transpiler.ts +++ b/packages/rollup-dev-tools/src/tools/transpiler/transpiler.ts @@ -9,7 +9,12 @@ import { bufferToBytecode, getPCOfEVMBytecodeIndex, } from '@eth-optimism/rollup-core' -import { getLogger, bufToHexString, add0x } from '@eth-optimism/core-utils' +import { + getLogger, + bufToHexString, + add0x, + bufferUtils, +} from '@eth-optimism/core-utils' import BigNum = require('bn.js') @@ -28,6 +33,7 @@ import { } from '../../types/transpiler' import { accountForJumps } from './jump-replacement' import { createError } from './util' +import { format } from 'path' const log = getLogger('transpiler-impl') @@ -82,12 +88,16 @@ export class TranspilerImpl implements Transpiler { ) const startOfDeployedBytecode: number = bytecode.indexOf(deployedBytecode) if (startOfDeployedBytecode === -1) { - log.debug( - `WARNING: Could not find deployed bytecode (${bufToHexString( - deployedBytecode - )}) within the original bytecode (${bufToHexString( - bytecode - )}). If you are using a custom compiler, this may break.` + const errMsg = `WARNING: Could not find deployed bytecode (${bufToHexString( + deployedBytecode + )}) within the original bytecode (${bufToHexString(bytecode)}).` + log.debug(errMsg) + errors.push( + TranspilerImpl.createError( + 0, + TranspilationErrors.MISSING_DEPLOYED_BYTECODE_ERROR, + errMsg + ) ) } @@ -270,7 +280,7 @@ export class TranspilerImpl implements Transpiler { newConstantOffset ).toBuffer('be', op.opcode.programBytesConsumed) log.debug( - `fixing CODECOPY(constant) ad PC 0x${getPCOfEVMBytecodeIndex( + `fixing CODECOPY(constant) at PC 0x${getPCOfEVMBytecodeIndex( index, taggedBytecode ).toString(16)}. Setting new index to 0x${bufToHexString( @@ -284,20 +294,20 @@ export class TranspilerImpl implements Transpiler { } // Finds and tags the PUSHN's which are detected to be associated with CODECOPYing deployed bytecode which is returned during CREATE/CREATE2. - // Tags based on the pattern: - // PUSH2 // codecopy's and RETURN's length - // DUP1 // DUPed to use twice, for RETURN and CODECOPY both - // PUSH2 // codecopy's offset - // PUSH1 codecopy's destOffset - // CODECOPY // copy - // PUSH1 0 // RETURN offset - // RETURN // uses above RETURN offset and DUP'ed length above // See https://github.com/ethereum-optimism/optimistic-rollup/wiki/CODECOPYs for more details. private findAndTagDeployedBytecodeReturner( bytecode: EVMBytecode ): EVMBytecode { for (let index = 0; index < bytecode.length - 6; index++) { const op: EVMOpcodeAndBytes = bytecode[index] + // Tags based on the pattern used for deploying non-library contracts: + // PUSH2 // codecopy's and RETURN's length + // DUP1 // DUPed to use twice, for RETURN and CODECOPY both + // PUSH2 // codecopy's offset + // PUSH1 codecopy's destOffset + // CODECOPY // copy + // PUSH1 0 // RETURN offset + // RETURN // uses above RETURN offset and DUP'ed length above if ( Opcode.isPUSHOpcode(op.opcode) && Opcode.isPUSHOpcode(bytecode[index + 2].opcode) && @@ -305,7 +315,7 @@ export class TranspilerImpl implements Transpiler { bytecode[index + 6].opcode === Opcode.RETURN ) { log.debug( - `detected a [CODECOPY(deployed bytecode)... RETURN] (CREATE/2 deployment logic) pattern starting at PC: 0x${getPCOfEVMBytecodeIndex( + `detected a NON-LIBRARY [CODECOPY(deployed bytecode)... RETURN] (CREATE/2 deployment logic) pattern starting at PC: 0x${getPCOfEVMBytecodeIndex( index, bytecode ).toString(16)}. Tagging the offset and size...` @@ -329,6 +339,45 @@ export class TranspilerImpl implements Transpiler { }, } } + // Tags based on the pattern used for deploying library contracts: + // PUSH2 // deployed bytecode length + // PUSH2 // deployed bytecode start + // PUSH1: // destoffset of code to copy + // DUP3 + // DUP3 + // DUP3 + // CODECOPY + else if ( + Opcode.isPUSHOpcode(op.opcode) && + Opcode.isPUSHOpcode(bytecode[index + 1].opcode) && + Opcode.isPUSHOpcode(bytecode[index + 2].opcode) && + bytecode[index + 6].opcode === Opcode.CODECOPY + ) { + log.debug( + `detected a LIBRARY [CODECOPY(deployed bytecode)... RETURN] (library deployment logic) pattern starting at PC: 0x${getPCOfEVMBytecodeIndex( + index, + bytecode + ).toString(16)}. Tagging the offset and size...` + ) + bytecode[index] = { + opcode: op.opcode, + consumedBytes: op.consumedBytes, + tag: { + padPUSH: true, + reasonTagged: IS_DEPLOY_CODE_LENGTH, + metadata: undefined, + }, + } + bytecode[index + 1] = { + opcode: bytecode[index + 1].opcode, + consumedBytes: bytecode[index + 1].consumedBytes, + tag: { + padPUSH: true, + reasonTagged: IS_DEPLOY_CODECOPY_OFFSET, + metadata: undefined, + }, + } + } } return bytecode } @@ -497,6 +546,27 @@ export class TranspilerImpl implements Transpiler { let lastOpcode: EVMOpcode let insideUnreachableCode: boolean = false + const [lastOpcodeAndConsumedBytes] = bytecode.slice(-1) + if ( + Opcode.isPUSHOpcode(lastOpcodeAndConsumedBytes.opcode) && + lastOpcodeAndConsumedBytes.consumedBytes.byteLength < + lastOpcodeAndConsumedBytes.opcode.programBytesConsumed + ) { + // todo: handle with warnings[] separate from errors[]? + const message: string = `Final input opcode: ${ + lastOpcodeAndConsumedBytes.opcode.name + } consumes ${ + lastOpcodeAndConsumedBytes.opcode.programBytesConsumed + }, but only has 0x${bufToHexString( + lastOpcodeAndConsumedBytes.consumedBytes + )} following it. Padding with zeros under the assumption that this arises from a constant at EOF...` + log.debug(message) + lastOpcodeAndConsumedBytes.consumedBytes = bufferUtils.padRight( + lastOpcodeAndConsumedBytes.consumedBytes, + lastOpcodeAndConsumedBytes.opcode.programBytesConsumed + ) + } + const bytecodeBuf: Buffer = bytecodeToBuffer(bytecode) // todo remove once confirmed with Kevin? let seenJump: boolean = false @@ -543,16 +613,6 @@ export class TranspilerImpl implements Transpiler { pc += opcode.programBytesConsumed continue } - if ( - !TranspilerImpl.enoughBytesLeft( - opcode, - bytecodeBuf.length, - pc, - errors - ) - ) { - break - } } if (insideUnreachableCode && !opcode) { const unreachableCode: Buffer = bytecodeBuf.slice(pc, pc + 1) @@ -696,38 +756,6 @@ export class TranspilerImpl implements Transpiler { return true } - /** - * Returns whether or not there are enough bytes left in the bytecode for the provided Opcode. - * If it is not, it creates a new TranpilationError and appends it to the provided list. - * - * @param opcode The opcode in question. - * @param bytecodeLength The length of the bytecode being transpiled. - * @param pc The current program counter value. - * @param errors The cumulative errors list. - * @returns True if enough bytes are left for the Opcode to consume, False otherwise. - */ - private static enoughBytesLeft( - opcode: EVMOpcode, - bytecodeLength: number, - pc: number, - errors: TranspilationError[] - ): boolean { - if (pc + opcode.programBytesConsumed >= bytecodeLength) { - const bytesLeft: number = bytecodeLength - pc - 1 - const message: string = `Opcode: ${opcode.name} consumes ${ - opcode.programBytesConsumed - }, but ${!!bytesLeft ? 'only ' : ''}${bytesLeft} ${ - bytesLeft !== 1 ? 'bytes are' : 'byte is' - } left in input bytecode.` - log.debug(message) - errors.push( - createError(pc, TranspilationErrors.INVALID_BYTES_CONSUMED, message) - ) - return false - } - return true - } - /** * Util function to create TranspilationErrors. * diff --git a/packages/rollup-dev-tools/src/types/transpiler/errors.ts b/packages/rollup-dev-tools/src/types/transpiler/errors.ts index d61c62efb81d..bce3aef722a8 100644 --- a/packages/rollup-dev-tools/src/types/transpiler/errors.ts +++ b/packages/rollup-dev-tools/src/types/transpiler/errors.ts @@ -37,24 +37,6 @@ export class InvalidInitcodeError extends Error { } } -export class MissingConstantError extends Error { - constructor(msg?: string) { - super(msg) - } -} - -export class DetectedConstantOutOfBoundsError extends Error { - constructor(msg?: string) { - super(msg) - } -} - -export class SubTranspilationError extends Error { - constructor(msg?: string) { - super(msg) - } -} - export class TranspilationErrors { public static readonly UNSUPPORTED_OPCODE: number = 0 public static readonly OPCODE_NOT_WHITELISTED: number = 1 @@ -63,5 +45,6 @@ export class TranspilationErrors { public static readonly INVALID_INITCODE: number = 4 public static readonly MISSING_CONSTANT_ERROR: number = 5 public static readonly DETECTED_CONSTANT_OOB: number = 6 - public static readonly SUB_TRANSPILATION_ERROR: number = 6 + public static readonly SUB_TRANSPILATION_ERROR: number = 7 + public static readonly MISSING_DEPLOYED_BYTECODE_ERROR: number = 8 } diff --git a/packages/rollup-dev-tools/test/contracts/constants-transpilation/ConstantGetter.sol b/packages/rollup-dev-tools/test/contracts/constants-transpilation/ConstantGetter.sol index 5c99d68f9215..4b4347379edc 100644 --- a/packages/rollup-dev-tools/test/contracts/constants-transpilation/ConstantGetter.sol +++ b/packages/rollup-dev-tools/test/contracts/constants-transpilation/ConstantGetter.sol @@ -4,7 +4,7 @@ contract ConstantGetter { bytes32 public constant bytes32Constant = 0xABCDEF34ABCDEF34ABCDEF34ABCDEF34ABCDEF34ABCDEF34ABCDEF34ABCDEF34; bytes public constant bytesMemoryConstantA = hex"AAAdeadbeefAAAAAAdeadbeefAAAAAAdeadbeefAAAAAAdeadbeefAAAAAAdeadbeefAAAAAAdeadbeefAAAAAAdeadbeefAAA"; - bytes public constant bytesMemoryConstantB = hex"BBBbeedfeedBBBBBBbeedfeedBBBBBBbeedfeedBBBBBBbeedfeedBBBBBBbeedfeedBBBBBBbeedfeedBBBBBBbeedfeedBBB"; + bytes public constant bytesMemoryConstantB = "this should pass but the error message is much longer"; constructor(bytes memory _param) public { map[420] = _param; diff --git a/packages/rollup-dev-tools/test/contracts/library-transpilation/SafeMathUser.sol b/packages/rollup-dev-tools/test/contracts/library-transpilation/SafeMathUser.sol new file mode 100644 index 000000000000..f0ccf4ca9707 --- /dev/null +++ b/packages/rollup-dev-tools/test/contracts/library-transpilation/SafeMathUser.sol @@ -0,0 +1,9 @@ +pragma solidity ^0.5.0; + +import {SimpleSafeMath} from './SimpleSafeMath.sol'; + +contract SafeMathUser { + function use() public returns (uint) { + return SimpleSafeMath.addUint(2, 3); + } +} diff --git a/packages/rollup-dev-tools/test/contracts/library-transpilation/SimpleSafeMath.sol b/packages/rollup-dev-tools/test/contracts/library-transpilation/SimpleSafeMath.sol new file mode 100644 index 000000000000..3ea2aa32da59 --- /dev/null +++ b/packages/rollup-dev-tools/test/contracts/library-transpilation/SimpleSafeMath.sol @@ -0,0 +1,18 @@ +pragma solidity ^0.5.0; + +library SimpleSafeMath { + + function subUint(uint a, uint b) public returns(uint){ + + require(a >= b); // Make sure it doesn't return a negative value. + return a - b; + + } + function addUint(uint a , uint b) public pure returns(uint){ + + uint c = a + b; + + require(c >= a); // Makre sure the right computation was made + return c; + } +} \ No newline at end of file diff --git a/packages/rollup-dev-tools/test/transpiler/constants-transpilation.spec.ts b/packages/rollup-dev-tools/test/transpiler/constants-transpilation.spec.ts index a1cb8ac61c31..9c85878eb5cf 100644 --- a/packages/rollup-dev-tools/test/transpiler/constants-transpilation.spec.ts +++ b/packages/rollup-dev-tools/test/transpiler/constants-transpilation.spec.ts @@ -145,8 +145,8 @@ describe('Solitity contracts should have constants correctly accessible when usi retrievedBytesMemoryAVal.should.deep.equal(encodedBytesMemoryConstA) }) - const bytesMemoryConstB: Buffer = hexStrToBuf( - 'BBBbeedfeedBBBBBBbeedfeedBBBBBBbeedfeedBBBBBBbeedfeedBBBBBBbeedfeedBBBBBBbeedfeedBBBBBBbeedfeedBBB' + const bytesMemoryConstB: Buffer = Buffer.from( + `this should pass but the error message is much longer` ) it('should work for the first bytes memory constant', async () => { const retrievedBytesMemoryBVal: Buffer = await getGetterReturnedVal( diff --git a/packages/rollup-dev-tools/test/transpiler/constructor-transpilation.spec.ts b/packages/rollup-dev-tools/test/transpiler/constructor-transpilation.spec.ts index 4716bc5dad0a..d3bad97f246d 100644 --- a/packages/rollup-dev-tools/test/transpiler/constructor-transpilation.spec.ts +++ b/packages/rollup-dev-tools/test/transpiler/constructor-transpilation.spec.ts @@ -24,6 +24,7 @@ import * as ConstructorUsingConstantWithMultipleParams from '../contracts/build/ import * as ConstructorStoringParam from '../contracts/build/ConstructorStoringParam.json' import * as ConstructorStoringMultipleParams from '../contracts/build/ConstructorStoringMultipleParams.json' import * as Counter from '../contracts/build/Counter.json' +import * as SimpleSafeMath from '../contracts/build/SimpleSafeMath.json' /* Internal Imports */ import { @@ -114,6 +115,26 @@ describe('Solitity contracts with constructors that take inputs should be correc evmUtil ) }) + it('should work for a library construction', async () => { + const constructorParams = [] + const constructorParamTypes = [] + const resultsToCompare = await getManuallyTranspiledAndInitcodeTranspiledDeployedBytecode( + SimpleSafeMath, + constructorParams, + constructorParamTypes, + transpiler, + evmUtil + ) + const deployedViaInitcode: Buffer = + resultsToCompare.deployedViaTranspiledInitcode + const manuallyTranspiled: Buffer = + resultsToCompare.manuallyTranspiledDeployedBytecode + // deployed libraries should have their deployed address subbed in as the first thing being pushed to the stack. + // copy it over from the deployed version before checking equality + deployedViaInitcode.copy(manuallyTranspiled, 1, 1, 21) + + manuallyTranspiled.should.deep.equal(deployedViaInitcode) + }) it(`should work for waffle's counter example`, async () => { const constructorParams = [12345] const constructorParamTypes = ['uint256'] @@ -219,6 +240,31 @@ const assertTranspiledInitcodeDeploysManuallyTranspiledRawDeployedBytecode = asy transpiler: TranspilerImpl, evmUtil: EvmIntrospectionUtil ): Promise => { + const resultsToCompare = await getManuallyTranspiledAndInitcodeTranspiledDeployedBytecode( + contractBuildJSON, + constructorParams, + constructorParamsEncoding, + transpiler, + evmUtil + ) + const successfullyDeployedBytecode = + resultsToCompare.deployedViaTranspiledInitcode + const transpiledDeployedBytecode = + resultsToCompare.manuallyTranspiledDeployedBytecode + + successfullyDeployedBytecode.should.deep.equal(transpiledDeployedBytecode) +} + +const getManuallyTranspiledAndInitcodeTranspiledDeployedBytecode = async ( + contractBuildJSON: any, + constructorParams: any[], + constructorParamsEncoding: string[], + transpiler: TranspilerImpl, + evmUtil: EvmIntrospectionUtil +): Promise<{ + deployedViaTranspiledInitcode: Buffer + manuallyTranspiledDeployedBytecode: Buffer +}> => { // ****** // TRANSPILE AND DEPLOY INITCODE via transpiler.transpile() // ****** @@ -265,5 +311,8 @@ const assertTranspiledInitcodeDeploysManuallyTranspiledRawDeployedBytecode = asy )}` ) - successfullyDeployedBytecode.should.deep.equal(transpiledDeployedBytecode) + return { + deployedViaTranspiledInitcode: successfullyDeployedBytecode, + manuallyTranspiledDeployedBytecode: transpiledDeployedBytecode, + } } diff --git a/packages/rollup-dev-tools/test/transpiler/transpile.spec.ts b/packages/rollup-dev-tools/test/transpiler/transpile.spec.ts index 7824350dd1c3..6b3ee5ff7dfe 100644 --- a/packages/rollup-dev-tools/test/transpiler/transpile.spec.ts +++ b/packages/rollup-dev-tools/test/transpiler/transpile.spec.ts @@ -147,7 +147,8 @@ describe('Transpile', () => { }) describe('Enforces Invalid Bytes Consumed', () => { - it('flags invalid bytes consumed', () => { + // TODO: Handle this as a warning, since we now pad instead of throwing + it.skip('flags invalid bytes consumed', () => { const bytecode: Buffer = bytecodeToBuffer( invalidBytesConsumedBytecodeNoReturn ) @@ -230,17 +231,13 @@ describe('Transpile', () => { result.succeeded.should.equal(false) const error: ErroredTranspilation = result as ErroredTranspilation - error.errors.length.should.equal(3) + error.errors.length.should.equal(2) error.errors[0].index.should.equal(0) error.errors[0].error.should.equal(TranspilationErrors.UNSUPPORTED_OPCODE) error.errors[1].index.should.equal(bytecode.length - 2) error.errors[1].error.should.equal( TranspilationErrors.OPCODE_NOT_WHITELISTED ) - error.errors[2].index.should.equal(bytecode.length - 1) - error.errors[2].error.should.equal( - TranspilationErrors.INVALID_BYTES_CONSUMED - ) }) }) diff --git a/packages/rollup-full-node/src/app/utils.ts b/packages/rollup-full-node/src/app/utils.ts index d8bf0f4304c2..2ecb8f88bc86 100644 --- a/packages/rollup-full-node/src/app/utils.ts +++ b/packages/rollup-full-node/src/app/utils.ts @@ -76,9 +76,8 @@ export async function deployOvmContract( ) } -export async function createMockProvider() { +export async function createMockProvider(port: number = 9999) { const host = '0.0.0.0' - const port = 9999 const fullnodeHandler = await DefaultWeb3Handler.create() const fullnodeRpcServer = new FullnodeRpcServer(fullnodeHandler, host, port) fullnodeRpcServer.listen() @@ -110,7 +109,7 @@ export async function deployContract( ) { const factory = new ContractFactory( contractJSON.abi, - contractJSON.bytecode, + contractJSON.bytecode || contractJSON.evm.bytecode, wallet ) diff --git a/packages/solc-transpiler/package.json b/packages/solc-transpiler/package.json index 37eb637ef25e..a21639a0bb8f 100644 --- a/packages/solc-transpiler/package.json +++ b/packages/solc-transpiler/package.json @@ -31,10 +31,12 @@ "dependencies": { "@eth-optimism/core-utils": "0.0.1-alpha.12", "@eth-optimism/rollup-dev-tools": "0.0.1-alpha.12", + "ethers": "^4.0.45", "require-from-string": "^2.0.2", "solc": "^0.5.12" }, "devDependencies": { + "@eth-optimism/rollup-full-node": "^0.0.1-alpha.9", "@types/chai": "^4.1.7", "@types/mocha": "^5.2.7", "@types/node": "^12.0.7", diff --git a/packages/solc-transpiler/src/compiler.ts b/packages/solc-transpiler/src/compiler.ts index c9078b9f2de9..c77c0dfb2811 100644 --- a/packages/solc-transpiler/src/compiler.ts +++ b/packages/solc-transpiler/src/compiler.ts @@ -15,10 +15,14 @@ import { isValidHexAddress, Logger, remove0x, + objectsEqual, } from '@eth-optimism/core-utils' import * as solc from 'solc' import { execSync } from 'child_process' import * as requireFromString from 'require-from-string' +import { link } from 'fs' +import { formatBytecode, bufferToBytecode } from '../../rollup-core/build' +import { ethers } from 'ethers' const log: Logger = getLogger('solc-transpiler') @@ -77,15 +81,22 @@ export const compile = (configJsonString: string, callbacks?: any): string => { for (const [filename, fileJson] of Object.entries(res.contracts)) { log.debug(`Transpiling file: ${filename}`) for (const [contractName, contractJson] of Object.entries(fileJson)) { - log.debug(`Transpiling contract: ${contractName}`) + // Library links in bytecode strings have invalid hex: they are of the form __$asdfasdf$__. + // Because __$ is not a valid hex string, we replace with a valid hex string during transpilation, + // storing the links re-substituting the __$* strings afterwards + const originalRefStrings = getOriginalLinkRefStringsAndSubstituteValidHex( + contractJson + ) + log.debug( + `Transpiling contract: ${contractName} with valid hex strings for link placeholders.` + ) const output = transpileContract( transpiler, contractJson, filename, contractName ) - - log.debug(`Transpiled output ${JSON.stringify(output)}`) + log.debug(`Transpiled contract ${contractName}.`) res.contracts[filename][contractName].evm.bytecode.object = remove0x( output.bytecode || '' @@ -100,6 +111,14 @@ export const compile = (configJsonString: string, callbacks?: any): string => { contractName ].evm.deployedBytecode.object = remove0x(output.deployedBytecode || '') + log.debug( + `Updating links for all libraries by putting back original invalid hex (__$...$__) strings and updating link .start's.` + ) + updateLinkRefsAndSubstituteOriginalStrings( + res.contracts[filename][contractName], + originalRefStrings + ) + if (!!output.errors) { if (!res.errors) { res.errors = [] @@ -407,9 +426,6 @@ const transpileContract = ( filename: string, contractName: string ): TranspilationOutput => { - const originalBytecodeSize: number = hexStrToBuf( - contractSolcOutput.evm.bytecode.object - ).byteLength let bytecode: string = getBytecode(contractSolcOutput, false) let deployedBytecode: string = getBytecode(contractSolcOutput, true) @@ -418,6 +434,10 @@ const transpileContract = ( } if (!!bytecode) { + const originalBytecodeSize: number = hexStrToBuf( + contractSolcOutput.evm.bytecode.object + ).byteLength + const transpilationResult = transpiler.transpile( hexStrToBuf(bytecode), hexStrToBuf(deployedBytecode), @@ -433,12 +453,17 @@ const transpileContract = ( ), } } + bytecode = bufToHexString( (transpilationResult as SuccessfulTranspilation).bytecode ) } if (!!deployedBytecode) { + // log.debug(`replacing (DEPLOYED) bytecode library linkReferences with valid hex strings... input: \n${contractSolcOutput.evm.deployedBytecode.object.hex}`) + // const placeholders = substituteLinkPlaceholdersForValidBytes(contractSolcOutput.evm.deployedBytecode.object) + // log.debug(`replaced (DEPLOYED) bytecode library linkReferences with valid hex strings... output: \n${contractSolcOutput.evm.deployedBytecode.object.hex}`) + const transpilationResult = transpiler.transpileRawBytecode( hexStrToBuf(deployedBytecode) ) @@ -463,3 +488,157 @@ const transpileContract = ( deployedBytecode, } } + +/** + * Iterates over all library links, replacing the temporary valid hex strings with their original valid hex strings, and updating the new byte locations for the links. + * + * @param contractSolcOutput The contract solc output. + * @param originalPlaceholderStrings A mapping from library name string to __$*$__ string (invalid hex) which was substituted for a different placeholder so that transpilation works. + */ +const updateLinkRefsAndSubstituteOriginalStrings = ( + contractSolcOutput: any, + originalPlaceholderStrings: Map +): void => { + let placeholderIndex: number = 0 + const updatePlaceholderStartAndSubstituteOriginalString = ( + bytecodeObject: any, + linkLocation: any, + fileName: string, + libraryName: string + ) => { + const replacedHexString: string = getPlaceholderHexString(libraryName) + const newPlaceholderStart = bytecodeObject.object.indexOf(replacedHexString) + linkLocation.start = newPlaceholderStart / 2 // /2 because we found this from a hex string, but we operate on + + const placeholderLength = linkLocation.length * 2 // 2x because this is expressed in bytes but we will operate on hex string + const newPlaceholderEnd = newPlaceholderStart + placeholderLength + const originalPlaceholderString = originalPlaceholderStrings.get( + libraryName + ) + const prevBytecodeString: string = bytecodeObject.object + log.debug( + `Rebuilding ${placeholderIndex}th link for file ${fileName}, AKA library ${libraryName} with original placeholder ${originalPlaceholderString} at new location (${newPlaceholderStart},${newPlaceholderEnd}).` + ) + bytecodeObject.object = + prevBytecodeString.slice(0, newPlaceholderStart) + + originalPlaceholderString + + prevBytecodeString.slice(newPlaceholderEnd) + placeholderIndex++ + } + executeOnAllLinks( + contractSolcOutput.evm.bytecode, + updatePlaceholderStartAndSubstituteOriginalString, + 'PUT ORIGINAL __$ PLACEHOLDER STRINGS BACK INTO BYTECODE' + ) + executeOnAllLinks( + contractSolcOutput.evm.deployedBytecode, + updatePlaceholderStartAndSubstituteOriginalString, + 'PUT ORIGINAL __$ PLACEHOLDER STRINGS BACK INTO DEPLOYED BYTECODE' + ) +} + +/** + * Takes a contract solc output, and iterates over all library links, replacing the invalid hex __$*$__ strings with temporary valid hex strings so that they pass through the transpiler. + * + * @param contractSolcOutput The contract solc output. + * @returns A mapping from library name string to original __$*$__ string which was replaced with a valid hex string + */ +const getOriginalLinkRefStringsAndSubstituteValidHex = ( + solcOutput: any +): Map => { + const originalPlaceholderStrings = new Map() + let placeholderIndex: number = 0 + + const pushOriginalPlaceholderAndSubstituteValidHex = ( + bytecodeObject: any, + linkLocation: any, + fileName: string, + libraryName: string + ) => { + const placeholderStart: number = linkLocation.start * 2 // 2x because this is expressed in bytes but we will operate on hex string + const placeholderLength: number = linkLocation.length * 2 // 2x because this is expressed in bytes but we will operate on hex string + const placeholderEnd: number = placeholderLength + placeholderStart + const prevBytecodeString: string = bytecodeObject.object + const originalPlaceholderString: string = prevBytecodeString.slice( + placeholderStart, + placeholderEnd + ) + log.debug( + `Parsed ${placeholderIndex}th link for file ${fileName}, AKA library ${libraryName} with placeholder ${originalPlaceholderString} at elements (${placeholderStart},${placeholderEnd}) bytecode string.` + ) + bytecodeObject.object = + prevBytecodeString.slice(0, placeholderStart) + + getPlaceholderHexString(libraryName) + + prevBytecodeString.slice(placeholderEnd) + originalPlaceholderStrings.set(libraryName, originalPlaceholderString) + placeholderIndex++ + } + + executeOnAllLinks( + solcOutput.evm.bytecode, + pushOriginalPlaceholderAndSubstituteValidHex, + 'REPLACE BYTECODE INVALID HEX STRINGS WITH VALID' + ) + executeOnAllLinks( + solcOutput.evm.deployedBytecode, + pushOriginalPlaceholderAndSubstituteValidHex, + 'REPLACE DEPLOYED BYTECODE INVALID HEX STRINGS WITH VALID' + ) + + return originalPlaceholderStrings +} + +/** + * Executes the given callback on each linkReference (library metadata) for a given contractJSONOutput.bytecode or .deployedBytecode object + * + * @param bytecodeObjectFromSolcOutput The given contractJSONOutput.bytecode or .deployedBytecode object + * @param callback The callback to execute + * @param callbackDescription Optional logging string describing the functionality this callback will perform + */ +const executeOnAllLinks = ( + bytecodeObjectFromSolcOutput: any, + callback: ( + bytecodeObject: any, + linkLocation: any, + fileName: string, + libraryName: string + ) => void, + callbackDescription: string = '(UNSPECIFIED)' +) => { + log.debug( + `asked to execute callback performing functionality: [${callbackDescription}] on all JSON links...` + ) + const linkRefs = bytecodeObjectFromSolcOutput.linkReferences + for (const fileName in linkRefs) { + // tslint:disable + const libraryName: string = Object.keys(linkRefs[fileName])[0] + log.debug(`Parsing links for ${libraryName}...`) + for (const linkLocation of linkRefs[fileName][libraryName]) { + callback( + bytecodeObjectFromSolcOutput, + linkLocation, + fileName, + libraryName + ) + } + } +} + +/** + * Gets a deterministic, collision-resistant valid hex string placeholder for a given input string + * + * @param libraryName The string for which to get a deterministic hex string. + * @param byteLength Number of bytes worth of hex string to return. + */ +const getPlaceholderHexString = ( + libraryName: string, + bytelength: number = 20 +): string => { + const randomHexString: string = ethers.utils.keccak256( + '0x' + + Buffer.from( + `${libraryName}PLACEHOLDERPLACEHOLDERPLACEHOLDERPLACEHOLDERPLACEHOLDEROPTIMSIMPBC` + ).toString('hex') + ) + return remove0x(randomHexString).slice(0, 2 * bytelength) +} diff --git a/packages/solc-transpiler/test/contracts/Dummy.sol b/packages/solc-transpiler/test/contracts/dummy/Dummy.sol similarity index 100% rename from packages/solc-transpiler/test/contracts/Dummy.sol rename to packages/solc-transpiler/test/contracts/dummy/Dummy.sol diff --git a/packages/solc-transpiler/test/contracts/Dummy2.sol b/packages/solc-transpiler/test/contracts/dummy/Dummy2.sol similarity index 100% rename from packages/solc-transpiler/test/contracts/Dummy2.sol rename to packages/solc-transpiler/test/contracts/dummy/Dummy2.sol diff --git a/packages/solc-transpiler/test/contracts/library/SafeMathUser.sol b/packages/solc-transpiler/test/contracts/library/SafeMathUser.sol new file mode 100644 index 000000000000..347c9a836fb5 --- /dev/null +++ b/packages/solc-transpiler/test/contracts/library/SafeMathUser.sol @@ -0,0 +1,13 @@ +pragma solidity ^0.5.0; + +import {SimpleSafeMath} from './SimpleSafeMath.sol'; +import {SimpleUnsafeMath} from './SimpleUnsafeMath.sol'; + +contract SafeMathUser { + function useLib() public pure returns (uint) { + return SimpleSafeMath.addUint(2, 3); + } + function use2Libs() public pure returns(uint) { + return SimpleUnsafeMath.addUint(SimpleSafeMath.addUint(1, 2), 3); + } +} diff --git a/packages/solc-transpiler/test/contracts/library/SimpleSafeMath.sol b/packages/solc-transpiler/test/contracts/library/SimpleSafeMath.sol new file mode 100644 index 000000000000..3ea2aa32da59 --- /dev/null +++ b/packages/solc-transpiler/test/contracts/library/SimpleSafeMath.sol @@ -0,0 +1,18 @@ +pragma solidity ^0.5.0; + +library SimpleSafeMath { + + function subUint(uint a, uint b) public returns(uint){ + + require(a >= b); // Make sure it doesn't return a negative value. + return a - b; + + } + function addUint(uint a , uint b) public pure returns(uint){ + + uint c = a + b; + + require(c >= a); // Makre sure the right computation was made + return c; + } +} \ No newline at end of file diff --git a/packages/solc-transpiler/test/contracts/library/SimpleUnsafeMath.sol b/packages/solc-transpiler/test/contracts/library/SimpleUnsafeMath.sol new file mode 100644 index 000000000000..21eae455d839 --- /dev/null +++ b/packages/solc-transpiler/test/contracts/library/SimpleUnsafeMath.sol @@ -0,0 +1,14 @@ +pragma solidity ^0.5.0; + +library SimpleUnsafeMath { + + function subUint(uint a, uint b) public returns(uint){ + + return a - b; + + } + function addUint(uint a , uint b) public pure returns(uint){ + uint c = a + b; + return c; + } +} \ No newline at end of file diff --git a/packages/solc-transpiler/test/libraries.spec.ts b/packages/solc-transpiler/test/libraries.spec.ts new file mode 100644 index 000000000000..7584bb67cf6a --- /dev/null +++ b/packages/solc-transpiler/test/libraries.spec.ts @@ -0,0 +1,141 @@ +import './setup' + +/* External Imports */ +import { + OpcodeReplacerImpl, + OpcodeWhitelistImpl, + SuccessfulTranspilation, + TranspilationResult, + Transpiler, + TranspilerImpl, +} from '@eth-optimism/rollup-dev-tools' +import { + bufToHexString, + hexStrToBuf, + remove0x, + ZERO_ADDRESS, + getLogger, +} from '@eth-optimism/core-utils' +import { + createMockProvider, + getWallets, + deployContract, +} from '@eth-optimism/rollup-full-node' +import { link } from 'ethereum-waffle' + +import * as path from 'path' +import * as fs from 'fs' + +/* Internal Imports */ +import { compile } from '../src' +import { formatBytecode, bufferToBytecode } from '../../rollup-core/build' + +const log = getLogger('library-use-compilation') + +const safeMathUserPath = path.resolve( + __dirname, + './contracts/library/SafeMathUser.sol' +) +const simpleSafeMathPath = path.resolve( + __dirname, + './contracts/library/SimpleSafeMath.sol' +) +const simpleUnsafeMathPath = path.resolve( + __dirname, + './contracts/library/SimpleUnsafeMath.sol' +) + +describe('Library usage tests', () => { + let config + before(() => { + config = { + language: 'Solidity', + sources: { + 'SafeMathUser.sol': { + content: fs.readFileSync(safeMathUserPath, 'utf8'), + }, + 'SimpleSafeMath.sol': { + content: fs.readFileSync(simpleSafeMathPath, 'utf8'), + }, + 'SimpleUnsafeMath.sol': { + content: fs.readFileSync(simpleUnsafeMathPath, 'utf8'), + }, + }, + settings: { + outputSelection: { + '*': { + '*': ['*'], + }, + }, + executionManagerAddress: '0xA193E42526F1FEA8C99AF609dcEabf30C1c29fAA', + }, + } + }) + + let provider + let wallet + let deployedLibUser + beforeEach(async function() { + this.timeout(20000) + // NOTE: if we run this test in isolation on default port, it works, but in multi-package tests it fails. + // Hypothesis for why this is: multi-package tests are run in parallel, so we need to use a separate port per package. + provider = await createMockProvider(9998) + const wallets = getWallets(provider) + wallet = wallets[0] + + const wrappedSolcResult = compile(JSON.stringify(config)) + const wrappedSolcJson = JSON.parse(wrappedSolcResult) + const simpleSafeMathJSON = + wrappedSolcJson['contracts']['SimpleSafeMath.sol']['SimpleSafeMath'] + const simpleUnsafeMathJSON = + wrappedSolcJson['contracts']['SimpleUnsafeMath.sol']['SimpleUnsafeMath'] + const libUserJSON = + wrappedSolcJson['contracts']['SafeMathUser.sol']['SafeMathUser'] + + // Deploy and link safe math + const deployedSafeMath = await deployContract( + wallet, + simpleSafeMathJSON, + [], + [] + ) + log.debug(`deployed SimpleSafeMath to: ${deployedSafeMath.address}`) + link( + libUserJSON, + 'SimpleSafeMath.sol:SimpleSafeMath', + deployedSafeMath.address + ) + + // Deoloy and link unsafe math + const deployedUnsafeMath = await deployContract( + wallet, + simpleUnsafeMathJSON, + [], + [] + ) + log.debug(`deployed UnsafeMath to: ${deployedUnsafeMath.address}`) + log.debug(`before second link: ${JSON.stringify(libUserJSON)}`) + link( + libUserJSON, + 'SimpleUnsafeMath.sol:SimpleUnsafeMath', + deployedUnsafeMath.address + ) + + // Deploy library user + deployedLibUser = await deployContract(wallet, libUserJSON, [], []) + log.debug(`deployed library user to: ${deployedLibUser.address}`) + }) + afterEach(async () => { + await provider.closeOVM() + }) + + it('should allow us to transpile, link, and query contract methods which use a single library', async () => { + const returnedUsingLib = await deployedLibUser.useLib() + returnedUsingLib._hex.should.equal('0x05') + }) + + it('should allow us to transpile, link, and query contract methods which use a multiple libraries', async () => { + const returnedUsingLib = await deployedLibUser.use2Libs() + returnedUsingLib._hex.should.equal('0x06') + }) +}) diff --git a/packages/solc-transpiler/test/wrapper.spec.ts b/packages/solc-transpiler/test/wrapper.spec.ts index 79576d492112..63ea554105f3 100644 --- a/packages/solc-transpiler/test/wrapper.spec.ts +++ b/packages/solc-transpiler/test/wrapper.spec.ts @@ -24,38 +24,44 @@ import * as DummyContract from './contracts/build/Dummy.json' import * as Dummy2Contract from './contracts/build/Dummy2.json' import * as Dummy3Contract from './contracts/build/Dummy3.json' -const dummyPath = path.resolve(__dirname, './contracts/Dummy.sol') -const dummy2Path = path.resolve(__dirname, './contracts/Dummy2.sol') -const config = { - language: 'Solidity', - sources: { - 'Dummy.sol': { - content: fs.readFileSync(dummyPath, 'utf8'), - }, - }, - settings: { - outputSelection: { - '*': { - '*': ['*'], - }, - }, - executionManagerAddress: ZERO_ADDRESS, - }, -} - -const multiConfig = { ...config } -multiConfig.sources['Dummy2.sol'] = { - content: fs.readFileSync(dummy2Path, 'utf8'), -} - -const configWithoutLegacyAssembly = { ...config } -configWithoutLegacyAssembly.settings.outputSelection['*']['*'] = [ - 'abi', - 'evm.bytecode', - 'evm.deployedBytecode', -] +const dummyPath = path.resolve(__dirname, './contracts/dummy/Dummy.sol') +const dummy2Path = path.resolve(__dirname, './contracts/dummy/Dummy2.sol') describe('Wrapper tests', () => { + let config + let configWithoutLegacyAssembly + let multiConfig + before(() => { + config = { + language: 'Solidity', + sources: { + 'Dummy.sol': { + content: fs.readFileSync(dummyPath, 'utf8'), + }, + }, + settings: { + outputSelection: { + '*': { + '*': ['*'], + }, + }, + executionManagerAddress: ZERO_ADDRESS, + }, + } + + multiConfig = { ...config } + multiConfig.sources['Dummy2.sol'] = { + content: fs.readFileSync(dummy2Path, 'utf8'), + } + + configWithoutLegacyAssembly = { ...config } + configWithoutLegacyAssembly.settings.outputSelection['*']['*'] = [ + 'abi', + 'evm.bytecode', + 'evm.deployedBytecode', + ] + }) + const transpiler: Transpiler = new TranspilerImpl( new OpcodeWhitelistImpl(), new OpcodeReplacerImpl(ZERO_ADDRESS) diff --git a/yarn.lock b/yarn.lock index c875a150402e..28776d343a1f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4311,7 +4311,7 @@ ethers@4.0.0-beta.3: uuid "2.0.1" xmlhttprequest "1.8.0" -ethers@^4.0.0, ethers@^4.0.37, ethers@^4.0.39, ethers@^4.0.42: +ethers@^4.0.0, ethers@^4.0.37, ethers@^4.0.39, ethers@^4.0.42, ethers@^4.0.45: version "4.0.45" resolved "https://registry.yarnpkg.com/ethers/-/ethers-4.0.45.tgz#8d4cd764d7c7690836b583d4849203c225eb56e2" integrity sha512-N/Wmc6Mw4pQO+Sss1HnKDCSS6KSCx0luoBMiPNq+1GbOaO3YaZOyplBEhj+NEoYsizZYODtkITg2oecPeNnidQ==