diff --git a/README.md b/README.md index daf986e..c9132d0 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,11 @@ There are already tools that allow you to debug Ethereum transactions (Solidity) # Usage +### Download + Use one of these releases: - * solc 0.4.24 compatible with ganache use: [v2.1.0](https://github.com/fergarrui/ethereum-graph-debugger/releases/tag/v2.1.0) + * solc 0.4.24 compatible with ganache use: [v2.2.0](https://github.com/fergarrui/ethereum-graph-debugger/releases/tag/v2.2.0) * solc 0.5.8 (not compatible with ganache) use: [v3.0.2](https://github.com/fergarrui/ethereum-graph-debugger/releases/tag/v3.0.2) If you want to use master (it can be more unstable), clone and start the application @@ -38,6 +40,20 @@ npm start Go to localhost:9090 +### Use + + * Go to localhost:9090 + * Enter the path where the contracts are in the input text (it will load Solidity contracts recursively) + * A tab per file found will be created + * Under a file tab there are a few actions using the left menu + +### How to debug bytecode (with no source code) [Experimental] + + * Create a file with extension `.evm` and paste the runtime bytecode (:warning: important: with `0x` as prefix) + * For example: create a file named: `contract1.evm` with content `0x60806040` + * Scan the directory as described above + * You won't get source code mappings when clicking in operations of the CFG + # Features * Now interactive :star2:: it has a sepparate frontend and API instead of building a static HTML file like in earlier versions @@ -49,6 +65,7 @@ Go to localhost:9090 * EVM state in transaction: it is shown below the editor when selecting an opcode present in the execution trace of the provided transaction hash * Settings: right now, there are settings to point to a different chain (by default it connects to http://127.0.0.1:8545) and basic authentication can be configured, so the RPC endpoint can be accessed if authentication is needed (to be compatible with platforms like [Kaleido](http://kaleido.io)) * When building the CFG a basic dynamic execution is made to calculate jumps and to remove most of orphan blocks (this will be improved in the future, probably with SymExec) + * To debug directly bytecode, use `.evm` extension files # Limitations/Considerations diff --git a/package.json b/package.json index 9b49d60..af53ac4 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "ethereum-graph-debugger", "author": "Fernando Garcia", "license": "GPL", - "version": "2.1.0", + "version": "2.2.0", "description": "Ethereum graph debugger", "main": "dist/run-server.js", "scripts": { diff --git a/src/api/bytecode/EVMDisassembler.ts b/src/api/bytecode/EVMDisassembler.ts index d699e73..fa5990c 100644 --- a/src/api/bytecode/EVMDisassembler.ts +++ b/src/api/bytecode/EVMDisassembler.ts @@ -41,14 +41,17 @@ export class EVMDisassembler implements Disassembler { } disassembleContract(bytecode: string): DisassembledContract { - let code = bytecode + let code = bytecode.trim() if (bytecode.startsWith('0x')) { code = bytecode.slice(2) } - + if (code.includes(EVMDisassembler.metadataPrefix)) { + code = code.split(EVMDisassembler.metadataPrefix)[0] + } + if (code.length % 2 !== 0) { - throw new Error(`Bad input, bytecode length not even: ${code}`) + throw new Error(`Bad input, bytecode length not even: ${code}, length: ${code.length}`) } const operations: Operation[] = this.disassembleBytecode(bytecode) @@ -70,7 +73,7 @@ export class EVMDisassembler implements Disassembler { } disassembleBytecode(bytecode: string): Operation[] { - let code = bytecode + let code = bytecode.trim() if (bytecode.startsWith('0x')) { code = bytecode.slice(2) @@ -79,9 +82,8 @@ export class EVMDisassembler implements Disassembler { if (code.includes(EVMDisassembler.metadataPrefix)) { code = code.split(EVMDisassembler.metadataPrefix)[0] } - if (code.length % 2 !== 0) { - throw new Error(`Bad input, bytecode length not even: ${code}`) + throw new Error(`Bad input, bytecode length not even: ${code}, length: ${code.length}`) } let offset = 0 const operations = code.match(/.{1,2}/g) diff --git a/src/api/cfg/CFGBlocks.ts b/src/api/cfg/CFGBlocks.ts index 088089c..0d67096 100644 --- a/src/api/cfg/CFGBlocks.ts +++ b/src/api/cfg/CFGBlocks.ts @@ -7,8 +7,22 @@ export class CFGBlocks { this.blocks[offset] = block } + // getBlock(offset: number): OperationBlock { + // return this.blocks[offset] + // } + get(offset: number): OperationBlock { - return this.blocks[offset] + const block: OperationBlock = this.blocks[offset] + if(!block) { + for (const key of Object.keys(this.blocks)) { + const b: OperationBlock = this.blocks[key] + const found = b.operations.find(op => op.offset === offset) + if (found) { + return b + } + } + } + return block } keys(): number[] { diff --git a/src/api/service/service/CFGService.ts b/src/api/service/service/CFGService.ts index d816e00..9b1edfd 100644 --- a/src/api/service/service/CFGService.ts +++ b/src/api/service/service/CFGService.ts @@ -18,8 +18,15 @@ export class CFGService { ) {} buildCFGFromSource(contractName: string, source: string, path: string): CFGContract { - const contract: DisassembledContract = this.disassembler.disassembleSourceCode(contractName, source, path) - return this.buildCfgContract(contract) + let contract: DisassembledContract + if(source.startsWith('0x')) { + const cfg: CFGContract = this.buildCFGFromBytecode(source) + cfg.contractRuntime.rawBytecode = source + return cfg + } else { + contract = this.disassembler.disassembleSourceCode(contractName, source, path) + return this.buildCfgContract(contract) + } } buildCFGFromBytecode(bytecode: string): CFGContract { diff --git a/src/api/service/service/FileServiceDefault.ts b/src/api/service/service/FileServiceDefault.ts index c82c045..1311602 100644 --- a/src/api/service/service/FileServiceDefault.ts +++ b/src/api/service/service/FileServiceDefault.ts @@ -8,7 +8,7 @@ var fs = require('fs') @injectable() export class FileServiceDefault implements FileService { async findContractssWithExtension(dir: string, extension: string): Promise { - const files = await recursive(dir, [`!*.${extension}`]) + const files = await recursive(dir, [`!*.{${extension},evm}`]) return await files .map(file => { diff --git a/src/api/symbolic/evm/EVMExecutor.ts b/src/api/symbolic/evm/EVMExecutor.ts index 0729eae..51e005c 100644 --- a/src/api/symbolic/evm/EVMExecutor.ts +++ b/src/api/symbolic/evm/EVMExecutor.ts @@ -11,6 +11,7 @@ export class EVMExecutor { evm: EVM blocks: CFGBlocks executor: OpcodeExecutor + alreadyRunOffsets: number[] = [] constructor(blocks: CFGBlocks, executor: OpcodeExecutor) { this.evm = new EVM() @@ -24,6 +25,7 @@ export class EVMExecutor { throw new Error(`Could not find block with offset ${offset}`) } this.runBlock(block) + this.alreadyRunOffsets.push(offset) const nextBlocks: OperationBlock[] = this.findNextBlocks(block) for (const nextBlock of nextBlocks) { if (block.childA !== nextBlock.offset && block.childB !== nextBlock.offset) { @@ -57,15 +59,17 @@ export class EVMExecutor { const jumpLocation = this.evm.nextJumpLocation this.evm.nextJumpLocation = undefined if (jumpLocation && !jumpLocation.isSymbolic) { - const locationBlock: OperationBlock = this.blocks.get(jumpLocation.value.toNumber()) - if (locationBlock) { + const nextOffset = jumpLocation.value.toNumber() + const locationBlock: OperationBlock = this.blocks.get(nextOffset) + if (locationBlock && !this.alreadyRunOffsets.includes(nextOffset)) { nextBlocks.push(locationBlock) } } } if (!this.NO_NEXT_BLOCK.includes(lastOp.opcode.name)) { - const nextBlock = this.blocks.get(lastOp.offset + 1) - if (nextBlock) { + const nextOffset = lastOp.offset + 1 + const nextBlock = this.blocks.get(nextOffset) + if (nextBlock && !this.alreadyRunOffsets.includes(nextOffset)) { nextBlocks.push(nextBlock) } } diff --git a/src/client/components/Graph/main.js b/src/client/components/Graph/main.js index eff67eb..fdda3c8 100644 --- a/src/client/components/Graph/main.js +++ b/src/client/components/Graph/main.js @@ -27,7 +27,7 @@ class ConnectedGraph extends React.Component { componentDidMount() { const { cfg, graphId, graphType } = this.props; - const graphclass = graphId.replace('.sol', ''); + const graphclass = graphId.replace('.sol', '').replace('.evm', ''); const graphviz = d3.select(`.graph--${graphclass}--${graphType}`).graphviz() graphviz.totalMemory(537395200) graphviz.renderDot(cfg); @@ -71,7 +71,7 @@ class ConnectedGraph extends React.Component { render() { const { cfg, graphId, graphType } = this.props; - const graphclass = `${graphId.replace('.sol', '')}--${graphType}`; + const graphclass = `${graphId.replace('.sol', '').replace('.evm', '')}--${graphType}`; return (
diff --git a/src/client/components/Tab/TabPanel/main.js b/src/client/components/Tab/TabPanel/main.js index a10bc90..3f9f505 100644 --- a/src/client/components/Tab/TabPanel/main.js +++ b/src/client/components/Tab/TabPanel/main.js @@ -140,7 +140,7 @@ class ConnectedTabPanel extends React.Component { }); const params = { - name: name.replace('.sol', ''), + name: name.replace('.sol', '').replace('.evm', ''), path: encodeURIComponent(path), source: encodeURIComponent(code), blockchainHost: localStorage.getItem('host'), @@ -180,7 +180,7 @@ class ConnectedTabPanel extends React.Component { const { name, path, code } = this.props; const params = { - name: name.replace('.sol', ''), + name: name.replace('.sol', '').replace('.evm', ''), path: encodeURIComponent(path), source: encodeURIComponent(code), 'constructor': 'false' @@ -194,7 +194,7 @@ class ConnectedTabPanel extends React.Component { const { name, code, path } = this.props; const params = { - name: name.replace('.sol', ''), + name: name.replace('.sol', '').replace('.evm', ''), path: encodeURIComponent(path), source: encodeURIComponent(code) } diff --git a/src/routes.ts b/src/routes.ts index 1ab1fc0..f83ceb3 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -66,7 +66,7 @@ const models: TsoaRoute.Models = { }, "Storage": { "properties": { - "storage": { "dataType": "any", "required": true }, + "storage": { "dataType": "any", "default": {} }, }, }, };