diff --git a/core/vm/contracts.go b/core/vm/contracts.go index 0b244f988e..fbd83595b3 100644 --- a/core/vm/contracts.go +++ b/core/vm/contracts.go @@ -286,19 +286,18 @@ func RunPrecompiledContract(stateDB StateDB, p PrecompiledContract, address comm // Serve pure precompiles from the shared result cache if one is attached. // Gas accounting and state touching above are identical on hit and miss, // only the recomputation is skipped. - if cache != nil && cacheablePrecompile(p, input) { - var ( - set = activePrecompiledContracts(rules) - key = precompileCacheKey(address, input) - ) - if output, ok := cache.load(set, address, key); ok { - return output, gas, nil + if cache != nil { + if key, ok := precompileCacheKey(p, input); ok { + scope := precompileCacheScope{activePrecompiledContracts(rules), address} + if output, ok := cache.load(scope, key); ok { + return output, gas, nil + } + output, err := p.Run(input) + if err == nil && len(output) <= maxCacheablePrecompileOutput { + cache.store(scope, key, output) + } + return output, gas, err } - output, err := p.Run(input) - if err == nil && len(output) <= maxCacheablePrecompileOutput { - cache.store(set, address, key, output) - } - return output, gas, err } output, err := p.Run(input) return output, gas, err @@ -307,13 +306,15 @@ func RunPrecompiledContract(stateDB StateDB, p PrecompiledContract, address comm // ecrecover implemented as a native contract. type ecrecover struct{} +// ecRecoverInputLength is the number of input bytes ecrecover reads, shorter +// inputs are right padded and longer ones are ignored past this point. +const ecRecoverInputLength = 128 + func (c *ecrecover) RequiredGas(input []byte) uint64 { return params.EcrecoverGas } func (c *ecrecover) Run(input []byte) ([]byte, error) { - const ecRecoverInputLength = 128 - input = common.RightPadBytes(input, ecRecoverInputLength) // "input" is (hash, v, r, s), each 32 bytes // but for ecrecover we want (r, s, v) @@ -346,6 +347,13 @@ func (c *ecrecover) Name() string { return "ECREC" } +func (c *ecrecover) Cacheable() bool { return true } + +// NormalizeInput drops everything past the bytes Run reads. +func (c *ecrecover) NormalizeInput(input []byte) ([]byte, bool) { + return normalizeZeroPadded(input, ecRecoverInputLength), true +} + // SHA256 implemented as a native contract. type sha256hash struct{} @@ -365,9 +373,7 @@ func (c *sha256hash) Name() string { return "SHA256" } -// Cacheable opts out of result caching, deriving the cache key costs about -// as much as running the hash itself. -func (c *sha256hash) Cacheable() bool { return false } +func (c *sha256hash) Cacheable() bool { return true } // RIPEMD160 implemented as a native contract. type ripemd160hash struct{} @@ -389,9 +395,7 @@ func (c *ripemd160hash) Name() string { return "RIPEMD160" } -// Cacheable opts out of result caching, hashing the input for the cache key -// costs about as much as running it. -func (c *ripemd160hash) Cacheable() bool { return false } +func (c *ripemd160hash) Cacheable() bool { return true } // data copy implemented as a native contract. type dataCopy struct{} @@ -411,10 +415,6 @@ func (c *dataCopy) Name() string { return "ID" } -// Cacheable opts out of result caching, identity is cheaper to rerun than -// to cache. -func (c *dataCopy) Cacheable() bool { return false } - // bigModExp implements a native big integer exponential modular operation. type bigModExp struct { eip2565 bool @@ -598,6 +598,10 @@ func osakaModexpGas(baseLen, expLen, modLen uint64, expHead uint256.Int) uint64 return max(gas, minGas) } +// modExpHeaderLength is the size of the modexp header holding the base, +// exponent and modulus lengths. +const modExpHeaderLength = 96 + // RequiredGas returns the gas required to execute the pre-compiled contract. func (c *bigModExp) RequiredGas(input []byte) uint64 { // Parse input lengths @@ -682,7 +686,7 @@ func (c *bigModExp) Run(input []byte) ([]byte, error) { // Modulo 0 is undefined, return zero return common.LeftPadBytes([]byte{}, int(modLen)), nil case base.BitLen() == 1: // a bit length of 1 means it's 1 (or -1). - //If base == 1, then we can just return base % mod (if mod >= 1, which it is) + // If base == 1, then we can just return base % mod (if mod >= 1, which it is) v = base.Mod(base, mod).Bytes() default: v = base.Exp(base, exp, mod).Bytes() @@ -694,6 +698,39 @@ func (c *bigModExp) Name() string { return "MODEXP" } +func (c *bigModExp) Cacheable() bool { return true } + +// NormalizeInput drops everything past the operands the header declares. +func (c *bigModExp) NormalizeInput(input []byte) ([]byte, bool) { + if len(input) <= modExpHeaderLength { + return input, true + } + var ( + baseLen = new(uint256.Int).SetBytes(getData(input, 0, 32)) + expLen = new(uint256.Int).SetBytes(getData(input, 32, 32)) + modLen = new(uint256.Int).SetBytes(getData(input, 64, 32)) + ) + // A length past what Run can address is not something the header alone + // decides. Run truncates it to its low word rather than rejecting it, so it + // goes on to read operands, and gas does not always price that out of reach. + // Nobody can pay to run these, so skip them rather than reason about them. + if !baseLen.IsUint64() || !expLen.IsUint64() || !modLen.IsUint64() { + return nil, false + } + // With no base and no modulus, nothing past the header changes the outcome, + // whether that is an empty output or an Osaka length failure. + if baseLen.IsZero() && modLen.IsZero() { + return input[:modExpHeaderLength], true + } + end := new(uint256.Int).AddUint64(baseLen, modExpHeaderLength) + end.Add(end, expLen) + end.Add(end, modLen) + if !end.IsUint64() || end.Uint64() >= uint64(len(input)) { + return input, true + } + return input[:end.Uint64()], true +} + // newCurvePoint unmarshals a binary blob into a bn256 elliptic curve point, // returning it, or an error if the point is invalid. func newCurvePoint(blob []byte) (*bn256.G1, error) { @@ -714,6 +751,10 @@ func newTwistPoint(blob []byte) (*bn256.G2, error) { return p, nil } +// bn256AddInputLength is the number of input bytes runBn256Add reads, shorter +// inputs are zero padded and longer ones are ignored past this point. +const bn256AddInputLength = 128 + // runBn256Add implements the Bn256Add precompile, referenced by both // Byzantium and Istanbul operations. func runBn256Add(input []byte) ([]byte, error) { @@ -747,6 +788,13 @@ func (c *bn256AddIstanbul) Name() string { return "BN254_ADD" } +func (c *bn256AddIstanbul) Cacheable() bool { return true } + +// NormalizeInput drops everything past the bytes runBn256Add reads. +func (c *bn256AddIstanbul) NormalizeInput(input []byte) ([]byte, bool) { + return normalizeZeroPadded(input, bn256AddInputLength), true +} + // bn256AddByzantium implements a native elliptic curve point addition // conforming to Byzantium consensus rules. type bn256AddByzantium struct{} @@ -764,6 +812,18 @@ func (c *bn256AddByzantium) Name() string { return "BN254_ADD" } +func (c *bn256AddByzantium) Cacheable() bool { return true } + +// NormalizeInput drops everything past the bytes runBn256Add reads. +func (c *bn256AddByzantium) NormalizeInput(input []byte) ([]byte, bool) { + return normalizeZeroPadded(input, bn256AddInputLength), true +} + +// bn256ScalarMulInputLength is the number of input bytes runBn256ScalarMul +// reads, shorter inputs are zero padded and longer ones are ignored past this +// point. +const bn256ScalarMulInputLength = 96 + // runBn256ScalarMul implements the Bn256ScalarMul precompile, referenced by // both Byzantium and Istanbul operations. func runBn256ScalarMul(input []byte) ([]byte, error) { @@ -793,6 +853,13 @@ func (c *bn256ScalarMulIstanbul) Name() string { return "BN254_MUL" } +func (c *bn256ScalarMulIstanbul) Cacheable() bool { return true } + +// NormalizeInput drops everything past the bytes runBn256ScalarMul reads. +func (c *bn256ScalarMulIstanbul) NormalizeInput(input []byte) ([]byte, bool) { + return normalizeZeroPadded(input, bn256ScalarMulInputLength), true +} + // bn256ScalarMulByzantium implements a native elliptic curve scalar // multiplication conforming to Byzantium consensus rules. type bn256ScalarMulByzantium struct{} @@ -810,6 +877,13 @@ func (c *bn256ScalarMulByzantium) Name() string { return "BN254_MUL" } +func (c *bn256ScalarMulByzantium) Cacheable() bool { return true } + +// NormalizeInput drops everything past the bytes runBn256ScalarMul reads. +func (c *bn256ScalarMulByzantium) NormalizeInput(input []byte) ([]byte, bool) { + return normalizeZeroPadded(input, bn256ScalarMulInputLength), true +} + var ( // true32Byte is returned if the bn256 pairing check succeeds. true32Byte = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1} @@ -869,6 +943,13 @@ func (c *bn256PairingIstanbul) Name() string { return "BN254_PAIRING" } +func (c *bn256PairingIstanbul) Cacheable() bool { return true } + +// NormalizeInput skips inputs Run rejects on length. +func (c *bn256PairingIstanbul) NormalizeInput(input []byte) ([]byte, bool) { + return input, len(input)%192 == 0 +} + // bn256PairingByzantium implements a pairing pre-compile for the bn256 curve // conforming to Byzantium consensus rules. type bn256PairingByzantium struct{} @@ -886,6 +967,13 @@ func (c *bn256PairingByzantium) Name() string { return "BN254_PAIRING" } +func (c *bn256PairingByzantium) Cacheable() bool { return true } + +// NormalizeInput skips inputs Run rejects on length. +func (c *bn256PairingByzantium) NormalizeInput(input []byte) ([]byte, bool) { + return input, len(input)%192 == 0 +} + type blake2F struct{} func (c *blake2F) RequiredGas(input []byte) uint64 { @@ -951,6 +1039,13 @@ func (c *blake2F) Name() string { return "BLAKE2F" } +func (c *blake2F) Cacheable() bool { return true } + +// NormalizeInput skips inputs Run rejects on length. +func (c *blake2F) NormalizeInput(input []byte) ([]byte, bool) { + return input, len(input) == blake2FInputLength +} + var ( errBLS12381InvalidInputLength = errors.New("invalid input length") errBLS12381InvalidFieldElementTopBytes = errors.New("invalid field element top bytes") @@ -998,6 +1093,13 @@ func (c *bls12381G1Add) Name() string { return "BLS12_G1ADD" } +func (c *bls12381G1Add) Cacheable() bool { return true } + +// NormalizeInput skips inputs Run rejects on length. +func (c *bls12381G1Add) NormalizeInput(input []byte) ([]byte, bool) { + return input, len(input) == 256 +} + // bls12381G1MultiExp implements EIP-2537 G1MultiExp precompile. type bls12381G1MultiExp struct{} @@ -1062,6 +1164,13 @@ func (c *bls12381G1MultiExp) Name() string { return "BLS12_G1MSM" } +func (c *bls12381G1MultiExp) Cacheable() bool { return true } + +// NormalizeInput skips inputs Run rejects on length. +func (c *bls12381G1MultiExp) NormalizeInput(input []byte) ([]byte, bool) { + return input, len(input) != 0 && len(input)%160 == 0 +} + // bls12381G2Add implements EIP-2537 G2Add precompile. type bls12381G2Add struct{} @@ -1103,6 +1212,13 @@ func (c *bls12381G2Add) Name() string { return "BLS12_G2ADD" } +func (c *bls12381G2Add) Cacheable() bool { return true } + +// NormalizeInput skips inputs Run rejects on length. +func (c *bls12381G2Add) NormalizeInput(input []byte) ([]byte, bool) { + return input, len(input) == 512 +} + // bls12381G2MultiExp implements EIP-2537 G2MultiExp precompile. type bls12381G2MultiExp struct{} @@ -1167,6 +1283,13 @@ func (c *bls12381G2MultiExp) Name() string { return "BLS12_G2MSM" } +func (c *bls12381G2MultiExp) Cacheable() bool { return true } + +// NormalizeInput skips inputs Run rejects on length. +func (c *bls12381G2MultiExp) NormalizeInput(input []byte) ([]byte, bool) { + return input, len(input) != 0 && len(input)%288 == 0 +} + // bls12381Pairing implements EIP-2537 Pairing precompile. type bls12381Pairing struct{} @@ -1234,6 +1357,13 @@ func (c *bls12381Pairing) Name() string { return "BLS12_PAIRING_CHECK" } +func (c *bls12381Pairing) Cacheable() bool { return true } + +// NormalizeInput skips inputs Run rejects on length. +func (c *bls12381Pairing) NormalizeInput(input []byte) ([]byte, bool) { + return input, len(input) != 0 && len(input)%384 == 0 +} + func decodePointG1(in []byte) (*bls12381.G1Affine, error) { if len(in) != 128 { return nil, errors.New("invalid g1 point length") @@ -1356,6 +1486,13 @@ func (c *bls12381MapG1) Name() string { return "BLS12_MAP_FP_TO_G1" } +func (c *bls12381MapG1) Cacheable() bool { return true } + +// NormalizeInput skips inputs Run rejects on length. +func (c *bls12381MapG1) NormalizeInput(input []byte) ([]byte, bool) { + return input, len(input) == 64 +} + // bls12381MapG2 implements EIP-2537 MapG2 precompile. type bls12381MapG2 struct{} @@ -1393,6 +1530,13 @@ func (c *bls12381MapG2) Name() string { return "BLS12_MAP_FP2_TO_G2" } +func (c *bls12381MapG2) Cacheable() bool { return true } + +// NormalizeInput skips inputs Run rejects on length. +func (c *bls12381MapG2) NormalizeInput(input []byte) ([]byte, bool) { + return input, len(input) == 128 +} + // kzgPointEvaluation implements the EIP-4844 point evaluation precompile. type kzgPointEvaluation struct{} @@ -1453,6 +1597,13 @@ func (b *kzgPointEvaluation) Name() string { return "KZG_POINT_EVALUATION" } +func (b *kzgPointEvaluation) Cacheable() bool { return true } + +// NormalizeInput skips inputs Run rejects on length. +func (b *kzgPointEvaluation) NormalizeInput(input []byte) ([]byte, bool) { + return input, len(input) == blobVerifyInputLength +} + // kZGToVersionedHash implements kzg_to_versioned_hash from EIP-4844 func kZGToVersionedHash(kzg kzg4844.Commitment) common.Hash { h := sha256.Sum256(kzg[:]) @@ -1470,9 +1621,11 @@ func (c *p256Verify) RequiredGas(input []byte) uint64 { return params.P256VerifyGas } +// p256VerifyInputLength is the only input length p256Verify accepts. +const p256VerifyInputLength = 160 + // Run executes the precompiled contract with given 160 bytes of param, returning the output and the used gas func (c *p256Verify) Run(input []byte) ([]byte, error) { - const p256VerifyInputLength = 160 if len(input) != p256VerifyInputLength { return nil, nil } @@ -1492,3 +1645,10 @@ func (c *p256Verify) Run(input []byte) ([]byte, error) { func (c *p256Verify) Name() string { return "P256VERIFY" } + +func (c *p256Verify) Cacheable() bool { return true } + +// NormalizeInput skips inputs Run rejects on length. +func (c *p256Verify) NormalizeInput(input []byte) ([]byte, bool) { + return input, len(input) == p256VerifyInputLength +} diff --git a/core/vm/precompile_cache.go b/core/vm/precompile_cache.go index b725d30bc4..3045c38f68 100644 --- a/core/vm/precompile_cache.go +++ b/core/vm/precompile_cache.go @@ -22,7 +22,6 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/lru" - "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/metrics" ) @@ -31,29 +30,30 @@ var ( precompileCacheMissMeter = metrics.NewRegisteredMeter("chain/cache/precompile/miss", nil) precompileCachePrefetchHitMeter = metrics.NewRegisteredMeter("chain/cache/precompile/prefetch/hit", nil) precompileCachePrefetchMissMeter = metrics.NewRegisteredMeter("chain/cache/precompile/prefetch/miss", nil) - precompileCacheEntryGauge = metrics.NewRegisteredGauge("chain/cache/precompile/entries", nil) ) const ( - // maxCacheablePrecompileInput bounds the input size eligible for result - // caching. Larger inputs are rare one-offs and hashing them for the key - // eats into the win. + // maxCacheablePrecompileInput bounds the normalized input size eligible for + // result caching. The key is the input itself, so this also bounds how much + // an entry can cost. maxCacheablePrecompileInput = 8192 // maxCacheablePrecompileOutput bounds the output size stored in the // cache, keeping the worst case memory use of an entry small. maxCacheablePrecompileOutput = 1024 - // precompileCacheEntries is the maximum number of cached results. With - // outputs capped by maxCacheablePrecompileOutput, the worst case memory - // use stays at a few megabytes. - precompileCacheEntries = 4096 + // maxCacheablePrecompileBytes is the budget each precompile gets per fork, + // counting keys and values along with what an entry costs to hold. Entries + // run from tens of bytes to kilobytes, so a budget in entries would mean + // very different memory depending on the mix. + maxCacheablePrecompileBytes = 1024 * 1024 ) -// PrecompileCache is a thread-safe LRU of precompile outputs, shared between -// the state prefetcher and block processing so the serial pass can reuse -// results the prefetcher already computed. Entries are namespaced by -// precompile set, so forks never share results across a behaviour change. +// PrecompileCache is a thread-safe cache of precompile outputs, shared between +// the state prefetcher and block processing so the serial pass can reuse what +// the prefetcher already computed. Each precompile gets its own cache per fork, +// so results never cross a repricing and a cheap precompile cannot evict the +// results of an expensive one. type PrecompileCache struct { data *precompileCacheData @@ -69,8 +69,15 @@ type PrecompileCache struct { // precompileCacheData is the storage shared by the two cache handles. type precompileCacheData struct { - mu sync.RWMutex - sets map[*PrecompiledContracts]*lru.Cache[common.Hash, []byte] + mu sync.RWMutex + caches map[precompileCacheScope]*lru.SizeConstrainedCache[string, []byte] +} + +// precompileCacheScope identifies the cache of one precompile at one fork. The +// set pointer keeps forks apart, the address keeps precompiles apart. +type precompileCacheScope struct { + set *PrecompiledContracts + addr common.Address } // precompileCacheMeters holds the per-address hit and miss meters. @@ -82,7 +89,7 @@ type precompileCacheMeters struct { // NewPrecompileCache constructs a precompile result cache. func NewPrecompileCache() *PrecompileCache { data := &precompileCacheData{ - sets: make(map[*PrecompiledContracts]*lru.Cache[common.Hash, []byte]), + caches: make(map[precompileCacheScope]*lru.SizeConstrainedCache[string, []byte]), } return &PrecompileCache{ data: data, @@ -90,6 +97,7 @@ func NewPrecompileCache() *PrecompileCache { hit: precompileCacheHitMeter, miss: precompileCacheMissMeter, meters: make(map[common.Address]*precompileCacheMeters), + prefetch: &PrecompileCache{ data: data, prefix: "chain/cache/precompile/prefetch", @@ -111,14 +119,14 @@ func (c *PrecompileCache) PrefetchView() *PrecompileCache { // load retrieves the cached output for the given key. The returned slice is // a private copy owned by the caller, entries cross goroutine boundaries. -func (c *PrecompileCache) load(set *PrecompiledContracts, addr common.Address, key common.Hash) ([]byte, bool) { +func (c *PrecompileCache) load(scope precompileCacheScope, key []byte) ([]byte, bool) { c.data.mu.RLock() - results := c.data.sets[set] + results := c.data.caches[scope] c.data.mu.RUnlock() - meters := c.metersFor(addr) + meters := c.metersFor(scope.addr) if results != nil { - if output, ok := results.Get(key); ok { + if output, ok := results.Get(string(key)); ok { c.hit.Mark(1) meters.hit.Mark(1) return common.CopyBytes(output), true @@ -129,23 +137,24 @@ func (c *PrecompileCache) load(set *PrecompiledContracts, addr common.Address, k return nil, false } -// store saves the output of a precompile run under the given key. The value -// is copied, the cache never aliases caller memory. -func (c *PrecompileCache) store(set *PrecompiledContracts, addr common.Address, key common.Hash, output []byte) { +// store saves the output of a precompile run under the given key. Both the key +// and the value are copied, the cache never aliases caller memory. That matters +// for the key in particular, it aliases the caller's memory which the EVM goes +// on to overwrite. +func (c *PrecompileCache) store(scope precompileCacheScope, key []byte, output []byte) { c.data.mu.RLock() - results := c.data.sets[set] + results := c.data.caches[scope] c.data.mu.RUnlock() if results == nil { c.data.mu.Lock() - if results = c.data.sets[set]; results == nil { - results = lru.NewCache[common.Hash, []byte](precompileCacheEntries) - c.data.sets[set] = results + if results = c.data.caches[scope]; results == nil { + results = lru.NewSizeConstrainedCache[string, []byte](maxCacheablePrecompileBytes) + c.data.caches[scope] = results } c.data.mu.Unlock() } - results.Add(key, common.CopyBytes(output)) - precompileCacheEntryGauge.Update(int64(results.Len())) + results.Add(string(key), common.CopyBytes(output)) } // metersFor returns the hit and miss meters of the given precompile address, @@ -171,28 +180,53 @@ func (c *PrecompileCache) metersFor(addr common.Address) *precompileCacheMeters return meters } -// CacheablePrecompile lets a precompile opt out of result caching, either -// because its output is not a pure function of the input or because it is -// cheaper to rerun than to cache. +// CacheablePrecompile is implemented by precompiles that opt in to result +// caching. Anything that does not implement it is never cached, so a new +// precompile is not enrolled until someone decides it should be. type CacheablePrecompile interface { Cacheable() bool } -// cacheablePrecompile reports whether an invocation is eligible for result -// caching. -func cacheablePrecompile(p PrecompiledContract, input []byte) bool { - if len(input) > maxCacheablePrecompileInput { - return false - } - if c, ok := p.(CacheablePrecompile); ok { - return c.Cacheable() - } - return true +// NormalizingPrecompile is implemented by precompiles that can narrow an input +// down to the bytes that determine the result. +type NormalizingPrecompile interface { + // NormalizeInput returns the bytes identifying the result, and whether the + // invocation is cacheable at all. Two inputs that normalize alike share an + // entry, so they must run to the same output. Returning false skips the + // cache, which is how a precompile rejects lengths it will fail on. + NormalizeInput(input []byte) ([]byte, bool) } -// precompileCacheKey derives the cache key for a precompile invocation. Fork -// discrimination is handled by the set namespacing, so the key only covers -// the address and input. -func precompileCacheKey(addr common.Address, input []byte) common.Hash { - return crypto.Keccak256Hash(addr[:], input) +// precompileCacheKey returns the key identifying an invocation and whether it +// is eligible for result caching. +func precompileCacheKey(p PrecompiledContract, input []byte) ([]byte, bool) { + c, ok := p.(CacheablePrecompile) + if !ok || !c.Cacheable() { + return nil, false + } + key := input + if n, ok := p.(NormalizingPrecompile); ok { + if key, ok = n.NormalizeInput(input); !ok { + return nil, false + } + } + if len(key) > maxCacheablePrecompileInput { + return nil, false + } + return key, true +} + +// normalizeZeroPadded narrows an input for a precompile that reads a fixed +// length prefix and zero extends anything shorter. Bytes past the prefix are +// never read, and trailing zeros inside it read the same as not being there, +// so both can be dropped from the key. +func normalizeZeroPadded(input []byte, prefix int) []byte { + if len(input) > prefix { + input = input[:prefix] + } + end := len(input) + for end > 0 && input[end-1] == 0 { + end-- + } + return input[:end] } diff --git a/core/vm/precompile_cache_test.go b/core/vm/precompile_cache_test.go new file mode 100644 index 0000000000..75f18d68a7 --- /dev/null +++ b/core/vm/precompile_cache_test.go @@ -0,0 +1,433 @@ +// 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 . + +package vm + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "fmt" + "math/rand" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/params" +) + +// allPrecompileSets returns every set a node can run with, labelled by a fork +// that selects it. It walks the flags of params.Rules and asks +// activePrecompiledContracts what each one activates, so a set added for a new +// fork is covered here without anyone remembering to list it. +func allPrecompileSets() map[string]PrecompiledContracts { + var ( + rules = reflect.TypeOf(params.Rules{}) + seen = make(map[*PrecompiledContracts]bool) + sets = make(map[string]PrecompiledContracts) + ) + // The zero value picks whatever the switch falls through to. + base := activePrecompiledContracts(params.Rules{}) + seen[base], sets["default"] = true, *base + + for i := range rules.NumField() { + field := rules.Field(i) + if field.Type.Kind() != reflect.Bool { + continue + } + // Activate only this field so the fork resolves to the set it gates. + var forked params.Rules + reflect.ValueOf(&forked).Elem().Field(i).SetBool(true) + + // Later forks shadow earlier ones, so the first flag reaching a set is + // the one that names it. + if set := activePrecompiledContracts(forked); !seen[set] { + seen[set], sets[strings.TrimPrefix(field.Name, "Is")] = true, *set + } + } + return sets +} + +// probeGasLimit is a generous stand-in for the block gas limit, bounding the +// probe corpus to invocations the EVM could actually pay to run. +const probeGasLimit = 1 << 30 + +// cacheProbeInputs builds inputs that stress normalization: the lengths each +// precompile cares about, either side of them, zero padded and non-zero padded +// variants, and a few random ones. +// +// Random bytes alone are not enough. They fail every signature and curve check, +// so a precompile whose failure path returns one fixed value looks consistent +// no matter how badly its inputs are merged. The corpus therefore also carries +// every input from testdata, which is where the succeeding cases live, and pads +// each of them so a valid call and its padded form can be caught colliding. +func cacheProbeInputs(t *testing.T, rng *rand.Rand) [][]byte { + lengths := []int{0, 1, 31, 32, 63, 64, 65, 95, 96, 97, 127, 128, 129, 160, 161, + 191, 192, 193, 213, 214, 255, 256, 257, 288, 384, 385, 512, 576, 1920, 4096, 8192, 8193} + + var inputs [][]byte + for _, n := range lengths { + inputs = append(inputs, make([]byte, n)) // all zero + + filled := make([]byte, n) + rng.Read(filled) + inputs = append(inputs, filled) + + // A short body followed by zeros, which normalization is allowed to + // drop, and by non-zeros, which it is not unless unread. + if n >= 64 { + zeroTail := make([]byte, n) + copy(zeroTail, filled[:32]) + inputs = append(inputs, zeroTail) + + oneTail := make([]byte, n) + copy(oneTail, filled[:32]) + for i := 32; i < n; i++ { + oneTail[i] = 1 + } + inputs = append(inputs, oneTail) + } + } + for _, fixture := range precompileFixtureInputs(t) { + inputs = append(inputs, fixture) + for _, extra := range []int{1, 64, 512} { + padded := make([]byte, len(fixture)+extra) + copy(padded, fixture) + inputs = append(inputs, padded) + + nonzero := make([]byte, len(fixture)+extra) + copy(nonzero, fixture) + for i := len(fixture); i < len(nonzero); i++ { + nonzero[i] = 0xff + } + inputs = append(inputs, nonzero) + } + } + return inputs +} + +// precompileFixtureInputs reads every precompile test vector on disk. These are +// the inputs that actually exercise success paths. +func precompileFixtureInputs(t *testing.T) [][]byte { + t.Helper() + + entries, err := os.ReadDir("testdata/precompiles") + if err != nil { + t.Fatalf("reading precompile fixtures: %v", err) + } + var inputs [][]byte + for _, entry := range entries { + if filepath.Ext(entry.Name()) != ".json" { + continue + } + blob, err := os.ReadFile(filepath.Join("testdata/precompiles", entry.Name())) + if err != nil { + t.Fatalf("reading %s: %v", entry.Name(), err) + } + var cases []struct{ Input string } + if err := json.Unmarshal(blob, &cases); err != nil { + t.Fatalf("parsing %s: %v", entry.Name(), err) + } + for _, c := range cases { + if in, err := hex.DecodeString(c.Input); err == nil && len(in) <= maxCacheablePrecompileInput { + inputs = append(inputs, in) + } + } + } + if len(inputs) == 0 { + t.Fatal("no precompile fixtures found") + } + return inputs +} + +// TestPrecompileCacheNormalizationSound is the load bearing test of the cache: +// two inputs that normalize to the same key are served the same entry, so they +// must run to the same result. A violation here is a consensus bug, not a +// performance one. +func TestPrecompileCacheNormalizationSound(t *testing.T) { + rng := rand.New(rand.NewSource(1)) + inputs := cacheProbeInputs(t, rng) + + for fork, set := range allPrecompileSets() { + for addr, p := range set { + type outcome struct { + input []byte + output []byte + err error + } + seen := make(map[string]outcome) + for _, in := range inputs { + // Skip what the EVM could never reach. A random modexp header + // declares operands nobody can pay for, and RunPrecompiledContract + // charges before it runs, so Run never sees them. + if p.RequiredGas(in) > probeGasLimit { + continue + } + key, ok := precompileCacheKey(p, in) + if !ok { + continue + } + output, err := p.Run(in) + prev, dup := seen[string(key)] + if !dup { + seen[string(key)] = outcome{in, output, err} + continue + } + if !bytes.Equal(output, prev.output) || !errEqual(err, prev.err) { + t.Errorf("%s %s (%x): inputs %x and %x share key %x but run differently:\n %x / %v\n %x / %v", + fork, p.Name(), addr, prev.input, in, key, prev.output, prev.err, output, err) + } + } + } + } +} + +// TestPrecompileCacheNormalizationCollapsesPadding checks the other direction +// for the precompiles that read a fixed prefix: padding must land on the entry +// the unpadded call already made, otherwise every padded call mints its own. +func TestPrecompileCacheNormalizationCollapsesPadding(t *testing.T) { + rng := rand.New(rand.NewSource(2)) + for _, tc := range []struct { + name string + p PrecompiledContract + read int + valid int + }{ + {"ecrecover", &ecrecover{}, ecRecoverInputLength, ecRecoverInputLength}, + {"bn256ScalarMul", &bn256ScalarMulIstanbul{}, bn256ScalarMulInputLength, bn256ScalarMulInputLength}, + } { + t.Run(tc.name, func(t *testing.T) { + body := make([]byte, tc.valid) + rng.Read(body) + + want, ok := precompileCacheKey(tc.p, body) + if !ok { + t.Fatal("unpadded input is not cacheable") + } + for _, n := range []int{tc.read + 1, tc.read * 2, maxCacheablePrecompileInput} { + padded := make([]byte, n) + copy(padded, body) + got, ok := precompileCacheKey(tc.p, padded) + if !ok { + t.Errorf("input padded to %d is not cacheable", n) + continue + } + if !bytes.Equal(got, want) { + t.Errorf("padding to %d changed the key: %x != %x", n, got, want) + } + } + }) + } + // modexp declares its own operand lengths, so padding past them collapses + // too even though the read length is not a constant. + modexp := &bigModExp{eip2565: true} + header := make([]byte, modExpHeaderLength) + header[31], header[63], header[95] = 1, 1, 1 // one byte each of base, exp, mod + body := append(append([]byte{}, header...), 2, 3, 5) + + want, ok := precompileCacheKey(modexp, body) + if !ok { + t.Fatal("modexp body is not cacheable") + } + padded := make([]byte, 4096) + copy(padded, body) + got, ok := precompileCacheKey(modexp, padded) + if !ok { + t.Fatal("padded modexp is not cacheable") + } + if !bytes.Equal(got, want) { + t.Errorf("modexp padding changed the key: %x != %x", got, want) + } +} + +// TestPrecompileCacheOptIn asserts that caching is opt in, so a new precompile +// cannot be enrolled by accident before anyone has checked that its output is a +// pure function of its input. +func TestPrecompileCacheOptIn(t *testing.T) { + if _, ok := precompileCacheKey(&undeclaredPrecompile{}, nil); ok { + t.Fatal("a precompile that does not implement CacheablePrecompile is cacheable") + } +} + +// TestPrecompileCacheHit runs a precompile twice through the cache and asserts +// the served result matches the computed one, gas included. +func TestPrecompileCacheHit(t *testing.T) { + var ( + cache = NewPrecompileCache() + addr = common.BytesToAddress([]byte{8}) + p = PrecompiledContractsOsaka[addr] + rules = params.Rules{IsBerlin: true, IsIstanbul: true, IsByzantium: true} + input = make([]byte, 192) + ) + gasCost := p.RequiredGas(input) + key, ok := precompileCacheKey(p, input) + if !ok { + t.Fatalf("%s is not cacheable, pick a different fixture", p.Name()) + } + want, wantGas, wantErr := RunPrecompiledContract(nil, p, addr, input, NewGasBudget(gasCost, 0), nil, rules, cache) + got, gotGas, gotErr := RunPrecompiledContract(nil, p, addr, input, NewGasBudget(gasCost, 0), nil, rules, cache) + if !bytes.Equal(got, want) { + t.Errorf("cached output %x, computed %x", got, want) + } + if gotGas != wantGas { + t.Errorf("cached gas %v, computed %v", gotGas, wantGas) + } + if gotErr != wantErr { + t.Errorf("cached error %v, computed %v", gotErr, wantErr) + } + // Prove the second call was served rather than recomputed, the assertions + // above would also pass if the cache never stored anything. + scope := precompileCacheScope{activePrecompiledContracts(rules), addr} + if _, ok := cache.load(scope, key); !ok { + t.Error("result was not stored under its key") + } +} + +func errEqual(a, b error) bool { + if a == nil || b == nil { + return a == nil && b == nil + } + return a.Error() == b.Error() +} + +// undeclaredPrecompile stands in for a precompile added without a caching +// decision. +type undeclaredPrecompile struct{} + +func (c *undeclaredPrecompile) RequiredGas([]byte) uint64 { return 0 } +func (c *undeclaredPrecompile) Run([]byte) ([]byte, error) { return nil, nil } +func (c *undeclaredPrecompile) Name() string { return "UNDECLARED" } + +// precompileCacheSynthetic lists the precompiles with no test vectors on disk, +// benchmarked on generated inputs instead. +var precompileCacheSynthetic = []struct{ name, addr string }{ + {"SHA256", "02"}, {"RIPEMD160", "03"}, {"ID", "04"}, +} + +// precompileCacheFixtures maps a testdata fixture to the address its precompile +// sits at in allPrecompiles. The Byzantium bn256 variants share their Run with +// the Istanbul ones and only differ in price, so they would be duplicate rows. +var precompileCacheFixtures = []struct{ fixture, addr string }{ + {"ecRecover", "01"}, {"modexp_eip2565", "f5"}, {"bn256Add", "06"}, + {"bn256ScalarMul", "07"}, {"bn256Pairing", "08"}, {"blake2F", "09"}, + {"pointEvaluation", "0a"}, {"blsG1Add", "f0a"}, {"blsG2Add", "f0c"}, + {"blsG1MultiExp", "f0b"}, {"blsG2MultiExp", "f0d"}, {"blsPairing", "f0e"}, + {"blsMapG1", "f0f"}, {"blsMapG2", "f10"}, {"p256Verify", "0b"}, +} + +// BenchmarkPrecompileCacheHitVsRun compares serving a warm entry against just +// running the precompile, over the real test vectors. It is the evidence for +// which precompiles opt in: caching only belongs where the hit is the cheaper +// path, and for sha256 and blake2F that margin is thin enough to be worth +// rechecking on the machine you care about. Run it with -count and read +// medians, the first case measured absorbs the warm-up. +func BenchmarkPrecompileCacheHitVsRun(b *testing.B) { + rules := params.Rules{IsByzantium: true, IsIstanbul: true, IsBerlin: true, IsCancun: true, IsPrague: true, IsOsaka: true} + set := activePrecompiledContracts(rules) + + for _, bench := range precompileCacheFixtures { + tests, err := loadJson(bench.fixture) + if err != nil { + b.Fatalf("%s: %v", bench.fixture, err) + } + addr := common.HexToAddress(bench.addr) + p := allPrecompiles[addr] + for _, tc := range tests { + if tc.NoBenchmark { + continue + } + in := common.Hex2Bytes(tc.Input) + key, ok := precompileCacheKey(p, in) + if !ok || p.RequiredGas(in) > probeGasLimit { + continue + } + out, err := p.Run(in) + if err != nil { + continue // errors are never cached, there is no hit to measure + } + cache := NewPrecompileCache() + scope := precompileCacheScope{set, addr} + cache.store(scope, key, out) + + label := fmt.Sprintf("%s/%s/len=%d", bench.fixture, tc.Name, len(in)) + b.Run(label+"/hit", func(b *testing.B) { + b.ReportAllocs() + for b.Loop() { + k, _ := precompileCacheKey(p, in) + cache.load(scope, k) + } + }) + b.Run(label+"/run", func(b *testing.B) { + b.ReportAllocs() + for b.Loop() { + p.Run(in) + } + }) + } + } +} + +// BenchmarkPrecompileCacheSynthetic covers the precompiles that ship no test +// vectors, which are also the ones whose caching decision is closest. Their +// cost scales with input length rather than varying per vector, so synthetic +// inputs of a few sizes say more here than a fixture corpus would. +func BenchmarkPrecompileCacheSynthetic(b *testing.B) { + rules := params.Rules{IsByzantium: true, IsIstanbul: true, IsBerlin: true, IsCancun: true, IsPrague: true, IsOsaka: true} + set := activePrecompiledContracts(rules) + + for _, tc := range precompileCacheSynthetic { + addr := common.HexToAddress(tc.addr) + p := allPrecompiles[addr] + for _, n := range []int{32, 128, 1024, 8192} { + in := make([]byte, n) + for i := range in { + in[i] = byte(i) + } + out, err := p.Run(in) + if err != nil { + continue + } + var ( + scope = precompileCacheScope{set, addr} + cache = NewPrecompileCache() + ) + label := fmt.Sprintf("%s/len=%d", tc.name, n) + + // Only the precompiles that opt in get a hit measured, going through + // the same path RunPrecompiledContract does. For the one that does + // not, its run cost against the lookups below is the whole argument. + if key, ok := precompileCacheKey(p, in); ok && len(out) <= maxCacheablePrecompileOutput { + cache.store(scope, key, out) + b.Run(label+"/hit", func(b *testing.B) { + b.ReportAllocs() + for b.Loop() { + k, _ := precompileCacheKey(p, in) + cache.load(scope, k) + } + }) + } + b.Run(label+"/run", func(b *testing.B) { + b.ReportAllocs() + for b.Loop() { + p.Run(in) + } + }) + } + } +}