Skip to content

Commit

Permalink
test: Introduce TestState and TestAccount
Browse files Browse the repository at this point in the history
This decouples the `state::State` for transaction execution from
the state object for test definitions and asserts.

The `state::State` (also called "intra state") has some constructs
and data related to transaction execution only (like "current" and
"original" storage values). These should not be exposed to tests
and make test definitions more complicated.

This separation should also help with adding new public API
for EVM with transaction-level execution granularity.
  • Loading branch information
chfast committed Jun 6, 2024
1 parent 189bbaf commit 924b6b8
Show file tree
Hide file tree
Showing 21 changed files with 369 additions and 313 deletions.
6 changes: 3 additions & 3 deletions test/blockchaintest/blockchaintest.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

#include "../state/bloom_filter.hpp"
#include "../state/state.hpp"
#include "../state/test_state.hpp"
#include "../utils/utils.hpp"
#include <evmc/evmc.hpp>
#include <span>
Expand Down Expand Up @@ -43,7 +44,6 @@ struct BlockHeader
struct TestBlock
{
state::BlockInfo block_info;
state::State pre_state;
std::vector<state::Transaction> transactions;

BlockHeader expected_block_header;
Expand All @@ -54,14 +54,14 @@ struct BlockchainTest
struct Expectation
{
hash256 last_block_hash;
std::variant<state::State, hash256> post_state;
std::variant<TestState, hash256> post_state;
};

std::string name;

std::vector<TestBlock> test_blocks;
BlockHeader genesis_block_header;
state::State pre_state;
TestState pre_state;
RevisionSchedule rev;

Expectation expectation;
Expand Down
4 changes: 2 additions & 2 deletions test/blockchaintest/blockchaintest_loader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ BlockchainTest load_blockchain_test_case(const std::string& name, const json::js
BlockchainTest bt;
bt.name = name;
bt.genesis_block_header = from_json<BlockHeader>(j.at("genesisBlockHeader"));
bt.pre_state = from_json<State>(j.at("pre"));
bt.pre_state = from_json<TestState>(j.at("pre"));
bt.rev = to_rev_schedule(j.at("network").get<std::string>());

for (const auto& el : j.at("blocks"))
Expand All @@ -128,7 +128,7 @@ BlockchainTest load_blockchain_test_case(const std::string& name, const json::js
bt.expectation.last_block_hash = from_json<hash256>(j.at("lastblockhash"));

if (const auto it = j.find("postState"); it != j.end())
bt.expectation.post_state = from_json<State>(*it);
bt.expectation.post_state = from_json<TestState>(*it);
else if (const auto it_hash = j.find("postStateHash"); it_hash != j.end())
bt.expectation.post_state = from_json<hash256>(*it_hash);

Expand Down
42 changes: 18 additions & 24 deletions test/blockchaintest/blockchaintest_runner.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ TransitionResult apply_block(state::State& state, evmc::VM& vm, const state::Blo
cumulative_gas_used += receipt.gas_used;
receipt.cumulative_gas_used = cumulative_gas_used;
if (rev < EVMC_BYZANTIUM)
receipt.post_state = state::mpt_hash(state.get_accounts());
receipt.post_state = state::mpt_hash(TestState{state});

block_gas_left -= receipt.gas_used;
blob_gas_left -= tx.blob_gas_used();
Expand All @@ -91,13 +91,11 @@ std::optional<int64_t> mining_reward(evmc_revision rev) noexcept
return std::nullopt;
}

std::string print_state(const state::State& s)
std::string print_state(const TestState& s)

Check warning on line 94 in test/blockchaintest/blockchaintest_runner.cpp

View check run for this annotation

Codecov / codecov/patch

test/blockchaintest/blockchaintest_runner.cpp#L94

Added line #L94 was not covered by tests
{
std::stringstream out;
const std::map<address, state::Account> ordered(
s.get_accounts().begin(), s.get_accounts().end());

for (const auto& [key, acc] : ordered)
for (const auto& [key, acc] : s)

Check warning on line 98 in test/blockchaintest/blockchaintest_runner.cpp

View check run for this annotation

Codecov / codecov/patch

test/blockchaintest/blockchaintest_runner.cpp#L98

Added line #L98 was not covered by tests
{
out << key << " : \n";
out << "\tnonce : " << acc.nonce << "\n";
Expand All @@ -106,15 +104,11 @@ std::string print_state(const state::State& s)

if (!acc.storage.empty())
{
const std::map<bytes32, state::StorageValue> ordered_storage(
acc.storage.begin(), acc.storage.end());

out << "\tstorage : "
<< "\n";
for (const auto& [s_key, val] : ordered_storage)
out << "\tstorage : \n";
for (const auto& [s_key, val] : acc.storage)

Check warning on line 108 in test/blockchaintest/blockchaintest_runner.cpp

View check run for this annotation

Codecov / codecov/patch

test/blockchaintest/blockchaintest_runner.cpp#L107-L108

Added lines #L107 - L108 were not covered by tests
{
if (val.current) // Skip 0 values.
out << "\t\t" << s_key << " : " << hex0x(val.current) << "\n";
if (!is_zero(val)) // Skip 0 values.
out << "\t\t" << s_key << " : " << hex0x(val) << "\n";

Check warning on line 111 in test/blockchaintest/blockchaintest_runner.cpp

View check run for this annotation

Codecov / codecov/patch

test/blockchaintest/blockchaintest_runner.cpp#L110-L111

Added lines #L110 - L111 were not covered by tests
}
}
}
Expand All @@ -131,7 +125,7 @@ void run_blockchain_tests(std::span<const BlockchainTest> tests, evmc::VM& vm)
SCOPED_TRACE(std::string{evmc::to_string(c.rev.get_revision(0))} + '/' +
std::to_string(case_index) + '/' + c.name);

auto state = c.pre_state;
auto state = c.pre_state.to_intra_state();

const state::BlockInfo genesis{
.number = c.genesis_block_header.block_number,
Expand All @@ -151,8 +145,7 @@ void run_blockchain_tests(std::span<const BlockchainTest> tests, evmc::VM& vm)

const auto genesis_res = apply_block(state, vm, genesis, {}, c.rev.get_revision(0), {});

EXPECT_EQ(
state::mpt_hash(state.get_accounts()), state::mpt_hash(c.pre_state.get_accounts()));
EXPECT_EQ(state::mpt_hash(TestState{state}), state::mpt_hash(c.pre_state));

if (c.rev.get_revision(0) >= EVMC_SHANGHAI)
{
Expand Down Expand Up @@ -184,7 +177,7 @@ void run_blockchain_tests(std::span<const BlockchainTest> tests, evmc::VM& vm)
'/' + c.name + '/' + std::to_string(test_block.block_info.number));

EXPECT_EQ(
state::mpt_hash(state.get_accounts()), test_block.expected_block_header.state_root);
state::mpt_hash(TestState{state}), test_block.expected_block_header.state_root);

if (rev >= EVMC_SHANGHAI)
{
Expand All @@ -203,16 +196,17 @@ void run_blockchain_tests(std::span<const BlockchainTest> tests, evmc::VM& vm)
// TODO: Add difficulty calculation verification.
}

const auto post_state_hash =
std::holds_alternative<state::State>(c.expectation.post_state) ?
state::mpt_hash(std::get<state::State>(c.expectation.post_state).get_accounts()) :
const TestState post{state};
const auto expected_post_hash =
std::holds_alternative<TestState>(c.expectation.post_state) ?
state::mpt_hash(std::get<TestState>(c.expectation.post_state)) :
std::get<hash256>(c.expectation.post_state);
EXPECT_TRUE(state::mpt_hash(state.get_accounts()) == post_state_hash)
EXPECT_TRUE(state::mpt_hash(post) == expected_post_hash)
<< "Result state:\n"
<< print_state(state)
<< (std::holds_alternative<state::State>(c.expectation.post_state) ?
<< print_state(post)
<< (std::holds_alternative<TestState>(c.expectation.post_state) ?

Check warning on line 207 in test/blockchaintest/blockchaintest_runner.cpp

View check run for this annotation

Codecov / codecov/patch

test/blockchaintest/blockchaintest_runner.cpp#L206-L207

Added lines #L206 - L207 were not covered by tests
"\n\nExpected state:\n" +
print_state(std::get<state::State>(c.expectation.post_state)) :
print_state(std::get<TestState>(c.expectation.post_state)) :

Check warning on line 209 in test/blockchaintest/blockchaintest_runner.cpp

View check run for this annotation

Codecov / codecov/patch

test/blockchaintest/blockchaintest_runner.cpp#L209

Added line #L209 was not covered by tests
"");
}
}
Expand Down
2 changes: 2 additions & 0 deletions test/state/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ target_sources(
rlp.hpp
state.hpp
state.cpp
test_state.hpp
test_state.cpp
)

option(EVMONE_PRECOMPILES_SILKPRE "Enable precompiles support via silkpre library" OFF)
Expand Down
12 changes: 6 additions & 6 deletions test/state/mpt_hash.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,31 @@
// SPDX-License-Identifier: Apache-2.0

#include "mpt_hash.hpp"
#include "account.hpp"
#include "mpt.hpp"
#include "rlp.hpp"
#include "state.hpp"
#include "test_state.hpp"

namespace evmone::state
{
namespace
{
hash256 mpt_hash(const std::unordered_map<hash256, StorageValue>& storage)
hash256 mpt_hash(const std::map<bytes32, bytes32>& storage)
{
MPT trie;
for (const auto& [key, value] : storage)
{
if (!is_zero(value.current)) // Skip "deleted" values.
trie.insert(keccak256(key), rlp::encode(rlp::trim(value.current)));
if (!is_zero(value)) // Skip "deleted" values.
trie.insert(keccak256(key), rlp::encode(rlp::trim(value)));
}
return trie.hash();
}
} // namespace

hash256 mpt_hash(const std::unordered_map<address, Account>& accounts)
hash256 mpt_hash(const test::TestState& state)
{
MPT trie;
for (const auto& [addr, acc] : accounts)
for (const auto& [addr, acc] : state)
{
trie.insert(keccak256(addr),
rlp::encode_tuple(acc.nonce, acc.balance, mpt_hash(acc.storage), keccak256(acc.code)));
Expand Down
9 changes: 6 additions & 3 deletions test/state/mpt_hash.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@
#include <span>
#include <unordered_map>

namespace evmone::state
namespace evmone::test
{
struct Account;
class TestState;
}

namespace evmone::state
{
/// Computes Merkle Patricia Trie root hash for the given collection of state accounts.
hash256 mpt_hash(const std::unordered_map<address, Account>& accounts);
hash256 mpt_hash(const test::TestState& state);

/// Computes Merkle Patricia Trie root hash for the given list of structures.
template <typename T>
Expand Down
7 changes: 5 additions & 2 deletions test/state/state.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@
namespace evmone::state
{
/// The Ethereum State: the collection of accounts mapped by their addresses.
///
/// TODO: This class is copyable for testing. Consider making it non-copyable.
class State
{
struct JournalBase
Expand Down Expand Up @@ -71,6 +69,11 @@ class State
std::vector<JournalEntry> m_journal;

public:
State() = default;
State(const State&) = delete;
State(State&&) = default;
State& operator=(State&&) = default;

/// Inserts the new account at the address.
/// There must not exist any account under this address before.
Account& insert(const address& addr, Account account = {});
Expand Down
34 changes: 34 additions & 0 deletions test/state/test_state.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// evmone: Fast Ethereum Virtual Machine implementation
// Copyright 2024 The evmone Authors.
// SPDX-License-Identifier: Apache-2.0
#include "test_state.hpp"
#include "state.hpp"

namespace evmone::test
{
TestState::TestState(const state::State& intra_state)
{
for (const auto& [addr, acc] : intra_state.get_accounts())
{
auto& test_acc =
(*this)[addr] = {.nonce = acc.nonce, .balance = acc.balance, .code = acc.code};
auto& test_storage = test_acc.storage;
for (const auto& [key, value] : acc.storage)
test_storage[key] = value.current;
}
}

state::State TestState::to_intra_state() const
{
state::State intra_state;
for (const auto& [addr, acc] : *this)
{
auto& intra_acc = intra_state.insert(
addr, {.nonce = acc.nonce, .balance = acc.balance, .code = acc.code});
auto& storage = intra_acc.storage;
for (const auto& [key, value] : acc.storage)
storage[key] = {.current = value, .original = value};
}
return intra_state;
}
} // namespace evmone::test
67 changes: 67 additions & 0 deletions test/state/test_state.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// evmone: Fast Ethereum Virtual Machine implementation
// Copyright 2024 The evmone Authors.
// SPDX-License-Identifier: Apache-2.0
#pragma once

#include <evmc/evmc.hpp>
#include <intx/intx.hpp>
#include <map>

namespace evmone
{
namespace state
{
class State;
}

namespace test
{
using evmc::address;
using evmc::bytes;
using evmc::bytes32;
using intx::uint256;

/// Ethereum account representation for tests.
struct TestAccount

Check warning on line 25 in test/state/test_state.hpp

View check run for this annotation

Codecov / codecov/patch

test/state/test_state.hpp#L25

Added line #L25 was not covered by tests
{
uint64_t nonce = 0;
uint256 balance;
std::map<bytes32, bytes32> storage;
bytes code;

bool operator==(const TestAccount&) const noexcept = default;
};

/// Ethereum State representation for tests.
///
/// This is a simplified variant of state::State:
/// it hides some details related to transaction execution (e.g. original storage values)
/// and is also easier to work with in tests.
class TestState : public std::map<address, TestAccount>
{
public:
using map::map;

Check warning on line 43 in test/state/test_state.hpp

View check run for this annotation

Codecov / codecov/patch

test/state/test_state.hpp#L43

Added line #L43 was not covered by tests

/// Inserts new account to the state.
///
/// This method is for compatibility with state::State::insert().
/// Don't use it in new tests, use std::map interface instead.
/// TODO: deprecate this method.
void insert(const address& addr, TestAccount&& acc) { (*this)[addr] = std::move(acc); }

/// Gets the reference to an existing account.
///
/// This method is for compatibility with state::State::get().
/// Don't use it in new tests, use std::map interface instead.
/// TODO: deprecate this method.
TestAccount& get(const address& addr) { return (*this)[addr]; }

/// Converts the intra state to TestState.
explicit TestState(const state::State& intra_state);

/// Converts the TestState to intra state for transaction execution.
[[nodiscard]] state::State to_intra_state() const;
};

} // namespace test
} // namespace evmone
9 changes: 5 additions & 4 deletions test/statetest/statetest.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#pragma once

#include "../state/state.hpp"
#include "../state/test_state.hpp"
#include <nlohmann/json.hpp>

namespace json = nlohmann;
Expand Down Expand Up @@ -54,7 +55,7 @@ struct StateTransitionTest
};

std::string name;
state::State pre_state;
TestState pre_state;
state::BlockInfo block;
TestMultiTransaction multi_tx;
std::vector<Case> cases;
Expand Down Expand Up @@ -86,21 +87,21 @@ template <>
state::Withdrawal from_json<state::Withdrawal>(const json::json& j);

template <>
state::State from_json<state::State>(const json::json& j);
TestState from_json<TestState>(const json::json& j);

template <>
state::Transaction from_json<state::Transaction>(const json::json& j);

/// Exports the State (accounts) to JSON format (aka pre/post/alloc state).
json::json to_json(const std::unordered_map<address, state::Account>& accounts);
json::json to_json(const TestState& state);

std::vector<StateTransitionTest> load_state_tests(std::istream& input);

/// Validates an Ethereum state:
/// - checks that there are no zero-value storage entries,
/// - checks that there are no invalid EOF codes.
/// Throws std::invalid_argument exception.
void validate_state(const state::State& state, evmc_revision rev);
void validate_state(const TestState& state, evmc_revision rev);

/// Execute the state @p test using the @p vm.
///
Expand Down
Loading

0 comments on commit 924b6b8

Please sign in to comment.