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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 51 additions & 8 deletions config/gas.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ var (
_ gasGenerator = (*GasPicker)(nil)
_ gasGenerator = (*FixedGasGenerator)(nil)
_ gasGenerator = (*RandomGasGenerator)(nil)
_ gasGenerator = (*UniformGasGenerator)(nil)
_ gasGenerator = (*ZipfianGasGenerator)(nil)
)

type gasGenerator interface {
Expand Down Expand Up @@ -48,12 +50,22 @@ func (g *GasPicker) UnmarshalJSON(data []byte) error {
}
g.delegate = &fixed
return nil
case "random":
var random RandomGasGenerator
if err := json.Unmarshal(data, &random); err != nil {
case "random", "uniform":
var uniform UniformGasGenerator
if err := json.Unmarshal(data, &uniform); err != nil {
return err
}
g.delegate = &random
g.delegate = &uniform
return nil
case "zipfian":
var zipfian ZipfianGasGenerator
if err := json.Unmarshal(data, &zipfian); err != nil {
return err
}
if err := zipfian.dist.validate(); err != nil {
return err

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Zipfian gas skips theta validation

High Severity

UnmarshalJSON calls validate on the zero-value dist rather than the unmarshaled theta, so every zipfian gas config passes. GenerateGas then copies the unchecked theta onto dist and samples. An out-of-range value, including 1.0 where alpha diverges, yields a numerically broken gas distribution instead of a parse error.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c9adbb0. Configure here.

}
g.delegate = &zipfian
return nil
default:
return fmt.Errorf("unknown gas generator name: %s", g.name)
Expand All @@ -74,9 +86,40 @@ type RandomGasGenerator struct {
}

func (r *RandomGasGenerator) GenerateGas(rng *mrand.Rand) (uint64, error) {
if r.Min >= r.Max {
return 0, fmt.Errorf("invalid random gas range: min %d must be less than max %d", r.Min, r.Max)
u := UniformGasGenerator(*r)
return u.GenerateGas(rng)
}

type UniformGasGenerator struct {
Min uint64 `json:"Min"`
Max uint64 `json:"Max"`
}

func (u *UniformGasGenerator) GenerateGas(rng *mrand.Rand) (uint64, error) {
if u.Min >= u.Max {
return 0, fmt.Errorf("invalid uniform gas range: min %d must be less than max %d", u.Min, u.Max)
}
span := u.Max - u.Min + 1
return u.Min + rng.Uint64N(span), nil
}

type ZipfianGasGenerator struct {
Min uint64 `json:"Min"`
Max uint64 `json:"Max"`
Theta float64 `json:"theta"`

dist ZipfianDistribution
}

func (z *ZipfianGasGenerator) GenerateGas(rng *mrand.Rand) (uint64, error) {
if z.Min >= z.Max {
return 0, fmt.Errorf("invalid zipfian gas range: min %d must be less than max %d", z.Min, z.Max)
}
z.dist.Theta = z.Theta
n := z.Max - z.Min + 1
idx, err := z.dist.SampleIndex(rng, n)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Zipfian gas range is uncapped

High Severity

GenerateGas passes Max - Min + 1 to the zipfian sampler, which precomputes zeta in O(n) with no upper bound, and Validate does not cap this span. The same picker type is used for gasFeeCapPicker and gasTipCapPicker, where a typical wei range is billions of values and stalls the first draw with nothing logged.

Fix in Cursor Fix in Web

Triggered by learned rule: config/: Validate() must reject silently-degrading misconfigurations

Reviewed by Cursor Bugbot for commit c9adbb0. Configure here.

if err != nil {
return 0, err
}
span := r.Max - r.Min + 1
return r.Min + rng.Uint64N(span), nil
return z.Min + idx, nil
}
44 changes: 44 additions & 0 deletions config/gas_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,22 @@ func TestGasPicker(t *testing.T) {
require.GreaterOrEqual(t, gas, uint64(20000))
require.LessOrEqual(t, gas, uint64(30000))
})
t.Run("uniform", func(t *testing.T) {
var subject config.GasPicker
require.NoError(t, subject.UnmarshalJSON([]byte(`{"Name":"uniform","Min":20000,"Max":30000}`)))
gas, err := subject.GenerateGas(newTestRng(1))
require.NoError(t, err)
require.GreaterOrEqual(t, gas, uint64(20000))
require.LessOrEqual(t, gas, uint64(30000))
})
t.Run("zipfian", func(t *testing.T) {
var subject config.GasPicker
require.NoError(t, subject.UnmarshalJSON([]byte(`{"Name":"zipfian","Min":20000,"Max":30000,"theta":0.9}`)))
gas, err := subject.GenerateGas(newTestRng(1))
require.NoError(t, err)
require.GreaterOrEqual(t, gas, uint64(20000))
require.LessOrEqual(t, gas, uint64(30000))
})
t.Run("unknown", func(t *testing.T) {
var subject config.GasPicker
require.Error(t, subject.UnmarshalJSON([]byte(`{"Name":"unknown"}`)))
Expand Down Expand Up @@ -87,3 +103,31 @@ func TestGenerateGasForFixedAndEmpty(t *testing.T) {
require.NoError(t, err)
require.Zero(t, gas)
}

// zipfianPicker unmarshals a fresh zipfian gas picker over [min,max] with theta.
func zipfianPicker(t *testing.T, min, max uint64, theta float64) *config.GasPicker {
t.Helper()
var gp config.GasPicker
require.NoError(t, gp.UnmarshalJSON(fmt.Appendf(nil, `{"Name":"zipfian","Min":%d,"Max":%d,"theta":%f}`, min, max, theta)))
return &gp
}

func TestZipfianGasPickerStreamSeeds(t *testing.T) {
const seed, n = 42, 64

seededA := drawN(t, zipfianPicker(t, 20000, 30000, 0.9), newTestRng(seed), n)
seededB := drawN(t, zipfianPicker(t, 20000, 30000, 0.9), newTestRng(seed), n)
require.Equal(t, seededA, seededB, "same seed must reproduce the zipfian draw sequence")
}

func TestZipfianGasPickerRange(t *testing.T) {
const min, max = 20000, 30000
gp := zipfianPicker(t, min, max, 0.99)
rng := newTestRng(1)
for i := 0; i < 1000; i++ {
gas, err := gp.GenerateGas(rng)
require.NoError(t, err)
require.GreaterOrEqual(t, gas, uint64(min))
require.LessOrEqual(t, gas, uint64(max))
}
}