Skip to content
Draft
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
3 changes: 3 additions & 0 deletions lib/evmone/advanced_instructions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,9 @@ constexpr std::array<instruction_exec_fn, 256> instruction_implementations = [](
table[OP_DUPN] = op_undefined;
table[OP_SWAPN] = op_undefined;
table[OP_EXCHANGE] = op_undefined;
table[OP_CALLSUB] = op_undefined;
table[OP_CALLDEST] = op_undefined;
table[OP_RETURNSUB] = op_undefined;

return table;
}();
Expand Down
18 changes: 16 additions & 2 deletions lib/evmone/baseline.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,18 @@ class CodeAnalysis

BitsetSpan m_jumpdest_bitset{nullptr};

/// EIP-7979: the positions of CALLDEST instructions, the only valid CALLSUB targets
/// and, from the revision that has them, also valid jump destinations.
BitsetSpan m_calldest_bitset{nullptr};

public:
/// Constructor for legacy code.
CodeAnalysis(std::unique_ptr<uint8_t[]> padded_code, size_t code_size, BitsetSpan map)
CodeAnalysis(std::unique_ptr<uint8_t[]> padded_code, size_t code_size, BitsetSpan jumpdest_map,
BitsetSpan calldest_map)
: m_code{padded_code.get(), code_size},
m_padded_code{std::move(padded_code)},
m_jumpdest_bitset{map}
m_jumpdest_bitset{jumpdest_map},
m_calldest_bitset{calldest_map}
{}

/// The executable code. This is where the interpreter should start execution.
Expand All @@ -82,6 +88,14 @@ class CodeAnalysis
return false;
return m_jumpdest_bitset.test(static_cast<size_t>(position));
}

/// Check if given position is a CALLDEST, i.e. a valid CALLSUB destination (EIP-7979).
[[nodiscard]] bool check_calldest(uint64_t position) const noexcept
{
if (position >= m_code.size())
return false;
return m_calldest_bitset.test(static_cast<size_t>(position));
}
};

/// Analyze the EVM code in preparation for execution.
Expand Down
19 changes: 13 additions & 6 deletions lib/evmone/baseline_analysis.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ static_assert(!std::is_copy_assignable_v<CodeAnalysis>);

namespace
{
void analyze_jumpdests(BitsetSpan map, bytes_view code) noexcept
/// Builds the map of valid jump destinations and, for EIP-7979, the map of
/// CALLDEST positions. The analysis is revision-independent, so CALLDESTs are
/// kept in their own map; jumps accept them only from the revision that has them.
void analyze_jumpdests(BitsetSpan jumpdest_map, BitsetSpan calldest_map, bytes_view code) noexcept
{
// To find if op is any PUSH opcode (OP_PUSH1 <= op <= OP_PUSH32)
// it can be noticed that OP_PUSH32 is INT8_MAX (0x7f) therefore,
Expand All @@ -28,7 +31,9 @@ void analyze_jumpdests(BitsetSpan map, bytes_view code) noexcept
if (static_cast<int8_t>(op) >= OP_PUSH1) // If any PUSH opcode (see explanation above).
i += op - size_t{OP_PUSH1 - 1}; // Skip PUSH data.
else if (INTX_UNLIKELY(op == OP_JUMPDEST))
map.set(i);
jumpdest_map.set(i);
else if (INTX_UNLIKELY(op == OP_CALLDEST))
calldest_map.set(i);
}
}

Expand All @@ -45,18 +50,20 @@ CodeAnalysis analyze_legacy(bytes_view code)
const auto aligned_code_size =
(padded_code_size + (BITSET_ALIGNMENT - 1)) / BITSET_ALIGNMENT * BITSET_ALIGNMENT;
const auto bitset_words = (code.size() + (BitsetSpan::WORD_BITS)) / BitsetSpan::WORD_BITS;
const auto total_size = aligned_code_size + bitset_words * sizeof(BitsetSpan::word_type);
// Two bitsets: jump destinations and, for EIP-7979, CALLDEST positions.
const auto total_size = aligned_code_size + 2 * bitset_words * sizeof(BitsetSpan::word_type);

