Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions core/vm/evm.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,10 @@ type EVM struct {
readOnly bool // Whether to throw on stateful modifications
returnData []byte // Last CALL's return data for subsequent reuse

// mulmodMemo caches the reciprocal of the last full-width MULMOD modulus so
// that repeated moduli skip uint256.MulMod's internal Reciprocal recompute.
mulmodMemo modReciprocal

// interrupt is the block-building timeout flag. When set, the interpreter
// checks it on every opcode across all call depths (Call, DelegateCall,
// StaticCall, CallCode, Create). Use SetInterrupt to configure.
Expand Down
2 changes: 1 addition & 1 deletion core/vm/gen_dispatch/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ var opcodes = []opDef{
{name: "ADDMOD", gas: "GasMidStep", mode: modeAccumulate, shape: shapeTernaryOp, funcName: "opAddmod",
body: "x := stack.data[stack.top+1]\ny := stack.data[stack.top]\nz := &stack.data[stack.top-1]\nz.AddMod(&x, &y, z)"},
{name: "MULMOD", gas: "GasMidStep", mode: modeAccumulate, shape: shapeTernaryOp, funcName: "opMulmod",
body: "x := stack.data[stack.top+1]\ny := stack.data[stack.top]\nz := &stack.data[stack.top-1]\nz.MulMod(&x, &y, z)"},
body: "x := stack.data[stack.top+1]\ny := stack.data[stack.top]\nz := &stack.data[stack.top-1]\nevm.mulmodMemo.mulmod(&x, &y, z)"},
{name: "SIGNEXTEND", gas: "GasFastStep", mode: modeAccumulate, shape: shapeBinaryOp, funcName: "opSignextend",
body: "back := stack.data[stack.top]\nnum := &stack.data[stack.top-1]\nnum.ExtendSign(num, &back)"},

Expand Down
2 changes: 1 addition & 1 deletion core/vm/instructions.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ func opAddmod(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {

func opMulmod(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
x, y, z := scope.Stack.pop(), scope.Stack.pop(), scope.Stack.peek()
z.MulMod(&x, &y, z)
evm.mulmodMemo.mulmod(&x, &y, z)

return nil, nil
}
Expand Down
2 changes: 1 addition & 1 deletion core/vm/interpreter_dispatch.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

66 changes: 66 additions & 0 deletions core/vm/mulmod_memo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright 2026 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.

package vm

import "github.com/holiman/uint256"

// modReciprocal memoizes uint256.Reciprocal for the last full-width (4-word)
// MULMOD modulus seen by an EVM instance.
//
// uint256.MulMod, for a full-width modulus, computes Reciprocal(m) internally
// and then reduces with it; the reciprocal is ~60% of that call's cost and is
// thrown away every time. A common on-chain pattern is a long run of modular
// multiplications against a single fixed-prime modulus (field arithmetic used
// broadly by cryptographic and verification contracts), so a one-entry memo of
// (modulus, reciprocal) hits almost always and elides the recompute.
//
// No synchronization: the memo is per-EVM scratch state, like depth and
// returnData. The EVM "should never be reused and is not thread safe" (see the
// EVM type doc); every construction site builds a fresh instance and each
// executes on a single goroutine, so an unsynchronized field is safe here.
type modReciprocal struct {
mod uint256.Int
mu [5]uint64
ok bool
}

// mulmod computes z = (x * y) % z in place, mirroring the exact semantics of
// z.MulMod(x, y, z) — including m == 0 yielding 0 — but reuses the memoized
// reciprocal when the modulus repeats.
//
// Correctness is a library guarantee, not a re-derivation: MulModWithReciprocal
// with mu == Reciprocal(m) produces the identical result to MulMod, because
// MulMod computes exactly that reciprocal and reduction internally. The memo
// only changes how the reciprocal is obtained (cached vs recomputed), never the
// arithmetic — the output is a pure function of (x, y, m), independent of cache
// state. On a miss we recompute Reciprocal exactly as stock would.
//
// Only full-width (z[3] != 0) moduli take the memo path; narrower moduli — and
// m == 0 — never reach the reciprocal reduction inside MulMod, so they fall
// through to the stock call unchanged.
func (mm *modReciprocal) mulmod(x, y, z *uint256.Int) {
if z[3] != 0 {
if !mm.ok || mm.mod != *z {
mm.mod = *z
mm.mu = uint256.Reciprocal(z)
mm.ok = true
}
z.MulModWithReciprocal(x, y, z, &mm.mu)
return
}
z.MulMod(x, y, z)
}
Loading
Loading