From cd55f1f33fcb7a4850f758c86e63f149e92029a6 Mon Sep 17 00:00:00 2001 From: Outcry <843648230@qq.com> Date: Fri, 11 Sep 2026 09:56:08 +0000 Subject: [PATCH] fix(evm): exclude access status from EVM prestate Warm/cold storage access is per-transaction execution state, not world state. Stop serializing access_status and ignore it when loading legacy prestate files so the next transaction's first storage access pays cold-access gas. Closes #590 --- src/tests/evm_interp_tests.cpp | 104 ++++++++++++++++++++++++++++++++- src/utils/evm.cpp | 12 ++-- 2 files changed, 106 insertions(+), 10 deletions(-) diff --git a/src/tests/evm_interp_tests.cpp b/src/tests/evm_interp_tests.cpp index f2cc1819c..fe79da23c 100644 --- a/src/tests/evm_interp_tests.cpp +++ b/src/tests/evm_interp_tests.cpp @@ -1231,8 +1231,7 @@ TEST(EVMStateSaveLoad, LoadedStorageInitializesOriginalForNewTransaction) { "nonce": 0, "storage": { "0000000000000000000000000000000000000000000000000000000000000000": { - "value": "0000000000000000000000000000000000000000000000000000000000000005", - "access_status": 0 + "value": "0000000000000000000000000000000000000000000000000000000000000005" } } }, @@ -1308,6 +1307,107 @@ TEST(EVMStateSaveLoad, LoadedStorageInitializesOriginalForNewTransaction) { std::filesystem::remove(StateFilePath); } +// Regression test for https://github.com/DTVMStack/DTVM/issues/590. +// Warm/cold access is transaction execution state, not world state. A legacy +// prestate's access_status must not cold-skip the next transaction's first +// access, and freshly saved states must not carry the field. +TEST(EVMStateSaveLoad, AccessStatusIsNotPrestateState) { + const std::string StateFilePath = "/tmp/dtvm_issue590_state.json"; + const std::string SavedStateFilePath = "/tmp/dtvm_issue590_saved_state.json"; + const std::string ContractAddr = "00000000000000000000000000000000000000f1"; + const std::string SenderAddr = "a94f5374fce5edbc8e2a8697c15331677e6ebf0b"; + const std::vector Bytecode = {0x60, 0x03, 0x60, 0x00, 0x55, 0x00}; + + { + std::ofstream StateFile(StateFilePath); + ASSERT_TRUE(StateFile) << "Failed to create issue #590 state file"; + StateFile << R"({ + "accounts": { + ")" << ContractAddr + << R"(": { + "balance": "0000000000000000000000000000000000000000000000000000000000000000", + "code": "0x600360005500", + "nonce": 0, + "storage": { + "0000000000000000000000000000000000000000000000000000000000000000": { + "value": "0000000000000000000000000000000000000000000000000000000000000000", + "access_status": 1 + } + } + }, + ")" << SenderAddr + << R"(": { + "balance": "0000000000000000000000000000000000000000000000000de0b6b3a7640000", + "code": "0x", + "nonce": 0, + "storage": {} + } + }, + "tx_context": { + "gas_price": "0000000000000000000000000000000000000000000000000000000000000010", + "block_number": 1, + "block_timestamp": 1000, + "block_coinbase": "b94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "block_prev_randao": "0000000000000000000000000000000000000000000000000000000000000020", + "block_gas_limit": 10000000, + "block_base_fee": "0000000000000000000000000000000000000000000000000000000000000010", + "tx_origin": ")" + << SenderAddr << R"(" + } + })"; + } + + auto HostPtr = std::make_unique(); + ASSERT_TRUE(zen::utils::loadState(*HostPtr, StateFilePath)); + + const evmc::address ContractAddress = zen::utils::parseAddress(ContractAddr); + const evmc::address SenderAddress = zen::utils::parseAddress(SenderAddr); + const evmc::bytes32 StorageKey{}; + const auto &Slot = HostPtr->accounts[ContractAddress].storage.at(StorageKey); + EXPECT_EQ(Slot.access_status, EVMC_ACCESS_COLD) + << "Prestate access_status must not warm storage for a new transaction"; + + RuntimeConfig Config; + Config.Mode = common::RunMode::InterpMode; + auto RT = Runtime::newEVMRuntime(Config, HostPtr.get()); + ASSERT_TRUE(RT); + HostPtr->setRuntime(RT.get()); + + zen::evm::ZenMockedEVMHost::TransactionExecutionConfig ExecConfig; + ExecConfig.ModuleName = "issue590"; + ExecConfig.Bytecode = Bytecode.data(); + ExecConfig.BytecodeSize = Bytecode.size(); + ExecConfig.Revision = EVMC_CANCUN; + ExecConfig.GasLimit = 100000; + evmc_message Msg{}; + Msg.kind = EVMC_CALL; + Msg.gas = 100000; + Msg.sender = SenderAddress; + Msg.recipient = ContractAddress; + Msg.code_address = ContractAddress; + ExecConfig.Message = Msg; + + auto Result = HostPtr->executeTransaction(ExecConfig); + ASSERT_TRUE(Result.Success) << Result.ErrorMessage; + EXPECT_EQ(Result.Status, EVMC_SUCCESS); + EXPECT_EQ(Result.GasUsed, 22106u) + << "The first SSTORE must pay cold access plus set cost"; + + ASSERT_TRUE(zen::utils::saveState(*HostPtr, SavedStateFilePath)); + std::ifstream SavedStateFile(SavedStateFilePath); + std::string SavedState((std::istreambuf_iterator(SavedStateFile)), + std::istreambuf_iterator()); + ASSERT_TRUE(SavedStateFile) << "Failed to read saved state"; + EXPECT_EQ(SavedState.find("access_status"), std::string::npos) + << "Saved state must not contain transaction access status"; + auto SavedHost = std::make_unique(); + EXPECT_TRUE(zen::utils::loadState(*SavedHost, SavedStateFilePath)) + << "Saved state must remain valid JSON"; + + std::filesystem::remove(StateFilePath); + std::filesystem::remove(SavedStateFilePath); +} + namespace { // Helper that builds and returns the final sender balance for a transaction diff --git a/src/utils/evm.cpp b/src/utils/evm.cpp index 312ccac7e..0dc267ec5 100644 --- a/src/utils/evm.cpp +++ b/src/utils/evm.cpp @@ -257,8 +257,7 @@ bool saveState(const evmc::MockedHost &Host, const std::string &FilePath) { File << " \"value\": "; writeJsonString(File, toHex(Value.current.bytes, sizeof(Value.current.bytes))); - File << ",\n"; - File << " \"access_status\": " << Value.access_status << "\n"; + File << "\n"; File << " }"; } if (!FirstStorage) @@ -386,7 +385,9 @@ bool loadState(evmc::MockedHost &Host, const std::string &FilePath) { evmc::StorageValue StorageVal; if (StorageValue.IsObject()) { - // New format with value and access_status + // 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 = @@ -402,11 +403,6 @@ bool loadState(evmc::MockedHost &Host, const std::string &FilePath) { // current is also the transaction's original value. StorageVal.original = StorageVal.current; } - if (StorageValue.HasMember("access_status") && - StorageValue["access_status"].IsUint()) { - StorageVal.access_status = static_cast( - StorageValue["access_status"].GetUint()); - } } else if (StorageValue.IsString()) { // Old format with just value StorageVal.current =