Skip to content

Commit ae3c915

Browse files
authored
Merge pull request #4 from ethpandaops/feat/precompiles-for-sim
feat: add precompile gas override system and simulation tracing
2 parents 9948e78 + 55cce68 commit ae3c915

File tree

5 files changed

+407
-3
lines changed

5 files changed

+407
-3
lines changed
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// Copyright 2024 The Erigon Authors
2+
// This file is part of Erigon.
3+
//
4+
// Erigon is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Lesser General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// Erigon is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Lesser General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Lesser General Public License
15+
// along with Erigon. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package vm
18+
19+
import (
20+
"encoding/binary"
21+
22+
"github.com/erigontech/erigon/execution/protocol/params"
23+
)
24+
25+
// Precompile gas override key constants.
26+
// Fixed-gas precompiles use PC_<name> as a single total key.
27+
// Variable-gas precompiles use PC_<name>_<param> for each formula parameter.
28+
const (
29+
GasKeyPCEcrec = "PC_ECREC"
30+
GasKeyPCBn254Add = "PC_BN254_ADD"
31+
GasKeyPCBn254Mul = "PC_BN254_MUL"
32+
GasKeyPCBls12G1Add = "PC_BLS12_G1ADD"
33+
GasKeyPCBls12G2Add = "PC_BLS12_G2ADD"
34+
GasKeyPCBls12MapFpToG1 = "PC_BLS12_MAP_FP_TO_G1"
35+
GasKeyPCBls12MapFp2ToG2 = "PC_BLS12_MAP_FP2_TO_G2"
36+
GasKeyPCKzgPointEvaluation = "PC_KZG_POINT_EVALUATION"
37+
GasKeyPCP256Verify = "PC_P256VERIFY"
38+
39+
GasKeyPCSha256Base = "PC_SHA256_BASE"
40+
GasKeyPCSha256PerWord = "PC_SHA256_PER_WORD"
41+
42+
GasKeyPCRipemd160Base = "PC_RIPEMD160_BASE"
43+
GasKeyPCRipemd160PerWord = "PC_RIPEMD160_PER_WORD"
44+
45+
GasKeyPCIdBase = "PC_ID_BASE"
46+
GasKeyPCIdPerWord = "PC_ID_PER_WORD"
47+
48+
GasKeyPCModexpMinGas = "PC_MODEXP_MIN_GAS"
49+
50+
GasKeyPCBn254PairingBase = "PC_BN254_PAIRING_BASE"
51+
GasKeyPCBn254PairingPerPair = "PC_BN254_PAIRING_PER_PAIR"
52+
53+
GasKeyPCBlake2fPerRound = "PC_BLAKE2F_PER_ROUND"
54+
55+
GasKeyPCBls12PairingBase = "PC_BLS12_PAIRING_CHECK_BASE"
56+
GasKeyPCBls12PairingPerPair = "PC_BLS12_PAIRING_CHECK_PER_PAIR"
57+
58+
GasKeyPCBls12G1MsmMulGas = "PC_BLS12_G1MSM_MUL_GAS"
59+
GasKeyPCBls12G2MsmMulGas = "PC_BLS12_G2MSM_MUL_GAS"
60+
)
61+
62+
// PrecompileGasWithOverrides calculates precompile gas cost with optional overrides.
63+
// Fixed-gas precompiles: single key (PC_<name>) overrides the flat cost.
64+
// Variable-gas precompiles: parameter keys (PC_<name>_BASE, etc.) override formula inputs.
65+
func PrecompileGasWithOverrides(schedule *GasSchedule, name string, input []byte, defaultGas uint64) uint64 {
66+
if schedule == nil {
67+
return defaultGas
68+
}
69+
70+
switch name {
71+
// Fixed-gas precompiles — single total key
72+
case "ECREC", "BN254_ADD", "BN254_MUL", "BLS12_G1ADD", "BLS12_G2ADD",
73+
"BLS12_MAP_FP_TO_G1", "BLS12_MAP_FP2_TO_G2", "KZG_POINT_EVALUATION", "P256VERIFY":
74+
return schedule.GetOr("PC_"+name, defaultGas)
75+
76+
// Variable-gas precompiles — parameter overrides
77+
case "SHA256":
78+
return precompileBasePerWord(schedule, GasKeyPCSha256Base, GasKeyPCSha256PerWord, input, params.Sha256BaseGas, params.Sha256PerWordGas)
79+
case "RIPEMD160":
80+
return precompileBasePerWord(schedule, GasKeyPCRipemd160Base, GasKeyPCRipemd160PerWord, input, params.Ripemd160BaseGas, params.Ripemd160PerWordGas)
81+
case "ID":
82+
return precompileBasePerWord(schedule, GasKeyPCIdBase, GasKeyPCIdPerWord, input, params.IdentityBaseGas, params.IdentityPerWordGas)
83+
case "MODEXP":
84+
return precompileModexp(schedule, defaultGas)
85+
case "BN254_PAIRING":
86+
return precompileBasePerPair(schedule, GasKeyPCBn254PairingBase, GasKeyPCBn254PairingPerPair, input, 192, params.Bn254PairingBaseGasIstanbul, params.Bn254PairingPerPointGasIstanbul)
87+
case "BLAKE2F":
88+
return precompileBlake2f(schedule, input)
89+
case "BLS12_PAIRING_CHECK":
90+
return precompileBasePerPair(schedule, GasKeyPCBls12PairingBase, GasKeyPCBls12PairingPerPair, input, 384, params.Bls12381PairingBaseGas, params.Bls12381PairingPerPairGas)
91+
case "BLS12_G1MSM":
92+
return precompileMsm(schedule, GasKeyPCBls12G1MsmMulGas, input, 160, params.Bls12381G1MulGas)
93+
case "BLS12_G2MSM":
94+
return precompileMsm(schedule, GasKeyPCBls12G2MsmMulGas, input, 288, params.Bls12381G2MulGas)
95+
}
96+
97+
return defaultGas
98+
}
99+
100+
// precompileBasePerWord computes base + perWord * ceil(len(input)/32).
101+
// Used by SHA256, RIPEMD160, IDENTITY.
102+
func precompileBasePerWord(schedule *GasSchedule, baseKey, perWordKey string, input []byte, defaultBase, defaultPerWord uint64) uint64 {
103+
base := schedule.GetOr(baseKey, defaultBase)
104+
perWord := schedule.GetOr(perWordKey, defaultPerWord)
105+
words := uint64(len(input)+31) / 32
106+
return base + perWord*words
107+
}
108+
109+
// precompileBasePerPair computes base + perPair * (len(input) / pairSize).
110+
// Used by BN254_PAIRING (pairSize=192), BLS12_PAIRING_CHECK (pairSize=384).
111+
func precompileBasePerPair(schedule *GasSchedule, baseKey, perPairKey string, input []byte, pairSize int, defaultBase, defaultPerPair uint64) uint64 {
112+
base := schedule.GetOr(baseKey, defaultBase)
113+
perPair := schedule.GetOr(perPairKey, defaultPerPair)
114+
pairs := uint64(len(input) / pairSize)
115+
return base + perPair*pairs
116+
}
117+
118+
// precompileBlake2f computes perRound * rounds, where rounds is read from input[0:4].
119+
func precompileBlake2f(schedule *GasSchedule, input []byte) uint64 {
120+
if len(input) != 213 {
121+
return 0
122+
}
123+
rounds := uint64(binary.BigEndian.Uint32(input[0:4]))
124+
perRound := schedule.GetOr(GasKeyPCBlake2fPerRound, 1)
125+
return perRound * rounds
126+
}
127+
128+
// precompileMsm computes k * mulGas * discount[k] / 1000.
129+
// The discount table is not overridable — only the per-point mulGas is.
130+
func precompileMsm(schedule *GasSchedule, mulGasKey string, input []byte, pointSize int, defaultMulGas uint64) uint64 {
131+
k := len(input) / pointSize
132+
if k == 0 {
133+
return 0
134+
}
135+
mulGas := schedule.GetOr(mulGasKey, defaultMulGas)
136+
137+
// Use the correct discount table based on point size
138+
var discount uint64
139+
if pointSize == 160 {
140+
if dLen := len(params.Bls12381MSMDiscountTableG1); k < dLen {
141+
discount = params.Bls12381MSMDiscountTableG1[k-1]
142+
} else {
143+
discount = params.Bls12381MSMDiscountTableG1[dLen-1]
144+
}
145+
} else {
146+
if dLen := len(params.Bls12381MSMDiscountTableG2); k < dLen {
147+
discount = params.Bls12381MSMDiscountTableG2[k-1]
148+
} else {
149+
discount = params.Bls12381MSMDiscountTableG2[dLen-1]
150+
}
151+
}
152+
153+
return (uint64(k) * mulGas * discount) / 1000
154+
}
155+
156+
// precompileModexp applies the MODEXP min gas override.
157+
// The complex EIP-2565/7883 formula itself is not overridable — only the floor value is.
158+
func precompileModexp(schedule *GasSchedule, defaultGas uint64) uint64 {
159+
minGas := schedule.GetOr(GasKeyPCModexpMinGas, 200)
160+
if defaultGas < minGas {
161+
return minGas
162+
}
163+
return defaultGas
164+
}

