Skip to content
91 changes: 31 additions & 60 deletions pkg/gas/block_history_estimator.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/rpc"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"

"github.com/smartcontractkit/chainlink-common/pkg/logger"
"github.com/smartcontractkit/chainlink-common/pkg/services"
Expand All @@ -35,45 +33,6 @@ import (
// block the application from starting.
var MaxStartTime = 10 * time.Second

var (
promBlockHistoryEstimatorAllGasPricePercentiles = promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "gas_updater_all_gas_price_percentiles",
Help: "Gas price at given percentile",
},
[]string{"percentile", "evmChainID"},
)
promBlockHistoryEstimatorAllTipCapPercentiles = promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "gas_updater_all_tip_cap_percentiles",
Help: "Tip cap at given percentile",
},
[]string{"percentile", "evmChainID"},
)
promBlockHistoryEstimatorSetGasPrice = promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "gas_updater_set_gas_price",
Help: "Gas updater set gas price (in Wei)",
},
[]string{"percentile", "evmChainID"},
)
promBlockHistoryEstimatorSetTipCap = promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "gas_updater_set_tip_cap",
Help: "Gas updater set gas tip cap (in Wei)",
},
[]string{"percentile", "evmChainID"},
)
promBlockHistoryEstimatorCurrentBaseFee = promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "gas_updater_current_base_fee",
Help: "Gas updater current block base fee in Wei",
},
[]string{"evmChainID"},
)
promBlockHistoryEstimatorConnectivityFailureCount = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "block_history_estimator_connectivity_failure_count",
Help: "Counter is incremented every time a gas bump is prevented due to a detected network propagation/connectivity issue",
},
[]string{"evmChainID", "mode"},
)
)

const BumpingHaltedLabel = "Tx gas bumping halted since price exceeds current block prices by significant margin; tx will continue to be rebroadcasted but your node, RPC, or the chain might be experiencing connectivity issues; please investigate and fix ASAP"

var _ EvmEstimator = &BlockHistoryEstimator{}
Expand Down Expand Up @@ -116,13 +75,16 @@ type BlockHistoryEstimator struct {
logger logger.SugaredLogger

l1Oracle rollups.L1Oracle

metrics *blockHistoryEstimatorMetrics
}

