eth, miner: report local block fees and remote (MEV) fees too

This commit is contained in:
Péter Szilágyi 2024-03-12 17:03:55 +02:00
parent 753e77edbc
commit 2f01d3783d
5 changed files with 114 additions and 15 deletions

View file

@ -611,6 +611,11 @@ func (api *ConsensusAPI) newPayload(params engine.ExecutableData, versionedHashe
return api.invalid(err, parent.Header()), nil return api.invalid(err, parent.Header()), nil
} }
// Share the block with the miner to pull out any relevant stats to previous
// local block production attempt
if payload := api.localBlocks.find(block.NumberU64()); payload != nil {
api.eth.Miner().ReportFeeMetrics(payload, block)
}
hash := block.Hash() hash := block.Hash()
return engine.PayloadStatusV1{Status: engine.VALID, LatestValidHash: &hash}, nil return engine.PayloadStatusV1{Status: engine.VALID, LatestValidHash: &hash}, nil
} }

View file

@ -107,6 +107,20 @@ func (q *payloadQueue) has(id engine.PayloadID) bool {
return false return false
} }
// find tries to find a locally mined block with the given number. This method is
// relatively expensive and should only be used sparsely (goal == metrics).
func (q *payloadQueue) find(number uint64) *miner.Payload {
for _, item := range q.payloads {
if item == nil {
return nil
}
if item.payload.Number() == number {
return item.payload
}
}
return nil
}
// headerQueueItem represents an hash->header tuple to store until it's retrieved // headerQueueItem represents an hash->header tuple to store until it's retrieved
// or evicted. // or evicted.
type headerQueueItem struct { type headerQueueItem struct {

View file

@ -22,6 +22,11 @@ import "github.com/ethereum/go-ethereum/metrics"
// the block fees generated by each recommit round (relative to the latest). // the block fees generated by each recommit round (relative to the latest).
var blockRecommitFeeGaugeName = "miner/fees/recommits" var blockRecommitFeeGaugeName = "miner/fees/recommits"
// blockFinalFeeGauge is the metric tracking the best fee proposed based on the // blockProposedFeeGauge is the metric tracking the best fee proposed based on the
// local transaction pool. // local transaction pool.
var blockFinalFeeGauge = metrics.NewRegisteredResettingGauge("miner/fees/proposed", nil) var blockProposedFeeGauge = metrics.NewRegisteredResettingGauge("miner/fees/proposed", nil)
// blockIncludedFeeGauge is the metric tracking the final fee of the block as
// seen on chain. In case of a detected MEV block, it will be the MEV fees, not
// the block proposal fees.
var blockIncludedFeeGauge = metrics.NewRegisteredResettingGauge("miner/fees/included", nil)

View file

@ -30,6 +30,8 @@ import (
"github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/txpool"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/metrics"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
) )
@ -163,3 +165,76 @@ func (miner *Miner) getPending() *newPayloadResult {
miner.pending.update(header.Hash(), ret) miner.pending.update(header.Hash(), ret)
return ret return ret
} }
// ReportFeeMetrics injects a few miner metrics based on live blocks matched with
// previously locally proposed blocks.
func (miner *Miner) ReportFeeMetrics(payload *Payload, block *types.Block) {
// Skip everything if something's screwy being sent to us
if payload.full == nil {
return
}
// Report all the local recommits into individual metrics
for i := len(payload.recommits) - 1; i >= 0; i-- {
// Track the generated fees in the metrics
gauge := fmt.Sprintf("%s/%d", blockRecommitFeeGaugeName, len(payload.recommits)-i-1)
metrics.GetOrRegisterResettingGauge(gauge, nil).Update(new(big.Int).Div(payload.recommits[i], bigGwei).Int64())
}
// Report the local proposed fees into its own metric
blockProposedFeeGauge.Update(new(big.Int).Div(payload.fullFees, bigGwei).Int64())
// If the local fee recipient is the zero address, we're running in benchmark
// mode. In this case we cannot know if a block is or is not MEV. In this case,
// case, assume that MEV blocks will do a final tx to the actual fee recipient
// from the coinbase and hope it's a goon enough heuristic.
if payload.full.Coinbase() == (common.Address{}) {
if count := len(block.Transactions()); count > 0 {
payout := block.Transactions()[count-1]
payer, err := types.Sender(types.LatestSignerForChainID(miner.chainConfig.ChainID), payout)
if err != nil {
log.Error("Failed to retrieve potential MEV sender", "err", err)
return
}
if block.Coinbase() == payer {
// The fee recipient made the last transaction, possibly MEV block
mevInEther := new(big.Float).Quo(new(big.Float).SetInt(payout.Value()), big.NewFloat(params.Ether))
feeInEther := new(big.Float).Quo(new(big.Float).SetInt(payload.fullFees), big.NewFloat(params.Ether))
log.Info("MEV block detected", "reward", mevInEther, "local", feeInEther)
blockIncludedFeeGauge.Update(new(big.Int).Div(payout.Value(), bigGwei).Int64())
} else {
// Possibly not an MEV block, report the boring mining fees
feeInEther := new(big.Float).Quo(new(big.Float).SetInt(payload.fullFees), big.NewFloat(params.Ether))
log.Info("Plain block detected", "reward", feeInEther)
blockIncludedFeeGauge.Update(new(big.Int).Div(payout.Value(), bigGwei).Int64())
}
}
} else if payload.full.Coinbase() != block.Coinbase() {
// We are not in benchmarking mode and out proposed fee recipient is
// different from the live reward recipient. Definitely an MEV block.
// Try to extract the rewards.
if count := len(block.Transactions()); count > 0 {
payout := block.Transactions()[count-1]
if payout.To() != nil && *payout.To() == payload.full.Coinbase() {
mevInEther := new(big.Float).Quo(new(big.Float).SetInt(payout.Value()), big.NewFloat(params.Ether))
feeInEther := new(big.Float).Quo(new(big.Float).SetInt(payload.fullFees), big.NewFloat(params.Ether))
log.Info("MEV reward received", "reward", mevInEther, "local", feeInEther)
blockIncludedFeeGauge.Update(new(big.Int).Div(payout.Value(), bigGwei).Int64())
} else {
// Unknown MEV block, report the boring mining fees
feeInEther := new(big.Float).Quo(new(big.Float).SetInt(payload.fullFees), big.NewFloat(params.Ether))
log.Warn("MEV block detected, unknown reward", "reward", "unknown", "local", feeInEther)
blockIncludedFeeGauge.Update(new(big.Int).Div(payout.Value(), bigGwei).Int64())
}
}
} else {
// Probably not an MEV block, report the boring mining fees
feeInEther := new(big.Float).Quo(new(big.Float).SetInt(payload.fullFees), big.NewFloat(params.Ether))
log.Info("Plain reward received", "reward", feeInEther)
blockIncludedFeeGauge.Update(new(big.Int).Div(payload.fullFees, bigGwei).Int64())
}
}

