diff --git a/circle.yml b/circle.yml index a73a41c3b6..c6d7d97166 100644 --- a/circle.yml +++ b/circle.yml @@ -336,6 +336,35 @@ jobs: - upload_coverage: flags: consensus + state-tests: + executor: consensus-tests + environment: + BUILD_TYPE: Coverage + CMAKE_OPTIONS: -DCMAKE_CXX_FLAGS=-Og + steps: + - build + - run: + # TODO: Merge with download_consensus_tests + name: "Download tests" + working_directory: ~/tests + command: | + git clone --depth=50 --single-branch https://github.com/ethereum/tests . + git checkout cde14b047d0d2549ed8fd46b4946aed0ec938cbf + git submodule init LegacyTests + git config -f .gitmodules submodule.LegacyTests.shallow true + git submodule update + - run: + name: "State tests" + working_directory: ~/build + command: bin/evmone-statetest ~/tests/GeneralStateTests + - run: + name: "State tests (legacy)" + working_directory: ~/build + command: bin/evmone-statetest ~/tests/LegacyTests/Constantinople/GeneralStateTests + - collect_coverage_gcc + - upload_coverage: + flags: statetests + gcc-min: executor: linux-gcc-min steps: @@ -487,6 +516,7 @@ workflows: ignore: /.*/ tags: only: /^v[0-9].*/ + - state-tests - consensus-tests - cmake-min - gcc-min diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index dd89a26e44..041a6f7521 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -7,6 +7,9 @@ include(${PROJECT_SOURCE_DIR}/evmc/cmake/EVMC.cmake) set(evmone_private_include_dir ${PROJECT_SOURCE_DIR}/lib) +hunter_add_package(GTest) +find_package(GTest CONFIG REQUIRED) + hunter_add_package(benchmark) find_package(benchmark CONFIG REQUIRED) @@ -15,9 +18,10 @@ add_subdirectory(bench) add_subdirectory(integration) add_subdirectory(internal_benchmarks) add_subdirectory(state) +add_subdirectory(statetest) add_subdirectory(unittests) -set(targets evmone-bench evmone-bench-internal evmone-unittests testutils) +set(targets evmone-bench evmone-bench-internal evmone-state evmone-statetest evmone-unittests testutils) if(EVMONE_FUZZING) add_subdirectory(fuzzer) diff --git a/test/state/CMakeLists.txt b/test/state/CMakeLists.txt index 1f79ca0c53..c80ccee9f4 100644 --- a/test/state/CMakeLists.txt +++ b/test/state/CMakeLists.txt @@ -14,4 +14,5 @@ target_sources( mpt_hash.hpp mpt_hash.cpp rlp.hpp + state.hpp ) diff --git a/test/state/account.hpp b/test/state/account.hpp index 3f7c16d3e0..452eb1f675 100644 --- a/test/state/account.hpp +++ b/test/state/account.hpp @@ -18,6 +18,9 @@ struct StorageValue { /// The current value. bytes32 current{}; + + /// The original value. + bytes32 original{}; }; /// The state account. diff --git a/test/state/state.hpp b/test/state/state.hpp new file mode 100644 index 0000000000..8b640a42e3 --- /dev/null +++ b/test/state/state.hpp @@ -0,0 +1,59 @@ +// evmone: Fast Ethereum Virtual Machine implementation +// Copyright 2022 The evmone Authors. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "account.hpp" +#include "hash_utils.hpp" +#include +#include +#include + +namespace evmone::state +{ +class State +{ + std::unordered_map m_accounts; + +public: + /// Creates new account under the address. + Account& create(const address& addr) + { + const auto r = m_accounts.insert({addr, {}}); + assert(r.second); + return r.first->second; + } +}; + +struct BlockInfo +{ + int64_t number = 0; + int64_t timestamp = 0; + int64_t gas_limit = 0; + address coinbase; + bytes32 prev_randao; + uint64_t base_fee = 0; +}; + +using AccessList = std::vector>>; + +struct Transaction +{ + enum class Kind + { + legacy, + eip1559 + }; + + Kind kind = Kind::legacy; + bytes data; + int64_t gas_limit; + intx::uint256 max_gas_price; + intx::uint256 max_priority_gas_price; + address sender; + std::optional
to; + intx::uint256 value; + AccessList access_list; +}; +} // namespace evmone::state diff --git a/test/statetest/.clang-tidy b/test/statetest/.clang-tidy new file mode 100644 index 0000000000..efc628c8e9 --- /dev/null +++ b/test/statetest/.clang-tidy @@ -0,0 +1,3 @@ +InheritParentConfig: true +Checks: > + -clang-analyzer-cplusplus.NewDeleteLeaks diff --git a/test/statetest/CMakeLists.txt b/test/statetest/CMakeLists.txt new file mode 100644 index 0000000000..20e9511e40 --- /dev/null +++ b/test/statetest/CMakeLists.txt @@ -0,0 +1,15 @@ +# evmone: Fast Ethereum Virtual Machine implementation +# Copyright 2022 The evmone Authors. +# SPDX-License-Identifier: Apache-2.0 + +hunter_add_package(nlohmann_json) +find_package(nlohmann_json CONFIG REQUIRED) + +add_executable(evmone-statetest) +target_link_libraries(evmone-statetest PRIVATE evmone evmone::state nlohmann_json::nlohmann_json GTest::gtest) +target_sources( + evmone-statetest PRIVATE + statetest.hpp + statetest.cpp + statetest_loader.cpp +) diff --git a/test/statetest/statetest.cpp b/test/statetest/statetest.cpp new file mode 100644 index 0000000000..9ef65537f0 --- /dev/null +++ b/test/statetest/statetest.cpp @@ -0,0 +1,50 @@ +// evmone: Fast Ethereum Virtual Machine implementation +// Copyright 2022 The evmone Authors. +// SPDX-License-Identifier: Apache-2.0 + +#include "statetest.hpp" +#include +#include + +namespace +{ +class StateTest : public testing::Test +{ + fs::path m_json_test_file; + +public: + explicit StateTest(fs::path json_test_file) noexcept + : m_json_test_file{std::move(json_test_file)} + {} + + void TestBody() final { evmone::test::load_state_test(m_json_test_file); } +}; +} // namespace + +int main(int argc, char* argv[]) +{ + testing::InitGoogleTest(&argc, argv); // Process GoogleTest flags. + + if (argc != 2) + { + std::cerr << "Missing argument with the path to the tests directory\n"; + return -1; + } + + std::vector test_files; + const fs::path root_test_dir{argv[1]}; + std::copy_if(fs::recursive_directory_iterator{root_test_dir}, + fs::recursive_directory_iterator{}, std::back_inserter(test_files), + [](const fs::directory_entry& entry) { + return entry.is_regular_file() && entry.path().extension() == ".json"; + }); + std::sort(test_files.begin(), test_files.end()); + for (const auto& p : test_files) + { + const auto d = fs::relative(p, root_test_dir); + testing::RegisterTest(d.parent_path().string().c_str(), d.stem().string().c_str(), nullptr, + nullptr, p.string().c_str(), 0, [p]() -> testing::Test* { return new StateTest(p); }); + } + + return RUN_ALL_TESTS(); +} diff --git a/test/statetest/statetest.hpp b/test/statetest/statetest.hpp new file mode 100644 index 0000000000..5930124e77 --- /dev/null +++ b/test/statetest/statetest.hpp @@ -0,0 +1,63 @@ +// evmone: Fast Ethereum Virtual Machine implementation +// Copyright 2022 The evmone Authors. +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "../state/state.hpp" +#include + +namespace fs = std::filesystem; + +namespace evmone::test +{ +struct TestMultiTransaction : state::Transaction +{ + struct Indexes + { + size_t input = 0; + size_t gas_limit = 0; + size_t value = 0; + }; + + std::vector access_lists; + std::vector inputs; + std::vector gas_limits; + std::vector values; + + [[nodiscard]] Transaction get(const Indexes& indexes) const noexcept + { + Transaction tx{*this}; + if (!access_lists.empty()) + tx.access_list = access_lists.at(indexes.input); + tx.data = inputs.at(indexes.input); + tx.gas_limit = gas_limits.at(indexes.gas_limit); + tx.value = values.at(indexes.value); + return tx; + } +}; + +struct StateTransitionTest +{ + struct Case + { + struct Expectation + { + TestMultiTransaction::Indexes indexes; + hash256 state_hash; + hash256 logs_hash; + bool exception = false; + }; + + evmc_revision rev; + std::vector expectations; + }; + + state::State pre_state; + state::BlockInfo block; + TestMultiTransaction multi_tx; + std::vector cases; +}; + +StateTransitionTest load_state_test(const fs::path& test_file); + +} // namespace evmone::test diff --git a/test/statetest/statetest_loader.cpp b/test/statetest/statetest_loader.cpp new file mode 100644 index 0000000000..1bfc566a77 --- /dev/null +++ b/test/statetest/statetest_loader.cpp @@ -0,0 +1,202 @@ +// evmone: Fast Ethereum Virtual Machine implementation +// Copyright 2022 The evmone Authors. +// SPDX-License-Identifier: Apache-2.0 + +#include "statetest.hpp" +#include +#include + +namespace evmone::test +{ +namespace json = nlohmann; +using evmc::from_hex; + +namespace +{ +template +T from_json(const json::json& j) = delete; + +template <> +int64_t from_json(const json::json& j) +{ + return static_cast(std::stoll(j.get(), nullptr, 16)); +} + +template <> +uint64_t from_json(const json::json& j) +{ + return static_cast(std::stoull(j.get(), nullptr, 16)); +} + +template <> +bytes from_json(const json::json& j) +{ + return from_hex(j.get()).value(); +} + +template <> +address from_json
(const json::json& j) +{ + return evmc::from_hex
(j.get()).value(); +} + +template <> +hash256 from_json(const json::json& j) +{ + return evmc::from_hex(j.get()).value(); +} + +template <> +intx::uint256 from_json(const json::json& j) +{ + const auto s = j.get(); + static constexpr std::string_view bigint_marker{"0x:bigint "}; + if (std::string_view{s}.substr(0, bigint_marker.size()) == bigint_marker) + return std::numeric_limits::max(); // Fake it + return intx::from_string(s); +} + +template <> +state::AccessList from_json(const json::json& j) +{ + state::AccessList o; + for (const auto& a : j) + { + std::vector storage_access_list; + for (const auto& storage_key : a.at("storageKeys")) + storage_access_list.emplace_back(from_json(storage_key)); + o.emplace_back(from_json
(a.at("address")), std::move(storage_access_list)); + } + return o; +} + +template <> +state::BlockInfo from_json(const json::json& j) +{ + const auto prev_randao_it = j.find("currentRandom"); + return { + from_json(j.at("currentNumber")), + from_json(j.at("currentTimestamp")), + from_json(j.at("currentGasLimit")), + from_json(j.at("currentCoinbase")), + from_json( + (prev_randao_it != j.end()) ? *prev_randao_it : j.at("currentDifficulty")), + from_json(j.value("currentBaseFee", std::string{"0"})), + }; +} + +evmc_revision to_rev(std::string_view s) +{ + if (s == "Frontier") + return EVMC_FRONTIER; + if (s == "Homestead") + return EVMC_HOMESTEAD; + if (s == "EIP150") + return EVMC_TANGERINE_WHISTLE; + if (s == "EIP158") + return EVMC_SPURIOUS_DRAGON; + if (s == "Byzantium") + return EVMC_BYZANTIUM; + if (s == "Constantinople") + return EVMC_CONSTANTINOPLE; + if (s == "ConstantinopleFix") + return EVMC_PETERSBURG; + if (s == "Istanbul") + return EVMC_ISTANBUL; + if (s == "Berlin") + return EVMC_BERLIN; + if (s == "London") + return EVMC_LONDON; + if (s == "Merge") + return EVMC_PARIS; + throw std::invalid_argument{"unknown revision: " + std::string{s}}; +} +} // namespace + +static void from_json(const json::json& j, TestMultiTransaction& o) +{ + if (j.contains("gasPrice")) + { + o.kind = state::Transaction::Kind::legacy; + o.max_gas_price = from_json(j.at("gasPrice")); + o.max_priority_gas_price = o.max_gas_price; + } + else + { + o.kind = state::Transaction::Kind::eip1559; + o.max_gas_price = from_json(j.at("maxFeePerGas")); + o.max_priority_gas_price = from_json(j.at("maxPriorityFeePerGas")); + } + o.sender = from_json(j.at("sender")); + if (!j.at("to").get().empty()) + o.to = from_json(j["to"]); + + for (const auto& j_data : j.at("data")) + o.inputs.emplace_back(from_json(j_data)); + + if (j.contains("accessLists")) + { + for (const auto& j_access_list : j["accessLists"]) + o.access_lists.emplace_back(from_json(j_access_list)); + } + + for (const auto& j_gas_limit : j.at("gasLimit")) + o.gas_limits.emplace_back(from_json(j_gas_limit)); + + for (const auto& j_value : j.at("value")) + o.values.emplace_back(from_json(j_value)); +} + +static void from_json(const json::json& j, TestMultiTransaction::Indexes& o) +{ + o.input = j.at("data").get(); + o.gas_limit = j.at("gas").get(); + o.value = j.at("value").get(); +} + +static void from_json(const json::json& j, StateTransitionTest::Case::Expectation& o) +{ + o.indexes = j.at("indexes").get(); + o.state_hash = from_json(j.at("hash")); + o.logs_hash = from_json(j.at("logs")); + o.exception = j.contains("expectException"); +} + +static void from_json(const json::json& j, StateTransitionTest& o) +{ + const auto& j_t = j.begin().value(); // Content is in a dict with the test name. + + for (const auto& [j_addr, j_acc] : j_t.at("pre").items()) + { + const auto addr = from_json
(j_addr); + auto& acc = o.pre_state.create(addr); + acc.balance = from_json(j_acc.at("balance")); + acc.nonce = from_json(j_acc.at("nonce")); + acc.code = from_json(j_acc.at("code")); + + for (const auto& [j_key, j_value] : j_acc.at("storage").items()) + { + auto& slot = acc.storage[from_json(j_key)]; + const auto value = from_json(j_value); + slot.original = value; + slot.current = value; + } + } + + o.multi_tx = j_t.at("transaction").get(); + + o.block = from_json(j_t.at("env")); + + for (const auto& [rev_name, expectations] : j_t.at("post").items()) + { + // TODO(c++20): Use emplace_back with aggregate initialization. + o.cases.push_back({to_rev(rev_name), + expectations.get>()}); + } +} + +StateTransitionTest load_state_test(const fs::path& test_file) +{ + return json::json::parse(std::ifstream{test_file}).get(); +} +} // namespace evmone::test diff --git a/test/unittests/CMakeLists.txt b/test/unittests/CMakeLists.txt index 5700254866..d0b93c8605 100644 --- a/test/unittests/CMakeLists.txt +++ b/test/unittests/CMakeLists.txt @@ -2,9 +2,6 @@ # Copyright 2018-2020 The evmone Authors. # SPDX-License-Identifier: Apache-2.0 -hunter_add_package(GTest) -find_package(GTest CONFIG REQUIRED) - # The internal evmone unit tests. The generic EVM ones are also built in. add_executable(evmone-unittests analysis_test.cpp