diff --git a/src/compiler/evm_frontend/evm_mir_compiler.cpp b/src/compiler/evm_frontend/evm_mir_compiler.cpp index 831de019a..dd8783031 100644 --- a/src/compiler/evm_frontend/evm_mir_compiler.cpp +++ b/src/compiler/evm_frontend/evm_mir_compiler.cpp @@ -4772,9 +4772,11 @@ void EVMMirBuilder::handleCodeCopy(Operand DestOffsetComponents, reloadGasFromMemory(); } #endif - if (!UsePreparedMemory) { - reloadMemorySizeFromInstance(); - } + // No-expand helpers run on a proven memory range, but the instance memory + // backing buffer may still be materialized or rebased on first use. Keep + // the cached base pointer and size in sync so the next direct memory + // operation never reads a stale base. + reloadMemorySizeFromInstance(); } typename EVMMirBuilder::Operand @@ -6010,9 +6012,11 @@ EVMMirBuilder::handleKeccak256(Operand OffsetComponents, #ifdef ZEN_ENABLE_EVM_GAS_REGISTER reloadGasFromMemory(); #endif - if (!UsePreparedMemory) { - reloadMemorySizeFromInstance(); - } + // No-expand helpers run on a proven memory range, but the instance memory + // backing buffer may still be materialized or rebased on first use. Keep + // the cached base pointer and size in sync so the next direct memory + // operation never reads a stale base. + reloadMemorySizeFromInstance(); return Result; } @@ -6042,9 +6046,11 @@ EVMMirBuilder::handleKeccak256TwoWord(Operand OffsetComponents, Operand Word0, #ifdef ZEN_ENABLE_EVM_GAS_REGISTER reloadGasFromMemory(); #endif - if (!UsePreparedMemory) { - reloadMemorySizeFromInstance(); - } + // No-expand helpers run on a proven memory range, but the instance memory + // backing buffer may still be materialized or rebased on first use. Keep + // the cached base pointer and size in sync so the next direct memory + // operation never reads a stale base. + reloadMemorySizeFromInstance(); return Result; } @@ -6075,9 +6081,11 @@ typename EVMMirBuilder::Operand EVMMirBuilder::handleKeccak256CallDataConstSlot( #ifdef ZEN_ENABLE_EVM_GAS_REGISTER reloadGasFromMemory(); #endif - if (!UsePreparedMemory) { - reloadMemorySizeFromInstance(); - } + // No-expand helpers run on a proven memory range, but the instance memory + // backing buffer may still be materialized or rebased on first use. Keep + // the cached base pointer and size in sync so the next direct memory + // operation never reads a stale base. + reloadMemorySizeFromInstance(); return Result; } @@ -6107,9 +6115,11 @@ EVMMirBuilder::handleKeccak256CallerConstSlot(Operand OffsetComponents, #ifdef ZEN_ENABLE_EVM_GAS_REGISTER reloadGasFromMemory(); #endif - if (!UsePreparedMemory) { - reloadMemorySizeFromInstance(); - } + // No-expand helpers run on a proven memory range, but the instance memory + // backing buffer may still be materialized or rebased on first use. Keep + // the cached base pointer and size in sync so the next direct memory + // operation never reads a stale base. + reloadMemorySizeFromInstance(); return Result; } @@ -7409,9 +7419,11 @@ void EVMMirBuilder::handleCallDataCopy(Operand DestOffsetComponents, reloadGasFromMemory(); } #endif - if (!UsePreparedMemory) { - reloadMemorySizeFromInstance(); - } + // No-expand helpers run on a proven memory range, but the instance memory + // backing buffer may still be materialized or rebased on first use. Keep + // the cached base pointer and size in sync so the next direct memory + // operation never reads a stale base. + reloadMemorySizeFromInstance(); } void EVMMirBuilder::handleExtCodeCopy(Operand AddressComponents, diff --git a/src/tests/evm_interp_tests.cpp b/src/tests/evm_interp_tests.cpp index fe79da23c..04e9ec342 100644 --- a/src/tests/evm_interp_tests.cpp +++ b/src/tests/evm_interp_tests.cpp @@ -1209,6 +1209,195 @@ TEST(EVMStateSaveLoad, MissingChainIdAndBlobBaseFee) { std::filesystem::remove(StateFilePath); } +// --------------------------------------------------------------------------- +// Regression tests: loadState() must preserve the caller's existing +// tx_context when the state file has no tx_context member (or omits fields), +// only overlaying the fields that are actually present in the JSON. The CLI +// sets tx_origin from --sender before loadState() and relies on the state file +// to override it only when tx_origin is present; minimal/old state files must +// not silently zero the transaction context. +// --------------------------------------------------------------------------- +TEST(EVMStateSaveLoad, MissingTxContextPreservesCallerTxContext) { + const std::string FilePath = "/tmp/dtvm_test_missing_tx_context_state.json"; + + // Minimal old-format state file: accounts only, no tx_context member. + const std::string StateJson = R"({ + "accounts": { + "a94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "balance": "0000000000000000000000000000000000000000000000000000000000000001", + "nonce": 0, "code": "", "codehash": "0000000000000000000000000000000000000000000000000000000000000000", "storage": {} + } + } +})"; + { + std::ofstream OutFile(FilePath); + OutFile << StateJson; + } + + // Pre-set the transaction context the same way the CLI does with --sender. + auto Host = std::make_unique(); + const evmc::address Sender = evmc::literals::operator""_address( + "a94f5374fce5edbc8e2a8697c15331677e6ebf0b"); + const evmc::address Coinbase = evmc::literals::operator""_address( + "b94f5374fce5edbc8e2a8697c15331677e6ebf0b"); + const evmc::uint256be GasPrice = zen::utils::parseUint256( + "0000000000000000000000000000000000000000000000000000000000000010"); + const evmc::uint256be PrevRandao = zen::utils::parseUint256( + "0000000000000000000000000000000000000000000000000000000000200000"); + const evmc::uint256be BaseFee = zen::utils::parseUint256( + "0000000000000000000000000000000000000000000000000000000000000007"); + const evmc::uint256be ChainId = zen::utils::parseUint256( + "0000000000000000000000000000000000000000000000000000000000000005"); + const evmc::uint256be BlobBaseFee = zen::utils::parseUint256( + "0000000000000000000000000000000000000000000000000000000000000003"); + + Host->tx_context.tx_origin = Sender; + Host->tx_context.tx_gas_price = GasPrice; + Host->tx_context.block_number = 42; + Host->tx_context.block_timestamp = 1700000000; + Host->tx_context.block_gas_limit = 30000000; + Host->tx_context.block_coinbase = Coinbase; + Host->tx_context.block_prev_randao = PrevRandao; + Host->tx_context.block_base_fee = BaseFee; + Host->tx_context.chain_id = ChainId; + Host->tx_context.blob_base_fee = BlobBaseFee; + + ASSERT_TRUE(zen::utils::loadState(*Host, FilePath)) + << "A state file without tx_context must still load"; + + // The caller's whole transaction context must survive untouched. + EXPECT_EQ(std::memcmp(Host->tx_context.tx_origin.bytes, Sender.bytes, 20), 0) + << "tx_origin from the caller must be preserved when the state file has " + "no tx_context"; + EXPECT_EQ( + std::memcmp(Host->tx_context.tx_gas_price.bytes, GasPrice.bytes, 32), 0) + << "gas_price from the caller must be preserved"; + EXPECT_EQ(Host->tx_context.block_number, 42); + EXPECT_EQ(Host->tx_context.block_timestamp, 1700000000); + EXPECT_EQ(Host->tx_context.block_gas_limit, 30000000); + EXPECT_EQ( + std::memcmp(Host->tx_context.block_coinbase.bytes, Coinbase.bytes, 20), + 0); + EXPECT_EQ(std::memcmp(Host->tx_context.block_prev_randao.bytes, + PrevRandao.bytes, 32), + 0); + EXPECT_EQ( + std::memcmp(Host->tx_context.block_base_fee.bytes, BaseFee.bytes, 32), 0); + EXPECT_EQ(std::memcmp(Host->tx_context.chain_id.bytes, ChainId.bytes, 32), 0); + EXPECT_EQ( + std::memcmp(Host->tx_context.blob_base_fee.bytes, BlobBaseFee.bytes, 32), + 0); + + // The account from the state file must still be loaded. + EXPECT_NE(Host->accounts.find(Sender), Host->accounts.end()); + + std::filesystem::remove(FilePath); +} + +TEST(EVMStateSaveLoad, PartialTxContextOverlaysOnlyPresentFields) { + const std::string FilePath = "/tmp/dtvm_test_partial_tx_context_state.json"; + + // tx_context object with only tx_origin present; every other field absent. + const std::string StateJson = R"({ + "accounts": {}, + "tx_context": { + "tx_origin": "b94f5374fce5edbc8e2a8697c15331677e6ebf0b" + } +})"; + { + std::ofstream OutFile(FilePath); + OutFile << StateJson; + } + + auto Host = std::make_unique(); + const evmc::address CallerOrigin = evmc::literals::operator""_address( + "a94f5374fce5edbc8e2a8697c15331677e6ebf0b"); + const evmc::address StateOrigin = evmc::literals::operator""_address( + "b94f5374fce5edbc8e2a8697c15331677e6ebf0b"); + const evmc::uint256be GasPrice = zen::utils::parseUint256( + "0000000000000000000000000000000000000000000000000000000000000010"); + + Host->tx_context.tx_origin = CallerOrigin; + Host->tx_context.tx_gas_price = GasPrice; + Host->tx_context.block_number = 42; + Host->tx_context.block_timestamp = 1700000000; + Host->tx_context.block_gas_limit = 30000000; + + ASSERT_TRUE(zen::utils::loadState(*Host, FilePath)); + + // tx_origin is present in the state file and must override the caller. + EXPECT_EQ( + std::memcmp(Host->tx_context.tx_origin.bytes, StateOrigin.bytes, 20), 0) + << "tx_origin present in the state file must override the caller value"; + // Every absent field keeps the caller's value. + EXPECT_EQ( + std::memcmp(Host->tx_context.tx_gas_price.bytes, GasPrice.bytes, 32), 0) + << "Absent tx_context fields must keep the caller's value"; + EXPECT_EQ(Host->tx_context.block_number, 42); + EXPECT_EQ(Host->tx_context.block_timestamp, 1700000000); + EXPECT_EQ(Host->tx_context.block_gas_limit, 30000000); + + std::filesystem::remove(FilePath); +} + +// --------------------------------------------------------------------------- +// Regression test: loadState() must pre-warm the EIP-2930 access list on the +// caller's host. Account warmth lives in the host access journal (not in the +// accounts map), so any prewarming done on a temporary parsing host would be +// discarded together with it and execution would charge cold-access gas for +// access-list entries. +// --------------------------------------------------------------------------- +TEST(EVMStateSaveLoad, AccessListPrewarmSurvivesLoadState) { + const std::string FilePath = "/tmp/dtvm_test_access_list_state.json"; + + // One entry with storage keys, one plain address entry, and one malformed + // entry that must be skipped without failing the load. + const std::string StateJson = R"({ + "accounts": {}, + "access_list": [ + { + "address": "00000000000000000000000000000000000000aa", + "storage_keys": [ + "0000000000000000000000000000000000000000000000000000000000000001", + "not-hex" + ] + }, + {"address": "00000000000000000000000000000000000000bb"}, + {"address": "zzz"} + ] +})"; + { + std::ofstream OutFile(FilePath); + OutFile << StateJson; + } + + auto Host = std::make_unique(); + const evmc::address WarmAddr = evmc::literals::operator""_address( + "00000000000000000000000000000000000000aa"); + const evmc::address WarmAddrNoKeys = evmc::literals::operator""_address( + "00000000000000000000000000000000000000bb"); + const evmc::bytes32 WarmKey = zen::utils::parseBytes32( + "0000000000000000000000000000000000000000000000000000000000000001"); + + ASSERT_TRUE(zen::utils::loadState(*Host, FilePath)); + + // Account warmth must survive loadState (EIP-2930). + EXPECT_EQ(Host->access_account(WarmAddr), EVMC_ACCESS_WARM) + << "Access-list account must be warm after loadState"; + EXPECT_EQ(Host->access_account(WarmAddrNoKeys), EVMC_ACCESS_WARM) + << "Access-list entry without storage keys must also be warm"; + + // Storage key warmth must survive loadState. + EXPECT_EQ(Host->access_storage(WarmAddr, WarmKey), EVMC_ACCESS_WARM) + << "Access-list storage key must be warm after loadState"; + + // The access-list-only account must stay materialized so keys of accounts + // created later in the transaction remain observable. + EXPECT_NE(Host->accounts.find(WarmAddr), Host->accounts.end()); + + std::filesystem::remove(FilePath); +} + // Regression test for https://github.com/DTVMStack/DTVM/issues/589. // A storage value loaded from prestate is both current and original for the // new transaction. If original remains zero, a non-zero prestate slot written @@ -2093,3 +2282,353 @@ TEST(EVMStateSaveLoad, BlockTimestampAtInt64Max) { std::filesystem::remove(FilePath); } +// --------------------------------------------------------------------------- +// Regression tests for upstream issues that were fixed by PR #601 - #610. +// --------------------------------------------------------------------------- +TEST(EVMStateSaveLoad, Issue601_OverlongUint256FailsWithoutAbort) { + const std::string StateFilePath = "/tmp/dtvm_issue601_state.json"; + const std::string OverlongHex(66, 'a'); + const std::string StateJson = R"({ + "accounts": { + "00000000000000000000000000000000000000f1": { + "balance": ")" + OverlongHex + + R"(", + "code": "0x5f00", + "nonce": 0, + "storage": {} + } + } + })"; + + { + std::ofstream StateFile(StateFilePath); + ASSERT_TRUE(StateFile) << "Failed to create issue #601 state file"; + StateFile << StateJson; + } + + auto Host = std::make_unique(); + EXPECT_FALSE(zen::utils::loadState(*Host, StateFilePath)) + << "An overlong uint256 must be rejected, not crash the process"; + std::filesystem::remove(StateFilePath); +} + +TEST(EVMRegressionTest, Issue602_KZGPrecompileIsWarmOnCancun) { + auto Host = std::make_unique(); + evmc::address Sender{}; + evmc::address Recipient{}; + evmc::address Coinbase{}; + zen::utils::prewarmTransactionAccounts(*Host, EVMC_CANCUN, Sender, Recipient, + Coinbase); + evmc::address KZGPrecompile{}; + KZGPrecompile.bytes[19] = 0x0a; + EXPECT_EQ(Host->access_account(KZGPrecompile), EVMC_ACCESS_WARM) + << "EIP-4844 KZG precompile must be warm at transaction start"; +} + +TEST(EVMRegressionTest, Issue602_KZGPrecompileStaysColdBeforeCancun) { + auto Host = std::make_unique(); + evmc::address Sender{}; + evmc::address Recipient{}; + evmc::address Coinbase{}; + zen::utils::prewarmTransactionAccounts(*Host, EVMC_BERLIN, Sender, Recipient, + Coinbase); + evmc::address KZGPrecompile{}; + KZGPrecompile.bytes[19] = 0x0a; + EXPECT_EQ(Host->access_account(KZGPrecompile), EVMC_ACCESS_COLD) + << "KZG precompile must not be pre-warmed before Cancun"; +} + +// EIP-2537 (Prague) activates the BLS12-381 precompiles at 0x0b-0x10, so the +// revision-aware prewarm range must extend to 0x10; 0x11 and above stay cold. +TEST(EVMRegressionTest, BLSPrecompilesAreWarmOnPrague) { + auto Host = std::make_unique(); + evmc::address Sender{}; + evmc::address Recipient{}; + evmc::address Coinbase{}; + zen::utils::prewarmTransactionAccounts(*Host, EVMC_PRAGUE, Sender, Recipient, + Coinbase); + for (int PrecompileIdx = 0x0b; PrecompileIdx <= 0x10; ++PrecompileIdx) { + evmc::address BLSPrecompile{}; + BLSPrecompile.bytes[19] = static_cast(PrecompileIdx); + EXPECT_EQ(Host->access_account(BLSPrecompile), EVMC_ACCESS_WARM) + << "BLS precompile at index " << PrecompileIdx + << " must be warm at transaction start on Prague"; + } + // Nothing above 0x10 is a known precompile, so it must not be pre-warmed. + evmc::address FirstUnassigned{}; + FirstUnassigned.bytes[19] = 0x11; + EXPECT_EQ(Host->access_account(FirstUnassigned), EVMC_ACCESS_COLD) + << "Addresses above the BLS range must not be pre-warmed"; +} + +TEST(EVMRegressionTest, BLSPrecompilesStayColdBeforePrague) { + auto Host = std::make_unique(); + evmc::address Sender{}; + evmc::address Recipient{}; + evmc::address Coinbase{}; + zen::utils::prewarmTransactionAccounts(*Host, EVMC_CANCUN, Sender, Recipient, + Coinbase); + evmc::address BLSPrecompile{}; + BLSPrecompile.bytes[19] = 0x0b; + EXPECT_EQ(Host->access_account(BLSPrecompile), EVMC_ACCESS_COLD) + << "BLS precompiles must not be pre-warmed before Prague"; +} + +TEST(EVMRegressionTest, Issue606_EmptyPrestateAccountChargesNewAccountGas) { + const evmc::address SenderAddr = evmc::literals::operator""_address( + "1111111111111111111111111111111111111111"); + const evmc::address ContractAddr = evmc::literals::operator""_address( + "0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f"); + const evmc::address CalleeAddr = evmc::literals::operator""_address( + "a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7"); + std::string BytecodeHex = "5f5f5f5f600173"; + for (int I = 0; I < 20; ++I) { + BytecodeHex += "a7"; + } + BytecodeHex += "600ff15a6280010555"; + const std::vector Bytecode = + zen::utils::fromHex(BytecodeHex).value(); + + auto Host = std::make_unique(); + Host->accounts[ContractAddr].code = + evmc::bytes(Bytecode.data(), Bytecode.size()); + Host->accounts[SenderAddr].set_balance(0x1bc16d674ec80000ULL); + Host->accounts[CalleeAddr] = evmc::MockedAccount{}; + Host->tx_context.block_coinbase = evmc::address{}; + Host->tx_context.tx_origin = SenderAddr; + Host->tx_context.tx_gas_price = + intx::be::store(intx::uint256(1)); + Host->tx_context.block_base_fee = + intx::be::store(intx::uint256(1)); + + RuntimeConfig Config; + Config.Mode = common::RunMode::InterpMode; + auto RT = Runtime::newEVMRuntime(Config, Host.get()); + ASSERT_TRUE(RT); + Host->setRuntime(RT.get()); + + zen::evm::ZenMockedEVMHost::TransactionExecutionConfig ExecConfig; + ExecConfig.ModuleName = "issue606"; + ExecConfig.Bytecode = Bytecode.data(); + ExecConfig.BytecodeSize = Bytecode.size(); + ExecConfig.Revision = EVMC_CANCUN; + // The issue's CLI command starts the contract with gas-limit 1000000 minus + // the basic intrinsic gas. This keeps the verdict independent of the + // CLI's different fee paths. + ExecConfig.GasLimit = 1000000 - 21000; + evmc_message Msg{}; + Msg.kind = EVMC_CALL; + Msg.gas = 1000000 - 21000; + Msg.sender = SenderAddr; + Msg.recipient = ContractAddr; + Msg.code_address = ContractAddr; + ExecConfig.Message = Msg; + + auto Result = Host->executeTransaction(ExecConfig); + ASSERT_TRUE(Result.Success) << Result.ErrorMessage; + ASSERT_EQ(Result.Status, EVMC_SUCCESS); + + const evmc::bytes32 StorageKey = zen::utils::parseBytes32( + "0000000000000000000000000000000000000000000000000000000000800105"); + const auto &StorageValue = + Host->accounts[ContractAddr].storage.at(StorageKey).current; + const evmc::bytes32 ExpectedRemainingGas = zen::utils::parseBytes32( + "00000000000000000000000000000000000000000000000000000000000e6a29"); + EXPECT_EQ(std::memcmp(StorageValue.bytes, ExpectedRemainingGas.bytes, 32), 0) + << "CALL with value to an empty prestate account must charge 25000 gas"; +} + +TEST(EVMRegressionTest, Issue593_CreateDoesNotCreditPhantomBalance) { + const evmc::address SenderAddr = evmc::literals::operator""_address( + "a94f5374fce5edbc8e2a8697c15331677e6ebf0b"); + const evmc::address ContractAddr = evmc::literals::operator""_address( + "000000000000000000000000000000ca1100f022"); + const std::vector Bytecode = + zen::utils::fromHex( + "5f5f5f5f5f5f5f63304130305f5ff13030f45f5f5ff05f5f5ff0") + .value(); + std::vector Calldata(80, 0x01); + + auto Host = std::make_unique(); + Host->accounts[ContractAddr].code = + evmc::bytes(Bytecode.data(), Bytecode.size()); + Host->accounts[SenderAddr].set_balance(0x3fffffffffffffffULL); + evmc_tx_context TxCtx{}; + TxCtx.tx_origin = SenderAddr; + TxCtx.tx_gas_price = intx::be::store(intx::uint256(0x80)); + TxCtx.block_base_fee = intx::be::store(intx::uint256(0x10)); + TxCtx.block_coinbase = evmc::literals::operator""_address( + "b94f5374fce5edbc8e2a8697c15331677e6ebf0b"); + Host->tx_context = TxCtx; + + RuntimeConfig Config; + Config.Mode = common::RunMode::InterpMode; + auto RT = Runtime::newEVMRuntime(Config, Host.get()); + ASSERT_TRUE(RT); + Host->setRuntime(RT.get()); + + zen::evm::ZenMockedEVMHost::TransactionExecutionConfig ExecConfig; + ExecConfig.ModuleName = "issue593"; + ExecConfig.Bytecode = Bytecode.data(); + ExecConfig.BytecodeSize = Bytecode.size(); + ExecConfig.Revision = EVMC_CANCUN; + ExecConfig.GasLimit = 20000000; + evmc_message Msg{}; + Msg.kind = EVMC_CALL; + Msg.sender = SenderAddr; + Msg.recipient = ContractAddr; + Msg.code_address = ContractAddr; + Msg.gas = 20000000; + Msg.input_data = Calldata.data(); + Msg.input_size = Calldata.size(); + ExecConfig.Message = Msg; + + auto Result = Host->executeTransaction(ExecConfig); + ASSERT_TRUE(Result.Success) << Result.ErrorMessage; + + for (const auto &[Address, Account] : Host->accounts) { + if (Address == SenderAddr || Address == TxCtx.block_coinbase) { + continue; + } + const intx::uint256 Balance = + intx::be::load(Account.balance); + EXPECT_EQ(Balance, intx::uint256(0)) + << "No account may receive balance out of thin air"; + } +} + +TEST(EVMRegressionTest, Issue610_IntrinsicGasChargedOnce) { + const std::vector Bytecode = {0x00}; // STOP + auto Result = runDtvmCliSettlementTransaction( + SettlementContractAddr, SettlementSenderAddr, Bytecode, EVMC_CANCUN, + 21000, [&](zen::evm::ZenMockedEVMHost &Host) { + prepareSettlementHost(Host, Bytecode, SettlementSenderAddr, + SettlementContractAddr); + }); + ASSERT_TRUE(Result.Success) << "A plain STOP transaction should succeed"; + EXPECT_EQ(Result.GasUsed, 21000u) + << "Only basic intrinsic gas may be charged for an empty contract"; + EXPECT_EQ(Result.GasCharged, 21000u); + EXPECT_EQ(Result.InitialSenderBalance - Result.FinalSenderBalance, + SettlementGasPrice * 21000); +} + +#ifdef ZEN_ENABLE_MULTIPASS_JIT +TEST(EVMRegressionTest, Issue592_DelegateCallRecursionMultipassSucceeds) { + const evmc::address SenderAddr = evmc::literals::operator""_address( + "a94f5374fce5edbc8e2a8697c15331677e6ebf0b"); + const evmc::address ContractAddr = evmc::literals::operator""_address( + "000000000000000000000000000000ca1100f022"); + const std::vector Bytecode = + zen::utils::fromHex("5f5f5f60305f5f5f5f5f5ff23030f45f5f535f5f53").value(); + + RuntimeConfig Config; + Config.Mode = common::RunMode::MultipassMode; + Config.EnableEvmGasMetering = true; + + auto Host = std::make_unique(); + Host->accounts[ContractAddr].code = + evmc::bytes(Bytecode.data(), Bytecode.size()); + Host->accounts[SenderAddr].set_balance(0x3fffffffffffffffULL); + Host->tx_context.tx_origin = SenderAddr; + + auto RT = Runtime::newEVMRuntime(Config, Host.get()); + ASSERT_TRUE(RT); + Host->setRuntime(RT.get()); + + zen::evm::ZenMockedEVMHost::TransactionExecutionConfig ExecConfig; + ExecConfig.ModuleName = "issue592_multipass"; + ExecConfig.Bytecode = Bytecode.data(); + ExecConfig.BytecodeSize = Bytecode.size(); + ExecConfig.Revision = EVMC_CANCUN; + ExecConfig.GasLimit = 20000000; + evmc_message Msg{}; + Msg.kind = EVMC_CALL; + Msg.gas = 20000000; + Msg.sender = SenderAddr; + Msg.recipient = ContractAddr; + Msg.code_address = ContractAddr; + ExecConfig.Message = Msg; + + auto Result = Host->executeTransaction(ExecConfig); + ASSERT_TRUE(Result.Success) << Result.ErrorMessage; + ASSERT_EQ(Result.Status, EVMC_SUCCESS) + << "Multipass must execute the delegatecall recursion without aborting"; +} + +TEST(EVMRegressionTest, Issue603_KeccakOffsetThenMLoadsMultipassSucceeds) { + const std::vector Bytecode = + zen::utils::fromHex("6020610100205f515f51").value(); + auto InterpExec = executeEvmBytecode("issue603_interp", Bytecode, + common::RunMode::InterpMode); + auto MultipassExec = executeEvmBytecode("issue603_multipass", Bytecode, + common::RunMode::MultipassMode); + ASSERT_EQ(InterpExec.Status, EVMC_SUCCESS); + ASSERT_EQ(MultipassExec.Status, EVMC_SUCCESS) + << "Multipass must not crash for KECCAK with a non-zero offset followed " + "by memory loads"; +#ifdef ZEN_ENABLE_JIT + EXPECT_TRUE(MultipassExec.JITCompiled); +#endif +} + +TEST(EVMRegressionTest, Issue594_JumpdestThenMloadMultipassSucceeds) { + const std::vector Bytecode = + zen::utils::fromHex("5b5f515f5f57").value(); + auto InterpExec = executeEvmBytecode("issue594_interp", Bytecode, + common::RunMode::InterpMode); + auto MultipassExec = executeEvmBytecode("issue594_multipass", Bytecode, + common::RunMode::MultipassMode); + ASSERT_EQ(InterpExec.Status, EVMC_SUCCESS); + ASSERT_EQ(MultipassExec.Status, EVMC_SUCCESS) + << "Multipass must execute a simple JUMPDEST/MLOAD/JUMPI sequence"; +#ifdef ZEN_ENABLE_JIT + EXPECT_TRUE(MultipassExec.JITCompiled); +#endif +} + +// --------------------------------------------------------------------------- +// Review follow-up to the Issue #603 KECCAK memory-cache fix: the specialized +// two-word KECCAK helpers (caller-slot and calldata-slot macro-ops) also take +// the prepared-memory no-expand path and can initialize/grow the instance +// memory on first use. Direct memory operations right after them must not see +// a stale cached base pointer/size. +// --------------------------------------------------------------------------- +TEST(EVMRegressionTest, KeccakCallerSlotThenMLoadsMultipassSucceeds) { + // CALLER PUSH1 0x40 MSTORE PUSH1 5 PUSH1 0x60 MSTORE + // PUSH1 0x40 PUSH1 0x40 KECCAK256 PUSH0 MLOAD PUSH0 MLOAD STOP + const std::vector Bytecode = + zen::utils::fromHex("33604052600560605260406040205f515f5100").value(); + auto InterpExec = executeEvmBytecode("keccak_caller_slot_interp", Bytecode, + common::RunMode::InterpMode); + auto MultipassExec = executeEvmBytecode( + "keccak_caller_slot_multipass", Bytecode, common::RunMode::MultipassMode); + ASSERT_EQ(InterpExec.Status, EVMC_SUCCESS); + ASSERT_EQ(MultipassExec.Status, EVMC_SUCCESS) + << "Multipass must not crash for the caller-slot KECCAK macro-op " + "followed by memory loads"; +#ifdef ZEN_ENABLE_JIT + EXPECT_TRUE(MultipassExec.JITCompiled); +#endif +} + +TEST(EVMRegressionTest, KeccakCallDataSlotThenMLoadsMultipassSucceeds) { + // PUSH1 0 CALLDATALOAD PUSH1 0x40 MSTORE PUSH1 5 PUSH1 0x60 MSTORE + // PUSH1 0x40 PUSH1 0x40 KECCAK256 PUSH0 MLOAD PUSH0 MLOAD STOP + const std::vector Bytecode = + zen::utils::fromHex("600035604052600560605260406040205f515f5100").value(); + const std::vector CallData = makeUint256Calldata(0x1234); + auto InterpExec = executeEvmBytecode("keccak_calldata_slot_interp", Bytecode, + common::RunMode::InterpMode, CallData); + auto MultipassExec = + executeEvmBytecode("keccak_calldata_slot_multipass", Bytecode, + common::RunMode::MultipassMode, CallData); + ASSERT_EQ(InterpExec.Status, EVMC_SUCCESS); + ASSERT_EQ(MultipassExec.Status, EVMC_SUCCESS) + << "Multipass must not crash for the calldata-slot KECCAK macro-op " + "followed by memory loads"; +#ifdef ZEN_ENABLE_JIT + EXPECT_TRUE(MultipassExec.JITCompiled); +#endif +} +#endif // ZEN_ENABLE_MULTIPASS_JIT diff --git a/src/tests/evm_test_host.hpp b/src/tests/evm_test_host.hpp index faf8ac9ab..fb52bf469 100644 --- a/src/tests/evm_test_host.hpp +++ b/src/tests/evm_test_host.hpp @@ -473,13 +473,10 @@ class ZenMockedEVMHost : public evmc::MockedHost { return false; } const auto &Acc = It->second; - if (Acc.nonce != 0) { - return true; - } - if (!Acc.code.empty()) { - return true; - } - if (std::memcmp(Acc.codehash.bytes, EMPTY_CODE_HASH.bytes, 32) != 0) { + // EIP-161: An account is empty when nonce, balance, and code are all + // zero. Do not use a zero code hash as evidence of a non-empty account: + // persisted prestate accounts may legitimately have a zeroed code hash. + if (Acc.nonce != 0 || !Acc.code.empty()) { return true; } return toUint256Bytes(Acc.balance) != 0; diff --git a/src/utils/evm.cpp b/src/utils/evm.cpp index 0dc267ec5..65f267299 100644 --- a/src/utils/evm.cpp +++ b/src/utils/evm.cpp @@ -13,6 +13,8 @@ #include #include #include +#include +#include namespace zen::utils { @@ -329,204 +331,240 @@ bool loadState(evmc::MockedHost &Host, const std::string &FilePath) { return false; } - Host.accounts.clear(); - - // Parse accounts - if (Doc.HasMember("accounts") && Doc["accounts"].IsObject()) { - const rapidjson::Value &Accounts = Doc["accounts"]; - - for (auto It = Accounts.MemberBegin(); It != Accounts.MemberEnd(); ++It) { - const std::string AddressStr = It->name.GetString(); - evmc::address Address = zen::utils::parseAddress(AddressStr); - - const rapidjson::Value &AccountData = It->value; - evmc::MockedAccount Account; - - // Parse balance - if (AccountData.HasMember("balance") && - AccountData["balance"].IsString()) { - Account.balance = - zen::utils::parseUint256(AccountData["balance"].GetString()); - } + // Parse into temporary storage first so failures cannot partially mutate the + // caller-supplied host. Start from the caller's tx_context so a state file + // without tx_context (or with missing fields) keeps the values the caller + // already established (e.g. tx_origin from the CLI --sender); fields present + // in the JSON are overlaid on top of it. + evmc::MockedHost ParsedHost; + ParsedHost.accounts.clear(); + ParsedHost.tx_context = Host.tx_context; + + // Access-list entries are validated against the temporary state and replayed + // on the caller's host only after the atomic commit below: account warmth + // lives in the host access journal, which would be discarded together with + // the temporary host. + std::vector>> + ParsedAccessList; + + try { + // Parse accounts + if (Doc.HasMember("accounts") && Doc["accounts"].IsObject()) { + const rapidjson::Value &Accounts = Doc["accounts"]; + + for (auto It = Accounts.MemberBegin(); It != Accounts.MemberEnd(); ++It) { + const std::string AddressStr = It->name.GetString(); + evmc::address Address = zen::utils::parseAddress(AddressStr); + + const rapidjson::Value &AccountData = It->value; + evmc::MockedAccount &Account = ParsedHost.accounts[Address]; + + // Parse balance + if (AccountData.HasMember("balance") && + AccountData["balance"].IsString()) { + Account.balance = + zen::utils::parseUint256(AccountData["balance"].GetString()); + } - // Parse nonce - if (AccountData.HasMember("nonce") && AccountData["nonce"].IsUint64()) { - Account.nonce = AccountData["nonce"].GetUint64(); - } else if (AccountData.HasMember("nonce") && - AccountData["nonce"].IsString()) { - std::string NonceStr = - zen::utils::stripHexPrefix(AccountData["nonce"].GetString()); - Account.nonce = std::stoull(NonceStr, nullptr, 16); - } + // Parse nonce + if (AccountData.HasMember("nonce") && AccountData["nonce"].IsUint64()) { + Account.nonce = AccountData["nonce"].GetUint64(); + } else if (AccountData.HasMember("nonce") && + AccountData["nonce"].IsString()) { + std::string NonceStr = + zen::utils::stripHexPrefix(AccountData["nonce"].GetString()); + Account.nonce = std::stoull(NonceStr, nullptr, 16); + } - // Parse code - if (AccountData.HasMember("code") && AccountData["code"].IsString()) { - Account.code = zen::utils::hexToBytes(AccountData["code"].GetString()); - } + // Parse code + if (AccountData.HasMember("code") && AccountData["code"].IsString()) { + Account.code = + zen::utils::hexToBytes(AccountData["code"].GetString()); + } - // Parse codehash - if (AccountData.HasMember("codehash") && - AccountData["codehash"].IsString()) { - Account.codehash = - zen::utils::parseBytes32(AccountData["codehash"].GetString()); - } + // Parse codehash + if (AccountData.HasMember("codehash") && + AccountData["codehash"].IsString()) { + Account.codehash = + zen::utils::parseBytes32(AccountData["codehash"].GetString()); + } - // Parse storage - if (AccountData.HasMember("storage") && - AccountData["storage"].IsObject()) { - const rapidjson::Value &Storage = AccountData["storage"]; - - for (auto StorageIt = Storage.MemberBegin(); - StorageIt != Storage.MemberEnd(); ++StorageIt) { - const std::string KeyStr = StorageIt->name.GetString(); - evmc::bytes32 Key = zen::utils::parseBytes32(KeyStr); - - const rapidjson::Value &StorageValue = StorageIt->value; - evmc::StorageValue StorageVal; - - if (StorageValue.IsObject()) { - // The optional original value allows callers to provide a - // transaction-specific original. access_status is never part of a - // pre-state; legacy files may include it and it must be ignored. - if (StorageValue.HasMember("value") && - StorageValue["value"].IsString()) { + // Parse storage + if (AccountData.HasMember("storage") && + AccountData["storage"].IsObject()) { + const rapidjson::Value &Storage = AccountData["storage"]; + + for (auto StorageIt = Storage.MemberBegin(); + StorageIt != Storage.MemberEnd(); ++StorageIt) { + const std::string KeyStr = StorageIt->name.GetString(); + evmc::bytes32 Key = zen::utils::parseBytes32(KeyStr); + + const rapidjson::Value &StorageValue = StorageIt->value; + evmc::StorageValue StorageVal; + + if (StorageValue.IsObject()) { + // The optional original value allows callers to provide a + // transaction-specific original. access_status is never part of a + // pre-state; legacy files may include it and it must be ignored. + if (StorageValue.HasMember("value") && + StorageValue["value"].IsString()) { + StorageVal.current = + zen::utils::parseBytes32(StorageValue["value"].GetString()); + } + if (StorageValue.HasMember("original") && + StorageValue["original"].IsString()) { + StorageVal.original = zen::utils::parseBytes32( + StorageValue["original"].GetString()); + } else { + // A persisted transaction-final state is the start of a new + // transaction. Unless an explicit original value is provided, + // current is also the transaction's original value. + StorageVal.original = StorageVal.current; + } + } else if (StorageValue.IsString()) { + // Old format with just value StorageVal.current = - zen::utils::parseBytes32(StorageValue["value"].GetString()); - } - if (StorageValue.HasMember("original") && - StorageValue["original"].IsString()) { - StorageVal.original = zen::utils::parseBytes32( - StorageValue["original"].GetString()); - } else { - // A persisted transaction-final state is the start of a new - // transaction. Unless an explicit original value is provided, - // current is also the transaction's original value. + zen::utils::parseBytes32(StorageValue.GetString()); StorageVal.original = StorageVal.current; } - } else if (StorageValue.IsString()) { - // Old format with just value - StorageVal.current = - zen::utils::parseBytes32(StorageValue.GetString()); - StorageVal.original = StorageVal.current; - } - Account.storage[Key] = StorageVal; + ParsedHost.accounts[Address].storage[Key] = StorageVal; + } } } - - Host.accounts[Address] = Account; } - } - // Parse tx_context if available - if (Doc.HasMember("tx_context") && Doc["tx_context"].IsObject()) { - const rapidjson::Value &TxContext = Doc["tx_context"]; + // Parse tx_context if available + if (Doc.HasMember("tx_context") && Doc["tx_context"].IsObject()) { + const rapidjson::Value &TxContext = Doc["tx_context"]; - if (TxContext.HasMember("gas_price") && TxContext["gas_price"].IsString()) { - Host.tx_context.tx_gas_price = - zen::utils::parseUint256(TxContext["gas_price"].GetString()); - } - - if (TxContext.HasMember("block_number") && - TxContext["block_number"].IsUint64()) { - auto Val = TxContext["block_number"].GetUint64(); - if (Val > static_cast(std::numeric_limits::max())) { - Val = static_cast(std::numeric_limits::max()); + if (TxContext.HasMember("gas_price") && + TxContext["gas_price"].IsString()) { + ParsedHost.tx_context.tx_gas_price = + zen::utils::parseUint256(TxContext["gas_price"].GetString()); } - Host.tx_context.block_number = static_cast(Val); - } - if (TxContext.HasMember("block_timestamp") && - TxContext["block_timestamp"].IsUint64()) { - auto Val = TxContext["block_timestamp"].GetUint64(); - if (Val > static_cast(std::numeric_limits::max())) { - Val = static_cast(std::numeric_limits::max()); + if (TxContext.HasMember("block_number") && + TxContext["block_number"].IsUint64()) { + auto Val = TxContext["block_number"].GetUint64(); + if (Val > static_cast(std::numeric_limits::max())) { + Val = static_cast(std::numeric_limits::max()); + } + ParsedHost.tx_context.block_number = static_cast(Val); } - Host.tx_context.block_timestamp = static_cast(Val); - } - - if (TxContext.HasMember("block_coinbase") && - TxContext["block_coinbase"].IsString()) { - Host.tx_context.block_coinbase = - zen::utils::parseAddress(TxContext["block_coinbase"].GetString()); - } - if (TxContext.HasMember("block_prev_randao") && - TxContext["block_prev_randao"].IsString()) { - Host.tx_context.block_prev_randao = - zen::utils::parseUint256(TxContext["block_prev_randao"].GetString()); - } - - if (TxContext.HasMember("block_gas_limit") && - TxContext["block_gas_limit"].IsUint64()) { - auto Val = TxContext["block_gas_limit"].GetUint64(); - if (Val > static_cast(std::numeric_limits::max())) { - Val = static_cast(std::numeric_limits::max()); + if (TxContext.HasMember("block_timestamp") && + TxContext["block_timestamp"].IsUint64()) { + auto Val = TxContext["block_timestamp"].GetUint64(); + if (Val > static_cast(std::numeric_limits::max())) { + Val = static_cast(std::numeric_limits::max()); + } + ParsedHost.tx_context.block_timestamp = static_cast(Val); } - Host.tx_context.block_gas_limit = static_cast(Val); - } - if (TxContext.HasMember("block_base_fee") && - TxContext["block_base_fee"].IsString()) { - Host.tx_context.block_base_fee = - zen::utils::parseUint256(TxContext["block_base_fee"].GetString()); - } + if (TxContext.HasMember("block_coinbase") && + TxContext["block_coinbase"].IsString()) { + ParsedHost.tx_context.block_coinbase = + zen::utils::parseAddress(TxContext["block_coinbase"].GetString()); + } - if (TxContext.HasMember("tx_origin") && TxContext["tx_origin"].IsString()) { - Host.tx_context.tx_origin = - zen::utils::parseAddress(TxContext["tx_origin"].GetString()); - } + if (TxContext.HasMember("block_prev_randao") && + TxContext["block_prev_randao"].IsString()) { + ParsedHost.tx_context.block_prev_randao = zen::utils::parseUint256( + TxContext["block_prev_randao"].GetString()); + } - if (TxContext.HasMember("chain_id") && TxContext["chain_id"].IsString()) { - Host.tx_context.chain_id = - zen::utils::parseUint256(TxContext["chain_id"].GetString()); - } + if (TxContext.HasMember("block_gas_limit") && + TxContext["block_gas_limit"].IsUint64()) { + auto Val = TxContext["block_gas_limit"].GetUint64(); + if (Val > static_cast(std::numeric_limits::max())) { + Val = static_cast(std::numeric_limits::max()); + } + ParsedHost.tx_context.block_gas_limit = static_cast(Val); + } - if (TxContext.HasMember("blob_base_fee") && - TxContext["blob_base_fee"].IsString()) { - Host.tx_context.blob_base_fee = - zen::utils::parseUint256(TxContext["blob_base_fee"].GetString()); - } - } + if (TxContext.HasMember("block_base_fee") && + TxContext["block_base_fee"].IsString()) { + ParsedHost.tx_context.block_base_fee = + zen::utils::parseUint256(TxContext["block_base_fee"].GetString()); + } - // Parse and pre-warm EIP-2930 access list if present. - // Warm addresses cost 100 gas instead of cold 2600, warm storage slots - // cost 100 gas instead of cold 2100. - if (Doc.HasMember("access_list") && Doc["access_list"].IsArray()) { - for (const auto &Entry : Doc["access_list"].GetArray()) { - if (!Entry.IsObject() || !Entry.HasMember("address") || - !Entry["address"].IsString()) { - continue; + if (TxContext.HasMember("tx_origin") && + TxContext["tx_origin"].IsString()) { + ParsedHost.tx_context.tx_origin = + zen::utils::parseAddress(TxContext["tx_origin"].GetString()); } - evmc::address Address; - try { - Address = zen::utils::parseAddress(Entry["address"].GetString()); - } catch (...) { - continue; + + if (TxContext.HasMember("chain_id") && TxContext["chain_id"].IsString()) { + ParsedHost.tx_context.chain_id = + zen::utils::parseUint256(TxContext["chain_id"].GetString()); } - Host.access_account(Address); - if (!Entry.HasMember("storage_keys") || - !Entry["storage_keys"].IsArray()) { - continue; + if (TxContext.HasMember("blob_base_fee") && + TxContext["blob_base_fee"].IsString()) { + ParsedHost.tx_context.blob_base_fee = + zen::utils::parseUint256(TxContext["blob_base_fee"].GetString()); } - // EIP-2930 requires access-list storage keys to be warm even when the - // account is not yet present in the initial host state (e.g. when the - // contract is created later in the same transaction). Use the host - // access_storage API so the account entry is materialized automatically. - for (const auto &KeyVal : Entry["storage_keys"].GetArray()) { - if (!KeyVal.IsString()) { + } + + // Parse (validate) the EIP-2930 access list if present. + // Warm addresses cost 100 gas instead of cold 2600, warm storage slots + // cost 100 gas instead of cold 2100. + if (Doc.HasMember("access_list") && Doc["access_list"].IsArray()) { + for (const auto &Entry : Doc["access_list"].GetArray()) { + if (!Entry.IsObject() || !Entry.HasMember("address") || + !Entry["address"].IsString()) { continue; } + evmc::address Address; try { - evmc::bytes32 Key = zen::utils::parseBytes32(KeyVal.GetString()); - Host.access_storage(Address, Key); + Address = zen::utils::parseAddress(Entry["address"].GetString()); } catch (...) { continue; } + + std::vector StorageKeys; + if (Entry.HasMember("storage_keys") && + Entry["storage_keys"].IsArray()) { + for (const auto &KeyVal : Entry["storage_keys"].GetArray()) { + if (!KeyVal.IsString()) { + continue; + } + try { + StorageKeys.emplace_back( + zen::utils::parseBytes32(KeyVal.GetString())); + } catch (...) { + continue; + } + } + } + ParsedAccessList.emplace_back(Address, std::move(StorageKeys)); } } + } catch (const std::exception &) { + return false; } + + // Data is assigned only after all fields parse successfully so a malformed + // state file cannot leave the caller with a partially-updated host. + Host.accounts = std::move(ParsedHost.accounts); + Host.tx_context = ParsedHost.tx_context; + + // Replay the validated access list on the caller's host. Account warmth is + // journaled in the host itself (not in accounts), so prewarming must happen + // here; replaying after the atomic commit keeps a malformed state file from + // partially warming the caller's state. + for (const auto &AccessEntry : ParsedAccessList) { + Host.access_account(AccessEntry.first); + // EIP-2930 requires access-list storage keys to be warm even when the + // account is not yet present in the initial host state (e.g. when the + // contract is created later in the same transaction). access_storage + // materializes the account entry automatically. + for (const auto &Key : AccessEntry.second) { + Host.access_storage(AccessEntry.first, Key); + } + } + return true; } @@ -559,8 +597,11 @@ void prewarmTransactionAccounts(evmc::MockedHost &Host, evmc_revision Revision, const evmc::address &Sender, const evmc::address &Recipient, const evmc::address &Coinbase) { - // EIP-2929 (Berlin+): sender, recipient, and precompiled contracts - // (0x01-0x09) are always warm at the start of a transaction. + // EIP-2929 (Berlin+): sender, recipient, and precompiled contracts are + // always warm at the start of a transaction. Keep the precompile range + // revision-aware: Cancun adds the KZG point evaluation precompile at 0x0a + // (EIP-4844) and Prague adds the BLS12-381 precompiles at 0x0b-0x10 + // (EIP-2537). if (Revision >= EVMC_BERLIN) { Host.access_account(Sender); // Contract-creation transactions do not have a transaction-level recipient. @@ -569,9 +610,17 @@ void prewarmTransactionAccounts(evmc::MockedHost &Host, evmc_revision Revision, if (Recipient != evmc::address{}) { Host.access_account(Recipient); } - for (int PrecompileIdx = 1; PrecompileIdx <= 9; ++PrecompileIdx) { + int LastPrecompileIdx = 9; + if (Revision >= EVMC_CANCUN) { + LastPrecompileIdx = 10; + } + if (Revision >= EVMC_PRAGUE) { + LastPrecompileIdx = 16; + } + for (int PrecompileIdx = 1; PrecompileIdx <= LastPrecompileIdx; + ++PrecompileIdx) { evmc::address PrecompileAddr{}; - PrecompileAddr.bytes[19] = static_cast(PrecompileIdx); + PrecompileAddr.bytes[19] = PrecompileIdx; Host.access_account(PrecompileAddr); } }