core/vm: key the precompile cache on the input instead of its hash (#35473)

#35388 keys the cache on `keccak256(address, input)`. Hashing the input
is most of the cost of a lookup, and it scales with input size, so a
large input is expensive to look up even when the result is trivial.
That is why several precompiles had to opt out to stay ahead of it.

@ayamiyaguchi spotted this and wrote the first commit here, which
excluded the precompiles the key cost was hurting most. This takes the
same finding in a different direction by making the key cheap in the
first place, so those precompiles do not need excluding.

The solution here is to key on the input bytes directly, plus three
changes that follow from it:

1) Entries used to share one LRU, so a high-volume precompile could
evict the pairing and modexp results the cache exists to hold. Each
precompile now gets its own.

2) Now that the key is the input, entries run from tens of bytes to
kilobytes, and a count means very different memory depending on the mix.
Each precompile gets 1 MiB of keys and values.

3) A precompile is cached only if it says so, and can narrow an input to
the bytes that determine the result via `NormalizeInput` so padding
lands on the entry the unpadded call already made.

### Benchmarks

Medians of five runs. 

#### Lookup versus running the precompile, at real vector sizes

| precompile           |  input |     #35388 |    this PR |       run |
|----------------------|--------|------------|------------|-----------|
| ECREC                |    128 |     480 ns |      55 ns |   18.2 µs |
| SHA256               |    128 | not cached |      58 ns |     79 ns |
| RIPEMD160            |    128 | not cached |      55 ns |    454 ns |
| ID                   |    128 | not cached | not cached |     16 ns |
| MODEXP               |    609 |     1.1 µs |     128 ns |    2.9 µs |
| BN254_ADD            |    128 |     478 ns |      54 ns |    1.0 µs |
| BN254_MUL            |     96 |     279 ns |      52 ns |   13.4 µs |
| BN254_PAIRING        |    384 |     698 ns |      85 ns |  490.1 µs |
| BLAKE2F              |    213 |     486 ns |      67 ns |    118 ns |
| KZG_POINT_EVALUATION |    192 |     488 ns |      61 ns |  785.8 µs |
| BLS12_G1ADD          |    256 |     697 ns |      68 ns |    1.9 µs |
| BLS12_G1MSM          |    160 |     488 ns |      60 ns |   95.4 µs |
| BLS12_G2ADD          |    512 |     911 ns |     103 ns |    2.9 µs |
| BLS12_G2MSM          |    288 |     708 ns |      74 ns |  181.3 µs |
| BLS12_PAIRING_CHECK  |    384 |     713 ns |      85 ns |  445.6 µs |
| BLS12_MAP_FP_TO_G1   |     64 |     282 ns |      45 ns |   30.7 µs |
| BLS12_MAP_FP2_TO_G2  |    128 |     490 ns |      55 ns |  135.0 µs |
| P256VERIFY           |    160 |     494 ns |      60 ns |   33.6 µs |

RIPEMD160 wasn't cached before and a lookup now costs 55 ns against 454
ns to run it. BN254_ADD was cached previously, but at 478 ns to look up
against a 1 µs run the cache was barely paying for itself. With this
change, the lookup is now 54 ns and much more worth it.

#### The same measurement at the largest eligible input, 8 KiB

