From 20304ddf95ae7209afe20dfe07a0f7bc79f6d282 Mon Sep 17 00:00:00 2001 From: Outcry <843648230@qq.com> Date: Wed, 9 Sep 2026 12:13:51 +0000 Subject: [PATCH 1/3] fix(evm): apply capped gas refund and intrinsic gas once in CLI settlement Settle the execution refund counter with the EIP-3529 revision-specific cap and count intrinsic gas exactly once when charging the sender. This aligns CLI gas settlement with EVM spec behavior and adds regression tests for refund limits across revisions. Closes #588 --- src/cli/dtvm.cpp | 69 +---- src/tests/evm_interp_tests.cpp | 504 +++++++++++++++++++++++++++++++++ src/tests/evm_test_host.hpp | 49 +--- src/utils/evm.cpp | 101 +++++++ src/utils/evm.h | 42 +++ 5 files changed, 677 insertions(+), 88 deletions(-) diff --git a/src/cli/dtvm.cpp b/src/cli/dtvm.cpp index 2c8fa323a..b9dfd87c1 100644 --- a/src/cli/dtvm.cpp +++ b/src/cli/dtvm.cpp @@ -443,66 +443,27 @@ int main(int argc, char *argv[]) { } } - // Deduct intrinsic gas before EVM execution. - const int64_t IntrinsicGas = zen::utils::computeIntrinsicGas( - EvmRevision, MsgKind, Msg.input_data, Msg.input_size); - if (Msg.gas < IntrinsicGas) { - ZEN_LOG_ERROR("intrinsic gas (%ld) exceeds gas limit (%ld)", - (long)IntrinsicGas, (long)Msg.gas); - return exitMain(EVMC_OUT_OF_GAS, RT.get()); - } - Msg.gas -= IntrinsicGas; - - // EIP-2929/EIP-3651: Pre-warm transaction-level accounts. - zen::utils::prewarmTransactionAccounts( - MockedHost, EvmRevision, Msg.sender, Msg.recipient, - MockedHost.tx_context.block_coinbase); - - // Deduct upfront gas cost from sender's balance before execution. - // Per EVM spec (Yellow Paper §6), the sender's balance is reduced by - // effective_gas_price * gas_limit at the start of transaction execution. - intx::uint256 GasPrice = - intx::be::load(MockedHost.tx_context.tx_gas_price); - intx::uint256 BaseFee = - intx::be::load(MockedHost.tx_context.block_base_fee); - intx::uint256 EffectiveGasPrice = GasPrice > BaseFee ? GasPrice : BaseFee; - intx::uint256 UpfrontGasCost = intx::uint256(GasLimit) * EffectiveGasPrice; - auto &SenderAccount = MockedHost.accounts[Msg.sender]; - intx::uint256 SenderBalance = - intx::be::load(SenderAccount.balance); - if (SenderBalance < UpfrontGasCost) { + // Apply upfront gas: deduct intrinsic gas, pre-warm accounts, and deduct + // upfront balance. Failure exits with the appropriate EVM status code. + const auto UpfrontResult = + zen::utils::applyEvmUpfrontGas(MockedHost, Msg, GasLimit, EvmRevision); + if (UpfrontResult != zen::utils::EvmUpfrontGasResult::Success) { + if (UpfrontResult == + zen::utils::EvmUpfrontGasResult::IntrinsicGasExceedsLimit) { + const int64_t IntrinsicGas = zen::utils::computeIntrinsicGas( + EvmRevision, MsgKind, Msg.input_data, Msg.input_size); + ZEN_LOG_ERROR("intrinsic gas (%ld) exceeds gas limit (%ld)", + (long)IntrinsicGas, (long)(GasLimit)); + return exitMain(EVMC_OUT_OF_GAS, RT.get()); + } ZEN_LOG_ERROR("sender balance insufficient for upfront gas cost"); return exitMain(EVMC_INSUFFICIENT_BALANCE, RT.get()); } - SenderBalance -= UpfrontGasCost; - SenderAccount.balance = intx::be::store(SenderBalance); RT->callEVMMain(*Inst, Msg, ExeResult); - // Settle gas charges after execution: refund unused gas to sender, - // pay priority fee to coinbase. - uint64_t GasUsed = static_cast( - GasLimit - (ExeResult.gas_left > 0 ? ExeResult.gas_left : 0)); - GasUsed += static_cast(IntrinsicGas); - intx::uint256 PriorityFee = - GasPrice > BaseFee ? GasPrice - BaseFee : intx::uint256{0}; - // Refund unused gas: (GasLimit - GasUsed) * EffectiveGasPrice - if (GasLimit > GasUsed) { - intx::uint256 Refund = - intx::uint256(GasLimit - GasUsed) * EffectiveGasPrice; - SenderBalance += Refund; - SenderAccount.balance = intx::be::store(SenderBalance); - } - // Pay priority fee to coinbase: GasUsed * PriorityFee - if (PriorityFee != intx::uint256{0}) { - auto &CoinbaseAccount = - MockedHost.accounts[MockedHost.tx_context.block_coinbase]; - intx::uint256 CoinbaseBalance = - intx::be::load(CoinbaseAccount.balance); - CoinbaseBalance += intx::uint256(GasUsed) * PriorityFee; - CoinbaseAccount.balance = intx::be::store(CoinbaseBalance); - } - + zen::utils::applyEvmPostExecutionSettlement(MockedHost, Msg, GasLimit, + ExeResult, EvmRevision); if (EVMC_CREATE == MsgKind && ExeResult.status_code == EVMC_SUCCESS) { evmc::address DeployerAddr = zen::utils::parseAddress(SenderAddress); auto &DeployerAccount = MockedHost.accounts[DeployerAddr]; diff --git a/src/tests/evm_interp_tests.cpp b/src/tests/evm_interp_tests.cpp index 3c280ef6c..9ec4dd723 100644 --- a/src/tests/evm_interp_tests.cpp +++ b/src/tests/evm_interp_tests.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -1208,6 +1209,509 @@ TEST(EVMStateSaveLoad, MissingChainIdAndBlobBaseFee) { std::filesystem::remove(StateFilePath); } +namespace { + +// Helper that builds and returns the final sender balance for a transaction +// against a contract with the given Bytecode. The caller is expected to set +// initial balances, storage, and tx_context before calling this. +struct SettlementResult { + intx::uint256 InitialSenderBalance; + intx::uint256 FinalSenderBalance; + uint64_t GasUsed = 0; + uint64_t GasRefund = 0; + uint64_t GasCharged = 0; + bool Success = false; +}; + +template +SettlementResult runSettlementTransaction(const evmc::address &ContractAddr, + const evmc::address &SenderAddr, + const std::vector &Bytecode, + evmc_revision Revision, + uint64_t GasLimit, + PrepareHostT &&PrepareHost) { + SettlementResult Result; + + RuntimeConfig Config; + Config.Mode = common::RunMode::InterpMode; + + auto HostPtr = std::make_unique(); + std::forward(PrepareHost)(*HostPtr); + + auto &SenderAcc = HostPtr->accounts[SenderAddr]; + Result.InitialSenderBalance = + intx::be::load(SenderAcc.balance); + + auto RT = Runtime::newEVMRuntime(Config, HostPtr.get()); + if (!RT) { + return Result; + } + HostPtr->setRuntime(RT.get()); + + zen::evm::ZenMockedEVMHost::TransactionExecutionConfig ExecConfig; + ExecConfig.ModuleName = "settlement"; + ExecConfig.Bytecode = Bytecode.data(); + ExecConfig.BytecodeSize = Bytecode.size(); + ExecConfig.Revision = Revision; + ExecConfig.GasLimit = GasLimit; + + evmc_message Msg{}; + Msg.kind = EVMC_CALL; + Msg.gas = static_cast(GasLimit); + Msg.sender = SenderAddr; + Msg.recipient = ContractAddr; + Msg.code_address = ContractAddr; + ExecConfig.Message = Msg; + + auto ExecResult = HostPtr->executeTransaction(ExecConfig); + Result.Success = ExecResult.Success; + Result.GasUsed = ExecResult.GasUsed; + Result.GasRefund = ExecResult.GasRefund; + Result.GasCharged = ExecResult.GasCharged; + Result.FinalSenderBalance = + intx::be::load(HostPtr->accounts[SenderAddr].balance); + return Result; +} + +// Helper that exercises the same settlement code used by the dtvm CLI: +// applyEvmUpfrontGas, callEVMMain, applyEvmPostExecutionSettlement. +template +SettlementResult runDtvmCliSettlementTransaction( + const evmc::address &ContractAddr, const evmc::address &SenderAddr, + const std::vector &Bytecode, evmc_revision Revision, + uint64_t GasLimit, PrepareHostT &&PrepareHost) { + SettlementResult Result; + + RuntimeConfig Config; + Config.Mode = common::RunMode::InterpMode; + + auto HostPtr = std::make_unique(); + std::forward(PrepareHost)(*HostPtr); + + auto &SenderAcc = HostPtr->accounts[SenderAddr]; + Result.InitialSenderBalance = + intx::be::load(SenderAcc.balance); + + auto RT = Runtime::newEVMRuntime(Config, HostPtr.get()); + if (!RT) { + return Result; + } + HostPtr->setRuntime(RT.get()); + + auto ModRet = + RT->loadEVMModule("cli_settlement", Bytecode.data(), Bytecode.size()); + if (!ModRet) { + return Result; + } + EVMModule *Mod = *ModRet; + + Isolation *Iso = RT->createManagedIsolation(); + if (!Iso) { + return Result; + } + + evmc_message Msg{}; + Msg.kind = EVMC_CALL; + Msg.gas = static_cast(GasLimit); + Msg.sender = SenderAddr; + Msg.recipient = ContractAddr; + Msg.code_address = ContractAddr; + + const auto UpfrontResult = + zen::utils::applyEvmUpfrontGas(*HostPtr, Msg, GasLimit, Revision); + if (UpfrontResult != zen::utils::EvmUpfrontGasResult::Success) { + return Result; + } + + auto InstRet = Iso->createEVMInstance(*Mod, static_cast(Msg.gas)); + if (!InstRet) { + return Result; + } + EVMInstance *Inst = *InstRet; + Inst->setRevision(Revision); + + evmc::Result ExecResult{}; + RT->callEVMMain(*Inst, Msg, ExecResult); + + zen::utils::applyEvmPostExecutionSettlement(*HostPtr, Msg, GasLimit, + ExecResult, Revision); + + Result.Success = ExecResult.status_code == EVMC_SUCCESS; + Result.GasUsed = static_cast( + GasLimit - std::max(0, ExecResult.gas_left)); + const uint64_t RefundLimit = + zen::utils::computeRefundCap(Revision, Result.GasUsed); + Result.GasRefund = std::min( + static_cast(std::max(0, ExecResult.gas_refund)), + RefundLimit); + Result.GasCharged = + Result.GasUsed > Result.GasRefund ? Result.GasUsed - Result.GasRefund : 0; + Result.FinalSenderBalance = + intx::be::load(HostPtr->accounts[SenderAddr].balance); + return Result; +} + +} // namespace + +// Regression tests for https://github.com/DTVMStack/DTVM/issues/588. +// Unlike the CLI helper below, executeTransaction models the state-test path +// where intrinsic gas has already been consumed before this host method. +// These tests therefore validate the host's refund-cap behavior on execution +// gas only; the CLI-specific tests cover intrinsic gas and total settlement. +TEST(EVMRegressionTest, Issue588_CappedRefundDoesNotReduceChargesOnCancun) { + // SSTORE set slot 0 to 5, then clear it back to 0. Cancun refunds are capped + // at 1/5 of GasUsed. + const std::string BytecodeHex = "6005600055600060005500"; + auto BytecodeBuf = zen::utils::fromHex(BytecodeHex); + ASSERT_TRUE(BytecodeBuf) << "Failed to parse issue #588 bytecode"; + + const evmc::address ContractAddr = evmc::literals::operator""_address( + "00000000000000000000000000000000000000f1"); + const evmc::address SenderAddr = evmc::literals::operator""_address( + "a94f5374fce5edbc8e2a8697c15331677e6ebf0b"); + + constexpr uint64_t GasLimit = 8000000; + constexpr uint64_t SenderInitialBalanceValue = 0xffffffffffff; + constexpr intx::uint256 GasPrice = intx::uint256(16); + + auto Result = runSettlementTransaction( + ContractAddr, SenderAddr, *BytecodeBuf, EVMC_CANCUN, GasLimit, + [&](zen::evm::ZenMockedEVMHost &Host) { + evmc::MockedAccount ContractAccount; + ContractAccount.code = + evmc::bytes(BytecodeBuf->data(), BytecodeBuf->size()); + Host.accounts[ContractAddr] = ContractAccount; + + evmc::MockedAccount SenderAccount; + SenderAccount.set_balance(SenderInitialBalanceValue); + Host.accounts[SenderAddr] = SenderAccount; + + evmc_tx_context TxCtx{}; + TxCtx.tx_origin = SenderAddr; + TxCtx.tx_gas_price = intx::be::store(GasPrice); + TxCtx.block_base_fee = intx::be::store(GasPrice); + Host.tx_context = TxCtx; + }); + + ASSERT_TRUE(Result.Success) << "Issue #588 transaction should succeed"; + + // Expected host-path gas accounting: + // ExecutionGasUsed = 22212; intrinsic is consumed before executeTransaction + // refund counter = 19900, capped to 22212/5 = 4442 + // GasCharged = 22212 - 4442 = 17770 + // Net sender cost = GasCharged * gas_price. + EXPECT_EQ(Result.GasUsed, 22212u) + << "ExecutionGasUsed should exclude intrinsic gas on the host path"; + EXPECT_EQ(Result.GasCharged, 17770u) + << "Refund cap should produce the expected Cancun gas charge"; + + intx::uint256 ExpectedCost = GasPrice * Result.GasCharged; + EXPECT_EQ(Result.InitialSenderBalance - Result.FinalSenderBalance, + ExpectedCost) + << "Sender should be charged GasCharged * gas_price on issue #588"; +} + +// On the host path, ensure that a REVERT discards the refund accumulator so +// it does not reduce the sender's net gas charge. +TEST(EVMRegressionTest, Issue588_RevertResetsRefundAccumulator) { + // SSTORE slot 0 to 5 (which would create a refund if the slot is later + // cleared), then REVERT. The refund counter is reset on revert, so the gas + // refunded to the sender must be (GasLimit - GasUsed) only, not more. + const std::string BytecodeHex = "600560005560006000fd"; + auto BytecodeBuf = zen::utils::fromHex(BytecodeHex); + ASSERT_TRUE(BytecodeBuf) << "Failed to parse revert bytecode"; + + const evmc::address ContractAddr = evmc::literals::operator""_address( + "00000000000000000000000000000000000000f1"); + const evmc::address SenderAddr = evmc::literals::operator""_address( + "a94f5374fce5edbc8e2a8697c15331677e6ebf0b"); + + constexpr uint64_t GasLimit = 8000000; + constexpr uint64_t SenderInitialBalanceValue = 0xffffffffffff; + constexpr intx::uint256 GasPrice = intx::uint256(16); + + auto Result = runSettlementTransaction( + ContractAddr, SenderAddr, *BytecodeBuf, EVMC_CANCUN, GasLimit, + [&](zen::evm::ZenMockedEVMHost &Host) { + evmc::MockedAccount ContractAccount; + ContractAccount.code = + evmc::bytes(BytecodeBuf->data(), BytecodeBuf->size()); + // Pre-existing non-zero value so the SSTORE costs the reset price and + // would generate a refund if the slot were later cleared. + evmc::bytes32 Key{}; + evmc::bytes32 Val{}; + Val.bytes[31] = 1; + ContractAccount.storage[Key].current = Val; + ContractAccount.storage[Key].original = Val; + Host.accounts[ContractAddr] = ContractAccount; + + evmc::MockedAccount SenderAccount; + SenderAccount.set_balance(SenderInitialBalanceValue); + Host.accounts[SenderAddr] = SenderAccount; + + evmc_tx_context TxCtx{}; + TxCtx.tx_origin = SenderAddr; + TxCtx.tx_gas_price = intx::be::store(GasPrice); + TxCtx.block_base_fee = intx::be::store(GasPrice); + Host.tx_context = TxCtx; + }); + + ASSERT_TRUE(Result.Success) << "Revert transaction should succeed"; + EXPECT_EQ(Result.GasCharged, Result.GasUsed) + << "REVERT must reset refund counter so no refund is applied"; + + intx::uint256 ExpectedCost = GasPrice * Result.GasCharged; + EXPECT_EQ(Result.InitialSenderBalance - Result.FinalSenderBalance, + ExpectedCost) + << "Sender cost must equal GasUsed * gas_price after revert"; +} + +// Regression test for https://github.com/DTVMStack/DTVM/issues/588. +// On the host path, pre-London forks (Byzantium) cap refunds at +// ExecutionGasUsed / 2 instead of ExecutionGasUsed / 5 (EIP-3529). The same +// SSTORE bytecode that yields a large raw refund produces a different cap and +// therefore different GasCharged. +TEST(EVMRegressionTest, Issue588_HalfRefundCapOnByzantium) { + // Byzantium is pre-London, pre-Berlin. Storage costs use the legacy + // schedule: Clear refund = 15000. No cold access surcharge. + // SSTORE set slot 0 → 5 (ADDED), then clear slot 0 → 0 (ADDED_DELETED, + // which maps to DELETED in the legacy table → ReSet=5000, Clear=15000). + const std::string BytecodeHex = "6005600055600060005500"; + auto BytecodeBuf = zen::utils::fromHex(BytecodeHex); + ASSERT_TRUE(BytecodeBuf) << "Failed to parse issue #588 pre-London bytecode"; + + const evmc::address ContractAddr = evmc::literals::operator""_address( + "00000000000000000000000000000000000000f1"); + const evmc::address SenderAddr = evmc::literals::operator""_address( + "a94f5374fce5edbc8e2a8697c15331677e6ebf0b"); + + constexpr uint64_t GasLimit = 8000000; + constexpr uint64_t SenderInitialBalanceValue = 0xffffffffffff; + constexpr intx::uint256 GasPrice = intx::uint256(16); + + auto Result = runSettlementTransaction( + ContractAddr, SenderAddr, *BytecodeBuf, EVMC_BYZANTIUM, GasLimit, + [&](zen::evm::ZenMockedEVMHost &Host) { + evmc::MockedAccount ContractAccount; + ContractAccount.code = + evmc::bytes(BytecodeBuf->data(), BytecodeBuf->size()); + Host.accounts[ContractAddr] = ContractAccount; + + evmc::MockedAccount SenderAccount; + SenderAccount.set_balance(SenderInitialBalanceValue); + Host.accounts[SenderAddr] = SenderAccount; + + evmc_tx_context TxCtx{}; + TxCtx.tx_origin = SenderAddr; + TxCtx.tx_gas_price = intx::be::store(GasPrice); + TxCtx.block_base_fee = intx::be::store(GasPrice); + Host.tx_context = TxCtx; + }); + + ASSERT_TRUE(Result.Success) + << "Issue #588 pre-London transaction should succeed"; + + // On Byzantium: + // 4×PUSH1(base=3) = 12 + // SSTORE#1 (cold skip, ADDED): cold=0, warm=20000 (Set), refund=0 → 20000 + // SSTORE#2 (warm, ADDED_DELETED→DELETED): cold=0, warm=5000 (ReSet), + // refund=15000 (Clear) → 5000 + // GasUsed = 12 + 20000 + 5000 = 25012 + // Raw refund = 15000 + // Refund cap (pre-London) = GasUsed / 2 = 12506 + // Applied refund = min(15000, 12506) = 12506 + // GasCharged = 25012 - 12506 = 12996 + EXPECT_EQ(Result.GasUsed, 25012u) + << "GasUsed should equal PUSH base + SSTORE costs on Byzantium"; + + uint64_t HalfCap = Result.GasUsed / 2; // = 12506 + EXPECT_EQ(Result.GasRefund, HalfCap) + << "Pre-London refund must be capped at GasUsed/2, not GasUsed/5"; + + // Raw refund (15000) must exceed the half cap, confirming the cap is active. + EXPECT_LT(Result.GasRefund, 15000u) + << "Raw EIP-2200 Clear refund (15000) should be capped by /2 rule"; + + EXPECT_EQ(Result.GasCharged, Result.GasUsed - HalfCap) + << "GasCharged = GasUsed - GasUsed/2 after refund cap"; + + intx::uint256 ExpectedCost = GasPrice * Result.GasCharged; + EXPECT_EQ(Result.InitialSenderBalance - Result.FinalSenderBalance, + ExpectedCost) + << "Sender should be charged GasCharged * gas_price on issue #588"; +} + +// Regression test for https://github.com/DTVMStack/DTVM/issues/588 +// SELFDESTRUCT on a pre-London revision adds EXTRA_REFUND_BEFORE_LONDON (24000) +// to the refund accumulator. The /2 refund cap must still apply. +TEST(EVMRegressionTest, Issue588_SELFDESTRUCTPreLondonRefundCapped) { + // Bytecode: PUSH1 0x00 SELFDESTRUCT — destroys contract, transfers balance + // to address(0). On pre-London this adds 24000 refund, capped at GasUsed/2. + const std::string BytecodeHex = "6000FF"; + auto BytecodeBuf = zen::utils::fromHex(BytecodeHex); + ASSERT_TRUE(BytecodeBuf) << "Failed to parse SELFDESTRUCT bytecode hex"; + + const evmc::address ContractAddr = evmc::literals::operator""_address( + "00000000000000000000000000000000000000f1"); + const evmc::address SenderAddr = evmc::literals::operator""_address( + "a94f5374fce5edbc8e2a8697c15331677e6ebf0b"); + + constexpr uint64_t GasLimit = 8000000; + constexpr uint64_t SenderInitialBalanceValue = 0xffffffffffff; + constexpr intx::uint256 GasPrice = intx::uint256(16); + + auto Result = runSettlementTransaction( + ContractAddr, SenderAddr, *BytecodeBuf, EVMC_BYZANTIUM, GasLimit, + [&](zen::evm::ZenMockedEVMHost &Host) { + evmc::MockedAccount ContractAccount; + ContractAccount.code = + evmc::bytes(BytecodeBuf->data(), BytecodeBuf->size()); + Host.accounts[ContractAddr] = ContractAccount; + + evmc::MockedAccount SenderAccount; + SenderAccount.set_balance(SenderInitialBalanceValue); + Host.accounts[SenderAddr] = SenderAccount; + + evmc_tx_context TxCtx{}; + TxCtx.tx_origin = SenderAddr; + TxCtx.tx_gas_price = intx::be::store(GasPrice); + TxCtx.block_base_fee = intx::be::store(GasPrice); + Host.tx_context = TxCtx; + }); + + ASSERT_TRUE(Result.Success) + << "SELFDESTRUCT must succeed on Byzantium without cold-access costs"; + + // On Byzantium (pre-Berlin): + // PUSH1(base=3) + SELFDESTRUCT(base=5000) = 5003 + // No cold access surcharge, no account creation cost (recipient balance=0) + // EXTRA_REFUND_BEFORE_LONDON = 24000 added to refund counter + // Refund cap = GasUsed / 2 = 5003 / 2 = 2501 + // Applied refund = min(24000, 2501) = 2501 + // GasCharged = 5003 - 2501 = 2502 + constexpr uint64_t SelfDestructGas = 3 + 5000; // PUSH1 + SELFDESTRUCT + EXPECT_EQ(Result.GasUsed, SelfDestructGas) + << "GasUsed must be PUSH1 + SELFDESTRUCT base cost on Byzantium"; + + uint64_t HalfCap = Result.GasUsed / 2; // = 2501 + EXPECT_EQ(Result.GasRefund, HalfCap) + << "Pre-London SELFDESTRUCT refund must be capped at GasUsed/2"; + EXPECT_LT(Result.GasRefund, zen::evm::EXTRA_REFUND_BEFORE_LONDON) + << "Raw EXTRA_REFUND_BEFORE_LONDON (24000) must be capped"; + + EXPECT_EQ(Result.GasCharged, SelfDestructGas - HalfCap) + << "GasCharged must be GasUsed minus the /2 cap after SELFDESTRUCT"; + + intx::uint256 ExpectedCost = GasPrice * Result.GasCharged; + EXPECT_EQ(Result.InitialSenderBalance - Result.FinalSenderBalance, + ExpectedCost) + << "Sender must be charged GasCharged * gas_price after SELFDESTRUCT"; +} + +// Regression test for https://github.com/DTVMStack/DTVM/issues/588 +// Exercises the actual dtvm.cpp CLI settlement path (not the test-host helper) +// to ensure intrinsic gas, refund cap, and balance update stay aligned. +TEST(EVMRegressionTest, Issue588_CliSettlement_CappedRefundOnCancun) { + const std::string BytecodeHex = "6005600055600060005500"; + auto BytecodeBuf = zen::utils::fromHex(BytecodeHex); + ASSERT_TRUE(BytecodeBuf) + << "Failed to parse issue #588 CLI settlement bytecode"; + + const evmc::address ContractAddr = evmc::literals::operator""_address( + "00000000000000000000000000000000000000f1"); + const evmc::address SenderAddr = evmc::literals::operator""_address( + "a94f5374fce5edbc8e2a8697c15331677e6ebf0b"); + + constexpr uint64_t GasLimit = 8000000; + constexpr uint64_t SenderInitialBalanceValue = 0xffffffffffff; + constexpr intx::uint256 GasPrice = intx::uint256(16); + + auto Result = runDtvmCliSettlementTransaction( + ContractAddr, SenderAddr, *BytecodeBuf, EVMC_CANCUN, GasLimit, + [&](zen::evm::ZenMockedEVMHost &Host) { + evmc::MockedAccount ContractAccount; + ContractAccount.code = + evmc::bytes(BytecodeBuf->data(), BytecodeBuf->size()); + Host.accounts[ContractAddr] = ContractAccount; + + evmc::MockedAccount SenderAccount; + SenderAccount.set_balance(SenderInitialBalanceValue); + Host.accounts[SenderAddr] = SenderAccount; + + evmc_tx_context TxCtx{}; + TxCtx.tx_origin = SenderAddr; + TxCtx.tx_gas_price = intx::be::store(GasPrice); + TxCtx.block_base_fee = intx::be::store(GasPrice); + Host.tx_context = TxCtx; + }); + + ASSERT_TRUE(Result.Success) << "CLI settlement transaction should succeed"; + + EXPECT_EQ(Result.GasUsed, 43212u) + << "CLI path should be the actual SSTORE execution cost"; + EXPECT_EQ(Result.GasCharged, 34570u) + << "CLI path refund cap should produce the actual Cancun gas charge"; + + intx::uint256 ExpectedCost = GasPrice * Result.GasCharged; + EXPECT_EQ(Result.InitialSenderBalance - Result.FinalSenderBalance, + ExpectedCost) + << "CLI path sender should be charged GasCharged * gas_price"; +} + +// Regression test for https://github.com/DTVMStack/DTVM/issues/588 +// Same as the Byzantium SSTORE test above, but driven through the CLI path. +TEST(EVMRegressionTest, Issue588_CliSettlement_HalfRefundCapOnByzantium) { + const std::string BytecodeHex = "6005600055600060005500"; + auto BytecodeBuf = zen::utils::fromHex(BytecodeHex); + ASSERT_TRUE(BytecodeBuf) + << "Failed to parse issue #588 pre-London CLI settlement bytecode"; + + const evmc::address ContractAddr = evmc::literals::operator""_address( + "00000000000000000000000000000000000000f1"); + const evmc::address SenderAddr = evmc::literals::operator""_address( + "a94f5374fce5edbc8e2a8697c15331677e6ebf0b"); + + constexpr uint64_t GasLimit = 8000000; + constexpr uint64_t SenderInitialBalanceValue = 0xffffffffffff; + constexpr intx::uint256 GasPrice = intx::uint256(16); + + auto Result = runDtvmCliSettlementTransaction( + ContractAddr, SenderAddr, *BytecodeBuf, EVMC_BYZANTIUM, GasLimit, + [&](zen::evm::ZenMockedEVMHost &Host) { + evmc::MockedAccount ContractAccount; + ContractAccount.code = + evmc::bytes(BytecodeBuf->data(), BytecodeBuf->size()); + Host.accounts[ContractAddr] = ContractAccount; + + evmc::MockedAccount SenderAccount; + SenderAccount.set_balance(SenderInitialBalanceValue); + Host.accounts[SenderAddr] = SenderAccount; + + evmc_tx_context TxCtx{}; + TxCtx.tx_origin = SenderAddr; + TxCtx.tx_gas_price = intx::be::store(GasPrice); + TxCtx.block_base_fee = intx::be::store(GasPrice); + Host.tx_context = TxCtx; + }); + + ASSERT_TRUE(Result.Success) + << "CLI settlement pre-London transaction should succeed"; + + EXPECT_EQ(Result.GasUsed, 46012u) + << "CLI path GasUsed should equal PUSH base + SSTORE costs on Byzantium"; + + EXPECT_EQ(Result.GasRefund, 15000u) + << "CLI path pre-London refund must reserve the original refund counter"; + + EXPECT_EQ(Result.GasCharged, 31012u) + << "CLI path GasCharged should be GasUsed minus the refund"; + + intx::uint256 ExpectedCost = GasPrice * Result.GasCharged; + EXPECT_EQ(Result.InitialSenderBalance - Result.FinalSenderBalance, + ExpectedCost) + << "CLI path sender should be charged GasCharged * gas_price"; +} + // Regression test for https://github.com/DTVMStack/DTVM/issues/545 // BALANCE should reflect the upfront gas deduction (gas_price * gas_limit) // from the sender's balance, per EVM spec (Yellow Paper §6). diff --git a/src/tests/evm_test_host.hpp b/src/tests/evm_test_host.hpp index 30234b6d2..4b688e888 100644 --- a/src/tests/evm_test_host.hpp +++ b/src/tests/evm_test_host.hpp @@ -309,8 +309,8 @@ class ZenMockedEVMHost : public evmc::MockedHost { Result.GasUsed += Config.IntrinsicGas; uint64_t GasRefund = static_cast( std::max(0, PrecompileResult.gas_refund)); - uint64_t RefundLimit = - refundLimitForRevision(ActiveRevision, Result.GasUsed); + const uint64_t RefundLimit = + zen::utils::computeRefundCap(ActiveRevision, Result.GasUsed); Result.GasRefund = std::min(GasRefund, RefundLimit); Result.GasCharged = Result.GasUsed > Result.GasRefund ? Result.GasUsed - Result.GasRefund @@ -347,8 +347,8 @@ class ZenMockedEVMHost : public evmc::MockedHost { Result.GasUsed += Config.IntrinsicGas; uint64_t GasRefund = static_cast(std::max(0, CreateResult.gas_refund)); - uint64_t RefundLimit = - refundLimitForRevision(ActiveRevision, Result.GasUsed); + const uint64_t RefundLimit = + zen::utils::computeRefundCap(ActiveRevision, Result.GasUsed); Result.GasRefund = std::min(GasRefund, RefundLimit); Result.GasCharged = Result.GasUsed > Result.GasRefund ? Result.GasUsed - Result.GasRefund @@ -451,8 +451,8 @@ class ZenMockedEVMHost : public evmc::MockedHost { Result.GasUsed += Config.IntrinsicGas; uint64_t GasRefund = static_cast(std::max(0, Inst->getGasRefund())); - uint64_t RefundLimit = - refundLimitForRevision(ActiveRevision, Result.GasUsed); + const uint64_t RefundLimit = + zen::utils::computeRefundCap(ActiveRevision, Result.GasUsed); Result.GasRefund = std::min(GasRefund, RefundLimit); Result.GasCharged = Result.GasUsed > Result.GasRefund ? Result.GasUsed - Result.GasRefund @@ -1097,20 +1097,10 @@ class ZenMockedEVMHost : public evmc::MockedHost { const TransactionExecutionConfig &Config, const evmc_message &Msg, TransactionExecutionResult &Result) { - intx::uint256 GasPrice = toUint256BE(tx_context.tx_gas_price); - intx::uint256 BaseFee = toUint256BE(tx_context.block_base_fee); - intx::uint256 PriorityFee = - GasPrice > BaseFee ? GasPrice - BaseFee : intx::uint256{0}; - intx::uint256 EffectiveGasPrice = GasPrice; - - if (Config.MaxPriorityFeePerGas) { - intx::uint256 MaxPriority = toUint256BE(*Config.MaxPriorityFeePerGas); - intx::uint256 MaxFeeMinusBase = - GasPrice > BaseFee ? GasPrice - BaseFee : intx::uint256{0}; - PriorityFee = - MaxPriority < MaxFeeMinusBase ? MaxPriority : MaxFeeMinusBase; - EffectiveGasPrice = BaseFee + PriorityFee; - } + const auto Fees = zen::utils::computeEip1559Fees( + tx_context.tx_gas_price, tx_context.block_base_fee, + Config.MaxPriorityFeePerGas); + const intx::uint256 EffectiveGasPrice = Fees.EffectiveGasPrice; intx::uint256 UpfrontGasCost = intx::uint256(GasLimit) * EffectiveGasPrice; intx::uint256 BlobFee = 0; @@ -1144,20 +1134,11 @@ class ZenMockedEVMHost : public evmc::MockedHost { const TransactionExecutionConfig &Config, const evmc_message &Msg, TransactionExecutionResult &Result, bool FeesPrepaid) { - intx::uint256 GasPrice = toUint256BE(tx_context.tx_gas_price); - intx::uint256 BaseFee = toUint256BE(tx_context.block_base_fee); - intx::uint256 PriorityFee = - GasPrice > BaseFee ? GasPrice - BaseFee : intx::uint256{0}; - intx::uint256 EffectiveGasPrice = GasPrice; - - if (Config.MaxPriorityFeePerGas) { - intx::uint256 MaxPriority = toUint256BE(*Config.MaxPriorityFeePerGas); - intx::uint256 MaxFeeMinusBase = - GasPrice > BaseFee ? GasPrice - BaseFee : intx::uint256{0}; - PriorityFee = - MaxPriority < MaxFeeMinusBase ? MaxPriority : MaxFeeMinusBase; - EffectiveGasPrice = BaseFee + PriorityFee; - } + const auto Fees = zen::utils::computeEip1559Fees( + tx_context.tx_gas_price, tx_context.block_base_fee, + Config.MaxPriorityFeePerGas); + const intx::uint256 EffectiveGasPrice = Fees.EffectiveGasPrice; + const intx::uint256 PriorityFee = Fees.PriorityFee; intx::uint256 GasCharged256 = intx::uint256(GasCharged); intx::uint256 CoinbaseReward = GasCharged256 * PriorityFee; diff --git a/src/utils/evm.cpp b/src/utils/evm.cpp index 6a58daf55..d3e8aa1bb 100644 --- a/src/utils/evm.cpp +++ b/src/utils/evm.cpp @@ -7,6 +7,7 @@ #include "host/evm/crypto.h" #include "intx/intx.hpp" #include "utils/rlp_encoding.h" +#include #include #include #include @@ -574,4 +575,104 @@ void prewarmTransactionAccounts(evmc::MockedHost &Host, evmc_revision Revision, } } +uint64_t computeRefundCap(evmc_revision Revision, uint64_t GasUsed) { + // EIP-3529: London and later cap refunds at 1/5 of GasUsed; pre-London + // capped them at 1/2. + return Revision >= EVMC_LONDON ? GasUsed / 5 : GasUsed / 2; +} + +Eip1559FeeComponents +computeEip1559Fees(const evmc::uint256be &EffectiveOrMaxFeePerGas, + const evmc::uint256be &BaseFee, + const std::optional &MaxPriorityFee) { + intx::uint256 GasPriceN = + intx::be::load(EffectiveOrMaxFeePerGas); + intx::uint256 BaseFeeN = intx::be::load(BaseFee); + intx::uint256 PriorityFee = + GasPriceN > BaseFeeN ? GasPriceN - BaseFeeN : intx::uint256{0}; + intx::uint256 EffectiveGasPrice = GasPriceN; + + if (MaxPriorityFee) { + intx::uint256 MaxPriorityN = intx::be::load(*MaxPriorityFee); + intx::uint256 MaxFeeMinusBase = + GasPriceN > BaseFeeN ? GasPriceN - BaseFeeN : intx::uint256{0}; + PriorityFee = + MaxPriorityN < MaxFeeMinusBase ? MaxPriorityN : MaxFeeMinusBase; + EffectiveGasPrice = BaseFeeN + PriorityFee; + } + return {EffectiveGasPrice, PriorityFee}; +} + +EvmUpfrontGasResult applyEvmUpfrontGas(evmc::MockedHost &Host, + evmc_message &Msg, uint64_t GasLimit, + evmc_revision Revision) { + // Deduct intrinsic gas before EVM execution. + const int64_t IntrinsicGas = + computeIntrinsicGas(Revision, Msg.kind, Msg.input_data, Msg.input_size); + if (Msg.gas < IntrinsicGas) { + return EvmUpfrontGasResult::IntrinsicGasExceedsLimit; + } + Msg.gas -= IntrinsicGas; + + // EIP-2929/EIP-3651: Pre-warm transaction-level accounts. + zen::utils::prewarmTransactionAccounts(Host, Revision, Msg.sender, + Msg.recipient, + Host.tx_context.block_coinbase); + + // Deduct upfront gas cost from sender's balance before execution. + // Per EVM spec (Yellow Paper §6), the sender's balance is reduced by + // effective_gas_price * gas_limit at the start of transaction execution. + const auto Fees = computeEip1559Fees(Host.tx_context.tx_gas_price, + Host.tx_context.block_base_fee); + intx::uint256 UpfrontGasCost = + intx::uint256(GasLimit) * Fees.EffectiveGasPrice; + auto &SenderAccount = Host.accounts[Msg.sender]; + intx::uint256 SenderBalance = + intx::be::load(SenderAccount.balance); + if (SenderBalance < UpfrontGasCost) { + return EvmUpfrontGasResult::InsufficientBalance; + } + SenderBalance -= UpfrontGasCost; + SenderAccount.balance = intx::be::store(SenderBalance); + return EvmUpfrontGasResult::Success; +} + +void applyEvmPostExecutionSettlement(evmc::MockedHost &Host, + const evmc_message &Msg, uint64_t GasLimit, + const evmc::Result &Result, + evmc_revision Revision) { + const uint64_t TotalGasUsed = static_cast( + GasLimit - (Result.gas_left > 0 ? Result.gas_left : 0)); + // Intrinsic gas was already deducted from Msg.gas before execution, so it + // is part of TotalGasUsed above; do not add it again (double counting). + const uint64_t RawGasRefund = + static_cast(std::max(0, Result.gas_refund)); + const uint64_t AppliedRefund = + std::min(RawGasRefund, computeRefundCap(Revision, TotalGasUsed)); + const uint64_t GasCharged = + AppliedRefund < TotalGasUsed ? TotalGasUsed - AppliedRefund : 0; + + const auto Fees = computeEip1559Fees(Host.tx_context.tx_gas_price, + Host.tx_context.block_base_fee); + auto &SenderAccount = Host.accounts[Msg.sender]; + intx::uint256 SenderBalance = + intx::be::load(SenderAccount.balance); + + // Refund unused gas: (GasLimit - GasCharged) * EffectiveGasPrice + if (GasLimit > GasCharged) { + intx::uint256 Refund = + intx::uint256(GasLimit - GasCharged) * Fees.EffectiveGasPrice; + SenderBalance += Refund; + } + // Pay priority fee to coinbase: GasCharged * PriorityFee + if (Fees.PriorityFee != intx::uint256{0}) { + auto &CoinbaseAccount = Host.accounts[Host.tx_context.block_coinbase]; + intx::uint256 CoinbaseBalance = + intx::be::load(CoinbaseAccount.balance); + CoinbaseBalance += intx::uint256(GasCharged) * Fees.PriorityFee; + CoinbaseAccount.balance = intx::be::store(CoinbaseBalance); + } + SenderAccount.balance = intx::be::store(SenderBalance); +} + } // namespace zen::utils diff --git a/src/utils/evm.h b/src/utils/evm.h index 25b76145d..78664d4b5 100644 --- a/src/utils/evm.h +++ b/src/utils/evm.h @@ -6,6 +6,7 @@ #include "utils/others.h" #include #include +#include #include namespace zen::utils { @@ -50,6 +51,47 @@ void prewarmTransactionAccounts(evmc::MockedHost &Host, evmc_revision Revision, const evmc::address &Recipient, const evmc::address &Coinbase); +/// EIP-3529 refund cap: London and later cap refunds at GasUsed/5; +/// pre-London caps at GasUsed/2. +uint64_t computeRefundCap(evmc_revision Revision, uint64_t GasUsed); + +struct Eip1559FeeComponents { + intx::uint256 EffectiveGasPrice; + intx::uint256 PriorityFee; +}; + +/// Compute the effective gas price and priority fee. +/// When MaxPriorityFee is provided, EffectiveOrMaxFeePerGas is the EIP-1559 +/// maxFeePerGas, PriorityFee is min(maxPriorityFee, maxFeePerGas - baseFee), +/// and EffectiveGasPrice is baseFee + PriorityFee. +/// Otherwise EffectiveOrMaxFeePerGas is already the effective gas price for a +/// legacy transaction, PriorityFee is max(effectiveGasPrice - baseFee, 0), +/// and EffectiveGasPrice is effectiveGasPrice. +Eip1559FeeComponents computeEip1559Fees( + const evmc::uint256be &EffectiveOrMaxFeePerGas, + const evmc::uint256be &BaseFee, + const std::optional &MaxPriorityFee = std::nullopt); + +enum class EvmUpfrontGasResult { + Success, + IntrinsicGasExceedsLimit, + InsufficientBalance +}; + +/// Deduct intrinsic gas from Msg.gas and pre-warm transaction-level accounts. +/// Also deducts gas_limit * effective_gas_price from the sender's balance. +/// Call this before callEVMMain. +EvmUpfrontGasResult applyEvmUpfrontGas(evmc::MockedHost &Host, + evmc_message &Msg, uint64_t GasLimit, + evmc_revision Revision); + +/// Apply the dtvm-cli post-execution gas settlement: refund cap (EIP-3529), +/// refund unused gas to sender, and pay priority fee to coinbase. +void applyEvmPostExecutionSettlement(evmc::MockedHost &Host, + const evmc_message &Msg, uint64_t GasLimit, + const evmc::Result &Result, + evmc_revision Revision); + } // namespace zen::utils #endif // ZEN_UTILS_EVM_H From 0769ca1903d40b7a81d2f1c79b7180b24b237338 Mon Sep 17 00:00:00 2001 From: Outcry <843648230@qq.com> Date: Wed, 9 Sep 2026 13:27:44 +0000 Subject: [PATCH 2/3] fix(evm): align intrinsic gas setup for cli and jit Create the EVM instance after upfront gas deduction so interpreter and JIT start from the same Msg.gas value. Rename the fee helper for transaction generality, share settlement test setup, and document the utility contracts. Verified the issue #588 CLI repro and EVM interpreter regression tests. --- docs/modules/cli/spec.md | 2 + docs/modules/utils/spec.md | 4 + src/cli/dtvm.cpp | 24 ++-- src/tests/evm_interp_tests.cpp | 228 ++++++++++----------------------- src/tests/evm_test_host.hpp | 4 +- src/utils/evm.cpp | 16 +-- src/utils/evm.h | 6 +- 7 files changed, 101 insertions(+), 183 deletions(-) diff --git a/docs/modules/cli/spec.md b/docs/modules/cli/spec.md index 1e26f77f2..8fbcf4daf 100644 --- a/docs/modules/cli/spec.md +++ b/docs/modules/cli/spec.md @@ -70,6 +70,8 @@ evmc::address computeCreateAddress(evmc::address, uint64_t nonce); bool readBinaryFile(path, std::vector&); bool saveState(evmc::MockedHost const&, path); bool loadState(evmc::MockedHost&, path); +EvmUpfrontGasResult applyEvmUpfrontGas(evmc::MockedHost&, evmc_message&, uint64_t, evmc_revision); +void applyEvmPostExecutionSettlement(evmc::MockedHost&, const evmc_message&, uint64_t, const evmc::Result&, evmc_revision); // zen::utils (logging) std::shared_ptr createConsoleLogger(name, LoggerLevel); diff --git a/docs/modules/utils/spec.md b/docs/modules/utils/spec.md index 6d127f96d..441569103 100644 --- a/docs/modules/utils/spec.md +++ b/docs/modules/utils/spec.md @@ -136,6 +136,10 @@ The utils module is DTVM's **utility and shared facilities layer**, providing cr | `bytesToHex` | `std::string bytesToHex(const std::vector& Value)` | Byte vector to hex | | `uint256beToBytes` | `std::vector uint256beToBytes(const evmc::uint256be& Value)` | uint256be to bytes | | `computeCreateAddress` | `evmc::address computeCreateAddress(const evmc::address& Sender, uint64_t SenderNonce)` | Create contract address | +| `computeRefundCap` | `uint64_t computeRefundCap(evmc_revision Revision, uint64_t GasUsed)` | Revision-aware EVM gas refund cap | +| `computeEvmTransactionFees` | `EvmFeeComponents computeEvmTransactionFees(const evmc::uint256be& EffectiveOrMaxFeePerGas, const evmc::uint256be& BaseFee, const std::optional& MaxPriorityFee)` | Effective gas price and priority fee | +| `applyEvmUpfrontGas` | `EvmUpfrontGasResult applyEvmUpfrontGas(evmc::MockedHost& Host, evmc_message& Msg, uint64_t GasLimit, evmc_revision Revision)` | Deduct intrinsic gas, prewarm accounts, and prepay gas | +| `applyEvmPostExecutionSettlement` | `void applyEvmPostExecutionSettlement(evmc::MockedHost& Host, const evmc_message& Msg, uint64_t GasLimit, const evmc::Result& Result, evmc_revision Revision)` | Apply refund cap, refund unused gas, and pay priority fee | | `saveState` | `bool saveState(const evmc::MockedHost& Host, const std::string& FilePath)` | MockedHost state persistence | | `loadState` | `bool loadState(evmc::MockedHost& Host, const std::string& FilePath)` | MockedHost state load | diff --git a/src/cli/dtvm.cpp b/src/cli/dtvm.cpp index b9dfd87c1..42bcb77dd 100644 --- a/src/cli/dtvm.cpp +++ b/src/cli/dtvm.cpp @@ -387,16 +387,6 @@ int main(int argc, char *argv[]) { return exitMain(EXIT_FAILURE, RT.get()); } - MayBe InstRet = Iso->createEVMInstance(*Mod, GasLimit); - if (!InstRet) { - const Error &Err = InstRet.getError(); - ZEN_ASSERT(!Err.isEmpty()); - const auto &ErrMsg = Err.getFormattedMessage(false); - SIMPLE_LOG_ERROR("failed to create EVM instance: %s", ErrMsg.c_str()); - return exitMain(EXIT_FAILURE, RT.get()); - } - EVMInstance *Inst = *InstRet; - Inst->setRevision(EvmRevision); evmc_call_kind MsgKind = DeployMode ? EVMC_CREATE : EVMC_CALL; evmc::Result ExeResult; std::vector Bytecode; @@ -460,6 +450,20 @@ int main(int argc, char *argv[]) { return exitMain(EVMC_INSUFFICIENT_BALANCE, RT.get()); } + // Create the instance only after Msg.gas has been reduced by intrinsic + // gas so that both interpreter and JIT paths start with the same gas. + MayBe InstRet = + Iso->createEVMInstance(*Mod, static_cast(Msg.gas)); + if (!InstRet) { + const Error &Err = InstRet.getError(); + ZEN_ASSERT(!Err.isEmpty()); + const auto &ErrMsg = Err.getFormattedMessage(false); + SIMPLE_LOG_ERROR("failed to create EVM instance: %s", ErrMsg.c_str()); + return exitMain(EXIT_FAILURE, RT.get()); + } + EVMInstance *Inst = *InstRet; + Inst->setRevision(EvmRevision); + RT->callEVMMain(*Inst, Msg, ExeResult); zen::utils::applyEvmPostExecutionSettlement(MockedHost, Msg, GasLimit, diff --git a/src/tests/evm_interp_tests.cpp b/src/tests/evm_interp_tests.cpp index 9ec4dd723..52525c44b 100644 --- a/src/tests/evm_interp_tests.cpp +++ b/src/tests/evm_interp_tests.cpp @@ -1223,6 +1223,44 @@ struct SettlementResult { bool Success = false; }; +const evmc::address SettlementContractAddr = evmc::literals::operator""_address( + "00000000000000000000000000000000000000f1"); +const evmc::address SettlementSenderAddr = evmc::literals::operator""_address( + "a94f5374fce5edbc8e2a8697c15331677e6ebf0b"); +constexpr uint64_t SettlementGasLimit = 8000000; +constexpr uint64_t SettlementSenderBalance = 0xffffffffffff; +constexpr intx::uint256 SettlementGasPrice = intx::uint256(16); + +// Creates the common account/tx-context state used by settlement tests. +// SeedStorage is used by the REVERT test to ensure the first SSTORE is a +// paid reset rather than a free set. +void prepareSettlementHost(zen::evm::ZenMockedEVMHost &Host, + const std::vector &Bytecode, + const evmc::address &SenderAddr, + const evmc::address &ContractAddr, + bool SeedStorage = false) { + evmc::MockedAccount ContractAccount; + ContractAccount.code = evmc::bytes(Bytecode.data(), Bytecode.size()); + if (SeedStorage) { + evmc::bytes32 Key{}; + evmc::bytes32 Val{}; + Val.bytes[31] = 1; + ContractAccount.storage[Key].current = Val; + ContractAccount.storage[Key].original = Val; + } + Host.accounts[ContractAddr] = ContractAccount; + + evmc::MockedAccount SenderAccount; + SenderAccount.set_balance(SettlementSenderBalance); + Host.accounts[SenderAddr] = SenderAccount; + + evmc_tx_context TxCtx{}; + TxCtx.tx_origin = SenderAddr; + TxCtx.tx_gas_price = intx::be::store(SettlementGasPrice); + TxCtx.block_base_fee = intx::be::store(SettlementGasPrice); + Host.tx_context = TxCtx; +} + template SettlementResult runSettlementTransaction(const evmc::address &ContractAddr, const evmc::address &SenderAddr, @@ -1365,32 +1403,11 @@ TEST(EVMRegressionTest, Issue588_CappedRefundDoesNotReduceChargesOnCancun) { auto BytecodeBuf = zen::utils::fromHex(BytecodeHex); ASSERT_TRUE(BytecodeBuf) << "Failed to parse issue #588 bytecode"; - const evmc::address ContractAddr = evmc::literals::operator""_address( - "00000000000000000000000000000000000000f1"); - const evmc::address SenderAddr = evmc::literals::operator""_address( - "a94f5374fce5edbc8e2a8697c15331677e6ebf0b"); - - constexpr uint64_t GasLimit = 8000000; - constexpr uint64_t SenderInitialBalanceValue = 0xffffffffffff; - constexpr intx::uint256 GasPrice = intx::uint256(16); - auto Result = runSettlementTransaction( - ContractAddr, SenderAddr, *BytecodeBuf, EVMC_CANCUN, GasLimit, - [&](zen::evm::ZenMockedEVMHost &Host) { - evmc::MockedAccount ContractAccount; - ContractAccount.code = - evmc::bytes(BytecodeBuf->data(), BytecodeBuf->size()); - Host.accounts[ContractAddr] = ContractAccount; - - evmc::MockedAccount SenderAccount; - SenderAccount.set_balance(SenderInitialBalanceValue); - Host.accounts[SenderAddr] = SenderAccount; - - evmc_tx_context TxCtx{}; - TxCtx.tx_origin = SenderAddr; - TxCtx.tx_gas_price = intx::be::store(GasPrice); - TxCtx.block_base_fee = intx::be::store(GasPrice); - Host.tx_context = TxCtx; + SettlementContractAddr, SettlementSenderAddr, *BytecodeBuf, EVMC_CANCUN, + SettlementGasLimit, [&](zen::evm::ZenMockedEVMHost &Host) { + prepareSettlementHost(Host, *BytecodeBuf, SettlementSenderAddr, + SettlementContractAddr); }); ASSERT_TRUE(Result.Success) << "Issue #588 transaction should succeed"; @@ -1405,7 +1422,7 @@ TEST(EVMRegressionTest, Issue588_CappedRefundDoesNotReduceChargesOnCancun) { EXPECT_EQ(Result.GasCharged, 17770u) << "Refund cap should produce the expected Cancun gas charge"; - intx::uint256 ExpectedCost = GasPrice * Result.GasCharged; + intx::uint256 ExpectedCost = SettlementGasPrice * Result.GasCharged; EXPECT_EQ(Result.InitialSenderBalance - Result.FinalSenderBalance, ExpectedCost) << "Sender should be charged GasCharged * gas_price on issue #588"; @@ -1421,46 +1438,18 @@ TEST(EVMRegressionTest, Issue588_RevertResetsRefundAccumulator) { auto BytecodeBuf = zen::utils::fromHex(BytecodeHex); ASSERT_TRUE(BytecodeBuf) << "Failed to parse revert bytecode"; - const evmc::address ContractAddr = evmc::literals::operator""_address( - "00000000000000000000000000000000000000f1"); - const evmc::address SenderAddr = evmc::literals::operator""_address( - "a94f5374fce5edbc8e2a8697c15331677e6ebf0b"); - - constexpr uint64_t GasLimit = 8000000; - constexpr uint64_t SenderInitialBalanceValue = 0xffffffffffff; - constexpr intx::uint256 GasPrice = intx::uint256(16); - auto Result = runSettlementTransaction( - ContractAddr, SenderAddr, *BytecodeBuf, EVMC_CANCUN, GasLimit, - [&](zen::evm::ZenMockedEVMHost &Host) { - evmc::MockedAccount ContractAccount; - ContractAccount.code = - evmc::bytes(BytecodeBuf->data(), BytecodeBuf->size()); - // Pre-existing non-zero value so the SSTORE costs the reset price and - // would generate a refund if the slot were later cleared. - evmc::bytes32 Key{}; - evmc::bytes32 Val{}; - Val.bytes[31] = 1; - ContractAccount.storage[Key].current = Val; - ContractAccount.storage[Key].original = Val; - Host.accounts[ContractAddr] = ContractAccount; - - evmc::MockedAccount SenderAccount; - SenderAccount.set_balance(SenderInitialBalanceValue); - Host.accounts[SenderAddr] = SenderAccount; - - evmc_tx_context TxCtx{}; - TxCtx.tx_origin = SenderAddr; - TxCtx.tx_gas_price = intx::be::store(GasPrice); - TxCtx.block_base_fee = intx::be::store(GasPrice); - Host.tx_context = TxCtx; + SettlementContractAddr, SettlementSenderAddr, *BytecodeBuf, EVMC_CANCUN, + SettlementGasLimit, [&](zen::evm::ZenMockedEVMHost &Host) { + prepareSettlementHost(Host, *BytecodeBuf, SettlementSenderAddr, + SettlementContractAddr, /*SeedStorage=*/true); }); ASSERT_TRUE(Result.Success) << "Revert transaction should succeed"; EXPECT_EQ(Result.GasCharged, Result.GasUsed) << "REVERT must reset refund counter so no refund is applied"; - intx::uint256 ExpectedCost = GasPrice * Result.GasCharged; + intx::uint256 ExpectedCost = SettlementGasPrice * Result.GasCharged; EXPECT_EQ(Result.InitialSenderBalance - Result.FinalSenderBalance, ExpectedCost) << "Sender cost must equal GasUsed * gas_price after revert"; @@ -1480,32 +1469,12 @@ TEST(EVMRegressionTest, Issue588_HalfRefundCapOnByzantium) { auto BytecodeBuf = zen::utils::fromHex(BytecodeHex); ASSERT_TRUE(BytecodeBuf) << "Failed to parse issue #588 pre-London bytecode"; - const evmc::address ContractAddr = evmc::literals::operator""_address( - "00000000000000000000000000000000000000f1"); - const evmc::address SenderAddr = evmc::literals::operator""_address( - "a94f5374fce5edbc8e2a8697c15331677e6ebf0b"); - - constexpr uint64_t GasLimit = 8000000; - constexpr uint64_t SenderInitialBalanceValue = 0xffffffffffff; - constexpr intx::uint256 GasPrice = intx::uint256(16); - auto Result = runSettlementTransaction( - ContractAddr, SenderAddr, *BytecodeBuf, EVMC_BYZANTIUM, GasLimit, + SettlementContractAddr, SettlementSenderAddr, *BytecodeBuf, + EVMC_BYZANTIUM, SettlementGasLimit, [&](zen::evm::ZenMockedEVMHost &Host) { - evmc::MockedAccount ContractAccount; - ContractAccount.code = - evmc::bytes(BytecodeBuf->data(), BytecodeBuf->size()); - Host.accounts[ContractAddr] = ContractAccount; - - evmc::MockedAccount SenderAccount; - SenderAccount.set_balance(SenderInitialBalanceValue); - Host.accounts[SenderAddr] = SenderAccount; - - evmc_tx_context TxCtx{}; - TxCtx.tx_origin = SenderAddr; - TxCtx.tx_gas_price = intx::be::store(GasPrice); - TxCtx.block_base_fee = intx::be::store(GasPrice); - Host.tx_context = TxCtx; + prepareSettlementHost(Host, *BytecodeBuf, SettlementSenderAddr, + SettlementContractAddr); }); ASSERT_TRUE(Result.Success) @@ -1535,7 +1504,7 @@ TEST(EVMRegressionTest, Issue588_HalfRefundCapOnByzantium) { EXPECT_EQ(Result.GasCharged, Result.GasUsed - HalfCap) << "GasCharged = GasUsed - GasUsed/2 after refund cap"; - intx::uint256 ExpectedCost = GasPrice * Result.GasCharged; + intx::uint256 ExpectedCost = SettlementGasPrice * Result.GasCharged; EXPECT_EQ(Result.InitialSenderBalance - Result.FinalSenderBalance, ExpectedCost) << "Sender should be charged GasCharged * gas_price on issue #588"; @@ -1551,32 +1520,12 @@ TEST(EVMRegressionTest, Issue588_SELFDESTRUCTPreLondonRefundCapped) { auto BytecodeBuf = zen::utils::fromHex(BytecodeHex); ASSERT_TRUE(BytecodeBuf) << "Failed to parse SELFDESTRUCT bytecode hex"; - const evmc::address ContractAddr = evmc::literals::operator""_address( - "00000000000000000000000000000000000000f1"); - const evmc::address SenderAddr = evmc::literals::operator""_address( - "a94f5374fce5edbc8e2a8697c15331677e6ebf0b"); - - constexpr uint64_t GasLimit = 8000000; - constexpr uint64_t SenderInitialBalanceValue = 0xffffffffffff; - constexpr intx::uint256 GasPrice = intx::uint256(16); - auto Result = runSettlementTransaction( - ContractAddr, SenderAddr, *BytecodeBuf, EVMC_BYZANTIUM, GasLimit, + SettlementContractAddr, SettlementSenderAddr, *BytecodeBuf, + EVMC_BYZANTIUM, SettlementGasLimit, [&](zen::evm::ZenMockedEVMHost &Host) { - evmc::MockedAccount ContractAccount; - ContractAccount.code = - evmc::bytes(BytecodeBuf->data(), BytecodeBuf->size()); - Host.accounts[ContractAddr] = ContractAccount; - - evmc::MockedAccount SenderAccount; - SenderAccount.set_balance(SenderInitialBalanceValue); - Host.accounts[SenderAddr] = SenderAccount; - - evmc_tx_context TxCtx{}; - TxCtx.tx_origin = SenderAddr; - TxCtx.tx_gas_price = intx::be::store(GasPrice); - TxCtx.block_base_fee = intx::be::store(GasPrice); - Host.tx_context = TxCtx; + prepareSettlementHost(Host, *BytecodeBuf, SettlementSenderAddr, + SettlementContractAddr); }); ASSERT_TRUE(Result.Success) @@ -1602,7 +1551,7 @@ TEST(EVMRegressionTest, Issue588_SELFDESTRUCTPreLondonRefundCapped) { EXPECT_EQ(Result.GasCharged, SelfDestructGas - HalfCap) << "GasCharged must be GasUsed minus the /2 cap after SELFDESTRUCT"; - intx::uint256 ExpectedCost = GasPrice * Result.GasCharged; + intx::uint256 ExpectedCost = SettlementGasPrice * Result.GasCharged; EXPECT_EQ(Result.InitialSenderBalance - Result.FinalSenderBalance, ExpectedCost) << "Sender must be charged GasCharged * gas_price after SELFDESTRUCT"; @@ -1617,32 +1566,11 @@ TEST(EVMRegressionTest, Issue588_CliSettlement_CappedRefundOnCancun) { ASSERT_TRUE(BytecodeBuf) << "Failed to parse issue #588 CLI settlement bytecode"; - const evmc::address ContractAddr = evmc::literals::operator""_address( - "00000000000000000000000000000000000000f1"); - const evmc::address SenderAddr = evmc::literals::operator""_address( - "a94f5374fce5edbc8e2a8697c15331677e6ebf0b"); - - constexpr uint64_t GasLimit = 8000000; - constexpr uint64_t SenderInitialBalanceValue = 0xffffffffffff; - constexpr intx::uint256 GasPrice = intx::uint256(16); - auto Result = runDtvmCliSettlementTransaction( - ContractAddr, SenderAddr, *BytecodeBuf, EVMC_CANCUN, GasLimit, - [&](zen::evm::ZenMockedEVMHost &Host) { - evmc::MockedAccount ContractAccount; - ContractAccount.code = - evmc::bytes(BytecodeBuf->data(), BytecodeBuf->size()); - Host.accounts[ContractAddr] = ContractAccount; - - evmc::MockedAccount SenderAccount; - SenderAccount.set_balance(SenderInitialBalanceValue); - Host.accounts[SenderAddr] = SenderAccount; - - evmc_tx_context TxCtx{}; - TxCtx.tx_origin = SenderAddr; - TxCtx.tx_gas_price = intx::be::store(GasPrice); - TxCtx.block_base_fee = intx::be::store(GasPrice); - Host.tx_context = TxCtx; + SettlementContractAddr, SettlementSenderAddr, *BytecodeBuf, EVMC_CANCUN, + SettlementGasLimit, [&](zen::evm::ZenMockedEVMHost &Host) { + prepareSettlementHost(Host, *BytecodeBuf, SettlementSenderAddr, + SettlementContractAddr); }); ASSERT_TRUE(Result.Success) << "CLI settlement transaction should succeed"; @@ -1652,7 +1580,7 @@ TEST(EVMRegressionTest, Issue588_CliSettlement_CappedRefundOnCancun) { EXPECT_EQ(Result.GasCharged, 34570u) << "CLI path refund cap should produce the actual Cancun gas charge"; - intx::uint256 ExpectedCost = GasPrice * Result.GasCharged; + intx::uint256 ExpectedCost = SettlementGasPrice * Result.GasCharged; EXPECT_EQ(Result.InitialSenderBalance - Result.FinalSenderBalance, ExpectedCost) << "CLI path sender should be charged GasCharged * gas_price"; @@ -1666,32 +1594,12 @@ TEST(EVMRegressionTest, Issue588_CliSettlement_HalfRefundCapOnByzantium) { ASSERT_TRUE(BytecodeBuf) << "Failed to parse issue #588 pre-London CLI settlement bytecode"; - const evmc::address ContractAddr = evmc::literals::operator""_address( - "00000000000000000000000000000000000000f1"); - const evmc::address SenderAddr = evmc::literals::operator""_address( - "a94f5374fce5edbc8e2a8697c15331677e6ebf0b"); - - constexpr uint64_t GasLimit = 8000000; - constexpr uint64_t SenderInitialBalanceValue = 0xffffffffffff; - constexpr intx::uint256 GasPrice = intx::uint256(16); - auto Result = runDtvmCliSettlementTransaction( - ContractAddr, SenderAddr, *BytecodeBuf, EVMC_BYZANTIUM, GasLimit, + SettlementContractAddr, SettlementSenderAddr, *BytecodeBuf, + EVMC_BYZANTIUM, SettlementGasLimit, [&](zen::evm::ZenMockedEVMHost &Host) { - evmc::MockedAccount ContractAccount; - ContractAccount.code = - evmc::bytes(BytecodeBuf->data(), BytecodeBuf->size()); - Host.accounts[ContractAddr] = ContractAccount; - - evmc::MockedAccount SenderAccount; - SenderAccount.set_balance(SenderInitialBalanceValue); - Host.accounts[SenderAddr] = SenderAccount; - - evmc_tx_context TxCtx{}; - TxCtx.tx_origin = SenderAddr; - TxCtx.tx_gas_price = intx::be::store(GasPrice); - TxCtx.block_base_fee = intx::be::store(GasPrice); - Host.tx_context = TxCtx; + prepareSettlementHost(Host, *BytecodeBuf, SettlementSenderAddr, + SettlementContractAddr); }); ASSERT_TRUE(Result.Success) @@ -1706,7 +1614,7 @@ TEST(EVMRegressionTest, Issue588_CliSettlement_HalfRefundCapOnByzantium) { EXPECT_EQ(Result.GasCharged, 31012u) << "CLI path GasCharged should be GasUsed minus the refund"; - intx::uint256 ExpectedCost = GasPrice * Result.GasCharged; + intx::uint256 ExpectedCost = SettlementGasPrice * Result.GasCharged; EXPECT_EQ(Result.InitialSenderBalance - Result.FinalSenderBalance, ExpectedCost) << "CLI path sender should be charged GasCharged * gas_price"; diff --git a/src/tests/evm_test_host.hpp b/src/tests/evm_test_host.hpp index 4b688e888..faf8ac9ab 100644 --- a/src/tests/evm_test_host.hpp +++ b/src/tests/evm_test_host.hpp @@ -1097,7 +1097,7 @@ class ZenMockedEVMHost : public evmc::MockedHost { const TransactionExecutionConfig &Config, const evmc_message &Msg, TransactionExecutionResult &Result) { - const auto Fees = zen::utils::computeEip1559Fees( + const auto Fees = zen::utils::computeEvmTransactionFees( tx_context.tx_gas_price, tx_context.block_base_fee, Config.MaxPriorityFeePerGas); const intx::uint256 EffectiveGasPrice = Fees.EffectiveGasPrice; @@ -1134,7 +1134,7 @@ class ZenMockedEVMHost : public evmc::MockedHost { const TransactionExecutionConfig &Config, const evmc_message &Msg, TransactionExecutionResult &Result, bool FeesPrepaid) { - const auto Fees = zen::utils::computeEip1559Fees( + const auto Fees = zen::utils::computeEvmTransactionFees( tx_context.tx_gas_price, tx_context.block_base_fee, Config.MaxPriorityFeePerGas); const intx::uint256 EffectiveGasPrice = Fees.EffectiveGasPrice; diff --git a/src/utils/evm.cpp b/src/utils/evm.cpp index d3e8aa1bb..3690a71f5 100644 --- a/src/utils/evm.cpp +++ b/src/utils/evm.cpp @@ -581,10 +581,10 @@ uint64_t computeRefundCap(evmc_revision Revision, uint64_t GasUsed) { return Revision >= EVMC_LONDON ? GasUsed / 5 : GasUsed / 2; } -Eip1559FeeComponents -computeEip1559Fees(const evmc::uint256be &EffectiveOrMaxFeePerGas, - const evmc::uint256be &BaseFee, - const std::optional &MaxPriorityFee) { +EvmFeeComponents computeEvmTransactionFees( + const evmc::uint256be &EffectiveOrMaxFeePerGas, + const evmc::uint256be &BaseFee, + const std::optional &MaxPriorityFee) { intx::uint256 GasPriceN = intx::be::load(EffectiveOrMaxFeePerGas); intx::uint256 BaseFeeN = intx::be::load(BaseFee); @@ -622,8 +622,8 @@ EvmUpfrontGasResult applyEvmUpfrontGas(evmc::MockedHost &Host, // Deduct upfront gas cost from sender's balance before execution. // Per EVM spec (Yellow Paper §6), the sender's balance is reduced by // effective_gas_price * gas_limit at the start of transaction execution. - const auto Fees = computeEip1559Fees(Host.tx_context.tx_gas_price, - Host.tx_context.block_base_fee); + const auto Fees = computeEvmTransactionFees(Host.tx_context.tx_gas_price, + Host.tx_context.block_base_fee); intx::uint256 UpfrontGasCost = intx::uint256(GasLimit) * Fees.EffectiveGasPrice; auto &SenderAccount = Host.accounts[Msg.sender]; @@ -652,8 +652,8 @@ void applyEvmPostExecutionSettlement(evmc::MockedHost &Host, const uint64_t GasCharged = AppliedRefund < TotalGasUsed ? TotalGasUsed - AppliedRefund : 0; - const auto Fees = computeEip1559Fees(Host.tx_context.tx_gas_price, - Host.tx_context.block_base_fee); + const auto Fees = computeEvmTransactionFees(Host.tx_context.tx_gas_price, + Host.tx_context.block_base_fee); auto &SenderAccount = Host.accounts[Msg.sender]; intx::uint256 SenderBalance = intx::be::load(SenderAccount.balance); diff --git a/src/utils/evm.h b/src/utils/evm.h index 78664d4b5..364d0759a 100644 --- a/src/utils/evm.h +++ b/src/utils/evm.h @@ -55,19 +55,19 @@ void prewarmTransactionAccounts(evmc::MockedHost &Host, evmc_revision Revision, /// pre-London caps at GasUsed/2. uint64_t computeRefundCap(evmc_revision Revision, uint64_t GasUsed); -struct Eip1559FeeComponents { +struct EvmFeeComponents { intx::uint256 EffectiveGasPrice; intx::uint256 PriorityFee; }; -/// Compute the effective gas price and priority fee. +/// Compute the effective gas price and priority fee for an EVM transaction. /// When MaxPriorityFee is provided, EffectiveOrMaxFeePerGas is the EIP-1559 /// maxFeePerGas, PriorityFee is min(maxPriorityFee, maxFeePerGas - baseFee), /// and EffectiveGasPrice is baseFee + PriorityFee. /// Otherwise EffectiveOrMaxFeePerGas is already the effective gas price for a /// legacy transaction, PriorityFee is max(effectiveGasPrice - baseFee, 0), /// and EffectiveGasPrice is effectiveGasPrice. -Eip1559FeeComponents computeEip1559Fees( +EvmFeeComponents computeEvmTransactionFees( const evmc::uint256be &EffectiveOrMaxFeePerGas, const evmc::uint256be &BaseFee, const std::optional &MaxPriorityFee = std::nullopt); From acdf8e707892385389a94461e0c808c741363c40 Mon Sep 17 00:00:00 2001 From: Outcry <843648230@qq.com> Date: Thu, 10 Sep 2026 03:40:10 +0000 Subject: [PATCH 3/3] fix(evm): initialize original storage value on prestate load Previously, loaded storage set current but left original as zero. A non-zero prestate slot cleared to zero was classified as a dirty clear, charging the wrong SSTORE gas and accruing an incorrect refund. Initialize original from the persisted current value unless an explicit original is provided, so loading state starts a new transaction from that boundary. Closes #589 --- src/tests/evm_interp_tests.cpp | 99 ++++++++++++++++++++++++++++++++++ src/utils/evm.cpp | 11 ++++ 2 files changed, 110 insertions(+) diff --git a/src/tests/evm_interp_tests.cpp b/src/tests/evm_interp_tests.cpp index 52525c44b..f2cc1819c 100644 --- a/src/tests/evm_interp_tests.cpp +++ b/src/tests/evm_interp_tests.cpp @@ -1209,6 +1209,105 @@ TEST(EVMStateSaveLoad, MissingChainIdAndBlobBaseFee) { std::filesystem::remove(StateFilePath); } +// 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 +// to zero is misclassified as a dirty clear instead of a reset. +TEST(EVMStateSaveLoad, LoadedStorageInitializesOriginalForNewTransaction) { + const std::string StateFilePath = "/tmp/dtvm_issue589_state.json"; + const std::string ContractAddr = "00000000000000000000000000000000000000f1"; + const std::string SenderAddr = "a94f5374fce5edbc8e2a8697c15331677e6ebf0b"; + const std::vector Bytecode = {0x60, 0x00, 0x60, 0x00, 0x55, 0x00}; + + { + std::ofstream StateFile(StateFilePath); + ASSERT_TRUE(StateFile) << "Failed to create issue #589 state file"; + StateFile << R"({ + "accounts": { + ")" << ContractAddr + << R"(": { + "balance": "0000000000000000000000000000000000000000000000000000000000000000", + "code": "0x600060005500", + "nonce": 0, + "storage": { + "0000000000000000000000000000000000000000000000000000000000000000": { + "value": "0000000000000000000000000000000000000000000000000000000000000005", + "access_status": 0 + } + } + }, + ")" << 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); + const auto PrestateValue = zen::utils::parseBytes32( + "0000000000000000000000000000000000000000000000000000000000000005"); + EXPECT_EQ(std::memcmp(Slot.current.bytes, PrestateValue.bytes, 32), 0) + << "Prestate current storage value was not loaded"; + EXPECT_EQ(std::memcmp(Slot.original.bytes, PrestateValue.bytes, 32), 0) + << "Prestate current value must initialize original for a new tx"; + + 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 = "issue589"; + ExecConfig.Bytecode = Bytecode.data(); + ExecConfig.BytecodeSize = Bytecode.size(); + ExecConfig.Revision = EVMC_CANCUN; + ExecConfig.GasLimit = 100000; + ExecConfig.IntrinsicGas = 21000; + 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); + // Execution gas: 2 PUSH1 + cold SLOAD + reset SSTORE = 5006. + // Intrinsic gas: 21000. Raw refund: 4800; cap floor(26006 / 5) = 5201. + EXPECT_EQ(Result.GasUsed, 26006u) + << "SSTORE(5 -> 0) must charge cold access + reset gas"; + EXPECT_EQ(Result.GasRefund, 4800u) + << "Clearing a non-zero prestate slot refunds R_clear"; + EXPECT_EQ(Result.GasCharged, 21206u) + << "Host-path net SSTORE gas is reset cost after refund"; + + std::filesystem::remove(StateFilePath); +} + 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 3690a71f5..312ccac7e 100644 --- a/src/utils/evm.cpp +++ b/src/utils/evm.cpp @@ -392,6 +392,16 @@ bool loadState(evmc::MockedHost &Host, const std::string &FilePath) { 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; + } if (StorageValue.HasMember("access_status") && StorageValue["access_status"].IsUint()) { StorageVal.access_status = static_cast( @@ -401,6 +411,7 @@ bool loadState(evmc::MockedHost &Host, const std::string &FilePath) { // Old format with just value StorageVal.current = zen::utils::parseBytes32(StorageValue.GetString()); + StorageVal.original = StorageVal.current; } Account.storage[Key] = StorageVal;