diff --git a/libs/remix-debug/src/debugger/debugger.ts b/libs/remix-debug/src/debugger/debugger.ts index 643a3917192..ed49708528b 100644 --- a/libs/remix-debug/src/debugger/debugger.ts +++ b/libs/remix-debug/src/debugger/debugger.ts @@ -78,6 +78,7 @@ export class Debugger { } this.debugger.callTree.getValidSourceLocationFromVMTraceIndexFromCache(address, index, compilationResultForAddress.data.contracts).then(async (rawLocationAndOpcode) => { + console.log(rawLocationAndOpcode) if (compilationResultForAddress && compilationResultForAddress.data) { const rawLocation = rawLocationAndOpcode.sourceLocation const stepDetail = rawLocationAndOpcode.stepDetail diff --git a/libs/remix-debug/src/solidity-decoder/internalCallTree.ts b/libs/remix-debug/src/solidity-decoder/internalCallTree.ts index 28ae0ec9f53..00fa219c83e 100644 --- a/libs/remix-debug/src/solidity-decoder/internalCallTree.ts +++ b/libs/remix-debug/src/solidity-decoder/internalCallTree.ts @@ -1,5 +1,6 @@ 'use strict' import { AstWalker } from '@remix-project/remix-astwalker' +import { nodesAtPositionForSourceLocation } from '../source/sourceMappingDecoder' import { util } from '@remix-project/remix-lib' import { SourceLocationTracker } from '../source/sourceLocationTracker' import { EventManager } from '../eventManager' @@ -50,6 +51,10 @@ export class InternalCallTree { [Key: number]: any } + codeManager: any + scopesMapping: any + processedFunctions: any + /** * constructor * @@ -66,6 +71,7 @@ export class InternalCallTree { this.solidityProxy = solidityProxy this.traceManager = traceManager this.offsetToLineColumnConverter = offsetToLineColumnConverter + this.codeManager = codeManager this.sourceLocationTracker = new SourceLocationTracker(codeManager, { debugWithGeneratedSources: opts.debugWithGeneratedSources }) debuggerEvent.register('newTraceLoaded', async (trace) => { const time = Date.now() @@ -131,6 +137,82 @@ export class InternalCallTree { this.constructorsStartExecution = {} this.pendingConstructor = null this.variables = {} + + this.scopesMapping = {} + this.processedFunctions = [] + } + + /* + async printSolArtefacts (vmtraceIndex) { + const address = this.traceManager.getCurrentCalledAddressAt(vmtraceIndex) + const contractObj = await this.solidityProxy.contractObjectAtAddress(address) + const pc = this.traceManager.getCurrentPC(vmtraceIndex) + const scope = this.findEntryByRange(pc, contractObj.contract.evm.deployedBytecode.functionDebugData) + console.log(pc, scope, contractObj.contract.evm.deployedBytecode.functionDebugData) + }*/ + + async getFunctionDebugData (vmtraceIndex, isCreation) { + const address = this.traceManager.getCurrentCalledAddressAt(vmtraceIndex) + const contractObj = await this.solidityProxy.contractObjectAtAddress(address) + const debugData = isCreation ? contractObj.contract.evm.bytecode.functionDebugData : contractObj.contract.evm.deployedBytecode.functionDebugData + const pc = this.traceManager.getCurrentPC(vmtraceIndex) + const key = this.findEntryByRange(pc, debugData) + return { key, value: debugData[key] } + } + + async printResolveLocals (vmtraceIndex) { + const address = this.traceManager.getCurrentCalledAddressAt(vmtraceIndex) + const sourceLocation = await this.extractValidSourceLocation(vmtraceIndex, address) + // this doesn't yet handle generated sources + const ast = await this.solidityProxy.ast(sourceLocation, null, address) + const nodes = nodesAtPositionForSourceLocation('', sourceLocation, { ast }) + const node = nodes.reverse().find((node) => { + return !!node.scope + }) + if (!node) { + console.log('node not found') + return + } + const nodesbyScope = getAllItemByScope(this, ast, this.astWalker) + const nodesForScope = nodesbyScope[node.scope] + const locals = getVariableDeclarationForScope(nodesForScope) + return { + firstStep: 0, + isCreation: false, + gasCost: 0, + lastStep: 0, + locals + } + } + + /** + * Find the entry in mapping whose range contains the given number. + * The range is defined between consecutive entryPoint values. + * + * @param {number} value - The number to search for + * @param {Object} mapping - Mapping of entries with entryPoint values + * @returns {string|null} The key of the matching entry, or null if not found + */ + findEntryByRange(value: number, mapping: { [key: string]: { entryPoint: number, id: number } }): string | null { + // Convert mapping to sorted array of [key, entryPoint] pairs + const entries = Object.entries(mapping) + .map(([key, obj]) => ({ key, entryPoint: obj.entryPoint })) + .sort((a, b) => a.entryPoint - b.entryPoint) + + // Find the entry whose range contains the value + for (let i = 0; i < entries.length; i++) { + const currentEntry = entries[i] + const nextEntry = entries[i + 1] + + // Check if value is in range [currentEntry.entryPoint, nextEntry.entryPoint) + if (value >= currentEntry.entryPoint) { + if (!nextEntry || value < nextEntry.entryPoint) { + return currentEntry.key + } + } + } + + return null } /** @@ -139,6 +221,9 @@ export class InternalCallTree { * @param {Int} vmtraceIndex - index on the vm trace */ findScope (vmtraceIndex) { + + this.printResolveLocals(vmtraceIndex) + let scopeId = this.findScopeId(vmtraceIndex) if (scopeId !== '' && !scopeId) return null let scope = this.scopes[scopeId] @@ -248,18 +333,33 @@ async function buildTree (tree, step, scopeId, isCreation, functionDefinition?, let previousValidSourceLocation = validSourceLocation || currentSourceLocation let compilationResult let currentAddress = '' + let currentFunctionDebugData while (step < tree.traceManager.trace.length) { let sourceLocation let validSourceLocation let address - + let isFunctionEntryPoint = false + let functionDebugData try { address = tree.traceManager.getCurrentCalledAddressAt(step) sourceLocation = await tree.extractSourceLocation(step, address) + functionDebugData = await tree.getFunctionDebugData(step, isCreation, currentFunctionDebugData) + + if (functionDebugData && functionDebugData.value && functionDebugData.value.id && !tree.processedFunctions.includes(functionDebugData.value.id)) { + currentFunctionDebugData = functionDebugData + tree.processedFunctions.push(functionDebugData.value.id) + console.log('found', functionDebugData.value.id) + isFunctionEntryPoint = true + } + + if (step === 92 || step === 93) { + console.log('start', sourceLocation) + } if (!includedSource(sourceLocation, currentSourceLocation)) { - tree.reducedTrace.push(step) + tree.reducedTrace.push(step) currentSourceLocation = sourceLocation + // if (step === 92 || step === 93) console.log('not include', currentSourceLocation) } if (currentAddress !== address) { compilationResult = await tree.solidityProxy.compilationResult(address) @@ -268,8 +368,10 @@ async function buildTree (tree, step, scopeId, isCreation, functionDefinition?, const amountOfSources = tree.sourceLocationTracker.getTotalAmountOfSources(address, compilationResult.data.contracts) if (tree.sourceLocationTracker.isInvalidSourceLocation(currentSourceLocation, amountOfSources)) { // file is -1 or greater than amount of sources validSourceLocation = previousValidSourceLocation - } else + } else { validSourceLocation = currentSourceLocation + if (step === 92 || step === 93) console.log('valid', validSourceLocation, currentSourceLocation) + } } catch (e) { return { outStep: step, error: 'InternalCallTree - Error resolving source location. ' + step + ' ' + e } @@ -277,6 +379,10 @@ async function buildTree (tree, step, scopeId, isCreation, functionDefinition?, if (!sourceLocation) { return { outStep: step, error: 'InternalCallTree - No source Location. ' + step } } + + if (step === 92 || step === 93) { + console.log('validSourceLocation', validSourceLocation, currentSourceLocation) + } const stepDetail: StepDetail = tree.traceManager.trace[step] const nextStepDetail: StepDetail = tree.traceManager.trace[step + 1] if (stepDetail && nextStepDetail) { @@ -300,7 +406,10 @@ async function buildTree (tree, step, scopeId, isCreation, functionDefinition?, } } + if (step === 92 || step === 93) console.log('bef offsetToLineColumn', validSourceLocation) + lineColumnPos = await tree.offsetToLineColumnConverter.offsetToLineColumn(validSourceLocation, validSourceLocation.file, sources, astSources) + if (step === 92 || step === 93) console.log('lineColumnPos', lineColumnPos) if (!tree.gasCostPerLine[validSourceLocation.file]) tree.gasCostPerLine[validSourceLocation.file] = {} if (!tree.gasCostPerLine[validSourceLocation.file][lineColumnPos.start.line]) { tree.gasCostPerLine[validSourceLocation.file][lineColumnPos.start.line] = { @@ -315,18 +424,34 @@ async function buildTree (tree, step, scopeId, isCreation, functionDefinition?, } } - tree.locationAndOpcodePerVMTraceIndex[step] = { sourceLocation, stepDetail, lineColumnPos, contractAddress: address } + if (step === 92 || step === 93) console.log('set locationAndOpcodePerVMTraceIndex', validSourceLocation, lineColumnPos, stepDetail) + tree.locationAndOpcodePerVMTraceIndex[step] = { sourceLocation: validSourceLocation, stepDetail, lineColumnPos, contractAddress: address } tree.scopes[scopeId].gasCost += stepDetail.gasCost const contractObj = await tree.solidityProxy.contractObjectAtAddress(address) const generatedSources = getGeneratedSources(tree, scopeId, contractObj) - const functionDefinition = await resolveFunctionDefinition(tree, sourceLocation, generatedSources, address) + // const functionDefinition = await resolveFunctionDefinition(tree, sourceLocation, generatedSources, address) const isInternalTxInstrn = isCallInstruction(stepDetail) const isCreateInstrn = isCreateInstruction(stepDetail) // we are checking if we are jumping in a new CALL or in an internal function - const constructorExecutionStarts = tree.pendingConstructorExecutionAt > -1 && tree.pendingConstructorExecutionAt < validSourceLocation.start + const ast = Object.entries(compilationResult.data.sources).find((entry) => { + return (entry[1] as any).id === validSourceLocation.file + }) + + let functionDefinition + tree.astWalker.walkFull((ast[1] as any).ast, (node) => { + if (functionDebugData.value && node.id === functionDebugData.value.id) { + functionDefinition = node + } + }) + // const nodesbyScope = getAllItemByScope(this, ast, this.astWalker) + + // const nodesForScope = nodesbyScope[functionDebugData.value.id] + //const locals = getVariableDeclarationForScope(nodesForScope) + + const constructorExecutionStarts = functionDebugData.key === 'constructor_' + contractObj.name // tree.pendingConstructorExecutionAt > -1 && tree.pendingConstructorExecutionAt < validSourceLocation.start if (functionDefinition && functionDefinition.kind === 'constructor' && tree.pendingConstructorExecutionAt === -1 && !tree.constructorsStartExecution[functionDefinition.id]) { tree.pendingConstructorExecutionAt = validSourceLocation.start tree.pendingConstructorId = functionDefinition.id @@ -334,7 +459,7 @@ async function buildTree (tree, step, scopeId, isCreation, functionDefinition?, // from now on we'll be waiting for a change in the source location which will mark the beginning of the constructor execution. // constructorsStartExecution allows to keep track on which constructor has already been executed. } - const internalfunctionCall = functionDefinition && previousSourceLocation.jump === 'i' + const internalfunctionCall = isFunctionEntryPoint if (constructorExecutionStarts || isInternalTxInstrn || internalfunctionCall) { try { const newScopeId = scopeId === '' ? subScope.toString() : scopeId + '.' + subScope @@ -525,3 +650,27 @@ function addParams (parameterList, tree, scopeId, states, contractObj, sourceLoc } return params } + +function getAllItemByScope (tree, ast, astWalker) { + if (Object.keys(tree.scopesMapping).length > 0) return tree.scopesMapping + astWalker.walkFull(ast, (node) => { + if (node.scope) { + if (!tree.scopesMapping[node.scope]) tree.scopesMapping[node.scope] = [] + tree.scopesMapping[node.scope].push(node) + } + }) + return tree.scopesMapping +} + +function getVariableDeclarationForScope (nodes) { + const ret = [] + nodes.filter((node) => { + if (node.nodeType === 'VariableDeclaration' || node.nodeType === 'YulVariableDeclaration') { + ret.push(node) + } + const hasChild = node.initialValue && (node.nodeType === 'VariableDeclarationStatement' || node.nodeType === 'YulVariableDeclarationStatement') + if (hasChild) ret.push(node.declarations) + }) + return ret +} + diff --git a/libs/remix-debug/src/solidity-decoder/internalCallTreeV2.ts b/libs/remix-debug/src/solidity-decoder/internalCallTreeV2.ts new file mode 100644 index 00000000000..de9d2ca66ec --- /dev/null +++ b/libs/remix-debug/src/solidity-decoder/internalCallTreeV2.ts @@ -0,0 +1,232 @@ +'use strict' +import { AstWalker } from '@remix-project/remix-astwalker' +import { nodesAtPosition } from '../source/sourceMappingDecoder' +import { SourceLocationTracker } from '../source/sourceLocationTracker' +import { EventManager } from '../eventManager' + +export type StepDetail = { + depth: number, + gas: number | string, + gasCost: number, + memory: number[], + op: string, + pc: number, + stack: number[], +} + +export type Scope = { + firstStep: number, + gasCost: number, + isCreation: boolean, + lastStep: number, + locals: Array +} + +/** + * Tree representing internal jump into function. + * Triggers `callTreeReady` event when tree is ready + * Triggers `callTreeBuildFailed` event when tree fails to build + * This use: + * - compilationResult.data.contracts['contracts/1_Storage.sol'].Storage.evm.deployedBytecode.functionDebugData + * - AST scope id + */ +export class InternalCallTree { + includeLocalVariables + debugWithGeneratedSources + event + solidityProxy + traceManager + sourceLocationTracker + scopes + scopeStarts + functionCallStack + functionDefinitionsByScope + variableDeclarationByFile + functionDefinitionByFile + astWalker + reducedTrace + locationAndOpcodePerVMTraceIndex: { + [Key: number]: any + } + gasCostPerLine + offsetToLineColumnConverter + pendingConstructorExecutionAt: number + pendingConstructorId: number + pendingConstructor + constructorsStartExecution + variables: { + [Key: number]: any + } + + scopesMapping: { + [Key: number]: any + } + + /** + * constructor + * + * @param {Object} debuggerEvent - event declared by the debugger (EthDebugger) + * @param {Object} traceManager - trace manager + * @param {Object} solidityProxy - solidity proxy + * @param {Object} codeManager - code manager + * @param {Object} opts - { includeLocalVariables, debugWithGeneratedSources } + */ + constructor (debuggerEvent, traceManager, solidityProxy, codeManager, opts, offsetToLineColumnConverter?) { + this.includeLocalVariables = opts.includeLocalVariables + this.debugWithGeneratedSources = opts.debugWithGeneratedSources + this.event = new EventManager() + this.solidityProxy = solidityProxy + this.traceManager = traceManager + this.offsetToLineColumnConverter = offsetToLineColumnConverter + this.sourceLocationTracker = new SourceLocationTracker(codeManager, { debugWithGeneratedSources: opts.debugWithGeneratedSources }) + debuggerEvent.register('newTraceLoaded', async (trace) => {}) + } + + /** + * reset tree + * + */ + reset () { + /* + scopes: map of scopes defined by range in the vmtrace {firstStep, lastStep, locals}. + Keys represent the level of deepness (scopeId) + scopeId : .. + */ + this.scopes = {} + /* + scopeStart: represent start of a new scope. Keys are index in the vmtrace, values are scopeId + */ + this.sourceLocationTracker.clearCache() + this.functionCallStack = [] + this.functionDefinitionsByScope = {} + this.scopeStarts = {} + this.gasCostPerLine = {} + this.variableDeclarationByFile = {} + this.functionDefinitionByFile = {} + this.astWalker = new AstWalker() + this.reducedTrace = [] + this.locationAndOpcodePerVMTraceIndex = {} + this.pendingConstructorExecutionAt = -1 + this.pendingConstructorId = -1 + this.constructorsStartExecution = {} + this.pendingConstructor = null + this.variables = {} + + this.scopesMapping = {} + } + + /** + * find the scope given @arg vmTraceIndex + * + * @param {Int} vmtraceIndex - index on the vm trace + */ + async findScope (vmtraceIndex): Promise { + const address = this.traceManager.getCurrentCalledAddressAt(vmtraceIndex) + const sourceLocation = await this.extractSourceLocation(vmtraceIndex, address) + const contractObj = await this.solidityProxy.contractObjectAtAddress(address) + // this doesn't yet handle generated sources + // const variables = await resolveVariableDeclaration(this, sourceLocation, null, address) + const ast = await this.solidityProxy.ast(sourceLocation, null, address) + const nodes = nodesAtPosition(null, sourceLocation, ast) + const node = nodes[nodes.length - 1] + const nodesForScope = getAllItemByScope(this, ast, this.astWalker, node.scope) + const locals = getVariableDeclarationForScope(nodesForScope) + return { + firstStep: 0, + isCreation: false, + gasCost: 0, + lastStep: 0, + locals + } + } + + async extractSourceLocation (step: number, address?: string) { + try { + if (!address) address = this.traceManager.getCurrentCalledAddressAt(step) + const compilationResult = await this.solidityProxy.compilationResult(address) + return await this.sourceLocationTracker.getSourceLocationFromVMTraceIndex(address, step, compilationResult.data.contracts) + } catch (error) { + throw new Error('InternalCallTree - Cannot retrieve sourcelocation for step ' + step + ' ' + error) + } + } + + async extractValidSourceLocation (step: number, address?: string) { + try { + if (!address) address = this.traceManager.getCurrentCalledAddressAt(step) + const compilationResult = await this.solidityProxy.compilationResult(address) + return await this.sourceLocationTracker.getValidSourceLocationFromVMTraceIndex(address, step, compilationResult.data.contracts) + } catch (error) { + throw new Error('InternalCallTree - Cannot retrieve valid sourcelocation for step ' + step + ' ' + error) + } + } + + async getValidSourceLocationFromVMTraceIndexFromCache (address: string, step: number, contracts: any) { + return await this.sourceLocationTracker.getValidSourceLocationFromVMTraceIndexFromCache(address, step, contracts, this.locationAndOpcodePerVMTraceIndex) + } + + async getGasCostPerLine(file: number, line: number) { + if (this.gasCostPerLine[file] && this.gasCostPerLine[file][line]) { + return this.gasCostPerLine[file][line] + } + throw new Error('Could not find gas cost per line') + } + + getLocalVariableById (id: number) { + return this.variables[id] + } +} + +function getGeneratedSources (tree, scopeId, contractObj) { + if (tree.debugWithGeneratedSources && contractObj && tree.scopes[scopeId]) { + return tree.scopes[scopeId].isCreation ? contractObj.contract.evm.bytecode.generatedSources : contractObj.contract.evm.deployedBytecode.generatedSources + } + return null +} + +// this extract all the variable declaration for a given ast and file +// and keep this in a cache +async function resolveVariableDeclaration (tree, sourceLocation, generatedSources, address) { + if (!tree.variableDeclarationByFile[sourceLocation.file]) { + const ast = await tree.solidityProxy.ast(sourceLocation, generatedSources, address) + if (ast) { + tree.variableDeclarationByFile[sourceLocation.file] = extractVariableDeclarations(ast, tree.astWalker) + } else { + return null + } + } + return tree.variableDeclarationByFile[sourceLocation.file][sourceLocation.start + ':' + sourceLocation.length + ':' + sourceLocation.file] +} + +function extractVariableDeclarations (ast, astWalker) { + const ret = {} + astWalker.walkFull(ast, (node) => { + if (node.nodeType === 'VariableDeclaration' || node.nodeType === 'YulVariableDeclaration') { + ret[node.src] = [node] + } + const hasChild = node.initialValue && (node.nodeType === 'VariableDeclarationStatement' || node.nodeType === 'YulVariableDeclarationStatement') + if (hasChild) ret[node.initialValue.src] = node.declarations + }) + return ret +} + +function getAllItemByScope (tree, ast, astWalker, scope) { + const ret = {} + astWalker.walkFull(ast, (node) => { + if (!tree.scopeMapping[node.scope]) tree.scopeMapping[node.scope] = [] + tree.scopesMapping[node.scope].push(node) + + }) + return ret +} + +function getVariableDeclarationForScope (nodes) { + const ret = [] + nodes.filter((node) => { + if (node.nodeType === 'VariableDeclaration' || node.nodeType === 'YulVariableDeclaration') { + ret.push(node) + } + const hasChild = node.initialValue && (node.nodeType === 'VariableDeclarationStatement' || node.nodeType === 'YulVariableDeclarationStatement') + if (hasChild) ret.push(node.declarations) + }) + return ret +} diff --git a/libs/remix-debug/src/solidity-decoder/solidityProxy.ts b/libs/remix-debug/src/solidity-decoder/solidityProxy.ts index 5cb1dc8bc9d..fca2f00c654 100644 --- a/libs/remix-debug/src/solidity-decoder/solidityProxy.ts +++ b/libs/remix-debug/src/solidity-decoder/solidityProxy.ts @@ -101,7 +101,7 @@ export class SolidityProxy { } /** - * get the AST of the file declare in the @arg sourceLocation + * get the AST of the file declared in the @arg sourceLocation * * @param {Object} sourceLocation - source location containing the 'file' to retrieve the AST from * @param {Object} generatedSources - compiler generated sources diff --git a/libs/remix-debug/src/source/sourceMappingDecoder.ts b/libs/remix-debug/src/source/sourceMappingDecoder.ts index 04b3bcb9ba4..03dc74a112b 100644 --- a/libs/remix-debug/src/source/sourceMappingDecoder.ts +++ b/libs/remix-debug/src/source/sourceMappingDecoder.ts @@ -130,6 +130,24 @@ function findNodeAtSourceLocation (astNodeType, sourceLocation, ast) { return found } +export function nodesAtPositionForSourceLocation (astNodeType, sourceLocation, ast) { + const astWalker = new AstWalker() + const found = [] + const callback = function (node) { + const nodeLocation = sourceLocationFromAstNode(node) + if (!nodeLocation) { + return + } + if (nodeLocation.start <= sourceLocation.start && nodeLocation.start + nodeLocation.length >= sourceLocation.start + sourceLocation.length) { + if (!astNodeType || astNodeType === node.nodeType) { + found.push(node) + } + } + } + astWalker.walkFull(ast.ast, callback) + return found +} + /** * get a list of nodes that are at the given @arg position * diff --git a/package.json b/package.json index e2f4cbe8ec6..c8fa4b14533 100644 --- a/package.json +++ b/package.json @@ -410,5 +410,6 @@ "@ethereumjs/util": "^10.0.0", "@ethereumjs/vm": "^10.0.0", "@ethereumjs/binarytree": "^10.0.0" - } + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" }