From 5b8614f1788667e256b0445d85bdadc8b5c20c44 Mon Sep 17 00:00:00 2001 From: chriseth Date: Wed, 6 May 2015 10:43:59 +0200 Subject: [PATCH 1/4] Structural gas estimator. --- libevmasm/GasMeter.cpp | 104 ++++++++++++++++++++++++ libevmasm/GasMeter.h | 67 +++++++++++++++ libsolidity/ASTPrinter.cpp | 8 +- libsolidity/ASTPrinter.h | 8 +- libsolidity/ASTVisitor.h | 44 ++++++++++ libsolidity/StructuralGasEstimator.cpp | 108 +++++++++++++++++++++++++ libsolidity/StructuralGasEstimator.h | 59 ++++++++++++++ solc/CommandLineInterface.cpp | 18 ++++- test/GasMeter.cpp | 98 ++++++++++++++++++++++ 9 files changed, 510 insertions(+), 4 deletions(-) create mode 100644 libevmasm/GasMeter.cpp create mode 100644 libevmasm/GasMeter.h create mode 100644 libsolidity/StructuralGasEstimator.cpp create mode 100644 libsolidity/StructuralGasEstimator.h create mode 100644 test/GasMeter.cpp diff --git a/libevmasm/GasMeter.cpp b/libevmasm/GasMeter.cpp new file mode 100644 index 000000000..e5fb0e09a --- /dev/null +++ b/libevmasm/GasMeter.cpp @@ -0,0 +1,104 @@ +/* + This file is part of cpp-ethereum. + + cpp-ethereum is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + cpp-ethereum is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with cpp-ethereum. If not, see . +*/ +/** @file GasMeter.cpp + * @author Christian + * @date 2015 + */ + +#include "GasMeter.h" +#include + +using namespace std; +using namespace dev; +using namespace dev::eth; + +GasMeter::GasConsumption& GasMeter::GasConsumption::operator+=(GasConsumption const& _other) +{ + isInfinite = isInfinite || _other.isInfinite; + if (isInfinite) + return *this; + bigint v = bigint(value) + _other.value; + if (v > std::numeric_limits::max()) + isInfinite = true; + else + value = u256(v); + return *this; +} + +GasMeter::GasConsumption GasMeter::estimateMax(AssemblyItem const& _item) +{ + switch (_item.type()) { + case Push: + case PushTag: + return runGas(Instruction::PUSH1); + case Tag: + return runGas(Instruction::JUMPDEST); + case Operation: + { + GasConsumption gas = runGas(_item.instruction()); + switch (_item.instruction()) + { + case Instruction::SSTORE: + // @todo logic can be improved + gas += c_sstoreSetGas; + break; + case Instruction::SLOAD: + gas += c_sloadGas; + break; + case Instruction::MSTORE: + case Instruction::MSTORE8: + case Instruction::MLOAD: + case Instruction::RETURN: + case Instruction::SHA3: + case Instruction::CALLDATACOPY: + case Instruction::CODECOPY: + case Instruction::EXTCODECOPY: + case Instruction::LOG0: + case Instruction::LOG1: + case Instruction::LOG2: + case Instruction::LOG3: + case Instruction::LOG4: + case Instruction::CALL: + case Instruction::CALLCODE: + case Instruction::CREATE: + case Instruction::EXP: + // @todo logic can be improved + gas = GasConsumption::infinite(); + break; + default: + break; + } + return gas; + break; + } + default: + break; + } + + return GasConsumption::infinite(); +} + +GasMeter::GasConsumption GasMeter::runGas(Instruction _instruction) +{ + if (_instruction == Instruction::JUMPDEST) + return GasConsumption(1); + + int tier = instructionInfo(_instruction).gasPriceTier; + return tier == InvalidTier ? GasConsumption::infinite() : c_tierStepGas[tier]; +} + + diff --git a/libevmasm/GasMeter.h b/libevmasm/GasMeter.h new file mode 100644 index 000000000..63dbc1380 --- /dev/null +++ b/libevmasm/GasMeter.h @@ -0,0 +1,67 @@ +/* + This file is part of cpp-ethereum. + + cpp-ethereum is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + cpp-ethereum is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with cpp-ethereum. If not, see . +*/ +/** @file GasMeter.cpp + * @author Christian + * @date 2015 + */ + +#pragma once + +#include +#include + +namespace dev +{ +namespace eth +{ + +/** + * Class that helps computing the maximum gas consumption for instructions. + */ +class GasMeter +{ +public: + struct GasConsumption + { + GasConsumption(u256 _value = 0, bool _infinite = false): value(_value), isInfinite(_infinite) {} + static GasConsumption infinite() { return GasConsumption(0, true); } + + GasConsumption& operator+=(GasConsumption const& _otherS); + std::ostream& operator<<(std::ostream& _str) const; + + u256 value; + bool isInfinite; + }; + + /// Returns an upper bound on the gas consumed by the given instruction. + GasConsumption estimateMax(AssemblyItem const& _item); + +private: + static GasConsumption runGas(Instruction _instruction); +}; + +inline std::ostream& operator<<(std::ostream& _str, GasMeter::GasConsumption const& _consumption) +{ + if (_consumption.isInfinite) + return _str << "inf"; + else + return _str << _consumption.value; +} + + +} +} diff --git a/libsolidity/ASTPrinter.cpp b/libsolidity/ASTPrinter.cpp index 713059d38..f21ec5e37 100644 --- a/libsolidity/ASTPrinter.cpp +++ b/libsolidity/ASTPrinter.cpp @@ -30,8 +30,10 @@ namespace dev namespace solidity { -ASTPrinter::ASTPrinter(ASTNode const& _ast, string const& _source): - m_indentation(0), m_source(_source), m_ast(&_ast) +ASTPrinter::ASTPrinter(ASTNode const& _ast, + string const& _source, + StructuralGasEstimator::ASTGasConsumption const& _gasCosts +): m_indentation(0), m_source(_source), m_ast(&_ast), m_gasCosts(_gasCosts) { } @@ -503,6 +505,8 @@ void ASTPrinter::endVisit(Literal const&) void ASTPrinter::printSourcePart(ASTNode const& _node) { + if (m_gasCosts.count(&_node)) + *m_ostream << getIndentation() << " Gas costs: " << m_gasCosts.at(&_node) << endl; if (!m_source.empty()) { SourceLocation const& location(_node.getLocation()); diff --git a/libsolidity/ASTPrinter.h b/libsolidity/ASTPrinter.h index 9494bf8cb..dabf6470a 100644 --- a/libsolidity/ASTPrinter.h +++ b/libsolidity/ASTPrinter.h @@ -24,6 +24,7 @@ #include #include +#include namespace dev { @@ -38,7 +39,11 @@ class ASTPrinter: public ASTConstVisitor public: /// Create a printer for the given abstract syntax tree. If the source is specified, /// the corresponding parts of the source are printed with each node. - ASTPrinter(ASTNode const& _ast, std::string const& _source = std::string()); + ASTPrinter( + ASTNode const& _ast, + std::string const& _source = std::string(), + StructuralGasEstimator::ASTGasConsumption const& _gasCosts = {} + ); /// Output the string representation of the AST to _stream. void print(std::ostream& _stream); @@ -128,6 +133,7 @@ private: int m_indentation; std::string m_source; ASTNode const* m_ast; + StructuralGasEstimator::ASTGasConsumption m_gasCosts; std::ostream* m_ostream; }; diff --git a/libsolidity/ASTVisitor.h b/libsolidity/ASTVisitor.h index ec22bd443..fbda50791 100644 --- a/libsolidity/ASTVisitor.h +++ b/libsolidity/ASTVisitor.h @@ -23,6 +23,8 @@ #pragma once #include +#include +#include #include namespace dev @@ -218,5 +220,47 @@ protected: virtual void endVisitNode(ASTNode const&) { } }; +/** + * Utility class that visits the AST in depth-first order and calls a function on each node and each edge. + * Child nodes are only visited if the node callback of the parent returns true. + * The node callback of a parent is called before any edge or node callback involving the children. + * The edge callbacks of all children are called before the edge callback of the parent. + * This way, the node callback can be used as an initializing callback and the edge callbacks can be + * used to compute a "reduce" function. + */ +class ASTReduce: public ASTConstVisitor +{ +public: + /** + * Constructs a new ASTReduce object with the given callback functions. + * @param _onNode called for each node, before its child edges and nodes, should return true to descend deeper + * @param _onEdge called for each edge with (parent, child) + */ + ASTReduce( + std::function _onNode, + std::function _onEdge + ): m_onNode(_onNode), m_onEdge(_onEdge) + { + } + +protected: + bool visitNode(ASTNode const& _node) override + { + m_parents.push_back(&_node); + return m_onNode(_node); + } + void endVisitNode(ASTNode const& _node) override + { + m_parents.pop_back(); + if (!m_parents.empty()) + m_onEdge(*m_parents.back(), _node); + } + +private: + std::vector m_parents; + std::function m_onNode; + std::function m_onEdge; +}; + } } diff --git a/libsolidity/StructuralGasEstimator.cpp b/libsolidity/StructuralGasEstimator.cpp new file mode 100644 index 000000000..2cf8c2a81 --- /dev/null +++ b/libsolidity/StructuralGasEstimator.cpp @@ -0,0 +1,108 @@ +/* + This file is part of cpp-ethereum. + + cpp-ethereum is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + cpp-ethereum is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with cpp-ethereum. If not, see . +*/ +/** + * @author Christian + * @date 2015 + * Gas consumption estimator working alongside the AST. + */ + +#include "StructuralGasEstimator.h" +#include +#include +#include +#include + +using namespace std; +using namespace dev; +using namespace dev::eth; +using namespace dev::solidity; + +map StructuralGasEstimator::performEstimation( + AssemblyItems const& _items, + vector const& _ast +) +{ + map particularCosts; + GasMeter meter; + + for (auto const& item: _items) + particularCosts[item.getLocation()] += meter.estimateMax(item); + + map gasCosts; + auto onNode = [&](ASTNode const& _node) + { + gasCosts[&_node][0] = gasCosts[&_node][1] = particularCosts[_node.getLocation()]; + return true; + }; + auto onEdge = [&](ASTNode const& _parent, ASTNode const& _child) + { + gasCosts[&_parent][1] += gasCosts[&_child][1]; + }; + ASTReduce folder(onNode, onEdge); + for (ASTNode const* ast: _ast) + ast->accept(folder); + + return gasCosts; +} + +map StructuralGasEstimator::breakToStatementLevel( + map const& _gasCosts, + vector const& _roots +) +{ + // first pass: statementDepth[node] is the distance from the deepend statement to node + // in direction of the tree root (or undefined if not possible) + map statementDepth; + auto onNodeFirstPass = [&](ASTNode const& _node) + { + if (dynamic_cast(&_node)) + statementDepth[&_node] = 0; + return true; + }; + auto onEdgeFirstPass = [&](ASTNode const& _parent, ASTNode const& _child) + { + if (statementDepth.count(&_child)) + statementDepth[&_parent] = max(statementDepth[&_parent], statementDepth[&_child] + 1); + }; + ASTReduce firstPass(onNodeFirstPass, onEdgeFirstPass); + for (ASTNode const* node: _roots) + node->accept(firstPass); + + // we use the location of a node if + // - its statement depth is 0 or + // - its statement depth is undefined but the parent's statement depth is at least 1 + map gasCosts; + auto onNodeSecondPass = [&](ASTNode const& _node) + { + return statementDepth.count(&_node); + }; + auto onEdgeSecondPass = [&](ASTNode const& _parent, ASTNode const& _child) + { + bool useNode = false; + if (statementDepth.count(&_child)) + useNode = statementDepth[&_child] == 0; + else + useNode = statementDepth.count(&_parent) && statementDepth.at(&_parent) > 0; + if (useNode) + gasCosts[&_child] = _gasCosts.at(&_child)[1]; + }; + ASTReduce secondPass(onNodeSecondPass, onEdgeSecondPass); + for (ASTNode const* node: _roots) + node->accept(secondPass); + // gasCosts should only contain non-overlapping locations + return gasCosts; +} diff --git a/libsolidity/StructuralGasEstimator.h b/libsolidity/StructuralGasEstimator.h new file mode 100644 index 000000000..b9e976432 --- /dev/null +++ b/libsolidity/StructuralGasEstimator.h @@ -0,0 +1,59 @@ +/* + This file is part of cpp-ethereum. + + cpp-ethereum is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + cpp-ethereum is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with cpp-ethereum. If not, see . +*/ +/** + * @author Christian + * @date 2015 + * Gas consumption estimator working alongside the AST. + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace dev +{ +namespace solidity +{ + +class StructuralGasEstimator +{ +public: + using ASTGasConsumption = std::map; + + /// Estimates the gas consumption for every assembly item in the given assembly and stores + /// it by source location. + /// @returns a mapping from each AST node to a pair of its particular and syntactically accumulated gas costs. + std::map performEstimation( + eth::AssemblyItems const& _items, + std::vector const& _ast + ); + /// @returns a mapping from nodes with non-overlapping source locations to gas consumptions such that + /// the following source locations are part of the mapping: + /// 1. source locations of statements that do not contain other statements + /// 2. maximal source locations that do not overlap locations coming from the first rule + ASTGasConsumption breakToStatementLevel( + std::map const& _gasCosts, + std::vector const& _roots + ); +}; + +} +} diff --git a/solc/CommandLineInterface.cpp b/solc/CommandLineInterface.cpp index e6f03a2ef..944c8f68a 100644 --- a/solc/CommandLineInterface.cpp +++ b/solc/CommandLineInterface.cpp @@ -42,6 +42,7 @@ #include #include #include +#include using namespace std; namespace po = boost::program_options; @@ -464,6 +465,17 @@ void CommandLineInterface::handleAst(string const& _argStr) // do we need AST output? if (m_args.count(_argStr)) { + StructuralGasEstimator gasEstimator; + vector asts; + for (auto const& sourceCode: m_sourceCodes) + asts.push_back(&m_compiler->getAST(sourceCode.first)); + map gasCosts; + if (m_compiler->getRuntimeAssemblyItems()) + gasCosts = gasEstimator.breakToStatementLevel( + gasEstimator.performEstimation(*m_compiler->getRuntimeAssemblyItems(), asts), + asts + ); + auto choice = m_args[_argStr].as(); if (outputToStdout(choice)) { @@ -473,7 +485,11 @@ void CommandLineInterface::handleAst(string const& _argStr) cout << endl << "======= " << sourceCode.first << " =======" << endl; if (_argStr == g_argAstStr) { - ASTPrinter printer(m_compiler->getAST(sourceCode.first), sourceCode.second); + ASTPrinter printer( + m_compiler->getAST(sourceCode.first), + sourceCode.second, + gasCosts + ); printer.print(cout); } else diff --git a/test/GasMeter.cpp b/test/GasMeter.cpp new file mode 100644 index 000000000..0ffe41712 --- /dev/null +++ b/test/GasMeter.cpp @@ -0,0 +1,98 @@ +/* + This file is part of cpp-ethereum. + + cpp-ethereum is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + cpp-ethereum is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with cpp-ethereum. If not, see . +*/ +/** + * @author Christian + * @date 2015 + * Unit tests for the gas estimator. + */ + +#include +#include +#include +#include + +using namespace std; +using namespace dev::eth; +using namespace dev::solidity; + +namespace dev +{ +namespace solidity +{ +namespace test +{ + +class GasMeterTestFramework: public ExecutionFramework +{ +public: + GasMeterTestFramework() { } + void compile(string const& _sourceCode) + { + m_compiler.setSource(_sourceCode); + ETH_TEST_REQUIRE_NO_THROW(m_compiler.compile(), "Compiling contract failed"); + + StructuralGasEstimator estimator; + AssemblyItems const* items = m_compiler.getRuntimeAssemblyItems(""); + ASTNode const& sourceUnit = m_compiler.getAST(); + BOOST_REQUIRE(items != nullptr); + m_gasCosts = estimator.breakToStatementLevel( + estimator.performEstimation(*items, vector({&sourceUnit})), + {&sourceUnit} + ); + } + +protected: + dev::solidity::CompilerStack m_compiler; + map m_gasCosts; +}; + +BOOST_FIXTURE_TEST_SUITE(GasMeterTests, GasMeterTestFramework) + +BOOST_AUTO_TEST_CASE(non_overlapping_filtered_costs) +{ + char const* sourceCode = R"( + contract test { + bytes x; + function f(uint a) returns (uint b) { + x.length = a; + for (; a < 200; ++a) { + x[a] = 9; + b = a * a; + } + return f(a - 1); + } + } + )"; + compile(sourceCode); + for (auto first = m_gasCosts.cbegin(); first != m_gasCosts.cend(); ++first) + { + auto second = first; + for (++second; second != m_gasCosts.cend(); ++second) + if (first->first->getLocation().intersects(second->first->getLocation())) + { + BOOST_CHECK_MESSAGE(false, "Source locations should not overlap!"); + SourceReferenceFormatter::printSourceLocation(cout, first->first->getLocation(), m_compiler.getScanner()); + SourceReferenceFormatter::printSourceLocation(cout, second->first->getLocation(), m_compiler.getScanner()); + } + } +} + +BOOST_AUTO_TEST_SUITE_END() + +} +} +} From 4ab07c6dd3b36d036da40a24da32bab56dd7a79d Mon Sep 17 00:00:00 2001 From: chriseth Date: Wed, 6 May 2015 13:56:40 +0200 Subject: [PATCH 2/4] Use std::array. --- libsolidity/StructuralGasEstimator.cpp | 6 +++--- libsolidity/StructuralGasEstimator.h | 7 +++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/libsolidity/StructuralGasEstimator.cpp b/libsolidity/StructuralGasEstimator.cpp index 2cf8c2a81..c90981a72 100644 --- a/libsolidity/StructuralGasEstimator.cpp +++ b/libsolidity/StructuralGasEstimator.cpp @@ -31,7 +31,7 @@ using namespace dev; using namespace dev::eth; using namespace dev::solidity; -map StructuralGasEstimator::performEstimation( +StructuralGasEstimator::ASTGasConsumptionSelfAccumulated StructuralGasEstimator::performEstimation( AssemblyItems const& _items, vector const& _ast ) @@ -42,7 +42,7 @@ map StructuralGasEstimator::perform for (auto const& item: _items) particularCosts[item.getLocation()] += meter.estimateMax(item); - map gasCosts; + ASTGasConsumptionSelfAccumulated gasCosts; auto onNode = [&](ASTNode const& _node) { gasCosts[&_node][0] = gasCosts[&_node][1] = particularCosts[_node.getLocation()]; @@ -60,7 +60,7 @@ map StructuralGasEstimator::perform } map StructuralGasEstimator::breakToStatementLevel( - map const& _gasCosts, + ASTGasConsumptionSelfAccumulated const& _gasCosts, vector const& _roots ) { diff --git a/libsolidity/StructuralGasEstimator.h b/libsolidity/StructuralGasEstimator.h index b9e976432..df1ae509d 100644 --- a/libsolidity/StructuralGasEstimator.h +++ b/libsolidity/StructuralGasEstimator.h @@ -24,6 +24,7 @@ #include #include +#include #include #include #include @@ -37,11 +38,13 @@ class StructuralGasEstimator { public: using ASTGasConsumption = std::map; + using ASTGasConsumptionSelfAccumulated = + std::map>; /// Estimates the gas consumption for every assembly item in the given assembly and stores /// it by source location. /// @returns a mapping from each AST node to a pair of its particular and syntactically accumulated gas costs. - std::map performEstimation( + ASTGasConsumptionSelfAccumulated performEstimation( eth::AssemblyItems const& _items, std::vector const& _ast ); @@ -50,7 +53,7 @@ public: /// 1. source locations of statements that do not contain other statements /// 2. maximal source locations that do not overlap locations coming from the first rule ASTGasConsumption breakToStatementLevel( - std::map const& _gasCosts, + ASTGasConsumptionSelfAccumulated const& _gasCosts, std::vector const& _roots ); }; From c57305025fcd89ba7f569debb91b277f423d8cae Mon Sep 17 00:00:00 2001 From: chriseth Date: Wed, 6 May 2015 13:56:55 +0200 Subject: [PATCH 3/4] Style. --- libsolidity/ASTPrinter.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libsolidity/ASTPrinter.cpp b/libsolidity/ASTPrinter.cpp index f21ec5e37..0a170f8e1 100644 --- a/libsolidity/ASTPrinter.cpp +++ b/libsolidity/ASTPrinter.cpp @@ -30,7 +30,8 @@ namespace dev namespace solidity { -ASTPrinter::ASTPrinter(ASTNode const& _ast, +ASTPrinter::ASTPrinter( + ASTNode const& _ast, string const& _source, StructuralGasEstimator::ASTGasConsumption const& _gasCosts ): m_indentation(0), m_source(_source), m_ast(&_ast), m_gasCosts(_gasCosts) From 7c8de3f0ea14f47babd037947ec7235f9a9c1023 Mon Sep 17 00:00:00 2001 From: chriseth Date: Thu, 7 May 2015 11:05:05 +0200 Subject: [PATCH 4/4] Added assertions. --- libsolidity/StructuralGasEstimator.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libsolidity/StructuralGasEstimator.cpp b/libsolidity/StructuralGasEstimator.cpp index c90981a72..ececd7116 100644 --- a/libsolidity/StructuralGasEstimator.cpp +++ b/libsolidity/StructuralGasEstimator.cpp @@ -36,6 +36,7 @@ StructuralGasEstimator::ASTGasConsumptionSelfAccumulated StructuralGasEstimator: vector const& _ast ) { + solAssert(std::count(_ast.begin(), _ast.end(), nullptr) == 0, ""); map particularCosts; GasMeter meter; @@ -64,6 +65,7 @@ map StructuralGasEstimator::breakToSta vector const& _roots ) { + solAssert(std::count(_roots.begin(), _roots.end(), nullptr) == 0, ""); // first pass: statementDepth[node] is the distance from the deepend statement to node // in direction of the tree root (or undefined if not possible) map statementDepth;