overlay/node/xatu/custom_gas.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,33 @@ var gasDescriptions = map[string]string{
239239

240240
// Self-destruct
241241
"SELFDESTRUCT": "Mark contract for destruction. Base cost; adds CALL_COLD if recipient is cold, CREATE_BY_SELFDESTRUCT if recipient doesn't exist.",
242+
243+
// Precompiles - Fixed gas
244+
"PC_ECREC": "ECRECOVER precompile. Signature recovery. Fixed cost.",
245+
"PC_BN254_ADD": "BN254 point addition (alt_bn128). Fixed cost.",
246+
"PC_BN254_MUL": "BN254 scalar multiplication. Fixed cost.",
247+
"PC_BLS12_G1ADD": "BLS12-381 G1 point addition. Fixed cost.",
248+
"PC_BLS12_G2ADD": "BLS12-381 G2 point addition. Fixed cost.",
249+
"PC_BLS12_MAP_FP_TO_G1": "BLS12-381 map field element to G1. Fixed cost.",
250+
"PC_BLS12_MAP_FP2_TO_G2": "BLS12-381 map field element to G2. Fixed cost.",
251+
"PC_KZG_POINT_EVALUATION": "KZG point evaluation (blob proof verification). Fixed cost.",
252+
"PC_P256VERIFY": "P256 ECDSA signature verification. Fixed cost.",
253+
254+
// Precompiles - Variable gas parameters
255+
"PC_SHA256_BASE": "SHA256 base cost. Total = base + per_word * ceil(len/32).",
256+
"PC_SHA256_PER_WORD": "SHA256 per-word (32 bytes) cost.",
257+
"PC_RIPEMD160_BASE": "RIPEMD160 base cost. Total = base + per_word * ceil(len/32).",
258+
"PC_RIPEMD160_PER_WORD": "RIPEMD160 per-word (32 bytes) cost.",
259+
"PC_ID_BASE": "Identity (data copy) base cost. Total = base + per_word * ceil(len/32).",
260+
"PC_ID_PER_WORD": "Identity per-word (32 bytes) cost.",
261+
"PC_MODEXP_MIN_GAS": "MODEXP minimum gas (floor). Complex formula result is clamped to at least this value.",
262+
"PC_BN254_PAIRING_BASE": "BN254 pairing check base cost. Total = base + per_pair * pairs.",
263+
"PC_BN254_PAIRING_PER_PAIR": "BN254 per-pair cost.",
264+
"PC_BLAKE2F_PER_ROUND": "BLAKE2F per-round cost. Total = per_round * rounds.",
265+
"PC_BLS12_PAIRING_CHECK_BASE": "BLS12-381 pairing check base cost. Total = base + per_pair * pairs.",
266+
"PC_BLS12_PAIRING_CHECK_PER_PAIR": "BLS12-381 per-pair cost.",
267+
"PC_BLS12_G1MSM_MUL_GAS": "BLS12-381 G1 MSM per-point multiplier. Total = k * mul_gas * discount[k] / 1000.",
268+
"PC_BLS12_G2MSM_MUL_GAS": "BLS12-381 G2 MSM per-point multiplier. Total = k * mul_gas * discount[k] / 1000.",
242269
}
243270