View file

@ -19,7 +19,6 @@ package miner
import ( import (
"crypto/sha256" "crypto/sha256"
"encoding/binary" "encoding/binary"
"fmt"
"math/big" "math/big"
"sync" "sync"
"time" "time"
@ -28,7 +27,6 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/metrics"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rlp"
) )
@ -74,6 +72,8 @@ func (args *BuildPayloadArgs) Id() engine.PayloadID {
// will be set/updated afterwards. // will be set/updated afterwards.
type Payload struct { type Payload struct {
id engine.PayloadID id engine.PayloadID
number uint64
empty *types.Block empty *types.Block
full *types.Block full *types.Block
sidecars []*types.BlobTxSidecar sidecars []*types.BlobTxSidecar
@ -89,6 +89,7 @@ type Payload struct {
func newPayload(empty *types.Block, id engine.PayloadID) *Payload { func newPayload(empty *types.Block, id engine.PayloadID) *Payload {
payload := &Payload{ payload := &Payload{
id: id, id: id,
number: empty.NumberU64(),
empty: empty, empty: empty,
stop: make(chan struct{}), stop: make(chan struct{}),
} }
@ -134,6 +135,12 @@ func (payload *Payload) update(r *newPayloadResult, elapsed time.Duration) {
payload.cond.Broadcast() // fire signal for notifying full block payload.cond.Broadcast() // fire signal for notifying full block
} }
// Number retrieves the block number this payload belongs to. This method is used
// in stats reporting to allow matching a block arriving later to a past payload.
func (payload *Payload) Number() uint64 {
return payload.number
}
// Resolve returns the latest built payload and also terminates the background // Resolve returns the latest built payload and also terminates the background
// thread for updating payload. It's safe to be called multiple times. // thread for updating payload. It's safe to be called multiple times.
func (payload *Payload) Resolve() *engine.ExecutionPayloadEnvelope { func (payload *Payload) Resolve() *engine.ExecutionPayloadEnvelope {
@ -183,13 +190,6 @@ func (payload *Payload) ResolveFull() *engine.ExecutionPayloadEnvelope {
default: default:
close(payload.stop) close(payload.stop)
} }
// Report all the recommits into individual metrics
for i := len(payload.recommits) - 1; i >= 0; i-- {
// Track the generated fees in the metrics
gauge := fmt.Sprintf("%s/%d", blockRecommitFeeGaugeName, len(payload.recommits)-i-1)
metrics.GetOrRegisterResettingGauge(gauge, nil).Update(new(big.Int).Div(payload.recommits[i], bigGwei).Int64())
}
blockFinalFeeGauge.Update(new(big.Int).Div(payload.fullFees, bigGwei).Int64())
return engine.BlockToExecutableData(payload.full, payload.fullFees, payload.sidecars) return engine.BlockToExecutableData(payload.full, payload.fullFees, payload.sidecars)
} }