// NewBlockHistoryEstimator returns a new BlockHistoryEstimator that listens
// for new heads and updates the base gas price dynamically based on the
// configured percentile of gas prices in that block
func NewBlockHistoryEstimator(lggr logger.Logger, ethClient FeeEstimatorClient, chaintype chaintype.ChainType, eCfg BlockHistoryEstimatorConfig, bhCfg evmconfig.BlockHistory, chainID *big.Int, l1Oracle rollups.L1Oracle) EvmEstimator {
return &BlockHistoryEstimator{
l := logger.Sugared(logger.Named(lggr, "BlockHistoryEstimator"))
b := &BlockHistoryEstimator{
ethClient: ethClient,
chainID: chainID,
chaintype: chaintype,
Expand All @@ -134,26 +96,29 @@ func NewBlockHistoryEstimator(lggr logger.Logger, ethClient FeeEstimatorClient,
mb: mailbox.NewSingle[*evmtypes.Head](),
wg: new(sync.WaitGroup),
stopCh: make(chan struct{}),
logger: logger.Sugared(logger.Named(lggr, "BlockHistoryEstimator")),
logger: l,
l1Oracle: l1Oracle,
metrics: newBlockHistoryEstimatorMetrics(l, chainID),
}
return b
}

// OnNewLongestChain recalculates and sets global gas price if a sampled new head comes
// in and we are not currently fetching
func (b *BlockHistoryEstimator) OnNewLongestChain(_ context.Context, head *evmtypes.Head) {
func (b *BlockHistoryEstimator) OnNewLongestChain(ctx context.Context, head *evmtypes.Head) {
// set latest base fee here to avoid potential lag introduced by block delay
// it is really important that base fee be as up-to-date as possible
b.setLatest(head)
b.setLatest(ctx, head)
b.mb.Deliver(head)
}

// setLatest assumes that head won't be mutated
func (b *BlockHistoryEstimator) setLatest(head *evmtypes.Head) {
func (b *BlockHistoryEstimator) setLatest(ctx context.Context, head *evmtypes.Head) {
// Non-eip1559 blocks don't include base fee
if baseFee := head.BaseFeePerGas; baseFee != nil {
promBlockHistoryEstimatorCurrentBaseFee.WithLabelValues(b.chainID.String()).Set(float64(baseFee.Int64()))
b.metrics.RecordCurrentBaseFee(ctx, float64(baseFee.Int64()))
}

b.logger.Debugw("Set latest block", "blockNum", head.Number, "blockHash", head.Hash, "baseFee", head.BaseFeePerGas, "baseFeeWei", head.BaseFeePerGas.ToInt())
b.latestMu.Lock()
defer b.latestMu.Unlock()
Expand Down Expand Up @@ -197,7 +162,7 @@ func (b *BlockHistoryEstimator) Start(ctx context.Context) error {
b.logger.Warnw("initial check for latest head failed, head was unexpectedly nil")
} else {
b.logger.Debugw("Got latest head", "number", latestHead.Number, "blockHash", latestHead.Hash.Hex())
b.setLatest(latestHead)
b.setLatest(fetchCtx, latestHead)
b.FetchBlocksAndRecalculate(fetchCtx, latestHead)
}

Expand Down Expand Up @@ -318,13 +283,13 @@ func (b *BlockHistoryEstimator) setMaxPercentileTipCap(tipCap *assets.Wei) {
b.maxPercentileTipCap = tipCap
}

func (b *BlockHistoryEstimator) BumpLegacyGas(_ context.Context, originalGasPrice *assets.Wei, gasLimit uint64, maxGasPriceWei *assets.Wei, attempts []EvmPriorAttempt) (bumpedGasPrice *assets.Wei, chainSpecificGasLimit uint64, err error) {
func (b *BlockHistoryEstimator) BumpLegacyGas(ctx context.Context, originalGasPrice *assets.Wei, gasLimit uint64, maxGasPriceWei *assets.Wei, attempts []EvmPriorAttempt) (bumpedGasPrice *assets.Wei, chainSpecificGasLimit uint64, err error) {
if b.bhConfig.CheckInclusionBlocks() > 0 {
if err = b.haltBumping(attempts); err != nil {
if errors.Is(err, fees.ErrConnectivity) {
b.logger.Criticalw(BumpingHaltedLabel, "err", err)
b.SvcErrBuffer.Append(err)
promBlockHistoryEstimatorConnectivityFailureCount.WithLabelValues(b.chainID.String(), "legacy").Inc()
b.metrics.RecordConnectivityFailure(ctx, "legacy")
}
return nil, 0, err
}
Expand Down Expand Up @@ -498,13 +463,14 @@ func calcFeeCap(latestAvailableBaseFeePerGas *assets.Wei, bufferBlocks int, tipC
return feeCap
}

func (b *BlockHistoryEstimator) BumpDynamicFee(_ context.Context, originalFee DynamicFee, maxGasPriceWei *assets.Wei, attempts []EvmPriorAttempt) (bumped DynamicFee, err error) {
func (b *BlockHistoryEstimator) BumpDynamicFee(ctx context.Context, originalFee DynamicFee, maxGasPriceWei *assets.Wei, attempts []EvmPriorAttempt) (bumped DynamicFee, err error) {
if b.bhConfig.CheckInclusionBlocks() > 0 {
if err = b.haltBumping(attempts); err != nil {
if errors.Is(err, fees.ErrConnectivity) {
b.logger.Criticalw(BumpingHaltedLabel, "err", err)
b.SvcErrBuffer.Append(err)
promBlockHistoryEstimatorConnectivityFailureCount.WithLabelValues(b.chainID.String(), "eip1559").Inc()

b.metrics.RecordConnectivityFailure(ctx, "eip1559")
}
return bumped, err
}
Expand Down Expand Up @@ -539,11 +505,11 @@ func (b *BlockHistoryEstimator) FetchBlocksAndRecalculate(ctx context.Context, h
return
}
b.initialFetch.Store(true)
b.Recalculate(head)
b.Recalculate(ctx, head)
}

// Recalculate adds the given heads to the history and recalculates gas price.
func (b *BlockHistoryEstimator) Recalculate(head *evmtypes.Head) {
func (b *BlockHistoryEstimator) Recalculate(ctx context.Context, head *evmtypes.Head) {
lggr := b.logger.With("head", head)
Comment thread
cll-gg marked this conversation as resolved.

blockHistory := b.getBlocks()
Expand All @@ -553,13 +519,13 @@ func (b *BlockHistoryEstimator) Recalculate(head *evmtypes.Head) {
}

// Calculate and set the TransactionPercentile gas price and tip cap to use during gas estimation
b.calculateGasPriceTipCap(lggr, blockHistory, head)
b.calculateGasPriceTipCap(ctx, lggr, blockHistory, head)

// Calculate and set the CheckInclusionPercentile gas price and tip cap to halt excessive bumping
b.calculateMaxPercentileGasPriceTipCap(lggr, blockHistory, head)
}

func (b *BlockHistoryEstimator) calculateGasPriceTipCap(lggr logger.SugaredLogger, blockHistory []evmtypes.Block, head *evmtypes.Head) {
func (b *BlockHistoryEstimator) calculateGasPriceTipCap(ctx context.Context, lggr logger.SugaredLogger, blockHistory []evmtypes.Block, head *evmtypes.Head) {
percentile := int(b.bhConfig.TransactionPercentile())
eip1559 := b.eConfig.EIP1559DynamicFees()

Expand All @@ -571,12 +537,14 @@ func (b *BlockHistoryEstimator) calculateGasPriceTipCap(lggr logger.SugaredLogge
func(gasPrices []*assets.Wei) {
for i := 0; i <= 100; i += 5 {
jdx := ((len(gasPrices) - 1) * i) / 100
promBlockHistoryEstimatorAllGasPricePercentiles.WithLabelValues(fmt.Sprintf("%v%%", i), b.chainID.String()).Set(float64(gasPrices[jdx].Int64()))
percentileLabel := fmt.Sprintf("%v%%", i)
b.metrics.RecordAllGasPricePercentile(ctx, percentileLabel, float64(gasPrices[jdx].Int64()))
}
}, func(tipCaps []*assets.Wei) {
for i := 0; i <= 100; i += 5 {
jdx := ((len(tipCaps) - 1) * i) / 100
promBlockHistoryEstimatorAllTipCapPercentiles.WithLabelValues(fmt.Sprintf("%v%%", i), b.chainID.String()).Set(float64(tipCaps[jdx].Int64()))
percentileLabel := fmt.Sprintf("%v%%", i)
b.metrics.RecordAllTipCapPercentile(ctx, percentileLabel, float64(tipCaps[jdx].Int64()))
}
})
if err != nil {
Expand All @@ -602,7 +570,9 @@ func (b *BlockHistoryEstimator) calculateGasPriceTipCap(lggr logger.SugaredLogge
"priceBlocks", numsForPrice,
}
b.setPercentileGasPrice(percentileGasPrice)
promBlockHistoryEstimatorSetGasPrice.WithLabelValues(fmt.Sprintf("%v%%", percentile), b.chainID.String()).Set(float64(percentileGasPrice.Int64()))

percentileLabel := fmt.Sprintf("%v%%", percentile)
b.metrics.RecordSetGasPrice(ctx, percentileLabel, float64(percentileGasPrice.Int64()))

if !eip1559 {
lggr.Debugw(fmt.Sprintf("Setting new default GasPrice: %v Gwei", gasPriceGwei), lggrFields...)
Expand All @@ -616,7 +586,8 @@ func (b *BlockHistoryEstimator) calculateGasPriceTipCap(lggr logger.SugaredLogge
}...)
lggr.Debugw(fmt.Sprintf("Setting new default prices, GasPrice: %v Gwei, TipCap: %v Gwei", gasPriceGwei, tipCapGwei), lggrFields...)
b.setPercentileTipCap(percentileTipCap)
promBlockHistoryEstimatorSetTipCap.WithLabelValues(fmt.Sprintf("%v%%", percentile), b.chainID.String()).Set(float64(percentileTipCap.Int64()))

b.metrics.RecordSetTipCap(ctx, percentileLabel, float64(percentileTipCap.Int64()))
}

func (b *BlockHistoryEstimator) calculateMaxPercentileGasPriceTipCap(lggr logger.SugaredLogger, blockHistory []evmtypes.Block, head *evmtypes.Head) {
Expand Down
151 changes: 151 additions & 0 deletions pkg/gas/block_history_estimator_metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package gas

import (
"context"
"math/big"

"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"

"github.com/smartcontractkit/chainlink-common/pkg/beholder"
"github.com/smartcontractkit/chainlink-common/pkg/logger"
)

var (
promBlockHistoryEstimatorAllGasPricePercentiles = promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "gas_updater_all_gas_price_percentiles",
Help: "Gas price at given percentile",
}, []string{"percentile", "evmChainID"})
promBlockHistoryEstimatorAllTipCapPercentiles = promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "gas_updater_all_tip_cap_percentiles",
Help: "Tip cap at given percentile",
}, []string{"percentile", "evmChainID"})
promBlockHistoryEstimatorSetGasPrice = promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "gas_updater_set_gas_price",
Help: "Gas updater set gas price (in Wei)",
}, []string{"percentile", "evmChainID"})
promBlockHistoryEstimatorSetTipCap = promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "gas_updater_set_tip_cap",
Help: "Gas updater set gas tip cap (in Wei)",
}, []string{"percentile", "evmChainID"})
promBlockHistoryEstimatorCurrentBaseFee = promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "gas_updater_current_base_fee",
Help: "Gas updater current block base fee in Wei",
}, []string{"evmChainID"})
promBlockHistoryEstimatorConnectivityFailureCount = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "block_history_estimator_connectivity_failure_count",
Help: "Counter is incremented every time a gas bump is prevented due to a detected network propagation/connectivity issue",
}, []string{"evmChainID", "mode"})
)

// blockHistoryEstimatorMetrics dual-writes to Prometheus and optional Beholder OTel instruments.
// All Record* methods are safe to call when Beholder instruments are nil (no-op for OTel side).
type blockHistoryEstimatorMetrics struct {
chainID string

gasPriceGauge metric.Float64Gauge
tipCapGauge metric.Float64Gauge
allGasPricePercentilesGauge metric.Float64Gauge
allTipCapPercentilesGauge metric.Float64Gauge
currentBaseFeeGauge metric.Float64Gauge
connectivityFailureCountCounter metric.Int64Counter
}

func newBlockHistoryEstimatorMetrics(lggr logger.SugaredLogger, chainID *big.Int) *blockHistoryEstimatorMetrics {
m := &blockHistoryEstimatorMetrics{chainID: chainID.String()}

// otel
if g, err := beholder.GetMeter().Float64Gauge("gas_updater_set_gas_price"); err != nil {
lggr.Errorw("Failed to register Beholder gas_updater_set_gas_price gauge", "err", err)
} else {
m.gasPriceGauge = g
}
if g, err := beholder.GetMeter().Float64Gauge("gas_updater_set_tip_cap"); err != nil {
lggr.Errorw("Failed to register Beholder gas_updater_set_tip_cap gauge", "err", err)
} else {
m.tipCapGauge = g
}
if g, err := beholder.GetMeter().Float64Gauge("gas_updater_all_gas_price_percentiles"); err != nil {
lggr.Errorw("Failed to register Beholder gas_updater_all_gas_price_percentiles gauge", "err", err)
} else {
m.allGasPricePercentilesGauge = g
}
if g, err := beholder.GetMeter().Float64Gauge("gas_updater_all_tip_cap_percentiles"); err != nil {
lggr.Errorw("Failed to register Beholder gas_updater_all_tip_cap_percentiles gauge", "err", err)
} else {
m.allTipCapPercentilesGauge = g
}
if g, err := beholder.GetMeter().Float64Gauge("gas_updater_current_base_fee"); err != nil {
lggr.Errorw("Failed to register Beholder gas_updater_current_base_fee gauge", "err", err)
} else {
m.currentBaseFeeGauge = g
}
if c, err := beholder.GetMeter().Int64Counter("block_history_estimator_connectivity_failure_count"); err != nil {
lggr.Errorw("Failed to register Beholder block_history_estimator_connectivity_failure_count counter", "err", err)
} else {
m.connectivityFailureCountCounter = c
}
Comment thread
cll-gg marked this conversation as resolved.

return m
}

func (m *blockHistoryEstimatorMetrics) RecordSetGasPrice(ctx context.Context, percentileLabel string, value float64) {
promBlockHistoryEstimatorSetGasPrice.WithLabelValues(percentileLabel, m.chainID).Set(value)
if m.gasPriceGauge != nil {
m.gasPriceGauge.Record(ctx, value, metric.WithAttributes(
attribute.String("chainID", m.chainID),
attribute.String("percentile", percentileLabel),
))
}
}

func (m *blockHistoryEstimatorMetrics) RecordSetTipCap(ctx context.Context, percentileLabel string, value float64) {
promBlockHistoryEstimatorSetTipCap.WithLabelValues(percentileLabel, m.chainID).Set(value)
if m.tipCapGauge != nil {
m.tipCapGauge.Record(ctx, value, metric.WithAttributes(
attribute.String("chainID", m.chainID),
attribute.String("percentile", percentileLabel),
))
}
}

func (m *blockHistoryEstimatorMetrics) RecordAllGasPricePercentile(ctx context.Context, percentileLabel string, value float64) {
promBlockHistoryEstimatorAllGasPricePercentiles.WithLabelValues(percentileLabel, m.chainID).Set(value)
if m.allGasPricePercentilesGauge != nil {
m.allGasPricePercentilesGauge.Record(ctx, value, metric.WithAttributes(
attribute.String("chainID", m.chainID),
attribute.String("percentile", percentileLabel),
))
}
}

func (m *blockHistoryEstimatorMetrics) RecordAllTipCapPercentile(ctx context.Context, percentileLabel string, value float64) {
promBlockHistoryEstimatorAllTipCapPercentiles.WithLabelValues(percentileLabel, m.chainID).Set(value)
if m.allTipCapPercentilesGauge != nil {
m.allTipCapPercentilesGauge.Record(ctx, value, metric.WithAttributes(
attribute.String("chainID", m.chainID),
attribute.String("percentile", percentileLabel),
))
}
}

func (m *blockHistoryEstimatorMetrics) RecordCurrentBaseFee(ctx context.Context, value float64) {
promBlockHistoryEstimatorCurrentBaseFee.WithLabelValues(m.chainID).Set(value)
if m.currentBaseFeeGauge != nil {
m.currentBaseFeeGauge.Record(ctx, value, metric.WithAttributes(
attribute.String("chainID", m.chainID),
))
}
}

func (m *blockHistoryEstimatorMetrics) RecordConnectivityFailure(ctx context.Context, mode string) {
promBlockHistoryEstimatorConnectivityFailureCount.WithLabelValues(m.chainID, mode).Inc()
if m.connectivityFailureCountCounter != nil {
m.connectivityFailureCountCounter.Add(ctx, 1, metric.WithAttributes(
attribute.String("chainID", m.chainID),
attribute.String("mode", mode),
))
}
}
Loading
Loading