244271
// GasScheduleForRules returns default gas values for a fork.
@@ -300,6 +327,39 @@ func GasScheduleForRules(rules *chain.Rules) *CustomGasSchedule {
300327
schedule.Overrides[vm.GasKeySstoreReset] = params.SstoreResetGasEIP2200
301328
}
302329

330+
// Precompile gas defaults (fork-aware)
331+
precompiles := vm.Precompiles(rules)
332+
for _, p := range precompiles {
333+
switch p.Name() {
334+
case "SHA256":
335+
schedule.Overrides[vm.GasKeyPCSha256Base] = params.Sha256BaseGas
336+
schedule.Overrides[vm.GasKeyPCSha256PerWord] = params.Sha256PerWordGas
337+
case "RIPEMD160":
338+
schedule.Overrides[vm.GasKeyPCRipemd160Base] = params.Ripemd160BaseGas
339+
schedule.Overrides[vm.GasKeyPCRipemd160PerWord] = params.Ripemd160PerWordGas
340+
case "ID":
341+
schedule.Overrides[vm.GasKeyPCIdBase] = params.IdentityBaseGas
342+
schedule.Overrides[vm.GasKeyPCIdPerWord] = params.IdentityPerWordGas
343+
case "MODEXP":
344+
schedule.Overrides[vm.GasKeyPCModexpMinGas] = 200
345+
case "BN254_PAIRING":
346+
schedule.Overrides[vm.GasKeyPCBn254PairingBase] = params.Bn254PairingBaseGasIstanbul
347+
schedule.Overrides[vm.GasKeyPCBn254PairingPerPair] = params.Bn254PairingPerPointGasIstanbul
348+
case "BLAKE2F":
349+
schedule.Overrides[vm.GasKeyPCBlake2fPerRound] = 1
350+
case "BLS12_PAIRING_CHECK":
351+
schedule.Overrides[vm.GasKeyPCBls12PairingBase] = params.Bls12381PairingBaseGas
352+
schedule.Overrides[vm.GasKeyPCBls12PairingPerPair] = params.Bls12381PairingPerPairGas
353+
case "BLS12_G1MSM":
354+
schedule.Overrides[vm.GasKeyPCBls12G1MsmMulGas] = params.Bls12381G1MulGas
355+
case "BLS12_G2MSM":
356+
schedule.Overrides[vm.GasKeyPCBls12G2MsmMulGas] = params.Bls12381G2MulGas
357+
default:
358+
// Fixed-gas precompiles: single key IS the total cost
359+
schedule.Overrides["PC_"+p.Name()] = p.RequiredGas(nil)
360+
}
361+
}
362+
303363
return schedule
304364
}
305365