| precompile           |     #35388 |          this PR |       run |
|----------------------|------------|------------------|-----------|
| ECREC                |    12.9 µs |            74 ns |    131 ns |
| SHA256               | not cached |           1.1 µs |    2.6 µs |
| RIPEMD160            | not cached |           1.2 µs |   17.9 µs |
| ID                   | not cached |       not cached |    969 ns |
| MODEXP               |    13.0 µs |            56 ns |     50 ns |
| BN254_ADD            |    12.9 µs |            67 ns |     60 ns |
| BN254_MUL            |    12.9 µs |            56 ns |    6.4 µs |
| BN254_PAIRING        |    12.8 µs | 5 ns, not cached |      2 ns |
| BLAKE2F              |    12.9 µs | 5 ns, not cached |      1 ns |
| KZG_POINT_EVALUATION |    12.9 µs | 4 ns, not cached |      1 ns |
| BLS12_G1ADD          |    12.8 µs | 5 ns, not cached |      1 ns |
| BLS12_G1MSM          |    13.0 µs | 4 ns, not cached |      1 ns |
| BLS12_G2ADD          |    13.0 µs | 4 ns, not cached |      1 ns |
| BLS12_G2MSM          |    12.9 µs | 4 ns, not cached |      1 ns |
| BLS12_PAIRING_CHECK  |    13.0 µs | 5 ns, not cached |      1 ns |
| BLS12_MAP_FP_TO_G1   |    13.0 µs | 4 ns, not cached |      1 ns |
| BLS12_MAP_FP2_TO_G2  |    12.8 µs | 4 ns, not cached |      1 ns |
| P256VERIFY           |    13.0 µs | 4 ns, not cached |      1 ns |

The `#35388` column is flat at ~13 µs because the key is a hash of the
input and does not depend on which precompile receives it. In the `this
PR` column, the number beside "not cached" is the cost of deciding not
to build a key: those precompiles require an exact length, so an 8 KiB
input is one they will reject, and there is no result worth keying. The
`run` column here is mostly a length rejection rather than work, so it
is context for the other two columns rather than a comparison.

---------

Co-authored-by: aya <aya@ethereum.org>
Co-authored-by: Gary Rong <garyrong0905@gmail.com>
This commit is contained in:
Jonny Rhea 2026-08-12 02:41:46 -05:00 committed by GitHub
parent ceced06cb0
commit 98a0080a2c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 700 additions and 73 deletions

View file

