Skip to content

Commit

Permalink
state: Add insert-only MPT implementation
Browse files Browse the repository at this point in the history
Add insert-only Merkle Patricia Trie implementation for computing MPT
root hashes of (key, value) maps.
  • Loading branch information
chfast committed Jun 16, 2022
1 parent 9048bea commit e8882f3
Show file tree
Hide file tree
Showing 8 changed files with 663 additions and 4 deletions.
2 changes: 1 addition & 1 deletion .clang-tidy
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
FormatStyle: file
HeaderFilterRegex: 'lib/evmone/'
HeaderFilterRegex: 'lib/evmone/|test/state/'
WarningsAsErrors: '*'
Checks: >
bugprone-*,
Expand Down
8 changes: 5 additions & 3 deletions test/state/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
# Copyright 2022 The evmone Authors.
# SPDX-License-Identifier: Apache-2.0

add_library(evmone-state INTERFACE)
add_library(evmone-state STATIC)
add_library(evmone::state ALIAS evmone-state)
target_link_libraries(evmone-state INTERFACE ethash::keccak)
target_link_libraries(evmone-state PRIVATE evmc::evmc_cpp ethash::keccak)
target_sources(
evmone-state INTERFACE
evmone-state PRIVATE
hash_utils.hpp
mpt.hpp
mpt.cpp
rlp.hpp
)
1 change: 1 addition & 0 deletions test/state/hash_utils.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ namespace evmone
{
using evmc::bytes;
using evmc::bytes_view;
using namespace evmc::literals;

/// Default type for 256-bit hash.
///
Expand Down
282 changes: 282 additions & 0 deletions test/state/mpt.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
// evmone: Fast Ethereum Virtual Machine implementation
// Copyright 2022 The evmone Authors.
// SPDX-License-Identifier: Apache-2.0

#include "mpt.hpp"
#include "rlp.hpp"
#include <algorithm>
#include <cassert>
#include <cstring>

namespace evmone::state
{
namespace
{
/// The collection of nibbles (4-bit values) representing a path in a MPT.
struct Path
{
size_t length; // TODO: Can be converted to uint8_t.
uint8_t nibbles[64]{};

explicit Path(bytes_view k) noexcept : length(2 * k.size())
{
assert(length <= 64);
size_t i = 0;
for (const auto b : k)
{
// static_cast is only needed in GCC <= 8.
nibbles[i++] = static_cast<uint8_t>(b >> 4);
nibbles[i++] = static_cast<uint8_t>(b & 0x0f);
}
}

[[nodiscard]] Path tail(size_t index) const
{
assert(index <= length);
Path p{{}};
p.length = length - index;
std::memcpy(p.nibbles, &nibbles[index], p.length);
return p;
}

[[nodiscard]] Path head(size_t size) const
{
assert(size < length);
Path p{{}};
p.length = size;
std::memcpy(p.nibbles, nibbles, size);
return p;
}

[[nodiscard]] bytes encode(bool extended) const
{
bytes bs;
const auto is_even = length % 2 == 0;
if (is_even)
bs.push_back(0x00);
else
bs.push_back(0x10 | nibbles[0]);
for (size_t i = is_even ? 0 : 1; i < length; ++i)
{
const auto h = nibbles[i++];
const auto l = nibbles[i];
assert(h <= 0x0f);
assert(l <= 0x0f);
bs.push_back(uint8_t((h << 4) | l));
}
if (!extended)
bs[0] |= 0x20;
return bs;
}
};
} // namespace

/// The MPT Node.
///
/// The implementation is based on StackTrie from go-ethereum.
class MPTNode
{
enum class Kind : uint8_t
{
leaf,
ext,
branch
};

static constexpr size_t num_children = 16;

Kind m_kind = Kind::leaf;
Path m_path{{}};
bytes m_value;
std::unique_ptr<MPTNode> children[num_children];

MPTNode(Kind kind, const Path& path, bytes&& value = {}) noexcept
: m_kind{kind}, m_path{path}, m_value{std::move(value)}
{}

/// Named constructor for an extended node.
static MPTNode ext(const Path& k, std::unique_ptr<MPTNode> child) noexcept
{
MPTNode node{Kind::ext, k};
node.children[0] = std::move(child);
return node;
}

static size_t diff_index(const Path& p1, const Path& p2) noexcept
{
assert(p1.length <= p2.length);
return static_cast<size_t>(
std::mismatch(p1.nibbles, p1.nibbles + p1.length, p2.nibbles).first - p1.nibbles);
}

public:
MPTNode() = default;

/// Named constructor for a leaf node.
static MPTNode leaf(const Path& k, bytes&& v) noexcept { return {Kind::leaf, k, std::move(v)}; }

void insert(const Path& k, bytes&& v);

[[nodiscard]] hash256 hash() const;
};

MPT::MPT() noexcept = default;
MPT::~MPT() noexcept = default;

void MPT::insert(bytes_view key, bytes&& value)
{
if (m_root == nullptr)
m_root = std::make_unique<MPTNode>(MPTNode::leaf(Path{key}, std::move(value)));
else
m_root->insert(Path{key}, std::move(value));
}

[[nodiscard]] hash256 MPT::hash() const
{
if (m_root == nullptr)
return emptyMPTHash;
return m_root->hash();
}

void MPTNode::insert(const Path& k, bytes&& v) // NOLINT(misc-no-recursion)
{
switch (m_kind)
{
case Kind::branch:
{
assert(m_path.length == 0);
const auto idx = k.nibbles[0];
auto& child = children[idx];
if (!child)
child = std::make_unique<MPTNode>(leaf(k.tail(1), std::move(v)));
else
child->insert(k.tail(1), std::move(v));
break;
}

case Kind::ext:
{
const auto diffidx = diff_index(m_path, k);

if (diffidx == m_path.length)
{
// Go into child.
return children[0]->insert(k.tail(diffidx), std::move(v));
}

std::unique_ptr<MPTNode> n;
if (diffidx < m_path.length - 1)
n = std::make_unique<MPTNode>(ext(m_path.tail(diffidx + 1), std::move(children[0])));
else
n = std::move(children[0]);

MPTNode* branch = nullptr;
if (diffidx == 0)
{
branch = this;
branch->m_kind = Kind::branch;
}
else
{
branch = (children[0] = std::make_unique<MPTNode>()).get();
branch->m_kind = Kind::branch;
}

const auto origIdx = m_path.nibbles[diffidx];
const auto newIdx = k.nibbles[diffidx];

branch->children[origIdx] = std::move(n);
branch->children[newIdx] =
std::make_unique<MPTNode>(leaf(k.tail(diffidx + 1), std::move(v)));
m_path = m_path.head(diffidx);
break;
}

case Kind::leaf:
{
// TODO: Add assert for k == key.
const auto diffidx = diff_index(m_path, k);

MPTNode* branch = nullptr;
if (diffidx == 0) // Convert into a branch.
{
m_kind = Kind::branch;
branch = this;
}
else
{
m_kind = Kind::ext;
branch = (children[0] = std::make_unique<MPTNode>()).get();
branch->m_kind = Kind::branch;
}

const auto origIdx = m_path.nibbles[diffidx];
branch->children[origIdx] =
std::make_unique<MPTNode>(leaf(m_path.tail(diffidx + 1), std::move(m_value)));

const auto newIdx = k.nibbles[diffidx];
assert(origIdx != newIdx);
branch->children[newIdx] =
std::make_unique<MPTNode>(leaf(k.tail(diffidx + 1), std::move(v)));

m_path = m_path.head(diffidx);
break;
}

default:
assert(false);
}
}

hash256 MPTNode::hash() const // NOLINT(misc-no-recursion)
{
hash256 r{};
switch (m_kind)
{
case Kind::leaf:
{
const auto node = rlp::tuple(m_path.encode(false), m_value);
r = keccak256(node);
break;
}
case Kind::branch:
{
assert(m_path.length == 0);

// Temporary storage for children hashes.
// The `bytes` type could be used instead, but this way dynamic allocation is avoided.
hash256 children_hashes[num_children];

// Views of children hash bytes. Additional item for hash list
// terminator (always empty). Does not seem needed for correctness,
// but this is what the spec says.
bytes_view children_hash_bytes[num_children + 1];

for (size_t i = 0; i < num_children; ++i)
{
if (children[i])
{
children_hashes[i] = children[i]->hash();
children_hash_bytes[i] = children_hashes[i];
}
}

r = keccak256(rlp::encode(children_hash_bytes));
break;
}
case Kind::ext:
{
const auto branch = children[0].get();
assert(branch != nullptr);
assert(branch->m_kind == Kind::branch);
r = keccak256(rlp::tuple(m_path.encode(true), branch->hash()));
break;
}
default:
assert(false);
}

return r;
}

} // namespace evmone::state
29 changes: 29 additions & 0 deletions test/state/mpt.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// evmone: Fast Ethereum Virtual Machine implementation
// Copyright 2022 The evmone Authors.
// SPDX-License-Identifier: Apache-2.0
#pragma once

#include "hash_utils.hpp"
#include <memory>

namespace evmone::state
{
constexpr auto emptyMPTHash =
0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421_bytes32;

/// Insert-only Merkle Patricia Trie implementation for getting the root hash
/// out of (key, value) pairs.
class MPT
{
std::unique_ptr<class MPTNode> m_root;

public:
MPT() noexcept;
~MPT() noexcept;

void insert(bytes_view key, bytes&& value);

[[nodiscard]] hash256 hash() const;
};

} // namespace evmone::state
1 change: 1 addition & 0 deletions test/unittests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ add_executable(evmone-unittests
evmone_test.cpp
execution_state_test.cpp
instructions_test.cpp
state_mpt_test.cpp
state_rlp_test.cpp
tracing_test.cpp
utils_test.cpp
Expand Down
Loading

0 comments on commit e8882f3

Please sign in to comment.