From 140708550770828b15b0501ebbead0a20543db73 Mon Sep 17 00:00:00 2001 From: ben-chain Date: Sun, 5 Apr 2020 15:51:05 -0400 Subject: [PATCH 01/11] footer bytecode gen looks right --- packages/rollup-core/src/types/opcodes.ts | 2 +- .../src/tools/transpiler/index.ts | 1 + .../tools/transpiler/log-search-generation.ts | 279 ++++++++++++++++++ .../test/transpiler/jump-integration.spec.ts | 2 +- .../transpiler/log-search-generation.spec.ts | 52 ++++ 5 files changed, 334 insertions(+), 2 deletions(-) create mode 100644 packages/rollup-dev-tools/src/tools/transpiler/log-search-generation.ts create mode 100644 packages/rollup-dev-tools/test/transpiler/log-search-generation.spec.ts diff --git a/packages/rollup-core/src/types/opcodes.ts b/packages/rollup-core/src/types/opcodes.ts index a9d51a1f02fc..fd9df655ef31 100644 --- a/packages/rollup-core/src/types/opcodes.ts +++ b/packages/rollup-core/src/types/opcodes.ts @@ -17,7 +17,7 @@ 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 + metadata: any } export class Opcode { diff --git a/packages/rollup-dev-tools/src/tools/transpiler/index.ts b/packages/rollup-dev-tools/src/tools/transpiler/index.ts index 2b861e980ea7..a5aeb0b62f45 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 './log-search-generation' diff --git a/packages/rollup-dev-tools/src/tools/transpiler/log-search-generation.ts b/packages/rollup-dev-tools/src/tools/transpiler/log-search-generation.ts new file mode 100644 index 000000000000..02835539f1ea --- /dev/null +++ b/packages/rollup-dev-tools/src/tools/transpiler/log-search-generation.ts @@ -0,0 +1,279 @@ +import { + bytecodeToBuffer, + EVMBytecode, + Opcode, + EVMOpcodeAndBytes, + formatBytecode, + getPCOfEVMBytecodeIndex, +} from '@eth-optimism/rollup-core' +import { bufferUtils, getLogger } from '@eth-optimism/core-utils' +import { getPUSHOpcode, getPUSHIntegerOp } from './helpers' +import { + JumpReplacementResult, + TranspilationError, + TranspilationErrors, +} from '../../types/transpiler' +import { createError } from './util' +import { TranscodeEncoding } from 'buffer' +import { UnicodeNormalizationForm } from 'ethers/utils' + +const log = getLogger('log-search-generator') + +type LogSearchLeafNode = { + key: number + value: number +} + +type LogSearchInternalNode = { + largestAncestorKey: number + keyToCompare: number +} + +type LogSearchNode = LogSearchLeafNode | LogSearchInternalNode + +type LogSearchTree = LogSearchNode[][] + +const maxBytesOfContractSize = 2 + +const IS_PUSH_BINARY_SEARCH_NODE_LOCATION = + 'IS_PUSH_BINARY_SEARCH_NODE_LOCATION' +const IS_BINARY_SEARCH_NODE_JUMPDEST = 'IS_BINARY_SEARCH_NODE_JUMPDEST' + +export const generateLogSearchTree = ( + keys: number[], + values: number[] +): LogSearchTree => { + let tree: LogSearchTree = [[]] + // initialize tree's bottom level with key/value leaves + tree[0] = keys.map((v, i) => { + return { + key: keys[i], + value: values[i], + } + }) + + let treeHeight = Math.ceil(Math.log2(keys.length)) + for (let depth = 1; depth <= treeHeight; depth++) { + tree[depth] = [] + for (let i = 0; i < tree[depth - 1].length; i += 2) { + let nodeToCreate: LogSearchInternalNode = { + largestAncestorKey: undefined, + keyToCompare: undefined, + } + const indexToCreate = i / 2 + const leftChild = tree[depth - 1][i] + const rightChild = tree[depth - 1][i + 1] + if (!rightChild) { + tree[depth][indexToCreate] = tree[depth - 1].pop() + continue + } + // if leaf node right child, its key is greatest ancestor + if (isLeafNode(rightChild)) { + nodeToCreate.largestAncestorKey = (rightChild as LogSearchLeafNode).key + } else { + nodeToCreate.largestAncestorKey = (rightChild as LogSearchInternalNode).largestAncestorKey + } + + if (isLeafNode(leftChild)) { + nodeToCreate.keyToCompare = (leftChild as LogSearchLeafNode).key + } else { + nodeToCreate.keyToCompare = (leftChild as LogSearchInternalNode).largestAncestorKey + } + + tree[depth][indexToCreate] = nodeToCreate + } + } + return tree.reverse() // reverse so that tree[0][0] is the root node +} + +const isLeafNode = (node: LogSearchNode): boolean => { + return !!(node as LogSearchLeafNode).value +} + +export const getJumpIndexSearchBytecode = ( + jumpdestIndexesBefore: number[], + jumpdestIndexesAfter: number[], + indexOfThisBlock: number +): EVMBytecode => { + const searchTree = generateLogSearchTree( + jumpdestIndexesBefore, + jumpdestIndexesAfter + ) + const bytecode: EVMBytecode = [ + { + opcode: Opcode.JUMPDEST, + consumedBytes: undefined, + }, + ...appendNodeToBytecode(searchTree, 0, 0, []), // should recursively fill out tree + ] + return fixTaggedNodePositions(bytecode, indexOfThisBlock) +} + +const fixTaggedNodePositions = ( + bytecode: EVMBytecode, + indexOfThisBlock: number +): EVMBytecode => { + for (let opcodeAndBytes of bytecode) { + if ( + !!opcodeAndBytes.tag && + opcodeAndBytes.tag.reasonTagged == IS_PUSH_BINARY_SEARCH_NODE_LOCATION + ) { + const thisNodePosition = opcodeAndBytes.tag.metadata + const rightChildNodeLevel = thisNodePosition.level + 1 + const rightChildNodeIndex = thisNodePosition.index * 2 + 1 + const rightChildJumpdestIndexInBytecodeBlock = bytecode.findIndex( + (opcodeAndBytes: EVMOpcodeAndBytes) => { + return ( + !!opcodeAndBytes.tag && + opcodeAndBytes.tag.reasonTagged == IS_BINARY_SEARCH_NODE_JUMPDEST && + opcodeAndBytes.tag.metadata.level == rightChildNodeLevel && + opcodeAndBytes.tag.metadata.index == rightChildNodeIndex + ) + } + ) + const rightChildJumpdestInFinalBuffer = + indexOfThisBlock + + getPCOfEVMBytecodeIndex( + rightChildJumpdestIndexInBytecodeBlock, + bytecode + ) + opcodeAndBytes.consumedBytes = bufferUtils.numberToBuffer( + rightChildJumpdestInFinalBuffer, + maxBytesOfContractSize, + maxBytesOfContractSize + ) + } + } + return bytecode +} + +const appendNodeToBytecode = ( + tree: LogSearchTree, + level: number, + index: number, + bytecode: EVMBytecode +): EVMBytecode => { + const thisNode: LogSearchNode = tree[level][index] + log.info( + `Processing node at level ${level} and index ${index} of tree into bytecode, its parameters are ${thisNode}.` + ) + if (isLeafNode(thisNode)) { + bytecode = [ + ...bytecode, + ...generateLeafBytecode(thisNode as LogSearchLeafNode, level, index), + ] + } else { + bytecode = [ + ...bytecode, + ...generateComparisonBytecode( + thisNode as LogSearchInternalNode, + level, + index + ), + ] + const leftChildIndex = index * 2 + const rightChildIndex = leftChildIndex + 1 + const childrenLevel = level + 1 + bytecode = appendNodeToBytecode( + tree, + childrenLevel, + leftChildIndex, + bytecode + ) + bytecode = appendNodeToBytecode( + tree, + childrenLevel, + rightChildIndex, + bytecode + ) + } + return bytecode +} + +const generateComparisonBytecode = ( + node: LogSearchInternalNode, + level: number, + index: number +): EVMBytecode => { + // if index is odd, add and tag JUMPDEST with "treeNodePosition" = [index, level] + // For GT check working, add "destinationNodePosition" = index*2+1, level+1 + let bytecodeToReturn: EVMBytecode = [] + const willBeJUMPedTo: boolean = index % 2 == 0 ? false : true + if (willBeJUMPedTo) { + bytecodeToReturn.push(generateSearchNodeJumpdest(level, index)) + } + + bytecodeToReturn = [ + ...bytecodeToReturn, + { + opcode: Opcode.DUP1, + consumedBytes: undefined, + }, + getPUSHIntegerOp(node.keyToCompare), + { + opcode: Opcode.LT, + consumedBytes: undefined, + }, + { + opcode: getPUSHOpcode(maxBytesOfContractSize), + consumedBytes: Buffer.alloc(maxBytesOfContractSize), + tag: { + padPUSH: false, + reasonTagged: IS_PUSH_BINARY_SEARCH_NODE_LOCATION, + metadata: { level, index }, + }, + }, + { + opcode: Opcode.SWAP1, + consumedBytes: undefined, + }, + { + opcode: Opcode.JUMPI, + consumedBytes: undefined, + }, + ] + + return bytecodeToReturn +} + +const generateLeafBytecode = ( + node: LogSearchLeafNode, + level: number, + index: number +): EVMBytecode => { + // do the matching stuff + let bytecodeToReturn: EVMBytecode = [] + const willBeJUMPedTo: boolean = index % 2 == 0 ? false : true + if (willBeJUMPedTo) { + bytecodeToReturn.push(generateSearchNodeJumpdest(level, index)) + } + + bytecodeToReturn = [ + ...bytecodeToReturn, + { + opcode: Opcode.POP, + consumedBytes: undefined, + }, + getPUSHIntegerOp(node.value), + { + opcode: Opcode.JUMP, + consumedBytes: undefined, + }, + ] + return bytecodeToReturn +} + +const generateSearchNodeJumpdest = ( + level: number, + index: number +): EVMOpcodeAndBytes => { + return { + opcode: Opcode.JUMPDEST, + consumedBytes: undefined, + tag: { + padPUSH: false, + reasonTagged: IS_BINARY_SEARCH_NODE_JUMPDEST, + metadata: { level, index }, + }, + } +} 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..eb21135c8227 100644 --- a/packages/rollup-dev-tools/test/transpiler/jump-integration.spec.ts +++ b/packages/rollup-dev-tools/test/transpiler/jump-integration.spec.ts @@ -59,7 +59,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 { diff --git a/packages/rollup-dev-tools/test/transpiler/log-search-generation.spec.ts b/packages/rollup-dev-tools/test/transpiler/log-search-generation.spec.ts new file mode 100644 index 000000000000..d9d6e0f65568 --- /dev/null +++ b/packages/rollup-dev-tools/test/transpiler/log-search-generation.spec.ts @@ -0,0 +1,52 @@ +import { should } from '../setup' + +/* External Imports */ +import { bufferUtils, bufToHexString } from '@eth-optimism/core-utils' +import { + Opcode, + EVMOpcode, + EVMBytecode, + bytecodeToBuffer, + bufferToBytecode, + EVMOpcodeAndBytes, + formatBytecode, +} from '@eth-optimism/rollup-core' + +/* Internal imports */ +import { + ErroredTranspilation, + OpcodeReplacer, + OpcodeWhitelist, + SuccessfulTranspilation, + TranspilationResult, + Transpiler, +} from '../../src/types/transpiler' +import { + TranspilerImpl, + OpcodeReplacerImpl, + OpcodeWhitelistImpl, + generateLogSearchTree, + getJumpIndexSearchBytecode, +} from '../../src/tools/transpiler' +import { + assertExecutionEqual, + stateManagerAddress, + whitelistedOpcodes, +} from '../helpers' +import { EvmIntrospectionUtil } from '../../src/types/vm' +import { EvmIntrospectionUtilImpl } from '../../src/tools/vm' + +describe.only('Binary search bytecode generator', () => { + it('should generate the conceptual tree correctly', () => { + const keys = [1, 2, 3, 4, 5, 6, 7] + const values = [8, 9, 10, 11, 12, 13, 14] + const tree = generateLogSearchTree(keys, values) + console.log(tree) + }) + it('should gen some reasonable looking bytecode', () => { + const keys = [1, 2, 3, 4, 5] + const vals = [4, 5, 6, 7, 8] + const bytecode: EVMBytecode = getJumpIndexSearchBytecode(keys, vals, 0) + console.log(formatBytecode(bytecode)) + }) +}) From 65e32f8766d181dd9d4e1c84eb2dc1b8de7e8bfa Mon Sep 17 00:00:00 2001 From: ben-chain Date: Sun, 5 Apr 2020 21:03:13 -0400 Subject: [PATCH 02/11] tests all passing --- .../src/tools/transpiler/jump-replacement.ts | 130 +-------------- .../tools/transpiler/log-search-generation.ts | 151 ++++++++++-------- .../src/tools/vm/evm-introspection-util.ts | 2 +- .../jump-transpilation/SimpleJumper.sol | 2 +- .../test/transpiler/jump-integration.spec.ts | 24 ++- .../transpiler/log-search-generation.spec.ts | 52 ------ .../test/transpiler/transpile.jump.spec.ts | 97 ----------- 7 files changed, 101 insertions(+), 357 deletions(-) delete mode 100644 packages/rollup-dev-tools/test/transpiler/log-search-generation.spec.ts 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..6f7be0b0f8f8 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 { getJumpIndexSearchBytecode } 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( + ...getJumpIndexSearchBytecode( 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,5 @@ export const getExpectedFooterSwitchStatementJumpdestIndex = ( length += 1 + opcodeAndBytes.opcode.programBytesConsumed } } - length += bytecodeToBuffer( - getJumpIndexSwitchStatementSuccessJumpdestBytecode() - ).length return length } diff --git a/packages/rollup-dev-tools/src/tools/transpiler/log-search-generation.ts b/packages/rollup-dev-tools/src/tools/transpiler/log-search-generation.ts index 02835539f1ea..dfb380598ad7 100644 --- a/packages/rollup-dev-tools/src/tools/transpiler/log-search-generation.ts +++ b/packages/rollup-dev-tools/src/tools/transpiler/log-search-generation.ts @@ -19,14 +19,19 @@ import { UnicodeNormalizationForm } from 'ethers/utils' const log = getLogger('log-search-generator') + type LogSearchLeafNode = { key: number value: number + nodeId: number } type LogSearchInternalNode = { largestAncestorKey: number keyToCompare: number + nodeId: number, + leftChildNodeId: number, + rightChildNodeId: number } type LogSearchNode = LogSearchLeafNode | LogSearchInternalNode @@ -39,51 +44,55 @@ const IS_PUSH_BINARY_SEARCH_NODE_LOCATION = 'IS_PUSH_BINARY_SEARCH_NODE_LOCATION' const IS_BINARY_SEARCH_NODE_JUMPDEST = 'IS_BINARY_SEARCH_NODE_JUMPDEST' -export const generateLogSearchTree = ( +export const generateLogSearchTreeNodes = ( keys: number[], values: number[] -): LogSearchTree => { - let tree: LogSearchTree = [[]] - // initialize tree's bottom level with key/value leaves - tree[0] = keys.map((v, i) => { +): LogSearchNode[] => { + let allTreeNodes: LogSearchNode[] = keys.map((v, i) => { return { key: keys[i], value: values[i], + nodeId: i } }) - let treeHeight = Math.ceil(Math.log2(keys.length)) - for (let depth = 1; depth <= treeHeight; depth++) { - tree[depth] = [] - for (let i = 0; i < tree[depth - 1].length; i += 2) { - let nodeToCreate: LogSearchInternalNode = { - largestAncestorKey: undefined, - keyToCompare: undefined, - } - const indexToCreate = i / 2 - const leftChild = tree[depth - 1][i] - const rightChild = tree[depth - 1][i + 1] + let curLevel: LogSearchNode[] = [...allTreeNodes] + while (curLevel.length > 1) { + // console.log(`processing level: ${JSON.stringify(curLevel)}`) + let nextLevel: LogSearchNode[] = [] + for (let i = 0; i < curLevel.length; i += 2) { + const leftChild = curLevel[i] + const rightChild = curLevel[i+1] if (!rightChild) { - tree[depth][indexToCreate] = tree[depth - 1].pop() + nextLevel.push(leftChild) continue } - // if leaf node right child, its key is greatest ancestor + + let newNode: LogSearchInternalNode = { + nodeId: allTreeNodes.length, + leftChildNodeId: leftChild.nodeId, + rightChildNodeId: rightChild.nodeId, + largestAncestorKey: undefined, + keyToCompare: undefined + } + if (isLeafNode(rightChild)) { - nodeToCreate.largestAncestorKey = (rightChild as LogSearchLeafNode).key + newNode.largestAncestorKey = (rightChild as LogSearchLeafNode).key } else { - nodeToCreate.largestAncestorKey = (rightChild as LogSearchInternalNode).largestAncestorKey + newNode.largestAncestorKey = (rightChild as LogSearchInternalNode).largestAncestorKey } if (isLeafNode(leftChild)) { - nodeToCreate.keyToCompare = (leftChild as LogSearchLeafNode).key + newNode.keyToCompare = (leftChild as LogSearchLeafNode).key } else { - nodeToCreate.keyToCompare = (leftChild as LogSearchInternalNode).largestAncestorKey + newNode.keyToCompare = (leftChild as LogSearchInternalNode).largestAncestorKey } - - tree[depth][indexToCreate] = nodeToCreate + allTreeNodes.push(newNode) + nextLevel.push(newNode) } + curLevel = nextLevel } - return tree.reverse() // reverse so that tree[0][0] is the root node + return allTreeNodes } const isLeafNode = (node: LogSearchNode): boolean => { @@ -95,39 +104,42 @@ export const getJumpIndexSearchBytecode = ( jumpdestIndexesAfter: number[], indexOfThisBlock: number ): EVMBytecode => { - const searchTree = generateLogSearchTree( + const searchTreeNodes: LogSearchNode[] = generateLogSearchTreeNodes( jumpdestIndexesBefore, jumpdestIndexesAfter ) + log.debug(`successfully generated conceptual log search tree, its flat structure is: \n${JSON.stringify(searchTreeNodes)}`) + const rootNodeId = searchTreeNodes.length - 1 // root node is the final one const bytecode: EVMBytecode = [ { opcode: Opcode.JUMPDEST, consumedBytes: undefined, }, - ...appendNodeToBytecode(searchTree, 0, 0, []), // should recursively fill out tree + ...appendNodeToBytecode(searchTreeNodes, rootNodeId, true, []), // should recursively fill out tree ] - return fixTaggedNodePositions(bytecode, indexOfThisBlock) + const finalBytecode = fixTaggedNodePositions(bytecode, indexOfThisBlock, searchTreeNodes) + // log.debug(`generated final bytecode for log searcher : \n${formatBytecode(finalBytecode)}`) + return finalBytecode } const fixTaggedNodePositions = ( bytecode: EVMBytecode, - indexOfThisBlock: number + indexOfThisBlock: number, + searchTreeNodes: LogSearchNode[] ): EVMBytecode => { for (let opcodeAndBytes of bytecode) { if ( !!opcodeAndBytes.tag && opcodeAndBytes.tag.reasonTagged == IS_PUSH_BINARY_SEARCH_NODE_LOCATION ) { - const thisNodePosition = opcodeAndBytes.tag.metadata - const rightChildNodeLevel = thisNodePosition.level + 1 - const rightChildNodeIndex = thisNodePosition.index * 2 + 1 + const thisNodeId = opcodeAndBytes.tag.metadata.nodeId + const rightChildNodeId = (searchTreeNodes[thisNodeId] as LogSearchInternalNode).rightChildNodeId const rightChildJumpdestIndexInBytecodeBlock = bytecode.findIndex( (opcodeAndBytes: EVMOpcodeAndBytes) => { return ( !!opcodeAndBytes.tag && opcodeAndBytes.tag.reasonTagged == IS_BINARY_SEARCH_NODE_JUMPDEST && - opcodeAndBytes.tag.metadata.level == rightChildNodeLevel && - opcodeAndBytes.tag.metadata.index == rightChildNodeIndex + opcodeAndBytes.tag.metadata.nodeId == rightChildNodeId ) } ) @@ -148,42 +160,46 @@ const fixTaggedNodePositions = ( } const appendNodeToBytecode = ( - tree: LogSearchTree, - level: number, - index: number, + treeNodes: LogSearchNode[], + nodeId: number, + isLeftSibling: boolean, bytecode: EVMBytecode ): EVMBytecode => { - const thisNode: LogSearchNode = tree[level][index] + const thisNode: LogSearchNode = treeNodes[nodeId] log.info( - `Processing node at level ${level} and index ${index} of tree into bytecode, its parameters are ${thisNode}.` + `Processing node with Id ${nodeId} of tree into bytecode, its parameters are ${JSON.stringify(thisNode)}.` ) if (isLeafNode(thisNode)) { bytecode = [ ...bytecode, - ...generateLeafBytecode(thisNode as LogSearchLeafNode, level, index), + ...generateLeafBytecode( + thisNode as LogSearchLeafNode, + nodeId, + isLeftSibling + ), ] } else { + const thisInternalNode = thisNode as LogSearchInternalNode bytecode = [ ...bytecode, ...generateComparisonBytecode( - thisNode as LogSearchInternalNode, - level, - index + thisInternalNode, + nodeId, + isLeftSibling ), ] - const leftChildIndex = index * 2 - const rightChildIndex = leftChildIndex + 1 - const childrenLevel = level + 1 + const leftChildId = thisInternalNode.leftChildNodeId + const rightChildId = thisInternalNode.rightChildNodeId bytecode = appendNodeToBytecode( - tree, - childrenLevel, - leftChildIndex, - bytecode + treeNodes, + leftChildId, + true, + bytecode, ) bytecode = appendNodeToBytecode( - tree, - childrenLevel, - rightChildIndex, + treeNodes, + rightChildId, + false, bytecode ) } @@ -192,15 +208,15 @@ const appendNodeToBytecode = ( const generateComparisonBytecode = ( node: LogSearchInternalNode, - level: number, - index: number + nodeId: number, + isLeftSibling: boolean ): EVMBytecode => { // if index is odd, add and tag JUMPDEST with "treeNodePosition" = [index, level] // For GT check working, add "destinationNodePosition" = index*2+1, level+1 let bytecodeToReturn: EVMBytecode = [] - const willBeJUMPedTo: boolean = index % 2 == 0 ? false : true + const willBeJUMPedTo: boolean = !isLeftSibling if (willBeJUMPedTo) { - bytecodeToReturn.push(generateSearchNodeJumpdest(level, index)) + bytecodeToReturn.push(generateSearchNodeJumpdest(nodeId)) } bytecodeToReturn = [ @@ -220,13 +236,9 @@ const generateComparisonBytecode = ( tag: { padPUSH: false, reasonTagged: IS_PUSH_BINARY_SEARCH_NODE_LOCATION, - metadata: { level, index }, + metadata: { nodeId }, }, }, - { - opcode: Opcode.SWAP1, - consumedBytes: undefined, - }, { opcode: Opcode.JUMPI, consumedBytes: undefined, @@ -238,14 +250,14 @@ const generateComparisonBytecode = ( const generateLeafBytecode = ( node: LogSearchLeafNode, - level: number, - index: number + nodeId: number, + isLeftSibling: boolean ): EVMBytecode => { // do the matching stuff let bytecodeToReturn: EVMBytecode = [] - const willBeJUMPedTo: boolean = index % 2 == 0 ? false : true + const willBeJUMPedTo: boolean = !isLeftSibling if (willBeJUMPedTo) { - bytecodeToReturn.push(generateSearchNodeJumpdest(level, index)) + bytecodeToReturn.push(generateSearchNodeJumpdest(nodeId)) } bytecodeToReturn = [ @@ -264,8 +276,7 @@ const generateLeafBytecode = ( } const generateSearchNodeJumpdest = ( - level: number, - index: number + nodeId: number ): EVMOpcodeAndBytes => { return { opcode: Opcode.JUMPDEST, @@ -273,7 +284,7 @@ const generateSearchNodeJumpdest = ( tag: { padPUSH: false, reasonTagged: IS_BINARY_SEARCH_NODE_JUMPDEST, - metadata: { level, index }, - }, + metadata: { nodeId } + } } } 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/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 eb21135c8227..5079beb3f6c1 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.only('JUMP table solidity integration', () => { +describe('JUMP table solidity integration', () => { let evmUtil: EvmIntrospectionUtil const mockReplacer: OpcodeReplacer = { replaceIfNecessary(opcodeAndBytes: EVMOpcodeAndBytes): EVMBytecode { @@ -109,13 +110,14 @@ describe.only('JUMP table solidity integration', () => { const transpiledJumperDeployedBytecode: Buffer = (transpiler.transpileRawBytecode( originalJumperDeployedBytecde ) as SuccessfulTranspilation).bytecode + log.debug(`transpiled output with log searcher: \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 +125,7 @@ describe.only('JUMP table solidity integration', () => { ) }) it('should handle an if(false)', async () => { - assertCallsProduceSameResult( + await assertCallsProduceSameResult( evmUtil, originalJumperAddr, transpiledJumperAddr, @@ -131,7 +133,7 @@ describe.only('JUMP table solidity integration', () => { ) }) it('should handle for loops', async () => { - assertCallsProduceSameResult( + await assertCallsProduceSameResult( evmUtil, originalJumperAddr, transpiledJumperAddr, @@ -139,7 +141,7 @@ describe.only('JUMP table solidity integration', () => { ) }) it('should handle while loops', async () => { - assertCallsProduceSameResult( + await assertCallsProduceSameResult( evmUtil, originalJumperAddr, transpiledJumperAddr, @@ -147,7 +149,7 @@ describe.only('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 +163,7 @@ describe.only('JUMP table solidity integration', () => { remove0x(abi.encode(paramTypes, [nonzeroInput])), 'hex' ) - assertCallsProduceSameResult( + await assertCallsProduceSameResult( evmUtil, originalJumperAddr, transpiledJumperAddr, @@ -169,7 +171,7 @@ describe.only('JUMP table solidity integration', () => { paramTypes, callParams ) - }) + }).timeout(10000) }) const assertCallsProduceSameResult = async ( @@ -180,6 +182,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 +191,12 @@ 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 +208,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/log-search-generation.spec.ts b/packages/rollup-dev-tools/test/transpiler/log-search-generation.spec.ts deleted file mode 100644 index d9d6e0f65568..000000000000 --- a/packages/rollup-dev-tools/test/transpiler/log-search-generation.spec.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { should } from '../setup' - -/* External Imports */ -import { bufferUtils, bufToHexString } from '@eth-optimism/core-utils' -import { - Opcode, - EVMOpcode, - EVMBytecode, - bytecodeToBuffer, - bufferToBytecode, - EVMOpcodeAndBytes, - formatBytecode, -} from '@eth-optimism/rollup-core' - -/* Internal imports */ -import { - ErroredTranspilation, - OpcodeReplacer, - OpcodeWhitelist, - SuccessfulTranspilation, - TranspilationResult, - Transpiler, -} from '../../src/types/transpiler' -import { - TranspilerImpl, - OpcodeReplacerImpl, - OpcodeWhitelistImpl, - generateLogSearchTree, - getJumpIndexSearchBytecode, -} from '../../src/tools/transpiler' -import { - assertExecutionEqual, - stateManagerAddress, - whitelistedOpcodes, -} from '../helpers' -import { EvmIntrospectionUtil } from '../../src/types/vm' -import { EvmIntrospectionUtilImpl } from '../../src/tools/vm' - -describe.only('Binary search bytecode generator', () => { - it('should generate the conceptual tree correctly', () => { - const keys = [1, 2, 3, 4, 5, 6, 7] - const values = [8, 9, 10, 11, 12, 13, 14] - const tree = generateLogSearchTree(keys, values) - console.log(tree) - }) - it('should gen some reasonable looking bytecode', () => { - const keys = [1, 2, 3, 4, 5] - const vals = [4, 5, 6, 7, 8] - const bytecode: EVMBytecode = getJumpIndexSearchBytecode(keys, vals, 0) - console.log(formatBytecode(bytecode)) - }) -}) 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..0fdfc1f08b9b 100644 --- a/packages/rollup-dev-tools/test/transpiler/transpile.jump.spec.ts +++ b/packages/rollup-dev-tools/test/transpiler/transpile.jump.spec.ts @@ -34,98 +34,6 @@ import { import { EvmIntrospectionUtil } from '../../src/types/vm' import { EvmIntrospectionUtilImpl } from '../../src/tools/vm' -/** - * Validates transpiled JUMP bytecode provided via the TranspilationResult parameter. - * - * @param successResult The transpilation result in question. - */ -const validateJumpBytecode = (successResult: SuccessfulTranspilation): void => { - const outputBytecode: EVMBytecode = bufferToBytecode(successResult.bytecode) - - let pc = 0 - let lastOpcode: EVMOpcodeAndBytes - const jumpdestIndexes: number[] = [] - const opcodesBeforeJump: Map = new Map< - number, - EVMOpcodeAndBytes - >() - // Build map of index => opcode immediately before JUMP and get index of footer switch - for (const opcodeAndBytes of outputBytecode) { - if (opcodeAndBytes.opcode === Opcode.JUMPDEST) { - jumpdestIndexes.push(pc) - } - if ( - opcodeAndBytes.opcode === Opcode.JUMP || - opcodeAndBytes.opcode === Opcode.JUMPI - ) { - opcodesBeforeJump.set( - pc - 1 - lastOpcode.opcode.programBytesConsumed, - lastOpcode - ) - } - lastOpcode = opcodeAndBytes - pc += 1 + opcodeAndBytes.opcode.programBytesConsumed - } - - jumpdestIndexes.length.should.be.greaterThan( - 0, - 'There should be JUMPDESTs, but there are not!' - ) - - const switchJumpdestIndex: number = jumpdestIndexes.pop() - const switchJumpdest: Buffer = successResult.bytecode.slice( - switchJumpdestIndex, - switchJumpdestIndex + 1 - ) - switchJumpdest.should.eql( - Opcode.JUMPDEST.code, - `Switch JUMPDEST index is ${switchJumpdestIndex}, but byte at that index is ${bufToHexString( - switchJumpdest - )}, not ${bufToHexString(Opcode.JUMPDEST.code)}` - ) - - const switchSuccessJumpdestIndex: number = jumpdestIndexes.pop() - const switchSuccessJumpdest: Buffer = successResult.bytecode.slice( - switchSuccessJumpdestIndex, - switchSuccessJumpdestIndex + 1 - ) - switchSuccessJumpdest.should.eql( - Opcode.JUMPDEST.code, - `Switch success JUMPDEST index is ${switchJumpdestIndex}, but byte at that index is ${bufToHexString( - switchJumpdest - )}, not ${bufToHexString(Opcode.JUMPDEST.code)}` - ) - - opcodesBeforeJump.size.should.be.greaterThan( - 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 = ( transpiler: Transpiler, bytecode: Buffer @@ -171,7 +79,6 @@ describe('Transpile - JUMPs', () => { initialBytecode ) - validateJumpBytecode(successResult) await assertExecutionEqual(evmUtil, initialBytecode, successResult.bytecode) }) @@ -192,7 +99,6 @@ describe('Transpile - JUMPs', () => { transpiler, initialBytecode ) - validateJumpBytecode(successResult) await assertExecutionEqual(evmUtil, initialBytecode, successResult.bytecode) }) @@ -224,7 +130,6 @@ describe('Transpile - JUMPs', () => { transpiler, initialBytecode ) - validateJumpBytecode(successResult) await assertExecutionEqual(evmUtil, initialBytecode, successResult.bytecode) }) @@ -243,7 +148,6 @@ describe('Transpile - JUMPs', () => { transpiler, initialBytecode ) - validateJumpBytecode(successResult) await assertExecutionEqual(evmUtil, initialBytecode, successResult.bytecode) }) @@ -270,7 +174,6 @@ describe('Transpile - JUMPs', () => { initialBytecode, 'Transpilation should not have changed anything but did!' ) - await assertExecutionEqual(evmUtil, initialBytecode, successResult.bytecode) }) }) From c25da57d8f18c32b5ed393c891b27df61493ece4 Mon Sep 17 00:00:00 2001 From: ben-chain Date: Sun, 5 Apr 2020 21:26:49 -0400 Subject: [PATCH 03/11] lint --- .../tools/transpiler/log-search-generation.ts | 100 ++++++++---------- .../test/transpiler/jump-integration.spec.ts | 10 +- 2 files changed, 54 insertions(+), 56 deletions(-) diff --git a/packages/rollup-dev-tools/src/tools/transpiler/log-search-generation.ts b/packages/rollup-dev-tools/src/tools/transpiler/log-search-generation.ts index dfb380598ad7..26edd9b19175 100644 --- a/packages/rollup-dev-tools/src/tools/transpiler/log-search-generation.ts +++ b/packages/rollup-dev-tools/src/tools/transpiler/log-search-generation.ts @@ -19,18 +19,17 @@ import { UnicodeNormalizationForm } from 'ethers/utils' const log = getLogger('log-search-generator') - -type LogSearchLeafNode = { +interface LogSearchLeafNode { key: number value: number nodeId: number } -type LogSearchInternalNode = { +interface LogSearchInternalNode { largestAncestorKey: number keyToCompare: number - nodeId: number, - leftChildNodeId: number, + nodeId: number + leftChildNodeId: number rightChildNodeId: number } @@ -48,45 +47,42 @@ export const generateLogSearchTreeNodes = ( keys: number[], values: number[] ): LogSearchNode[] => { - let allTreeNodes: LogSearchNode[] = keys.map((v, i) => { + const allTreeNodes: LogSearchNode[] = keys.map((v, i) => { return { key: keys[i], value: values[i], - nodeId: i + nodeId: i, } }) let curLevel: LogSearchNode[] = [...allTreeNodes] while (curLevel.length > 1) { // console.log(`processing level: ${JSON.stringify(curLevel)}`) - let nextLevel: LogSearchNode[] = [] + const nextLevel: LogSearchNode[] = [] for (let i = 0; i < curLevel.length; i += 2) { const leftChild = curLevel[i] - const rightChild = curLevel[i+1] + const rightChild = curLevel[i + 1] if (!rightChild) { - nextLevel.push(leftChild) + nextLevel.push(leftChild) continue } - let newNode: LogSearchInternalNode = { + const newNode: LogSearchInternalNode = { nodeId: allTreeNodes.length, leftChildNodeId: leftChild.nodeId, rightChildNodeId: rightChild.nodeId, largestAncestorKey: undefined, - keyToCompare: undefined + keyToCompare: undefined, } - if (isLeafNode(rightChild)) { - newNode.largestAncestorKey = (rightChild as LogSearchLeafNode).key - } else { - newNode.largestAncestorKey = (rightChild as LogSearchInternalNode).largestAncestorKey - } + newNode.largestAncestorKey = isLeafNode(rightChild) + ? (rightChild as LogSearchLeafNode).key + : (rightChild as LogSearchInternalNode).largestAncestorKey + + newNode.keyToCompare = isLeafNode(leftChild) + ? (leftChild as LogSearchLeafNode).key + : (newNode.keyToCompare = (leftChild as LogSearchInternalNode).largestAncestorKey) - if (isLeafNode(leftChild)) { - newNode.keyToCompare = (leftChild as LogSearchLeafNode).key - } else { - newNode.keyToCompare = (leftChild as LogSearchInternalNode).largestAncestorKey - } allTreeNodes.push(newNode) nextLevel.push(newNode) } @@ -108,7 +104,11 @@ export const getJumpIndexSearchBytecode = ( jumpdestIndexesBefore, jumpdestIndexesAfter ) - log.debug(`successfully generated conceptual log search tree, its flat structure is: \n${JSON.stringify(searchTreeNodes)}`) + log.debug( + `successfully generated conceptual log search tree, its flat structure is: \n${JSON.stringify( + searchTreeNodes + )}` + ) const rootNodeId = searchTreeNodes.length - 1 // root node is the final one const bytecode: EVMBytecode = [ { @@ -117,7 +117,11 @@ export const getJumpIndexSearchBytecode = ( }, ...appendNodeToBytecode(searchTreeNodes, rootNodeId, true, []), // should recursively fill out tree ] - const finalBytecode = fixTaggedNodePositions(bytecode, indexOfThisBlock, searchTreeNodes) + const finalBytecode = fixTaggedNodePositions( + bytecode, + indexOfThisBlock, + searchTreeNodes + ) // log.debug(`generated final bytecode for log searcher : \n${formatBytecode(finalBytecode)}`) return finalBytecode } @@ -127,19 +131,21 @@ const fixTaggedNodePositions = ( indexOfThisBlock: number, searchTreeNodes: LogSearchNode[] ): EVMBytecode => { - for (let opcodeAndBytes of bytecode) { + for (const opcodeAndBytes of bytecode) { if ( !!opcodeAndBytes.tag && - opcodeAndBytes.tag.reasonTagged == IS_PUSH_BINARY_SEARCH_NODE_LOCATION + opcodeAndBytes.tag.reasonTagged === IS_PUSH_BINARY_SEARCH_NODE_LOCATION ) { const thisNodeId = opcodeAndBytes.tag.metadata.nodeId - const rightChildNodeId = (searchTreeNodes[thisNodeId] as LogSearchInternalNode).rightChildNodeId + const rightChildNodeId = (searchTreeNodes[ + thisNodeId + ] as LogSearchInternalNode).rightChildNodeId const rightChildJumpdestIndexInBytecodeBlock = bytecode.findIndex( - (opcodeAndBytes: EVMOpcodeAndBytes) => { + (toCheck: EVMOpcodeAndBytes) => { return ( - !!opcodeAndBytes.tag && - opcodeAndBytes.tag.reasonTagged == IS_BINARY_SEARCH_NODE_JUMPDEST && - opcodeAndBytes.tag.metadata.nodeId == rightChildNodeId + !!toCheck.tag && + toCheck.tag.reasonTagged === IS_BINARY_SEARCH_NODE_JUMPDEST && + toCheck.tag.metadata.nodeId === rightChildNodeId ) } ) @@ -167,7 +173,9 @@ const appendNodeToBytecode = ( ): EVMBytecode => { const thisNode: LogSearchNode = treeNodes[nodeId] log.info( - `Processing node with Id ${nodeId} of tree into bytecode, its parameters are ${JSON.stringify(thisNode)}.` + `Processing node with Id ${nodeId} of tree into bytecode, its parameters are ${JSON.stringify( + thisNode + )}.` ) if (isLeafNode(thisNode)) { bytecode = [ @@ -182,26 +190,12 @@ const appendNodeToBytecode = ( const thisInternalNode = thisNode as LogSearchInternalNode bytecode = [ ...bytecode, - ...generateComparisonBytecode( - thisInternalNode, - nodeId, - isLeftSibling - ), + ...generateComparisonBytecode(thisInternalNode, nodeId, isLeftSibling), ] const leftChildId = thisInternalNode.leftChildNodeId const rightChildId = thisInternalNode.rightChildNodeId - bytecode = appendNodeToBytecode( - treeNodes, - leftChildId, - true, - bytecode, - ) - bytecode = appendNodeToBytecode( - treeNodes, - rightChildId, - false, - bytecode - ) + bytecode = appendNodeToBytecode(treeNodes, leftChildId, true, bytecode) + bytecode = appendNodeToBytecode(treeNodes, rightChildId, false, bytecode) } return bytecode } @@ -275,16 +269,14 @@ const generateLeafBytecode = ( return bytecodeToReturn } -const generateSearchNodeJumpdest = ( - nodeId: number -): EVMOpcodeAndBytes => { +const generateSearchNodeJumpdest = (nodeId: number): EVMOpcodeAndBytes => { return { opcode: Opcode.JUMPDEST, consumedBytes: undefined, tag: { padPUSH: false, reasonTagged: IS_BINARY_SEARCH_NODE_JUMPDEST, - metadata: { nodeId } - } + metadata: { nodeId }, + }, } } 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 5079beb3f6c1..ac4998190c51 100644 --- a/packages/rollup-dev-tools/test/transpiler/jump-integration.spec.ts +++ b/packages/rollup-dev-tools/test/transpiler/jump-integration.spec.ts @@ -110,7 +110,11 @@ describe('JUMP table solidity integration', () => { const transpiledJumperDeployedBytecode: Buffer = (transpiler.transpileRawBytecode( originalJumperDeployedBytecde ) as SuccessfulTranspilation).bytecode - log.debug(`transpiled output with log searcher: \n${formatBytecode(bufferToBytecode(transpiledJumperDeployedBytecode))}`) + log.debug( + `transpiled output with log searcher: \n${formatBytecode( + bufferToBytecode(transpiledJumperDeployedBytecode) + )}` + ) await evmUtil.deployBytecodeToAddress( transpiledJumperDeployedBytecode, hexStrToBuf(transpiledJumperAddr) @@ -196,7 +200,9 @@ const assertCallsProduceSameResult = async ( `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}...`) + log.debug( + `Completed first call successfully. Calling second contract at address ${addr2}...` + ) const res2 = await util.callContract( addr2, methodName, From a0aeebf2446ce922e4c51f6dcb916ce10e875b56 Mon Sep 17 00:00:00 2001 From: ben-chain Date: Sun, 5 Apr 2020 22:00:35 -0400 Subject: [PATCH 04/11] fix timeout for CI --- .../src/tools/transpiler/log-search-generation.ts | 2 +- .../rollup-dev-tools/test/transpiler/jump-integration.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/rollup-dev-tools/src/tools/transpiler/log-search-generation.ts b/packages/rollup-dev-tools/src/tools/transpiler/log-search-generation.ts index 26edd9b19175..19d87c157c92 100644 --- a/packages/rollup-dev-tools/src/tools/transpiler/log-search-generation.ts +++ b/packages/rollup-dev-tools/src/tools/transpiler/log-search-generation.ts @@ -37,7 +37,7 @@ type LogSearchNode = LogSearchLeafNode | LogSearchInternalNode type LogSearchTree = LogSearchNode[][] -const maxBytesOfContractSize = 2 +const maxBytesOfContractSize = 3 const IS_PUSH_BINARY_SEARCH_NODE_LOCATION = 'IS_PUSH_BINARY_SEARCH_NODE_LOCATION' 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 ac4998190c51..9ae61685fd92 100644 --- a/packages/rollup-dev-tools/test/transpiler/jump-integration.spec.ts +++ b/packages/rollup-dev-tools/test/transpiler/jump-integration.spec.ts @@ -175,7 +175,7 @@ describe('JUMP table solidity integration', () => { paramTypes, callParams ) - }).timeout(10000) + }).timeout(25000) }) const assertCallsProduceSameResult = async ( From ba768f7c20e5154c47977d8b90edea614f073ce2 Mon Sep 17 00:00:00 2001 From: ben-chain Date: Mon, 6 Apr 2020 12:33:33 -0400 Subject: [PATCH 05/11] naming and comments --- .../tools/transpiler/binary-search-tree.ts | 355 ++++++++++++++++++ .../src/tools/transpiler/index.ts | 2 +- .../tools/transpiler/log-search-generation.ts | 282 -------------- .../src/types/transpiler/types.ts | 18 + .../test/transpiler/transpile.jump.spec.ts | 73 ++++ 5 files changed, 447 insertions(+), 283 deletions(-) create mode 100644 packages/rollup-dev-tools/src/tools/transpiler/binary-search-tree.ts delete mode 100644 packages/rollup-dev-tools/src/tools/transpiler/log-search-generation.ts 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..53b5b7f106e9 --- /dev/null +++ b/packages/rollup-dev-tools/src/tools/transpiler/binary-search-tree.ts @@ -0,0 +1,355 @@ +import { + bytecodeToBuffer, + EVMBytecode, + Opcode, + EVMOpcodeAndBytes, + formatBytecode, + getPCOfEVMBytecodeIndex, +} from '@eth-optimism/rollup-core' +import { bufferUtils, getLogger } from '@eth-optimism/core-utils' +import { getPUSHOpcode, getPUSHIntegerOp } from './helpers' +import { + BinarySearchInternalNode, + BinarySearchLeafNode, + BinarySearchNode, +} from '../../types/transpiler' + +const log = getLogger('binary-search-generator') + +// 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 maxBytesOfContractSize = 3 + +// reasonTagged label for opcode PUSHing the PC of a binary search tree node to the stack +const IS_PUSH_BINARY_SEARCH_NODE_LOCATION = + 'IS_PUSH_BINARY_SEARCH_NODE_LOCATION' +// reasonTagged label for JUMPDEST of a binary searchh tree node. +const IS_BINARY_SEARCH_NODE_JUMPDEST = 'IS_BINARY_SEARCH_NODE_JUMPDEST' + +/** + * 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 flat array of nodes representing the tree, with the final node in the array being the root. + */ +export const generateLogSearchTreeNodes = ( + keys: number[], + values: number[] +): BinarySearchNode[] => { + // initialize our array of all tree nodes, populating first with all the leaves + let allTreeNodes: BinarySearchNode[] = keys.map((v, i) => { + return { + key: keys[i], + value: values[i], + nodeId: i, + } + }) + + // We process the tree in levels, with the parents of the current level becoming the next level + // If the current level is length 1, it is the root and we have generated the full tree. + let curLevel: BinarySearchNode[] = [...allTreeNodes] + while (curLevel.length > 1) { + const nextLevel: BinarySearchNode[] = [] + // Iterate over every other node in the level to compute parent nodes + for (let i = 0; i < curLevel.length; i += 2) { + const leftChild = curLevel[i] + const rightChild = curLevel[i + 1] + // If there is no right child, push the left child up to the next level so that the tree is a full binary tree. + if (!rightChild) { + nextLevel.push(leftChild) + continue + } + + // Now we calculate the parent for these children + const parentNode: BinarySearchInternalNode = { + nodeId: allTreeNodes.length, // nodeId is set as the position in the returned array + leftChildNodeId: leftChild.nodeId, + rightChildNodeId: rightChild.nodeId, + largestAncestorKey: undefined, + keyToCompare: undefined, + } + + // The largest ancestor key is the right child's largest ancestor or its key if a leaf + parentNode.largestAncestorKey = isLeafNode(rightChild) + ? (rightChild as BinarySearchLeafNode).key + : (rightChild as BinarySearchInternalNode).largestAncestorKey + + // The decision to go to the left or right sibling in a search is a comparison with the largest key in the left child's ancestors. + parentNode.keyToCompare = isLeafNode(leftChild) + ? (leftChild as BinarySearchLeafNode).key + : (parentNode.keyToCompare = (leftChild as BinarySearchInternalNode).largestAncestorKey) + + // Add parent to the tree so it will be returned + allTreeNodes.push(parentNode) + // Add parent to the next level so its parent can be processed + nextLevel.push(parentNode) + } + curLevel = nextLevel + } + return allTreeNodes +} + +const isLeafNode = (node: BinarySearchNode): boolean => { + return !!(node as BinarySearchLeafNode).value +} + +/** + * Generates the bytecode which will compute a binary search comparing jumpdestIndexesBefore to the + * top stack element, and JUMPs to tohe corresponding element of jumpdestIndexesAfter. + * + * @param jumpdestIndexesBefore The (lowest-to-highest) ordered array of jump indexes expected as stack inputs. + * @param values The array of corresponding PCs to JUMP to based on the stack input + * @param indexOfThisBlock The offset of this block in the bytecode where the result will be inserted + * @returns The EVM Bytecode performing the binary-search-and-JUMP operation. + */ +export const getJumpIndexSearchBytecode = ( + jumpdestIndexesBefore: number[], + jumpdestIndexesAfter: number[], + indexOfThisBlock: number +): EVMBytecode => { + // Generate a conceptual binary search tree for these jump indexes. + const searchTreeNodes: BinarySearchNode[] = generateLogSearchTreeNodes( + jumpdestIndexesBefore, + jumpdestIndexesAfter + ) + log.debug( + `successfully generated conceptual log search tree, its flat structure is: \n${JSON.stringify( + searchTreeNodes + )}` + ) + // The root node is always the final element rerturned by generateLogSearchTreeNodes() + const rootNodeId = searchTreeNodes.length - 1 + + const bytecode: EVMBytecode = [ + { + opcode: Opcode.JUMPDEST, + consumedBytes: undefined, + }, + ...appendAncestorsToBytecode(searchTreeNodes, rootNodeId, true, []), // Recursively fills out the bytecode based on the tree. + ] + // Fix the PUSH (jumpdests) to be correct once the tree has been generated. + const finalBytecode = fixJUMPsToNodes( + bytecode, + indexOfThisBlock, + searchTreeNodes + ) + log.debug( + `Generated final bytecode for log search jump table : \n${formatBytecode( + finalBytecode + )}` + ) + return finalBytecode +} + +/** + * Recursively appends to the given bytecode all ancestors for the given NodeId. + * + * @param treeNodes The binary search tree representing the nodes to process into bytecode + * @param nodeId The nodeId to process all ancestors of. + * @param isLeftSibling Whether the given nodeId is a left or right sibling (if a right sibling, it will be JUMPed to.) + * @param bytecode The existing EVM bytecode to append to. + * @returns The EVM Bytecode with all ancestors of the given nodeId having been added. + */ +const appendAncestorsToBytecode = ( + treeNodes: BinarySearchNode[], + nodeId: number, + isLeftSibling: boolean, + bytecode: EVMBytecode +): EVMBytecode => { + const thisNode: BinarySearchNode = treeNodes[nodeId] + log.info( + `Processing node with Id ${nodeId} of tree into bytecode, its parameters are ${JSON.stringify( + thisNode + )}.` + ) + if (isLeafNode(thisNode)) { + // If this is a leaf node, we can append the leaf and return since no ancestors. + bytecode = [ + ...bytecode, + ...generateLeafBytecode( + thisNode as BinarySearchLeafNode, + nodeId, + isLeftSibling + ), + ] + } else { + // Otherwise, we process and append the left and right siblings, recursively using this function. + // Append this node + const thisInternalNode = thisNode as BinarySearchInternalNode + bytecode = [ + ...bytecode, + ...generateInternalNodeBytecode(thisInternalNode, nodeId, isLeftSibling), + ] + // Append left and right children (left first--right siblings are JUMPed to, left siblings not!) + const leftChildId = thisInternalNode.leftChildNodeId + const rightChildId = thisInternalNode.rightChildNodeId + bytecode = appendAncestorsToBytecode(treeNodes, leftChildId, true, bytecode) + bytecode = appendAncestorsToBytecode( + treeNodes, + rightChildId, + false, + bytecode + ) + } + return bytecode +} + +/** + * 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 + * @param searchTreeNodes All of the binary nodes from which the bytecode was generated + * @returns The EVM Bytecode with fixed PUSHes. + */ +const fixJUMPsToNodes = ( + bytecode: EVMBytecode, + indexOfThisBlock: number, + searchTreeNodes: BinarySearchNode[] +): EVMBytecode => { + for (const opcodeAndBytes of bytecode) { + // Find all the PUSHes which we need to append the right sibling's JUMPDEST location to. + if ( + !!opcodeAndBytes.tag && + opcodeAndBytes.tag.reasonTagged === IS_PUSH_BINARY_SEARCH_NODE_LOCATION + ) { + // Get this node and the node it should JUMP to from the input tree + const thisNodeId = opcodeAndBytes.tag.metadata.nodeId + const rightChildNodeId = (searchTreeNodes[ + thisNodeId + ] as BinarySearchInternalNode).rightChildNodeId + // Find the index of the right child's JUMPDEST in the bytecode. + const rightChildJumpdestIndexInBytecodeBlock = bytecode.findIndex( + (toCheck: EVMOpcodeAndBytes) => { + return ( + !!toCheck.tag && + toCheck.tag.reasonTagged === IS_BINARY_SEARCH_NODE_JUMPDEST && + toCheck.tag.metadata.nodeId === rightChildNodeId + ) + } + ) + // 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 + opcodeAndBytes.consumedBytes = bufferUtils.numberToBuffer( + rightChildJumpdestPC, + maxBytesOfContractSize, + maxBytesOfContractSize + ) + } + } + return bytecode +} + +/** + * Generates bytecode for an internal binary search node, which either JUMPs to its right child + * if the input is greater than its valueToCompare, or continues to its left child which is immediately below. + * + * @param node The internal node to process + * @param nodeId The nodeId of this node (used for tagging for later correction) + * @param isLeftSibling Whether this is a left sibling which will be continued to, or a right sibling which will be JUMPed to. + * @returns The tagged block of EVM Bytecode for this internal node. + */ +const generateInternalNodeBytecode = ( + node: BinarySearchInternalNode, + nodeId: number, + isLeftSibling: boolean +): EVMBytecode => { + let bytecodeToReturn: EVMBytecode = [] + const willBeJUMPedTo: boolean = !isLeftSibling + if (willBeJUMPedTo) { + bytecodeToReturn.push(generateSearchNodeJumpdest(nodeId)) + } + + bytecodeToReturn = [ + ...bytecodeToReturn, + // DUP the input to compare + { + opcode: Opcode.DUP1, + consumedBytes: undefined, + }, + // PUSH the key to be compared to determine which node to proceed to + getPUSHIntegerOp(node.keyToCompare), + // 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(maxBytesOfContractSize), + consumedBytes: Buffer.alloc(maxBytesOfContractSize), + tag: { + padPUSH: false, + reasonTagged: IS_PUSH_BINARY_SEARCH_NODE_LOCATION, + metadata: { nodeId }, + }, + }, + // JUMP if the LT check passed + { + opcode: Opcode.JUMPI, + consumedBytes: undefined, + }, + ] + + return bytecodeToReturn +} + +/** + * Generates bytecode for binary search leaf node -- if we're here, the internal nodes have fully sorted the input + * + * @param node The leaf node to process + * @param nodeId The nodeId of this node (used for tagging for later correction) + * @param isLeftSibling Whether this is a left sibling which will be continued to, or a right sibling which will be JUMPed to. + * @returns The tagged block of EVM Bytecode for this internal node. + */ +const generateLeafBytecode = ( + node: BinarySearchLeafNode, + nodeId: number, + isLeftSibling: boolean +): EVMBytecode => { + let bytecodeToReturn: EVMBytecode = [] + const willBeJUMPedTo: boolean = !isLeftSibling + if (willBeJUMPedTo) { + bytecodeToReturn.push(generateSearchNodeJumpdest(nodeId)) + } + + bytecodeToReturn = [ + ...bytecodeToReturn, + // We have found a match for the input so it's no longer needed--POP it. + { + opcode: Opcode.POP, + consumedBytes: undefined, + }, + // JUMP to the jumpdestIndexAfter. + getPUSHIntegerOp(node.value), + { + opcode: Opcode.JUMP, + consumedBytes: undefined, + }, + ] + return bytecodeToReturn +} + +/** + * Generates a tagged JUMPDEST for a binary search node + * + * @param nodeId The nodeId of this node (used for tagging for later correction) + * @returns The correctly tagged JUMPDEST. + */ +const generateSearchNodeJumpdest = (nodeId: number): EVMOpcodeAndBytes => { + return { + opcode: Opcode.JUMPDEST, + consumedBytes: undefined, + tag: { + padPUSH: false, + reasonTagged: IS_BINARY_SEARCH_NODE_JUMPDEST, + metadata: { nodeId }, + }, + } +} diff --git a/packages/rollup-dev-tools/src/tools/transpiler/index.ts b/packages/rollup-dev-tools/src/tools/transpiler/index.ts index a5aeb0b62f45..970a65c588f9 100644 --- a/packages/rollup-dev-tools/src/tools/transpiler/index.ts +++ b/packages/rollup-dev-tools/src/tools/transpiler/index.ts @@ -7,4 +7,4 @@ export * from './opcode-whitelist' export * from './static-memory-opcodes' export * from './transpiler' export * from './util' -export * from './log-search-generation' +export * from './binary-search-tree' diff --git a/packages/rollup-dev-tools/src/tools/transpiler/log-search-generation.ts b/packages/rollup-dev-tools/src/tools/transpiler/log-search-generation.ts deleted file mode 100644 index 19d87c157c92..000000000000 --- a/packages/rollup-dev-tools/src/tools/transpiler/log-search-generation.ts +++ /dev/null @@ -1,282 +0,0 @@ -import { - bytecodeToBuffer, - EVMBytecode, - Opcode, - EVMOpcodeAndBytes, - formatBytecode, - getPCOfEVMBytecodeIndex, -} from '@eth-optimism/rollup-core' -import { bufferUtils, getLogger } from '@eth-optimism/core-utils' -import { getPUSHOpcode, getPUSHIntegerOp } from './helpers' -import { - JumpReplacementResult, - TranspilationError, - TranspilationErrors, -} from '../../types/transpiler' -import { createError } from './util' -import { TranscodeEncoding } from 'buffer' -import { UnicodeNormalizationForm } from 'ethers/utils' - -const log = getLogger('log-search-generator') - -interface LogSearchLeafNode { - key: number - value: number - nodeId: number -} - -interface LogSearchInternalNode { - largestAncestorKey: number - keyToCompare: number - nodeId: number - leftChildNodeId: number - rightChildNodeId: number -} - -type LogSearchNode = LogSearchLeafNode | LogSearchInternalNode - -type LogSearchTree = LogSearchNode[][] - -const maxBytesOfContractSize = 3 - -const IS_PUSH_BINARY_SEARCH_NODE_LOCATION = - 'IS_PUSH_BINARY_SEARCH_NODE_LOCATION' -const IS_BINARY_SEARCH_NODE_JUMPDEST = 'IS_BINARY_SEARCH_NODE_JUMPDEST' - -export const generateLogSearchTreeNodes = ( - keys: number[], - values: number[] -): LogSearchNode[] => { - const allTreeNodes: LogSearchNode[] = keys.map((v, i) => { - return { - key: keys[i], - value: values[i], - nodeId: i, - } - }) - - let curLevel: LogSearchNode[] = [...allTreeNodes] - while (curLevel.length > 1) { - // console.log(`processing level: ${JSON.stringify(curLevel)}`) - const nextLevel: LogSearchNode[] = [] - for (let i = 0; i < curLevel.length; i += 2) { - const leftChild = curLevel[i] - const rightChild = curLevel[i + 1] - if (!rightChild) { - nextLevel.push(leftChild) - continue - } - - const newNode: LogSearchInternalNode = { - nodeId: allTreeNodes.length, - leftChildNodeId: leftChild.nodeId, - rightChildNodeId: rightChild.nodeId, - largestAncestorKey: undefined, - keyToCompare: undefined, - } - - newNode.largestAncestorKey = isLeafNode(rightChild) - ? (rightChild as LogSearchLeafNode).key - : (rightChild as LogSearchInternalNode).largestAncestorKey - - newNode.keyToCompare = isLeafNode(leftChild) - ? (leftChild as LogSearchLeafNode).key - : (newNode.keyToCompare = (leftChild as LogSearchInternalNode).largestAncestorKey) - - allTreeNodes.push(newNode) - nextLevel.push(newNode) - } - curLevel = nextLevel - } - return allTreeNodes -} - -const isLeafNode = (node: LogSearchNode): boolean => { - return !!(node as LogSearchLeafNode).value -} - -export const getJumpIndexSearchBytecode = ( - jumpdestIndexesBefore: number[], - jumpdestIndexesAfter: number[], - indexOfThisBlock: number -): EVMBytecode => { - const searchTreeNodes: LogSearchNode[] = generateLogSearchTreeNodes( - jumpdestIndexesBefore, - jumpdestIndexesAfter - ) - log.debug( - `successfully generated conceptual log search tree, its flat structure is: \n${JSON.stringify( - searchTreeNodes - )}` - ) - const rootNodeId = searchTreeNodes.length - 1 // root node is the final one - const bytecode: EVMBytecode = [ - { - opcode: Opcode.JUMPDEST, - consumedBytes: undefined, - }, - ...appendNodeToBytecode(searchTreeNodes, rootNodeId, true, []), // should recursively fill out tree - ] - const finalBytecode = fixTaggedNodePositions( - bytecode, - indexOfThisBlock, - searchTreeNodes - ) - // log.debug(`generated final bytecode for log searcher : \n${formatBytecode(finalBytecode)}`) - return finalBytecode -} - -const fixTaggedNodePositions = ( - bytecode: EVMBytecode, - indexOfThisBlock: number, - searchTreeNodes: LogSearchNode[] -): EVMBytecode => { - for (const opcodeAndBytes of bytecode) { - if ( - !!opcodeAndBytes.tag && - opcodeAndBytes.tag.reasonTagged === IS_PUSH_BINARY_SEARCH_NODE_LOCATION - ) { - const thisNodeId = opcodeAndBytes.tag.metadata.nodeId - const rightChildNodeId = (searchTreeNodes[ - thisNodeId - ] as LogSearchInternalNode).rightChildNodeId - const rightChildJumpdestIndexInBytecodeBlock = bytecode.findIndex( - (toCheck: EVMOpcodeAndBytes) => { - return ( - !!toCheck.tag && - toCheck.tag.reasonTagged === IS_BINARY_SEARCH_NODE_JUMPDEST && - toCheck.tag.metadata.nodeId === rightChildNodeId - ) - } - ) - const rightChildJumpdestInFinalBuffer = - indexOfThisBlock + - getPCOfEVMBytecodeIndex( - rightChildJumpdestIndexInBytecodeBlock, - bytecode - ) - opcodeAndBytes.consumedBytes = bufferUtils.numberToBuffer( - rightChildJumpdestInFinalBuffer, - maxBytesOfContractSize, - maxBytesOfContractSize - ) - } - } - return bytecode -} - -const appendNodeToBytecode = ( - treeNodes: LogSearchNode[], - nodeId: number, - isLeftSibling: boolean, - bytecode: EVMBytecode -): EVMBytecode => { - const thisNode: LogSearchNode = treeNodes[nodeId] - log.info( - `Processing node with Id ${nodeId} of tree into bytecode, its parameters are ${JSON.stringify( - thisNode - )}.` - ) - if (isLeafNode(thisNode)) { - bytecode = [ - ...bytecode, - ...generateLeafBytecode( - thisNode as LogSearchLeafNode, - nodeId, - isLeftSibling - ), - ] - } else { - const thisInternalNode = thisNode as LogSearchInternalNode - bytecode = [ - ...bytecode, - ...generateComparisonBytecode(thisInternalNode, nodeId, isLeftSibling), - ] - const leftChildId = thisInternalNode.leftChildNodeId - const rightChildId = thisInternalNode.rightChildNodeId - bytecode = appendNodeToBytecode(treeNodes, leftChildId, true, bytecode) - bytecode = appendNodeToBytecode(treeNodes, rightChildId, false, bytecode) - } - return bytecode -} - -const generateComparisonBytecode = ( - node: LogSearchInternalNode, - nodeId: number, - isLeftSibling: boolean -): EVMBytecode => { - // if index is odd, add and tag JUMPDEST with "treeNodePosition" = [index, level] - // For GT check working, add "destinationNodePosition" = index*2+1, level+1 - let bytecodeToReturn: EVMBytecode = [] - const willBeJUMPedTo: boolean = !isLeftSibling - if (willBeJUMPedTo) { - bytecodeToReturn.push(generateSearchNodeJumpdest(nodeId)) - } - - bytecodeToReturn = [ - ...bytecodeToReturn, - { - opcode: Opcode.DUP1, - consumedBytes: undefined, - }, - getPUSHIntegerOp(node.keyToCompare), - { - opcode: Opcode.LT, - consumedBytes: undefined, - }, - { - opcode: getPUSHOpcode(maxBytesOfContractSize), - consumedBytes: Buffer.alloc(maxBytesOfContractSize), - tag: { - padPUSH: false, - reasonTagged: IS_PUSH_BINARY_SEARCH_NODE_LOCATION, - metadata: { nodeId }, - }, - }, - { - opcode: Opcode.JUMPI, - consumedBytes: undefined, - }, - ] - - return bytecodeToReturn -} - -const generateLeafBytecode = ( - node: LogSearchLeafNode, - nodeId: number, - isLeftSibling: boolean -): EVMBytecode => { - // do the matching stuff - let bytecodeToReturn: EVMBytecode = [] - const willBeJUMPedTo: boolean = !isLeftSibling - if (willBeJUMPedTo) { - bytecodeToReturn.push(generateSearchNodeJumpdest(nodeId)) - } - - bytecodeToReturn = [ - ...bytecodeToReturn, - { - opcode: Opcode.POP, - consumedBytes: undefined, - }, - getPUSHIntegerOp(node.value), - { - opcode: Opcode.JUMP, - consumedBytes: undefined, - }, - ] - return bytecodeToReturn -} - -const generateSearchNodeJumpdest = (nodeId: number): EVMOpcodeAndBytes => { - return { - opcode: Opcode.JUMPDEST, - consumedBytes: undefined, - tag: { - padPUSH: false, - reasonTagged: IS_BINARY_SEARCH_NODE_JUMPDEST, - metadata: { nodeId }, - }, - } -} diff --git a/packages/rollup-dev-tools/src/types/transpiler/types.ts b/packages/rollup-dev-tools/src/types/transpiler/types.ts index 9128c0660bab..69067fc0594f 100644 --- a/packages/rollup-dev-tools/src/types/transpiler/types.ts +++ b/packages/rollup-dev-tools/src/types/transpiler/types.ts @@ -32,3 +32,21 @@ export interface TaggedTranspilationResult { errors?: TranspilationError[] bytecodeWithTags?: EVMBytecode } + +// Conceptual data structures representing a leaf node in a binary search tree. + +export interface BinarySearchLeafNode { + key: number // The number which the binary search should be searching against. + value: number // The number which should be returned if the binary search matches the input to this key. + nodeId: number // Identifier for this node in the binary search tree, used for indexing +} + +export interface BinarySearchInternalNode { + largestAncestorKey: number // The largest .key value of any leaf-node ancestor of this node. + keyToCompare: number // The key which this node should compare against in the search, to progress either to the right or left child. + nodeId: number // Identifier for this node in the binary search tree, used for indexing + leftChildNodeId: number // Identifier of this node's left child. + rightChildNodeId: number // Identifier of this node's right child. +} + +export type BinarySearchNode = BinarySearchLeafNode | BinarySearchInternalNode 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 0fdfc1f08b9b..d957f78bcf2d 100644 --- a/packages/rollup-dev-tools/test/transpiler/transpile.jump.spec.ts +++ b/packages/rollup-dev-tools/test/transpiler/transpile.jump.spec.ts @@ -34,6 +34,74 @@ import { import { EvmIntrospectionUtil } from '../../src/types/vm' import { EvmIntrospectionUtilImpl } from '../../src/tools/vm' +/** + * Validates transpiled JUMP bytecode provided via the TranspilationResult parameter. + * + * @param successResult The transpilation result in question. + */ +const validateJumpBytecode = (successResult: SuccessfulTranspilation): void => { + const outputBytecode: EVMBytecode = bufferToBytecode(successResult.bytecode) + + let pc = 0 + let lastOpcode: EVMOpcodeAndBytes + const jumpdestIndexes: number[] = [] + const opcodesBeforeJump: Map = new Map< + number, + EVMOpcodeAndBytes + >() + // Build map of index => opcode immediately before JUMP and get index of footer switch + for (const opcodeAndBytes of outputBytecode) { + if (opcodeAndBytes.opcode === Opcode.JUMPDEST) { + jumpdestIndexes.push(pc) + } + if ( + opcodeAndBytes.opcode === Opcode.JUMP || + opcodeAndBytes.opcode === Opcode.JUMPI + ) { + opcodesBeforeJump.set( + pc - 1 - lastOpcode.opcode.programBytesConsumed, + lastOpcode + ) + } + lastOpcode = opcodeAndBytes + pc += 1 + opcodeAndBytes.opcode.programBytesConsumed + } + + jumpdestIndexes.length.should.be.greaterThan( + 0, + 'There should be JUMPDESTs, but there are not!' + ) + + const switchJumpdestIndex: number = jumpdestIndexes.pop() + const switchJumpdest: Buffer = successResult.bytecode.slice( + switchJumpdestIndex, + switchJumpdestIndex + 1 + ) + switchJumpdest.should.eql( + Opcode.JUMPDEST.code, + `Switch JUMPDEST index is ${switchJumpdestIndex}, but byte at that index is ${bufToHexString( + switchJumpdest + )}, not ${bufToHexString(Opcode.JUMPDEST.code)}` + ) + + const switchSuccessJumpdestIndex: number = jumpdestIndexes.pop() + const switchSuccessJumpdest: Buffer = successResult.bytecode.slice( + switchSuccessJumpdestIndex, + switchSuccessJumpdestIndex + 1 + ) + switchSuccessJumpdest.should.eql( + Opcode.JUMPDEST.code, + `Switch success JUMPDEST index is ${switchJumpdestIndex}, but byte at that index is ${bufToHexString( + switchJumpdest + )}, not ${bufToHexString(Opcode.JUMPDEST.code)}` + ) + + opcodesBeforeJump.size.should.be.greaterThan( + 0, + 'opcodesBeforeJump should have entries but does not!' + ) +} + const getSuccessfulTranspilationResult = ( transpiler: Transpiler, bytecode: Buffer @@ -79,6 +147,7 @@ describe('Transpile - JUMPs', () => { initialBytecode ) + validateJumpBytecode(successResult) await assertExecutionEqual(evmUtil, initialBytecode, successResult.bytecode) }) @@ -99,6 +168,7 @@ describe('Transpile - JUMPs', () => { transpiler, initialBytecode ) + validateJumpBytecode(successResult) await assertExecutionEqual(evmUtil, initialBytecode, successResult.bytecode) }) @@ -130,6 +200,7 @@ describe('Transpile - JUMPs', () => { transpiler, initialBytecode ) + validateJumpBytecode(successResult) await assertExecutionEqual(evmUtil, initialBytecode, successResult.bytecode) }) @@ -148,6 +219,7 @@ describe('Transpile - JUMPs', () => { transpiler, initialBytecode ) + validateJumpBytecode(successResult) await assertExecutionEqual(evmUtil, initialBytecode, successResult.bytecode) }) @@ -174,6 +246,7 @@ describe('Transpile - JUMPs', () => { initialBytecode, 'Transpilation should not have changed anything but did!' ) + await assertExecutionEqual(evmUtil, initialBytecode, successResult.bytecode) }) }) From a1eb9429619f5241ce3b26126c0be13a5eb13f71 Mon Sep 17 00:00:00 2001 From: ben-chain Date: Mon, 6 Apr 2020 12:42:11 -0400 Subject: [PATCH 06/11] lint --- .../rollup-dev-tools/src/tools/transpiler/binary-search-tree.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 53b5b7f106e9..9a352cb9a877 100644 --- a/packages/rollup-dev-tools/src/tools/transpiler/binary-search-tree.ts +++ b/packages/rollup-dev-tools/src/tools/transpiler/binary-search-tree.ts @@ -37,7 +37,7 @@ export const generateLogSearchTreeNodes = ( values: number[] ): BinarySearchNode[] => { // initialize our array of all tree nodes, populating first with all the leaves - let allTreeNodes: BinarySearchNode[] = keys.map((v, i) => { + const allTreeNodes: BinarySearchNode[] = keys.map((v, i) => { return { key: keys[i], value: values[i], From ea06c824b4dd627dff2a8b14da2884f168051923 Mon Sep 17 00:00:00 2001 From: ben-chain Date: Tue, 7 Apr 2020 11:01:12 -0400 Subject: [PATCH 07/11] make search tree be a BST --- .../tools/transpiler/binary-search-tree.ts | 496 ++++++++---------- .../src/tools/transpiler/jump-replacement.ts | 10 +- .../src/types/transpiler/types.ts | 23 +- .../test/transpiler/jump-integration.spec.ts | 4 +- 4 files changed, 246 insertions(+), 287 deletions(-) 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 index 9a352cb9a877..6e3a562e5018 100644 --- a/packages/rollup-dev-tools/src/tools/transpiler/binary-search-tree.ts +++ b/packages/rollup-dev-tools/src/tools/transpiler/binary-search-tree.ts @@ -8,13 +8,9 @@ import { } from '@eth-optimism/rollup-core' import { bufferUtils, getLogger } from '@eth-optimism/core-utils' import { getPUSHOpcode, getPUSHIntegerOp } from './helpers' -import { - BinarySearchInternalNode, - BinarySearchLeafNode, - BinarySearchNode, -} from '../../types/transpiler' +import { BinarySearchTreeNode } from '../../types/transpiler' -const log = getLogger('binary-search-generator') +const log = getLogger('binary-search-tree-generator') // 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 maxBytesOfContractSize = 3 @@ -24,174 +20,242 @@ const IS_PUSH_BINARY_SEARCH_NODE_LOCATION = 'IS_PUSH_BINARY_SEARCH_NODE_LOCATION' // reasonTagged label for JUMPDEST of a binary searchh tree node. const IS_BINARY_SEARCH_NODE_JUMPDEST = 'IS_BINARY_SEARCH_NODE_JUMPDEST' +// reasonTagged label for PUSHing the PC of our jumpdestBefore match success block +const IS_PUSH_MATCH_SUCCESS_LOC = 'IS_PUSH_MATCH_SUCCESS_LOC' + +/** + * 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 => { + const rootNode = getBSTRoot(keys, values) + const BSTBytecodeWithIncorrectJUMPs: EVMBytecode = [ + // Bytecode to JUMP to when a successful match is found by a BST node + ...getJumpIndexSwitchStatementSuccessJumpdestBytecode(), + // 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 flat array of nodes representing the tree, with the final node in the array being the root. + * @returns A root BST node whose ancestors represent the full BST for the given k/v pairs. */ -export const generateLogSearchTreeNodes = ( - keys: number[], - values: number[] -): BinarySearchNode[] => { - // initialize our array of all tree nodes, populating first with all the leaves - const allTreeNodes: BinarySearchNode[] = keys.map((v, i) => { - return { - key: keys[i], - value: values[i], - nodeId: i, - } - }) - - // We process the tree in levels, with the parents of the current level becoming the next level - // If the current level is length 1, it is the root and we have generated the full tree. - let curLevel: BinarySearchNode[] = [...allTreeNodes] - while (curLevel.length > 1) { - const nextLevel: BinarySearchNode[] = [] - // Iterate over every other node in the level to compute parent nodes - for (let i = 0; i < curLevel.length; i += 2) { - const leftChild = curLevel[i] - const rightChild = curLevel[i + 1] - // If there is no right child, push the left child up to the next level so that the tree is a full binary tree. - if (!rightChild) { - nextLevel.push(leftChild) - continue +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, } - - // Now we calculate the parent for these children - const parentNode: BinarySearchInternalNode = { - nodeId: allTreeNodes.length, // nodeId is set as the position in the returned array - leftChildNodeId: leftChild.nodeId, - rightChildNodeId: rightChild.nodeId, - largestAncestorKey: undefined, - keyToCompare: undefined, - } - - // The largest ancestor key is the right child's largest ancestor or its key if a leaf - parentNode.largestAncestorKey = isLeafNode(rightChild) - ? (rightChild as BinarySearchLeafNode).key - : (rightChild as BinarySearchInternalNode).largestAncestorKey - - // The decision to go to the left or right sibling in a search is a comparison with the largest key in the left child's ancestors. - parentNode.keyToCompare = isLeafNode(leftChild) - ? (leftChild as BinarySearchLeafNode).key - : (parentNode.keyToCompare = (leftChild as BinarySearchInternalNode).largestAncestorKey) - - // Add parent to the tree so it will be returned - allTreeNodes.push(parentNode) - // Add parent to the next level so its parent can be processed - nextLevel.push(parentNode) } - curLevel = nextLevel - } - return allTreeNodes + ) + const sortedBottomNodes = bottomNodes.sort( + (node1: BinarySearchTreeNode, node2: BinarySearchTreeNode) => { + return node1.value.jumpdestBefore - node2.value.jumpdestBefore + } + ) + return buildBST(sortedBottomNodes) } -const isLeafNode = (node: BinarySearchNode): boolean => { - return !!(node as BinarySearchLeafNode).value +/** + * 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 the bytecode which will compute a binary search comparing jumpdestIndexesBefore to the - * top stack element, and JUMPs to tohe corresponding element of jumpdestIndexesAfter. - * - * @param jumpdestIndexesBefore The (lowest-to-highest) ordered array of jump indexes expected as stack inputs. - * @param values The array of corresponding PCs to JUMP to based on the stack input - * @param indexOfThisBlock The offset of this block in the bytecode where the result will be inserted - * @returns The EVM Bytecode performing the binary-search-and-JUMP operation. + * 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. */ -export const getJumpIndexSearchBytecode = ( - jumpdestIndexesBefore: number[], - jumpdestIndexesAfter: number[], - indexOfThisBlock: number +const getBytecodeForSubtreeRoot = ( + node: BinarySearchTreeNode, + isRightNode: boolean ): EVMBytecode => { - // Generate a conceptual binary search tree for these jump indexes. - const searchTreeNodes: BinarySearchNode[] = generateLogSearchTreeNodes( - jumpdestIndexesBefore, - jumpdestIndexesAfter - ) - log.debug( - `successfully generated conceptual log search tree, its flat structure is: \n${JSON.stringify( - searchTreeNodes - )}` + 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( + ...[ + // 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(maxBytesOfContractSize), + consumedBytes: Buffer.alloc(maxBytesOfContractSize), + tag: { + padPUSH: false, + reasonTagged: 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, + }, + ] ) - // The root node is always the final element rerturned by generateLogSearchTreeNodes() - const rootNodeId = searchTreeNodes.length - 1 - - const bytecode: EVMBytecode = [ - { - opcode: Opcode.JUMPDEST, + // 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, - }, - ...appendAncestorsToBytecode(searchTreeNodes, rootNodeId, true, []), // Recursively fills out the bytecode based on the tree. - ] - // Fix the PUSH (jumpdests) to be correct once the tree has been generated. - const finalBytecode = fixJUMPsToNodes( - bytecode, - indexOfThisBlock, - searchTreeNodes - ) - log.debug( - `Generated final bytecode for log search jump table : \n${formatBytecode( - finalBytecode - )}` + }) + 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( + ...[ + { + 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(maxBytesOfContractSize), + consumedBytes: Buffer.alloc(maxBytesOfContractSize), + tag: { + padPUSH: false, + reasonTagged: IS_PUSH_BINARY_SEARCH_NODE_LOCATION, + metadata: { node }, + }, + }, + // JUMP if the LT check passed + { + opcode: Opcode.JUMPI, + consumedBytes: undefined, + }, + ] + ) + } + // 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 finalBytecode + return bytecodeToReturn } /** - * Recursively appends to the given bytecode all ancestors for the given NodeId. + * Generates a tagged JUMPDEST for a binary search node * - * @param treeNodes The binary search tree representing the nodes to process into bytecode - * @param nodeId The nodeId to process all ancestors of. - * @param isLeftSibling Whether the given nodeId is a left or right sibling (if a right sibling, it will be JUMPed to.) - * @param bytecode The existing EVM bytecode to append to. - * @returns The EVM Bytecode with all ancestors of the given nodeId having been added. + * @param nodeId The nodeId of this node (used for tagging for later correction) + * @returns The correctly tagged JUMPDEST. */ -const appendAncestorsToBytecode = ( - treeNodes: BinarySearchNode[], - nodeId: number, - isLeftSibling: boolean, - bytecode: EVMBytecode -): EVMBytecode => { - const thisNode: BinarySearchNode = treeNodes[nodeId] - log.info( - `Processing node with Id ${nodeId} of tree into bytecode, its parameters are ${JSON.stringify( - thisNode - )}.` - ) - if (isLeafNode(thisNode)) { - // If this is a leaf node, we can append the leaf and return since no ancestors. - bytecode = [ - ...bytecode, - ...generateLeafBytecode( - thisNode as BinarySearchLeafNode, - nodeId, - isLeftSibling - ), - ] - } else { - // Otherwise, we process and append the left and right siblings, recursively using this function. - // Append this node - const thisInternalNode = thisNode as BinarySearchInternalNode - bytecode = [ - ...bytecode, - ...generateInternalNodeBytecode(thisInternalNode, nodeId, isLeftSibling), - ] - // Append left and right children (left first--right siblings are JUMPed to, left siblings not!) - const leftChildId = thisInternalNode.leftChildNodeId - const rightChildId = thisInternalNode.rightChildNodeId - bytecode = appendAncestorsToBytecode(treeNodes, leftChildId, true, bytecode) - bytecode = appendAncestorsToBytecode( - treeNodes, - rightChildId, - false, - bytecode - ) +const generateBinarySearchTreeNodeJumpdest = ( + node: BinarySearchTreeNode +): EVMOpcodeAndBytes => { + return { + opcode: Opcode.JUMPDEST, + consumedBytes: undefined, + tag: { + padPUSH: false, + reasonTagged: IS_BINARY_SEARCH_NODE_JUMPDEST, + metadata: { node }, + }, } - return bytecode +} + +/** + * 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 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 }, + ] } /** @@ -199,32 +263,38 @@ const appendAncestorsToBytecode = ( * * @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 - * @param searchTreeNodes All of the binary nodes from which the bytecode was generated * @returns The EVM Bytecode with fixed PUSHes. */ const fixJUMPsToNodes = ( bytecode: EVMBytecode, - indexOfThisBlock: number, - searchTreeNodes: BinarySearchNode[] + indexOfThisBlock: number ): EVMBytecode => { for (const opcodeAndBytes of bytecode) { + // Find all the PUSHes going to the "successful match" block which is always placed at the start of this block + if ( + !!opcodeAndBytes.tag && + opcodeAndBytes.tag.reasonTagged === IS_PUSH_MATCH_SUCCESS_LOC + ) { + opcodeAndBytes.consumedBytes = bufferUtils.numberToBuffer( + indexOfThisBlock, + maxBytesOfContractSize, + maxBytesOfContractSize + ) + } // Find all the PUSHes which we need to append the right sibling's JUMPDEST location to. if ( !!opcodeAndBytes.tag && opcodeAndBytes.tag.reasonTagged === IS_PUSH_BINARY_SEARCH_NODE_LOCATION ) { - // Get this node and the node it should JUMP to from the input tree - const thisNodeId = opcodeAndBytes.tag.metadata.nodeId - const rightChildNodeId = (searchTreeNodes[ - thisNodeId - ] as BinarySearchInternalNode).rightChildNodeId - // Find the index of the right child's JUMPDEST in the bytecode. + const rightChild: BinarySearchTreeNode = + opcodeAndBytes.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 === IS_BINARY_SEARCH_NODE_JUMPDEST && - toCheck.tag.metadata.nodeId === rightChildNodeId + toCheck.tag.metadata.node === rightChild ) } ) @@ -245,111 +315,3 @@ const fixJUMPsToNodes = ( } return bytecode } - -/** - * Generates bytecode for an internal binary search node, which either JUMPs to its right child - * if the input is greater than its valueToCompare, or continues to its left child which is immediately below. - * - * @param node The internal node to process - * @param nodeId The nodeId of this node (used for tagging for later correction) - * @param isLeftSibling Whether this is a left sibling which will be continued to, or a right sibling which will be JUMPed to. - * @returns The tagged block of EVM Bytecode for this internal node. - */ -const generateInternalNodeBytecode = ( - node: BinarySearchInternalNode, - nodeId: number, - isLeftSibling: boolean -): EVMBytecode => { - let bytecodeToReturn: EVMBytecode = [] - const willBeJUMPedTo: boolean = !isLeftSibling - if (willBeJUMPedTo) { - bytecodeToReturn.push(generateSearchNodeJumpdest(nodeId)) - } - - bytecodeToReturn = [ - ...bytecodeToReturn, - // DUP the input to compare - { - opcode: Opcode.DUP1, - consumedBytes: undefined, - }, - // PUSH the key to be compared to determine which node to proceed to - getPUSHIntegerOp(node.keyToCompare), - // 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(maxBytesOfContractSize), - consumedBytes: Buffer.alloc(maxBytesOfContractSize), - tag: { - padPUSH: false, - reasonTagged: IS_PUSH_BINARY_SEARCH_NODE_LOCATION, - metadata: { nodeId }, - }, - }, - // JUMP if the LT check passed - { - opcode: Opcode.JUMPI, - consumedBytes: undefined, - }, - ] - - return bytecodeToReturn -} - -/** - * Generates bytecode for binary search leaf node -- if we're here, the internal nodes have fully sorted the input - * - * @param node The leaf node to process - * @param nodeId The nodeId of this node (used for tagging for later correction) - * @param isLeftSibling Whether this is a left sibling which will be continued to, or a right sibling which will be JUMPed to. - * @returns The tagged block of EVM Bytecode for this internal node. - */ -const generateLeafBytecode = ( - node: BinarySearchLeafNode, - nodeId: number, - isLeftSibling: boolean -): EVMBytecode => { - let bytecodeToReturn: EVMBytecode = [] - const willBeJUMPedTo: boolean = !isLeftSibling - if (willBeJUMPedTo) { - bytecodeToReturn.push(generateSearchNodeJumpdest(nodeId)) - } - - bytecodeToReturn = [ - ...bytecodeToReturn, - // We have found a match for the input so it's no longer needed--POP it. - { - opcode: Opcode.POP, - consumedBytes: undefined, - }, - // JUMP to the jumpdestIndexAfter. - getPUSHIntegerOp(node.value), - { - opcode: Opcode.JUMP, - consumedBytes: undefined, - }, - ] - return bytecodeToReturn -} - -/** - * Generates a tagged JUMPDEST for a binary search node - * - * @param nodeId The nodeId of this node (used for tagging for later correction) - * @returns The correctly tagged JUMPDEST. - */ -const generateSearchNodeJumpdest = (nodeId: number): EVMOpcodeAndBytes => { - return { - opcode: Opcode.JUMPDEST, - consumedBytes: undefined, - tag: { - padPUSH: false, - reasonTagged: IS_BINARY_SEARCH_NODE_JUMPDEST, - metadata: { nodeId }, - }, - } -} 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 6f7be0b0f8f8..90b5c921b916 100644 --- a/packages/rollup-dev-tools/src/tools/transpiler/jump-replacement.ts +++ b/packages/rollup-dev-tools/src/tools/transpiler/jump-replacement.ts @@ -11,7 +11,10 @@ import { TranspilationErrors, } from '../../types/transpiler' import { createError } from './util' -import { getJumpIndexSearchBytecode } from './' +import { + buildJumpBSTBytecode, + getJumpIndexSwitchStatementSuccessJumpdestBytecode, +} from './' const log = getLogger('jump-replacement') @@ -72,7 +75,7 @@ export const accountForJumps = ( // Add the logic to handle the pre-transpilation to post-transpilation jump dest mapping. replacedBytecode.push( - ...getJumpIndexSearchBytecode( + ...buildJumpBSTBytecode( jumpdestIndexesBefore, jumpdestIndexesAfter, bytecodeToBuffer(replacedBytecode).length @@ -185,5 +188,8 @@ export const getExpectedFooterSwitchStatementJumpdestIndex = ( length += 1 + opcodeAndBytes.opcode.programBytesConsumed } } + length += bytecodeToBuffer( + getJumpIndexSwitchStatementSuccessJumpdestBytecode() + ).length return length } diff --git a/packages/rollup-dev-tools/src/types/transpiler/types.ts b/packages/rollup-dev-tools/src/types/transpiler/types.ts index 69067fc0594f..fabfa35fd0ce 100644 --- a/packages/rollup-dev-tools/src/types/transpiler/types.ts +++ b/packages/rollup-dev-tools/src/types/transpiler/types.ts @@ -33,20 +33,11 @@ export interface TaggedTranspilationResult { bytecodeWithTags?: EVMBytecode } -// Conceptual data structures representing a leaf node in a binary search tree. - -export interface BinarySearchLeafNode { - key: number // The number which the binary search should be searching against. - value: number // The number which should be returned if the binary search matches the input to this key. - nodeId: number // Identifier for this node in the binary search tree, used for indexing -} - -export interface BinarySearchInternalNode { - largestAncestorKey: number // The largest .key value of any leaf-node ancestor of this node. - keyToCompare: number // The key which this node should compare against in the search, to progress either to the right or left child. - nodeId: number // Identifier for this node in the binary search tree, used for indexing - leftChildNodeId: number // Identifier of this node's left child. - rightChildNodeId: number // Identifier of this node's right child. +export interface BinarySearchTreeNode { + value: { + jumpdestBefore: number + jumpdestAfter: number + } + left: BinarySearchTreeNode + right: BinarySearchTreeNode } - -export type BinarySearchNode = BinarySearchLeafNode | BinarySearchInternalNode 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 9ae61685fd92..00d8b37f05f8 100644 --- a/packages/rollup-dev-tools/test/transpiler/jump-integration.spec.ts +++ b/packages/rollup-dev-tools/test/transpiler/jump-integration.spec.ts @@ -60,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 { @@ -111,7 +111,7 @@ describe('JUMP table solidity integration', () => { originalJumperDeployedBytecde ) as SuccessfulTranspilation).bytecode log.debug( - `transpiled output with log searcher: \n${formatBytecode( + `Transpiled jumper output with BST jump table: \n${formatBytecode( bufferToBytecode(transpiledJumperDeployedBytecode) )}` ) From 23800093d3af11b6f5b9983d13081ba5c2bcdf14 Mon Sep 17 00:00:00 2001 From: ben-chain Date: Tue, 7 Apr 2020 11:51:29 -0400 Subject: [PATCH 08/11] other PR feedback --- packages/rollup-core/src/types/opcodes.ts | 12 +++- .../tools/transpiler/binary-search-tree.ts | 63 +++++++------------ .../src/tools/transpiler/transpiler.ts | 40 ++++++------ 3 files changed, 55 insertions(+), 60 deletions(-) diff --git a/packages/rollup-core/src/types/opcodes.ts b/packages/rollup-core/src/types/opcodes.ts index fd9df655ef31..8099712eb533 100644 --- a/packages/rollup-core/src/types/opcodes.ts +++ b/packages/rollup-core/src/types/opcodes.ts @@ -16,10 +16,20 @@ 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 + 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 { public static readonly STOP: EVMOpcode = { code: Buffer.from('00', 'hex'), 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 index 6e3a562e5018..3de344fa8ca3 100644 --- a/packages/rollup-dev-tools/src/tools/transpiler/binary-search-tree.ts +++ b/packages/rollup-dev-tools/src/tools/transpiler/binary-search-tree.ts @@ -5,6 +5,7 @@ import { EVMOpcodeAndBytes, formatBytecode, getPCOfEVMBytecodeIndex, + OpcodeTagReason } from '@eth-optimism/rollup-core' import { bufferUtils, getLogger } from '@eth-optimism/core-utils' import { getPUSHOpcode, getPUSHIntegerOp } from './helpers' @@ -13,15 +14,7 @@ import { BinarySearchTreeNode } from '../../types/transpiler' const log = getLogger('binary-search-tree-generator') // 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 maxBytesOfContractSize = 3 - -// reasonTagged label for opcode PUSHing the PC of a binary search tree node to the stack -const IS_PUSH_BINARY_SEARCH_NODE_LOCATION = - 'IS_PUSH_BINARY_SEARCH_NODE_LOCATION' -// reasonTagged label for JUMPDEST of a binary searchh tree node. -const IS_BINARY_SEARCH_NODE_JUMPDEST = 'IS_BINARY_SEARCH_NODE_JUMPDEST' -// reasonTagged label for PUSHing the PC of our jumpdestBefore match success block -const IS_PUSH_MATCH_SUCCESS_LOC = 'IS_PUSH_MATCH_SUCCESS_LOC' +const pcMaxByteSize = 3 /** * Generates a JUMP-correctiing binary search tree block which is used to map pre-transpiled JUMPDESTs too post-transpiled JUMPDESTs. @@ -148,11 +141,11 @@ const getBytecodeForSubtreeRoot = ( }, // PUSH success block location (via a tag--will be filled out later) { - opcode: getPUSHOpcode(maxBytesOfContractSize), - consumedBytes: Buffer.alloc(maxBytesOfContractSize), + opcode: getPUSHOpcode(pcMaxByteSize), + consumedBytes: Buffer.alloc(pcMaxByteSize), tag: { padPUSH: false, - reasonTagged: IS_PUSH_MATCH_SUCCESS_LOC, + reasonTagged: OpcodeTagReason.IS_PUSH_MATCH_SUCCESS_LOC, metadata: undefined, }, }, @@ -193,11 +186,11 @@ const getBytecodeForSubtreeRoot = ( }, // PUSH a *placeholder* for the destination of thde right child to be JUMPed to if check passes--to be set later { - opcode: getPUSHOpcode(maxBytesOfContractSize), - consumedBytes: Buffer.alloc(maxBytesOfContractSize), + opcode: getPUSHOpcode(pcMaxByteSize), + consumedBytes: Buffer.alloc(pcMaxByteSize), tag: { padPUSH: false, - reasonTagged: IS_PUSH_BINARY_SEARCH_NODE_LOCATION, + reasonTagged: OpcodeTagReason.IS_PUSH_BINARY_SEARCH_NODE_LOCATION, metadata: { node }, }, }, @@ -231,7 +224,7 @@ const generateBinarySearchTreeNodeJumpdest = ( consumedBytes: undefined, tag: { padPUSH: false, - reasonTagged: IS_BINARY_SEARCH_NODE_JUMPDEST, + reasonTagged: OpcodeTagReason.IS_BINARY_SEARCH_NODE_JUMPDEST, metadata: { node }, }, } @@ -269,31 +262,22 @@ const fixJUMPsToNodes = ( bytecode: EVMBytecode, indexOfThisBlock: number ): EVMBytecode => { - for (const opcodeAndBytes of bytecode) { - // Find all the PUSHes going to the "successful match" block which is always placed at the start of this block - if ( - !!opcodeAndBytes.tag && - opcodeAndBytes.tag.reasonTagged === IS_PUSH_MATCH_SUCCESS_LOC - ) { - opcodeAndBytes.consumedBytes = bufferUtils.numberToBuffer( - indexOfThisBlock, - maxBytesOfContractSize, - maxBytesOfContractSize - ) - } - // Find all the PUSHes which we need to append the right sibling's JUMPDEST location to. - if ( - !!opcodeAndBytes.tag && - opcodeAndBytes.tag.reasonTagged === IS_PUSH_BINARY_SEARCH_NODE_LOCATION - ) { - const rightChild: BinarySearchTreeNode = - opcodeAndBytes.tag.metadata.node.right + 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 === IS_BINARY_SEARCH_NODE_JUMPDEST && + toCheck.tag.reasonTagged === OpcodeTagReason.IS_BINARY_SEARCH_NODE_JUMPDEST && toCheck.tag.metadata.node === rightChild ) } @@ -306,12 +290,11 @@ const fixJUMPsToNodes = ( bytecode ) // Set the consumed bytes to be this PC - opcodeAndBytes.consumedBytes = bufferUtils.numberToBuffer( + pushBSTNodeOp.consumedBytes = bufferUtils.numberToBuffer( rightChildJumpdestPC, - maxBytesOfContractSize, - maxBytesOfContractSize + pcMaxByteSize, + pcMaxByteSize ) - } } return bytecode } 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, }, } From 25b8f3238a146aa96de88c668c4e382576aff39d Mon Sep 17 00:00:00 2001 From: ben-chain Date: Tue, 7 Apr 2020 12:12:44 -0400 Subject: [PATCH 09/11] linting --- .../tools/transpiler/binary-search-tree.ts | 56 ++++++++++--------- 1 file changed, 31 insertions(+), 25 deletions(-) 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 index 3de344fa8ca3..5df53fb33ef8 100644 --- a/packages/rollup-dev-tools/src/tools/transpiler/binary-search-tree.ts +++ b/packages/rollup-dev-tools/src/tools/transpiler/binary-search-tree.ts @@ -5,7 +5,7 @@ import { EVMOpcodeAndBytes, formatBytecode, getPCOfEVMBytecodeIndex, - OpcodeTagReason + OpcodeTagReason, } from '@eth-optimism/rollup-core' import { bufferUtils, getLogger } from '@eth-optimism/core-utils' import { getPUSHOpcode, getPUSHIntegerOp } from './helpers' @@ -262,39 +262,45 @@ 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)) { + 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)) { + 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 + // 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 ) - // Set the consumed bytes to be this PC - pushBSTNodeOp.consumedBytes = bufferUtils.numberToBuffer( - rightChildJumpdestPC, - pcMaxByteSize, - pcMaxByteSize - ) + } + ) + // 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 } From 018e7110422db2943d2f1c7101e0af664a3b3c66 Mon Sep 17 00:00:00 2001 From: ben-chain Date: Tue, 7 Apr 2020 16:41:49 -0400 Subject: [PATCH 10/11] final PR feedback --- .../tools/transpiler/binary-search-tree.ts | 171 ++++++++++-------- .../src/tools/transpiler/jump-replacement.ts | 9 +- 2 files changed, 101 insertions(+), 79 deletions(-) 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 index 5df53fb33ef8..d6e61990e5f9 100644 --- a/packages/rollup-dev-tools/src/tools/transpiler/binary-search-tree.ts +++ b/packages/rollup-dev-tools/src/tools/transpiler/binary-search-tree.ts @@ -30,10 +30,15 @@ export const buildJumpBSTBytecode = ( 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 - ...getJumpIndexSwitchStatementSuccessJumpdestBytecode(), + ...getJumpdestMatchSuccessBytecode(), // Entry point for the actual BST matching logic { opcode: Opcode.JUMPDEST, @@ -120,47 +125,7 @@ const getBytecodeForSubtreeRoot = ( bytecodeToReturn.push(generateBinarySearchTreeNodeJumpdest(node)) } // Generate the match check for this node - bytecodeToReturn.push( - ...[ - // 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, - }, - ] - ) + 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({ @@ -172,34 +137,7 @@ const getBytecodeForSubtreeRoot = ( // 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( - ...[ - { - 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, - }, - ] + ...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 @@ -210,10 +148,99 @@ const getBytecodeForSubtreeRoot = ( 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 nodeId The nodeId of this node (used for tagging for later correction) + * @param node The BST node being jumped to (used for tagging for later correction) * @returns The correctly tagged JUMPDEST. */ const generateBinarySearchTreeNodeJumpdest = ( @@ -238,7 +265,7 @@ const generateBinarySearchTreeNodeJumpdest = ( * @returns The success bytecode. */ -export const getJumpIndexSwitchStatementSuccessJumpdestBytecode = (): EVMBytecode => { +export const getJumpdestMatchSuccessBytecode = (): EVMBytecode => { return [ // This JUMPDEST is hit on successful switch match { opcode: Opcode.JUMPDEST, consumedBytes: undefined }, 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 90b5c921b916..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,10 +11,7 @@ import { TranspilationErrors, } from '../../types/transpiler' import { createError } from './util' -import { - buildJumpBSTBytecode, - getJumpIndexSwitchStatementSuccessJumpdestBytecode, -} from './' +import { buildJumpBSTBytecode, getJumpdestMatchSuccessBytecode } from './' const log = getLogger('jump-replacement') @@ -188,8 +185,6 @@ export const getExpectedFooterSwitchStatementJumpdestIndex = ( length += 1 + opcodeAndBytes.opcode.programBytesConsumed } } - length += bytecodeToBuffer( - getJumpIndexSwitchStatementSuccessJumpdestBytecode() - ).length + length += bytecodeToBuffer(getJumpdestMatchSuccessBytecode()).length return length } From 248d4484b9d3a5e685672c0579190ede0f834819 Mon Sep 17 00:00:00 2001 From: ben-chain Date: Tue, 7 Apr 2020 19:36:13 -0400 Subject: [PATCH 11/11] remove unused logger --- .../rollup-dev-tools/src/tools/transpiler/binary-search-tree.ts | 2 -- 1 file changed, 2 deletions(-) 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 index d6e61990e5f9..395059c7b41c 100644 --- a/packages/rollup-dev-tools/src/tools/transpiler/binary-search-tree.ts +++ b/packages/rollup-dev-tools/src/tools/transpiler/binary-search-tree.ts @@ -11,8 +11,6 @@ import { bufferUtils, getLogger } from '@eth-optimism/core-utils' import { getPUSHOpcode, getPUSHIntegerOp } from './helpers' import { BinarySearchTreeNode } from '../../types/transpiler' -const log = getLogger('binary-search-tree-generator') - // 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