miner: add OpenTelemetry tracing spans to block building

Add tracing instrumentation to the payload building pipeline so operators
can identify where time is spent during block construction. This adds four
spans:

  - miner.buildPayload   (root span per build iteration)
  - miner.prepareWork    (header + state setup)
  - miner.fillTransactions (transaction selection + execution)
  - miner.finalizeAndAssemble (block finalization)

The root span is created via StartRootSpan since the background build
goroutine has no parent RPC context. Context is threaded through
generateWork → prepareWork/fillTransactions to enable child spans.

This complements the existing RPC-level tracing (jsonrpc.engine/getPayloadV*
server spans with rpc.encodeJSONResponse children) by providing visibility
into the block building phase itself.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Barnabas Busa 2026-03-03 12:54:43 +01:00
parent d318e8eba9
commit 42fe582e44
No known key found for this signature in database
GPG key ID: D7AF98F214C59E4E
4 changed files with 39 additions and 9 deletions

View file

@ -88,6 +88,13 @@ func StartServerSpan(ctx context.Context, tracer trace.Tracer, rpc RPCInfo, othe
return ctx, end return ctx, end
} }
// StartRootSpan creates a SpanKind=INTERNAL root span that does not require
// a parent span in the context. Use this for background operations that are
// not triggered by an incoming RPC request.
func StartRootSpan(spanName string, attributes ...Attribute) (context.Context, trace.Span, func(*error)) {
return startSpan(context.Background(), otel.Tracer(""), trace.SpanKindInternal, spanName, attributes...)
}
// startSpan creates a span with the given kind. // startSpan creates a span with the given kind.
func startSpan(ctx context.Context, tracer trace.Tracer, kind trace.SpanKind, spanName string, attributes ...Attribute) (context.Context, trace.Span, func(*error)) { func startSpan(ctx context.Context, tracer trace.Tracer, kind trace.SpanKind, spanName string, attributes ...Attribute) (context.Context, trace.Span, func(*error)) {
ctx, span := tracer.Start(ctx, spanName, trace.WithSpanKind(kind)) ctx, span := tracer.Start(ctx, spanName, trace.WithSpanKind(kind))

View file

@ -18,6 +18,7 @@
package miner package miner
import ( import (
"context"
"fmt" "fmt"
"math/big" "math/big"
"sync" "sync"
@ -156,7 +157,7 @@ func (miner *Miner) getPending() *newPayloadResult {
if miner.chainConfig.IsShanghai(new(big.Int).Add(header.Number, big.NewInt(1)), timestamp) { if miner.chainConfig.IsShanghai(new(big.Int).Add(header.Number, big.NewInt(1)), timestamp) {
withdrawal = []*types.Withdrawal{} withdrawal = []*types.Withdrawal{}
} }
ret := miner.generateWork(&generateParams{ ret := miner.generateWork(context.Background(), &generateParams{
timestamp: timestamp, timestamp: timestamp,
forceTime: false, forceTime: false,
parentHash: header.Hash(), parentHash: header.Hash(),

View file

@ -17,6 +17,7 @@
package miner package miner
import ( import (
"context"
"crypto/sha256" "crypto/sha256"
"encoding/binary" "encoding/binary"
"math/big" "math/big"
@ -28,6 +29,7 @@ import (
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/stateless" "github.com/ethereum/go-ethereum/core/stateless"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/internal/telemetry"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rlp"
@ -225,7 +227,7 @@ func (miner *Miner) buildPayload(args *BuildPayloadArgs, witness bool) (*Payload
slotNum: args.SlotNum, slotNum: args.SlotNum,
noTxs: true, noTxs: true,
} }
empty := miner.generateWork(emptyParams, witness) empty := miner.generateWork(context.Background(), emptyParams, witness)
if empty.err != nil { if empty.err != nil {
return nil, empty.err return nil, empty.err
} }
@ -261,11 +263,17 @@ func (miner *Miner) buildPayload(args *BuildPayloadArgs, witness bool) (*Payload
select { select {
case <-timer.C: case <-timer.C:
start := time.Now() start := time.Now()
r := miner.generateWork(fullParams, witness) ctx, _, spanEnd := telemetry.StartRootSpan("miner.buildPayload",
telemetry.Int64Attribute("block.number", int64(args.Timestamp)),
telemetry.StringAttribute("payload.id", args.Id().String()),
)
r := miner.generateWork(ctx, fullParams, witness)
if r.err == nil { if r.err == nil {
payload.update(r, time.Since(start)) payload.update(r, time.Since(start))
spanEnd(nil)
} else { } else {
log.Info("Error while generating work", "id", payload.id, "err", r.err) log.Info("Error while generating work", "id", payload.id, "err", r.err)
spanEnd(&r.err)
} }
timer.Reset(max(0, miner.config.Recommit-time.Since(start))) timer.Reset(max(0, miner.config.Recommit-time.Since(start)))
case <-payload.stop: case <-payload.stop:
@ -296,7 +304,7 @@ func (miner *Miner) BuildTestingPayload(args *BuildPayloadArgs, transactions []*
overrideExtraData: extraData, overrideExtraData: extraData,
overrideTxs: transactions, overrideTxs: transactions,
} }
res := miner.generateWork(fullParams, false) res := miner.generateWork(context.Background(), fullParams, false)
if res.err != nil { if res.err != nil {
return nil, res.err return nil, res.err
} }

View file

@ -17,6 +17,7 @@
package miner package miner
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"math/big" "math/big"
@ -32,6 +33,7 @@ import (
"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/core/vm" "github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/internal/telemetry"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"github.com/holiman/uint256" "github.com/holiman/uint256"
@ -120,8 +122,8 @@ type generateParams struct {
} }
// generateWork generates a sealing block based on the given parameters. // generateWork generates a sealing block based on the given parameters.
func (miner *Miner) generateWork(genParam *generateParams, witness bool) *newPayloadResult { func (miner *Miner) generateWork(ctx context.Context, genParam *generateParams, witness bool) *newPayloadResult {
work, err := miner.prepareWork(genParam, witness) work, err := miner.prepareWork(ctx, genParam, witness)
if err != nil { if err != nil {
return &newPayloadResult{err: err} return &newPayloadResult{err: err}
} }
@ -157,7 +159,7 @@ func (miner *Miner) generateWork(genParam *generateParams, witness bool) *newPay
}) })
defer timer.Stop() defer timer.Stop()
err := miner.fillTransactions(interrupt, work) err := miner.fillTransactions(ctx, interrupt, work)
if errors.Is(err, errBlockInterruptedByTimeout) { if errors.Is(err, errBlockInterruptedByTimeout) {
log.Warn("Block building is interrupted", "allowance", common.PrettyDuration(miner.config.Recommit)) log.Warn("Block building is interrupted", "allowance", common.PrettyDuration(miner.config.Recommit))
} }
@ -192,10 +194,16 @@ func (miner *Miner) generateWork(genParam *generateParams, witness bool) *newPay
work.header.RequestsHash = &reqHash work.header.RequestsHash = &reqHash
} }
var finalizeErr error
_, _, spanEnd := telemetry.StartSpan(ctx, "miner.finalizeAndAssemble")
block, err := miner.engine.FinalizeAndAssemble(miner.chain, work.header, work.state, &body, work.receipts) block, err := miner.engine.FinalizeAndAssemble(miner.chain, work.header, work.state, &body, work.receipts)
if err != nil { if err != nil {
finalizeErr = err
spanEnd(&finalizeErr)
return &newPayloadResult{err: err} return &newPayloadResult{err: err}
} }
spanEnd(&finalizeErr)
return &newPayloadResult{ return &newPayloadResult{
block: block, block: block,
fees: totalFees(block, work.receipts), fees: totalFees(block, work.receipts),
@ -210,7 +218,10 @@ func (miner *Miner) generateWork(genParam *generateParams, witness bool) *newPay
// prepareWork constructs the sealing task according to the given parameters, // prepareWork constructs the sealing task according to the given parameters,
// either based on the last chain head or specified parent. In this function // either based on the last chain head or specified parent. In this function
// the pending transactions are not filled yet, only the empty task returned. // the pending transactions are not filled yet, only the empty task returned.
func (miner *Miner) prepareWork(genParams *generateParams, witness bool) (*environment, error) { func (miner *Miner) prepareWork(ctx context.Context, genParams *generateParams, witness bool) (_ *environment, err error) {
_, _, spanEnd := telemetry.StartSpan(ctx, "miner.prepareWork")
defer spanEnd(&err)
miner.confMu.RLock() miner.confMu.RLock()
defer miner.confMu.RUnlock() defer miner.confMu.RUnlock()
@ -502,7 +513,10 @@ func (miner *Miner) commitTransactions(env *environment, plainTxs, blobTxs *tran
// fillTransactions retrieves the pending transactions from the txpool and fills them // fillTransactions retrieves the pending transactions from the txpool and fills them
// into the given sealing block. The transaction selection and ordering strategy can // into the given sealing block. The transaction selection and ordering strategy can
// be customized with the plugin in the future. // be customized with the plugin in the future.
func (miner *Miner) fillTransactions(interrupt *atomic.Int32, env *environment) error { func (miner *Miner) fillTransactions(ctx context.Context, interrupt *atomic.Int32, env *environment) (err error) {
_, _, spanEnd := telemetry.StartSpan(ctx, "miner.fillTransactions")
defer spanEnd(&err)
miner.confMu.RLock() miner.confMu.RLock()
tip := miner.config.GasPrice tip := miner.config.GasPrice
prio := miner.prio prio := miner.prio