-
Notifications
You must be signed in to change notification settings - Fork 6k
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
Changes from all commits
557b172
aebb990
9eb87b0
54ab990
3847073
d6375bd
a8c179d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -51,6 +51,25 @@ class ExecutionFramework | |
{ | ||
|
||
public: | ||
struct log_record | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
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; | ||
|
@@ -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(); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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> | ||
|
@@ -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") | ||
{ | ||
|
@@ -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) | ||
|
@@ -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) | ||
|
@@ -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( | ||
|
@@ -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; | ||
|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)); | ||
} | ||
|
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.