Skip to content

Commit c170d4d

Browse files
committed
Adding support for bytecode without source code debugging
1 parent 1f1d26c commit c170d4d

File tree

10 files changed

+173
-46
lines changed

10 files changed

+173
-46
lines changed

README.md

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,26 @@ and jump where it is necessary in a quick and graphical way.
1212
```
1313
edebugger-<VERSION>-SNAPSHOT.jar <OPTIONS>
1414
Options category 'mandatory':
15+
--address [-a] (a string; default: "")
16+
The Address of the contract the transaction is executed against. Cannot be
17+
set in combination with -f
1518
--node [-n] (a string; default: "")
16-
The node where the transaction was run. It must support debug_traceTransaction
19+
The node where the transaction was run. It must support
20+
debug_traceTransaction
1721
--source-file [-f] (a string; default: "")
18-
The source file of the contract the transaction is executed against
22+
The source file of the contract the transaction is executed against. Cannot
23+
be set in combination with -a
1924
--transaction-hash [-t] (a string; default: "")
2025
Transaction hash to debug
2126
2227
Options category 'optional':
2328
--d3-memory [-m] (a string; default: "537395200")
24-
D3 graph memory. If the graph is too large, you may want to increase this value (by multiplying it)
29+
D3 graph memory. If the graph is too large, you may want to increase this
30+
value (by multiplying it)
2531
--[no]only-trace [-o] (a boolean; default: "false")
26-
Exclude opcodes that are not executed in the transaction.
27-
This may help if the graph is too large and/or the opcodes not executed are not important
32+
Exclude opcodes that are not executed in the transaction when creating the
33+
graph. This may help if the graph is too large and the opcodes not executed
34+
are not important
2835
```
2936

3037
Example:
@@ -50,6 +57,7 @@ There are already tools that allow you to debug Ethereum transactions (Solidity)
5057
# Features
5158

5259
* It compiles the provided source code and draws a control flow graph (also imported contracts)
60+
* It supports debugging of transactions even if no source code is provided (option `-a`). It will draw the CFG and trace the transaction
5361
* From the provided transaction hash (using `debug_traceTransaction`) it gets the trace of the transaction
5462
* Combines the control flow graph with the execution trace, highlighting the executed instructions in red
5563
* Supports contracts calls. All contracts involved in the transaction can be debugged
@@ -63,7 +71,7 @@ For now there are many limitations since this is a very early release of the deb
6371
* The control flow graph is drawn from the static bytecode, so there can be nodes without all edges, a symbolic execution would be needed. Maybe will be added in future releases
6472
* Transactions executed in the runtime bytecode are supported (for example, the constructor execution of a contract cannot be debugged right now)
6573
* You must provide a node URL that supports `debug_traceTransaction`, like Geth or Ganache, therefore, Infura is not supported
66-
* It only supports Solidity for now, but planning to make it more modular to support different languages (if the compiler gives source mappings)
74+
* It only supports Solidity or bytecode for now, but planning to make it more modular to support different languages (if the compiler gives source mappings)
6775
* The interface is quite ugly, but usable
6876
* Not really a limitation, but the editor syntax highlighting is set to Javascript at the moment
6977
* Probably many more

