diff --git a/app/app.go b/app/app.go index ec1c7e8324..819c0e0b78 100644 --- a/app/app.go +++ b/app/app.go @@ -1327,7 +1327,7 @@ func (app *App) FinalizeBlocker(ctx sdk.Context, req *abci.RequestFinalizeBlock) cms := app.WriteState() app.LightInvarianceChecks(ctx.Context(), cms, app.lightInvarianceConfig) appHash := app.GetWorkingHash() - resp := app.getFinalizeBlockResponse(appHash, events, txRes, endBlockResp, consensusParamUpdates) + resp := app.getFinalizeBlockResponse(ctx.Context(), appHash, events, txRes, endBlockResp, consensusParamUpdates) if hasHeadNotifier { headNotifier.Stash(req, &resp) } @@ -1357,7 +1357,7 @@ func (app *App) FinalizeBlocker(ctx sdk.Context, req *abci.RequestFinalizeBlock) cms := app.WriteState() app.LightInvarianceChecks(ctx.Context(), cms, app.lightInvarianceConfig) appHash := app.GetWorkingHash() - resp := app.getFinalizeBlockResponse(appHash, events, txResults, endBlockResp, consensusParamUpdates) + resp := app.getFinalizeBlockResponse(ctx.Context(), appHash, events, txResults, endBlockResp, consensusParamUpdates) if hasHeadNotifier { headNotifier.Stash(req, &resp) } @@ -2318,6 +2318,7 @@ func (app *App) DecodeTransactionsConcurrently(ctx sdk.Context, txs [][]byte) [] } func (app *App) getFinalizeBlockResponse( + ctx context.Context, appHash []byte, events []abci.Event, txResults []*abci.ExecTxResult, @@ -2327,6 +2328,14 @@ func (app *App) getFinalizeBlockResponse( if app.EvmKeeper.EthReplayConfig.Enabled || app.EvmKeeper.EthBlockTestConfig.Enabled { return abci.ResponseFinalizeBlock{} } + + // Both FinalizeBlocker paths that build a response converge here, so this records once + // per finalized block; ProcessBlock runs twice for a height whose optimistic result is + // discarded. + if gasUsed, ok := sumBlockGasUsed(txResults); ok { + appMetrics.blockGasUsed.Record(ctx, gasUsed) + } + return abci.ResponseFinalizeBlock{ Events: events, TxResults: txResults, @@ -2341,6 +2350,24 @@ func (app *App) getFinalizeBlockResponse( } } +// sumBlockGasUsed totals the gas consumed by every non-nil transaction result in a block. +// It reports false when a result carries negative gas or when the total overflows int64; +// the total is then meaningless and callers must discard it. +func sumBlockGasUsed(txResults []*abci.ExecTxResult) (int64, bool) { + var total int64 + for _, txResult := range txResults { + if txResult == nil { + continue + } + gasUsed := txResult.GasUsed + if gasUsed < 0 || total > math.MaxInt64-gasUsed { + return 0, false + } + total += gasUsed + } + return total, true +} + func cloneConsensusParams(params *tmproto.ConsensusParams) *tmproto.ConsensusParams { if params == nil { return nil diff --git a/app/block_gas_used_test.go b/app/block_gas_used_test.go new file mode 100644 index 0000000000..5cf0de0a7d --- /dev/null +++ b/app/block_gas_used_test.go @@ -0,0 +1,61 @@ +package app + +import ( + "math" + "testing" + + "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" + "github.com/stretchr/testify/require" +) + +// gasUsedResults builds one ExecTxResult per gas value, matching the shape +// FinalizeBlock hands to sumBlockGasUsed. +func gasUsedResults(gasUsed ...int64) []*types.ExecTxResult { + results := make([]*types.ExecTxResult, 0, len(gasUsed)) + for _, g := range gasUsed { + results = append(results, &types.ExecTxResult{GasUsed: g}) + } + return results +} + +// TestSumBlockGasUsedEmptyBlock records a zero total for a block with no transactions, so +// the histogram carries one sample per finalized block. +func TestSumBlockGasUsedEmptyBlock(t *testing.T) { + total, ok := sumBlockGasUsed(nil) + require.True(t, ok) + require.Zero(t, total) +} + +// TestSumBlockGasUsedSumsResults covers the ordinary path. +func TestSumBlockGasUsedSumsResults(t *testing.T) { + total, ok := sumBlockGasUsed(gasUsedResults(21_000, 100_000, 0, 5_000_000)) + require.True(t, ok) + require.Equal(t, int64(5_121_000), total) +} + +// TestSumBlockGasUsedSkipsNilResults exercises the nil-entry branch of the accounting loop. +func TestSumBlockGasUsedSkipsNilResults(t *testing.T) { + results := []*types.ExecTxResult{nil, {GasUsed: 21_000}, nil, {GasUsed: 42_000}} + total, ok := sumBlockGasUsed(results) + require.True(t, ok) + require.Equal(t, int64(63_000), total) +} + +// TestSumBlockGasUsedRejectsNegativeGas exercises the negative-gas branch. +func TestSumBlockGasUsedRejectsNegativeGas(t *testing.T) { + _, ok := sumBlockGasUsed(gasUsedResults(21_000, -1)) + require.False(t, ok) +} + +// TestSumBlockGasUsedRejectsOverflow exercises the int64 overflow branch. +func TestSumBlockGasUsedRejectsOverflow(t *testing.T) { + _, ok := sumBlockGasUsed(gasUsedResults(math.MaxInt64, 1)) + require.False(t, ok) +} + +// TestSumBlockGasUsedAcceptsExactInt64Max confirms the overflow guard is not off by one. +func TestSumBlockGasUsedAcceptsExactInt64Max(t *testing.T) { + total, ok := sumBlockGasUsed(gasUsedResults(math.MaxInt64-1, 1)) + require.True(t, ok) + require.Equal(t, int64(math.MaxInt64), total) +} diff --git a/app/consensus_params_test.go b/app/consensus_params_test.go index 8d60fc16fd..aecfadcdc0 100644 --- a/app/consensus_params_test.go +++ b/app/consensus_params_test.go @@ -56,6 +56,7 @@ func TestGetFinalizeBlockResponsePropagatesFullConsensusParams(t *testing.T) { app := &App{} resp := app.getFinalizeBlockResponse( + t.Context(), []byte("hash"), nil, nil, diff --git a/app/metrics.go b/app/metrics.go index 1b1168cdc6..1e3de602e6 100644 --- a/app/metrics.go +++ b/app/metrics.go @@ -25,7 +25,9 @@ var ( 0.000025, 0.000050, 0.0001, 0.0005, 0.001, 0.0025, 0.005, 0.010, 0.020, 0.050, 0.075, 0.1, 0.25, 0.5, 1, 10, ) - // blockGasWantedBuckets covers the 0–50 M gas range (current MaxGasWanted cap) + // blockGasWantedBuckets covers the 0–50 M gas range (current MaxGasWanted cap). + // The per-block gas wanted and gas used histograms share it: a transaction's gas + // used is bounded by its gas wanted, so the same cap bounds both block totals. blockGasWantedBuckets = metric.WithExplicitBucketBoundaries( 10e3, 25e3, 50e3, 100e3, 250e3, 500e3, 1e6, 2.5e6, 5e6, 10e6, 12.5e6, 25e6, 50e6, @@ -66,6 +68,7 @@ var ( // Per-block gas utilisation blockGasWanted metric.Int64Histogram blockGasWantedRatio metric.Float64Histogram + blockGasUsed metric.Int64Histogram // Light invariance check invarianceDuration metric.Float64Histogram @@ -180,6 +183,16 @@ var ( blockGasWantedRatioBuckets, )), + blockGasUsed: must(meter.Int64Histogram( + "app_block_gas_used", + metric.WithDescription("Per-block total gas used across all transactions. Recorded on "+ + "FinalizeBlock, so unlike app_block_gas_wanted (recorded on ProcessProposal) it has "+ + "samples during block/state sync with no matching gas_wanted sample -- do not divide "+ + "the two without accounting for that"), + metric.WithUnit("{gas}"), + blockGasWantedBuckets, + )), + invarianceDuration: must(meter.Float64Histogram( "app_lightinvariance_supply_duration", metric.WithDescription("Duration of light invariance total supply check"),