From 1e46ac94d150f2c23041cce9e09501e6035867d0 Mon Sep 17 00:00:00 2001 From: Outcry <843648230@qq.com> Date: Wed, 6 May 2026 08:53:17 +0000 Subject: [PATCH 1/2] perf(evm): add soft-fallback opcode table for const memory precheck --- src/action/evm_bytecode_visitor.h | 121 +++++++++++++++++++++++++++++- 1 file changed, 119 insertions(+), 2 deletions(-) diff --git a/src/action/evm_bytecode_visitor.h b/src/action/evm_bytecode_visitor.h index 07a1726ad..b8515a3db 100644 --- a/src/action/evm_bytecode_visitor.h +++ b/src/action/evm_bytecode_visitor.h @@ -1207,6 +1207,106 @@ template class EVMByteCodeVisitor { Opcode == OP_STOP || Opcode == OP_INVALID || Opcode == OP_REVERT || Opcode == OP_SELFDESTRUCT; } + // Stack effect for opcodes that the const-precheck simulator does not + // hand-model. `Pops`/`Pushes` describe the EVM stack delta; setting `Pops` + // to `kPrecheckBailSentinel` rejects the plan when the opcode is + // encountered (used for memory mutators or anything that would invalidate + // the abstract memory model). + struct PrecheckOpaqueStackEffect { + uint8_t Pops; + uint8_t Pushes; + }; + + static constexpr uint8_t kPrecheckBailSentinel = 0xFF; + + // Build a 256-entry lookup table from EVM opcode byte to its stack effect + // for the const-precheck simulator's `default:` fallback. Opcodes that are + // hand-modeled in the switch (PUSH*/DUP*/SWAP*/POP/ADD/SUB/MLOAD/MSTORE/ + // MSTORE8/MSIZE) and block terminators are still routed through their + // dedicated arms; entries here are only consulted when the switch falls + // through. Opcodes left at the bail sentinel reject the plan (e.g. MCOPY, + // unallocated opcode bytes). Helper-sensitive opcodes are already filtered + // out earlier by `isHelperSensitiveOpcode`. + static constexpr std::array + buildPrecheckOpaqueStackEffectTable() { + std::array Table{}; + for (auto &Entry : Table) { + Entry = {kPrecheckBailSentinel, 0}; + } + + // Arithmetic (binary, push 1). + Table[OP_MUL] = {2, 1}; + Table[OP_DIV] = {2, 1}; + Table[OP_SDIV] = {2, 1}; + Table[OP_MOD] = {2, 1}; + Table[OP_SMOD] = {2, 1}; + Table[OP_EXP] = {2, 1}; + Table[OP_SIGNEXTEND] = {2, 1}; + // Ternary modular arithmetic. + Table[OP_ADDMOD] = {3, 1}; + Table[OP_MULMOD] = {3, 1}; + + // Comparison / bitwise (binary, push 1). + Table[OP_LT] = {2, 1}; + Table[OP_GT] = {2, 1}; + Table[OP_SLT] = {2, 1}; + Table[OP_SGT] = {2, 1}; + Table[OP_EQ] = {2, 1}; + Table[OP_AND] = {2, 1}; + Table[OP_OR] = {2, 1}; + Table[OP_XOR] = {2, 1}; + Table[OP_BYTE] = {2, 1}; + Table[OP_SHL] = {2, 1}; + Table[OP_SHR] = {2, 1}; + Table[OP_SAR] = {2, 1}; + + // Unary. + Table[OP_ISZERO] = {1, 1}; + Table[OP_NOT] = {1, 1}; + Table[OP_CLZ] = {1, 1}; + + // Environment / context reads (push 1, no pops). + Table[OP_ADDRESS] = {0, 1}; + Table[OP_ORIGIN] = {0, 1}; + Table[OP_CALLER] = {0, 1}; + Table[OP_CALLVALUE] = {0, 1}; + Table[OP_CALLDATASIZE] = {0, 1}; + Table[OP_CODESIZE] = {0, 1}; + Table[OP_GASPRICE] = {0, 1}; + Table[OP_RETURNDATASIZE] = {0, 1}; + Table[OP_COINBASE] = {0, 1}; + Table[OP_TIMESTAMP] = {0, 1}; + Table[OP_NUMBER] = {0, 1}; + Table[OP_PREVRANDAO] = {0, 1}; + Table[OP_GASLIMIT] = {0, 1}; + Table[OP_CHAINID] = {0, 1}; + Table[OP_SELFBALANCE] = {0, 1}; + Table[OP_BASEFEE] = {0, 1}; + Table[OP_BLOBBASEFEE] = {0, 1}; + Table[OP_PC] = {0, 1}; + Table[OP_GAS] = {0, 1}; + + // Environment reads that consume one stack input. + Table[OP_BALANCE] = {1, 1}; + Table[OP_CALLDATALOAD] = {1, 1}; + Table[OP_EXTCODESIZE] = {1, 1}; + Table[OP_EXTCODEHASH] = {1, 1}; + Table[OP_BLOCKHASH] = {1, 1}; + Table[OP_BLOBHASH] = {1, 1}; + + // Storage / transient storage. SLOAD/TLOAD push an opaque value; + // SSTORE/TSTORE only mutate non-memory state, so leaving the abstract + // memory model intact is safe. + Table[OP_SLOAD] = {1, 1}; + Table[OP_TLOAD] = {1, 1}; + Table[OP_SSTORE] = {2, 0}; + Table[OP_TSTORE] = {2, 0}; + + // MCOPY mutates linear memory with non-tracked semantics; left at the + // bail sentinel above so the plan is rejected on encounter. + + return Table; + } static AbstractConstU64 makeUnknownConstU64() { return {}; } @@ -1619,12 +1719,29 @@ template class EVMByteCodeVisitor { case OP_MSIZE: SimStack.push_back(makeUnknownConstU64()); break; - default: + default: { if (isBlockTerminatorOpcode(Opcode)) { ScanPC = BytecodeSize; break; } - return {}; + static constexpr auto OpaqueEffects = + buildPrecheckOpaqueStackEffectTable(); + const PrecheckOpaqueStackEffect &Eff = + OpaqueEffects[static_cast(Opcode)]; + if (Eff.Pops == kPrecheckBailSentinel) { + return {}; + } + if (SimStack.size() < Eff.Pops) { + return {}; + } + for (uint8_t I = 0; I < Eff.Pops; ++I) { + SimStack.pop_back(); + } + for (uint8_t I = 0; I < Eff.Pushes; ++I) { + SimStack.push_back(makeUnknownConstU64()); + } + break; + } } } From 2f99a8ff470e3ec82524ddb0ace9e1deae79004b Mon Sep 17 00:00:00 2001 From: Outcry <843648230@qq.com> Date: Wed, 6 May 2026 10:11:52 +0000 Subject: [PATCH 2/2] fix(evm): bail const memory precheck on MSIZE and gas-sensitive opcodes --- src/action/evm_bytecode_visitor.h | 51 +++++++++++++++++++------------ 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/src/action/evm_bytecode_visitor.h b/src/action/evm_bytecode_visitor.h index b8515a3db..fbf18c0ff 100644 --- a/src/action/evm_bytecode_visitor.h +++ b/src/action/evm_bytecode_visitor.h @@ -1240,7 +1240,10 @@ template class EVMByteCodeVisitor { Table[OP_SDIV] = {2, 1}; Table[OP_MOD] = {2, 1}; Table[OP_SMOD] = {2, 1}; - Table[OP_EXP] = {2, 1}; + // OP_EXP is intentionally LEFT at the bail sentinel: its dynamic gas cost + // depends on the exponent byte length and is sensitive to the same gas + // accounting issue described below for storage opcodes + // (stEIP150singleCodeGasPrices.gasCostExp). Table[OP_SIGNEXTEND] = {2, 1}; // Ternary modular arithmetic. Table[OP_ADDMOD] = {3, 1}; @@ -1284,26 +1287,33 @@ template class EVMByteCodeVisitor { Table[OP_BASEFEE] = {0, 1}; Table[OP_BLOBBASEFEE] = {0, 1}; Table[OP_PC] = {0, 1}; - Table[OP_GAS] = {0, 1}; - - // Environment reads that consume one stack input. - Table[OP_BALANCE] = {1, 1}; + // OP_GAS reads the precise remaining-gas balance. Hoisting expandMemoryIR + // to the BB entry charges memory-expansion gas ahead of OP_GAS, polluting + // its observed value (state root mismatch in stEIP150singleCodeGasPrices. + // gasCostExp). Left at bail sentinel. + + // Environment reads that consume one stack input and have STATIC gas. + // EIP-2929-affected opcodes (BALANCE / EXTCODESIZE / EXTCODEHASH) are + // intentionally NOT listed here: their cold/warm gas accounting depends + // on the exact remaining-gas balance at execution time, which hoisted + // memory-expansion gas would perturb. Table[OP_CALLDATALOAD] = {1, 1}; - Table[OP_EXTCODESIZE] = {1, 1}; - Table[OP_EXTCODEHASH] = {1, 1}; Table[OP_BLOCKHASH] = {1, 1}; Table[OP_BLOBHASH] = {1, 1}; - // Storage / transient storage. SLOAD/TLOAD push an opaque value; - // SSTORE/TSTORE only mutate non-memory state, so leaving the abstract - // memory model intact is safe. - Table[OP_SLOAD] = {1, 1}; - Table[OP_TLOAD] = {1, 1}; - Table[OP_SSTORE] = {2, 0}; - Table[OP_TSTORE] = {2, 0}; - - // MCOPY mutates linear memory with non-tracked semantics; left at the - // bail sentinel above so the plan is rejected on encounter. + // Storage / transient storage are LEFT at the bail sentinel: + // * SLOAD / SSTORE / TLOAD / TSTORE charge dynamic gas whose cold/warm + // classification (EIP-2929) and SSTORE refund logic depend on the + // exact remaining-gas balance at execution time. Hoisting memory- + // expansion gas ahead of these opcodes shifts the balance and + // produces wrong state roots (stEIP2930.variedContext failures). + // * SSTORE additionally invokes a host helper that may observe gas. + // + // OP_EXP is also LEFT at the bail sentinel: its dynamic gas depends on + // the exponent byte length and is sensitive to the same accounting issue + // (stEIP150singleCodeGasPrices.gasCostExp). + // + // MCOPY mutates linear memory with non-tracked semantics; also bails. return Table; } @@ -1717,8 +1727,11 @@ template class EVMByteCodeVisitor { break; } case OP_MSIZE: - SimStack.push_back(makeUnknownConstU64()); - break; + // MSIZE observes the precise memory size at the moment it executes. + // Hoisting expandMemoryIR to the BB entry would expand memory ahead + // of MSIZE and pollute its observed value, breaking EVM semantics + // (state root mismatch in vmIOandFlowOperations.msize). Hard-bail. + return {}; default: { if (isBlockTerminatorOpcode(Opcode)) { ScanPC = BytecodeSize;