From 7fa30c15ee9543f529acfff69a27eb13cd7d61a4 Mon Sep 17 00:00:00 2001 From: ECNUyhy <93872942+ECNUyhy@users.noreply.github.com> Date: Tue, 28 Jul 2026 09:02:54 +0000 Subject: [PATCH 1/2] perf(evm): propagate KECCAK memory guarantees through CFG Teach the guaranteed-minimum analysis that a non-empty constant KECCAK range expands logical EVM memory through its read end. Preserve zero-length behavior and propagate the resulting proof within and across analyzer blocks for later lowering consumers. --- .../evm_frontend/evm_memory_analysis.h | 2 + src/tests/evm_jit_frontend_tests.cpp | 37 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/compiler/evm_frontend/evm_memory_analysis.h b/src/compiler/evm_frontend/evm_memory_analysis.h index a837ae03..39f097f1 100644 --- a/src/compiler/evm_frontend/evm_memory_analysis.h +++ b/src/compiler/evm_frontend/evm_memory_analysis.h @@ -876,6 +876,8 @@ class MemoryGuaranteedMinBytesAnalysis { switch (Op.Kind) { case MemoryOpKind::MLoad: return Op.Reads.size() == 1 && getIntervalEnd(Op.Reads[0], End); + case MemoryOpKind::Keccak: + return Op.Reads.size() == 1 && getIntervalEnd(Op.Reads[0], End); case MemoryOpKind::MStore: case MemoryOpKind::MStore8: return Op.Writes.size() == 1 && getIntervalEnd(Op.Writes[0], End); diff --git a/src/tests/evm_jit_frontend_tests.cpp b/src/tests/evm_jit_frontend_tests.cpp index f046c7ad..ad9d0e54 100644 --- a/src/tests/evm_jit_frontend_tests.cpp +++ b/src/tests/evm_jit_frontend_tests.cpp @@ -396,6 +396,43 @@ TEST(EVMMemoryGuaranteedMinBytesAnalysisTest, LearnsConstMCopyUnionEnd) { EXPECT_EQ(Guaranteed.getGuaranteedMinBytesBeforeOp(Facts.Ops[1].Id), 0x40u); } +TEST(EVMMemoryGuaranteedMinBytesAnalysisTest, LearnsConstKeccakReadEnd) { + const std::vector Bytecode = {OP_PUSH1, 0x40, OP_PUSH1, + 0x20, OP_KECCAK256, OP_POP, + OP_PUSH1, 0x20, OP_MLOAD}; + + COMPILER::MemoryFacts Facts = collectMemoryFacts(Bytecode); + COMPILER::MemoryGuaranteedMinBytesAnalysis Guaranteed(Facts); + + ASSERT_EQ(Facts.Ops.size(), 2u); + EXPECT_EQ(Guaranteed.getGuaranteedMinBytesBeforeOp(Facts.Ops[1].Id), 0x60u); +} + +TEST(EVMMemoryGuaranteedMinBytesAnalysisTest, + PropagatesConstKeccakGuaranteeToSuccessor) { + const std::vector Bytecode = { + OP_PUSH1, 0x40, OP_PUSH1, 0x20, OP_KECCAK256, + OP_POP, OP_JUMPDEST, OP_PUSH1, 0x20, OP_MLOAD}; + + COMPILER::MemoryFacts Facts = collectAnalyzerMemoryFacts(Bytecode); + COMPILER::MemoryGuaranteedMinBytesAnalysis Guaranteed(Facts); + + ASSERT_EQ(Facts.Ops.size(), 2u); + EXPECT_EQ(Guaranteed.getGuaranteedMinBytesAtEntry(6), 0x60u); +} + +TEST(EVMMemoryGuaranteedMinBytesAnalysisTest, IgnoresZeroLengthKeccak) { + const std::vector Bytecode = {OP_PUSH1, 0x00, OP_PUSH1, + 0x80, OP_KECCAK256, OP_POP, + OP_PUSH1, 0x00, OP_MLOAD}; + + COMPILER::MemoryFacts Facts = collectMemoryFacts(Bytecode); + COMPILER::MemoryGuaranteedMinBytesAnalysis Guaranteed(Facts); + + ASSERT_EQ(Facts.Ops.size(), 2u); + EXPECT_EQ(Guaranteed.getGuaranteedMinBytesBeforeOp(Facts.Ops[1].Id), 0u); +} + TEST(EVMMemoryGuaranteedMinBytesAnalysisTest, IgnoresZeroLengthMCopy) { const std::vector Bytecode = {OP_PUSH1, 0x00, OP_PUSH1, 0x80, OP_PUSH1, 0xa0, OP_MCOPY, OP_PUSH1, From 7247317bdd708f342192de229b8dfcf880793d75 Mon Sep 17 00:00:00 2001 From: ECNUyhy <93872942+ECNUyhy@users.noreply.github.com> Date: Tue, 28 Jul 2026 09:37:00 +0000 Subject: [PATCH 2/2] perf(evm): reuse CFG memory proofs for KECCAK expansion Reuse guaranteed minimum memory facts at the exact KECCAK opcode PC to elide redundant pre-expansion while preserving dynamic word gas charging. Add MIR and differential coverage for cross-block reuse, fallback expansion, hash output, gas equality, and the word-gas OOG boundary. --- .../evm_frontend/evm_mir_compiler.cpp | 8 +- src/tests/evm_differential_tests.cpp | 72 +++++++++++++-- src/tests/evm_jit_frontend_tests.cpp | 88 +++++++++++++++++++ 3 files changed, 162 insertions(+), 6 deletions(-) diff --git a/src/compiler/evm_frontend/evm_mir_compiler.cpp b/src/compiler/evm_frontend/evm_mir_compiler.cpp index 9ff0e22f..a71a88d9 100644 --- a/src/compiler/evm_frontend/evm_mir_compiler.cpp +++ b/src/compiler/evm_frontend/evm_mir_compiler.cpp @@ -5861,7 +5861,12 @@ EVMMirBuilder::handleKeccak256(Operand OffsetComponents, syncGasToMemory(); #endif } else { - preExpandMemoryRange(OffsetComponents, LengthComponents); + const bool ExpansionCovered = OffsetWasConstU64 && LengthWasConstU64 && + tryUseGuaranteedMinBytesExpansionElision( + true, ConstOffset, ConstLength); + if (!ExpansionCovered) { + preExpandMemoryRange(OffsetComponents, LengthComponents); + } MInstruction *Length = extractKnownU64LowOperand(LengthComponents); chargeKeccakWordGasIR(Length); } @@ -8639,6 +8644,7 @@ void EVMMirBuilder::noteMemoryOpcodeInBlock(evmc_opcode Opcode, uint64_t PC) { } void EVMMirBuilder::noteHelperOpcodeInBlock(evmc_opcode Opcode, uint64_t PC) { + CurrentMemoryOpPC = PC; if (!CurBlockMemStats.Active) { return; } diff --git a/src/tests/evm_differential_tests.cpp b/src/tests/evm_differential_tests.cpp index 82dcecd9..1de5b879 100644 --- a/src/tests/evm_differential_tests.cpp +++ b/src/tests/evm_differential_tests.cpp @@ -73,10 +73,11 @@ struct EVMExecutionResult { // config can fall back to the interpreter for a single call, which would make // a differential test vacuous; DisableMultipassMultithread compiles before the // run so the JIT side is non-vacuous. -EVMExecutionResult -runEvmBytecode(const std::string &Label, const std::vector &Bytecode, - common::RunMode Mode, const std::vector &CallData = {}, - uint32_t MessageFlags = 0u, bool EnableGasMetering = false) { +EVMExecutionResult runEvmBytecode( + const std::string &Label, const std::vector &Bytecode, + common::RunMode Mode, const std::vector &CallData = {}, + uint32_t MessageFlags = 0u, bool EnableGasMetering = false, + uint64_t GasLimit = 0xFFFF'FFFF'FFFF - zen::evm::BASIC_EXECUTION_COST) { EVMExecutionResult Empty; RuntimeConfig Config; @@ -106,7 +107,6 @@ runEvmBytecode(const std::string &Label, const std::vector &Bytecode, return Empty; } - const uint64_t GasLimit = 0xFFFF'FFFF'FFFF - zen::evm::BASIC_EXECUTION_COST; auto InstRet = Iso->createEVMInstance(*Mod, GasLimit); if (!InstRet) { ADD_FAILURE() << "instance create failed: " << Label; @@ -262,6 +262,68 @@ std::string fixtureTestName(const testing::TestParamInfo &Info) { } // namespace +TEST(EVMKeccakMemoryProofDifferential, + CrossBlockProofReusePreservesHashAndGas) { + const std::vector Bytecode = { + 0x60, 0x01, // PC0: PUSH1 1 + 0x60, 0x80, // PC2: PUSH1 0x80 + 0x52, // PC4: MSTORE, proves memory through 0xa0 + 0x60, 0x08, // PC5: PUSH1 successor + 0x56, // PC7: JUMP + 0x5b, // PC8: JUMPDEST + 0x60, 0x20, // PC9: PUSH1 hash length + 0x60, 0x80, // PC11: PUSH1 hash offset + 0x20, // PC13: KECCAK256 + 0x5f, // PC14: PUSH0 result offset + 0x52, // PC15: MSTORE hash result + 0x60, 0x20, // PC16: PUSH1 return length + 0x5f, // PC18: PUSH0 return offset + 0xf3, // PC19: RETURN + }; + + const auto Output = + expectInterpMatchesMultipassWithGas("keccak_cfg_proof", Bytecode, {}); + EXPECT_EQ(Output, + "B10E2D527612073B26EECDFD717E6A320CF44B4AFAC2B0732D9FCBE2B7FA0CF6"); +} + +TEST(EVMKeccakMemoryProofDifferential, + WordGasOutOfGasBoundaryMatchesInterpreter) { + // KECCAK256 is the final operation, so one gas below the successful + // execution budget exercises its final dynamic word-gas charge. + const std::vector Bytecode = { + 0x60, 0x01, // PUSH1 1 + 0x60, 0x80, // PUSH1 0x80 + 0x52, // MSTORE, expands memory before KECCAK256 + 0x60, 0x20, // PUSH1 hash length + 0x60, 0x80, // PUSH1 hash offset + 0x20, // KECCAK256 + }; + constexpr uint64_t ProbeGasLimit = 1'000'000; + const auto Reference = + runEvmBytecode("keccak_word_gas_reference", Bytecode, + common::RunMode::InterpMode, {}, 0u, true, ProbeGasLimit); + ASSERT_EQ(Reference.Status, EVMC_SUCCESS); + ASSERT_GE(Reference.GasLeft, 0); + const auto RequiredGas = + ProbeGasLimit - static_cast(Reference.GasLeft); + ASSERT_GT(RequiredGas, uint64_t{1}); + + const auto Interp = runEvmBytecode("keccak_word_gas_oog_interp", Bytecode, + common::RunMode::InterpMode, {}, 0u, true, + RequiredGas - 1); + const auto Multi = runEvmBytecode("keccak_word_gas_oog_multipass", Bytecode, + common::RunMode::MultipassMode, {}, 0u, + true, RequiredGas - 1); +#ifdef ZEN_ENABLE_JIT + EXPECT_TRUE(Multi.JITCompiled); +#endif + EXPECT_EQ(Interp.Status, EVMC_OUT_OF_GAS); + EXPECT_EQ(Multi.Status, Interp.Status); + EXPECT_EQ(Multi.GasLeft, Interp.GasLeft); + EXPECT_EQ(Multi.OutputHex, Interp.OutputHex); +} + // =========================================================================== // Fixture-based differential suite. // diff --git a/src/tests/evm_jit_frontend_tests.cpp b/src/tests/evm_jit_frontend_tests.cpp index ad9d0e54..d608dd5d 100644 --- a/src/tests/evm_jit_frontend_tests.cpp +++ b/src/tests/evm_jit_frontend_tests.cpp @@ -3,6 +3,7 @@ #include "action/evm_bytecode_visitor.h" #include "compiler/evm_frontend/evm_analyzer.h" +#include "compiler/evm_frontend/evm_imported.h" #include "compiler/evm_frontend/evm_memory_analysis.h" #include "compiler/evm_frontend/evm_memory_facts.h" #include "compiler/evm_frontend/evm_memory_grouping.h" @@ -433,6 +434,93 @@ TEST(EVMMemoryGuaranteedMinBytesAnalysisTest, IgnoresZeroLengthKeccak) { EXPECT_EQ(Guaranteed.getGuaranteedMinBytesBeforeOp(Facts.Ops[1].Id), 0u); } +#ifdef ZEN_ENABLE_EVM_MEMORY_PLAN_FRAMEWORK +std::optional +countKeccakExpandMemoryCalls(const std::vector &Bytecode) { + COMPILER::EVMFrontendContext Ctx; + Ctx.setRevision(EVMC_CANCUN); + Ctx.setBytecode(reinterpret_cast(Bytecode.data()), + Bytecode.size()); + + COMPILER::MModule Mod(Ctx); + std::array ParamTypes = { + COMPILER::MPointerType::create(Ctx, Ctx.VoidType)}; + COMPILER::MFunctionType *FuncType = COMPILER::MFunctionType::create( + Ctx, Ctx.VoidType, llvm::ArrayRef(ParamTypes)); + Mod.addFuncType(FuncType); + + COMPILER::MFunction Func(Ctx, 0); + Func.setFunctionType(FuncType); + EVMMirBuilder Builder(Ctx, Func); + if (!Builder.compile(&Ctx)) { + return std::nullopt; + } + + std::string Mir; + llvm::raw_string_ostream OS(Mir); + Func.print(OS); + OS.flush(); + + const auto &RuntimeFunctions = COMPILER::getRuntimeFunctionTable(); + const auto Address = + COMPILER::getFunctionAddress(RuntimeFunctions.ExpandMemoryNoGas); + const std::string Needle = + "target = const.i64 " + std::to_string(Address) + ", "; + size_t Count = 0; + for (size_t Pos = Mir.find(Needle); Pos != std::string::npos; + Pos = Mir.find(Needle, Pos + Needle.size())) { + ++Count; + } + return Count; +} + +TEST(EVMMirBuilderMemoryProofTest, + ReusesCrossBlockGuaranteedSizeForKeccakConsumer) { + const std::vector ProducerOnly = { + OP_PUSH1, 0x01, OP_PUSH1, 0x80, OP_MSTORE, + OP_PUSH1, 0x08, OP_JUMP, OP_JUMPDEST, OP_STOP}; + const std::vector CoveredKeccak = { + OP_PUSH1, 0x01, OP_PUSH1, 0x80, OP_MSTORE, OP_PUSH1, + 0x08, OP_JUMP, OP_JUMPDEST, OP_PUSH1, 0x20, OP_PUSH1, + 0x80, OP_KECCAK256, OP_POP, OP_STOP}; + const std::vector UncoveredKeccak = { + OP_PUSH1, 0x01, OP_PUSH1, 0x80, OP_MSTORE, OP_PUSH1, + 0x08, OP_JUMP, OP_JUMPDEST, OP_PUSH1, 0x20, OP_PUSH1, + 0xa0, OP_KECCAK256, OP_POP, OP_STOP}; + + const std::optional ProducerExpansionCalls = + countKeccakExpandMemoryCalls(ProducerOnly); + const std::optional CoveredExpansionCalls = + countKeccakExpandMemoryCalls(CoveredKeccak); + const std::optional UncoveredExpansionCalls = + countKeccakExpandMemoryCalls(UncoveredKeccak); + + ASSERT_TRUE(ProducerExpansionCalls.has_value()); + ASSERT_TRUE(CoveredExpansionCalls.has_value()); + ASSERT_TRUE(UncoveredExpansionCalls.has_value()); + EXPECT_EQ(*CoveredExpansionCalls, *ProducerExpansionCalls); + EXPECT_EQ(*UncoveredExpansionCalls, *ProducerExpansionCalls + 1); +} + +TEST(EVMMirBuilderMemoryProofTest, ReusesPriorKeccakProofAtExactOpcodePC) { + const std::vector OneKeccak = {OP_PUSH1, 0x20, OP_PUSH1, 0x80, + OP_KECCAK256, OP_POP, OP_STOP}; + const std::vector TwoKeccaks = { + OP_PUSH1, 0x20, OP_PUSH1, 0x80, OP_KECCAK256, + OP_POP, OP_PUSH1, 0x20, OP_PUSH1, 0x80, + OP_KECCAK256, OP_POP, OP_STOP}; + + const std::optional OneExpansionCalls = + countKeccakExpandMemoryCalls(OneKeccak); + const std::optional TwoExpansionCalls = + countKeccakExpandMemoryCalls(TwoKeccaks); + + ASSERT_TRUE(OneExpansionCalls.has_value()); + ASSERT_TRUE(TwoExpansionCalls.has_value()); + EXPECT_EQ(*TwoExpansionCalls, *OneExpansionCalls); +} +#endif + TEST(EVMMemoryGuaranteedMinBytesAnalysisTest, IgnoresZeroLengthMCopy) { const std::vector Bytecode = {OP_PUSH1, 0x00, OP_PUSH1, 0x80, OP_PUSH1, 0xa0, OP_MCOPY, OP_PUSH1,