Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[isoltest] Add support for events. #10728

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions liblangutil/Common.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ inline bool isIdentifierPart(char c)
return isIdentifierStart(c) || isDecimalDigit(c);
}

inline bool isIdentifierPartWithDot(char c)
{
return isIdentifierPart(c) || c == '.';
}

inline int hexValue(char c)
{
if (c >= '0' && c <= '9')
Expand Down
36 changes: 36 additions & 0 deletions test/ExecutionFramework.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,25 @@ class ExecutionFramework
{

public:
struct log_record
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
struct log_record
struct LogRecord

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the same as in MockHost except for the index - do we need the duplication?

{
size_t index;
/// The address of the account which created the log.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of these comments are redundant. What I would like to know the meaning of is index. Is it its position in the transaction or the call?

util::h160 creator;

/// The data attached to the log.
bytes data;

/// The log topics.
std::vector<util::h256> topics;

/// Equal operator.
bool operator==(const log_record& other) const noexcept
{
return creator == other.creator && data == other.data && topics == other.topics;
}
};

ExecutionFramework();
ExecutionFramework(langutil::EVMVersion _evmVersion, std::vector<boost::filesystem::path> const& _vmPaths);
virtual ~ExecutionFramework() = default;
Expand Down Expand Up @@ -277,6 +296,23 @@ class ExecutionFramework
util::h160 logAddress(size_t _logIdx) const;
bytes logData(size_t _logIdx) const;

std::vector<log_record> recordedLogs() {
std::vector<log_record> logs{};
for (size_t logIdx = 0; logIdx < numLogs(); ++logIdx)
{
log_record record;
const auto& data = m_evmcHost->recorded_logs.at(logIdx).data;
record.index = logIdx;
record.data = bytes{data.begin(), data.end()};
record.creator = EVMHost::convertFromEVMC(m_evmcHost->recorded_logs.at(logIdx).creator);
for (size_t topicIdx = 0; topicIdx < numLogTopics(logIdx); ++topicIdx) {
record.topics.emplace_back(EVMHost::convertFromEVMC(m_evmcHost->recorded_logs.at(logIdx).topics.at(topicIdx)));
}
logs.emplace_back(record);
}
return logs;
}

langutil::EVMVersion m_evmVersion;
solidity::frontend::RevertStrings m_revertStrings = solidity::frontend::RevertStrings::Default;
solidity::frontend::OptimiserSettings m_optimiserSettings = solidity::frontend::OptimiserSettings::minimal();
Expand Down
217 changes: 208 additions & 9 deletions test/libsolidity/SemanticTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
*/

#include <test/libsolidity/SemanticTest.h>
#include <test/libsolidity/util/BytesUtils.h>
#include <libsolutil/Whiskers.h>
#include <libyul/Exceptions.h>
#include <test/Common.h>
Expand Down Expand Up @@ -46,6 +47,18 @@ SemanticTest::SemanticTest(string const& _filename, langutil::EVMVersion _evmVer
m_lineOffset(m_reader.lineNumber()),
m_enforceViaYul(enforceViaYul)
{
using namespace std::placeholders;
m_builtins
= {{"logs",
{
{"numLogs()", std::bind(&SemanticTest::numLogs, this, _1)},
{"numLogTopics(uint256)", std::bind(&SemanticTest::numLogTopics, this, _1)},
{"logTopic(uint256,uint256)", std::bind(&SemanticTest::logTopic, this, _1)},
{"logAddress(uint256)", std::bind(&SemanticTest::logAddress, this, _1)},
{"logData(uint256)", std::bind(&SemanticTest::logData, this, _1)},
{"expectEvent(uint256,string)", std::bind(&SemanticTest::expectEvent, this, _1)},
}}};

string choice = m_reader.stringSetting("compileViaYul", "default");
if (choice == "also")
{
Expand Down Expand Up @@ -130,6 +143,7 @@ TestCase::TestResult SemanticTest::runTest(ostream& _stream, string const& _line
selectVM(evmc_capabilities::EVMC_CAPABILITY_EVM1);

reset();
m_touchedLogs.clear();

m_compileViaYul = _compileViaYul;
if (_compileToEwasm)
Expand All @@ -150,6 +164,14 @@ TestCase::TestResult SemanticTest::runTest(ostream& _stream, string const& _line

bool constructed = false;

// Iterate through the test calls and set the previous call.
TestFunctionCall* previousCall{nullptr};
for (auto& test: m_tests)
{
test.setPreviousCall(previousCall);
previousCall = &test;
}

for (auto& test: m_tests)
{
if (constructed)
Expand Down Expand Up @@ -198,6 +220,15 @@ TestCase::TestResult SemanticTest::runTest(ostream& _stream, string const& _line
bytes output;
if (test.call().kind == FunctionCall::Kind::LowLevel)
output = callLowLevel(test.call().arguments.rawBytes(), test.call().value.value);
else if (test.call().kind == FunctionCall::Kind::Builtin)
{
std::vector<string> builtinPath;
boost::split(builtinPath, test.call().signature, boost::is_any_of("."));
assert(builtinPath.size() == 2);
auto builtin = m_builtins[builtinPath.front()][builtinPath.back()];
output = builtin(test.call());
test.setFailure(output.empty());
}
else
{
soltestAssert(
Expand All @@ -213,20 +244,49 @@ TestCase::TestResult SemanticTest::runTest(ostream& _stream, string const& _line
);
}

bool outputMismatch = (output != test.call().expectations.rawBytes());
// Pre byzantium, it was not possible to return failure data, so we disregard
// output mismatch for those EVM versions.
if (test.call().expectations.failure && !m_transactionSuccessful && !m_evmVersion.supportsReturndata())
outputMismatch = false;
if (m_transactionSuccessful != !test.call().expectations.failure || outputMismatch)
success = false;
bytes expectationOutput;
if (test.call().expectations.builtin)
{
std::vector<string> builtinPath;
boost::split(builtinPath, test.call().expectations.builtin->signature, boost::is_any_of("."));
assert(builtinPath.size() == 2);
auto builtin = m_builtins[builtinPath.front()][builtinPath.back()];
expectationOutput = builtin(*test.call().expectations.builtin);
}
else
expectationOutput = test.call().expectations.rawBytes();

bool outputMismatch = (output != expectationOutput);
if (test.call().kind == FunctionCall::Kind::Builtin)
{
if (outputMismatch)
success = false;
}
else
{
// Pre byzantium, it was not possible to return failure data, so we disregard
// output mismatch for those EVM versions.
if (test.call().expectations.failure && !m_transactionSuccessful && !m_evmVersion.supportsReturndata())
outputMismatch = false;

if (m_transactionSuccessful != !test.call().expectations.failure || outputMismatch)
success = false;

if (success)
success = checkLogs(test);

test.setFailure(!m_transactionSuccessful);
}

test.setFailure(!m_transactionSuccessful);
test.setRawBytes(std::move(output));
test.setContractABI(m_compiler.contractABI(m_compiler.lastContractName()));
}
}

TestFunctionCall fakeLastTestFunctionCall(FunctionCall{});
fakeLastTestFunctionCall.setPreviousCall(previousCall);
success &= checkLogs(fakeLastTestFunctionCall);

if (success && !m_runWithYul && _compileViaYul)
{
m_compileViaYulCanBeSet = true;
Expand Down Expand Up @@ -362,9 +422,148 @@ void SemanticTest::printUpdatedSettings(ostream& _stream, string const& _linePre
_stream << _linePrefix << "// " << setting.first << ": " << setting.second << endl;
}

bool SemanticTest::checkLogs(TestFunctionCall& _call)
{
_call.setLogs(SolidityExecutionFramework::recordedLogs());

TestFunctionCall* producer = _call.previousCall();
std::vector<FunctionCall const*> consumers{};
// Only non-builtins are able to produce logs.
// So lets search from the current call up to the first non-builtin.
while (producer != nullptr && producer->call().kind == FunctionCall::Kind::Builtin)
if (producer->previousCall() == nullptr)
break;
else
{
// On the way up to the producer we track all builtins that where on the way.
// Only builtins can consume logs, we store them in the consumers vector.
consumers.emplace_back(&producer->call());
producer = producer->previousCall();
}

// Producer will now point to the call that probably produced a log.
if (producer)
{
// We iterate through the consumers to find out what logs they have consumed.
for (auto& consumer: consumers)
for (auto logIdx: m_touchedLogs[consumer])
// All logs that where touched by the consumer, will be marked as
// touched within the producer.
touchLog(producer->call(), logIdx);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say let's rather check that the number is the same and they are equal individually.


// Finally we update the consumed logs within the producer.
for (auto& logIdx: m_touchedLogs[&producer->call()])
producer->consumedLogIndexes().insert(logIdx);

// It is ok if some builtins consumed log events.
return producer->consumedLogIndexes().size() >= producer->logs().size();
}
return true;
}

bytes SemanticTest::numLogs(FunctionCall const&)
{
// numLogs()
size_t result = SolidityExecutionFramework::numLogs();
bytes r = util::toBigEndian(u256{result});
return r;
}

bytes SemanticTest::numLogTopics(FunctionCall const& call)
{
// numLogTopics(uint256)
assert(call.arguments.parameters.size() == 1);
size_t logCount = SolidityExecutionFramework::numLogs();
// todo: hex strings not supported by lexical_cast<..>(..)
auto logIdx = lexical_cast<size_t>(call.arguments.parameters.front().rawString);
touchLog(call, logIdx);
if (logCount > 0 && logIdx < logCount)
return util::toBigEndian(u256{SolidityExecutionFramework::numLogTopics(logIdx)});
// empty result means failure.
return bytes{};
}

bytes SemanticTest::logTopic(FunctionCall const& call)
{
// logTopic(uint256,uint256)
assert(call.arguments.parameters.size() == 2);
auto logIdx = lexical_cast<size_t>(call.arguments.parameters.front().rawString);
touchLog(call, logIdx);
auto topicIdx = lexical_cast<size_t>(call.arguments.parameters.back().rawString);
size_t logCount = SolidityExecutionFramework::numLogs();
// todo: hex strings not supported by lexical_cast<..>(..)
if (logCount > 0 && logIdx < logCount)
{
size_t topicCount = SolidityExecutionFramework::numLogTopics(logIdx);
if (topicCount > 0 && topicIdx < topicCount)
return util::toBigEndian(u256{SolidityExecutionFramework::logTopic(logIdx, topicIdx)});
}
return{};
}

bytes SemanticTest::logAddress(FunctionCall const& call)
{
// logAddress(uint256)
assert(call.arguments.parameters.size() == 1);
size_t logCount = SolidityExecutionFramework::numLogs();
// todo: hex strings not supported by lexical_cast<..>(..)
auto logIdx = lexical_cast<size_t>(call.arguments.parameters.front().rawString);
touchLog(call, logIdx);
if (logCount > 0 && logIdx < logCount)
return util::toBigEndian(u256{u160{SolidityExecutionFramework::logAddress(logIdx)}});
// empty result means failure.
return bytes{};
}

bytes SemanticTest::logData(FunctionCall const& call)
{
// logData(uint256)
assert(call.arguments.parameters.size() == 1);
size_t logCount = SolidityExecutionFramework::numLogs();
// todo: hex strings not supported by lexical_cast<..>(..)
auto logIdx = lexical_cast<size_t>(call.arguments.parameters.front().rawString);
touchLog(call, logIdx);
if (logCount > 0 && logIdx < logCount)
return SolidityExecutionFramework::logData(logIdx);
// empty result means failure.
return bytes{};
}

bytes SemanticTest::expectEvent(FunctionCall const& call)
{
// expectEvent(uint256,string): logIdx, eventSignature
assert(call.arguments.parameters.size() == 2);
size_t logCount = SolidityExecutionFramework::numLogs();
// todo: hex strings not supported by lexical_cast<..>(..)
auto logIdx = lexical_cast<size_t>(call.arguments.parameters.front().rawString);
touchLog(call, logIdx);
auto logSignature = call.arguments.parameters.back().rawString;
assert(logSignature.length() >= 2);
logSignature = logSignature.substr(1, logSignature.length() - 2);
h256 logSignatureHash{util::keccak256(logSignature)};
if (logCount > 0 && logIdx < logCount)
{
vector<h256> topics;
size_t topicCount = SolidityExecutionFramework::numLogTopics(logIdx);
for (size_t topicIdx = 0; topicIdx < topicCount; ++topicIdx)
topics.push_back(SolidityExecutionFramework::logTopic(logIdx, topicIdx));
// remove topics[0], if the signature matches.
if (!topics.empty() && topics[0] == logSignatureHash)
topics.erase(topics.begin());
bytes result;
for (auto& topic : topics)
result += util::toBigEndian(topic);
result += SolidityExecutionFramework::logData(logIdx);
// todo: anonymous events with no data would be treated as error, maybe not that important.
return result;
}
// empty result means failure.
return bytes{};
}

void SemanticTest::parseExpectations(istream& _stream)
{
TestFileParser parser{_stream};
TestFileParser parser{_stream, &this->m_builtins};
auto functionCalls = parser.parseFunctionCalls(m_lineOffset);
std::move(functionCalls.begin(), functionCalls.end(), back_inserter(m_tests));
}
Expand Down
17 changes: 17 additions & 0 deletions test/libsolidity/SemanticTest.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,22 @@ class SemanticTest: public SolidityExecutionFramework, public EVMVersionRestrict
/// Compiles and deploys currently held source.
/// Returns true if deployment was successful, false otherwise.
bool deploy(std::string const& _contractName, u256 const& _value, bytes const& _arguments, std::map<std::string, solidity::test::Address> const& _libraries = {});

private:
bool checkLogs(TestFunctionCall& _call);

// logs builtins.
bytes numLogs(FunctionCall const& call);
bytes numLogTopics(FunctionCall const& call);
bytes logTopic(FunctionCall const& call);
bytes logAddress(FunctionCall const& call);
bytes logData(FunctionCall const& call);
bytes expectEvent(FunctionCall const& call);
void touchLog(FunctionCall const& call, size_t _logIdx)
{
m_touchedLogs[&call].insert(_logIdx);
}

TestResult runTest(std::ostream& _stream, std::string const& _linePrefix, bool _formatted, bool _compileViaYul, bool _compileToEwasm);
SourceMap m_sources;
std::size_t m_lineOffset;
Expand All @@ -70,6 +85,8 @@ class SemanticTest: public SolidityExecutionFramework, public EVMVersionRestrict
bool m_runWithABIEncoderV1Only = false;
bool m_allowNonExistingFunctions = false;
bool m_compileViaYulCanBeSet = false;
BuiltinFunctions m_builtins;
std::map<FunctionCall const*, std::set<size_t>> m_touchedLogs;
};

}
10 changes: 10 additions & 0 deletions test/libsolidity/semanticTests/builtins/smoke.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
contract ClientReceipt {
}
// ====
// compileViaYul: also
// ----
// logs.numLogs() -> logs.numLogs()
// logs.logAddress(uint256): 0 -> logs.logAddress(uint256): 0
// logs.logData(uint256): 0 -> logs.logData(uint256): 0
// logs.numLogTopics(uint256): 0 -> logs.numLogTopics(uint256): 0
// logs.logTopic(uint256,uint256): 0, 0 -> logs.logTopic(uint256,uint256): 0, 0
Loading