Skip to content
Draft
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
49 changes: 48 additions & 1 deletion eth/gaspricemonitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ type GasPriceMonitor struct {
// maxGasPrice is the max acceptable gas price defined by the user
maxGasPrice *big.Int

avgGasPrice *big.Int
smoothingNum uint64
smoothingDen uint64

// update is a channel used to send notifications to a listener
// when the gas price is updated
update chan struct{}
Expand All @@ -61,6 +65,9 @@ func NewGasPriceMonitor(gpo GasPriceOracle, pollingInterval time.Duration, minGa
gasPrice: big.NewInt(0),
minGasPrice: minGasP,
maxGasPrice: maxGasPrice,
smoothingNum: uint64(1),
smoothingDen: uint64(5),
avgGasPrice: nil,
}
}

Expand All @@ -76,6 +83,25 @@ func (gpm *GasPriceMonitor) GasPrice() *big.Int {
return gpm.gasPrice
}

// AvgGasPrice returns a rolling average of observed gas prices
func (gpm *GasPriceMonitor) AvgGasPrice() *big.Int {
gpm.gasPriceMu.RLock()
defer gpm.gasPriceMu.RUnlock()

var result *big.Int
if gpm.avgGasPrice == nil {
result = new(big.Int).Set(gpm.gasPrice)
} else {
result = new(big.Int).Set(gpm.avgGasPrice)
}

if gpm.minGasPrice != nil && result.Cmp(gpm.minGasPrice) < 0 {
result = new(big.Int).Set(gpm.minGasPrice)
}

return result
}

