From 19c2e94ce1cd08a8d9275baa0392151537e36050 Mon Sep 17 00:00:00 2001 From: ben-chain Date: Tue, 7 Apr 2020 20:00:08 -0400 Subject: [PATCH] YAS 277 Logarithmic-time JUMP transpilation (#69) * footer bytecode gen looks right * tests all passing * lint * fix timeout for CI * naming and comments * lint * make search tree be a BST * other PR feedback * linting * final PR feedback * remove unused logger --- packages/rollup-core/src/types/opcodes.ts | 14 +- .../tools/transpiler/binary-search-tree.ts | 331 ++++++++++++++++++ .../src/tools/transpiler/index.ts | 1 + .../src/tools/transpiler/jump-replacement.ts | 131 +------ .../src/tools/transpiler/transpiler.ts | 40 ++- .../src/tools/vm/evm-introspection-util.ts | 2 +- .../src/types/transpiler/types.ts | 9 + .../jump-transpilation/SimpleJumper.sol | 2 +- .../test/transpiler/jump-integration.spec.ts | 30 +- .../test/transpiler/transpile.jump.spec.ts | 24 -- 10 files changed, 401 insertions(+), 183 deletions(-) create mode 100644 packages/rollup-dev-tools/src/tools/transpiler/binary-search-tree.ts diff --git a/packages/rollup-core/src/types/opcodes.ts b/packages/rollup-core/src/types/opcodes.ts index a9d51a1f02fc..8099712eb533 100644 --- a/packages/rollup-core/src/types/opcodes.ts +++ b/packages/rollup-core/src/types/opcodes.ts @@ -16,8 +16,18 @@ export type EVMBytecode = EVMOpcodeAndBytes[] export interface OpcodeTag { padPUSH: boolean // whether this PUSHN should be turned into a PUSH(N+1) to preempt later changes to consumedBytes in transpilation. - reasonTagged: string - metadata: Buffer + reasonTagged: string | OpcodeTagReason + metadata: any +} + +export enum OpcodeTagReason { + IS_CONSTANT_OFFSET, + IS_DEPLOY_CODECOPY_OFFSET, + IS_DEPLOY_CODE_LENGTH, + IS_CONSTRUCTOR_INPUTS_OFFSET, + IS_PUSH_BINARY_SEARCH_NODE_LOCATION, + IS_BINARY_SEARCH_NODE_JUMPDEST, + IS_PUSH_MATCH_SUCCESS_LOC, } export class Opcode { diff --git a/packages/rollup-dev-tools/src/tools/transpiler/binary-search-tree.ts b/packages/rollup-dev-tools/src/tools/transpiler/binary-search-tree.ts new file mode 100644 index 000000000000..395059c7b41c --- /dev/null +++ b/packages/rollup-dev-tools/src/tools/transpiler/binary-search-tree.ts @@ -0,0 +1,331 @@ +import { + bytecodeToBuffer, + EVMBytecode, + Opcode, + EVMOpcodeAndBytes, + formatBytecode, + getPCOfEVMBytecodeIndex, + OpcodeTagReason, +} from '@eth-optimism/rollup-core' +import { bufferUtils, getLogger } from '@eth-optimism/core-utils' +import { getPUSHOpcode, getPUSHIntegerOp } from './helpers' +import { BinarySearchTreeNode } from '../../types/transpiler' + +// The max number of bytes we expect a JUMPDEST's PC to be expressible in. Setting to 3 allows 16 MB contracts--more than enough! +const pcMaxByteSize = 3 + +/** + * Generates a JUMP-correctiing binary search tree block which is used to map pre-transpiled JUMPDESTs too post-transpiled JUMPDESTs. + * + * @param keys The jumpdest PCs before transpilation occurred + * @param values The jumpdest PCs after transpilation occurred. + * @param indexOfThisBlock the PC that this block of bytecode will be placed in. + * @returns Bytecode block of the JUMPDEST-mapping functionality + */ + +export const buildJumpBSTBytecode = ( + keys: number[], + values: number[], + indexOfThisBlock: number +): EVMBytecode => { + if (keys.length !== values.length) { + throw new Error( + `Asked to build binary search tree, but given key array of length ${keys.length} and value array of length ${values.length}` + ) + } + const rootNode = getBSTRoot(keys, values) + const BSTBytecodeWithIncorrectJUMPs: EVMBytecode = [ + // Bytecode to JUMP to when a successful match is found by a BST node + ...getJumpdestMatchSuccessBytecode(), + // Entry point for the actual BST matching logic + { + opcode: Opcode.JUMPDEST, + consumedBytes: undefined, + }, + // Bytecode for the "root subtree" recursively generates all BST logic as bytecode. + ...getBytecodeForSubtreeRoot(rootNode, false), + ] + return fixJUMPsToNodes(BSTBytecodeWithIncorrectJUMPs, indexOfThisBlock) +} + +/** + * Generates a binary search tree based on a set of keys to search against, and values associated with each key. + * + * @param keys The (lowest-to-highest) ordered array of keys to be searched for. + * @param values The array of corresponding values to return. + * @returns A root BST node whose ancestors represent the full BST for the given k/v pairs. + */ +export const getBSTRoot = (keys: number[], values: number[]) => { + // Associate nodes with k/v pairs and sort before building the tree + const bottomNodes: BinarySearchTreeNode[] = keys.map( + (key: number, index: number) => { + return { + value: { + jumpdestBefore: keys[index], + jumpdestAfter: values[index], + }, + left: undefined, + right: undefined, + } + } + ) + const sortedBottomNodes = bottomNodes.sort( + (node1: BinarySearchTreeNode, node2: BinarySearchTreeNode) => { + return node1.value.jumpdestBefore - node2.value.jumpdestBefore + } + ) + return buildBST(sortedBottomNodes) +} + +/** + * Generates a binary search tree from a sorted list of values. + * @param sortedValues The sorted BST nodes to be searched for + * @returns The root node of the resulting BST. + */ +export const buildBST = ( + sortedAncestors: BinarySearchTreeNode[] +): BinarySearchTreeNode => { + if (sortedAncestors.length === 0) { + return undefined + } + + const rootIndex = Math.floor(sortedAncestors.length / 2) + const leftSubtreeElements: BinarySearchTreeNode[] = sortedAncestors.slice( + 0, + rootIndex + ) + const rightSubtreeElements: BinarySearchTreeNode[] = sortedAncestors.slice( + rootIndex + 1 + ) + return { + value: sortedAncestors[rootIndex].value, + left: buildBST(leftSubtreeElements), + right: buildBST(rightSubtreeElements), + } +} + +/** + * Generates bytecode executing a binary search tree for a given node's subtree. + * Recursively executes with left->right, depth-first approach. + * @param node The BST nodeto + * @returns The root node of the resulting BST. + */ +const getBytecodeForSubtreeRoot = ( + node: BinarySearchTreeNode, + isRightNode: boolean +): EVMBytecode => { + if (!node) { + return [] + } + const bytecodeToReturn: EVMBytecode = [] + // Left->right, depth first makes it so that right nodes are always JUMPed to, and left nodes are continued to from the parent node with no JUMP. + if (isRightNode) { + bytecodeToReturn.push(generateBinarySearchTreeNodeJumpdest(node)) + } + // Generate the match check for this node + bytecodeToReturn.push(...generateNodeEqualityCheckBytecode(node)) + // If there are no children to continue to, there is definitely no match--STOP as this was an invalid JUMP according to pre-transpilation JUMPDESTs + if (!node.left && !node.right) { + bytecodeToReturn.push({ + opcode: Opcode.STOP, + consumedBytes: undefined, + }) + return bytecodeToReturn + } + // If this node has a right child, check whether the stack input is greater than this node value and JUMP there. Otherwise we will continue to the left. + if (node.right) { + bytecodeToReturn.push( + ...generateIfGreaterThenJumpToRightChildBytecode(node) + ) + } + // generate bytecode for the next subtree enforcing left->right depth first execution so that every left sibling is continued to, every right sibling JUMPed to + bytecodeToReturn.push( + ...getBytecodeForSubtreeRoot(node.left, false), + ...getBytecodeForSubtreeRoot(node.right, true) + ) + return bytecodeToReturn +} + +/** + * Generates a bytecode block that checks if the current stack element is >= this node's value, and jumping to the right child node if so. + * + * @param node The BST node being inequality checked + * @returns The correctly tagged bytecode jumping to the right child of this node if needed. + */ +const generateIfGreaterThenJumpToRightChildBytecode = ( + node: BinarySearchTreeNode +): EVMBytecode => { + return [ + { + opcode: Opcode.DUP1, + consumedBytes: undefined, + }, + // PUSH the key to be compared to determine which node to proceed to + getPUSHIntegerOp(node.value.jumpdestBefore), + // Compare the keys + { + opcode: Opcode.LT, + consumedBytes: undefined, + }, + // PUSH a *placeholder* for the destination of thde right child to be JUMPed to if check passes--to be set later + { + opcode: getPUSHOpcode(pcMaxByteSize), + consumedBytes: Buffer.alloc(pcMaxByteSize), + tag: { + padPUSH: false, + reasonTagged: OpcodeTagReason.IS_PUSH_BINARY_SEARCH_NODE_LOCATION, + metadata: { node }, + }, + }, + // JUMP if the LT check passed + { + opcode: Opcode.JUMPI, + consumedBytes: undefined, + }, + ] +} + +/** + * Generates a bytecode block that checks for equality of the stack element to the node, and jumping to a match success block if so. + * + * @param node The BST node being equality checked + * @returns The correctly tagged bytecode jumping to the match success case for this node. + */ +const generateNodeEqualityCheckBytecode = ( + node: BinarySearchTreeNode +): EVMBytecode => { + return [ + // DUP the value to match without deleting it forever + { + opcode: Opcode.DUP1, + consumedBytes: undefined, + }, + // Compare to JUMPDEST before + getPUSHIntegerOp(node.value.jumpdestBefore), + { + opcode: Opcode.EQ, + consumedBytes: undefined, + }, + // If match, we will send the JUMPDEST after to the success block + getPUSHIntegerOp(node.value.jumpdestAfter), + { + opcode: Opcode.SWAP1, + consumedBytes: undefined, + }, + // PUSH success block location (via a tag--will be filled out later) + { + opcode: getPUSHOpcode(pcMaxByteSize), + consumedBytes: Buffer.alloc(pcMaxByteSize), + tag: { + padPUSH: false, + reasonTagged: OpcodeTagReason.IS_PUSH_MATCH_SUCCESS_LOC, + metadata: undefined, + }, + }, + // JUMPI to success block if match + { + opcode: Opcode.JUMPI, + consumedBytes: undefined, + }, + // POP the JUMPDESTafter if not a match. + { + opcode: Opcode.POP, + consumedBytes: undefined, + }, + ] +} + +/** + * Generates a tagged JUMPDEST for a binary search node + * + * @param node The BST node being jumped to (used for tagging for later correction) + * @returns The correctly tagged JUMPDEST. + */ +const generateBinarySearchTreeNodeJumpdest = ( + node: BinarySearchTreeNode +): EVMOpcodeAndBytes => { + return { + opcode: Opcode.JUMPDEST, + consumedBytes: undefined, + tag: { + padPUSH: false, + reasonTagged: OpcodeTagReason.IS_BINARY_SEARCH_NODE_JUMPDEST, + metadata: { node }, + }, + } +} + +/** + * Gets the success jumpdest for the footer switch statement. This will be jumped to when the + * switch statement finds a match. It is responsible for getting rid of extra stack arguments + * that the footer switch statement adds. + * + * @returns The success bytecode. + */ + +export const getJumpdestMatchSuccessBytecode = (): EVMBytecode => { + return [ + // This JUMPDEST is hit on successful switch match + { opcode: Opcode.JUMPDEST, consumedBytes: undefined }, + // Swaps the duped pre-transpilation JUMPDEST with the post-transpilation JUMPDEST + { opcode: Opcode.SWAP1, consumedBytes: undefined }, + // Pops the pre-transpilation JUMPDEST + { opcode: Opcode.POP, consumedBytes: undefined }, + // Jumps to the post-transpilation JUMPDEST + { opcode: Opcode.JUMP, consumedBytes: undefined }, + ] +} + +/** + * Fixes all PUSHes tagged by appendAncestorsToBytecode() to correspond to the correct nodes' JUMPDESTs + * + * @param bytecode The tagged bytecode of the jump search table with incorrect PUSHes for the jumnpdests + * @param indexOfThisBlock The offset of this block in the bytecode where the result will be inserted + * @returns The EVM Bytecode with fixed PUSHes. + */ +const fixJUMPsToNodes = ( + bytecode: EVMBytecode, + indexOfThisBlock: number +): EVMBytecode => { + for (const pushMatchSuccessOp of bytecode.filter( + (x) => + !!x.tag && + x.tag.reasonTagged === OpcodeTagReason.IS_PUSH_MATCH_SUCCESS_LOC + )) { + pushMatchSuccessOp.consumedBytes = bufferUtils.numberToBuffer( + indexOfThisBlock, + pcMaxByteSize, + pcMaxByteSize + ) + } + for (const pushBSTNodeOp of bytecode.filter( + (x) => + !!x.tag && + x.tag.reasonTagged === OpcodeTagReason.IS_PUSH_BINARY_SEARCH_NODE_LOCATION + )) { + const rightChild: BinarySearchTreeNode = + pushBSTNodeOp.tag.metadata.node.right + // Find the index of the right child's JUMPDEST in the bytecode, for each node. + const rightChildJumpdestIndexInBytecodeBlock = bytecode.findIndex( + (toCheck: EVMOpcodeAndBytes) => { + return ( + !!toCheck.tag && + toCheck.tag.reasonTagged === + OpcodeTagReason.IS_BINARY_SEARCH_NODE_JUMPDEST && + toCheck.tag.metadata.node === rightChild + ) + } + ) + // Calculate the PC of the found JUMPDEST, offsetting by the index of this block. + const rightChildJumpdestPC = + indexOfThisBlock + + getPCOfEVMBytecodeIndex(rightChildJumpdestIndexInBytecodeBlock, bytecode) + // Set the consumed bytes to be this PC + pushBSTNodeOp.consumedBytes = bufferUtils.numberToBuffer( + rightChildJumpdestPC, + pcMaxByteSize, + pcMaxByteSize + ) + } + return bytecode +} diff --git a/packages/rollup-dev-tools/src/tools/transpiler/index.ts b/packages/rollup-dev-tools/src/tools/transpiler/index.ts index 2b861e980ea7..970a65c588f9 100644 --- a/packages/rollup-dev-tools/src/tools/transpiler/index.ts +++ b/packages/rollup-dev-tools/src/tools/transpiler/index.ts @@ -7,3 +7,4 @@ export * from './opcode-whitelist' export * from './static-memory-opcodes' export * from './transpiler' export * from './util' +export * from './binary-search-tree' diff --git a/packages/rollup-dev-tools/src/tools/transpiler/jump-replacement.ts b/packages/rollup-dev-tools/src/tools/transpiler/jump-replacement.ts index 9bf3e4c4dbbe..d9760ab07765 100644 --- a/packages/rollup-dev-tools/src/tools/transpiler/jump-replacement.ts +++ b/packages/rollup-dev-tools/src/tools/transpiler/jump-replacement.ts @@ -11,6 +11,7 @@ import { TranspilationErrors, } from '../../types/transpiler' import { createError } from './util' +import { buildJumpBSTBytecode, getJumpdestMatchSuccessBytecode } from './' const log = getLogger('jump-replacement') @@ -71,7 +72,7 @@ export const accountForJumps = ( // Add the logic to handle the pre-transpilation to post-transpilation jump dest mapping. replacedBytecode.push( - ...getJumpIndexSwitchStatementBytecode( + ...buildJumpBSTBytecode( jumpdestIndexesBefore, jumpdestIndexesAfter, bytecodeToBuffer(replacedBytecode).length @@ -163,130 +164,6 @@ export const getJumpiReplacementBytecode = ( ] } -/** - * Gets the success jumpdest for the footer switch statement. This will be jumped to when the - * switch statement finds a match. It is responsible for getting rid of extra stack arguments - * that the footer switch statement adds. - * - * @returns The success bytecode. - */ -const getJumpIndexSwitchStatementSuccessJumpdestBytecode = (): EVMBytecode => { - return [ - // This JUMPDEST is hit on successful switch match - { opcode: Opcode.JUMPDEST, consumedBytes: undefined }, - // Swaps the duped pre-transpilation JUMPDEST with the post-transpilation JUMPDEST - { opcode: Opcode.SWAP1, consumedBytes: undefined }, - // Pops the pre-transpilation JUMPDEST - { opcode: Opcode.POP, consumedBytes: undefined }, - // Jumps to the post-transpilation JUMPDEST - { opcode: Opcode.JUMP, consumedBytes: undefined }, - ] -} - -/** - * Gets the EVMBytecode to read a pre-transpilation JUMPDEST index off of the stack and - * JUMP to the associated post-transpilation JUMPDEST. - * See: https://github.com/op-optimism/optimistic-rollup/wiki/Transpiler#jump-transpilation-approach - * for more information on why this is necessary and how replacement occurs. - * - * @param jumpdestIndexesBefore The array of of pre-transpilation JUMPDEST indexes. - * @param jumpdestIndexesAfter The array of of post-transpilation JUMPDEST indexes. - * @param indexOfThisBlock The index in the bytecode that this block will be at. - * @returns The JUMP switch statement bytecode. - */ -export const getJumpIndexSwitchStatementBytecode = ( - jumpdestIndexesBefore: number[], - jumpdestIndexesAfter: number[], - indexOfThisBlock: number -): EVMBytecode => { - const successJumpIndex: Buffer = bufferUtils.numberToBufferPacked( - indexOfThisBlock, - 2 - ) - - const footerBytecode: EVMBytecode = [ - ...getJumpIndexSwitchStatementSuccessJumpdestBytecode(), - // Switch statement jumpdest - { opcode: Opcode.JUMPDEST, consumedBytes: undefined }, - ] - for (let i = 0; i < jumpdestIndexesBefore.length; i++) { - log.debug( - `Adding bytecode to replace ${jumpdestIndexesBefore[i]} with ${jumpdestIndexesAfter[i]}` - ) - - const beforeIndex: number = jumpdestIndexesBefore[i] - const afterIndex: number = jumpdestIndexesAfter[i] - const beforeBuffer: Buffer = bufferUtils.numberToBufferPacked( - jumpdestIndexesBefore[i], - 2 - ) - const afterBuffer: Buffer = bufferUtils.numberToBufferPacked( - jumpdestIndexesAfter[i], - 2 - ) - - footerBytecode.push( - ...[ - { - opcode: Opcode.DUP1, - consumedBytes: undefined, - }, - getPUSHIntegerOp(beforeIndex), - { - opcode: Opcode.EQ, - consumedBytes: undefined, - }, - // push ACTUAL jumpdest - getPUSHIntegerOp(afterIndex), - { - // swap actual jumpdest with EQ result so stack is [eq result, actual jumpdest, duped compare jumpdest, ...] - opcode: Opcode.SWAP1, - consumedBytes: undefined, - }, - { - // push loop exit jumpdest - opcode: getPUSHOpcode(successJumpIndex.length), - consumedBytes: successJumpIndex, - }, - { - opcode: Opcode.JUMPI, - consumedBytes: undefined, - }, - // pop ACTUAL jumpdest because this is not a match - { - opcode: Opcode.POP, - consumedBytes: undefined, - }, - ] - ) - } - // If pre-transpile JUMPDEST index is not found, revert. - footerBytecode.push({ opcode: Opcode.REVERT, consumedBytes: undefined }) - return footerBytecode -} - -/** - * Gets the expected index of the successful switch match JUMPDEST in the footer switch statement. - * - * @param transpiledBytecode The transpiled bytecode in question. - * @returns The expected index of the JUMPDEST for successful footer switch matches. - */ -const getFooterSwitchStatementSuccessJumpdestIndex = ( - transpiledBytecode: EVMBytecode -): number => { - let length: number = 0 - for (const opcodeAndBytes of transpiledBytecode) { - if (opcodeAndBytes.opcode === Opcode.JUMP) { - length += getJumpReplacementBytecodeLength() - } else if (opcodeAndBytes.opcode === Opcode.JUMPI) { - length += getJumpiReplacementBytecodeLength() - } else { - length += 1 + opcodeAndBytes.opcode.programBytesConsumed - } - } - return length -} - /** * Gets the expected index of the footer JUMP switch statement, given EVMBytecode * that will *only* change by replacing JUMP, JUMPI, and JUMPDEST with the appropriate @@ -308,8 +185,6 @@ export const getExpectedFooterSwitchStatementJumpdestIndex = ( length += 1 + opcodeAndBytes.opcode.programBytesConsumed } } - length += bytecodeToBuffer( - getJumpIndexSwitchStatementSuccessJumpdestBytecode() - ).length + length += bytecodeToBuffer(getJumpdestMatchSuccessBytecode()).length return length } diff --git a/packages/rollup-dev-tools/src/tools/transpiler/transpiler.ts b/packages/rollup-dev-tools/src/tools/transpiler/transpiler.ts index 70a98c626a53..8337611cdad0 100644 --- a/packages/rollup-dev-tools/src/tools/transpiler/transpiler.ts +++ b/packages/rollup-dev-tools/src/tools/transpiler/transpiler.ts @@ -8,6 +8,7 @@ import { formatBytecode, bufferToBytecode, getPCOfEVMBytecodeIndex, + OpcodeTagReason, } from '@eth-optimism/rollup-core' import { getLogger, @@ -34,14 +35,6 @@ import { accountForJumps } from './jump-replacement' const log = getLogger('transpiler-impl') -const IS_CONSTANT_OFFSET: string = 'THIS IS A CONSTANT OFFSET' -const IS_DEPLOY_CODECOPY_OFFSET: string = - 'THIS IS THE OFFSET TO CODECOPY DEPLOYED BYTECODE FROM INITCODE DURING CREATE/2' -const IS_DEPLOY_CODE_LENGTH: string = - 'THIS IS THE LENGTH OF DEPLOYED BYTECODE TO COPY AND RETURN DURING CREATE/2' -const IS_CONSTRUCTOR_INPUTS_OFFSET: string = - 'THIS IS THE OFFSET AFTER WHICH CONSTRUCTOR INPUTS SHOULD BE' - export class TranspilerImpl implements Transpiler { constructor( private readonly opcodeWhitelist: OpcodeWhitelist, @@ -219,7 +212,10 @@ export class TranspilerImpl implements Transpiler { for (const [index, op] of transpiledConstructorInitLogic.entries()) { const tag = op.tag - if (!!tag && tag.reasonTagged === IS_CONSTRUCTOR_INPUTS_OFFSET) { + if ( + !!tag && + tag.reasonTagged === OpcodeTagReason.IS_CONSTRUCTOR_INPUTS_OFFSET + ) { // this should be the total length of the bytecode we're about to have generated! transpiledConstructorInitLogic[index].consumedBytes = new BigNum( transpiledInitLogicByteLength + @@ -227,12 +223,15 @@ export class TranspilerImpl implements Transpiler { constantsUsedByConstructorLength ).toBuffer('be', op.opcode.programBytesConsumed) } - if (!!tag && tag.reasonTagged === IS_DEPLOY_CODE_LENGTH) { + if (!!tag && tag.reasonTagged === OpcodeTagReason.IS_DEPLOY_CODE_LENGTH) { transpiledConstructorInitLogic[index].consumedBytes = new BigNum( transpiledDeployedBytecodeByteLength ).toBuffer('be', op.opcode.programBytesConsumed) } - if (!!tag && tag.reasonTagged === IS_DEPLOY_CODECOPY_OFFSET) { + if ( + !!tag && + tag.reasonTagged === OpcodeTagReason.IS_DEPLOY_CODECOPY_OFFSET + ) { transpiledConstructorInitLogic[index].consumedBytes = new BigNum( transpiledInitLogicByteLength ).toBuffer('be', op.opcode.programBytesConsumed) @@ -272,7 +271,10 @@ export class TranspilerImpl implements Transpiler { opcode: taggedBytecode[index].opcode, consumedBytes: taggedBytecode[index].consumedBytes, } - if (!!op.tag && op.tag.reasonTagged === IS_CONSTANT_OFFSET) { + if ( + !!op.tag && + op.tag.reasonTagged === OpcodeTagReason.IS_CONSTANT_OFFSET + ) { const theConstant: Buffer = op.tag.metadata const newConstantOffset: number = inputAsBuf.indexOf(theConstant) if (newConstantOffset === -1) { @@ -339,7 +341,7 @@ export class TranspilerImpl implements Transpiler { consumedBytes: op.consumedBytes, tag: { padPUSH: true, - reasonTagged: IS_DEPLOY_CODE_LENGTH, + reasonTagged: OpcodeTagReason.IS_DEPLOY_CODE_LENGTH, metadata: undefined, }, } @@ -348,7 +350,7 @@ export class TranspilerImpl implements Transpiler { consumedBytes: bytecode[index + 2].consumedBytes, tag: { padPUSH: true, - reasonTagged: IS_DEPLOY_CODECOPY_OFFSET, + reasonTagged: OpcodeTagReason.IS_DEPLOY_CODECOPY_OFFSET, metadata: undefined, }, } @@ -378,7 +380,7 @@ export class TranspilerImpl implements Transpiler { consumedBytes: op.consumedBytes, tag: { padPUSH: true, - reasonTagged: IS_DEPLOY_CODE_LENGTH, + reasonTagged: OpcodeTagReason.IS_DEPLOY_CODE_LENGTH, metadata: undefined, }, } @@ -387,7 +389,7 @@ export class TranspilerImpl implements Transpiler { consumedBytes: bytecode[index + 1].consumedBytes, tag: { padPUSH: true, - reasonTagged: IS_DEPLOY_CODECOPY_OFFSET, + reasonTagged: OpcodeTagReason.IS_DEPLOY_CODECOPY_OFFSET, metadata: undefined, }, } @@ -457,7 +459,7 @@ export class TranspilerImpl implements Transpiler { consumedBytes: op.consumedBytes, tag: { padPUSH: true, - reasonTagged: IS_CONSTRUCTOR_INPUTS_OFFSET, + reasonTagged: OpcodeTagReason.IS_CONSTRUCTOR_INPUTS_OFFSET, metadata: undefined, }, } @@ -466,7 +468,7 @@ export class TranspilerImpl implements Transpiler { consumedBytes: bytecode[index + 4].consumedBytes, tag: { padPUSH: true, - reasonTagged: IS_CONSTRUCTOR_INPUTS_OFFSET, + reasonTagged: OpcodeTagReason.IS_CONSTRUCTOR_INPUTS_OFFSET, metadata: undefined, }, } @@ -538,7 +540,7 @@ export class TranspilerImpl implements Transpiler { consumedBytes: op.consumedBytes, tag: { padPUSH: true, - reasonTagged: IS_CONSTANT_OFFSET, + reasonTagged: OpcodeTagReason.IS_CONSTANT_OFFSET, metadata: theConstant, }, } diff --git a/packages/rollup-dev-tools/src/tools/vm/evm-introspection-util.ts b/packages/rollup-dev-tools/src/tools/vm/evm-introspection-util.ts index 81d29f1c5def..6e64344aab73 100644 --- a/packages/rollup-dev-tools/src/tools/vm/evm-introspection-util.ts +++ b/packages/rollup-dev-tools/src/tools/vm/evm-introspection-util.ts @@ -182,9 +182,9 @@ export class EvmIntrospectionUtilImpl implements EvmIntrospectionUtil { origin: hexStrToBuf(this.wallet.address), data, }) - this.vm.removeListener('step', stepCallback) return ret }) + this.vm.removeListener('step', stepCallback) if (result.execResult.exceptionError) { const params: string = bufToHexString(abiEncodedParams) diff --git a/packages/rollup-dev-tools/src/types/transpiler/types.ts b/packages/rollup-dev-tools/src/types/transpiler/types.ts index 9128c0660bab..fabfa35fd0ce 100644 --- a/packages/rollup-dev-tools/src/types/transpiler/types.ts +++ b/packages/rollup-dev-tools/src/types/transpiler/types.ts @@ -32,3 +32,12 @@ export interface TaggedTranspilationResult { errors?: TranspilationError[] bytecodeWithTags?: EVMBytecode } + +export interface BinarySearchTreeNode { + value: { + jumpdestBefore: number + jumpdestAfter: number + } + left: BinarySearchTreeNode + right: BinarySearchTreeNode +} diff --git a/packages/rollup-dev-tools/test/contracts/jump-transpilation/SimpleJumper.sol b/packages/rollup-dev-tools/test/contracts/jump-transpilation/SimpleJumper.sol index 239324fe34f6..93a3c7e3a7db 100644 --- a/packages/rollup-dev-tools/test/contracts/jump-transpilation/SimpleJumper.sol +++ b/packages/rollup-dev-tools/test/contracts/jump-transpilation/SimpleJumper.sol @@ -54,7 +54,7 @@ contract SimpleJumper { return 0; } else { uint256 val = 29; - for (uint i=0; i<25; i++) { + for (uint i=0; i<8; i++) { times[block.timestamp] = doLoopingSubcalls(); val = val + 7*i; } diff --git a/packages/rollup-dev-tools/test/transpiler/jump-integration.spec.ts b/packages/rollup-dev-tools/test/transpiler/jump-integration.spec.ts index 687aa46453f5..00d8b37f05f8 100644 --- a/packages/rollup-dev-tools/test/transpiler/jump-integration.spec.ts +++ b/packages/rollup-dev-tools/test/transpiler/jump-integration.spec.ts @@ -14,6 +14,7 @@ import { formatBytecode, Opcode, EVMOpcodeAndBytes, + bufferToBytecode, } from '@eth-optimism/rollup-core' import * as ethereumjsAbi from 'ethereumjs-abi' import * as SimpleJumper from '../contracts/build/SimpleJumper.json' @@ -59,7 +60,7 @@ import { const log = getLogger(`test-solidity-JUMPs`) const abi = new ethers.utils.AbiCoder() -describe('JUMP table solidity integration', () => { +describe.only('JUMP table solidity integration', () => { let evmUtil: EvmIntrospectionUtil const mockReplacer: OpcodeReplacer = { replaceIfNecessary(opcodeAndBytes: EVMOpcodeAndBytes): EVMBytecode { @@ -109,13 +110,18 @@ describe('JUMP table solidity integration', () => { const transpiledJumperDeployedBytecode: Buffer = (transpiler.transpileRawBytecode( originalJumperDeployedBytecde ) as SuccessfulTranspilation).bytecode + log.debug( + `Transpiled jumper output with BST jump table: \n${formatBytecode( + bufferToBytecode(transpiledJumperDeployedBytecode) + )}` + ) await evmUtil.deployBytecodeToAddress( transpiledJumperDeployedBytecode, hexStrToBuf(transpiledJumperAddr) ) }) it('should handle an if(true)', async () => { - assertCallsProduceSameResult( + await assertCallsProduceSameResult( evmUtil, originalJumperAddr, transpiledJumperAddr, @@ -123,7 +129,7 @@ describe('JUMP table solidity integration', () => { ) }) it('should handle an if(false)', async () => { - assertCallsProduceSameResult( + await assertCallsProduceSameResult( evmUtil, originalJumperAddr, transpiledJumperAddr, @@ -131,7 +137,7 @@ describe('JUMP table solidity integration', () => { ) }) it('should handle for loops', async () => { - assertCallsProduceSameResult( + await assertCallsProduceSameResult( evmUtil, originalJumperAddr, transpiledJumperAddr, @@ -139,7 +145,7 @@ describe('JUMP table solidity integration', () => { ) }) it('should handle while loops', async () => { - assertCallsProduceSameResult( + await assertCallsProduceSameResult( evmUtil, originalJumperAddr, transpiledJumperAddr, @@ -147,7 +153,7 @@ describe('JUMP table solidity integration', () => { ) }) it('should handle a while loop whose inner function calls another method with a for loop', async () => { - assertCallsProduceSameResult( + await assertCallsProduceSameResult( evmUtil, originalJumperAddr, transpiledJumperAddr, @@ -161,7 +167,7 @@ describe('JUMP table solidity integration', () => { remove0x(abi.encode(paramTypes, [nonzeroInput])), 'hex' ) - assertCallsProduceSameResult( + await assertCallsProduceSameResult( evmUtil, originalJumperAddr, transpiledJumperAddr, @@ -169,7 +175,7 @@ describe('JUMP table solidity integration', () => { paramTypes, callParams ) - }) + }).timeout(25000) }) const assertCallsProduceSameResult = async ( @@ -180,6 +186,8 @@ const assertCallsProduceSameResult = async ( paramTypes?: string[], abiEncodedParams?: Buffer ) => { + log.debug(`Asked to compare the result of two calls.`) + log.debug(`Calling first contract at address ${addr1}...`) const res1 = await util.callContract( addr1, methodName, @@ -187,10 +195,14 @@ const assertCallsProduceSameResult = async ( abiEncodedParams ) if (res1.error) { + log.debug(`oh, erroring`) throw new Error( `TEST ERROR: failed to execute callContract() for contract address: ${addr1}. Error was: \n${res1.error}` ) } + log.debug( + `Completed first call successfully. Calling second contract at address ${addr2}...` + ) const res2 = await util.callContract( addr2, methodName, @@ -202,5 +214,7 @@ const assertCallsProduceSameResult = async ( `TEST ERROR: failed to execute callContract() for contract address: ${addr2}. Error was: \n${res2.error}` ) } + log.debug(`Completed second call successfully. Comparing results...`) res2.result.should.deep.equal(res1.result) + return } diff --git a/packages/rollup-dev-tools/test/transpiler/transpile.jump.spec.ts b/packages/rollup-dev-tools/test/transpiler/transpile.jump.spec.ts index 878cfc53a50e..d957f78bcf2d 100644 --- a/packages/rollup-dev-tools/test/transpiler/transpile.jump.spec.ts +++ b/packages/rollup-dev-tools/test/transpiler/transpile.jump.spec.ts @@ -100,30 +100,6 @@ const validateJumpBytecode = (successResult: SuccessfulTranspilation): void => { 0, 'opcodesBeforeJump should have entries but does not!' ) - - for (const [index, opcodeBeforeJump] of opcodesBeforeJump.entries()) { - if (index < switchSuccessJumpdestIndex) { - // All regular program JUMPs should go to the footer JUMPDEST - opcodeBeforeJump.opcode.programBytesConsumed.should.be.gt( - 0, - 'Opcode before JUMP should be a PUSH32, pushing the location of the footer JUMP switch!' - ) - opcodeBeforeJump.consumedBytes.should.eql( - bufferUtils.numberToBufferPacked(switchJumpdestIndex, 2), - 'JUMP should be equal to index of footer switch JUMPDEST!' - ) - } else if (index > switchJumpdestIndex) { - // Make sure that all footer JUMPS go to footer JUMP success jumpdest - const dest: number = parseInt( - opcodeBeforeJump.consumedBytes.toString('hex'), - 16 - ) - dest.should.eq( - switchSuccessJumpdestIndex, - 'All footer JUMPs should go to success JUMPDEST block' - ) - } - } } const getSuccessfulTranspilationResult = (