Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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/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