func (gpm *GasPriceMonitor) SetMinGasPrice(minGasPrice *big.Int) {
gpm.gasPriceMu.Lock()
defer gpm.gasPriceMu.Unlock()
Expand Down Expand Up @@ -192,5 +218,26 @@ func (gpm *GasPriceMonitor) updateGasPrice(gasPrice *big.Int) {
gpm.gasPriceMu.Lock()
defer gpm.gasPriceMu.Unlock()

gpm.gasPrice = gasPrice
gpm.gasPrice = new(big.Int).Set(gasPrice)
gpm.updateAvgLocked(gasPrice)
}

func (gpm *GasPriceMonitor) updateAvgLocked(gasPrice *big.Int) {
if gpm.avgGasPrice == nil {
gpm.avgGasPrice = new(big.Int).Set(gasPrice)
return
}

den := int64(gpm.smoothingDen)
num := int64(gpm.smoothingNum)
if den <= 0 || num <= 0 || num >= den {
num = 1
den = 5
}
Comment on lines +232 to +236
Copy link
Preview

Copilot AI Sep 25, 2025

Choose a reason for hiding this comment

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

The fallback values num = 1 and den = 5 are magic numbers that duplicate the default values set in NewGasPriceMonitor. Consider extracting these as named constants to maintain consistency and make the relationship clear.

Copilot uses AI. Check for mistakes.


term1 := new(big.Int).Mul(gpm.avgGasPrice, big.NewInt(den-num))
term2 := new(big.Int).Mul(gasPrice, big.NewInt(num))
term1.Add(term1, term2)
term1.Add(term1, new(big.Int).Div(big.NewInt(den), big.NewInt(2)))
Copy link
Preview

Copilot AI Sep 25, 2025

Choose a reason for hiding this comment

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

The addition of den/2 appears to be for rounding purposes but is not documented. This magic number makes the calculation unclear. Consider adding a comment explaining this is for rounding to nearest integer in the division, or extract it to a named variable like roundingOffset.

Suggested change
term1.Add(term1, new(big.Int).Div(big.NewInt(den), big.NewInt(2)))
// Add roundingOffset to round to the nearest integer when dividing
roundingOffset := big.NewInt(den / 2)
term1.Add(term1, roundingOffset)

Copilot uses AI. Check for mistakes.

gpm.avgGasPrice = term1.Div(term1, big.NewInt(den))
}
49 changes: 49 additions & 0 deletions eth/gaspricemonitor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,55 @@ func TestStop(t *testing.T) {
assert.False(ok)
}

func TestAvgGasPrice(t *testing.T) {
gasPrice1 := big.NewInt(100)
gpo := newStubGasPriceOracle(gasPrice1)

gpm := NewGasPriceMonitor(gpo, 1*time.Hour, big.NewInt(0), nil)

update, err := gpm.Start(context.Background())
require.NotNil(t, update)
require.Nil(t, err)
defer gpm.Stop()

assert.Equal(t, gasPrice1, gpm.AvgGasPrice())

gasPrice2 := big.NewInt(200)
gpm.updateGasPrice(gasPrice2)
assert.Equal(t, big.NewInt(120), gpm.AvgGasPrice())

gasPrice3 := big.NewInt(50)
gpm.updateGasPrice(gasPrice3)
assert.Equal(t, big.NewInt(106), gpm.AvgGasPrice())
}

func TestAvgGasPriceFallback(t *testing.T) {
min := big.NewInt(3)
gpm := NewGasPriceMonitor(newStubGasPriceOracle(big.NewInt(5)), 1*time.Hour, min, nil)

assert.Equal(t, min, gpm.AvgGasPrice())

gpm.updateGasPrice(big.NewInt(10))
assert.Equal(t, big.NewInt(10), gpm.AvgGasPrice())
}

func TestAvgGasPriceIgnoresBelowMin(t *testing.T) {
min := big.NewInt(10)
gpo := newStubGasPriceOracle(big.NewInt(5))
gpm := NewGasPriceMonitor(gpo, 1*time.Hour, min, nil)

update, err := gpm.Start(context.Background())
require.NotNil(t, update)
require.Nil(t, err)
defer gpm.Stop()

assert.Equal(t, min, gpm.GasPrice())
assert.Equal(t, min, gpm.AvgGasPrice())

gpm.updateGasPrice(big.NewInt(40))
assert.Equal(t, big.NewInt(40), gpm.AvgGasPrice())
}

func TestMinGasPrice(t *testing.T) {
gasPrice := big.NewInt(777)
gpo := newStubGasPriceOracle(gasPrice)
Expand Down
8 changes: 3 additions & 5 deletions pm/recipient.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,6 @@ var paramsExpiryBuffer = int64(1)

var evMultiplier = big.NewInt(100)

// Hardcode to 3 gwei
// TODO: Replace this hardcoded value by dynamically determining the average gas price during a period of time
var avgGasPrice = new(big.Int).Mul(big.NewInt(3), new(big.Int).Exp(big.NewInt(10), big.NewInt(9), nil))

// Recipient is an interface which describes an object capable
// of receiving tickets
type Recipient interface {
Expand Down Expand Up @@ -74,6 +70,7 @@ type TicketParamsConfig struct {
// GasPriceMonitor defines methods for monitoring gas prices
type GasPriceMonitor interface {
GasPrice() *big.Int
AvgGasPrice() *big.Int
}

// recipient is an implementation of the Recipient interface that
Expand Down Expand Up @@ -307,7 +304,8 @@ func (r *recipient) faceValue(sender ethcommon.Address) (*big.Int, error) {
// The expectation is that if the avg gasPrice check runs and passes then it is reasonable to advertise ticket params
// because there is a good chance that the current gasPrice will come back down by the time a winning ticket is received
// and needs to be redeemed.
// For now, avgGasPrice is hardcoded. See the comment for avgGasPrice for TODO information.
// avgGasPrice is derived from the gas price monitor's rolling average to smooth transient spikes.
avgGasPrice := r.gpm.AvgGasPrice()
if faceValue.Cmp(txCost) < 0 && faceValue.Cmp(r.txCostWithGasPrice(avgGasPrice)) < 0 {
return nil, errInsufficientSenderReserve
}
Expand Down
6 changes: 4 additions & 2 deletions pm/recipient_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func newRecipientFixtureOrFatal(_ *testing.T) (ethcommon.Address, *stubBroker, *
v := &stubValidator{}
v.SetIsValidTicket(true)

gm := &stubGasPriceMonitor{gasPrice: big.NewInt(100)}
gm := &stubGasPriceMonitor{gasPrice: big.NewInt(100), avgPrice: big.NewInt(100)}
sm := newStubSenderMonitor()
sm.maxFloat = big.NewInt(10000000000)
tm := &stubTimeManager{lastSeenBlock: big.NewInt(1), round: big.NewInt(1), blkHash: RandHash(), preBlkHash: RandHash()}
Expand Down Expand Up @@ -532,9 +532,11 @@ func TestTicketParams(t *testing.T) {

// Test faceValue < txCostWithGasPrice(current gasPrice) and faceValue > txCostWithGasPrice(avg gasPrice)
// Set current gasPrice higher than avg gasPrice
avgGasPrice := big.NewInt(200)
gm.avgPrice = new(big.Int).Set(avgGasPrice)
gm.gasPrice = new(big.Int).Add(avgGasPrice, big.NewInt(1))
txCost := new(big.Int).Mul(big.NewInt(int64(cfg.RedeemGas)), gm.gasPrice)
txCostAvgGasPrice := new(big.Int).Mul(big.NewInt(int64(cfg.RedeemGas)), avgGasPrice)
txCostAvgGasPrice := new(big.Int).Mul(big.NewInt(int64(cfg.RedeemGas)), gm.avgPrice)
sm.maxFloat = new(big.Int).Sub(txCost, big.NewInt(1))
require.True(sm.maxFloat.Cmp(txCost) < 0)
require.True(sm.maxFloat.Cmp(txCostAvgGasPrice) > 0)
Expand Down
8 changes: 8 additions & 0 deletions pm/stub.go
Original file line number Diff line number Diff line change
Expand Up @@ -373,12 +373,20 @@ func (s *stubSenderManager) SubscribeReserveChange(sink chan<- ethcommon.Address

type stubGasPriceMonitor struct {
gasPrice *big.Int
avgPrice *big.Int
}

func (s *stubGasPriceMonitor) GasPrice() *big.Int {
return s.gasPrice
}

func (s *stubGasPriceMonitor) AvgGasPrice() *big.Int {
if s.avgPrice == nil {
return nil
}
return new(big.Int).Set(s.avgPrice)
}

type stubSenderMonitor struct {
maxFloat *big.Int
redeemable chan *redemption
Expand Down
Loading