diff --git a/category/execution/ethereum/dispatch_transaction.cpp b/category/execution/ethereum/dispatch_transaction.cpp index 99aa96914..ad1b04bd7 100644 --- a/category/execution/ethereum/dispatch_transaction.cpp +++ b/category/execution/ethereum/dispatch_transaction.cpp @@ -27,7 +27,8 @@ Result dispatch_transaction( BlockHeader const &header, BlockHashBuffer const &block_hash_buffer, BlockState &block_state, BlockMetrics &block_metrics, boost::fibers::promise &prev, CallTracerBase &call_tracer, - trace::StateTracer &state_tracer, ChainContext const &chain_ctx) + trace::StateTracer &state_tracer, ChainContext const &chain_ctx, + bool const trace_transfers) { return ExecuteTransaction{ chain, @@ -42,7 +43,8 @@ Result dispatch_transaction( prev, call_tracer, state_tracer, - chain_ctx}(); + chain_ctx, + trace_transfers}(); } EXPLICIT_EVM_TRAITS(dispatch_transaction) diff --git a/category/execution/ethereum/dispatch_transaction.hpp b/category/execution/ethereum/dispatch_transaction.hpp index 4db72ce28..c7fa16431 100644 --- a/category/execution/ethereum/dispatch_transaction.hpp +++ b/category/execution/ethereum/dispatch_transaction.hpp @@ -50,6 +50,7 @@ Result dispatch_transaction( BlockHeader const &header, BlockHashBuffer const &block_hash_buffer, BlockState &block_state, BlockMetrics &block_metrics, boost::fibers::promise &prev, CallTracerBase &call_tracer, - trace::StateTracer &state_tracer, ChainContext const &chain_ctx); + trace::StateTracer &state_tracer, ChainContext const &chain_ctx, + bool trace_transfers = false); MONAD_NAMESPACE_END diff --git a/category/execution/ethereum/execute_block.cpp b/category/execution/ethereum/execute_block.cpp index 83dd47d58..901602e09 100644 --- a/category/execution/ethereum/execute_block.cpp +++ b/category/execution/ethereum/execute_block.cpp @@ -203,7 +203,7 @@ Result> execute_block_transactions( fiber::FiberGroup &priority_pool, BlockMetrics &block_metrics, std::span> const call_tracers, std::span> const state_tracers, - ChainContext const &chain_ctx) + ChainContext const &chain_ctx, bool const trace_transfers) { MONAD_ASSERT(senders.size() == transactions.size()); MONAD_ASSERT(senders.size() == call_tracers.size()); @@ -234,7 +234,8 @@ Result> execute_block_transactions( &block_metrics, &call_tracer = *call_tracers[i], &state_tracer = *state_tracers[i], - &chain_ctx = chain_ctx] { + &chain_ctx = chain_ctx, + trace_transfers = trace_transfers] { record_txn_marker_event(MONAD_EXEC_TXN_PERF_EVM_ENTER, i); try { results[i] = dispatch_transaction( @@ -250,7 +251,8 @@ Result> execute_block_transactions( promises[i], call_tracer, state_tracer, - chain_ctx); + chain_ctx, + trace_transfers); if (results[i]->has_error()) { record_txn_error_event(i, results[i]->error()); } @@ -304,7 +306,7 @@ Result> execute_block( fiber::FiberGroup &priority_pool, BlockMetrics &block_metrics, std::span> const call_tracers, std::span> const state_tracers, - ChainContext const &chain_ctx) + ChainContext const &chain_ctx, bool const trace_transfers) { TRACE_BLOCK_EVENT(StartBlock); @@ -328,7 +330,8 @@ Result> execute_block( block_metrics, call_tracers, state_tracers, - chain_ctx)); + chain_ctx, + trace_transfers)); State state{ block_state, Incarnation{block.header.number, Incarnation::LAST_TX}}; diff --git a/category/execution/ethereum/execute_block.hpp b/category/execution/ethereum/execute_block.hpp index 5c188b015..e1b4384c8 100644 --- a/category/execution/ethereum/execute_block.hpp +++ b/category/execution/ethereum/execute_block.hpp @@ -57,7 +57,7 @@ Result> execute_block_transactions( BlockState &, BlockHashBuffer const &, fiber::FiberGroup &, BlockMetrics &, std::span>, std::span> state_tracers, - ChainContext const &chain_ctx); + ChainContext const &chain_ctx, bool trace_transfers = false); template Result> execute_block( @@ -66,7 +66,7 @@ Result> execute_block( BlockState &, BlockHashBuffer const &, fiber::FiberGroup &, BlockMetrics &, std::span>, std::span> state_tracers, - ChainContext const &chain_ctx); + ChainContext const &chain_ctx, bool trace_transfers = false); std::vector> recover_senders(std::span, fiber::PriorityPool &); diff --git a/category/execution/ethereum/execute_transaction.cpp b/category/execution/ethereum/execute_transaction.cpp index a85fef53b..c8762c00e 100644 --- a/category/execution/ethereum/execute_transaction.cpp +++ b/category/execution/ethereum/execute_transaction.cpp @@ -291,7 +291,8 @@ ExecuteTransaction::ExecuteTransaction( BlockHeader const &header, BlockHashBuffer const &block_hash_buffer, BlockState &block_state, BlockMetrics &block_metrics, boost::fibers::promise &prev, CallTracerBase &call_tracer, - trace::StateTracer &state_tracer, ChainContext const &chain_ctx) + trace::StateTracer &state_tracer, ChainContext const &chain_ctx, + bool const trace_transfers) : ExecuteTransactionNoValidation< traits>{chain, tx, sender, authorities, header} , i_{i} @@ -302,6 +303,7 @@ ExecuteTransaction::ExecuteTransaction( , prev_{prev} , call_tracer_{call_tracer} , state_tracer_{state_tracer} + , trace_transfers_{trace_transfers} { record_txn_header_events(static_cast(i), tx, sender, authorities); } @@ -337,7 +339,8 @@ Result ExecuteTransaction::execute_impl2(State &state) tx_, header_.base_fee_per_gas, i_, - chain_ctx_}; + chain_ctx_, + trace_transfers_}; return ExecuteTransactionNoValidation::operator()(state, host); } diff --git a/category/execution/ethereum/execute_transaction.hpp b/category/execution/ethereum/execute_transaction.hpp index 2241a3d63..04b19902d 100644 --- a/category/execution/ethereum/execute_transaction.hpp +++ b/category/execution/ethereum/execute_transaction.hpp @@ -82,6 +82,7 @@ class ExecuteTransaction : public ExecuteTransactionNoValidation boost::fibers::promise &prev_; CallTracerBase &call_tracer_; trace::StateTracer &state_tracer_; + bool trace_transfers_; Result execute_impl2(State &); Receipt execute_final(State &, evmc::Result const &); @@ -92,7 +93,8 @@ class ExecuteTransaction : public ExecuteTransactionNoValidation std::span const>, BlockHeader const &, BlockHashBuffer const &, BlockState &, BlockMetrics &, boost::fibers::promise &prev, CallTracerBase &, - trace::StateTracer &, ChainContext const &chain_ctx); + trace::StateTracer &, ChainContext const &chain_ctx, + bool trace_transfers = false); ~ExecuteTransaction() = default; Result operator()(); diff --git a/category/execution/monad/dispatch_transaction.cpp b/category/execution/monad/dispatch_transaction.cpp index 6575fb8c4..95997a1c6 100644 --- a/category/execution/monad/dispatch_transaction.cpp +++ b/category/execution/monad/dispatch_transaction.cpp @@ -29,7 +29,8 @@ Result dispatch_transaction( BlockHeader const &header, BlockHashBuffer const &block_hash_buffer, BlockState &block_state, BlockMetrics &block_metrics, boost::fibers::promise &prev, CallTracerBase &call_tracer, - trace::StateTracer &state_tracer, ChainContext const &chain_ctx) + trace::StateTracer &state_tracer, ChainContext const &chain_ctx, + bool const trace_transfers) { if (traits::monad_rev() >= MONAD_FOUR && sender == SYSTEM_SENDER) { // System transactions is a concept used in Monad for consensus to @@ -61,7 +62,8 @@ Result dispatch_transaction( prev, call_tracer, state_tracer, - chain_ctx}(); + chain_ctx, + trace_transfers}(); } } diff --git a/category/execution/monad/dispatch_transaction.hpp b/category/execution/monad/dispatch_transaction.hpp index 5f98c202b..a592b0222 100644 --- a/category/execution/monad/dispatch_transaction.hpp +++ b/category/execution/monad/dispatch_transaction.hpp @@ -28,6 +28,7 @@ Result dispatch_transaction( BlockHeader const &header, BlockHashBuffer const &block_hash_buffer, BlockState &block_state, BlockMetrics &block_metrics, boost::fibers::promise &prev, CallTracerBase &call_tracer, - trace::StateTracer &, ChainContext const &chain_ctx); + trace::StateTracer &, ChainContext const &chain_ctx, + bool trace_transfers); MONAD_NAMESPACE_END diff --git a/category/rpc/monad_executor.cpp b/category/rpc/monad_executor.cpp index 0c5e8a380..e18761b35 100644 --- a/category/rpc/monad_executor.cpp +++ b/category/rpc/monad_executor.cpp @@ -42,11 +42,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -56,6 +58,7 @@ #include #include #include +#include #include #include #include @@ -74,6 +77,7 @@ #include #include +#include #include #include #include @@ -111,14 +115,14 @@ namespace // creates its own LazyBlockHash instance. class LazyBlockHash : public BlockHashBuffer { - using BlockHashBuffer::N; - mpt::RODb const &db_; uint64_t const n_; using Cache = static_lru_cache; mutable Cache blockhash_cache_; public: + using BlockHashBuffer::N; + LazyBlockHash(mpt::RODb const &db, uint64_t const n) : db_{db} , n_{n} @@ -156,6 +160,8 @@ namespace char const *const UNEXPECTED_EXCEPTION_ERR_MSG = "unexpected error"; char const *const EXCEED_QUEUE_SIZE_ERR_MSG = "failure to submit eth_call to thread pool: queue size exceeded"; + char const *const ETH_SIMULATE_EXCEED_QUEUE_SIZE_ERR_MSG = + "failure to submit eth_simulateV1 to thread pool: queue size exceeded"; char const *const TIMEOUT_ERR_MSG = "failure to execute eth_call: queuing time exceeded timeout threshold"; char const *const PRESTATE_TRACER_SUPPORT_ERR_MSG = @@ -539,6 +545,657 @@ namespace return Result{std::move(traces)}; } } + + template + class ChainContextBuffer; + + /** + * Dummy buffer for EVM trait specializations that do not need chain context + * for reserve balance checks. + */ + template + requires(is_evm_trait_v) + class ChainContextBuffer + { + public: + ChainContext advance( + std::vector
const &, + std::vector>> const &) + { + return {}; + } + }; + + /** + * Circular buffer of combined senders and EIP-7702 authorities for the last + * K blocks. Use advance(senders, authorities) to obtain the context needed + * for each block's reserve balance checks in eth_simulatev1. + */ + template + requires(is_monad_trait_v) + class ChainContextBuffer + { + static constexpr size_t K = 3; + + public: + /// Advances the buffer with a new block's senders and authorities, + /// discarding the oldest currently stored context, then returns a + /// ChainContext for the given traits type. The arguments must outlive + /// this buffer. + ChainContext advance( + std::vector
const &senders, + std::vector>> const &authorities) + { + current_index_ = current_index_ == 0 ? K - 1 : current_index_ - 1; + current_senders_ = &senders; + current_authorities_ = &authorities; + senders_and_authorities_buffer_[current_index_] = + combine_senders_and_authorities(senders, authorities); + + return ChainContext{ + .grandparent_senders_and_authorities = get<2>(), + .parent_senders_and_authorities = get<1>(), + .senders_and_authorities = get<0>(), + .senders = *current_senders_, + .authorities = *current_authorities_, + }; + } + + private: + template + requires(age < K) + ankerl::unordered_dense::segmented_set
const &get() const + { + return senders_and_authorities_buffer_[(current_index_ + age) % K]; + } + + size_t current_index_{0}; + std::array, K> + senders_and_authorities_buffer_{}; + std::vector
const *current_senders_{}; + std::vector>> const + *current_authorities_{}; + }; + + // The `eth_simulateV1` method needs to be able to read hashes of both + // finalized and simulated blocks. This buffer piggybacks on lazy block hash + // buffer for finalized blocks, while providing a method for appending the + // block hashes of simulated blocks such that they can be read by later + // simulated blocks. + class EthSimulateBlockHashBuffer : public LazyBlockHash + { + using LazyBlockHash::N; + + uint64_t const n_; + uint64_t i_; + std::optional const base_block_hash_; + std::array simulated_block_hashes_; + + public: + EthSimulateBlockHashBuffer( + mpt::RODb const &db, uint64_t const n, + std::optional const &base_block_hash) + : LazyBlockHash(db, n) + , n_{n} + , i_{0} + , base_block_hash_{base_block_hash} + , simulated_block_hashes_{} + { + } + + ~EthSimulateBlockHashBuffer() override = default; + + uint64_t n() const override + { + return n_ + i_; + } + + bytes32_t const &get(uint64_t const n) const override + { + uint64_t const current_n = this->n(); + MONAD_ASSERT_PRINTF( + n < current_n && n + N >= current_n, + "n_=%lu, n=%lu", + current_n, + n); + + // Simulated blocks begin at `n_`. Querying a block number at or + // above `n_` means we should read from the simulated-hash window. + if (n >= n_) { + size_t const idx = static_cast(n - n_); + MONAD_ASSERT_PRINTF( + idx < i_, + "missing simulated block hash: n=%lu, idx=%zu, i_=%lu", + n, + idx, + i_); + return simulated_block_hashes_[idx]; + } + + // If we are querying the base block, then we need to take care, as + // the base block may not have been finalized yet, meaning a call to + // `LazyBlockHash::get(n)` would throw (since it only reads + // finalized blocks). Therefore we special case this query to return + // the provided base block hash. + if (n + 1 == n_ && base_block_hash_.has_value()) { + return *base_block_hash_; + } + + return LazyBlockHash::get(n); + } + + void advance(bytes32_t const &simulated_block_hash) + { + MONAD_ASSERT_PRINTF( + i_ < N, "block hash buffer overflow: i_=%lu, N=%u", i_, N); + simulated_block_hashes_[i_++] = simulated_block_hash; + } + }; + + void store_output_header( + Block const &block, std::vector const &receipts, + bytes32_t const &block_hash, std::vector const &txn_hashes, + nlohmann::json &output) + { + auto const format_hex = [](auto const &b) { + return std::format("0x{}", evmc::hex(b)); + }; + + BlockHeader const &header = block.header; + + // TODO(dhil): Computing the correct information for some of these + // fields currently requires a roundtrip to the db. However, in + // simulation mode we only have readonly access to the db. + + output["hash"] = format_hex(block_hash); + output["parentHash"] = format_hex(header.parent_hash); + output["sha3Uncles"] = format_hex(header.ommers_hash); + output["miner"] = format_hex(header.beneficiary); + output["stateRoot"] = format_hex(header.state_root); + { + auto const encoded = rlp::encode_block(block); + output["size"] = std::format("0x{:x}", encoded.size()); + } + output["transactionsRoot"] = format_hex(header.transactions_root); + output["receiptsRoot"] = format_hex(header.receipts_root); + { + Receipt::Bloom bloom = compute_bloom(receipts); + output["logsBloom"] = + format_hex(byte_string_view{bloom.data(), bloom.size()}); + } + output["difficulty"] = + std::format("0x{}", intx::to_string(header.difficulty)); + output["number"] = std::format("0x{:x}", header.number); + output["gasLimit"] = std::format("0x{:x}", header.gas_limit); + output["gasUsed"] = std::format("0x{:x}", header.gas_used); + output["timestamp"] = std::format("0x{:x}", header.timestamp); + output["extraData"] = format_hex(header.extra_data); + output["mixHash"] = format_hex(header.prev_randao); + output["nonce"] = std::format("0x0000000000000000"); + output["baseFeePerGas"] = std::format( + "0x{}", intx::to_string(header.base_fee_per_gas.value_or(0), 16)); + { + output["uncles"] = nlohmann::json::array(); + for (auto const &uncle : block.ommers) { + output["uncles"].emplace_back(format_hex( + to_bytes(keccak256(rlp::encode_block_header(uncle))))); + } + } + { + output["transactions"] = nlohmann::json::array(); + for (auto const &txn_hash : txn_hashes) { + output["transactions"].emplace_back(format_hex(txn_hash)); + } + } + { + output["withdrawals"] = nlohmann::json::array(); + for (auto const &withdrawal : + block.withdrawals.value_or(std::vector{})) { + output["withdrawals"].emplace_back(nlohmann::json{ + {"index", std::format("0x{:x}", withdrawal.index)}, + {"validatorIndex", + std::format("0x{:x}", withdrawal.validator_index)}, + {"amount", std::format("0x{:x}", withdrawal.amount)}, + {"recipient", + std::format("0x{}", evmc::hex(withdrawal.recipient))}, + }); + } + } + // TODO(dhil): We currently do not have a way to compute this + // information in simulation mode. + output["withdrawalsRoot"] = + format_hex(header.withdrawals_root.value_or(bytes32_t{})); + } + + void eth_simulate_validate_inputs( + size_t max_simulate_blocks, uint64_t default_timestamp_increment, + std::vector> const &calls, + struct monad_block_override_vec const &block_overrides, + BlockHeader const &header) + { + + MONAD_ASSERT_THROW(calls.size() > 0, "empty input"); + MONAD_ASSERT_THROW( + calls.size() <= max_simulate_blocks, "too many blocks"); + + BlockHeader previous_header = header; + for (size_t i = 0; i < block_overrides.size; ++i) { + auto const &bo = block_overrides.overrides[i]; + // From the specification + // (https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-eth): + // > When overriding multiple blocks, block numbers must + // > increment. Skipping numbers is allowed and skipped + // > blocks are included in the response. + uint64_t const block_number = + bo.number.value_or(previous_header.number + 1); + MONAD_ASSERT_THROW( + block_number > previous_header.number, + "block numbers must be strictly increasing"); + uint64_t const gap = block_number - previous_header.number; + // Possible block number override has been validated; we can + // partially update our loop carried header. + previous_header.number = block_number; + + if (bo.time.has_value()) { + // > Time must either increase or remain constant + // > relative to the previous block. If time is not + // > specified, it's incremented by one for each block. + + // If a gap is wide, then we need to count the synthetic + // timestamps before validating the possible block timestamp + // override. + if (gap > 1) { + // There are `(gap - 1)` synthetic blocks between + // `block_number` and `previous_header.number`. + previous_header.timestamp += + (gap - 1) * default_timestamp_increment; + } + + MONAD_ASSERT_THROW( + previous_header.timestamp <= *bo.time, + "block timestamps must be monotonically increasing"); + previous_header.timestamp = *bo.time; + } + else { + previous_header.timestamp += gap * default_timestamp_increment; + } + } + MONAD_ASSERT(previous_header.number > header.number); + size_t const num_blocks = previous_header.number - header.number; + MONAD_ASSERT(num_blocks > 0); + MONAD_ASSERT_THROW( + num_blocks <= max_simulate_blocks, "too many blocks"); + } + + void save_eth_simulate_log_entry( + Block const &block, std::vector const &receipts, + std::vector> const &call_frames, + bytes32_t const &block_hash, std::vector const &txn_hashes, + nlohmann::json &result) + { + MONAD_ASSERT(call_frames.size() == block.transactions.size()); + MONAD_ASSERT(receipts.size() == block.transactions.size()); + MONAD_ASSERT(txn_hashes.size() == block.transactions.size()); + + auto const format_hex = [](auto const &b) { + return std::format("0x{}", evmc::hex(b)); + }; + + auto entry = nlohmann::json::object(); + + entry["calls"] = nlohmann::json::array(); + auto &txns = entry["calls"]; + + for (size_t tx_idx = 0; tx_idx < block.transactions.size(); ++tx_idx) { + MONAD_ASSERT(call_frames[tx_idx].size() > 0); + auto call_result = nlohmann::json::object(); + + call_result["status"] = std::format( + "0x{:x}", + call_frames[tx_idx][0].status == EVMC_SUCCESS ? 1 : 0); + call_result["returnData"] = + format_hex(call_frames[tx_idx][0].output); + call_result["gasUsed"] = + std::format("0x{:x}", call_frames[tx_idx][0].gas_used); + + size_t log_index = 0; + if (call_frames[tx_idx][0].status == EVMC_SUCCESS) { + call_result["logs"] = nlohmann::json::array(); + for (auto const &log : receipts[tx_idx].logs) { + call_result["logs"].emplace_back(nlohmann::json{ + {"address", format_hex(log.address)}, + {"topics", nlohmann::json::array()}, + {"data", format_hex(log.data)}, + {"blockNumber", + std::format("0x{:x}", block.header.number)}, + { + "transactionHash", + format_hex(txn_hashes[tx_idx]), + }, + {"transactionIndex", std::format("0x{:x}", tx_idx)}, + {"blockHash", format_hex(block_hash)}, + {"logIndex", std::format("0x{:x}", log_index++)}, + // NOTE(dhil): Geth always emits logs with "removed" + // fixed to `false`. + {"removed", false}, + }); + for (auto const &topic : log.topics) { + call_result["logs"].back()["topics"].emplace_back( + format_hex(topic)); + } + } + } + else { + call_result["error"] = {{"message", "execution reverted"}}; + } + + txns.emplace_back(std::move(call_result)); + } + store_output_header(block, receipts, block_hash, txn_hashes, entry); + result.emplace_back(std::move(entry)); + } + + template + Result eth_simulate_impl( + Chain const &chain, std::vector> calls, + BlockHeader const &header, uint64_t const base_block_number, + bytes32_t const &block_id, bytes32_t const &grandparent_id, + std::vector> senders, + std::vector>>> + authorities, + mpt::RODb &db, vm::VM &vm, fiber::FiberGroup &tx_exec_pool, + struct monad_state_override_vec const &state_overrides, + struct monad_block_override_vec const &block_overrides, + bool emit_native_transfer_logs) + { + // TODO(dhil): Geth allows up to 256 blocks to be simulated, including + // synthetic blocks inserted to fill in possible gaps in the block + // overrides. + static constexpr size_t MAX_CALLS = 256; + // TODO(dhil): Other providers allow a maximum of 16 blocks to be + // simulated (c.f. + // https://docs.metamask.io/services/reference/ethereum/json-rpc-methods/eth_simulatev1). + + // TODO(dhil): Decide on the default timestamp increment. + static constexpr uint64_t DEFAULT_TIMESTAMP_INCREMENT = 1; + + MONAD_ASSERT(calls.size() == senders.size()); + MONAD_ASSERT(calls.size() == authorities.size()); + MONAD_ASSERT(calls.size() == state_overrides.size); + MONAD_ASSERT(calls.size() == block_overrides.size); + + for (size_t i = 0; i < calls.size(); ++i) { + MONAD_ASSERT(calls[i].size() == senders[i].size()); + MONAD_ASSERT(calls[i].size() == authorities[i].size()); + } + + // Validate the inputs before constructing the simulation objects. This + // validation procedure throws on bad input. + eth_simulate_validate_inputs( + MAX_CALLS, + DEFAULT_TIMESTAMP_INCREMENT, + calls, + block_overrides, + header); + + TrieRODb tdb{db}; + tdb.set_block_and_prefix(base_block_number, block_id); + + // Initialize the chain context buffer. + auto context_buffer = ChainContextBuffer{}; + // Load grandparent context if available. + if (MONAD_LIKELY(base_block_number > 0)) { + auto const grandparent_transactions = monad::get_transactions( + db, base_block_number - 1, grandparent_id); + MONAD_ASSERT_THROW( + grandparent_transactions.has_value(), + GRANDPARENT_TRANSACTIONS_CONTEXT_ERR_MSG); + auto const &[grandparent_senders, grandparent_authorities] = + recover_senders_and_authorities( + grandparent_transactions.assume_value()); + context_buffer.advance( + grandparent_senders, grandparent_authorities); + } + // Load parent context. + std::optional base_block_hash = std::nullopt; + { + auto const parent_transactions = + monad::get_transactions(db, base_block_number, block_id); + MONAD_ASSERT_THROW( + parent_transactions.has_value(), + PARENT_TRANSACTIONS_CONTEXT_ERR_MSG); + auto const &[parent_senders, parent_authorities] = + recover_senders_and_authorities( + parent_transactions.assume_value()); + context_buffer.advance(parent_senders, parent_authorities); + + // If the base block is in-flight then we compute a mock block hash + // using the header and the loaded transactions. + if (block_id != bytes32_t{}) { + Block const base_block{ + .header = header, + .transactions = parent_transactions.assume_value(), + }; + base_block_hash = + to_bytes(keccak256(rlp::encode_block(base_block))); + } + } + + // Instantiate the block hash buffer. We simulate on top of the base + // block, thus the head block is `base_block_number + 1`. + EthSimulateBlockHashBuffer block_hash_buffer{ + db, base_block_number + 1, base_block_hash}; + + // Simulate blocks including possibly synthetic blocks. + auto result = nlohmann::json::array(); + BlockHeader previous_header = header; + std::vector> const empty_call_frames{}; + + auto block_state = BlockState{tdb, vm}; + for (size_t block_idx = 0; block_idx < calls.size(); ++block_idx) { + auto const &bo = block_overrides.overrides[block_idx]; + + // First we have to check whether we need to insert synthetic blocks + // to fill in the gap between the previous block and block induced + // by `block_idx`. + size_t const gap = bo.number.value_or(previous_header.number + 1) - + previous_header.number; + // No-op for gap == 1. + for (size_t i = 1; i < gap; ++i) { + BlockHeader const synthetic_header{ + .parent_hash = + block_hash_buffer.get(previous_header.number), + .number = previous_header.number + 1, + // NOTE(dhil): Synthetic blocks carry forward the previous + // gas limit. + .gas_limit = previous_header.gas_limit, + // TODO(dhil): Better Monad timestamp simulation (e.g. pack + // multiple blocks into the same timestamp). + .timestamp = + previous_header.timestamp + DEFAULT_TIMESTAMP_INCREMENT, + // NOTE(dhil): Synthetic blocks carry forward the block + // beneficiary. + .beneficiary = previous_header.beneficiary, + }; + Block const synthetic_block{ + .header = synthetic_header, + }; + + auto block_metrics = BlockMetrics{}; + auto call_tracers = + std::vector>{}; + auto state_tracers = + std::vector>{}; + + static std::vector
empty_senders{}; + static std::vector>> + empty_authorities{}; + + auto const chain_context = + context_buffer.advance(empty_senders, empty_authorities); + + BOOST_OUTCOME_TRY( + auto const receipts, + execute_block( + chain, + synthetic_block, + empty_senders, + empty_authorities, + block_state, + block_hash_buffer, + tx_exec_pool, + block_metrics, + call_tracers, + state_tracers, + chain_context, + emit_native_transfer_logs)); + + bytes32_t const synthetic_block_hash = to_bytes(keccak256( + rlp::encode_block_header(synthetic_block.header))); + block_hash_buffer.advance(synthetic_block_hash); + + save_eth_simulate_log_entry( + synthetic_block, + receipts, + empty_call_frames, + synthetic_block_hash, + {}, + result); + + previous_header = synthetic_block.header; + } + // By this point it must be the case that the distance between the + // previous block and the block we are about to construct is + // exactly 1. + MONAD_ASSERT( + bo.number.value_or(previous_header.number + 1) - + previous_header.number == + 1); + + // Construct the block header. + BlockHeader const current_header{ + .parent_hash = block_hash_buffer.get(previous_header.number), + .prev_randao = bo.prev_randao.value_or(bytes32_t{}), + // NOTE(dhil): The possible increment by one is correct by + // construction of the synthetic blocks. + .number = bo.number.value_or(previous_header.number + 1), + // NOTE(dhil): The default is to inherit the **previous** + // header's gas limit irrespective of whether it is a real + // block, a synthetic block, or a user-defined block. + .gas_limit = bo.gas_limit.value_or(previous_header.gas_limit), + // TODO(dhil): Better Monad timestamp simulation (e.g. pack + // multiple blocks into the same timestamp). + .timestamp = bo.time.value_or( + previous_header.timestamp + DEFAULT_TIMESTAMP_INCREMENT), + .beneficiary = + bo.fee_recipient.value_or(previous_header.beneficiary), + .base_fee_per_gas = bo.base_fee_per_gas, + }; + + // Construct state + // State overrides are applied with an incarnation in the *previous* + // block, rather than with the current header's block number. + auto const override_incarnation = Incarnation{ + base_block_number + block_idx, Incarnation::LAST_TX - 1u}; + apply_state_overrides( + block_state, + override_incarnation, + state_overrides.overrides[block_idx]); + + // Patch up transactions with valid chain_id, signature, and nonce + // so that they can pass validation in execute_block. + { + State state{block_state, override_incarnation}; + + for (size_t tx_idx = 0; tx_idx < calls[block_idx].size(); + ++tx_idx) { + Transaction &tx = calls[block_idx][tx_idx]; + + tx.sc.chain_id = chain.get_chain_id(); + tx.sc.r = 1; + tx.sc.s = 1; + + // Update tx.nonce to match the expected nonce in the + // current block state. + tx.nonce = state.get_nonce(senders[block_idx][tx_idx]); + state.set_nonce(senders[block_idx][tx_idx], tx.nonce + 1); + } + } + + auto block_metrics = BlockMetrics{}; + auto call_frames = std::vector>{}; + call_frames.reserve(calls[block_idx].size()); + auto call_tracers = std::vector>{}; + call_tracers.reserve(calls[block_idx].size()); + auto state_tracers = + std::vector>{}; + state_tracers.reserve(calls[block_idx].size()); + + for (Transaction const &tx : calls[block_idx]) { + call_frames.emplace_back(); + call_tracers.emplace_back( + std::make_unique(tx, call_frames.back())); + state_tracers.emplace_back( + std::make_unique()); + } + + auto const chain_context = context_buffer.advance( + senders[block_idx], authorities[block_idx]); + + auto block = Block{ + .header = current_header, + .transactions = std::move(calls[block_idx]), + .withdrawals = bo.withdrawals, + }; + + BOOST_OUTCOME_TRY( + auto const receipts, + execute_block( + chain, + block, + senders[block_idx], + authorities[block_idx], + block_state, + block_hash_buffer, + tx_exec_pool, + block_metrics, + call_tracers, + state_tracers, + chain_context, + emit_native_transfer_logs)); + + // Patch up the block header for results reporting. + // TODO(dhil): Report gas used for Ethereum? + if constexpr (is_monad_trait_v) { + // Receipts have cumulative gas_used (YP eq. 22), so + // the last receipt's value is the total for the block. + size_t const gas_used = + receipts.empty() ? 0 : receipts.back().gas_used; + block.header.gas_used = gas_used; + if (!bo.gas_limit.has_value() && header.gas_limit == 0) { + block.header.gas_limit = gas_used; + } + } + + bytes32_t const block_hash = + to_bytes(keccak256(rlp::encode_block_header(block.header))); + block_hash_buffer.advance(block_hash); + + std::vector txn_hashes{}; + txn_hashes.reserve(block.transactions.size()); + for (Transaction const &txn : block.transactions) { + txn_hashes.emplace_back( + to_bytes(keccak256(rlp::encode_transaction(txn)))); + } + + save_eth_simulate_log_entry( + block, receipts, call_frames, block_hash, txn_hashes, result); + + previous_header = current_header; + } + + return result; + } } namespace monad @@ -1338,6 +1995,185 @@ struct monad_executor } }); } + + void submit_eth_simulate_to_pool( + monad_chain_config const chain_config, + std::vector> calls, + std::vector> senders, + struct monad_state_override_vec const *const state_overrides, + struct monad_block_override_vec const *const block_overrides, + BlockHeader const &block_header, uint64_t const block_number, + bytes32_t const &block_id, bytes32_t const &grandparent_id, + bool emit_native_transfer_logs, + void (*complete)(monad_executor_result *, void *user), void *const user) + { + monad_executor_result *const result = new monad_executor_result(); + + if (!trace_block_group_.try_enqueue()) { + result->status_code = EVMC_REJECTED; + result->message = strdup(ETH_SIMULATE_EXCEED_QUEUE_SIZE_ERR_MSG); + MONAD_ASSERT(result->message); + complete(result, user); + return; + } + + auto const priority = + call_seq_no_.fetch_add(1, std::memory_order_relaxed); + trace_block_group_.group->submit( + priority, + [calls = std::move(calls), + senders = std::move(senders), + state_overrides = state_overrides, + block_overrides = block_overrides, + block_header = block_header, + base_block_number = block_number, + block_id = block_id, + grandparent_id = grandparent_id, + chain_config = chain_config, + &db = db_, + emit_native_transfer_logs = emit_native_transfer_logs, + fiber_group = &trace_block_group_, + tx_exec_group = &trace_tx_exec_group_, + &vm = vm_, + complete = complete, + result = result, + user = user]() { + try { + fiber_group->queued_count.fetch_sub( + 1, std::memory_order_relaxed); + fiber_group->executing_count.fetch_add( + 1, std::memory_order_relaxed); + BOOST_SCOPE_EXIT_ALL(&fiber_group) + { + fiber_group->executing_count.fetch_sub( + 1, std::memory_order_relaxed); + }; + + auto const res = [&]() -> Result { + auto authorities = std::vector< + std::vector>>>( + calls.size()); + for (auto block_idx = 0u; block_idx < calls.size(); + ++block_idx) { + authorities[block_idx] = std::vector< + std::vector>>( + calls[block_idx].size()); + for (auto tx_idx = 0u; + tx_idx < calls[block_idx].size(); + ++tx_idx) { + authorities[block_idx][tx_idx] = + std::vector>( + calls[block_idx][tx_idx] + .authorization_list.size()); + for (auto auth_idx = 0u; + auth_idx < calls[block_idx][tx_idx] + .authorization_list.size(); + ++auth_idx) { + authorities[block_idx][tx_idx][auth_idx] = + recover_authority( + calls[block_idx][tx_idx] + .authorization_list[auth_idx]); + } + } + } + + auto const chain = + [chain_config] -> std::unique_ptr { + switch (chain_config) { + case CHAIN_CONFIG_ETHEREUM_MAINNET: + return std::make_unique(); + case CHAIN_CONFIG_MONAD_DEVNET: + return std::make_unique(); + case CHAIN_CONFIG_MONAD_TESTNET: + return std::make_unique(); + case CHAIN_CONFIG_MONAD_MAINNET: + return std::make_unique(); + case CHAIN_CONFIG_HIVE_NET: + return std::make_unique(); + } + MONAD_ASSERT(false); + }(); + + if (chain_config == CHAIN_CONFIG_ETHEREUM_MAINNET || + chain_config == CHAIN_CONFIG_HIVE_NET) { + evmc_revision const rev = chain->get_revision( + block_header.number, block_header.timestamp); + SWITCH_EVM_TRAITS( + eth_simulate_impl, + *chain, + calls, + block_header, + base_block_number, + block_id, + grandparent_id, + senders, + authorities, + db, + vm, + *tx_exec_group->group, + *state_overrides, + *block_overrides, + emit_native_transfer_logs); + MONAD_ASSERT(false); + } + else { + auto const rev = + dynamic_cast(chain.get()) + ->get_monad_revision( + block_header.timestamp); + SWITCH_MONAD_TRAITS( + eth_simulate_impl, + *chain, + calls, + block_header, + base_block_number, + block_id, + grandparent_id, + senders, + authorities, + db, + vm, + *tx_exec_group->group, + *state_overrides, + *block_overrides, + emit_native_transfer_logs); + MONAD_ASSERT(false); + } + }(); + + if (MONAD_UNLIKELY(res.has_error())) { + result->status_code = EVMC_REJECTED; + result->message = strdup(res.error().message().c_str()); + MONAD_ASSERT(result->message); + complete(result, user); + return; + } + std::vector cbor_state_trace = + nlohmann::json::to_cbor(res.assume_value()); + result->encoded_trace = + new uint8_t[cbor_state_trace.size()]; + result->encoded_trace_len = cbor_state_trace.size(); + memcpy( + result->encoded_trace, + cbor_state_trace.data(), + cbor_state_trace.size()); + + complete(result, user); + } + catch (MonadException const &e) { + result->status_code = EVMC_INTERNAL_ERROR; + result->message = strdup(e.message()); + MONAD_ASSERT(result->message); + complete(result, user); + } + catch (...) { + result->status_code = EVMC_INTERNAL_ERROR; + result->message = strdup(UNEXPECTED_EXCEPTION_ERR_MSG); + MONAD_ASSERT(result->message); + complete(result, user); + } + }); + } }; monad_executor *monad_executor_create( @@ -1492,3 +2328,118 @@ void monad_executor_run_transactions( user, tracer_config); } + +namespace +{ + template + using decoder_value_t = typename decltype(Decoder( + std::declval()))::value_type; + + template + auto decode_nested_items(byte_string_view &input) + -> Result>>> + { + using Item = decoder_value_t; + auto ret = std::vector>{}; + + BOOST_OUTCOME_TRY(auto outer_payload, rlp::parse_list_metadata(input)); + while (!outer_payload.empty()) { + ret.emplace_back(); + + BOOST_OUTCOME_TRY( + auto inner_payload, rlp::parse_list_metadata(outer_payload)); + + if constexpr (explicit_parse_string) { + while (!inner_payload.empty()) { + BOOST_OUTCOME_TRY( + auto item_payload, + rlp::parse_string_metadata(inner_payload)); + BOOST_OUTCOME_TRY(Item const item, Decoder(item_payload)); + ret.back().emplace_back(std::move(item)); + } + } + else { + while (!inner_payload.empty()) { + BOOST_OUTCOME_TRY(Item const item, Decoder(inner_payload)); + ret.back().emplace_back(std::move(item)); + } + } + } + + return ret; + } +} + +void monad_executor_eth_simulate_submit( + struct monad_executor *executor, enum monad_chain_config chain_config, + uint8_t const *const rlp_senders, size_t rlp_senders_len, + uint8_t const *const rlp_calls, size_t rlp_calls_len, uint64_t block_number, + uint8_t const *const rlp_header, size_t rlp_header_len, + uint8_t const *const rlp_block_id, size_t rlp_block_id_len, + uint8_t const *const rlp_grandparent_block_id, + size_t const rlp_grandparent_block_id_len, + struct monad_state_override_vec const *const state_overrides, + struct monad_block_override_vec const *const block_overrides, + bool emit_native_transfer_logs, + void (*complete)(monad_executor_result *, void *user), void *user) +{ + + MONAD_ASSERT(executor); + MONAD_ASSERT(rlp_senders); + MONAD_ASSERT(rlp_calls); + MONAD_ASSERT(rlp_header); + MONAD_ASSERT(rlp_block_id); + MONAD_ASSERT(rlp_grandparent_block_id); + MONAD_ASSERT(state_overrides); + MONAD_ASSERT(block_overrides); + + byte_string_view rlp_senders_view{rlp_senders, rlp_senders_len}; + auto const maybe_senders = + decode_nested_items(rlp_senders_view); + MONAD_ASSERT(maybe_senders.has_value()); + auto const &senders = maybe_senders.assume_value(); + + byte_string_view rlp_calls_view{rlp_calls, rlp_calls_len}; + auto const maybe_txns = + decode_nested_items(rlp_calls_view); + MONAD_ASSERT(maybe_txns.has_value()); + auto const &txns = maybe_txns.assume_value(); + + MONAD_ASSERT(senders.size() == txns.size()); + MONAD_ASSERT(state_overrides->size == txns.size()); + MONAD_ASSERT(block_overrides->size == txns.size()); + + byte_string_view rlp_header_view({rlp_header, rlp_header_len}); + auto const block_header_result = rlp::decode_block_header(rlp_header_view); + MONAD_ASSERT(!block_header_result.has_error()); + MONAD_ASSERT(rlp_header_view.empty()); + auto const &block_header = block_header_result.value(); + + byte_string_view block_id_view({rlp_block_id, rlp_block_id_len}); + auto const block_id_result = rlp::decode_bytes32(block_id_view); + MONAD_ASSERT(!block_id_result.has_error()); + MONAD_ASSERT(block_id_view.empty()); + auto const block_id = block_id_result.value(); + + byte_string_view grandparent_block_id_view( + {rlp_grandparent_block_id, rlp_grandparent_block_id_len}); + auto const grandparent_block_id_result = + rlp::decode_bytes32(grandparent_block_id_view); + MONAD_ASSERT(!grandparent_block_id_result.has_error()); + MONAD_ASSERT(grandparent_block_id_view.empty()); + auto const grandparent_block_id = grandparent_block_id_result.value(); + + executor->submit_eth_simulate_to_pool( + chain_config, + txns, + senders, + state_overrides, + block_overrides, + block_header, + block_number, + block_id, + grandparent_block_id, + emit_native_transfer_logs, + complete, + user); +} diff --git a/category/rpc/monad_executor.h b/category/rpc/monad_executor.h index 4ea066a1c..0b184c983 100644 --- a/category/rpc/monad_executor.h +++ b/category/rpc/monad_executor.h @@ -120,6 +120,19 @@ void monad_executor_run_transactions( void (*complete)(monad_executor_result *, void *user), void *user, enum monad_tracer_config); +void monad_executor_eth_simulate_submit( + struct monad_executor *, enum monad_chain_config, + uint8_t const *rlp_senders, size_t rlp_senders_len, + uint8_t const *rlp_calls, size_t rlp_calls_len, uint64_t block_number, + uint8_t const *rlp_header, size_t rlp_header_len, + uint8_t const *rlp_block_id, size_t rlp_block_id_len, + uint8_t const *rlp_grandparent_block_id, + size_t rlp_grandparent_block_id_len, + struct monad_state_override_vec const *const state_overrides, + struct monad_block_override_vec const *const block_overrides, + bool emit_native_transfer_logs, + void (*complete)(monad_executor_result *, void *user), void *user); + #ifdef __cplusplus } #endif diff --git a/category/rpc/monad_executor_test.cpp b/category/rpc/monad_executor_test.cpp index 170604a19..08342e9a4 100644 --- a/category/rpc/monad_executor_test.cpp +++ b/category/rpc/monad_executor_test.cpp @@ -33,10 +33,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -82,6 +84,7 @@ #include #include #include +#include #include #include #include @@ -89,6 +92,7 @@ #include #include #include +#include #include #include @@ -4378,10 +4382,9 @@ TEST(BlockOverride, set_all_fields) fee_bytes[31] = 9; set_block_override_base_fee_per_gas(bo, fee_bytes, sizeof(fee_bytes)); - uint8_t blob_fee_bytes[32] = {}; - blob_fee_bytes[31] = 1; - set_block_override_blob_base_fee( - bo, blob_fee_bytes, sizeof(blob_fee_bytes)); + add_block_override_withdrawal(bo, 1, 2, 3, addr_bytes, sizeof(addr_bytes)); + addr_bytes[19] = 0xCD; + add_block_override_withdrawal(bo, 4, 5, 6, addr_bytes, sizeof(addr_bytes)); EXPECT_EQ(bo->number, 42u); EXPECT_EQ(bo->time, 1700000000u); @@ -4399,8 +4402,20 @@ TEST(BlockOverride, set_all_fields) ASSERT_TRUE(bo->base_fee_per_gas.has_value()); EXPECT_EQ(*bo->base_fee_per_gas, uint256_t{9}); - ASSERT_TRUE(bo->blob_base_fee.has_value()); - EXPECT_EQ(*bo->blob_base_fee, uint256_t{1}); + ASSERT_TRUE(bo->withdrawals.has_value()); + EXPECT_EQ(bo->withdrawals->size(), 2u); + EXPECT_EQ(bo->withdrawals->at(0).index, 1u); + EXPECT_EQ(bo->withdrawals->at(0).validator_index, 2u); + EXPECT_EQ(bo->withdrawals->at(0).amount, 3u); + EXPECT_EQ( + bo->withdrawals->at(0).recipient, + 0x00000000000000000000000000000000000000ab_address); + EXPECT_EQ(bo->withdrawals->at(1).index, 4u); + EXPECT_EQ(bo->withdrawals->at(1).validator_index, 5u); + EXPECT_EQ(bo->withdrawals->at(1).amount, 6u); + EXPECT_EQ( + bo->withdrawals->at(1).recipient, + 0x00000000000000000000000000000000000000cd_address); monad_block_override_destroy(bo); } @@ -4422,7 +4437,7 @@ TEST(BlockOverride, partial_fields) EXPECT_EQ(bo->gas_limit, std::nullopt); EXPECT_EQ(bo->fee_recipient, std::nullopt); EXPECT_EQ(bo->prev_randao, std::nullopt); - EXPECT_EQ(bo->blob_base_fee, std::nullopt); + EXPECT_EQ(bo->withdrawals, std::nullopt); monad_block_override_destroy(bo); } @@ -4438,14 +4453,6 @@ TEST(BlockOverride, uint256_big_endian) ASSERT_TRUE(bo->base_fee_per_gas.has_value()); EXPECT_EQ(*bo->base_fee_per_gas, uint256_t{9}); - // blob_base_fee = 0x0100 = 256 in big-endian - uint8_t blob_fee[32] = {}; - blob_fee[30] = 0x01; - blob_fee[31] = 0x00; - set_block_override_blob_base_fee(bo, blob_fee, sizeof(blob_fee)); - ASSERT_TRUE(bo->blob_base_fee.has_value()); - EXPECT_EQ(*bo->blob_base_fee, uint256_t{256}); - monad_block_override_destroy(bo); } @@ -4483,3 +4490,2482 @@ TEST(BlockOverride, prev_randao_32_bytes) monad_block_override_destroy(bo); } + +TEST(StateOverrideVec, create_destroy) +{ + auto *vec = monad_state_override_vec_create(2); + ASSERT_NE(vec, nullptr); + EXPECT_EQ(vec->size, 2u); + ASSERT_NE(vec->overrides, nullptr); + monad_state_override_vec_destroy(vec); +} + +TEST(StateOverrideVec, set_fields_at_index) +{ + auto *vec = monad_state_override_vec_create(2); + + Address addr{}; + addr.bytes[19] = 0xAB; + + uint8_t balance_bytes[32] = {}; + balance_bytes[31] = 7; + + uint8_t const code_bytes[] = {0x60, 0x00, 0x55}; + + bytes32_t state_key{}; + state_key.bytes[31] = 0x01; + bytes32_t state_value{}; + state_value.bytes[31] = 0x02; + + bytes32_t state_diff_key{}; + state_diff_key.bytes[31] = 0x03; + bytes32_t state_diff_value{}; + state_diff_value.bytes[31] = 0x04; + + add_override_address_at(vec, 1, addr.bytes, sizeof(addr.bytes)); + set_override_balance_at( + vec, + 1, + addr.bytes, + sizeof(addr.bytes), + balance_bytes, + sizeof(balance_bytes)); + set_override_nonce_at(vec, 1, addr.bytes, sizeof(addr.bytes), 42); + set_override_code_at( + vec, 1, addr.bytes, sizeof(addr.bytes), code_bytes, sizeof(code_bytes)); + set_override_state_at( + vec, + 1, + addr.bytes, + sizeof(addr.bytes), + state_key.bytes, + sizeof(state_key.bytes), + state_value.bytes, + sizeof(state_value.bytes)); + set_override_state_diff_at( + vec, + 1, + addr.bytes, + sizeof(addr.bytes), + state_diff_key.bytes, + sizeof(state_diff_key.bytes), + state_diff_value.bytes, + sizeof(state_diff_value.bytes)); + + EXPECT_TRUE(vec->overrides[0].override_sets.empty()); + + auto const set_it = vec->overrides[1].override_sets.find(addr); + ASSERT_NE(set_it, vec->overrides[1].override_sets.end()); + auto const &obj = set_it->second; + + ASSERT_TRUE(obj.balance.has_value()); + EXPECT_EQ(*obj.balance, uint256_t{7}); + + ASSERT_TRUE(obj.nonce.has_value()); + EXPECT_EQ(*obj.nonce, 42u); + + ASSERT_TRUE(obj.code.has_value()); + byte_string const expected_code{ + code_bytes, code_bytes + sizeof(code_bytes)}; + EXPECT_EQ(*obj.code, expected_code); + + auto const state_it = obj.state.find(state_key); + ASSERT_NE(state_it, obj.state.end()); + EXPECT_EQ(state_it->second, state_value); + + auto const state_diff_it = obj.state_diff.find(state_diff_key); + ASSERT_NE(state_diff_it, obj.state_diff.end()); + EXPECT_EQ(state_diff_it->second, state_diff_value); + + monad_state_override_vec_destroy(vec); +} + +TEST(BlockOverrideVec, create_destroy) +{ + auto *vec = monad_block_override_vec_create(2); + ASSERT_NE(vec, nullptr); + EXPECT_EQ(vec->size, 2u); + ASSERT_NE(vec->overrides, nullptr); + monad_block_override_vec_destroy(vec); +} + +TEST(BlockOverrideVec, set_fields_at_index) +{ + auto *vec = monad_block_override_vec_create(2); + + uint8_t addr_bytes[20] = {}; + addr_bytes[19] = 0xAB; + + uint8_t randao_bytes[32] = {}; + randao_bytes[0] = 0xFF; + randao_bytes[31] = 0x01; + + uint8_t fee_bytes[32] = {}; + fee_bytes[31] = 9; + + set_block_override_number_at(vec, 1, 42); + set_block_override_time_at(vec, 1, 1700000000); + set_block_override_gas_limit_at(vec, 1, 30'000'000); + set_block_override_fee_recipient_at(vec, 1, addr_bytes, sizeof(addr_bytes)); + set_block_override_prev_randao_at( + vec, 1, randao_bytes, sizeof(randao_bytes)); + set_block_override_base_fee_per_gas_at( + vec, 1, fee_bytes, sizeof(fee_bytes)); + add_block_override_withdrawal_at( + vec, 1, 1, 2, 3, addr_bytes, sizeof(addr_bytes)); + addr_bytes[19] = 0xCD; + add_block_override_withdrawal_at( + vec, 1, 4, 5, 6, addr_bytes, sizeof(addr_bytes)); + + auto const &empty = vec->overrides[0]; + EXPECT_EQ(empty.number, std::nullopt); + EXPECT_EQ(empty.time, std::nullopt); + EXPECT_EQ(empty.gas_limit, std::nullopt); + EXPECT_EQ(empty.fee_recipient, std::nullopt); + EXPECT_EQ(empty.prev_randao, std::nullopt); + EXPECT_EQ(empty.base_fee_per_gas, std::nullopt); + EXPECT_EQ(empty.withdrawals, std::nullopt); + + auto const &bo = vec->overrides[1]; + EXPECT_EQ(bo.number, 42u); + EXPECT_EQ(bo.time, 1700000000u); + EXPECT_EQ(bo.gas_limit, 30'000'000u); + + ASSERT_TRUE(bo.fee_recipient.has_value()); + EXPECT_EQ( + *bo.fee_recipient, 0x00000000000000000000000000000000000000ab_address); + + ASSERT_TRUE(bo.prev_randao.has_value()); + EXPECT_EQ( + *bo.prev_randao, + 0xff00000000000000000000000000000000000000000000000000000000000001_bytes32); + + ASSERT_TRUE(bo.base_fee_per_gas.has_value()); + EXPECT_EQ(*bo.base_fee_per_gas, uint256_t{9}); + + ASSERT_TRUE(bo.withdrawals.has_value()); + ASSERT_EQ(bo.withdrawals->size(), 2u); + EXPECT_EQ(bo.withdrawals->at(0).index, 1u); + EXPECT_EQ(bo.withdrawals->at(0).validator_index, 2u); + EXPECT_EQ(bo.withdrawals->at(0).amount, 3u); + EXPECT_EQ( + bo.withdrawals->at(0).recipient, + 0x00000000000000000000000000000000000000ab_address); + EXPECT_EQ(bo.withdrawals->at(1).index, 4u); + EXPECT_EQ(bo.withdrawals->at(1).validator_index, 5u); + EXPECT_EQ(bo.withdrawals->at(1).amount, 6u); + EXPECT_EQ( + bo.withdrawals->at(1).recipient, + 0x00000000000000000000000000000000000000cd_address); + + monad_block_override_vec_destroy(vec); +} + +TEST_F(EthCallFixture, eth_simulate_v1_simple_transfer) +{ + static constexpr Address sender = + 0x00000000000000000000000000000000deadbeef_address; + static constexpr Address recipient = + 0x00000000000000000000000000000000feedface_address; + + commit_sequential( + tdb, + StateDeltas{ + {sender, + StateDelta{ + .account = + {std::nullopt, + Account{.balance = uint256_t{1'000'000}, .nonce = 0}}}}, + {recipient, + StateDelta{ + .account = + {std::nullopt, Account{.balance = 0, .nonce = 0}}}}}, + {}, + BlockHeader{.number = 0}); + + for (uint64_t i = 1; i < 256; ++i) { + commit_sequential(tdb, {}, {}, BlockHeader{.number = i}); + } + + auto *executor = create_executor(dbname.string()); + + auto *const state_override = monad_state_override_vec_create(1); + auto *const block_override = monad_block_override_vec_create(1); + + auto const rlp_senders = to_vec(rlp::encode_list2( + rlp::encode_list2(rlp::encode_address(std::make_optional(sender))))); + + // A simple transfer + Transaction const tx{ + .gas_limit = 200'000'000, + .value = uint256_t{1'000}, + .to = recipient, + }; + auto const encoded_tx = rlp::encode_transaction(tx); + auto const rlp_calls = to_vec(rlp::encode_list2( + rlp::encode_list2(rlp::encode_string2(byte_string_view(encoded_tx))))); + + // Header for the base block (block 1). + BlockHeader const header{ + .number = 1, + .gas_limit = 200'000'000, + }; + auto const rlp_header = to_vec(rlp::encode_block_header(header)); + auto const rlp_block_id = to_vec(rlp_finalized_id); + + struct callback_context ctx; + boost::fibers::future f = ctx.promise.get_future(); + + monad_executor_eth_simulate_submit( + executor, + CHAIN_CONFIG_MONAD_DEVNET, + rlp_senders.data(), + rlp_senders.size(), + rlp_calls.data(), + rlp_calls.size(), + 1, // block_number + rlp_header.data(), + rlp_header.size(), + rlp_block_id.data(), + rlp_block_id.size(), + rlp_finalized_id.data(), + rlp_finalized_id.size(), + state_override, + block_override, + false, + complete_callback, + (void *)&ctx); + f.get(); + + ASSERT_EQ(ctx.result->status_code, EVMC_SUCCESS); + ASSERT_TRUE(ctx.result->encoded_trace_len > 0); + + nlohmann::json output = nlohmann::json::from_cbor( + ctx.result->encoded_trace, + ctx.result->encoded_trace + ctx.result->encoded_trace_len); + + ASSERT_EQ(output.size(), 1); + ASSERT_EQ(output[0]["calls"].size(), 1); + EXPECT_EQ(output[0]["calls"][0]["status"], "0x1"); + EXPECT_EQ( + output[0]["calls"][0]["gasUsed"], std::format("0x{:x}", 200'000'000)); + EXPECT_EQ(output[0]["gasUsed"], std::format("0x{:x}", 200'000'000)); + + // Simulation should not affect the actual state, so the sender's balance + // should remain unchanged. + auto sender_account = tdb.read_account(sender); + ASSERT_TRUE(sender_account.has_value()); + EXPECT_EQ(sender_account->balance, uint256_t{1'000'000}); + + monad_block_override_vec_destroy(block_override); + monad_state_override_vec_destroy(state_override); + monad_executor_destroy(executor); +} + +// Similar to `eth_simulate_v1_simple_transfer`, but we simulate more than one +// block with transfers. +TEST_F(EthCallFixture, eth_simulate_v1_simple_transfers_multiple_blocks) +{ + static constexpr Address sender = + 0x00000000000000000000000000000000deadbeef_address; + static constexpr Address recipient = + 0x00000000000000000000000000000000feedface_address; + + commit_sequential( + tdb, + StateDeltas{ + {sender, + StateDelta{ + .account = + {std::nullopt, + Account{ + .balance = uint256_t{100} * + uint256_t{1'000'000'000'000'000'000ULL}, + .nonce = 0}}}}, + {recipient, + StateDelta{ + .account = + {std::nullopt, Account{.balance = 0, .nonce = 0}}}}}, + {}, + BlockHeader{.number = 0}); + + for (uint64_t i = 1; i < 256; ++i) { + commit_sequential(tdb, {}, {}, BlockHeader{.number = i}); + } + + auto *executor = create_executor(dbname.string()); + + auto *const state_overrides = monad_state_override_vec_create(2); + auto *const block_overrides = monad_block_override_vec_create(2); + + auto const encoded_sender = rlp::encode_address(std::make_optional(sender)); + auto const rlp_senders = to_vec(rlp::encode_list2( + rlp::encode_list2(encoded_sender, encoded_sender), + rlp::encode_list2(encoded_sender, encoded_sender, encoded_sender))); + + Transaction const tx0{ + .gas_limit = 200'000'000, + .value = uint256_t{1'000}, + .to = recipient, + }; + auto const encoded_tx0 = rlp::encode_string2(rlp::encode_transaction(tx0)); + auto const rlp_calls = to_vec(rlp::encode_list2( + rlp::encode_list2(encoded_tx0, encoded_tx0), + rlp::encode_list2(encoded_tx0, encoded_tx0, encoded_tx0))); + + // Header for the base block (block 1). + BlockHeader const header{ + .number = 1, + .gas_limit = 200'000'000, + }; + auto const rlp_header = to_vec(rlp::encode_block_header(header)); + auto const rlp_block_id = to_vec(rlp_finalized_id); + + struct callback_context ctx; + boost::fibers::future f = ctx.promise.get_future(); + + monad_executor_eth_simulate_submit( + executor, + CHAIN_CONFIG_MONAD_DEVNET, + rlp_senders.data(), + rlp_senders.size(), + rlp_calls.data(), + rlp_calls.size(), + 1, // block_number + rlp_header.data(), + rlp_header.size(), + rlp_block_id.data(), + rlp_block_id.size(), + rlp_finalized_id.data(), + rlp_finalized_id.size(), + state_overrides, + block_overrides, + false, + complete_callback, + (void *)&ctx); + f.get(); + + ASSERT_EQ(ctx.result->status_code, EVMC_SUCCESS); + ASSERT_TRUE(ctx.result->encoded_trace_len > 0); + + nlohmann::json output = nlohmann::json::from_cbor( + ctx.result->encoded_trace, + ctx.result->encoded_trace + ctx.result->encoded_trace_len); + + ASSERT_EQ(output.size(), 2); + ASSERT_EQ(output[0]["calls"].size(), 2); + for (size_t i = 0; i < output[0]["calls"].size(); ++i) { + EXPECT_EQ(output[0]["calls"][i]["status"], "0x1"); + EXPECT_EQ( + output[0]["calls"][i]["gasUsed"], + std::format("0x{:x}", 200'000'000)); + } + EXPECT_EQ(output[0]["gasUsed"], std::format("0x{:x}", 200'000'000 * 2)); + + ASSERT_EQ(output[1]["calls"].size(), 3); + for (size_t i = 0; i < output[1]["calls"].size(); ++i) { + EXPECT_EQ(output[1]["calls"][i]["status"], "0x1"); + EXPECT_EQ( + output[1]["calls"][i]["gasUsed"], + std::format("0x{:x}", 200'000'000)); + } + EXPECT_EQ(output[1]["gasUsed"], std::format("0x{:x}", 200'000'000 * 3)); + + // Simulation should not affect the actual state, so the sender's balance + // should remain unchanged. + auto sender_account = tdb.read_account(sender); + ASSERT_TRUE(sender_account.has_value()); + EXPECT_EQ( + sender_account->balance, + uint256_t{100} * uint256_t{1'000'000'000'000'000'000ULL}); + + monad_block_override_vec_destroy(block_overrides); + monad_state_override_vec_destroy(state_overrides); + monad_executor_destroy(executor); +} + +TEST_F(EthCallFixture, eth_simulate_v1_single_call_block_255) +{ + for (uint64_t i = 0; i < 256; ++i) { + commit_sequential(tdb, {}, {}, BlockHeader{.number = i}); + } + + auto *executor = create_executor(dbname.string()); + + // One sender for one simulated block. + Address const sender = 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266_address; + auto const rlp_senders = to_vec(rlp::encode_list2( + rlp::encode_list2(rlp::encode_address(std::make_optional(sender))))); + + // One simple EIP-1559 transfer: send 1 ETH to 0xdeadbeef... + Transaction const tx{ + .gas_limit = 200'000'000, + .value = uint256_t{1'000'000'000'000'000'000ULL}, // 1 ETH + .to = 0xdeadbeef00000000000000000000000000000000_address, + .type = TransactionType::eip1559, + }; + auto const encoded_tx = rlp::encode_transaction(tx); + auto const rlp_calls = to_vec(rlp::encode_list2( + rlp::encode_list2(rlp::encode_string2(byte_string_view(encoded_tx))))); + + // Header for the base block (block 255). + BlockHeader const header{ + .number = 255, + .gas_limit = 200'000'000, + }; + auto const rlp_header = to_vec(rlp::encode_block_header(header)); + auto const rlp_block_id = to_vec(rlp_finalized_id); + + // No state overrides, no block overrides (one empty entry per block). + auto *const so_overrides = monad_state_override_vec_create(1); + auto *const bo_overrides = monad_block_override_vec_create(1); + + struct callback_context ctx; + boost::fibers::future f = ctx.promise.get_future(); + + monad_executor_eth_simulate_submit( + executor, + CHAIN_CONFIG_MONAD_DEVNET, + rlp_senders.data(), + rlp_senders.size(), + rlp_calls.data(), + rlp_calls.size(), + 255, // simulate on top of block 255 + rlp_header.data(), + rlp_header.size(), + rlp_block_id.data(), + rlp_block_id.size(), + rlp_finalized_id.data(), + rlp_finalized_id.size(), + so_overrides, + bo_overrides, + false, + complete_callback, + (void *)&ctx); + f.get(); + + ASSERT_EQ(ctx.result->status_code, EVMC_SUCCESS); + + monad_block_override_vec_destroy(bo_overrides); + monad_state_override_vec_destroy(so_overrides); + + monad_executor_destroy(executor); +} + +TEST_F(EthCallFixture, eth_simulate_v1_empty_input) +{ + for (uint64_t i = 0; i < 256; ++i) { + commit_sequential(tdb, {}, {}, BlockHeader{.number = i}); + } + + auto *executor = create_executor(dbname.string()); + + // Empty nested lists for senders and calls. + auto const rlp_senders = to_vec(rlp::encode_list2(byte_string{})); + auto const rlp_calls = to_vec(rlp::encode_list2(byte_string{})); + + BlockHeader const header{ + .number = 255, + .gas_limit = 200'000'000, + }; + auto const rlp_header = to_vec(rlp::encode_block_header(header)); + auto const rlp_block_id = to_vec(rlp_finalized_id); + + // Empty overrides vectors. + auto *const so_overrides = monad_state_override_vec_create(0); + auto *const bo_overrides = monad_block_override_vec_create(0); + + struct callback_context ctx; + boost::fibers::future f = ctx.promise.get_future(); + + monad_executor_eth_simulate_submit( + executor, + CHAIN_CONFIG_MONAD_DEVNET, + rlp_senders.data(), + rlp_senders.size(), + rlp_calls.data(), + rlp_calls.size(), + 255, + rlp_header.data(), + rlp_header.size(), + rlp_block_id.data(), + rlp_block_id.size(), + rlp_finalized_id.data(), + rlp_finalized_id.size(), + so_overrides, + bo_overrides, + false, + complete_callback, + (void *)&ctx); + f.get(); + + ASSERT_EQ(ctx.result->status_code, EVMC_INTERNAL_ERROR); + ASSERT_STREQ(ctx.result->message, "empty input"); + + monad_block_override_vec_destroy(bo_overrides); + monad_state_override_vec_destroy(so_overrides); + monad_executor_destroy(executor); +} + +TEST_F(EthCallFixture, eth_simulate_v1_block_override_synthetic_gap) +{ + for (uint64_t i = 0; i < 256; ++i) { + commit_sequential(tdb, {}, {}, BlockHeader{.number = i}); + } + + auto *executor = create_executor(dbname.string()); + + // One sender for one simulated block. + Address const sender = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266_address; + auto const rlp_senders = to_vec(rlp::encode_list2( + rlp::encode_list2(rlp::encode_address(std::make_optional(sender))))); + + // One simple EIP-1559 transfer: send 1 ETH to 0xdeadbeef... + Transaction const tx{ + .gas_limit = 200'000'000, + .value = uint256_t{1'000'000'000'000'000'000ULL}, // 1 ETH + .to = 0xdeadbeef00000000000000000000000000000000_address, + .type = TransactionType::eip1559, + }; + auto const encoded_tx = rlp::encode_transaction(tx); + auto const rlp_calls = to_vec(rlp::encode_list2( + rlp::encode_list2(rlp::encode_string2(byte_string_view(encoded_tx))))); + + // Header for the base block (block 255). + BlockHeader const header{ + .number = 255, + .gas_limit = 200'000'000, + }; + auto const rlp_header = to_vec(rlp::encode_block_header(header)); + auto const rlp_block_id = to_vec(rlp_finalized_id); + + // One state override (empty) and one block override with number = 511, + // causing synthetic blocks 256..510 to be inserted. + auto *const so_overrides = monad_state_override_vec_create(1); + auto *const bo_overrides = monad_block_override_vec_create(1); + set_block_override_number_at(bo_overrides, 0, 511); + set_block_override_gas_limit_at(bo_overrides, 0, 200'000'000); + set_block_override_time_at(bo_overrides, 0, 512); + + struct callback_context ctx; + boost::fibers::future f = ctx.promise.get_future(); + + monad_executor_eth_simulate_submit( + executor, + CHAIN_CONFIG_MONAD_DEVNET, + rlp_senders.data(), + rlp_senders.size(), + rlp_calls.data(), + rlp_calls.size(), + 255, // simulate on top of block 255 + rlp_header.data(), + rlp_header.size(), + rlp_block_id.data(), + rlp_block_id.size(), + rlp_finalized_id.data(), + rlp_finalized_id.size(), + so_overrides, + bo_overrides, + false, + complete_callback, + (void *)&ctx); + f.get(); + + ASSERT_EQ(ctx.result->status_code, EVMC_SUCCESS); + ASSERT_TRUE(ctx.result->encoded_trace_len > 0); + + nlohmann::json output = nlohmann::json::from_cbor( + ctx.result->encoded_trace, + ctx.result->encoded_trace + ctx.result->encoded_trace_len); + + ASSERT_EQ(output.size(), 256); + for (size_t i = 0; i < 255; i++) { + EXPECT_EQ(output[i]["number"], std::format("0x{:x}", 256 + i)); + EXPECT_EQ(output[i]["timestamp"], std::format("0x{:x}", i + 1)); + } + EXPECT_EQ(output[255]["number"], "0x1ff"); + EXPECT_EQ(output[255]["timestamp"], "0x200"); + + monad_block_override_vec_destroy(bo_overrides); + monad_state_override_vec_destroy(so_overrides); + + monad_executor_destroy(executor); +} + +TEST_F(EthCallFixture, eth_simulate_v1_block_override_no_synthetic_gaps) +{ + for (uint64_t i = 0; i < 256; ++i) { + commit_sequential(tdb, {}, {}, BlockHeader{.number = i}); + } + + auto const encode_rlp_list = + [](std::vector const &items) -> byte_string { + byte_string payload; + for (auto const &item : items) { + payload += item; + } + return rlp::encode_list2(payload); + }; + + auto *executor = create_executor(dbname.string()); + + // One sender for one simulated block. + Address const ping = 0xdeadbeef_address; + Address const pong = 0xfeedface_address; + + std::vector encoded_senders{}; + encoded_senders.reserve(256); + std::vector encoded_calls{}; + encoded_calls.reserve(256); + auto *const bo_overrides = monad_block_override_vec_create(256); + auto *const so_overrides = monad_state_override_vec_create(256); + for (size_t i = 0; i < 256; i++) { + bool const is_even = (i % 2 == 0); + auto const encoded_tx = rlp::encode_transaction(Transaction{ + .gas_limit = 200'000'000, + .value = uint256_t{1}, + .to = is_even ? ping : pong, + .type = TransactionType::eip1559, + }); + encoded_calls.push_back(rlp::encode_list2( + rlp::encode_string2(byte_string_view(encoded_tx)))); + encoded_senders.push_back(rlp::encode_list2(rlp::encode_address( + std::optional
{is_even ? pong : ping}))); + set_block_override_number_at(bo_overrides, i, 256 + i); + set_block_override_time_at(bo_overrides, i, 256 + i * 10); + } + + auto const rlp_senders = to_vec(encode_rlp_list(encoded_senders)); + auto const rlp_calls = to_vec(encode_rlp_list(encoded_calls)); + + // Header for the base block (block 255). + BlockHeader const header{ + .number = 255, + .gas_limit = 200'000'000, + }; + auto const rlp_header = to_vec(rlp::encode_block_header(header)); + auto const rlp_block_id = to_vec(rlp_finalized_id); + + struct callback_context ctx; + boost::fibers::future f = ctx.promise.get_future(); + + monad_executor_eth_simulate_submit( + executor, + CHAIN_CONFIG_MONAD_DEVNET, + rlp_senders.data(), + rlp_senders.size(), + rlp_calls.data(), + rlp_calls.size(), + 255, // simulate on top of block 255 + rlp_header.data(), + rlp_header.size(), + rlp_block_id.data(), + rlp_block_id.size(), + rlp_finalized_id.data(), + rlp_finalized_id.size(), + so_overrides, + bo_overrides, + false, + complete_callback, + (void *)&ctx); + f.get(); + + ASSERT_EQ(ctx.result->status_code, EVMC_SUCCESS); + ASSERT_TRUE(ctx.result->encoded_trace_len > 0); + + nlohmann::json output = nlohmann::json::from_cbor( + ctx.result->encoded_trace, + ctx.result->encoded_trace + ctx.result->encoded_trace_len); + + ASSERT_EQ(output.size(), 256); + for (size_t i = 0; i < 256; i++) { + EXPECT_EQ(output[i]["number"], std::format("0x{:x}", 256 + i)); + std::string const expected_timestamp = + std::format("0x{:x}", 256 + i * 10); + EXPECT_EQ(output[i]["timestamp"], expected_timestamp); + } + + monad_block_override_vec_destroy(bo_overrides); + monad_state_override_vec_destroy(so_overrides); + + monad_executor_destroy(executor); +} + +TEST_F(EthCallFixture, eth_simulate_v1_stress_queue_rejection) +{ + for (uint64_t i = 0; i < 256; ++i) { + commit_sequential(tdb, {}, {}, BlockHeader{.number = i}); + } + + // Create executor with a tiny queue limit (2) for the block pool + // so that excess submissions get rejected. + monad_executor_pool_config const block_conf = {1, 2, max_timeout, 2}; + monad_executor_pool_config const default_conf = {1, 2, max_timeout, 1000}; + unsigned const tx_exec_num_fibers = 10; + auto *executor = monad_executor_create( + default_conf, + default_conf, + block_conf, + tx_exec_num_fibers, + node_lru_max_mem, + dbname.string().c_str()); + + // Build a minimal valid simulation payload (one call, no overrides). + Address const sender = ADDR_A; + auto const rlp_senders = to_vec(rlp::encode_list2( + rlp::encode_list2(rlp::encode_address(std::make_optional(sender))))); + + Transaction const tx{ + .gas_limit = 200'000'000, + .value = uint256_t{1'000'000'000'000'000'000ULL}, + .to = 0xdeadbeef00000000000000000000000000000000_address, + .type = TransactionType::eip1559, + }; + auto const encoded_tx = rlp::encode_transaction(tx); + auto const rlp_calls = to_vec(rlp::encode_list2( + rlp::encode_list2(rlp::encode_string2(byte_string_view(encoded_tx))))); + + BlockHeader const header{ + .number = 255, + .gas_limit = 200'000'000, + }; + auto const rlp_header = to_vec(rlp::encode_block_header(header)); + auto const rlp_block_id = to_vec(rlp_finalized_id); + + constexpr size_t N = 20; + + // Each submission needs its own overrides (they are consumed). + struct submission + { + callback_context ctx; + boost::fibers::future future; + monad_state_override_vec *so; + monad_block_override_vec *bo; + }; + + std::vector> subs; + subs.reserve(N); + + // Fire off N submissions. + for (size_t i = 0; i < N; ++i) { + subs.emplace_back(std::make_unique()); + subs[i]->future = subs[i]->ctx.promise.get_future(); + subs[i]->so = monad_state_override_vec_create(1); + subs[i]->bo = monad_block_override_vec_create(1); + + monad_executor_eth_simulate_submit( + executor, + CHAIN_CONFIG_MONAD_DEVNET, + rlp_senders.data(), + rlp_senders.size(), + rlp_calls.data(), + rlp_calls.size(), + 255, + rlp_header.data(), + rlp_header.size(), + rlp_block_id.data(), + rlp_block_id.size(), + rlp_finalized_id.data(), + rlp_finalized_id.size(), + subs[i]->so, + subs[i]->bo, + false, + complete_callback, + (void *)&subs[i]->ctx); + } + + // Wait for all to complete and tally results. + size_t accepted = 0; + size_t rejected = 0; + for (auto &s : subs) { + s->future.get(); + + if (s->ctx.result->status_code == EVMC_REJECTED) { + EXPECT_STREQ( + s->ctx.result->message, + "failure to submit eth_simulateV1 to thread pool: queue size " + "exceeded"); + ++rejected; + } + else { + EXPECT_EQ(s->ctx.result->status_code, EVMC_SUCCESS); + ASSERT_TRUE(s->ctx.result->encoded_trace_len > 0); + nlohmann::json output = nlohmann::json::from_cbor( + s->ctx.result->encoded_trace, + s->ctx.result->encoded_trace + + s->ctx.result->encoded_trace_len); + ASSERT_EQ(output.size(), 1); + EXPECT_EQ(output[0]["number"], "0x100"); + EXPECT_EQ(output[0]["calls"].size(), 1); + ++accepted; + } + + monad_block_override_vec_destroy(s->bo); + monad_state_override_vec_destroy(s->so); + } + + EXPECT_GT(accepted, 0u); + EXPECT_GT(rejected, 0u); + EXPECT_EQ(accepted + rejected, N); + + monad_executor_destroy(executor); +} + +// Test reserve balance behavior in eth_simulate. +// +// Monad has a reserve balance of 10 MON (10e18 wei). EOAs are allowed to dip +// into their reserve on the first transaction in a block, but subsequent +// transactions from the same sender in the same block are disallowed from +// dipping. +// +// Setup: +// sender_a: 11 MON (just above reserve) +// sender_b: 100 MON (well above reserve) +// +// Block layout (single block with 3 transactions): +// - tx0: sender_a sends 2 MON to recipient -> succeeds (first tx, allowed to +// dip: 11-2=9 < 10) +// - tx1: sender_b sends 2 MON to recipient -> succeeds (100-2=98 > 10, no +// dipping needed) +// - tx2: sender_a sends 1 wei to recipient -> reverts (second tx from +// sender_a, cannot dip) +TEST_F(EthCallFixture, eth_simulate_v1_reserve_balance) +{ + static constexpr auto WEI_PER_MON = uint256_t{1'000'000'000'000'000'000ULL}; + + static constexpr Address sender_a = + 0x00000000000000000000000000000000deadbeef_address; + static constexpr Address sender_b = + 0x00000000000000000000000000000000cafebabe_address; + static constexpr Address recipient = + 0x00000000000000000000000000000000feedface_address; + + commit_sequential( + tdb, + StateDeltas{ + {sender_a, + StateDelta{ + .account = + {std::nullopt, + Account{ + .balance = uint256_t{11} * WEI_PER_MON, + .nonce = 0}}}}, + {sender_b, + StateDelta{ + .account = + {std::nullopt, + Account{ + .balance = uint256_t{100} * WEI_PER_MON, + .nonce = 0}}}}, + {recipient, + StateDelta{ + .account = + {std::nullopt, Account{.balance = 0, .nonce = 0}}}}}, + {}, + BlockHeader{.number = 0}); + + for (uint64_t i = 1; i < 256; ++i) { + commit_sequential(tdb, {}, {}, BlockHeader{.number = i}); + } + + auto *executor = create_executor(dbname.string()); + + // 3 senders for 1 block: [sender_a, sender_b, sender_a] + auto const encoded_sender_a = + rlp::encode_address(std::make_optional(sender_a)); + auto const encoded_sender_b = + rlp::encode_address(std::make_optional(sender_b)); + auto const rlp_senders = to_vec(rlp::encode_list2(rlp::encode_list2( + encoded_sender_a, encoded_sender_b, encoded_sender_a))); + + // tx0: sender_a sends 2 MON (dips into reserve, allowed as first tx) + Transaction const tx_dip{ + .gas_limit = 200'000'000, + .value = uint256_t{2} * WEI_PER_MON, + .to = recipient, + }; + // tx1: sender_b sends 1 wei (plenty of balance, no dipping) + Transaction const tx_safe{ + .gas_limit = 200'000'000, + .value = uint256_t{2}, + .to = recipient, + }; + // tx2: sender_a sends 2 MON (second tx from sender_a, cannot dip -> + // reverts) + Transaction const tx_repeat{ + .gas_limit = 200'000'000, + .value = uint256_t{1} * WEI_PER_MON, + .to = recipient, + }; + + auto const encoded_tx_dip = + rlp::encode_string2(rlp::encode_transaction(tx_dip)); + auto const encoded_tx_safe = + rlp::encode_string2(rlp::encode_transaction(tx_safe)); + auto const encoded_tx_repeat = + rlp::encode_string2(rlp::encode_transaction(tx_repeat)); + auto const rlp_calls = to_vec(rlp::encode_list2( + rlp::encode_list2(encoded_tx_dip, encoded_tx_safe, encoded_tx_repeat))); + + BlockHeader const header{ + .number = 1, + .gas_limit = 200'000'000, + }; + auto const rlp_header = to_vec(rlp::encode_block_header(header)); + auto const rlp_block_id = to_vec(rlp_finalized_id); + + auto *const so = monad_state_override_vec_create(1); + auto *const bo = monad_block_override_vec_create(1); + + struct callback_context ctx; + boost::fibers::future f = ctx.promise.get_future(); + + monad_executor_eth_simulate_submit( + executor, + CHAIN_CONFIG_MONAD_DEVNET, + rlp_senders.data(), + rlp_senders.size(), + rlp_calls.data(), + rlp_calls.size(), + 1, + rlp_header.data(), + rlp_header.size(), + rlp_block_id.data(), + rlp_block_id.size(), + rlp_finalized_id.data(), + rlp_finalized_id.size(), + so, + bo, + false, + complete_callback, + (void *)&ctx); + f.get(); + + ASSERT_EQ(ctx.result->status_code, EVMC_SUCCESS); + ASSERT_TRUE(ctx.result->encoded_trace_len > 0); + + nlohmann::json output = nlohmann::json::from_cbor( + ctx.result->encoded_trace, + ctx.result->encoded_trace + ctx.result->encoded_trace_len); + + ASSERT_EQ(output.size(), 1); + ASSERT_EQ(output[0]["calls"].size(), 3); + + // tx0: sender_a dips into reserve (first tx in block) -> success + EXPECT_EQ(output[0]["calls"][0]["status"], "0x1"); + + // tx1: sender_b has plenty of balance -> success + EXPECT_EQ(output[0]["calls"][1]["status"], "0x1"); + + // tx2: sender_a tries again (second tx in block) -> reverts + EXPECT_EQ(output[0]["calls"][2]["status"], "0x0"); + EXPECT_TRUE(output[0]["calls"][2].contains("error")); + + monad_block_override_vec_destroy(bo); + monad_state_override_vec_destroy(so); + monad_executor_destroy(executor); +} + +// Test that the ChainContextBuffer sliding window correctly prevents reserve +// balance dipping across simulated blocks. +// +// Case 1: Sender history +// 4 blocks, each with one tx from sender_x (11 MON, dips below 10 MON +// reserve on transfer). +// Block 0: succeeds (grandparent/parent empty, first occurrence) +// Block 1: reverts (sender_x in parent context) +// Block 2: reverts (sender_x in both parent and grandparent) +// Block 6: succeeds (sender_x aged out of parent/grandparent contexts due +// to synthetic gap blocks 3..5) +// +// Case 2: Authority history +// 4 blocks. In block 0, a different sender (other) sends an EIP-7702 tx +// whose authorization_list recovers to sender_x. This puts sender_x into +// the combined senders_and_authorities set. +// Block 0: other sends tx with auth for sender_x -> succeeds +// Block 1: sender_x sends transfer -> reverts (sender_x in parent via +// authority) +// Block 2: sender_x sends transfer -> reverts (sender_x in grandparent +// via authority) +// Block 6: sender_x sends transfer -> succeeds (sender_x aged out of +// parent/grandparent contexts) +TEST_F(EthCallFixture, eth_simulate_v1_reserve_balance_chain_context_buffer) +{ + static constexpr auto WEI_PER_MON = uint256_t{1'000'000'000'000'000'000ULL}; + + // sender_x is the address recovered from the known test AuthorizationEntry + // (see test_transaction.cpp). Using this address lets us also test the + // authority path without needing to generate new ECDSA signatures. + static constexpr Address sender_x = + 0xc7f24cef4eed1f110196d7d939b388ac1caeb21d_address; + static constexpr Address other = + 0x00000000000000000000000000000000cafebabe_address; + static constexpr Address recipient = + 0x00000000000000000000000000000000feedface_address; + + commit_sequential( + tdb, + StateDeltas{ + {sender_x, + StateDelta{ + .account = + {std::nullopt, + Account{ + .balance = uint256_t{11} * WEI_PER_MON, + // Nonce starts at 1 so the EIP-7702 auth entry + // (nonce=0) won't match during execution, preventing + // the delegation designation from being set on + // sender_x. The authority is still recovered from the + // signature and enters the combined + // senders_and_authorities set. + .nonce = 1}}}}, + {other, + StateDelta{ + .account = + {std::nullopt, + Account{ + .balance = uint256_t{100} * WEI_PER_MON, + .nonce = 0}}}}, + {recipient, + StateDelta{ + .account = + {std::nullopt, Account{.balance = 0, .nonce = 0}}}}}, + {}, + BlockHeader{.number = 0}); + + for (uint64_t i = 1; i < 256; ++i) { + commit_sequential(tdb, {}, {}, BlockHeader{.number = i}); + } + + // Case 1: Sender in parent / grandparent + { + auto *executor = create_executor(dbname.string()); + + // 4 blocks, each with 1 tx from sender_x transferring 2 MON. + // Block 3 has a block override setting number=7, creating synthetic + // gap blocks 5 and 6 (empty senders) that push sender_x out of the + // K=3 sliding window. + auto const encoded_sender_x = + rlp::encode_address(std::make_optional(sender_x)); + auto const rlp_senders = to_vec(rlp::encode_list2( + rlp::encode_list2(encoded_sender_x), + rlp::encode_list2(encoded_sender_x), + rlp::encode_list2(encoded_sender_x), + rlp::encode_list2(encoded_sender_x))); + + Transaction const tx_dip{ + .gas_limit = 200'000'000, + .value = uint256_t{2} * WEI_PER_MON, + .to = recipient, + }; + auto const encoded_tx_dip = + rlp::encode_string2(rlp::encode_transaction(tx_dip)); + auto const rlp_calls = to_vec(rlp::encode_list2( + rlp::encode_list2(encoded_tx_dip), + rlp::encode_list2(encoded_tx_dip), + rlp::encode_list2(encoded_tx_dip), + rlp::encode_list2(encoded_tx_dip))); + + BlockHeader const header{.number = 1, .gas_limit = 200'000'000}; + auto const rlp_header = to_vec(rlp::encode_block_header(header)); + auto const rlp_block_id = to_vec(rlp_finalized_id); + + auto *const so = monad_state_override_vec_create(4); + auto *const bo = monad_block_override_vec_create(4); + set_block_override_number_at(bo, 3, 7); + set_block_override_gas_limit_at(bo, 3, 200'000'000); + + struct callback_context ctx; + boost::fibers::future f = ctx.promise.get_future(); + + monad_executor_eth_simulate_submit( + executor, + CHAIN_CONFIG_MONAD_DEVNET, + rlp_senders.data(), + rlp_senders.size(), + rlp_calls.data(), + rlp_calls.size(), + 1, + rlp_header.data(), + rlp_header.size(), + rlp_block_id.data(), + rlp_block_id.size(), + rlp_finalized_id.data(), + rlp_finalized_id.size(), + so, + bo, + false, + complete_callback, + (void *)&ctx); + f.get(); + + ASSERT_EQ(ctx.result->status_code, EVMC_SUCCESS); + ASSERT_TRUE(ctx.result->encoded_trace_len > 0); + + nlohmann::json output = nlohmann::json::from_cbor( + ctx.result->encoded_trace, + ctx.result->encoded_trace + ctx.result->encoded_trace_len); + + // 3 user blocks + 2 synthetic gap blocks + 1 user block = 6. + ASSERT_EQ(output.size(), 6); + + // Block 0 (number 2): sender_x first time -> succeeds (allowed to dip) + ASSERT_EQ(output[0]["calls"].size(), 1); + EXPECT_EQ(output[0]["calls"][0]["status"], "0x1"); + + // Block 1 (number 3): sender_x in parent -> reverts + ASSERT_EQ(output[1]["calls"].size(), 1); + EXPECT_EQ(output[1]["calls"][0]["status"], "0x0"); + + // Block 2 (number 4): sender_x in parent and grandparent -> reverts + ASSERT_EQ(output[2]["calls"].size(), 1); + EXPECT_EQ(output[2]["calls"][0]["status"], "0x0"); + + // Blocks 3-4 (numbers 5, 6): synthetic gap blocks (no txs) + EXPECT_EQ(output[3]["number"], "0x5"); + EXPECT_EQ(output[4]["number"], "0x6"); + + // Block 5 (number 7): sender_x again, but 2 empty synthetic blocks + // pushed it out of the K=3 window -> succeeds + ASSERT_EQ(output[5]["calls"].size(), 1); + EXPECT_EQ(output[5]["calls"][0]["status"], "0x1"); + EXPECT_EQ(output[5]["number"], "0x7"); + + monad_block_override_vec_destroy(bo); + monad_state_override_vec_destroy(so); + monad_executor_destroy(executor); + } + + // Case 2: Authority in parent / grandparent + // Use a known AuthorizationEntry whose recovered authority is sender_x. + { + auto *executor = create_executor(dbname.string()); + + // Build an EIP-7702 tx from `other` that has an authorization whose + // recovered signer is sender_x. + AuthorizationEntry const auth_for_sender_x{ + .sc = + { + .r = intx::from_string( + "200243422738954192737895577305537705175585899164895777" + "58020700015851504969560"), + .s = intx::from_string( + "530584326759386138899955455622746682303141934549216843" + "63060655866328293077815"), + .chain_id = uint256_t{20143}, + .y_parity = 0, + }, + .address = 0xdeadbeef00000000000000000000000000000000_address, + .nonce = 0, + }; + + // Block 0: `other` sends an EIP-7702 tx (puts sender_x into + // senders_and_authorities via authority recovery). + // Block 1: `sender_x` sends a transfer that dips -> should revert + // (sender_x in parent via authority). + // Block 2: `sender_x` sends a transfer that dips -> should revert + // (sender_x in grandparent via authority). + // Block 3: (number 7, gap from 4 -> 7) sender_x dips -> succeeds + // (2 empty synthetic blocks pushed authority out of window). + Transaction const tx_with_auth{ + .gas_limit = 200'000'000, + .value = uint256_t{1}, + .to = recipient, + .type = TransactionType::eip7702, + .authorization_list = {auth_for_sender_x}, + }; + Transaction const tx_dip{ + .gas_limit = 200'000'000, + .value = uint256_t{2} * WEI_PER_MON, + .to = recipient, + }; + + auto const encoded_other = + rlp::encode_address(std::make_optional(other)); + auto const encoded_sender_x = + rlp::encode_address(std::make_optional(sender_x)); + auto const rlp_senders = to_vec(rlp::encode_list2( + rlp::encode_list2(encoded_other), + rlp::encode_list2(encoded_sender_x), + rlp::encode_list2(encoded_sender_x), + rlp::encode_list2(encoded_sender_x))); + + auto const encoded_tx_auth = + rlp::encode_string2(rlp::encode_transaction(tx_with_auth)); + auto const encoded_tx_dip = + rlp::encode_string2(rlp::encode_transaction(tx_dip)); + auto const rlp_calls = to_vec(rlp::encode_list2( + rlp::encode_list2(encoded_tx_auth), + rlp::encode_list2(encoded_tx_dip), + rlp::encode_list2(encoded_tx_dip), + rlp::encode_list2(encoded_tx_dip))); + + BlockHeader const header{.number = 1, .gas_limit = 200'000'000}; + auto const rlp_header = to_vec(rlp::encode_block_header(header)); + auto const rlp_block_id = to_vec(rlp_finalized_id); + + auto *const so = monad_state_override_vec_create(4); + auto *const bo = monad_block_override_vec_create(4); + set_block_override_number_at(bo, 3, 7); + set_block_override_gas_limit_at(bo, 3, 200'000'000); + + struct callback_context ctx; + boost::fibers::future f = ctx.promise.get_future(); + + monad_executor_eth_simulate_submit( + executor, + CHAIN_CONFIG_MONAD_DEVNET, + rlp_senders.data(), + rlp_senders.size(), + rlp_calls.data(), + rlp_calls.size(), + 1, + rlp_header.data(), + rlp_header.size(), + rlp_block_id.data(), + rlp_block_id.size(), + rlp_finalized_id.data(), + rlp_finalized_id.size(), + so, + bo, + false, + complete_callback, + (void *)&ctx); + f.get(); + + ASSERT_EQ(ctx.result->status_code, EVMC_SUCCESS); + ASSERT_TRUE(ctx.result->encoded_trace_len > 0); + + nlohmann::json output = nlohmann::json::from_cbor( + ctx.result->encoded_trace, + ctx.result->encoded_trace + ctx.result->encoded_trace_len); + + // 3 user blocks + 2 synthetic gap blocks + 1 user block = 6. + ASSERT_EQ(output.size(), 6); + + // First block: `other` sends EIP-7702 tx -> succeeds. + ASSERT_EQ(output[0]["calls"].size(), 1); + EXPECT_EQ(output[0]["calls"][0]["status"], "0x1"); + EXPECT_EQ(output[0]["number"], "0x2"); + + // Second block: sender_x dips -> reverts due to parent via + // authority. + ASSERT_EQ(output[1]["calls"].size(), 1); + EXPECT_EQ(output[1]["calls"][0]["status"], "0x0"); + EXPECT_EQ(output[1]["number"], "0x3"); + + // Third block: sender_x dips -> reverts due to grandparent via + // authority. + ASSERT_EQ(output[2]["calls"].size(), 1); + EXPECT_EQ(output[2]["calls"][0]["status"], "0x0"); + EXPECT_EQ(output[2]["number"], "0x4"); + + // Fourth and fifth blocks: synthetic gap blocks (no txs). + EXPECT_EQ(output[3]["number"], "0x5"); + EXPECT_EQ(output[4]["number"], "0x6"); + + // Sixth block: sender_x dips -> succeeds as the authority has been + // pushed out of the K = 3 window by 2 empty synthetic blocks. + // sender_x is NOT delegated because its nonce (1) didn't match the + // auth entry's nonce (0), so the delegation was skipped. + ASSERT_EQ(output[5]["calls"].size(), 1); + EXPECT_EQ(output[5]["calls"][0]["status"], "0x1"); + EXPECT_EQ(output[5]["number"], "0x7"); + + monad_block_override_vec_destroy(bo); + monad_state_override_vec_destroy(so); + monad_executor_destroy(executor); + } +} + +// Test that eth_simulate correctly logs contract calls using each of the +// four EVM call types: CALL, STATICCALL, DELEGATECALL, and CALLCODE. +// +// Each call type is exercised by a dedicated wrapper contract that invokes a +// target contract (which simply STOPs), stores the subcall success flag, and +// returns it as 32-byte output. We submit 4 transactions in a single simulated +// block and verify each one succeeds with the expected return data. +TEST_F(EthCallFixture, eth_simulate_v1_call_types) +{ + using namespace monad::vm::utils; + using namespace evm_as::sugar; + + static constexpr Address sender = + 0x00000000000000000000000000000000deadbeef_address; + static constexpr Address target = + 0x00000000000000000000000000000000eeeeeeee_address; + static constexpr Address call_wrapper = + 0x00000000000000000000000000000000ca110001_address; + static constexpr Address staticcall_wrapper = + 0x00000000000000000000000000000000ca110002_address; + static constexpr Address delegatecall_wrapper = + 0x00000000000000000000000000000000ca110003_address; + static constexpr Address callcode_wrapper = + 0x00000000000000000000000000000000ca110004_address; + + struct CompiledCode + { + std::vector bytecode; + bytes32_t hash; + std::shared_ptr icode; + }; + + auto const compile = [](auto const &eb) -> CompiledCode { + MONAD_ASSERT(evm_as::validate(eb)); + std::vector bytecode{}; + evm_as::compile(eb, bytecode); + byte_string_view const view{bytecode.data(), bytecode.size()}; + return { + std::move(bytecode), + to_bytes(keccak256(view)), + vm::make_shared_intercode(view), + }; + }; + + // Target contract + CompiledCode const target_cc = compile(evm_as::latest().stop()); + + // CALL wrapper: CALL(target); MSTORE result; RETURN 32 bytes + CompiledCode const call_cc = compile(evm_as::latest() + .call({.address = target}) + .push0() + .mstore() + .return_(0, 32)); + + // STATICCALL wrapper + CompiledCode const staticcall_cc = + compile(evm_as::latest() + .staticcall({.address = target}) + .push0() + .mstore() + .return_(0, 32)); + + // DELEGATECALL wrapper + CompiledCode const delegatecall_cc = + compile(evm_as::latest() + .delegatecall({.address = target}) + .push0() + .mstore() + .return_(0, 32)); + + // CALLCODE wrapper + CompiledCode const callcode_cc = compile(evm_as::latest() + .callcode({.address = target}) + .push0() + .mstore() + .return_(0, 32)); + + commit_sequential( + tdb, + StateDeltas{ + {sender, + StateDelta{ + .account = + {std::nullopt, + Account{ + .balance = std::numeric_limits::max(), + .nonce = 0}}}}, + {target, + StateDelta{ + .account = + {std::nullopt, + Account{.balance = 0, .code_hash = target_cc.hash}}}}, + {call_wrapper, + StateDelta{ + .account = + {std::nullopt, + Account{.balance = 0, .code_hash = call_cc.hash}}}}, + {staticcall_wrapper, + StateDelta{ + .account = + {std::nullopt, + Account{.balance = 0, .code_hash = staticcall_cc.hash}}}}, + {delegatecall_wrapper, + StateDelta{ + .account = + {std::nullopt, + Account{ + .balance = 0, .code_hash = delegatecall_cc.hash}}}}, + {callcode_wrapper, + StateDelta{ + .account = + {std::nullopt, + Account{.balance = 0, .code_hash = callcode_cc.hash}}}}}, + Code{ + {target_cc.hash, target_cc.icode}, + {call_cc.hash, call_cc.icode}, + {staticcall_cc.hash, staticcall_cc.icode}, + {delegatecall_cc.hash, delegatecall_cc.icode}, + {callcode_cc.hash, callcode_cc.icode}, + }, + BlockHeader{.number = 0}); + + for (uint64_t i = 1; i < 256; ++i) { + commit_sequential(tdb, {}, {}, BlockHeader{.number = i}); + } + + auto *executor = create_executor(dbname.string()); + + // 4 transactions from sender, each calling a different wrapper. + auto const enc_sender = rlp::encode_address(std::make_optional(sender)); + auto const rlp_senders = to_vec(rlp::encode_list2( + rlp::encode_list2(enc_sender, enc_sender, enc_sender, enc_sender))); + + Transaction const tx_call{ + .gas_limit = 200'000'000, .value = 0, .to = call_wrapper}; + Transaction const tx_staticcall{ + .gas_limit = 200'000'000, .value = 0, .to = staticcall_wrapper}; + Transaction const tx_delegatecall{ + .gas_limit = 200'000'000, .value = 0, .to = delegatecall_wrapper}; + Transaction const tx_callcode{ + .gas_limit = 200'000'000, .value = 0, .to = callcode_wrapper}; + + auto const enc_call = rlp::encode_string2(rlp::encode_transaction(tx_call)); + auto const enc_staticcall = + rlp::encode_string2(rlp::encode_transaction(tx_staticcall)); + auto const enc_delegatecall = + rlp::encode_string2(rlp::encode_transaction(tx_delegatecall)); + auto const enc_callcode = + rlp::encode_string2(rlp::encode_transaction(tx_callcode)); + auto const rlp_calls = to_vec(rlp::encode_list2(rlp::encode_list2( + enc_call, enc_staticcall, enc_delegatecall, enc_callcode))); + + BlockHeader const header{.number = 1, .gas_limit = 800'000'000}; + auto const rlp_header = to_vec(rlp::encode_block_header(header)); + auto const rlp_block_id = to_vec(rlp_finalized_id); + + auto *const so = monad_state_override_vec_create(1); + auto *const bo = monad_block_override_vec_create(1); + + struct callback_context ctx; + boost::fibers::future f = ctx.promise.get_future(); + + monad_executor_eth_simulate_submit( + executor, + CHAIN_CONFIG_MONAD_DEVNET, + rlp_senders.data(), + rlp_senders.size(), + rlp_calls.data(), + rlp_calls.size(), + 1, + rlp_header.data(), + rlp_header.size(), + rlp_block_id.data(), + rlp_block_id.size(), + rlp_finalized_id.data(), + rlp_finalized_id.size(), + so, + bo, + false, + complete_callback, + (void *)&ctx); + f.get(); + + ASSERT_EQ(ctx.result->status_code, EVMC_SUCCESS); + ASSERT_TRUE(ctx.result->encoded_trace_len > 0); + + nlohmann::json output = nlohmann::json::from_cbor( + ctx.result->encoded_trace, + ctx.result->encoded_trace + ctx.result->encoded_trace_len); + + ASSERT_EQ(output.size(), 1); + ASSERT_EQ(output[0]["calls"].size(), 4); + + // Expected return data: 32-byte big-endian encoding of 1 (subcall success). + std::string const expected_return_data = + "0x0000000000000000000000000000000000000000000000000000000000000001"; + + // CALL + EXPECT_EQ(output[0]["calls"][0]["status"], "0x1"); + EXPECT_EQ(output[0]["calls"][0]["returnData"], expected_return_data); + + // STATICCALL + EXPECT_EQ(output[0]["calls"][1]["status"], "0x1"); + EXPECT_EQ(output[0]["calls"][1]["returnData"], expected_return_data); + + // DELEGATECALL + EXPECT_EQ(output[0]["calls"][2]["status"], "0x1"); + EXPECT_EQ(output[0]["calls"][2]["returnData"], expected_return_data); + + // CALLCODE + EXPECT_EQ(output[0]["calls"][3]["status"], "0x1"); + EXPECT_EQ(output[0]["calls"][3]["returnData"], expected_return_data); + + monad_block_override_vec_destroy(bo); + monad_state_override_vec_destroy(so); + monad_executor_destroy(executor); +} + +// Test that simulated state changes persist across blocks within the same +// eth_simulate call. Specifically: +// +// Block 1: account_a sends 60 MON to recipient -> success (has 100 MON) +// Block 2: account_a sends 60 MON to recipient -> failure (only ~40 MON left) +// Block 3: account_b sends 50 MON to account_a (refund) +// Block 4: account_a sends 60 MON to recipient -> success (~90 MON after +// refund) +TEST_F(EthCallFixture, eth_simulate_v1_state_changes_across_blocks) +{ + static constexpr auto WEI_PER_MON = uint256_t{1'000'000'000'000'000'000ULL}; + + static constexpr Address account_a = + 0x00000000000000000000000000000000aaaaaa01_address; + static constexpr Address account_b = + 0x00000000000000000000000000000000bbbbbb01_address; + static constexpr Address recipient = + 0x00000000000000000000000000000000feedface_address; + + commit_sequential( + tdb, + StateDeltas{ + {account_a, + StateDelta{ + .account = + {std::nullopt, + Account{ + .balance = uint256_t{100} * WEI_PER_MON, + .nonce = 0}}}}, + {account_b, + StateDelta{ + .account = + {std::nullopt, + Account{ + .balance = uint256_t{100} * WEI_PER_MON, + .nonce = 0}}}}, + {recipient, + StateDelta{ + .account = + {std::nullopt, Account{.balance = 0, .nonce = 0}}}}}, + {}, + BlockHeader{.number = 0}); + + for (uint64_t i = 1; i < 256; ++i) { + commit_sequential(tdb, {}, {}, BlockHeader{.number = i}); + } + + auto *executor = create_executor(dbname.string()); + + // Transactions. + Transaction const tx_a_sends_60{ + .gas_limit = 200'000'000, + .value = uint256_t{60} * WEI_PER_MON, + .to = recipient, + }; + Transaction const tx_b_refunds_a{ + .gas_limit = 200'000'000, + .value = uint256_t{50} * WEI_PER_MON, + .to = account_a, + }; + + auto const encoded_a = rlp::encode_address(std::make_optional(account_a)); + auto const encoded_b = rlp::encode_address(std::make_optional(account_b)); + + // Block 1: A sends 60 MON + // Block 2: A sends 60 MON (will fail) + // Block 3: B sends 50 MON to A + // Block 4: A sends 60 MON (will succeed after refund) + auto const rlp_senders = to_vec(rlp::encode_list2( + rlp::encode_list2(encoded_a), + rlp::encode_list2(encoded_a), + rlp::encode_list2(encoded_b), + rlp::encode_list2(encoded_a))); + + auto const encoded_tx_a = + rlp::encode_string2(rlp::encode_transaction(tx_a_sends_60)); + auto const encoded_tx_b = + rlp::encode_string2(rlp::encode_transaction(tx_b_refunds_a)); + auto const rlp_calls = to_vec(rlp::encode_list2( + rlp::encode_list2(encoded_tx_a), + rlp::encode_list2(encoded_tx_a), + rlp::encode_list2(encoded_tx_b), + rlp::encode_list2(encoded_tx_a))); + + BlockHeader const header{.number = 1, .gas_limit = 200'000'000}; + auto const rlp_header = to_vec(rlp::encode_block_header(header)); + auto const rlp_block_id = to_vec(rlp_finalized_id); + + auto *const so = monad_state_override_vec_create(4); + auto *const bo = monad_block_override_vec_create(4); + + struct callback_context ctx; + boost::fibers::future f = ctx.promise.get_future(); + + monad_executor_eth_simulate_submit( + executor, + CHAIN_CONFIG_MONAD_DEVNET, + rlp_senders.data(), + rlp_senders.size(), + rlp_calls.data(), + rlp_calls.size(), + 1, + rlp_header.data(), + rlp_header.size(), + rlp_block_id.data(), + rlp_block_id.size(), + rlp_finalized_id.data(), + rlp_finalized_id.size(), + so, + bo, + false, + complete_callback, + (void *)&ctx); + f.get(); + + ASSERT_EQ(ctx.result->status_code, EVMC_SUCCESS); + ASSERT_TRUE(ctx.result->encoded_trace_len > 0); + + nlohmann::json output = nlohmann::json::from_cbor( + ctx.result->encoded_trace, + ctx.result->encoded_trace + ctx.result->encoded_trace_len); + + ASSERT_EQ(output.size(), 4); + + // First simulated block: A sends 60 MON -> succeeds (A has 100 MON). + ASSERT_EQ(output[0]["calls"].size(), 1); + EXPECT_EQ(output[0]["calls"][0]["status"], "0x1"); + + // Second simulated block: A sends 60 MON -> reverts (A has ~40 MON after + // block 1). + ASSERT_EQ(output[1]["calls"].size(), 1); + EXPECT_EQ(output[1]["calls"][0]["status"], "0x0"); + + // Third simulated block: B sends 50 MON to A -> succeeds (B has 100 MON). + ASSERT_EQ(output[2]["calls"].size(), 1); + EXPECT_EQ(output[2]["calls"][0]["status"], "0x1"); + + // Fourth simulated block: A sends 60 MON -> succeeds (A has ~90 MON after + // refund). + ASSERT_EQ(output[3]["calls"].size(), 1); + EXPECT_EQ(output[3]["calls"][0]["status"], "0x1"); + + monad_block_override_vec_destroy(bo); + monad_state_override_vec_destroy(so); + monad_executor_destroy(executor); +} + +// Test contract deployment in one block followed by a call to the deployed +// contract in a subsequent block. The deployed contract sends half its received +// value to a beneficiary and returns the amount sent. +// +// Block 1: deploy the contract (CREATE tx) +// Block 2: call the deployed contract with 10 MON +// Block 3: call a balance-checker contract that returns the beneficiary's +// balance. +// +// We verify the call succeeds and the return data contains 5 MON (half the +// Deploy a contract in block 1 and call it in block 2. The deployed contract +// sends half its received value to a beneficiary (proving the code executes) +// and returns the amount sent. +TEST_F(EthCallFixture, eth_simulate_v1_deploy_and_call) +{ + using namespace monad::vm::utils; + + static constexpr auto WEI_PER_MON = uint256_t{1'000'000'000'000'000'000ULL}; + + static constexpr Address sender = + 0x00000000000000000000000000000000deadbeef_address; + static constexpr Address beneficiary = + 0x00000000000000000000000000000000cafef00d_address; + + // The deployed contract code computes msg.value / 2, logs it, sends it to + // the beneficiary, and returns it. Morally equivalent to the following + // pseudocode: + // let half = CALLVALUE / 2; + // LOG0(half); + // CALL(beneficiary, half, gas()); + // RETURN(half) + auto const eb_contract = evm_as::latest() + .push(2) + .callvalue() + .div() + .push0() + .mstore() + .log0(0, 32) + .push0() + .push0() + .push0() + .push0() + .mload(0) + .push(beneficiary) + .gas() + .call() + .pop() + .return_(0, 32); + + std::vector contract_bytecode{}; + ASSERT_TRUE(evm_as::validate(eb_contract)); + evm_as::compile(eb_contract, contract_bytecode); + + // Init code: copy runtime to memory, return it to deploy. + // Layout: [init_code (10 bytes)] [runtime_code] + static constexpr size_t INIT_CODE_SIZE = 10; + auto const init_eb = evm_as::latest() + .push(contract_bytecode.size()) + .push(INIT_CODE_SIZE) + .push0() + .codecopy() + .return_(0, contract_bytecode.size()); + + std::vector init_bytecode{}; + ASSERT_TRUE(evm_as::validate(init_eb)); + evm_as::compile(init_eb, init_bytecode); + ASSERT_EQ(init_bytecode.size(), INIT_CODE_SIZE); + + // Full deploy payload: init_code ++ runtime_code. + byte_string deploy_data(init_bytecode.begin(), init_bytecode.end()); + deploy_data.insert( + deploy_data.end(), contract_bytecode.begin(), contract_bytecode.end()); + + // Compute the address of the deployed contract. + Address const deployed = create_contract_address(sender, 0); + + commit_sequential( + tdb, + StateDeltas{ + {sender, + StateDelta{ + .account = + {std::nullopt, + Account{ + .balance = uint256_t{100} * WEI_PER_MON, + .nonce = 0}}}}, + {beneficiary, + StateDelta{ + .account = + {std::nullopt, Account{.balance = 0, .nonce = 0}}}}}, + {}, + BlockHeader{.number = 0}); + + for (uint64_t i = 1; i < 256; ++i) { + commit_sequential(tdb, {}, {}, BlockHeader{.number = i}); + } + + auto *executor = create_executor(dbname.string()); + + // Block 1: deploy (to = nullopt). + Transaction const deploy_tx{ + .gas_limit = 200'000'000, + .value = 0, + .to = std::nullopt, + .data = deploy_data, + }; + // Block 2: call deployed contract with 10 MON. + Transaction const call_tx{ + .gas_limit = 200'000'000, + .value = uint256_t{10} * WEI_PER_MON, + .to = deployed, + }; + + // Block 3: call a balance-checker contract (planted via state override) + // that returns BALANCE(beneficiary). + static constexpr Address checker = + 0x0000000000000000000000000000000000C0FFEE_address; + auto const checker_eb = evm_as::latest() + .push(beneficiary) + .balance() + .push0() + .mstore() + .return_(0, 32); + std::vector checker_bytecode{}; + ASSERT_TRUE(evm_as::validate(checker_eb)); + evm_as::compile(checker_eb, checker_bytecode); + + Transaction const check_tx{ + .gas_limit = 200'000'000, + .value = 0, + .to = checker, + }; + + auto const encoded_sender = rlp::encode_address(std::make_optional(sender)); + auto const rlp_senders = to_vec(rlp::encode_list2( + rlp::encode_list2(encoded_sender), + rlp::encode_list2(encoded_sender), + rlp::encode_list2(encoded_sender))); + + auto const encoded_deploy = + rlp::encode_string2(rlp::encode_transaction(deploy_tx)); + auto const encoded_call = + rlp::encode_string2(rlp::encode_transaction(call_tx)); + auto const encoded_check = + rlp::encode_string2(rlp::encode_transaction(check_tx)); + auto const rlp_calls = to_vec(rlp::encode_list2( + rlp::encode_list2(encoded_deploy), + rlp::encode_list2(encoded_call), + rlp::encode_list2(encoded_check))); + + BlockHeader const header{.number = 1, .gas_limit = 200'000'000}; + auto const rlp_header = to_vec(rlp::encode_block_header(header)); + auto const rlp_block_id = to_vec(rlp_finalized_id); + + auto *const so = monad_state_override_vec_create(3); + auto *const bo = monad_block_override_vec_create(3); + + // Plant the balance-checker bytecode at the checker address for block 3. + add_override_address_at(so, 2, checker.bytes, sizeof(checker.bytes)); + set_override_code_at( + so, + 2, + checker.bytes, + sizeof(checker.bytes), + checker_bytecode.data(), + checker_bytecode.size()); + + struct callback_context ctx; + boost::fibers::future f = ctx.promise.get_future(); + + monad_executor_eth_simulate_submit( + executor, + CHAIN_CONFIG_MONAD_DEVNET, + rlp_senders.data(), + rlp_senders.size(), + rlp_calls.data(), + rlp_calls.size(), + 1, + rlp_header.data(), + rlp_header.size(), + rlp_block_id.data(), + rlp_block_id.size(), + rlp_finalized_id.data(), + rlp_finalized_id.size(), + so, + bo, + false, + complete_callback, + (void *)&ctx); + f.get(); + + ASSERT_EQ(ctx.result->status_code, EVMC_SUCCESS); + ASSERT_TRUE(ctx.result->encoded_trace_len > 0); + + nlohmann::json output = nlohmann::json::from_cbor( + ctx.result->encoded_trace, + ctx.result->encoded_trace + ctx.result->encoded_trace_len); + + ASSERT_EQ(output.size(), 3); + + // First simulated block: deployment succeeds. + ASSERT_EQ(output[0]["calls"].size(), 1); + EXPECT_EQ(output[0]["calls"][0]["status"], "0x1"); + + // Second simulated block: calling the deployed contract succeeds and + // returns value/2. + ASSERT_EQ(output[1]["calls"].size(), 1); + EXPECT_EQ(output[1]["calls"][0]["status"], "0x1"); + + auto const expected_half = uint256_t{5} * WEI_PER_MON; + auto const expected_bytes = intx::be::store(expected_half); + auto const expected_return_data = + std::format("0x{}", to_hex(expected_bytes)); + EXPECT_EQ(output[1]["calls"][0]["returnData"], expected_return_data); + + ASSERT_EQ(output[1]["calls"][0]["logs"].size(), 1); + EXPECT_EQ(output[1]["calls"][0]["logs"][0]["data"], expected_return_data); + EXPECT_EQ(output[1]["calls"][0]["logs"][0]["topics"].size(), 0); + EXPECT_EQ(output[1]["calls"][0]["logs"][0]["blockNumber"], "0x3"); + + // Third simulated block: balance checker confirms beneficiary received 5 + // MON. + ASSERT_EQ(output[2]["calls"].size(), 1); + EXPECT_EQ(output[2]["calls"][0]["status"], "0x1"); + EXPECT_EQ(output[2]["calls"][0]["returnData"], expected_return_data); + + monad_block_override_vec_destroy(bo); + monad_state_override_vec_destroy(so); + monad_executor_destroy(executor); +} + +// Test that native transfer logs are emitted when emit_native_transfer_logs is +// true. A "forwarder" contract receives value from the sender and forwards +// half of it to a "sink" contract. With native transfer logging enabled, we +// expect two Transfer events emitted from the synthetic native-token address +// (0xeeee...eeee): +// +// 1. sender -> forwarder (10 MON) top-level value transfer +// 2. forwarder -> sink (5 MON) internal CALL value transfer +TEST_F(EthCallFixture, eth_simulate_v1_native_transfer_logs) +{ + using namespace monad::vm::utils; + + static constexpr auto WEI_PER_MON = uint256_t{1'000'000'000'000'000'000ULL}; + + static constexpr Address sender = + 0x00000000000000000000000000000000deadbeef_address; + static constexpr Address sink = + 0x00000000000000000000000000000000000051c0_address; + static constexpr Address forwarder_addr = + 0x000000000000000000000000000000000f0a4d01_address; + + // Sink contract: just STOPs (accepts value, does nothing). + auto const sink_eb = evm_as::latest().stop(); + std::vector sink_bytecode{}; + ASSERT_TRUE(evm_as::validate(sink_eb)); + evm_as::compile(sink_eb, sink_bytecode); + byte_string_view const sink_code{ + sink_bytecode.data(), sink_bytecode.size()}; + auto const sink_code_hash = to_bytes(keccak256(sink_code)); + auto const sink_icode = vm::make_shared_intercode(sink_code); + + // Forwarder contract: send CALLVALUE/2 to sink, then STOP. + // PUSH(2) CALLVALUE DIV => half = CALLVALUE/2 + // PUSH0 MSTORE mem[0] = half + // PUSH0 PUSH0 PUSH0 PUSH0 retSize=0 retOff=0 argsSize=0 argsOff=0 + // MLOAD(0) value = half + // PUSH(sink) address + // GAS gas + // CALL + // POP + // STOP + auto const fwd_eb = evm_as::latest() + .push(2) + .callvalue() + .div() + .push0() + .mstore() + .push0() + .push0() + .push0() + .push0() + .mload(0) + .push(sink) + .gas() + .call() + .pop() + .stop(); + std::vector fwd_bytecode{}; + ASSERT_TRUE(evm_as::validate(fwd_eb)); + evm_as::compile(fwd_eb, fwd_bytecode); + byte_string_view const fwd_code{fwd_bytecode.data(), fwd_bytecode.size()}; + auto const fwd_code_hash = to_bytes(keccak256(fwd_code)); + auto const fwd_icode = vm::make_shared_intercode(fwd_code); + + commit_sequential( + tdb, + StateDeltas{ + {sender, + StateDelta{ + .account = + {std::nullopt, + Account{ + .balance = uint256_t{100} * WEI_PER_MON, + .nonce = 0}}}}, + {sink, + StateDelta{ + .account = + {std::nullopt, + Account{ + .balance = 0, + .code_hash = sink_code_hash, + .nonce = 0}}}}, + {forwarder_addr, + StateDelta{ + .account = + {std::nullopt, + Account{ + .balance = 0, + .code_hash = fwd_code_hash, + .nonce = 0}}}}}, + Code{ + {sink_code_hash, sink_icode}, + {fwd_code_hash, fwd_icode}, + }, + BlockHeader{.number = 0}); + + for (uint64_t i = 1; i < 256; ++i) { + commit_sequential(tdb, {}, {}, BlockHeader{.number = i}); + } + + auto *executor = create_executor(dbname.string()); + + // Single tx: sender calls forwarder with 10 MON. + Transaction const tx{ + .gas_limit = 200'000'000, + .value = uint256_t{10} * WEI_PER_MON, + .to = forwarder_addr, + }; + + auto const enc_sender = rlp::encode_address(std::make_optional(sender)); + auto const rlp_senders = + to_vec(rlp::encode_list2(rlp::encode_list2(enc_sender))); + + auto const enc_tx = rlp::encode_string2(rlp::encode_transaction(tx)); + auto const rlp_calls = to_vec(rlp::encode_list2(rlp::encode_list2(enc_tx))); + + BlockHeader const header{.number = 1, .gas_limit = 200'000'000}; + auto const rlp_header = to_vec(rlp::encode_block_header(header)); + auto const rlp_block_id = to_vec(rlp_finalized_id); + + auto *const so = monad_state_override_vec_create(1); + auto *const bo = monad_block_override_vec_create(1); + + struct callback_context ctx; + boost::fibers::future f = ctx.promise.get_future(); + + monad_executor_eth_simulate_submit( + executor, + CHAIN_CONFIG_MONAD_DEVNET, + rlp_senders.data(), + rlp_senders.size(), + rlp_calls.data(), + rlp_calls.size(), + 1, + rlp_header.data(), + rlp_header.size(), + rlp_block_id.data(), + rlp_block_id.size(), + rlp_finalized_id.data(), + rlp_finalized_id.size(), + so, + bo, + true, // emit_native_transfer_logs + complete_callback, + (void *)&ctx); + f.get(); + + ASSERT_EQ(ctx.result->status_code, EVMC_SUCCESS); + ASSERT_TRUE(ctx.result->encoded_trace_len > 0); + + nlohmann::json output = nlohmann::json::from_cbor( + ctx.result->encoded_trace, + ctx.result->encoded_trace + ctx.result->encoded_trace_len); + + ASSERT_EQ(output.size(), 1); + ASSERT_EQ(output[0]["calls"].size(), 1); + EXPECT_EQ(output[0]["calls"][0]["status"], "0x1"); + + // Native transfer log constants. + static constexpr Address native_token = + 0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee_address; + static constexpr bytes32_t transfer_sig = + 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef_bytes32; + + auto const format_address_topic = [](Address const &addr) { + bytes32_t padded{}; + std::memcpy(&padded.bytes[12], addr.bytes, 20); + return std::format("0x{}", to_hex(padded)); + }; + + auto const &logs = output[0]["calls"][0]["logs"]; + ASSERT_EQ(logs.size(), 2); + + // Log 0: sender -> forwarder (10 MON) + EXPECT_EQ(logs[0]["logIndex"], "0x0"); + EXPECT_EQ(logs[0]["address"], std::format("0x{}", to_hex(native_token))); + ASSERT_EQ(logs[0]["topics"].size(), 3); + EXPECT_EQ(logs[0]["topics"][0], std::format("0x{}", to_hex(transfer_sig))); + EXPECT_EQ(logs[0]["topics"][1], format_address_topic(sender)); + EXPECT_EQ(logs[0]["topics"][2], format_address_topic(forwarder_addr)); + auto const ten_mon = + intx::be::store(uint256_t{10} * WEI_PER_MON); + EXPECT_EQ(logs[0]["data"], std::format("0x{}", to_hex(ten_mon))); + + // Log 1: forwarder -> sink (5 MON) + EXPECT_EQ(logs[1]["logIndex"], "0x1"); + EXPECT_EQ(logs[1]["address"], std::format("0x{}", to_hex(native_token))); + ASSERT_EQ(logs[1]["topics"].size(), 3); + EXPECT_EQ(logs[1]["topics"][0], std::format("0x{}", to_hex(transfer_sig))); + EXPECT_EQ(logs[1]["topics"][1], format_address_topic(forwarder_addr)); + EXPECT_EQ(logs[1]["topics"][2], format_address_topic(sink)); + auto const five_mon = + intx::be::store(uint256_t{5} * WEI_PER_MON); + EXPECT_EQ(logs[1]["data"], std::format("0x{}", to_hex(five_mon))); + + monad_block_override_vec_destroy(bo); + monad_state_override_vec_destroy(so); + monad_executor_destroy(executor); +} + +// Test time travelling +TEST_F(EthCallFixture, eth_simulate_v1_time_travel) +{ + using namespace monad::vm::utils; + + static constexpr auto WEI_PER_MON = uint256_t{1'000'000'000'000'000'000ULL}; + + static constexpr Address sender = + 0x00000000000000000000000000000000deadbeef_address; + static constexpr Address time_contract = + 0x00000000000000000000000000000000000051c0_address; + + auto const eb = + evm_as::latest().sload(0).timestamp().gt().push0().mstore().return_( + 0, 32); + std::vector bytecode{}; + ASSERT_TRUE(evm_as::validate(eb)); + evm_as::compile(eb, bytecode); + byte_string_view const code{bytecode.data(), bytecode.size()}; + auto const code_hash = to_bytes(keccak256(code)); + auto const icode = vm::make_shared_intercode(code); + + bytes32_t const unlock_time = intx::be::store(uint256_t{512}); + + commit_sequential( + tdb, + StateDeltas{ + {sender, + StateDelta{ + .account = + {std::nullopt, + Account{ + .balance = uint256_t{100} * WEI_PER_MON, + .nonce = 0}}}}, + {time_contract, + StateDelta{ + .account = + {std::nullopt, + Account{ + .balance = 0, .code_hash = code_hash, .nonce = 0}}, + .storage = {{bytes32_t{}, {bytes32_t{}, unlock_time}}}}}}, + Code{ + {code_hash, icode}, + }, + BlockHeader{.number = 0, .timestamp = 0}); + + for (uint64_t i = 1; i < 256; ++i) { + commit_sequential( + tdb, {}, {}, BlockHeader{.number = i, .timestamp = i}); + } + + auto *executor = create_executor(dbname.string()); + + Transaction const tx{ + .gas_limit = 200'000'000, + .to = time_contract, + }; + + auto const enc_sender = rlp::encode_address(std::make_optional(sender)); + auto const rlp_senders = to_vec(rlp::encode_list2( + rlp::encode_list2(enc_sender), rlp::encode_list2(enc_sender))); + + auto const enc_tx = rlp::encode_string2(rlp::encode_transaction(tx)); + auto const rlp_calls = to_vec(rlp::encode_list2( + rlp::encode_list2(enc_tx), rlp::encode_list2(enc_tx))); + + BlockHeader const header{.number = 255, .gas_limit = 200'000'000}; + auto const rlp_header = to_vec(rlp::encode_block_header(header)); + auto const rlp_block_id = to_vec(rlp_finalized_id); + + auto *const so = monad_state_override_vec_create(2); + auto *const bo = monad_block_override_vec_create(2); + set_block_override_time_at(bo, 1, 513); // time travel to after unlock_time + + struct callback_context ctx; + boost::fibers::future f = ctx.promise.get_future(); + + monad_executor_eth_simulate_submit( + executor, + CHAIN_CONFIG_MONAD_DEVNET, + rlp_senders.data(), + rlp_senders.size(), + rlp_calls.data(), + rlp_calls.size(), + 255, + rlp_header.data(), + rlp_header.size(), + rlp_block_id.data(), + rlp_block_id.size(), + rlp_finalized_id.data(), + rlp_finalized_id.size(), + so, + bo, + true, // emit_native_transfer_logs + complete_callback, + (void *)&ctx); + f.get(); + + ASSERT_EQ(ctx.result->status_code, EVMC_SUCCESS); + ASSERT_TRUE(ctx.result->encoded_trace_len > 0); + + nlohmann::json output = nlohmann::json::from_cbor( + ctx.result->encoded_trace, + ctx.result->encoded_trace + ctx.result->encoded_trace_len); + + ASSERT_EQ(output.size(), 2); + ASSERT_EQ(output[0]["calls"].size(), 1); + EXPECT_EQ(output[0]["calls"][0]["status"], "0x1"); + EXPECT_EQ( + output[0]["calls"][0]["returnData"], + "0x0000000000000000000000000000000000000000000000000000000000000000"); + + ASSERT_EQ(output[1]["calls"].size(), 1); + EXPECT_EQ(output[1]["calls"][0]["status"], "0x1"); + EXPECT_EQ( + output[1]["calls"][0]["returnData"], + "0x0000000000000000000000000000000000000000000000000000000000000001"); + + monad_block_override_vec_destroy(bo); + monad_state_override_vec_destroy(so); + monad_executor_destroy(executor); +} + +// This test checks that when we read the blockhash of real and simulated blocks +// during an eth_simulateV1 call. +TEST_F(EthCallFixture, eth_simulate_v1_blockhash_reads) +{ + static constexpr auto WEI_PER_MON = uint256_t{1'000'000'000'000'000'000ULL}; + + static constexpr Address sender = + 0x00000000000000000000000000000000deadbeef_address; + static constexpr Address blockhash_contract = + 0x000000000000000000000000000000000000b10c_address; + + using namespace monad::vm::utils; + auto const eb = evm_as::latest() + .push(1) + .number() + .sub() + .blockhash() + .push(0) + .mstore() + .push(32) + .push(0) + .return_(); + + ASSERT_TRUE(evm_as::validate(eb)); + + std::vector code{}; + evm_as::compile(eb, code); + byte_string_view const code_view{code.data(), code.size()}; + auto const code_hash = to_bytes(keccak256(code_view)); + auto const icode = vm::make_shared_intercode(code_view); + + commit_sequential( + tdb, + StateDeltas{ + {sender, + StateDelta{ + .account = + {std::nullopt, + Account{ + .balance = uint256_t{100} * WEI_PER_MON, + .nonce = 0}}}}, + {blockhash_contract, + StateDelta{ + .account = + {std::nullopt, + Account{ + .balance = 0, .code_hash = code_hash, .nonce = 0}}}}}, + Code{{code_hash, icode}}, + BlockHeader{.number = 0}); + + for (uint64_t i = 1; i < 256; ++i) { + commit_sequential(tdb, {}, {}, BlockHeader{.number = i}); + } + + auto *executor = create_executor(dbname.string()); + + Transaction const tx{ + .gas_limit = 200'000'000, + .to = blockhash_contract, + }; + + auto const enc_sender = rlp::encode_address(std::make_optional(sender)); + auto const rlp_senders = to_vec(rlp::encode_list2( + rlp::encode_list2(enc_sender), + rlp::encode_list2(enc_sender), + rlp::encode_list2(enc_sender))); + + auto const enc_tx = rlp::encode_string2(rlp::encode_transaction(tx)); + auto const rlp_calls = to_vec(rlp::encode_list2( + rlp::encode_list2(enc_tx), + rlp::encode_list2(enc_tx), + rlp::encode_list2(enc_tx))); + + BlockHeader const header{.number = 255, .gas_limit = 200'000'000}; + auto const rlp_header = to_vec(rlp::encode_block_header(header)); + auto const rlp_block_id = to_vec(rlp_finalized_id); + + auto *const so = monad_state_override_vec_create(3); + auto *const bo = monad_block_override_vec_create(3); + set_block_override_number_at(bo, 2, 511); // forces synthetic blocks + + struct callback_context ctx; + boost::fibers::future f = ctx.promise.get_future(); + + monad_executor_eth_simulate_submit( + executor, + CHAIN_CONFIG_MONAD_DEVNET, + rlp_senders.data(), + rlp_senders.size(), + rlp_calls.data(), + rlp_calls.size(), + 255, + rlp_header.data(), + rlp_header.size(), + rlp_block_id.data(), + rlp_block_id.size(), + rlp_finalized_id.data(), + rlp_finalized_id.size(), + so, + bo, + false, + complete_callback, + (void *)&ctx); + f.get(); + + ASSERT_EQ(ctx.result->status_code, EVMC_SUCCESS); + ASSERT_TRUE(ctx.result->encoded_trace_len > 0); + + nlohmann::json output = nlohmann::json::from_cbor( + ctx.result->encoded_trace, + ctx.result->encoded_trace + ctx.result->encoded_trace_len); + + // Check the first two user-defined blocks. + ASSERT_EQ(output.size(), 256); + ASSERT_EQ(output[0]["calls"].size(), 1); + EXPECT_EQ(output[0]["calls"][0]["status"], "0x1"); + + BlockHeader const finalized_header = tdb.read_eth_header(); + ASSERT_EQ(finalized_header.number, 255); + auto const expected_base_hash = std::format( + "0x{}", + to_hex( + to_bytes(keccak256(rlp::encode_block_header(finalized_header))))); + EXPECT_EQ(output[0]["parentHash"], expected_base_hash); + EXPECT_EQ(output[0]["calls"][0]["returnData"], expected_base_hash); + + ASSERT_EQ(output[1]["calls"].size(), 1); + EXPECT_EQ(output[1]["calls"][0]["status"], "0x1"); + EXPECT_EQ(output[1]["parentHash"], output[0]["hash"]); + EXPECT_EQ(output[1]["calls"][0]["returnData"], output[0]["hash"]); + EXPECT_NE(output[1]["hash"], output[0]["hash"]); + + // Check the synthetic blocks + for (size_t i = 2; i < output.size() - 1; ++i) { + ASSERT_EQ(output[i]["calls"].size(), 0); + EXPECT_EQ(output[i]["parentHash"], output[i - 1]["hash"]); + EXPECT_NE(output[i]["hash"], output[i - 1]["hash"]); + } + + // Check the last user-defined block + ASSERT_EQ(output[output.size() - 1]["calls"].size(), 1); + EXPECT_EQ(output[output.size() - 1]["calls"][0]["status"], "0x1"); + EXPECT_EQ( + output[output.size() - 1]["parentHash"], + output[output.size() - 2]["hash"]); + EXPECT_EQ( + output[output.size() - 1]["calls"][0]["returnData"], + output[output.size() - 2]["hash"]); + EXPECT_NE( + output[output.size() - 1]["hash"], output[output.size() - 2]["hash"]); + + monad_block_override_vec_destroy(bo); + monad_state_override_vec_destroy(so); + monad_executor_destroy(executor); +} + +// Submits legacy transactions in an eth_simulateV1 call and checks that they +// execute successfully. +TEST_F(EthCallFixture, eth_simulate_v1_legacy_transactions) +{ + static constexpr auto WEI_PER_MON = uint256_t{1'000'000'000'000'000'000ULL}; + + static constexpr Address sender = + 0x00000000000000000000000000000000deadbeef_address; + static constexpr Address recipient = + 0x00000000000000000000000000000000feedface_address; + + commit_sequential( + tdb, + StateDeltas{ + {sender, + StateDelta{ + .account = + {std::nullopt, + Account{ + .balance = uint256_t{100} * WEI_PER_MON, + .nonce = 0}}}}, + {recipient, + StateDelta{ + .account = + {std::nullopt, Account{.balance = 0, .nonce = 0}}}}}, + {}, + BlockHeader{.number = 0}); + + for (uint64_t i = 1; i < 256; ++i) { + commit_sequential(tdb, {}, {}, BlockHeader{.number = i}); + } + + auto *executor = create_executor(dbname.string()); + + auto *const state_overrides = monad_state_override_vec_create(1); + auto *const block_overrides = monad_block_override_vec_create(1); + + auto const encoded_sender = rlp::encode_address(std::make_optional(sender)); + auto const rlp_senders = to_vec( + rlp::encode_list2(rlp::encode_list2(encoded_sender, encoded_sender))); + + Transaction const tx0{ + .max_fee_per_gas = 1, + .gas_limit = 200'000'000, + .value = uint256_t{1'000}, + .to = recipient, + .type = TransactionType::legacy, + .max_priority_fee_per_gas = 0, + }; + Transaction const tx1{ + .max_fee_per_gas = 1, + .gas_limit = 200'000'000, + .value = uint256_t{2'000}, + .to = recipient, + .type = TransactionType::legacy, + .max_priority_fee_per_gas = 0, + }; + + auto const encoded_tx0 = rlp::encode_string2(rlp::encode_transaction(tx0)); + auto const encoded_tx1 = rlp::encode_string2(rlp::encode_transaction(tx1)); + auto const rlp_calls = + to_vec(rlp::encode_list2(rlp::encode_list2(encoded_tx0, encoded_tx1))); + + BlockHeader const header{ + .number = 1, + .gas_limit = 400'000'000, + }; + auto const rlp_header = to_vec(rlp::encode_block_header(header)); + auto const rlp_block_id = to_vec(rlp_finalized_id); + + struct callback_context ctx; + boost::fibers::future f = ctx.promise.get_future(); + + monad_executor_eth_simulate_submit( + executor, + CHAIN_CONFIG_MONAD_DEVNET, + rlp_senders.data(), + rlp_senders.size(), + rlp_calls.data(), + rlp_calls.size(), + 1, + rlp_header.data(), + rlp_header.size(), + rlp_block_id.data(), + rlp_block_id.size(), + rlp_finalized_id.data(), + rlp_finalized_id.size(), + state_overrides, + block_overrides, + false, + complete_callback, + (void *)&ctx); + f.get(); + + ASSERT_EQ(ctx.result->status_code, EVMC_SUCCESS); + ASSERT_TRUE(ctx.result->encoded_trace_len > 0); + + nlohmann::json output = nlohmann::json::from_cbor( + ctx.result->encoded_trace, + ctx.result->encoded_trace + ctx.result->encoded_trace_len); + + ASSERT_EQ(output.size(), 1); + ASSERT_EQ(output[0]["calls"].size(), 2); + EXPECT_EQ(output[0]["calls"][0]["status"], "0x1"); + EXPECT_EQ(output[0]["calls"][1]["status"], "0x1"); + + auto sender_account = tdb.read_account(sender); + ASSERT_TRUE(sender_account.has_value()); + EXPECT_EQ( + sender_account->balance, + uint256_t{100} * uint256_t{1'000'000'000'000'000'000ULL}); + + monad_block_override_vec_destroy(block_overrides); + monad_state_override_vec_destroy(state_overrides); + monad_executor_destroy(executor); +} diff --git a/category/rpc/overrides.cpp b/category/rpc/overrides.cpp index f5be69fb9..3272e067e 100644 --- a/category/rpc/overrides.cpp +++ b/category/rpc/overrides.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -24,6 +25,7 @@ #include #include +#include using namespace monad; @@ -162,6 +164,78 @@ void set_override_state( state_object.emplace(k, v); } +struct monad_state_override_vec *monad_state_override_vec_create(size_t size) +{ + auto *const vec = new monad_state_override_vec(size); + return vec; +} + +void monad_state_override_vec_destroy(struct monad_state_override_vec *v) +{ + MONAD_ASSERT(v); + delete[] v->overrides; + delete v; +} + +void add_override_address_at( + struct monad_state_override_vec *v, size_t index, uint8_t const *addr, + size_t addr_len) +{ + MONAD_ASSERT(v); + MONAD_ASSERT(index < v->size); + add_override_address(&v->overrides[index], addr, addr_len); +} + +void set_override_balance_at( + struct monad_state_override_vec *v, size_t index, uint8_t const *addr, + size_t addr_len, uint8_t const *balance, size_t balance_len) +{ + MONAD_ASSERT(v); + MONAD_ASSERT(index < v->size); + set_override_balance( + &v->overrides[index], addr, addr_len, balance, balance_len); +} + +void set_override_nonce_at( + struct monad_state_override_vec *v, size_t index, uint8_t const *addr, + size_t addr_len, uint64_t nonce) +{ + MONAD_ASSERT(v); + MONAD_ASSERT(index < v->size); + set_override_nonce(&v->overrides[index], addr, addr_len, nonce); +} + +void set_override_code_at( + struct monad_state_override_vec *v, size_t index, uint8_t const *addr, + size_t addr_len, uint8_t const *code, size_t code_len) +{ + MONAD_ASSERT(v); + MONAD_ASSERT(index < v->size); + set_override_code(&v->overrides[index], addr, addr_len, code, code_len); +} + +void set_override_state_diff_at( + struct monad_state_override_vec *v, size_t index, uint8_t const *addr, + size_t addr_len, uint8_t const *key, size_t key_len, uint8_t const *value, + size_t value_len) +{ + MONAD_ASSERT(v); + MONAD_ASSERT(index < v->size); + set_override_state_diff( + &v->overrides[index], addr, addr_len, key, key_len, value, value_len); +} + +void set_override_state_at( + struct monad_state_override_vec *v, size_t index, uint8_t const *addr, + size_t addr_len, uint8_t const *key, size_t key_len, uint8_t const *value, + size_t value_len) +{ + MONAD_ASSERT(v); + MONAD_ASSERT(index < v->size); + set_override_state( + &v->overrides[index], addr, addr_len, key, key_len, value, value_len); +} + monad_block_override *monad_block_override_create() { return new monad_block_override(); @@ -233,13 +307,105 @@ void set_block_override_base_fee_per_gas( m->base_fee_per_gas = intx::be::unsafe::load(fee); } -void set_block_override_blob_base_fee( - monad_block_override *const m, uint8_t const *const fee, - size_t const fee_len) +void add_block_override_withdrawal( + struct monad_block_override *const m, uint64_t index, + uint64_t validator_index, uint64_t amount, uint8_t const *recipient_addr, + size_t recipient_addr_len) { MONAD_ASSERT(m); - MONAD_ASSERT(fee); - MONAD_ASSERT(fee_len == sizeof(uint256_t)); - MONAD_ASSERT(!m->blob_base_fee.has_value()); - m->blob_base_fee = intx::be::unsafe::load(fee); + MONAD_ASSERT(recipient_addr); + MONAD_ASSERT(recipient_addr_len == sizeof(Address)); + Address recipient; + std::memcpy(recipient.bytes, recipient_addr, sizeof(Address)); + + if (!m->withdrawals.has_value()) { + m->withdrawals = std::vector{}; + } + + m->withdrawals->emplace_back(Withdrawal{ + .index = index, + .validator_index = validator_index, + .amount = amount, + .recipient = recipient, + }); +} + +struct monad_block_override_vec *monad_block_override_vec_create(size_t size) +{ + auto *const vec = new monad_block_override_vec(size); + return vec; +} + +void monad_block_override_vec_destroy(struct monad_block_override_vec *v) +{ + MONAD_ASSERT(v); + delete[] v->overrides; + delete v; +} + +void set_block_override_number_at( + struct monad_block_override_vec *v, size_t index, uint64_t number) +{ + MONAD_ASSERT(v); + MONAD_ASSERT(index < v->size); + set_block_override_number(&v->overrides[index], number); +} + +void set_block_override_time_at( + struct monad_block_override_vec *v, size_t index, uint64_t time) +{ + MONAD_ASSERT(v); + MONAD_ASSERT(index < v->size); + set_block_override_time(&v->overrides[index], time); +} + +void set_block_override_gas_limit_at( + struct monad_block_override_vec *v, size_t index, uint64_t gas_limit) +{ + MONAD_ASSERT(v); + MONAD_ASSERT(index < v->size); + set_block_override_gas_limit(&v->overrides[index], gas_limit); +} + +void set_block_override_fee_recipient_at( + struct monad_block_override_vec *v, size_t index, uint8_t const *addr, + size_t addr_len) +{ + MONAD_ASSERT(v); + MONAD_ASSERT(index < v->size); + set_block_override_fee_recipient(&v->overrides[index], addr, addr_len); +} + +void set_block_override_prev_randao_at( + struct monad_block_override_vec *v, size_t index, uint8_t const *randao, + size_t randao_len) +{ + MONAD_ASSERT(v); + MONAD_ASSERT(index < v->size); + set_block_override_prev_randao(&v->overrides[index], randao, randao_len); +} + +void set_block_override_base_fee_per_gas_at( + struct monad_block_override_vec *v, size_t index, uint8_t const *fee, + size_t fee_len) +{ + MONAD_ASSERT(v); + MONAD_ASSERT(index < v->size); + set_block_override_base_fee_per_gas(&v->overrides[index], fee, fee_len); +} + +void add_block_override_withdrawal_at( + struct monad_block_override_vec *v, size_t index, uint64_t withdrawal_index, + uint64_t validator_index, uint64_t amount, uint8_t const *recipient_addr, + size_t recipient_addr_len) +{ + MONAD_ASSERT(v); + MONAD_ASSERT(index < v->size); + add_block_override_withdrawal( + &v->overrides[index], + withdrawal_index, + validator_index, + amount, + recipient_addr, + recipient_addr_len); } diff --git a/category/rpc/overrides.h b/category/rpc/overrides.h index bac105b41..5031cbe6b 100644 --- a/category/rpc/overrides.h +++ b/category/rpc/overrides.h @@ -52,6 +52,38 @@ void set_override_state( struct monad_state_override *, uint8_t const *addr, size_t addr_len, uint8_t const *key, size_t key_len, uint8_t const *value, size_t value_len); +struct monad_state_override_vec; + +struct monad_state_override_vec *monad_state_override_vec_create(size_t size); + +void monad_state_override_vec_destroy(struct monad_state_override_vec *); + +void add_override_address_at( + struct monad_state_override_vec *, size_t index, uint8_t const *addr, + size_t addr_len); + +void set_override_balance_at( + struct monad_state_override_vec *, size_t index, uint8_t const *addr, + size_t addr_len, uint8_t const *balance, size_t balance_len); + +void set_override_nonce_at( + struct monad_state_override_vec *, size_t index, uint8_t const *addr, + size_t addr_len, uint64_t nonce); + +void set_override_code_at( + struct monad_state_override_vec *, size_t index, uint8_t const *addr, + size_t addr_len, uint8_t const *code, size_t code_len); + +void set_override_state_diff_at( + struct monad_state_override_vec *, size_t index, uint8_t const *addr, + size_t addr_len, uint8_t const *key, size_t key_len, uint8_t const *value, + size_t value_len); + +void set_override_state_at( + struct monad_state_override_vec *, size_t index, uint8_t const *addr, + size_t addr_len, uint8_t const *key, size_t key_len, uint8_t const *value, + size_t value_len); + struct monad_block_override; struct monad_block_override *monad_block_override_create(); @@ -74,8 +106,41 @@ void set_block_override_prev_randao( void set_block_override_base_fee_per_gas( struct monad_block_override *, uint8_t const *fee, size_t fee_len); -void set_block_override_blob_base_fee( - struct monad_block_override *, uint8_t const *fee, size_t fee_len); +void add_block_override_withdrawal( + struct monad_block_override *, uint64_t index, uint64_t validator_index, + uint64_t amount, uint8_t const *recipient_addr, size_t recipient_addr_len); + +struct monad_block_override_vec; + +struct monad_block_override_vec *monad_block_override_vec_create(size_t size); + +void monad_block_override_vec_destroy(struct monad_block_override_vec *); + +void set_block_override_number_at( + struct monad_block_override_vec *, size_t index, uint64_t number); + +void set_block_override_time_at( + struct monad_block_override_vec *, size_t index, uint64_t time); + +void set_block_override_gas_limit_at( + struct monad_block_override_vec *, size_t index, uint64_t gas_limit); + +void set_block_override_fee_recipient_at( + struct monad_block_override_vec *, size_t index, uint8_t const *addr, + size_t addr_len); + +void set_block_override_prev_randao_at( + struct monad_block_override_vec *, size_t index, uint8_t const *randao, + size_t randao_len); + +void set_block_override_base_fee_per_gas_at( + struct monad_block_override_vec *, size_t index, uint8_t const *fee, + size_t fee_len); + +void add_block_override_withdrawal_at( + struct monad_block_override_vec *, size_t index, uint64_t withdrawal_index, + uint64_t validator_index, uint64_t amount, uint8_t const *recipient_addr, + size_t recipient_addr_len); #ifdef __cplusplus } diff --git a/category/rpc/overrides.hpp b/category/rpc/overrides.hpp index d8129c68a..47b3c22bb 100644 --- a/category/rpc/overrides.hpp +++ b/category/rpc/overrides.hpp @@ -17,11 +17,13 @@ #include #include +#include #include #include #include +#include struct monad_state_override { @@ -43,6 +45,18 @@ struct monad_state_override override_sets; }; +struct monad_state_override_vec +{ + size_t const size; + monad_state_override *overrides; + + explicit monad_state_override_vec(size_t size) + : size(size) + , overrides(new monad_state_override[size]()) + { + } +}; + struct monad_block_override { std::optional number{std::nullopt}; @@ -51,5 +65,17 @@ struct monad_block_override std::optional fee_recipient{std::nullopt}; std::optional prev_randao{std::nullopt}; std::optional base_fee_per_gas{std::nullopt}; - std::optional blob_base_fee{std::nullopt}; + std::optional> withdrawals{std::nullopt}; +}; + +struct monad_block_override_vec +{ + size_t const size; + monad_block_override *overrides; + + explicit monad_block_override_vec(size_t size) + : size(size) + , overrides(new monad_block_override[size]()) + { + } }; diff --git a/rust/crates/monad-ethcall/Cargo.toml b/rust/crates/monad-ethcall/Cargo.toml index d7a4a5408..e5d274eb8 100644 --- a/rust/crates/monad-ethcall/Cargo.toml +++ b/rust/crates/monad-ethcall/Cargo.toml @@ -12,7 +12,7 @@ bench = false monad-cxx = { workspace = true } alloy-consensus = { workspace = true } -alloy-eips = { workspace = true } +alloy-eips = { workspace = true, features = ["serde"] } alloy-primitives = { workspace = true, features = ["serde"] } alloy-rlp = { workspace = true } alloy-sol-types = { workspace = true } diff --git a/rust/crates/monad-ethcall/src/ffi.rs b/rust/crates/monad-ethcall/src/ffi.rs index 8e34959de..41d0ce054 100644 --- a/rust/crates/monad-ethcall/src/ffi.rs +++ b/rust/crates/monad-ethcall/src/ffi.rs @@ -15,14 +15,21 @@ pub use self::bindings::monad_executor_pool_config as PoolConfig; pub(crate) use self::bindings::{ - add_override_address, monad_chain_config, monad_chain_config_CHAIN_CONFIG_ETHEREUM_MAINNET, + add_block_override_withdrawal_at, add_override_address, add_override_address_at, + monad_block_override_vec, monad_block_override_vec_create, monad_block_override_vec_destroy, + monad_chain_config, monad_chain_config_CHAIN_CONFIG_ETHEREUM_MAINNET, monad_chain_config_CHAIN_CONFIG_HIVE_NET, monad_chain_config_CHAIN_CONFIG_MONAD_DEVNET, monad_chain_config_CHAIN_CONFIG_MONAD_MAINNET, monad_chain_config_CHAIN_CONFIG_MONAD_TESTNET, monad_executor, monad_executor_create, monad_executor_destroy, monad_executor_eth_call_submit, - monad_executor_result, monad_executor_result_release, monad_executor_run_transactions, - monad_state_override_create, monad_state_override_destroy, monad_tracer_config_NOOP_TRACER, - set_override_balance, set_override_code, set_override_nonce, set_override_state, - set_override_state_diff, + monad_executor_eth_simulate_submit, monad_executor_result, monad_executor_result_release, + monad_executor_run_transactions, monad_state_override_create, monad_state_override_destroy, + monad_state_override_vec, monad_state_override_vec_create, monad_state_override_vec_destroy, + monad_tracer_config_NOOP_TRACER, set_block_override_base_fee_per_gas_at, + set_block_override_fee_recipient_at, set_block_override_gas_limit_at, + set_block_override_number_at, set_block_override_prev_randao_at, set_block_override_time_at, + set_override_balance, set_override_balance_at, set_override_code, set_override_code_at, + set_override_nonce, set_override_nonce_at, set_override_state, set_override_state_at, + set_override_state_diff, set_override_state_diff_at, }; #[allow(dead_code, non_camel_case_types, non_upper_case_globals)] diff --git a/rust/crates/monad-ethcall/src/lib.rs b/rust/crates/monad-ethcall/src/lib.rs index cc542e125..9ae9a3897 100644 --- a/rust/crates/monad-ethcall/src/lib.rs +++ b/rust/crates/monad-ethcall/src/lib.rs @@ -20,7 +20,7 @@ use std::{ }; use alloy_consensus::{Header, Transaction as _, TxEnvelope}; -use alloy_eips::eip2718::Encodable2718; +use alloy_eips::{eip2718::Encodable2718, eip4895::Withdrawal}; use alloy_primitives::{Address, Bytes, B256, U256, U64}; use alloy_rlp::Encodable; use alloy_sol_types::decode_revert_reason; @@ -100,6 +100,55 @@ impl Drop for EthCallExecutor { } } +struct MonadExecutorResult { + c_handle: *mut ffi::monad_executor_result, +} + +impl MonadExecutorResult { + fn from_c_handle(c_handle: *mut ffi::monad_executor_result) -> Self { + assert!(!c_handle.is_null(), "c_handle is null"); + Self { c_handle } + } + + fn status_code(&self) -> i32 { + unsafe { (*self.c_handle).status_code } + } + + fn encoded_trace(&self) -> Vec { + let output_data_len = unsafe { (*self.c_handle).encoded_trace_len }; + let output_data = if output_data_len != 0 { + unsafe { std::slice::from_raw_parts((*self.c_handle).encoded_trace, output_data_len) } + .to_vec() + } else { + vec![] + }; + output_data + } + + fn message(&self) -> String { + let cstr_msg = unsafe { CStr::from_ptr((*self.c_handle).message.cast()) }; + let message = match cstr_msg.to_str() { + Ok(str) => String::from(str), + Err(_) => String::from("execution error: message invalid utf-8"), + }; + message + } +} + +impl From<*mut ffi::monad_executor_result> for MonadExecutorResult { + fn from(c_handle: *mut ffi::monad_executor_result) -> Self { + MonadExecutorResult::from_c_handle(c_handle) + } +} + +impl Drop for MonadExecutorResult { + fn drop(&mut self) { + unsafe { + ffi::monad_executor_result_release(self.c_handle); + } + } +} + // ensure that only one of {State, StateDiff} can be set #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] @@ -190,6 +239,24 @@ pub struct SenderContext { sender: Sender<*mut monad_executor_result>, } +#[derive(Clone, Debug)] +pub enum SimulateResult { + Success(SuccessSimulateResult), + Failure(FailureSimulateResult), +} + +#[derive(Clone, Debug)] +pub struct SuccessSimulateResult { + pub output_data: Vec, +} + +#[derive(Clone, Debug)] +pub struct FailureSimulateResult { + pub error_code: EthCallResult, + pub message: String, + pub data: Option, +} + /// # Safety /// This should be used only as a callback for monad_eth_call_executor_submit /// @@ -624,6 +691,328 @@ pub async fn eth_trace_block_or_transaction( } } +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BlockOverride { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub number: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub time: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub gas_limit: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub fee_recipient: Option
, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub prev_randao: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub base_fee_per_gas: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub withdrawals: Vec, +} + +struct CStateOverrideVec { + c_handle: *mut ffi::monad_state_override_vec, +} + +impl CStateOverrideVec { + fn new(size: usize) -> Self { + let c_handle = unsafe { ffi::monad_state_override_vec_create(size) }; + Self { c_handle } + } + + fn as_mut_ptr(&self) -> *mut ffi::monad_state_override_vec { + self.c_handle + } +} + +impl Drop for CStateOverrideVec { + fn drop(&mut self) { + unsafe { + ffi::monad_state_override_vec_destroy(self.c_handle); + } + } +} +struct CBlockOverrideVec { + c_handle: *mut ffi::monad_block_override_vec, +} + +impl CBlockOverrideVec { + fn new(size: usize) -> Self { + let c_handle = unsafe { ffi::monad_block_override_vec_create(size) }; + Self { c_handle } + } + + fn as_mut_ptr(&self) -> *mut ffi::monad_block_override_vec { + self.c_handle + } +} + +impl Drop for CBlockOverrideVec { + fn drop(&mut self) { + unsafe { + ffi::monad_block_override_vec_destroy(self.c_handle); + } + } +} + +pub async fn eth_simulate_v1( + chain_id: ChainId, + senders: &Vec>, + calls: &Vec>, + block_header: Header, + block_number: u64, + block_id: Option<[u8; 32]>, + grandparent_id: Option<[u8; 32]>, + emit_native_transfer_logs: bool, + eth_call_executor: &EthCallExecutor, + overrides: &[(&BlockOverride, &StateOverrideSet)], +) -> SimulateResult { + assert_eq!(calls.len(), overrides.len()); + assert_eq!(calls.len(), senders.len()); + for (txs, senders) in calls.iter().zip(senders.iter()) { + assert_eq!(txs.len(), senders.len()); + } + + let mut rlp_encoded_senders = vec![]; + senders.encode(&mut rlp_encoded_senders); + + let mut rlp_encoded_txns = vec![]; + calls.encode(&mut rlp_encoded_txns); + + let mut rlp_encoded_block_header = vec![]; + block_header.encode(&mut rlp_encoded_block_header); + + let rlp_encoded_block_id = alloy_rlp::encode(block_id.unwrap_or([0_u8; 32])); + let rlp_encoded_grandparent_id = alloy_rlp::encode(grandparent_id.unwrap_or([0_u8; 32])); + + let chain_config = chain_id.to_ffi_chain_config(); + + let state_overrides = CStateOverrideVec::new(calls.len()); + let block_overrides = CBlockOverrideVec::new(calls.len()); + for (i, (block_override, state_override)) in overrides.iter().enumerate() { + for (addr, obj) in state_override.iter() { + let addr: &[u8] = addr.as_slice(); + + unsafe { + ffi::add_override_address_at( + state_overrides.as_mut_ptr(), + i, + addr.as_ptr(), + addr.len(), + ); + + if let Some(balance) = obj.balance { + // Big Endianess is to match with decode in eth_call.cpp (intx::be::load) + let balance_vec = balance.to_be_bytes_vec(); + + ffi::set_override_balance_at( + state_overrides.as_mut_ptr(), + i, + addr.as_ptr(), + addr.len(), + balance_vec.as_ptr(), + balance_vec.len(), + ); + } + + if let Some(nonce) = obj.nonce { + ffi::set_override_nonce_at( + state_overrides.as_mut_ptr(), + i, + addr.as_ptr(), + addr.len(), + nonce.as_limbs()[0], + ) + } + + if let Some(code) = &obj.code { + ffi::set_override_code_at( + state_overrides.as_mut_ptr(), + i, + addr.as_ptr(), + addr.len(), + code.as_ptr(), + code.len(), + ) + } + + match &obj.storage_override { + Some(StorageOverride::State(storage_override)) => { + for (k, v) in storage_override { + ffi::set_override_state_at( + state_overrides.as_mut_ptr(), + i, + addr.as_ptr(), + addr.len(), + k.as_ptr(), + k.len(), + v.as_ptr(), + v.len(), + ) + } + } + Some(StorageOverride::StateDiff(override_state_diff)) => { + for (k, v) in override_state_diff { + ffi::set_override_state_diff_at( + state_overrides.as_mut_ptr(), + i, + addr.as_ptr(), + addr.len(), + k.as_ptr(), + k.len(), + v.as_ptr(), + v.len(), + ) + } + } + None => {} + } + } + } + + if let Some(number) = block_override.number { + unsafe { + ffi::set_block_override_number_at( + block_overrides.as_mut_ptr(), + i, + number.as_limbs()[0], + ); + } + } + + if let Some(time) = block_override.time { + unsafe { + ffi::set_block_override_time_at( + block_overrides.as_mut_ptr(), + i, + time.as_limbs()[0], + ); + } + } + + if let Some(gas_limit) = block_override.gas_limit { + unsafe { + ffi::set_block_override_gas_limit_at( + block_overrides.as_mut_ptr(), + i, + gas_limit.as_limbs()[0], + ); + } + } + + if let Some(fee_recipient) = block_override.fee_recipient { + let fee_recipient_bytes: &[u8] = fee_recipient.as_slice(); + unsafe { + ffi::set_block_override_fee_recipient_at( + block_overrides.as_mut_ptr(), + i, + fee_recipient_bytes.as_ptr(), + fee_recipient_bytes.len(), + ); + } + } + + if let Some(prev_randao) = block_override.prev_randao { + let prev_randao_vec = prev_randao.as_slice(); + unsafe { + ffi::set_block_override_prev_randao_at( + block_overrides.as_mut_ptr(), + i, + prev_randao_vec.as_ptr(), + prev_randao_vec.len(), + ); + } + } + + if let Some(base_fee_per_gas) = block_override.base_fee_per_gas { + let base_fee_per_gas_vec = base_fee_per_gas.to_be_bytes_vec(); + unsafe { + ffi::set_block_override_base_fee_per_gas_at( + block_overrides.as_mut_ptr(), + i, + base_fee_per_gas_vec.as_ptr(), + base_fee_per_gas_vec.len(), + ); + } + } + + for withdrawal in &block_override.withdrawals { + let address_bytes: &[u8] = withdrawal.address.as_slice(); + unsafe { + ffi::add_block_override_withdrawal_at( + block_overrides.as_mut_ptr(), + i, + withdrawal.index, + withdrawal.validator_index, + withdrawal.amount, + address_bytes.as_ptr(), + address_bytes.len(), + ); + } + } + } + + let (send, recv) = channel(); + let sender_ctx = Box::new(SenderContext { sender: send }); + + unsafe { + let sender_ctx_ptr = Box::into_raw(sender_ctx); + + ffi::monad_executor_eth_simulate_submit( + eth_call_executor.eth_call_executor, + chain_config, + rlp_encoded_senders.as_ptr(), + rlp_encoded_senders.len(), + rlp_encoded_txns.as_ptr(), + rlp_encoded_txns.len(), + block_number, + rlp_encoded_block_header.as_ptr(), + rlp_encoded_block_header.len(), + rlp_encoded_block_id.as_ptr(), + rlp_encoded_block_id.len(), + rlp_encoded_grandparent_id.as_ptr(), + rlp_encoded_grandparent_id.len(), + state_overrides.as_mut_ptr(), + block_overrides.as_mut_ptr(), + emit_native_transfer_logs, + Some(eth_call_submit_callback), + sender_ctx_ptr as *mut std::ffi::c_void, + ); + } + + let result: MonadExecutorResult = match recv.await { + Ok(r) => r, + Err(e) => { + warn!("callback from eth_simulate_v1 failed: {:?}", e); + + return SimulateResult::Failure(FailureSimulateResult { + error_code: EthCallResult::OtherError, + message: "internal eth_simulate_v1 error".to_string(), + data: None, + }); + } + } + .into(); + + let status_code = result.status_code(); + + match status_code { + ETH_CALL_SUCCESS => { + let output_data = result.encoded_trace(); + + SimulateResult::Success(SuccessSimulateResult { output_data }) + } + _ => { + let message = result.message(); + SimulateResult::Failure(FailureSimulateResult { + error_code: EthCallResult::OtherError, + message, + data: None, + }) + } + } +} + #[cfg(test)] mod tests { use alloy_primitives::hex;