@ -286,19 +286,18 @@ func RunPrecompiledContract(stateDB StateDB, p PrecompiledContract, address comm
// Serve pure precompiles from the shared result cache if one is attached. // Serve pure precompiles from the shared result cache if one is attached.
// Gas accounting and state touching above are identical on hit and miss, // Gas accounting and state touching above are identical on hit and miss,
// only the recomputation is skipped. // only the recomputation is skipped.
if cache != nil && cacheablePrecompile(p, input) { if cache != nil {
var ( if key, ok := precompileCacheKey(p, input); ok {
set = activePrecompiledContracts(rules) scope := precompileCacheScope{activePrecompiledContracts(rules), address}
key = precompileCacheKey(address, input) if output, ok := cache.load(scope, key); ok {
) return output, gas, nil
if output, ok := cache.load(set, address, 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) output, err := p.Run(input)
return output, gas, err return output, gas, err
@ -307,13 +306,15 @@ func RunPrecompiledContract(stateDB StateDB, p PrecompiledContract, address comm
// ecrecover implemented as a native contract. // ecrecover implemented as a native contract.
type ecrecover struct{} 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 { func (c *ecrecover) RequiredGas(input []byte) uint64 {
return params.EcrecoverGas return params.EcrecoverGas
} }
func (c *ecrecover) Run(input []byte) ([]byte, error) { func (c *ecrecover) Run(input []byte) ([]byte, error) {
const ecRecoverInputLength = 128
input = common.RightPadBytes(input, ecRecoverInputLength) input = common.RightPadBytes(input, ecRecoverInputLength)
// "input" is (hash, v, r, s), each 32 bytes // "input" is (hash, v, r, s), each 32 bytes
// but for ecrecover we want (r, s, v) // but for ecrecover we want (r, s, v)
@ -346,6 +347,13 @@ func (c *ecrecover) Name() string {
return "ECREC" 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. // SHA256 implemented as a native contract.
type sha256hash struct{} type sha256hash struct{}
@ -365,9 +373,7 @@ func (c *sha256hash) Name() string {
return "SHA256" return "SHA256"
} }
// Cacheable opts out of result caching, deriving the cache key costs about func (c *sha256hash) Cacheable() bool { return true }
// as much as running the hash itself.
func (c *sha256hash) Cacheable() bool { return false }
// RIPEMD160 implemented as a native contract. // RIPEMD160 implemented as a native contract.
type ripemd160hash struct{} type ripemd160hash struct{}
@ -389,9 +395,7 @@ func (c *ripemd160hash) Name() string {
return "RIPEMD160" return "RIPEMD160"
} }
// Cacheable opts out of result caching, hashing the input for the cache key func (c *ripemd160hash) Cacheable() bool { return true }
// costs about as much as running it.
func (c *ripemd160hash) Cacheable() bool { return false }
// data copy implemented as a native contract. // data copy implemented as a native contract.
type dataCopy struct{} type dataCopy struct{}
@ -411,10 +415,6 @@ func (c *dataCopy) Name() string {
return "ID" 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. // bigModExp implements a native big integer exponential modular operation.
type bigModExp struct { type bigModExp struct {
eip2565 bool eip2565 bool
@ -598,6 +598,10 @@ func osakaModexpGas(baseLen, expLen, modLen uint64, expHead uint256.Int) uint64
return max(gas, minGas) 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. // RequiredGas returns the gas required to execute the pre-compiled contract.
func (c *bigModExp) RequiredGas(input []byte) uint64 { func (c *bigModExp) RequiredGas(input []byte) uint64 {
// Parse input lengths // Parse input lengths
@ -682,7 +686,7 @@ func (c *bigModExp) Run(input []byte) ([]byte, error) {
// Modulo 0 is undefined, return zero // Modulo 0 is undefined, return zero
return common.LeftPadBytes([]byte{}, int(modLen)), nil return common.LeftPadBytes([]byte{}, int(modLen)), nil
case base.BitLen() == 1: // a bit length of 1 means it's 1 (or -1). 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() v = base.Mod(base, mod).Bytes()
default: default:
v = base.Exp(base, exp, mod).Bytes() v = base.Exp(base, exp, mod).Bytes()
@ -694,6 +698,39 @@ func (c *bigModExp) Name() string {
return "MODEXP" 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, // newCurvePoint unmarshals a binary blob into a bn256 elliptic curve point,
// returning it, or an error if the point is invalid. // returning it, or an error if the point is invalid.
func newCurvePoint(blob []byte) (*bn256.G1, error) { func newCurvePoint(blob []byte) (*bn256.G1, error) {
@ -714,6 +751,10 @@ func newTwistPoint(blob []byte) (*bn256.G2, error) {
return p, nil 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 // runBn256Add implements the Bn256Add precompile, referenced by both
// Byzantium and Istanbul operations. // Byzantium and Istanbul operations.
func runBn256Add(input []byte) ([]byte, error) { func runBn256Add(input []byte) ([]byte, error) {
@ -747,6 +788,13 @@ func (c *bn256AddIstanbul) Name() string {
return "BN254_ADD" 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 // bn256AddByzantium implements a native elliptic curve point addition
// conforming to Byzantium consensus rules. // conforming to Byzantium consensus rules.
type bn256AddByzantium struct{} type bn256AddByzantium struct{}
@ -764,6 +812,18 @@ func (c *bn256AddByzantium) Name() string {
return "BN254_ADD" 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 // runBn256ScalarMul implements the Bn256ScalarMul precompile, referenced by
// both Byzantium and Istanbul operations. // both Byzantium and Istanbul operations.
func runBn256ScalarMul(input []byte) ([]byte, error) { func runBn256ScalarMul(input []byte) ([]byte, error) {
@ -793,6 +853,13 @@ func (c *bn256ScalarMulIstanbul) Name() string {
return "BN254_MUL" 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 // bn256ScalarMulByzantium implements a native elliptic curve scalar
// multiplication conforming to Byzantium consensus rules. // multiplication conforming to Byzantium consensus rules.
type bn256ScalarMulByzantium struct{} type bn256ScalarMulByzantium struct{}
@ -810,6 +877,13 @@ func (c *bn256ScalarMulByzantium) Name() string {
return "BN254_MUL" 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 ( var (
// true32Byte is returned if the bn256 pairing check succeeds. // 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} 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" 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 // bn256PairingByzantium implements a pairing pre-compile for the bn256 curve
// conforming to Byzantium consensus rules. // conforming to Byzantium consensus rules.
type bn256PairingByzantium struct{} type bn256PairingByzantium struct{}
@ -886,6 +967,13 @@ func (c *bn256PairingByzantium) Name() string {
return "BN254_PAIRING" 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{} type blake2F struct{}
func (c *blake2F) RequiredGas(input []byte) uint64 { func (c *blake2F) RequiredGas(input []byte) uint64 {
@ -951,6 +1039,13 @@ func (c *blake2F) Name() string {
return "BLAKE2F" 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 ( var (
errBLS12381InvalidInputLength = errors.New("invalid input length") errBLS12381InvalidInputLength = errors.New("invalid input length")
errBLS12381InvalidFieldElementTopBytes = errors.New("invalid field element top bytes") errBLS12381InvalidFieldElementTopBytes = errors.New("invalid field element top bytes")
@ -998,6 +1093,13 @@ func (c *bls12381G1Add) Name() string {
return "BLS12_G1ADD" 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. // bls12381G1MultiExp implements EIP-2537 G1MultiExp precompile.
type bls12381G1MultiExp struct{} type bls12381G1MultiExp struct{}
@ -1062,6 +1164,13 @@ func (c *bls12381G1MultiExp) Name() string {
return "BLS12_G1MSM" 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. // bls12381G2Add implements EIP-2537 G2Add precompile.
type bls12381G2Add struct{} type bls12381G2Add struct{}
@ -1103,6 +1212,13 @@ func (c *bls12381G2Add) Name() string {
return "BLS12_G2ADD" 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. // bls12381G2MultiExp implements EIP-2537 G2MultiExp precompile.
type bls12381G2MultiExp struct{} type bls12381G2MultiExp struct{}
@ -1167,6 +1283,13 @@ func (c *bls12381G2MultiExp) Name() string {
return "BLS12_G2MSM" 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. // bls12381Pairing implements EIP-2537 Pairing precompile.
type bls12381Pairing struct{} type bls12381Pairing struct{}
@ -1234,6 +1357,13 @@ func (c *bls12381Pairing) Name() string {
return "BLS12_PAIRING_CHECK" 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) { func decodePointG1(in []byte) (*bls12381.G1Affine, error) {
if len(in) != 128 { if len(in) != 128 {
return nil, errors.New("invalid g1 point length") return nil, errors.New("invalid g1 point length")
@ -1356,6 +1486,13 @@ func (c *bls12381MapG1) Name() string {
return "BLS12_MAP_FP_TO_G1" 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. // bls12381MapG2 implements EIP-2537 MapG2 precompile.
type bls12381MapG2 struct{} type bls12381MapG2 struct{}
@ -1393,6 +1530,13 @@ func (c *bls12381MapG2) Name() string {
return "BLS12_MAP_FP2_TO_G2" 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. // kzgPointEvaluation implements the EIP-4844 point evaluation precompile.
type kzgPointEvaluation struct{} type kzgPointEvaluation struct{}
@ -1453,6 +1597,13 @@ func (b *kzgPointEvaluation) Name() string {
return "KZG_POINT_EVALUATION" 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 // kZGToVersionedHash implements kzg_to_versioned_hash from EIP-4844
func kZGToVersionedHash(kzg kzg4844.Commitment) common.Hash { func kZGToVersionedHash(kzg kzg4844.Commitment) common.Hash {
h := sha256.Sum256(kzg[:]) h := sha256.Sum256(kzg[:])
@ -1470,9 +1621,11 @@ func (c *p256Verify) RequiredGas(input []byte) uint64 {
return params.P256VerifyGas 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 // 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) { func (c *p256Verify) Run(input []byte) ([]byte, error) {
const p256VerifyInputLength = 160
if len(input) != p256VerifyInputLength { if len(input) != p256VerifyInputLength {
return nil, nil return nil, nil
} }
@ -1492,3 +1645,10 @@ func (c *p256Verify) Run(input []byte) ([]byte, error) {
func (c *p256Verify) Name() string { func (c *p256Verify) Name() string {
return "P256VERIFY" 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
}

View file

@ -22,7 +22,6 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/lru" "github.com/ethereum/go-ethereum/common/lru"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/metrics"
) )
@ -31,29 +30,30 @@ var (
precompileCacheMissMeter = metrics.NewRegisteredMeter("chain/cache/precompile/miss", nil) precompileCacheMissMeter = metrics.NewRegisteredMeter("chain/cache/precompile/miss", nil)
precompileCachePrefetchHitMeter = metrics.NewRegisteredMeter("chain/cache/precompile/prefetch/hit", nil) precompileCachePrefetchHitMeter = metrics.NewRegisteredMeter("chain/cache/precompile/prefetch/hit", nil)
precompileCachePrefetchMissMeter = metrics.NewRegisteredMeter("chain/cache/precompile/prefetch/miss", nil) precompileCachePrefetchMissMeter = metrics.NewRegisteredMeter("chain/cache/precompile/prefetch/miss", nil)
precompileCacheEntryGauge = metrics.NewRegisteredGauge("chain/cache/precompile/entries", nil)
) )
const ( const (
// maxCacheablePrecompileInput bounds the input size eligible for result // maxCacheablePrecompileInput bounds the normalized input size eligible for
// caching. Larger inputs are rare one-offs and hashing them for the key // result caching. The key is the input itself, so this also bounds how much
// eats into the win. // an entry can cost.
maxCacheablePrecompileInput = 8192 maxCacheablePrecompileInput = 8192
// maxCacheablePrecompileOutput bounds the output size stored in the // maxCacheablePrecompileOutput bounds the output size stored in the
// cache, keeping the worst case memory use of an entry small. // cache, keeping the worst case memory use of an entry small.
maxCacheablePrecompileOutput = 1024 maxCacheablePrecompileOutput = 1024
// precompileCacheEntries is the maximum number of cached results. With // maxCacheablePrecompileBytes is the budget each precompile gets per fork,
// outputs capped by maxCacheablePrecompileOutput, the worst case memory // counting keys and values along with what an entry costs to hold. Entries
// use stays at a few megabytes. // run from tens of bytes to kilobytes, so a budget in entries would mean
precompileCacheEntries = 4096 // very different memory depending on the mix.
maxCacheablePrecompileBytes = 1024 * 1024
) )
// PrecompileCache is a thread-safe LRU of precompile outputs, shared between // PrecompileCache is a thread-safe cache of precompile outputs, shared between
// the state prefetcher and block processing so the serial pass can reuse // the state prefetcher and block processing so the serial pass can reuse what
// results the prefetcher already computed. Entries are namespaced by // the prefetcher already computed. Each precompile gets its own cache per fork,
// precompile set, so forks never share results across a behaviour change. // so results never cross a repricing and a cheap precompile cannot evict the
// results of an expensive one.
type PrecompileCache struct { type PrecompileCache struct {
data *precompileCacheData data *precompileCacheData
@ -69,8 +69,15 @@ type PrecompileCache struct {
// precompileCacheData is the storage shared by the two cache handles. // precompileCacheData is the storage shared by the two cache handles.
type precompileCacheData struct { type precompileCacheData struct {
mu sync.RWMutex mu sync.RWMutex
sets map[*PrecompiledContracts]*lru.Cache[common.Hash, []byte] 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. // precompileCacheMeters holds the per-address hit and miss meters.
@ -82,7 +89,7 @@ type precompileCacheMeters struct {
// NewPrecompileCache constructs a precompile result cache. // NewPrecompileCache constructs a precompile result cache.
func NewPrecompileCache() *PrecompileCache { func NewPrecompileCache() *PrecompileCache {
data := &precompileCacheData{ data := &precompileCacheData{
sets: make(map[*PrecompiledContracts]*lru.Cache[common.Hash, []byte]), caches: make(map[precompileCacheScope]*lru.SizeConstrainedCache[string, []byte]),
} }
return &PrecompileCache{ return &PrecompileCache{
data: data, data: data,
@ -90,6 +97,7 @@ func NewPrecompileCache() *PrecompileCache {
hit: precompileCacheHitMeter, hit: precompileCacheHitMeter,
miss: precompileCacheMissMeter, miss: precompileCacheMissMeter,
meters: make(map[common.Address]*precompileCacheMeters), meters: make(map[common.Address]*precompileCacheMeters),
prefetch: &PrecompileCache{ prefetch: &PrecompileCache{
data: data, data: data,
prefix: "chain/cache/precompile/prefetch", 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 // load retrieves the cached output for the given key. The returned slice is
// a private copy owned by the caller, entries cross goroutine boundaries. // 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() c.data.mu.RLock()
results := c.data.sets[set] results := c.data.caches[scope]
c.data.mu.RUnlock() c.data.mu.RUnlock()
meters := c.metersFor(addr) meters := c.metersFor(scope.addr)
if results != nil { if results != nil {
if output, ok := results.Get(key); ok { if output, ok := results.Get(string(key)); ok {
c.hit.Mark(1) c.hit.Mark(1)
meters.hit.Mark(1) meters.hit.Mark(1)
return common.CopyBytes(output), true return common.CopyBytes(output), true
@ -129,23 +137,24 @@ func (c *PrecompileCache) load(set *PrecompiledContracts, addr common.Address, k
return nil, false return nil, false
} }
// store saves the output of a precompile run under the given key. The value // store saves the output of a precompile run under the given key. Both the key
// is copied, the cache never aliases caller memory. // and the value are copied, the cache never aliases caller memory. That matters
func (c *PrecompileCache) store(set *PrecompiledContracts, addr common.Address, key common.Hash, output []byte) { // 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() c.data.mu.RLock()
results := c.data.sets[set] results := c.data.caches[scope]
c.data.mu.RUnlock() c.data.mu.RUnlock()
if results == nil { if results == nil {
c.data.mu.Lock() c.data.mu.Lock()
if results = c.data.sets[set]; results == nil { if results = c.data.caches[scope]; results == nil {
results = lru.NewCache[common.Hash, []byte](precompileCacheEntries) results = lru.NewSizeConstrainedCache[string, []byte](maxCacheablePrecompileBytes)
c.data.sets[set] = results c.data.caches[scope] = results
} }
c.data.mu.Unlock() c.data.mu.Unlock()
} }
results.Add(key, common.CopyBytes(output)) results.Add(string(key), common.CopyBytes(output))
precompileCacheEntryGauge.Update(int64(results.Len()))
} }
// metersFor returns the hit and miss meters of the given precompile address, // 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 return meters
} }
// CacheablePrecompile lets a precompile opt out of result caching, either // CacheablePrecompile is implemented by precompiles that opt in to result
// because its output is not a pure function of the input or because it is // caching. Anything that does not implement it is never cached, so a new
// cheaper to rerun than to cache. // precompile is not enrolled until someone decides it should be.
type CacheablePrecompile interface { type CacheablePrecompile interface {
Cacheable() bool Cacheable() bool
} }
// cacheablePrecompile reports whether an invocation is eligible for result // NormalizingPrecompile is implemented by precompiles that can narrow an input
// caching. // down to the bytes that determine the result.
func cacheablePrecompile(p PrecompiledContract, input []byte) bool { type NormalizingPrecompile interface {
if len(input) > maxCacheablePrecompileInput { // NormalizeInput returns the bytes identifying the result, and whether the
return false // 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
if c, ok := p.(CacheablePrecompile); ok { // cache, which is how a precompile rejects lengths it will fail on.
return c.Cacheable() NormalizeInput(input []byte) ([]byte, bool)
}
return true
} }
// precompileCacheKey derives the cache key for a precompile invocation. Fork // precompileCacheKey returns the key identifying an invocation and whether it
// discrimination is handled by the set namespacing, so the key only covers // is eligible for result caching.
// the address and input. func precompileCacheKey(p PrecompiledContract, input []byte) ([]byte, bool) {
func precompileCacheKey(addr common.Address, input []byte) common.Hash { c, ok := p.(CacheablePrecompile)
return crypto.Keccak256Hash(addr[:], input) 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]
} }

View file

@ -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 <http://www.gnu.org/licenses/>.
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)
}
})
}
}
}