src/main/java/net/nandgr/debugger/Arguments.java

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,21 @@ public class Arguments extends OptionsBase {
1111
@Option(
1212
name = "source-file",
1313
abbrev = 'f',
14-
help = "The source file of the contract the transaction is executed against",
14+
help = "The source file of the contract the transaction is executed against. Cannot be set in combination with -a",
1515
category = "mandatory",
1616
defaultValue = ""
1717
)
1818
public String sourceFile;
1919

20+
@Option(
21+
name = "address",
22+
abbrev = 'a',
23+
help = "The Address of the contract the transaction is executed against. Cannot be set in combination with -f",
24+
category = "mandatory",
25+
defaultValue = ""
26+
)
27+
public String address;
28+
2029
@Option(
2130
name = "node",
2231
abbrev = 'n',
@@ -54,12 +63,20 @@ public class Arguments extends OptionsBase {
5463
)
5564
public boolean onlyTraceOpcodes;
5665

57-
public void validate(OptionsParser parser) {
58-
if (sourceFile.isEmpty() || nodeUrl.isEmpty() || transactionHash.isEmpty()) {
66+
void validate(OptionsParser parser) {
67+
if (mandatoryParameters() || forbiddenCombinations()) {
5968
String execName = new File(Main.class.getProtectionDomain().getCodeSource().getLocation().getPath()).getName();
6069
System.out.println("Help: java -jar " + execName + " <OPTIONS>" + System.lineSeparator());
6170
System.out.println(parser.describeOptions(Collections.<String, String>emptyMap(), OptionsParser.HelpVerbosity.LONG));
6271
System.exit(0);
6372
}
6473
}
74+
75+
private boolean forbiddenCombinations() {
76+
return !sourceFile.isEmpty() && !address.isEmpty();
77+
}
78+
79+
private boolean mandatoryParameters() {
80+
return nodeUrl.isEmpty() || transactionHash.isEmpty() || (sourceFile.isEmpty() && address.isEmpty());
81+
}
6582
}

src/main/java/net/nandgr/debugger/Main.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,8 @@
33
import com.google.devtools.common.options.OptionsParser;
44
import net.nandgr.debugger.report.Report;
55
import net.nandgr.debugger.report.ReportException;
6-
import net.nandgr.debugger.transformers.ContractObject;
7-
import net.nandgr.debugger.transformers.SolidityTransformer;
8-
import net.nandgr.debugger.transformers.TransformException;
9-
import net.nandgr.debugger.transformers.Transformer;
6+
import net.nandgr.debugger.transformers.*;
7+
108
import java.util.List;
119

1210
public class Main {
@@ -16,12 +14,16 @@ public class Main {
1614
public static void main(String[] args){
1715

1816
parseArguments(args);
17+
Transformer transformer;
18+
if (!arguments.address.isEmpty()) {
19+
transformer = new ByteCodeTransformer(arguments.nodeUrl, arguments.transactionHash, arguments.address);
20+
} else {
21+
transformer = new SolidityTransformer(arguments.nodeUrl, arguments.transactionHash, arguments.sourceFile);
22+
}
1923

20-
// for now only solidity is supported
21-
Transformer solidityTransformer = new SolidityTransformer(arguments.nodeUrl, arguments.transactionHash);
2224
List<ContractObject> contracts = null;
2325
try {
24-
contracts = solidityTransformer.loadContracts(arguments.sourceFile);
26+
contracts = transformer.loadContracts();
2527
} catch (TransformException e) {
2628
e.printStackTrace();
2729
System.exit(0);

src/main/java/net/nandgr/debugger/cfg/CFGCreatorDefault.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,10 @@ public ContractBytecode createContractBytecode(List<OpcodeSource> contractOpcode
3232
int i = 0;
3333
for (; i < contractOpcodes.size() ; i++) {
3434
Opcode contractOpcode = contractOpcodes.get(i);
35-
if (contractOpcode.getOpcode().equals(Opcodes.CODECOPY)) {
36-
codeOffset = contractOpcodes.get(i-2).getParameter();
37-
}
35+
// TODO analyze this better, not needed here because constructor debugging is not supported yet
36+
// if (contractOpcode.getOpcode().equals(Opcodes.CODECOPY)) {
37+
// codeOffset = contractOpcodes.get(i-2).getParameter();
38+
// }
3839
if (contractOpcode.getOffset() != 0 && contractOpcode.getOffset() == codeOffset.intValue()) {
3940
constructorFound = true;
4041
List<OpcodeSource> constructorOpcodes = contractOpcodes.subList(0, i);

src/main/java/net/nandgr/debugger/disassembler/LinkedDisassembler.java

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,22 @@ public List<OpcodeSource> getOpcodeSources(List<Code> asmCode) throws Disassembl
1717
List<Opcode> opcodes = super.getOpcodes();
1818
asmCode.removeIf(elem -> elem.getName().equals("tag"));
1919

20-
if (opcodes.size()-1 != asmCode.size()) {
21-
// TODO move to a new exception somewhere
22-
throw new DisassemblerException("SolcOutput does not match with bytecode. Opcodes size: " + opcodes.size() + ", asm size: " + asmCode.size());
20+
if (!asmCode.isEmpty()) {
21+
if (opcodes.size()-1 != asmCode.size()) {
22+
throw new DisassemblerException("SolcOutput does not match with bytecode. Opcodes size: " + opcodes.size() + ", asm size: " + asmCode.size());
23+
}
2324
}
2425

2526
List<OpcodeSource> opcodeSources = new ArrayList<>();
26-
for (int i = 0; i < asmCode.size(); i++) {
27+
for (int i = 0; i < opcodes.size() - 1; i++) {
2728
OpcodeSource opcodeSource = new OpcodeSource(opcodes.get(i));
28-
Code currentAsmOpcode = asmCode.get(i);
29-
opcodeSource.setBegin(currentAsmOpcode.getBegin());
30-
opcodeSource.setEnd(currentAsmOpcode.getEnd());
29+
if (!asmCode.isEmpty()) {
30+
Code currentAsmOpcode = asmCode.get(i);
31+
opcodeSource.setBegin(currentAsmOpcode.getBegin());
32+
opcodeSource.setEnd(currentAsmOpcode.getEnd());
33+
}
3134
opcodeSources.add(opcodeSource);
35+
3236
}
3337

3438
return opcodeSources;

src/main/java/net/nandgr/debugger/node/NodeService.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ public NodeService(String nodeUrl) {
3131
this.nodeUrl = nodeUrl;
3232
}
3333

34-
public void populateTraceDataResponse(String txHash) throws IOException {
34+
public void populateTraceDataResponse(String address, String txHash) throws IOException {
3535
DebugTraceTransactionResponse debugTraceTransactionResponse = getDebugTraceTransactionResponse(txHash);
36-
populateAddressDebugTraceMap(EMPTY_ADDRESS, debugTraceTransactionResponse.getResult().getStructLogs());
36+
populateAddressDebugTraceMap(address, debugTraceTransactionResponse.getResult().getStructLogs());
3737
}
3838

3939
private void populateAddressDebugTraceMap(String address, List<DebugTraceTransactionLog> debugTrace) {
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package net.nandgr.debugger.transformers;
2+
3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import net.nandgr.debugger.cfg.CFGCreatorDefault;
6+
import net.nandgr.debugger.cfg.beans.BytecodeChunk;
7+
import net.nandgr.debugger.cfg.beans.ContractBytecode;
8+
import net.nandgr.debugger.cfg.beans.OpcodeSource;
9+
import net.nandgr.debugger.cfg.graphviz.GraphVizCreator;
10+
import net.nandgr.debugger.disassembler.DisassemblerException;
11+
import net.nandgr.debugger.disassembler.LinkedDisassembler;
12+
import net.nandgr.debugger.node.NodeService;
13+
import net.nandgr.debugger.node.response.json.DebugTraceTransactionLog;
14+
import net.nandgr.debugger.node.response.json.GetCodeResponse;
15+
16+
import java.io.IOException;
17+
import java.util.ArrayList;
18+
import java.util.Collections;
19+
import java.util.List;
20+
import java.util.Map;
21+
22+
public class ByteCodeTransformer implements Transformer {
23+
24+
private final String nodeUrl;
25+
private final String txHash;
26+
private final String address;
27+
28+
public ByteCodeTransformer(String nodeUrl, String txHash, String address) {
29+
this.nodeUrl = nodeUrl;
30+
this.txHash = txHash;
31+
this.address = address;
32+
}
33+
34+
@Override
35+
public List<ContractObject> loadContracts() throws TransformException {
36+
37+
ObjectMapper objectMapper = new ObjectMapper();
38+
NodeService nodeService = new NodeService(nodeUrl);
39+
Map<String, Map<Integer, DebugTraceTransactionLog>> traceData = null;
40+
41+
try {
42+
nodeService.populateTraceDataResponse(address, txHash);
43+
traceData = nodeService.getAddressTrace();
44+
} catch (IOException e) {
45+
throw new TransformException("Failed getting debug trace for hash: " + txHash, e);
46+
}
47+
48+
List<ContractObject> contracts = new ArrayList<>();
49+
for (Map.Entry<String, Map<Integer, DebugTraceTransactionLog>> stringMapEntry : traceData.entrySet()) {
50+
String contractAddress = stringMapEntry.getKey();
51+
String cName = stringMapEntry.getKey();
52+
List<OpcodeSource> opcodeSources = null;
53+
String code;
54+
try {
55+
GetCodeResponse contractCodeFromChain = nodeService.getContractCode(contractAddress);
56+
code = contractCodeFromChain.getResult();
57+
LinkedDisassembler disassembler = new LinkedDisassembler(code);
58+
opcodeSources = disassembler.getOpcodeSources(Collections.emptyList());
59+
} catch (IOException e) {
60+
throw new TransformException("Failed when getting code for contract: " + contractAddress + ", txHash: " + txHash, e);
61+
} catch (DisassemblerException e) {
62+
throw new TransformException("Failed when disassembling: " + contractAddress + ", txHash: " + txHash, e);
63+
}
64+
Map<Integer, DebugTraceTransactionLog> traceDataContract = traceData.get(contractAddress);
65+
String traceMapJson = null;
66+
try {
67+
traceMapJson = objectMapper.writeValueAsString(traceDataContract);
68+
} catch (JsonProcessingException e) {
69+
throw new TransformException("Failed when mapping trace data", e);
70+
}
71+
72+
CFGCreatorDefault cfgCreatorDefault = new CFGCreatorDefault();
73+
74+
ContractBytecode contractBytecode = cfgCreatorDefault.createContractBytecode(opcodeSources, traceDataContract);
75+
Map<Integer, BytecodeChunk> runtimeChunks = contractBytecode.getRuntime().getChunks();
76+
77+
GraphVizCreator graphVizCreator = new GraphVizCreator(runtimeChunks, traceDataContract, cName);
78+
String graph = graphVizCreator.buildStringGraph();
79+
80+
ContractObject contractObject = new ContractObject(cName, "", traceDataContract, contractAddress, code,
81+
traceMapJson, opcodeSources, null, graph);
82+
contracts.add(contractObject);
83+
}
84+
return contracts;
85+
}
86+
}

src/main/java/net/nandgr/debugger/transformers/SolidityTransformer.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,36 +28,38 @@ public class SolidityTransformer implements Transformer {
2828

2929
private final String nodeUrl;
3030
private final String txHash;
31+
private final String sourceFile;
3132

32-
public SolidityTransformer(String nodeUrl, String txHash) {
33+
public SolidityTransformer(String nodeUrl, String txHash, String sourceFile) {
3334
this.nodeUrl = nodeUrl;
3435
this.txHash = txHash;
36+
this.sourceFile = sourceFile;
3537
if (!Solc.checkSolcInClasspath()) {
3638
System.out.println("solc was not found in classpath");
3739
System.exit(0);
3840
}
3941
}
4042

4143
@Override
42-
public List<ContractObject> loadContracts(String sourceCodeFile) throws TransformException {
44+
public List<ContractObject> loadContracts() throws TransformException {
4345
ObjectMapper objectMapper = new ObjectMapper();
4446

4547
NodeService nodeService = new NodeService(nodeUrl);
4648

4749
Map<String, Map<Integer, DebugTraceTransactionLog>> traceData = null;
4850

4951
try {
50-
nodeService.populateTraceDataResponse(txHash);
52+
nodeService.populateTraceDataResponse(NodeService.EMPTY_ADDRESS, txHash);
5153
traceData = nodeService.getAddressTrace();
5254
} catch (IOException e) {
5355
throw new TransformException("Failed getting debug trace for hash: " + txHash, e);
5456
}
5557

56-
Path path = Paths.get(sourceCodeFile);
58+
Path path = Paths.get(sourceFile);
5759
String fileName = path.getFileName().toString();
5860
String contractName = fileName.substring(0, fileName.lastIndexOf("."));
5961

60-
Solc solc = new Solc(sourceCodeFile);
62+
Solc solc = new Solc(sourceFile);
6163
SolcOutput solcOutput = null;
6264
try {
6365
solcOutput = solc.compile();
@@ -72,7 +74,7 @@ public List<ContractObject> loadContracts(String sourceCodeFile) throws Transfor
7274
String contractPath = "";
7375
String cName = "";
7476
if (contractAddress.equals(NodeService.EMPTY_ADDRESS)) {
75-
contractPath = sourceCodeFile;
77+
contractPath = sourceFile;
7678
cName = contractName;
7779
} else {
7880
try {
@@ -98,7 +100,6 @@ public List<ContractObject> loadContracts(String sourceCodeFile) throws Transfor
98100
List<Code> asmCode = contract.getAsm().getData().get("0").getCode();
99101
String code = contract.getBinRuntime();
100102

101-
102103
LinkedDisassembler disassembler = new LinkedDisassembler(code);
103104
List<OpcodeSource> opcodeSources = null;
104105
try {

src/main/java/net/nandgr/debugger/transformers/Transformer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@
44

55
public interface Transformer {
66

7-
List<ContractObject> loadContracts(String sourceCodeFile) throws TransformException;
7+
List<ContractObject> loadContracts() throws TransformException;
88
}

src/main/resources/template/graph_template.html

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,14 @@
7979
}
8080

8181
.statemachine {
82-
margin-top: 400px;
82+
#if ($contracts[0].sourceCode)
83+
margin-top: 400px;
84+
height: 50%;
85+
#end
8386
font-size:14px;
8487
color:white;
8588
font-family: courier;
8689
padding: 10px;
87-
height: 50%;
8890
overflow-y: auto;
8991
}
9092

@@ -206,7 +208,9 @@
206208
pol1.classList.add("selectedPolygon");
207209
pol2.classList.add("selectedPolygon");
208210

209-
editorSelect(begin, end, contractName);
211+
#if ($contracts[0].sourceCode)
212+
editorSelect(begin, end, contractName);
213+
#end
210214
var trace = window["mapTrace-" + contractName][offset];
211215

212216
var stackElement = document.getElementById("stack-" + contractName);
@@ -284,13 +288,15 @@
284288

285289
#foreach ($contract in $contracts)
286290
render(dotSrc$contract.contractName, window["graphviz-$contract.contractName"], "$contract.contractName");
287-
window["editor-$contract.contractName"] = ace.edit("aceEditor-$contract.contractName");
288-
window["editor-$contract.contractName"].setTheme("ace/theme/monokai");
289-
window["editor-$contract.contractName"].setReadOnly(true);
290-
window["editor-$contract.contractName"].session.setMode("ace/mode/javascript");
291-
window["editor-$contract.contractName"].session.setOptions({useSoftTabs: false });
292-
window["editor-$contract.contractName"].getSession().setUseWorker(false);
293-
window["editor-$contract.contractName"].resize(true);
291+
#if ($contract.sourceCode)
292+
window["editor-$contract.contractName"] = ace.edit("aceEditor-$contract.contractName");
293+
window["editor-$contract.contractName"].setTheme("ace/theme/monokai");
294+
window["editor-$contract.contractName"].setReadOnly(true);
295+
window["editor-$contract.contractName"].session.setMode("ace/mode/javascript");
296+
window["editor-$contract.contractName"].session.setOptions({useSoftTabs: false });
297+
window["editor-$contract.contractName"].getSession().setUseWorker(false);
298+
window["editor-$contract.contractName"].resize(true);
299+
#end
294300
#end
295301

296302
function editorSelect(begin, end, contractName) {
@@ -354,7 +360,9 @@ <h4>Debug for transaction: $txHash</h4>
354360
<div id="tabs-$contract.contractName" class="tab">
355361
<div id="parent-$contract.contractName" class="parent">
356362
<div id="editor-$contract.contractName" class="editor">
363+
#if ($contract.sourceCode)
357364
<div id="aceEditor-$contract.contractName" class="aceEditor">$contract.sourceCode</div>
365+
#end
358366
<div id="statemachine-$contract.contractName" class="statemachine">
359367
<div id="gas-$contract.contractName" class="gas"></div>
360368
<h3>Stack</h3>

0 commit comments

Comments
 (0)