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
62 changes: 62 additions & 0 deletions chain/consensus/compute_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package consensus

import (
"context"
"sort"
"sync/atomic"
"time"

Expand Down Expand Up @@ -215,6 +216,18 @@ func (t *TipSetExecutor) ApplyBlocks(ctx context.Context,

var msgGas int64

// Track gas metrics for synced blocks
type msgGasMetrics struct {
gasLimit int64
gasUsed int64
}
var (
totalGasLimit int64
totalGasUsed int64
messageCount int64
msgGasList []msgGasMetrics
)

for _, b := range bms {
penalty := types.NewInt(0)
gasReward := big.Zero()
Expand All @@ -231,6 +244,16 @@ func (t *TipSetExecutor) ApplyBlocks(ctx context.Context,

msgGas += r.GasUsed

// Accumulate gas metrics
totalGasLimit += m.GasLimit
totalGasUsed += r.GasUsed
messageCount++
msgGasList = append(msgGasList, msgGasMetrics{gasLimit: m.GasLimit, gasUsed: r.GasUsed})

// Record per-message histograms
stats.Record(ctx, metrics.SyncedBlockMessageGasLimit.M(m.GasLimit))
stats.Record(ctx, metrics.SyncedBlockMessageGasUsed.M(r.GasUsed))

receipts = append(receipts, &r.MessageReceipt)
gasReward = big.Add(gasReward, r.GasCosts.MinerTip)
penalty = big.Add(penalty, r.GasCosts.MinerPenalty)
Expand Down Expand Up @@ -330,6 +353,45 @@ func (t *TipSetExecutor) ApplyBlocks(ctx context.Context,
metrics.VMApplyCronGas.M(cronGas),
)

// Record synced block gas metrics
stats.Record(ctx,
metrics.SyncedBlockTotalGasLimit.M(totalGasLimit),
metrics.SyncedBlockTotalGasUsed.M(totalGasUsed),
metrics.SyncedBlockMessageCount.M(messageCount),
)

// Compute and record medians if there are messages
if len(msgGasList) > 0 {
// Median gas limit (by message count)
sort.Slice(msgGasList, func(i, j int) bool { return msgGasList[i].gasLimit < msgGasList[j].gasLimit })
stats.Record(ctx, metrics.SyncedBlockGasLimitMedian.M(msgGasList[len(msgGasList)/2].gasLimit))

// Gas-weighted median of gas limit (what gas limit the median executed gas unit had)
halfGasUsed := totalGasUsed / 2
var accumulatedGas int64
for _, m := range msgGasList {
accumulatedGas += m.gasUsed
if accumulatedGas >= halfGasUsed {
stats.Record(ctx, metrics.SyncedBlockGasLimitMedianByGasUnits.M(m.gasLimit))
break
}
}

// Median gas used (by message count)
sort.Slice(msgGasList, func(i, j int) bool { return msgGasList[i].gasUsed < msgGasList[j].gasUsed })
stats.Record(ctx, metrics.SyncedBlockGasUsedMedian.M(msgGasList[len(msgGasList)/2].gasUsed))

// Gas-weighted median of gas used (what gas used the median gas unit had)
accumulatedGas = 0
for _, m := range msgGasList {
accumulatedGas += m.gasUsed
if accumulatedGas >= halfGasUsed {
stats.Record(ctx, metrics.SyncedBlockGasUsedMedianByGasUnits.M(m.gasUsed))
break
}
}
Comment on lines +384 to +392
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

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

The gas-weighted median calculation for gas used is incorrect. The code sorts by gasUsed and then accumulates gasUsed, which just computes the regular median of gasUsed, not a gas-weighted median. For a proper gas-weighted median, the list should be sorted by gasUsed but the accumulated weight should use a different field (like gasLimit). Currently, this metric is redundant with SyncedBlockGasUsedMedian. Consider either removing this metric or fixing the weighting logic to accumulate by gasLimit instead of gasUsed.

Copilot uses AI. Check for mistakes.
}

return st, rectroot, nil
}

Expand Down
105 changes: 105 additions & 0 deletions chain/messagepool/messagepool.go
Original file line number Diff line number Diff line change
Expand Up @@ -1440,6 +1440,11 @@ func (mp *MessagePool) HeadChange(ctx context.Context, revert []*types.TipSet, a
}
}

// Record gas metrics after head change
mp.lk.RLock()
mp.recordGasMetrics(ctx)
mp.lk.RUnlock()

return merr
}

Expand Down Expand Up @@ -1666,6 +1671,106 @@ func getBaseFeeLowerBound(baseFee, factor types.BigInt) types.BigInt {
return baseFeeLowerBound
}

// msgGasInfo holds gas-related info for a message, used for computing weighted medians
type msgGasInfo struct {
gasLimit int64
gasPremium big.Int
gasFeeCap big.Int
}

// bigIntToFloat64 converts a big.Int to float64 for metrics recording
func bigIntToFloat64(b big.Int) float64 {
if b.Int == nil {
return 0
}
f, _ := new(stdbig.Float).SetInt(b.Int).Float64()
return f
}

// recordGasMetrics records gas-related metrics for the current mpool state.
// Should be called with mp.lk held (at least RLock) and mp.curTsLk held.
func (mp *MessagePool) recordGasMetrics(ctx context.Context) {
// Get basefee from current tipset
if mp.curTs != nil && len(mp.curTs.Blocks()) > 0 {
baseFee := mp.curTs.Blocks()[0].ParentBaseFee
stats.Record(ctx, metrics.BaseFee.M(bigIntToFloat64(baseFee)))
}

// Collect gas metrics from pending messages
var totalGasLimit int64
var totalPremium big.Int = big.Zero()
var totalFeeCap big.Int = big.Zero()
var msgs []msgGasInfo

mp.forEachPending(func(_ address.Address, s *msgSet) {
for _, m := range s.msgs {
gasLimit := m.Message.GasLimit
totalGasLimit += gasLimit
totalPremium = big.Add(totalPremium, m.Message.GasPremium)
totalFeeCap = big.Add(totalFeeCap, m.Message.GasFeeCap)
msgs = append(msgs, msgGasInfo{
gasLimit: gasLimit,
gasPremium: m.Message.GasPremium,
gasFeeCap: m.Message.GasFeeCap,
})
// Record histograms
stats.Record(ctx, metrics.MpoolMessageGasLimit.M(gasLimit))
stats.Record(ctx, metrics.MpoolGasPremium.M(bigIntToFloat64(m.Message.GasPremium)))
stats.Record(ctx, metrics.MpoolGasFeeCap.M(bigIntToFloat64(m.Message.GasFeeCap)))
}
})

msgCount := int64(len(msgs))
stats.Record(ctx, metrics.MpoolTotalGasLimit.M(totalGasLimit))

// Record mean and median gas metrics (avoid division by zero)
if msgCount > 0 {
// Gas Premium statistics
meanPremium := big.Div(totalPremium, big.NewInt(msgCount))
stats.Record(ctx, metrics.MpoolGasPremiumMean.M(bigIntToFloat64(meanPremium)))

// Gas Fee Cap statistics
meanFeeCap := big.Div(totalFeeCap, big.NewInt(msgCount))
stats.Record(ctx, metrics.MpoolGasFeeCapMean.M(bigIntToFloat64(meanFeeCap)))

// Compute median gas limit (by message count)
sort.Slice(msgs, func(i, j int) bool { return msgs[i].gasLimit < msgs[j].gasLimit })
medianGasLimit := msgs[len(msgs)/2].gasLimit
stats.Record(ctx, metrics.MpoolGasLimitMedian.M(medianGasLimit))

// Compute median gas premium (by message count)
sort.Slice(msgs, func(i, j int) bool { return msgs[i].gasPremium.LessThan(msgs[j].gasPremium) })
medianPremium := msgs[len(msgs)/2].gasPremium
stats.Record(ctx, metrics.MpoolGasPremiumMedian.M(bigIntToFloat64(medianPremium)))

// Compute gas-weighted median of gas premium
halfGas := totalGasLimit / 2
var accumulatedGas int64
for _, m := range msgs {
accumulatedGas += m.gasLimit
if accumulatedGas >= halfGas {
stats.Record(ctx, metrics.MpoolGasPremiumMedianByGasUnits.M(bigIntToFloat64(m.gasPremium)))
break
}
}

// Compute median gas fee cap (by message count)
sort.Slice(msgs, func(i, j int) bool { return msgs[i].gasFeeCap.LessThan(msgs[j].gasFeeCap) })
medianFeeCap := msgs[len(msgs)/2].gasFeeCap
stats.Record(ctx, metrics.MpoolGasFeeCapMedian.M(bigIntToFloat64(medianFeeCap)))

// Compute gas-weighted median of gas fee cap
accumulatedGas = 0
for _, m := range msgs {
accumulatedGas += m.gasLimit
if accumulatedGas >= halfGas {
stats.Record(ctx, metrics.MpoolGasFeeCapMedianByGasUnits.M(bigIntToFloat64(m.gasFeeCap)))
break
}
}
}
}

type MpoolNonceAPI interface {
GetNonce(context.Context, address.Address, types.TipSetKey) (uint64, error)
GetActor(context.Context, address.Address, types.TipSetKey) (*types.Actor, error)
Expand Down
Loading
Loading