From 2f01d3783d350ca8b72cdf2e258b2d3f81f9cf77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= Date: Tue, 12 Mar 2024 17:03:55 +0200 Subject: [PATCH] eth, miner: report local block fees and remote (MEV) fees too --- eth/catalyst/api.go | 5 +++ eth/catalyst/queue.go | 14 ++++++++ miner/metrics.go | 9 +++-- miner/miner.go | 75 +++++++++++++++++++++++++++++++++++++++ miner/payload_building.go | 26 +++++++------- 5 files changed, 114 insertions(+), 15 deletions(-) diff --git a/eth/catalyst/api.go b/eth/catalyst/api.go index e5781b2c8f..c30e1658b0 100644 --- a/eth/catalyst/api.go +++ b/eth/catalyst/api.go @@ -611,6 +611,11 @@ func (api *ConsensusAPI) newPayload(params engine.ExecutableData, versionedHashe 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() return engine.PayloadStatusV1{Status: engine.VALID, LatestValidHash: &hash}, nil } diff --git a/eth/catalyst/queue.go b/eth/catalyst/queue.go index 634dc1b2e6..819a87d64f 100644 --- a/eth/catalyst/queue.go +++ b/eth/catalyst/queue.go @@ -107,6 +107,20 @@ func (q *payloadQueue) has(id engine.PayloadID) bool { 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 // or evicted. type headerQueueItem struct { diff --git a/miner/metrics.go b/miner/metrics.go index f5547a7eec..7b7df5023a 100644 --- a/miner/metrics.go +++ b/miner/metrics.go @@ -22,6 +22,11 @@ import "github.com/ethereum/go-ethereum/metrics" // the block fees generated by each recommit round (relative to the latest). 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. -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) diff --git a/miner/miner.go b/miner/miner.go index 430efcb2fc..4b4b067145 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -30,6 +30,8 @@ import ( "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/txpool" "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" ) @@ -163,3 +165,76 @@ func (miner *Miner) getPending() *newPayloadResult { miner.pending.update(header.Hash(), 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()) + } +} diff --git a/miner/payload_building.go b/miner/payload_building.go index 45fd899e7c..18f13c68e1 100644 --- a/miner/payload_building.go +++ b/miner/payload_building.go @@ -19,7 +19,6 @@ package miner import ( "crypto/sha256" "encoding/binary" - "fmt" "math/big" "sync" "time" @@ -28,7 +27,6 @@ import ( "github.com/ethereum/go-ethereum/common" "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/rlp" ) @@ -73,7 +71,9 @@ func (args *BuildPayloadArgs) Id() engine.PayloadID { // the revenue. Therefore, the empty-block here is always available and full-block // will be set/updated afterwards. type Payload struct { - id engine.PayloadID + id engine.PayloadID + number uint64 + empty *types.Block full *types.Block sidecars []*types.BlobTxSidecar @@ -88,9 +88,10 @@ type Payload struct { // newPayload initializes the payload object. func newPayload(empty *types.Block, id engine.PayloadID) *Payload { payload := &Payload{ - id: id, - empty: empty, - stop: make(chan struct{}), + id: id, + number: empty.NumberU64(), + empty: empty, + stop: make(chan struct{}), } log.Info("Starting work on payload", "id", payload.id) payload.cond = sync.NewCond(&payload.lock) @@ -134,6 +135,12 @@ func (payload *Payload) update(r *newPayloadResult, elapsed time.Duration) { 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 // thread for updating payload. It's safe to be called multiple times. func (payload *Payload) Resolve() *engine.ExecutionPayloadEnvelope { @@ -183,13 +190,6 @@ func (payload *Payload) ResolveFull() *engine.ExecutionPayloadEnvelope { default: 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) }