auto storage = std::make_unique_for_overwrite<uint8_t[]>(total_size);
std::ranges::copy(code, storage.get()); // Copy code.
std::fill_n(&storage[code.size()], total_size - code.size(), 0); // Pad code and init bitset.

const auto bitset_storage =
new (&storage[aligned_code_size]) BitsetSpan::word_type[bitset_words];
new (&storage[aligned_code_size]) BitsetSpan::word_type[2 * bitset_words];
const BitsetSpan jumpdest_bitset{bitset_storage};
analyze_jumpdests(jumpdest_bitset, code);
const BitsetSpan calldest_bitset{bitset_storage + bitset_words};
analyze_jumpdests(jumpdest_bitset, calldest_bitset, code);

return {std::move(storage), code.size(), jumpdest_bitset};
return {std::move(storage), code.size(), jumpdest_bitset, calldest_bitset};
}
} // namespace

Expand Down
6 changes: 6 additions & 0 deletions lib/evmone/execution_state.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@ class ExecutionState
/// Reference to original EVM code.
bytes_view original_code;

/// EIP-7979: the return stack. Holds positions of the instructions following CALLSUBs,
/// pushed only by CALLSUB and popped only by RETURNSUB.
static constexpr size_t RETURN_STACK_LIMIT = 1024;
std::vector<uint32_t> return_stack;

evmc_status_code status = EVMC_SUCCESS;
size_t output_offset = 0;
size_t output_size = 0;
Expand Down Expand Up @@ -178,6 +183,7 @@ class ExecutionState
host = {host_interface, host_ctx};
rev = revision;
return_data.clear();
return_stack.clear();
original_code = _code;
status = EVMC_SUCCESS;
output_offset = 0;
Expand Down
51 changes: 49 additions & 2 deletions lib/evmone/instructions.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ namespace instr::core
inline void noop(StackTop /*stack*/) noexcept {}
inline constexpr auto pop = noop;
inline constexpr auto jumpdest = noop;
inline constexpr auto calldest = noop; ///< EIP-7979: a label, like JUMPDEST.

