-
Notifications
You must be signed in to change notification settings - Fork 25
fix(compiler): keep merged jumpdest while charging exact gas in multipass #345
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,7 +9,6 @@ | |
| #include "runtime/evm_instance.h" | ||
| #include "utils/hash_utils.h" | ||
| #include <cstring> | ||
| #include <unordered_set> | ||
|
|
||
| #ifdef ZEN_ENABLE_EVM_GAS_REGISTER | ||
| #include "compiler/llvm-prebuild/Target/X86/X86Subtarget.h" | ||
|
|
@@ -21,6 +20,7 @@ namespace COMPILER { | |
| constexpr uint64_t HashMultiplier = 0x9E3779B97F4A7C15ULL; | ||
| constexpr uint64_t MinHashSize = 5; | ||
| constexpr uint64_t MaxHashSize = 1024; | ||
| constexpr uint32_t InvalidJumpDestRunPC = 0xFFFFFFFFu; | ||
|
|
||
| zen::common::EVMU256Type *EVMFrontendContext::getEVMU256Type() { | ||
| static zen::common::EVMU256Type U256Type; | ||
|
|
@@ -257,7 +257,6 @@ void EVMMirBuilder::initEVM(CompilerContext *Context) { | |
| evmc_get_instruction_names_table(zen::evm::DEFAULT_REVISION); | ||
| } | ||
|
|
||
| createJumpTable(); | ||
| ReturnBB = createBasicBlock(); | ||
| loadEVMInstanceAttr(); | ||
|
|
||
|
|
@@ -269,6 +268,8 @@ void EVMMirBuilder::initEVM(CompilerContext *Context) { | |
| initGasRegister(); | ||
| #endif | ||
|
|
||
| createJumpTable(); | ||
|
|
||
| #ifdef ZEN_ENABLE_LINUX_PERF | ||
| CurBB->setSourceOffset(1); | ||
| CurBB->setSourceName("MAIN_ENTRY"); | ||
|
|
@@ -404,6 +405,56 @@ void EVMMirBuilder::meterOpcode(evmc_opcode Opcode, uint64_t PC) { | |
| meterGas(static_cast<uint64_t>(Metrics.gas_cost)); | ||
| } | ||
|
|
||
| void EVMMirBuilder::meterOpcodeRange(uint64_t StartPC, | ||
| uint64_t EndPCExclusive) { | ||
| if (!Ctx.isGasMeteringEnabled() || StartPC >= EndPCExclusive) { | ||
| return; | ||
| } | ||
|
|
||
| // Fast path for merged consecutive JUMPDEST runs: the skipped cost for | ||
| // [StartPC, EndPCExclusive) is precomputed once when building the jump table. | ||
| if (StartPC < JumpDestRunLastPC.size()) { | ||
| const uint32_t RunLastPC = JumpDestRunLastPC[static_cast<size_t>(StartPC)]; | ||
| if (RunLastPC != InvalidJumpDestRunPC && | ||
| static_cast<uint64_t>(RunLastPC) == EndPCExclusive) { | ||
| meterGas(JumpDestRunSkipCost[static_cast<size_t>(StartPC)]); | ||
| return; | ||
| } | ||
| } | ||
|
|
||
| const auto *EvmCtx = static_cast<const EVMFrontendContext *>(&Ctx); | ||
| const Byte *Bytecode = EvmCtx->getBytecode(); | ||
| if (!Bytecode) { | ||
| return; | ||
| } | ||
|
|
||
| const uint64_t CodeSize = static_cast<uint64_t>(EvmCtx->getBytecodeSize()); | ||
| if (StartPC >= CodeSize) { | ||
| return; | ||
| } | ||
| EndPCExclusive = std::min(EndPCExclusive, CodeSize); | ||
|
|
||
| uint64_t TotalCost = 0; | ||
| for (uint64_t PC = StartPC; PC < EndPCExclusive; ++PC) { | ||
| uint64_t Cost = 0; | ||
| if (GasChunkEnd && GasChunkCost && PC < GasChunkSize && | ||
| GasChunkEnd[PC] > PC) { | ||
| Cost = GasChunkCost[PC]; | ||
| } else { | ||
| const uint8_t Opcode = static_cast<uint8_t>(Bytecode[PC]); | ||
ZR74 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Cost = static_cast<uint64_t>(InstructionMetrics[Opcode].gas_cost); | ||
| } | ||
|
Comment on lines
408
to
446
|
||
|
|
||
| if (UINT64_MAX - TotalCost < Cost) { | ||
| TotalCost = UINT64_MAX; | ||
| break; | ||
| } | ||
| TotalCost += Cost; | ||
| } | ||
|
|
||
| meterGas(TotalCost); | ||
| } | ||
|
|
||
| bool EVMMirBuilder::isOpcodeDefined(evmc_opcode Opcode) const { | ||
| const uint8_t Index = static_cast<uint8_t>(Opcode); | ||
| if (InstructionNames && InstructionNames[Index] != nullptr) { | ||
|
|
@@ -880,30 +931,97 @@ void EVMMirBuilder::createJumpTable() { | |
| const Byte *Bytecode = EvmCtx->getBytecode(); | ||
| size_t BytecodeSize = EvmCtx->getBytecodeSize(); | ||
|
|
||
| uint64_t ConsecutiveStart = 0; | ||
| bool InConsecutive = false; | ||
| JumpDestTable.clear(); | ||
| JumpDestBodyTable.clear(); | ||
| JumpHashTable.clear(); | ||
| JumpHashReverse.clear(); | ||
| HashMask = 0; | ||
| if (Ctx.isGasMeteringEnabled()) { | ||
| JumpDestRunLastPC.assign(BytecodeSize, InvalidJumpDestRunPC); | ||
| JumpDestRunSkipCost.assign(BytecodeSize, 0); | ||
| } else { | ||
| JumpDestRunLastPC.clear(); | ||
| JumpDestRunSkipCost.clear(); | ||
| } | ||
|
|
||
| MBasicBlock *SavedInsertBB = CurBB; | ||
|
|
||
| for (size_t PC = 0; PC < BytecodeSize; ++PC) { | ||
| if (Bytecode[PC] == static_cast<Byte>(evmc_opcode::OP_JUMPDEST)) { | ||
| if (JumpDestTable.count(PC - 1) > 0) { | ||
| // Reuse BB for consecutive JUMPDESTs | ||
| JumpDestTable[PC] = JumpDestTable[PC - 1]; | ||
| // We are in a consecutive sequence, continue tracking | ||
| if (!InConsecutive) { | ||
| ConsecutiveStart = PC - 1; | ||
| InConsecutive = true; | ||
| const size_t RangeStart = PC; | ||
| while (PC + 1 < BytecodeSize && | ||
| Bytecode[PC + 1] == static_cast<Byte>(evmc_opcode::OP_JUMPDEST)) { | ||
| ++PC; | ||
| } | ||
| const size_t RangeEnd = PC; | ||
|
|
||
| // Share one canonical execution block for the whole run. | ||
| MBasicBlock *BodyBB = createBasicBlock(); | ||
| BodyBB->setJumpDestBB(true); | ||
|
|
||
| for (size_t DestPC = RangeStart; DestPC <= RangeEnd; ++DestPC) { | ||
| JumpDestBodyTable[DestPC] = BodyBB; | ||
| } | ||
|
|
||
| if (!Ctx.isGasMeteringEnabled() || RangeStart == RangeEnd) { | ||
| for (size_t DestPC = RangeStart; DestPC <= RangeEnd; ++DestPC) { | ||
| JumpDestTable[DestPC] = BodyBB; | ||
| } | ||
| continue; | ||
| } else { | ||
| // For merged runs, materialize per-target entry thunks that charge the | ||
| // exact skipped metering before entering the shared body. | ||
| // | ||
| // NOTE: We may create O(n) thunks for a run of length n. Avoid an | ||
| // O(n^2) compile-time cost by precomputing the suffix sums of skipped | ||
| // metering once for the run. | ||
| const size_t SkipCount = RangeEnd - RangeStart; // exclude RangeEnd | ||
| const uint64_t JumpDestBaseCost = static_cast<uint64_t>( | ||
| InstructionMetrics[static_cast<uint8_t>(evmc_opcode::OP_JUMPDEST)] | ||
| .gas_cost); | ||
| std::vector<uint64_t> SkipCostByOffset(SkipCount, 0); | ||
| if (SkipCount > 0) { | ||
| uint64_t Running = 0; | ||
| for (size_t Offset = SkipCount; Offset > 0; --Offset) { | ||
| const size_t Pc = RangeStart + (Offset - 1); | ||
| uint64_t Cost = 0; | ||
| if (GasChunkEnd && GasChunkCost && Pc < GasChunkSize && | ||
| GasChunkEnd[Pc] > Pc) { | ||
| Cost = GasChunkCost[Pc]; | ||
| } else { | ||
| // All bytes in the run are JUMPDEST opcode bytes (PUSH payload is | ||
| // skipped in the scan above), so the fallback is a constant. | ||
| Cost = JumpDestBaseCost; | ||
| } | ||
|
|
||
| if (UINT64_MAX - Running < Cost) { | ||
| Running = UINT64_MAX; | ||
| } else { | ||
| Running += Cost; | ||
| } | ||
| SkipCostByOffset[Pc - RangeStart] = Running; | ||
| } | ||
| } | ||
|
|
||
| // Cache the total skipped cost at run start so the linear decode path | ||
| // can reuse it without re-scanning the same consecutive JUMPDEST range. | ||
| if (RangeStart < JumpDestRunLastPC.size()) { | ||
| JumpDestRunLastPC[RangeStart] = static_cast<uint32_t>(RangeEnd); | ||
| JumpDestRunSkipCost[RangeStart] = SkipCostByOffset[0]; | ||
| } | ||
|
|
||
| for (size_t DestPC = RangeStart; DestPC < RangeEnd; ++DestPC) { | ||
| MBasicBlock *EntryBB = createBasicBlock(); | ||
| EntryBB->setJumpDestBB(true); | ||
| JumpDestTable[DestPC] = EntryBB; | ||
|
|
||
| setInsertBlock(EntryBB); | ||
| meterGas(SkipCostByOffset[DestPC - RangeStart]); | ||
| createInstruction<BrInstruction>(true, Ctx, BodyBB); | ||
| addSuccessor(BodyBB); | ||
| } | ||
| JumpDestTable[RangeEnd] = BodyBB; | ||
| } | ||
| MBasicBlock *DestBB = createBasicBlock(); | ||
| DestBB->setJumpDestBB(true); | ||
| JumpDestTable[PC] = DestBB; | ||
| } else { | ||
| // End of consecutive sequence if we were in one | ||
| if (InConsecutive) { | ||
| ConsecutiveJumpDests.push_back({ConsecutiveStart, PC - 1}); | ||
| InConsecutive = false; | ||
| } | ||
| if (static_cast<Byte>(evmc_opcode::OP_PUSH0) <= Bytecode[PC] && | ||
| Bytecode[PC] <= static_cast<Byte>(evmc_opcode::OP_PUSH32)) { | ||
| uint8_t PushSize = static_cast<uint8_t>(Bytecode[PC]) + 1 - | ||
|
|
@@ -912,18 +1030,15 @@ void EVMMirBuilder::createJumpTable() { | |
| } | ||
| } | ||
| } | ||
| // Handle case where consecutive sequence extends to end of bytecode | ||
| if (InConsecutive) { | ||
| ConsecutiveJumpDests.push_back({ConsecutiveStart, BytecodeSize - 1}); | ||
| } | ||
|
|
||
| setInsertBlock(SavedInsertBB); | ||
|
|
||
| // If the size of JumpDests is greater than MinHashSize, create a hash table | ||
| // which calculates the hash of DestPC and use it as the index to jump | ||
| if (JumpDestTable.size() > MinHashSize) { | ||
| uint64_t HashSize = | ||
| std::min(nextPowerOfTwo(JumpDestTable.size()), MaxHashSize); | ||
| HashMask = HashSize - 1; | ||
| std::vector<std::vector<MBasicBlock *>> HashDests(HashSize); | ||
| for (const auto &[DestPC, DestBB] : JumpDestTable) { | ||
| // HashIndex(a) = (a * HashMultiplier) & (size - 1) | ||
| uint64_t Index = (DestPC * HashMultiplier) & HashMask; | ||
|
|
@@ -936,13 +1051,6 @@ void EVMMirBuilder::createJumpTable() { | |
| void EVMMirBuilder::implementConstantJump(uint64_t ConstDest, | ||
| MBasicBlock *FailureBB) { | ||
| if (JumpDestTable.count(ConstDest)) { | ||
| // Check if ConstDest falls within a consecutive JUMPDEST range | ||
| for (const auto &[StartPC, EndPC] : ConsecutiveJumpDests) { | ||
| if (ConstDest >= StartPC && ConstDest < EndPC) { | ||
| meterGas(EndPC - ConstDest); | ||
| break; | ||
| } | ||
| } | ||
| createInstruction<BrInstruction>(true, Ctx, JumpDestTable[ConstDest]); | ||
| addSuccessor(JumpDestTable[ConstDest]); | ||
| } else { | ||
|
|
@@ -960,55 +1068,6 @@ void EVMMirBuilder::implementIndirectJump(MInstruction *JumpTarget, | |
| } | ||
| HasIndirectJump = true; | ||
|
|
||
| // Check if JumpTarget falls within any consecutive JUMPDEST range | ||
| // and charge gas for skipped JUMPDESTs | ||
| if (!ConsecutiveJumpDests.empty() && Ctx.isGasMeteringEnabled()) { | ||
| MType *I64Type = &Ctx.I64Type; | ||
| MBasicBlock *FinalBB = createBasicBlock(); | ||
|
|
||
| for (const auto &[StartPC, EndPC] : ConsecutiveJumpDests) { | ||
| MBasicBlock *NextCheckBB = createBasicBlock(); | ||
| MBasicBlock *ChargeGasBB = createBasicBlock(); | ||
|
|
||
| // Check if JumpTarget >= StartPC | ||
| MInstruction *StartPCConst = createIntConstInstruction(I64Type, StartPC); | ||
| MInstruction *IsGEStart = createInstruction<CmpInstruction>( | ||
| false, CmpInstruction::Predicate::ICMP_UGE, I64Type, JumpTarget, | ||
| StartPCConst); | ||
|
|
||
| // Check if JumpTarget < EndPC | ||
| MInstruction *EndPCConst = createIntConstInstruction(I64Type, EndPC); | ||
| MInstruction *IsLTEnd = createInstruction<CmpInstruction>( | ||
| false, CmpInstruction::Predicate::ICMP_ULT, I64Type, JumpTarget, | ||
| EndPCConst); | ||
|
|
||
| // Combine: JumpTarget >= StartPC && JumpTarget < EndPC | ||
| MInstruction *InRange = createInstruction<BinaryInstruction>( | ||
| false, OP_and, I64Type, IsGEStart, IsLTEnd); | ||
|
|
||
| createInstruction<BrIfInstruction>(true, Ctx, InRange, ChargeGasBB, | ||
| NextCheckBB); | ||
| addSuccessor(ChargeGasBB); | ||
| addSuccessor(NextCheckBB); | ||
|
|
||
| // ChargeGasBB: charge gas for (EndPC - JumpTarget) | ||
| setInsertBlock(ChargeGasBB); | ||
| MInstruction *GasCost = createInstruction<BinaryInstruction>( | ||
| false, OP_sub, I64Type, EndPCConst, JumpTarget); | ||
| chargeDynamicGasIR(GasCost); | ||
| createInstruction<BrInstruction>(true, Ctx, FinalBB); | ||
| addSuccessor(FinalBB); | ||
|
|
||
| // Continue to next check | ||
| setInsertBlock(NextCheckBB); | ||
| } | ||
|
|
||
| // After all checks, branch to FinalBB | ||
| createInstruction<BrInstruction>(true, Ctx, FinalBB); | ||
| addSuccessor(FinalBB); | ||
| setInsertBlock(FinalBB); | ||
| } | ||
|
|
||
| MBasicBlock *TargetBB = getOrCreateIndirectJumpBB(); | ||
| createInstruction<DassignInstruction>(true, &(Ctx.VoidType), JumpTarget, | ||
| JumpTargetVar->getVarIdx()); | ||
|
|
@@ -1166,7 +1225,9 @@ void EVMMirBuilder::handleJumpI(Operand Dest, Operand Cond) { | |
| } | ||
|
|
||
| void EVMMirBuilder::handleJumpDest(const uint64_t &PC) { | ||
| MBasicBlock *DestBB = JumpDestTable.at(PC); | ||
| auto BodyIt = JumpDestBodyTable.find(PC); | ||
| ZEN_ASSERT(BodyIt != JumpDestBodyTable.end() && "JUMPDEST body not found"); | ||
| MBasicBlock *DestBB = BodyIt->second; | ||
| // Only add successor if the current BB is not ExceptionSetBB, | ||
| bool IsExceptionSetBB = false; | ||
| for (auto &[EC, BB] : CurFunc->getExceptionSetBBs()) { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this
meterOpcodeRangefunction change enough to fix the gas calculation for jumpdest? Do you have a bytecode to confirm this?I heard that consecutive JUMPDEST opcodes are generally NOT common in well-optimized EVM bytecode.
Are all other changes in this PRs necessary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Previously, pr merged consecutive jumpdest values and assumed that all jumpdest values were equivalent to deducting 1. However, in reality, SPP charges for jumpdest based on a cost of 0, 1, or greater. Therefore, the corresponding GasChunkCost should be deducted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand, my question is that are other changes like the
JumpDestBodyTablerelated to this fix?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I understand
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it separates "the shared main block BodyBB that the linear decode should enter when encountering JUMPDEST" from "the compensation entry thunk (JumpDestTable) that might need to be deducted before the jump/jumpi jump" to avoid linear execution entering the thunk by mistake, which could lead to disorder in instruction insertion or repeated deduction.