Skip to content

Commit

Permalink
fix(@embark/profiler): Fix profile output and update messaging
Browse files Browse the repository at this point in the history
The profiler was not formatted correctly in the console as `util.inspect` was being applied to the ASCII table before being output to the console REPL.

In addition, functions containing solidity assertions (require, revert, assert) that cause the function to fail when estimating gas would print an error to embark’s console log, and would show nothing as their gas estimate in the table.

Do not `util.inspect` command output if the result is a string. For API commands being run, allow the command to specify whether or not the output of the command should be HTML escaped. This could pose security risks!

For functions that have errors during gas estimation, add a message in the embark console explaining that the error may be due to solidity assertions in the function that prevent the gas from being estimated correctly. For functions that error, show `-ERROR-` in the gas estimation column. Additionally, show a description in the table footer explaining that the error may be due to solidity assertions in the function.

For events with no gas estimate, show `-EVENT-` in the gas estimate column of the profile table, and a description in the table footer explaining that there is no gas estimate for events.

### Warnings
This PR allows the console command to specify whether or not it should allow for a string result of the command to be HTML-escaped before being sent in the API response. Combining this with Cockpit’s `dangerouslySetInnerHTML`, this could allow a plugin to register a console command that injects XSS in to Cockpit.

![Imgur](https://i.imgur.com/1Rqkjyx.png)
![Imgur](https://i.imgur.com/s6Y1Ecy.png)
![Imgur](https://i.imgur.com/BhsjkBs.png)
  • Loading branch information
emizzle authored and iurimatias committed Feb 26, 2020
1 parent 1e9ed81 commit 74e2935
Show file tree
Hide file tree
Showing 3 changed files with 35 additions and 14 deletions.
8 changes: 4 additions & 4 deletions packages/core/console/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export default class Console {
const error = { name: "Console error", message: err, stack: err.stack };
return cb(error);
}
cb(null, util.inspect(result));
cb(null, typeof result !== "string" ? util.inspect(result) : result);
});
});
this.ipc.on("console:executePartial", (cmd: string, cb: any) => {
Expand Down Expand Up @@ -93,18 +93,18 @@ export default class Console {
private registerApi() {
const plugin = this.plugins.createPlugin("consoleApi", {});
plugin.registerAPICall("post", "/embark-api/command", (req: any, res: any) => {
this.executeCmd(req.body.command, (err: any, result: any) => {
this.executeCmd(req.body.command, (err: any, result: any, shouldEscapeHtml = true) => {
if (err) {
return res.send({ result: err.message || err });
}
let response = result;
if (typeof result !== "string") {
response = stringify(result, jsonFunctionReplacer, 2);
} else {
} else if (shouldEscapeHtml) {
// Avoid HTML injection in the Cockpit
response = escapeHtml(response);
}
const jsonResponse = {result: response};
const jsonResponse = { result: response };
if (res.headersSent) {
return res.end(jsonResponse);
}
Expand Down
28 changes: 20 additions & 8 deletions packages/plugins/profiler/src/gasEstimator.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,25 @@ const async = require('async');
const ContractFuzzer = require('./fuzzer.js');
const Web3 = require('web3');

class GasEstimator {
export const GAS_ERROR = ' -ERROR- ';
export const EVENT_NO_GAS = ' -EVENT- ';

export class GasEstimator {
constructor(embark) {
this.embark = embark;
this.logger = embark.logger;
this.events = embark.events;
this.fuzzer = new ContractFuzzer(embark);
}

printError(message, name, values = []) {
this.logger.error(`Error getting gas estimate for "${name}(${Object.values(values).join(",")})"`, message);
if (message.includes('always failing transaction')) {
this.logger.error(`This may mean function assertions (revert, assert, require) are preventing the estimate from completing. Gas will be listed as "${GAS_ERROR}" in the profile.`);
}
this.logger.error(''); // new line to separate likely many lines
}

estimateGas(contractName, cb) {
const self = this;
let gasMap = {};
Expand All @@ -19,13 +30,16 @@ class GasEstimator {
if (err) return cb(err);
let fuzzMap = self.fuzzer.generateFuzz(3, contract);
let contractObj = new web3.eth.Contract(contract.abiDefinition, contract.deployedAddress);
async.each(contract.abiDefinition.filter((x) => x.type !== "event"),
async.each(contract.abiDefinition,
(abiMethod, gasCb) => {
let name = abiMethod.name;
if (abiMethod.type === "constructor") {
// already provided for us
gasMap['constructor'] = parseFloat(contract.gasEstimates.creation.totalCost.toString());
return gasCb(null, name, abiMethod.type);
} else if (abiMethod.type === "event") {
gasMap[name] = EVENT_NO_GAS;
return gasCb(null, name, abiMethod.type);
} else if (abiMethod.type === "fallback") {
gasMap['fallback'] = parseFloat(contract.gasEstimates.external[""].toString());
return gasCb(null, name, abiMethod.type);
Expand All @@ -35,9 +49,9 @@ class GasEstimator {
// just run it and register it
contractObj.methods[name]
.apply(contractObj.methods[name], [])
.estimateGas((err, gasAmount) => {
.estimateGas({ from: web3.eth.defaultAccount }, (err, gasAmount) => {
if (err) {
self.logger.error(`Error getting gas estimate for "${name}"`, err.message || err);
self.printError(err.message || err, name);
return gasCb(null, name, abiMethod.type);
}
gasMap[name] = gasAmount;
Expand All @@ -49,13 +63,13 @@ class GasEstimator {
contractObj.methods[name].apply(contractObj.methods[name], values)
.estimateGas((err, gasAmount) => {
if (err) {
self.logger.error(`Error getting gas estimate for "${name}"`, err.message || err);
self.printError(err.message || err, name, values);
}
getVarianceCb(null, gasAmount);
});
}, (err, variance) => {
if (variance.every(v => v === variance[0])) {
gasMap[name] = variance[0];
gasMap[name] = variance[0] ?? GAS_ERROR;
} else {
// get average
let sum = variance.reduce(function(memo, num) { return memo + num; });
Expand All @@ -77,5 +91,3 @@ class GasEstimator {
});
}
}

module.exports = GasEstimator;
13 changes: 11 additions & 2 deletions packages/plugins/profiler/src/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { warnIfPackageNotDefinedLocally } from 'embark-utils';

const asciiTable = require('ascii-table');
const GasEstimator = require('./gasEstimator.js');
import { GasEstimator, GAS_ERROR, EVENT_NO_GAS } from './gasEstimator';

class Profiler {
constructor(embark, _options) {
Expand Down Expand Up @@ -60,10 +60,19 @@ class Profiler {

let table = new asciiTable(contractName);
table.setHeading('Function', 'Payable', 'Mutability', 'Inputs', 'Outputs', 'Gas Estimates');
table.setAlign(5, asciiTable.RIGHT);
profileObj.methods.forEach((method) => {
table.addRow(method.name, method.payable, method.mutability, self.formatParams(method.inputs), self.formatParams(method.outputs), method.gasEstimates);
});
return returnCb(null, table.toString());
const strTable = table.toString();
let result = [strTable];
if (strTable.includes(GAS_ERROR)) {
result.push(`${GAS_ERROR} indicates there was an error during gas estimation (see console for details).`);
}
if (strTable.includes(EVENT_NO_GAS)) {
result.push(`${EVENT_NO_GAS} indicates the method is an event, and therefore no gas was estimated.`);
}
return returnCb(null, result.join('\n'), false);
});
}

Expand Down

0 comments on commit 74e2935

Please sign in to comment.