template <evmc_status_code Status>
inline TermResult stop_impl(
Expand Down Expand Up @@ -765,8 +766,14 @@ inline code_iterator jump_impl(ExecutionState& state, const uint256& dst) noexce
const auto hi_part_is_nonzero = (dst[3] | dst[2] | dst[1]) != 0;
if (hi_part_is_nonzero || !state.analysis.baseline->check_jumpdest(dst[0])) [[unlikely]]
{
state.status = EVMC_BAD_JUMP_DESTINATION;
return nullptr;
// EIP-7979: a jump may also land on a CALLDEST. Checked only when the
// JUMPDEST test fails, so ordinary jumps pay nothing for it.
if (hi_part_is_nonzero || state.rev < EVMC_EXPERIMENTAL ||
!state.analysis.baseline->check_calldest(dst[0]))
{
state.status = EVMC_BAD_JUMP_DESTINATION;
return nullptr;
}
}

return &state.analysis.baseline->code()[static_cast<size_t>(dst[0])];
Expand All @@ -786,6 +793,46 @@ inline code_iterator jumpi(StackTop stack, ExecutionState& state, code_iterator
return cond ? jump_impl(state, dst) : pos + 1;
}

/// CALLSUB instruction implementation using baseline::CodeAnalysis (EIP-7979).
/// Pushes the position of the next instruction onto the return stack and
/// transfers control to the CALLDEST at the destination.
inline code_iterator callsub(StackTop stack, ExecutionState& state, code_iterator pos) noexcept
{
const auto& dst = stack.pop();
const auto hi_part_is_nonzero = (dst[3] | dst[2] | dst[1]) != 0;
if (hi_part_is_nonzero || !state.analysis.baseline->check_calldest(dst[0])) [[unlikely]]
{
state.status = EVMC_BAD_JUMP_DESTINATION;
return nullptr;
}
if (state.return_stack.size() >= ExecutionState::RETURN_STACK_LIMIT) [[unlikely]]
{
state.status = EVMC_STACK_OVERFLOW;
return nullptr;
}

const auto code = state.analysis.baseline->code();
state.return_stack.push_back(static_cast<uint32_t>(pos + 1 - code.data()));
return &code[static_cast<size_t>(dst[0])];
}

/// RETURNSUB instruction implementation using baseline::CodeAnalysis (EIP-7979).
/// Pops the return stack into the program counter.
inline code_iterator returnsub(
StackTop /*stack*/, ExecutionState& state, code_iterator /*pos*/) noexcept
{
if (state.return_stack.empty()) [[unlikely]]
{
state.status = EVMC_STACK_UNDERFLOW;
return nullptr;
}

const auto ret = state.return_stack.back();
state.return_stack.pop_back();
// The return position may be the code end, where the padding guarantees a STOP.
return state.analysis.baseline->code().data() + ret;
}

inline code_iterator pc(StackTop stack, ExecutionState& state, code_iterator pos) noexcept
{
stack.push(static_cast<uint64_t>(pos - state.analysis.baseline->code().data()));
Expand Down
4 changes: 4 additions & 0 deletions lib/evmone/instructions_opcodes.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,10 @@ enum Opcode : uint8_t // NOLINT(*-use-enum-class)
OP_LOG3 = 0xa3,
OP_LOG4 = 0xa4,

OP_CALLSUB = 0xb0,
OP_CALLDEST = 0xb1,
OP_RETURNSUB = 0xb2,

OP_DUPN = 0xe6,
OP_SWAPN = 0xe7,
OP_EXCHANGE = 0xe8,
Expand Down
8 changes: 8 additions & 0 deletions lib/evmone/instructions_traits.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,10 @@ constexpr inline GasCostTable gas_costs = []() noexcept {
table[EVMC_AMSTERDAM][OP_EXCHANGE] = 3;

table[EVMC_EXPERIMENTAL] = table[EVMC_AMSTERDAM];
// EIP-7979: Call and Return Opcodes for the EVM (mid, jumpdest, low).
table[EVMC_EXPERIMENTAL][OP_CALLSUB] = 8;
table[EVMC_EXPERIMENTAL][OP_CALLDEST] = 1;
table[EVMC_EXPERIMENTAL][OP_RETURNSUB] = 5;

return table;
}();
Expand Down Expand Up @@ -389,6 +393,10 @@ constexpr inline std::array<Traits, 256> traits = []() noexcept {
table[OP_LOG3] = {"LOG3", 0, false, 5, -5, EVMC_FRONTIER};
table[OP_LOG4] = {"LOG4", 0, false, 6, -6, EVMC_FRONTIER};

table[OP_CALLSUB] = {"CALLSUB", 0, false, 1, -1, EVMC_EXPERIMENTAL};
table[OP_CALLDEST] = {"CALLDEST", 0, false, 0, 0, EVMC_EXPERIMENTAL};
table[OP_RETURNSUB] = {"RETURNSUB", 0, false, 0, 0, EVMC_EXPERIMENTAL};

table[OP_MCOPY] = {"MCOPY", 0, false, 3, -3, EVMC_CANCUN};

table[OP_CREATE] = {"CREATE", 0, false, 3, -2, EVMC_FRONTIER};
Expand Down
6 changes: 3 additions & 3 deletions lib/evmone/instructions_xmacro.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -219,9 +219,9 @@
ON_OPCODE_UNDEFINED(0xae) \
ON_OPCODE_UNDEFINED(0xaf) \
\
ON_OPCODE_UNDEFINED(0xb0) \
ON_OPCODE_UNDEFINED(0xb1) \
ON_OPCODE_UNDEFINED(0xb2) \
ON_OPCODE_IDENTIFIER(OP_CALLSUB, callsub) \
ON_OPCODE_IDENTIFIER(OP_CALLDEST, calldest) \
ON_OPCODE_IDENTIFIER(OP_RETURNSUB, returnsub) \
ON_OPCODE_UNDEFINED(0xb3) \
ON_OPCODE_UNDEFINED(0xb4) \
ON_OPCODE_UNDEFINED(0xb5) \
Expand Down
1 change: 1 addition & 0 deletions test/unittests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ target_sources(
evm_eip2929_test.cpp
evm_eip3198_basefee_test.cpp
evm_eip3855_push0_test.cpp
evm_eip7979_callsub_test.cpp
evm_eip3860_initcode_test.cpp
evm_eip4844_blobhash_test.cpp
evm_eip7516_blobbasefee_test.cpp
Expand Down
Loading