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 2c8fa323a..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; @@ -443,66 +433,41 @@ 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); + // 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, + 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..f2cc1819c 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,516 @@ 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 +// 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; +}; + +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, + 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"; + + auto Result = runSettlementTransaction( + SettlementContractAddr, SettlementSenderAddr, *BytecodeBuf, EVMC_CANCUN, + SettlementGasLimit, [&](zen::evm::ZenMockedEVMHost &Host) { + prepareSettlementHost(Host, *BytecodeBuf, SettlementSenderAddr, + SettlementContractAddr); + }); + + 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 = SettlementGasPrice * 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"; + + auto Result = runSettlementTransaction( + 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 = SettlementGasPrice * 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"; + + auto Result = runSettlementTransaction( + SettlementContractAddr, SettlementSenderAddr, *BytecodeBuf, + EVMC_BYZANTIUM, SettlementGasLimit, + [&](zen::evm::ZenMockedEVMHost &Host) { + prepareSettlementHost(Host, *BytecodeBuf, SettlementSenderAddr, + SettlementContractAddr); + }); + + 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 = SettlementGasPrice * 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"; + + auto Result = runSettlementTransaction( + SettlementContractAddr, SettlementSenderAddr, *BytecodeBuf, + EVMC_BYZANTIUM, SettlementGasLimit, + [&](zen::evm::ZenMockedEVMHost &Host) { + prepareSettlementHost(Host, *BytecodeBuf, SettlementSenderAddr, + SettlementContractAddr); + }); + + 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 = SettlementGasPrice * 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"; + + auto Result = runDtvmCliSettlementTransaction( + SettlementContractAddr, SettlementSenderAddr, *BytecodeBuf, EVMC_CANCUN, + SettlementGasLimit, [&](zen::evm::ZenMockedEVMHost &Host) { + prepareSettlementHost(Host, *BytecodeBuf, SettlementSenderAddr, + SettlementContractAddr); + }); + + 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 = SettlementGasPrice * 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"; + + auto Result = runDtvmCliSettlementTransaction( + SettlementContractAddr, SettlementSenderAddr, *BytecodeBuf, + EVMC_BYZANTIUM, SettlementGasLimit, + [&](zen::evm::ZenMockedEVMHost &Host) { + prepareSettlementHost(Host, *BytecodeBuf, SettlementSenderAddr, + SettlementContractAddr); + }); + + 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 = SettlementGasPrice * 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..faf8ac9ab 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::computeEvmTransactionFees( + 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::computeEvmTransactionFees( + 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..312ccac7e 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 @@ -391,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( @@ -400,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; @@ -574,4 +586,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; +} + +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); + 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 = 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]; + 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 = 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); + + // 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..364d0759a 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 EvmFeeComponents { + intx::uint256 EffectiveGasPrice; + intx::uint256 PriorityFee; +}; + +/// 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. +EvmFeeComponents computeEvmTransactionFees( + 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