overlay/node/xatu/simulation_rpc.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,7 @@ func (s *Service) executeSingleTransaction(
420420

421421
// Set tracer if provided
422422
if tracer != nil {
423+
tracer.precompiles = vm.Precompiles(chainRules)
423424
statedb.SetHooks(tracer.Hooks())
424425
vmConfig.Tracer = tracer.Hooks()
425426
}

overlay/node/xatu/simulation_tracer.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/erigontech/erigon/execution/tracing"
2323
"github.com/erigontech/erigon/execution/types"
2424
"github.com/erigontech/erigon/execution/types/accounts"
25+
"github.com/erigontech/erigon/execution/vm"
2526
"github.com/holiman/uint256"
2627
)
2728

@@ -74,6 +75,13 @@ type SimulationTracer struct {
7475
pendingCallDepth int // Depth where the CALL was made
7576
pendingCallType string // Opcode name (CALL, STATICCALL, etc.)
7677

78+
// Precompile tracking - gas appears as PC_<name> in the opcode breakdown
79+
pendingPrecompile bool // True if we just entered a precompile call
80+
pendingPrecompileName string // e.g. "PC_SHA256"
81+
82+
// Precompile address->name lookup for gas breakdown attribution
83+
precompiles vm.PrecompiledContracts
84+
7785
// VM context
7886
env *tracing.VMContext
7987
}
@@ -143,6 +151,14 @@ func (t *SimulationTracer) OnEnter(depth int, typ byte, from accounts.Address, t
143151
t.pendingCallType = ""
144152
}
145153

154+
// Track precompile calls for gas breakdown attribution
155+
if precompile && t.precompiles != nil {
156+
if p, ok := t.precompiles[to]; ok {
157+
t.pendingPrecompile = true
158+
t.pendingPrecompileName = "PC_" + p.Name()
159+
}
160+
}
161+
146162
// Truncate address to first 20 chars (0x + 18 hex chars)
147163
addrStr := to.String()
148164
if len(addrStr) > 20 {
@@ -167,6 +183,15 @@ func (t *SimulationTracer) OnExit(depth int, output []byte, gasUsed uint64, err
167183
frame := t.callStack[len(t.callStack)-1]
168184
t.callStack = t.callStack[:len(t.callStack)-1]
169185

186+
// Record precompile gas in the opcode breakdown
187+
if t.pendingPrecompile {
188+
t.gasUsed[t.pendingPrecompileName] += gasUsed
189+
t.opcodeCounts[t.pendingPrecompileName]++
190+
t.totalGasUsed += gasUsed
191+
t.pendingPrecompile = false
192+
t.pendingPrecompileName = ""
193+
}
194+
170195
// Record error if call failed
171196
if err != nil || reverted {
172197
errMsg := "execution reverted"
@@ -281,6 +306,8 @@ func (t *SimulationTracer) Reset() {
281306
t.pendingCallCost = 0
282307
t.pendingCallDepth = 0
283308
t.pendingCallType = ""
309+
t.pendingPrecompile = false
310+
t.pendingPrecompileName = ""
284311
}
285312

286313
// Note: opcodeStrings is defined in tracer.go and shared across the package.

0 commit comments

Comments
 (0)