From 74e2935846db940500b7614bbef9bc1b484a860b Mon Sep 17 00:00:00 2001 From: emizzle Date: Thu, 13 Feb 2020 13:14:36 +1100 Subject: [PATCH] fix(@embark/profiler): Fix profile output and update messaging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- packages/core/console/src/index.ts | 8 +++--- packages/plugins/profiler/src/gasEstimator.js | 28 +++++++++++++------ packages/plugins/profiler/src/index.js | 13 +++++++-- 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/packages/core/console/src/index.ts b/packages/core/console/src/index.ts index 693d97b5f..abd4aab34 100644 --- a/packages/core/console/src/index.ts +++ b/packages/core/console/src/index.ts @@ -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) => { @@ -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); } diff --git a/packages/plugins/profiler/src/gasEstimator.js b/packages/plugins/profiler/src/gasEstimator.js index 32af6ab3a..bc7e34861 100644 --- a/packages/plugins/profiler/src/gasEstimator.js +++ b/packages/plugins/profiler/src/gasEstimator.js @@ -2,7 +2,10 @@ 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; @@ -10,6 +13,14 @@ class GasEstimator { 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 = {}; @@ -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); @@ -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; @@ -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; }); @@ -77,5 +91,3 @@ class GasEstimator { }); } } - -module.exports = GasEstimator; diff --git a/packages/plugins/profiler/src/index.js b/packages/plugins/profiler/src/index.js index 691c4c47e..c04eabcee 100644 --- a/packages/plugins/profiler/src/index.js +++ b/packages/plugins/profiler/src/index.js @@ -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) { @@ -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); }); }