mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-05-24 08:49:29 +00:00
testing_commitBlockV1 is the write companion of testing_buildBlockV1: it builds a block from the provided payloadAttributes and txs, inserts it, and sets it as the canonical head, returning the new head hash. Skipping the engine_newPayload + engine_forkchoiceUpdated serialize/deserialize round-trip makes it useful for state-shape benchmarking and reproducible test-chain construction where the caller wants the chain to advance. The new miner.CommitTestingBlock shares its generation path with BuildTestingPayload via an unexported helper, so both code paths produce the same block from the same inputs. Spec and cross-client fixtures: ethereum/execution-apis#801
386 lines
14 KiB
Go
386 lines
14 KiB
Go
// Copyright 2022 The go-ethereum Authors
|
|
// This file is part of the go-ethereum library.
|
|
//
|
|
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Lesser General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Lesser General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Lesser General Public License
|
|
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
package miner
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
"math/big"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/ethereum/go-ethereum/beacon/engine"
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
"github.com/ethereum/go-ethereum/core/stateless"
|
|
"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/params"
|
|
"github.com/ethereum/go-ethereum/rlp"
|
|
"go.opentelemetry.io/otel/trace"
|
|
)
|
|
|
|
// BuildPayloadArgs contains the provided parameters for building payload.
|
|
// Check engine-api specification for more details.
|
|
// https://github.com/ethereum/execution-apis/blob/main/src/engine/cancun.md#payloadattributesv3
|
|
type BuildPayloadArgs struct {
|
|
Parent common.Hash // The parent block to build payload on top
|
|
Timestamp uint64 // The provided timestamp of generated payload
|
|
FeeRecipient common.Address // The provided recipient address for collecting transaction fee
|
|
Random common.Hash // The provided randomness value
|
|
Withdrawals types.Withdrawals // The provided withdrawals
|
|
BeaconRoot *common.Hash // The provided beaconRoot (Cancun)
|
|
SlotNum *uint64 // The provided slotNumber
|
|
Version engine.PayloadVersion // Versioning byte for payload id calculation.
|
|
}
|
|
|
|
// Id computes an 8-byte identifier by hashing the components of the payload arguments.
|
|
func (args *BuildPayloadArgs) Id() engine.PayloadID {
|
|
hasher := sha256.New()
|
|
hasher.Write(args.Parent[:])
|
|
binary.Write(hasher, binary.BigEndian, args.Timestamp)
|
|
hasher.Write(args.Random[:])
|
|
hasher.Write(args.FeeRecipient[:])
|
|
rlp.Encode(hasher, args.Withdrawals)
|
|
if args.BeaconRoot != nil {
|
|
hasher.Write(args.BeaconRoot[:])
|
|
}
|
|
if args.SlotNum != nil {
|
|
binary.Write(hasher, binary.BigEndian, args.SlotNum)
|
|
}
|
|
var out engine.PayloadID
|
|
copy(out[:], hasher.Sum(nil)[:8])
|
|
out[0] = byte(args.Version)
|
|
return out
|
|
}
|
|
|
|
// Payload wraps the built payload(block waiting for sealing). According to the
|
|
// engine-api specification, EL should build the initial version of the payload
|
|
// which has an empty transaction set and then keep update it in order to maximize
|
|
// the revenue. Therefore, the empty-block here is always available and full-block
|
|
// will be set/updated afterwards.
|
|
type Payload struct {
|
|
id engine.PayloadID
|
|
empty *types.Block
|
|
emptyWitness *stateless.Witness
|
|
full *types.Block
|
|
fullWitness *stateless.Witness
|
|
sidecars []*types.BlobTxSidecar
|
|
emptyRequests [][]byte
|
|
requests [][]byte
|
|
fullFees *big.Int
|
|
stop chan struct{}
|
|
lock sync.Mutex
|
|
cond *sync.Cond
|
|
}
|
|
|
|
// newPayload initializes the payload object.
|
|
func newPayload(empty *types.Block, emptyRequests [][]byte, witness *stateless.Witness, id engine.PayloadID) *Payload {
|
|
payload := &Payload{
|
|
id: id,
|
|
empty: empty,
|
|
emptyRequests: emptyRequests,
|
|
emptyWitness: witness,
|
|
stop: make(chan struct{}),
|
|
}
|
|
log.Info("Starting work on payload", "id", payload.id)
|
|
payload.cond = sync.NewCond(&payload.lock)
|
|
return payload
|
|
}
|
|
|
|
// update updates the full-block with latest built version. It returns true if
|
|
// the update was accepted (i.e. the new block has higher fees than the previous).
|
|
func (payload *Payload) update(r *newPayloadResult, elapsed time.Duration) (result bool) {
|
|
payload.lock.Lock()
|
|
defer payload.lock.Unlock()
|
|
|
|
select {
|
|
case <-payload.stop:
|
|
return false // reject stale update
|
|
default:
|
|
}
|
|
// Ensure the newly provided full block has a higher transaction fee.
|
|
// In post-merge stage, there is no uncle reward anymore and transaction
|
|
// fee(apart from the mev revenue) is the only indicator for comparison.
|
|
if payload.full == nil || r.fees.Cmp(payload.fullFees) > 0 {
|
|
payload.full = r.block
|
|
payload.fullFees = r.fees
|
|
payload.sidecars = r.sidecars
|
|
payload.requests = r.requests
|
|
payload.fullWitness = r.witness
|
|
|
|
feesInEther := new(big.Float).Quo(new(big.Float).SetInt(r.fees), big.NewFloat(params.Ether))
|
|
log.Info("Updated payload",
|
|
"id", payload.id,
|
|
"number", r.block.NumberU64(),
|
|
"hash", r.block.Hash(),
|
|
"txs", len(r.block.Transactions()),
|
|
"withdrawals", len(r.block.Withdrawals()),
|
|
"gas", r.block.GasUsed(),
|
|
"fees", feesInEther,
|
|
"root", r.block.Root(),
|
|
"elapsed", common.PrettyDuration(elapsed),
|
|
)
|
|
result = true
|
|
}
|
|
payload.cond.Broadcast() // fire signal for notifying full block
|
|
return
|
|
}
|
|
|
|
// 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 {
|
|
payload.lock.Lock()
|
|
defer payload.lock.Unlock()
|
|
|
|
select {
|
|
case <-payload.stop:
|
|
default:
|
|
close(payload.stop)
|
|
}
|
|
if payload.full != nil {
|
|
envelope := engine.BlockToExecutableData(payload.full, payload.fullFees, payload.sidecars, payload.requests)
|
|
if payload.fullWitness != nil {
|
|
envelope.Witness = new(hexutil.Bytes)
|
|
*envelope.Witness, _ = rlp.EncodeToBytes(payload.fullWitness) // cannot fail
|
|
}
|
|
return envelope
|
|
}
|
|
envelope := engine.BlockToExecutableData(payload.empty, big.NewInt(0), nil, payload.emptyRequests)
|
|
if payload.emptyWitness != nil {
|
|
envelope.Witness = new(hexutil.Bytes)
|
|
*envelope.Witness, _ = rlp.EncodeToBytes(payload.emptyWitness) // cannot fail
|
|
}
|
|
return envelope
|
|
}
|
|
|
|
// ResolveEmpty is basically identical to Resolve, but it expects empty block only.
|
|
// It's only used in tests.
|
|
func (payload *Payload) ResolveEmpty() *engine.ExecutionPayloadEnvelope {
|
|
payload.lock.Lock()
|
|
defer payload.lock.Unlock()
|
|
|
|
envelope := engine.BlockToExecutableData(payload.empty, big.NewInt(0), nil, payload.emptyRequests)
|
|
if payload.emptyWitness != nil {
|
|
envelope.Witness = new(hexutil.Bytes)
|
|
*envelope.Witness, _ = rlp.EncodeToBytes(payload.emptyWitness) // cannot fail
|
|
}
|
|
return envelope
|
|
}
|
|
|
|
// ResolveFull is basically identical to Resolve, but it expects full block only.
|
|
// Don't call Resolve until ResolveFull returns, otherwise it might block forever.
|
|
func (payload *Payload) ResolveFull() *engine.ExecutionPayloadEnvelope {
|
|
payload.lock.Lock()
|
|
defer payload.lock.Unlock()
|
|
|
|
if payload.full == nil {
|
|
select {
|
|
case <-payload.stop:
|
|
return nil
|
|
default:
|
|
}
|
|
// Wait the full payload construction. Note it might block
|
|
// forever if Resolve is called in the meantime which
|
|
// terminates the background construction process.
|
|
payload.cond.Wait()
|
|
}
|
|
// Terminate the background payload construction
|
|
select {
|
|
case <-payload.stop:
|
|
default:
|
|
close(payload.stop)
|
|
}
|
|
envelope := engine.BlockToExecutableData(payload.full, payload.fullFees, payload.sidecars, payload.requests)
|
|
if payload.fullWitness != nil {
|
|
envelope.Witness = new(hexutil.Bytes)
|
|
*envelope.Witness, _ = rlp.EncodeToBytes(payload.fullWitness) // cannot fail
|
|
}
|
|
return envelope
|
|
}
|
|
|
|
func (miner *Miner) runBuildIteration(ctx context.Context, start time.Time, iteration int, payload *Payload, params *generateParams, witness bool) {
|
|
ctx, span, spanEnd := telemetry.StartSpan(ctx, "miner.buildIteration",
|
|
telemetry.Int64Attribute("iteration", int64(iteration)),
|
|
)
|
|
var err error
|
|
defer spanEnd(&err)
|
|
|
|
r := miner.generateWork(ctx, params, witness)
|
|
err = r.err
|
|
if err == nil {
|
|
accepted := payload.update(r, time.Since(start))
|
|
span.SetAttributes(telemetry.BoolAttribute("update.accepted", accepted))
|
|
} else {
|
|
log.Info("Error while generating work", "id", payload.id, "err", err)
|
|
}
|
|
}
|
|
|
|
// buildPayload builds the payload according to the provided parameters.
|
|
func (miner *Miner) buildPayload(ctx context.Context, args *BuildPayloadArgs, witness bool) (result *Payload, err error) {
|
|
payloadID := args.Id()
|
|
ctx, _, spanEnd := telemetry.StartSpan(ctx, "miner.buildPayload",
|
|
telemetry.StringAttribute("payload.id", payloadID.String()),
|
|
telemetry.StringAttribute("parent.hash", args.Parent.String()),
|
|
telemetry.Int64Attribute("timestamp", int64(args.Timestamp)),
|
|
)
|
|
defer spanEnd(&err)
|
|
|
|
// Build the initial version with no transaction included. It should be fast
|
|
// enough to run. The empty payload can at least make sure there is something
|
|
// to deliver for not missing slot.
|
|
emptyParams := &generateParams{
|
|
timestamp: args.Timestamp,
|
|
forceTime: true,
|
|
parentHash: args.Parent,
|
|
coinbase: args.FeeRecipient,
|
|
random: args.Random,
|
|
withdrawals: args.Withdrawals,
|
|
beaconRoot: args.BeaconRoot,
|
|
slotNum: args.SlotNum,
|
|
noTxs: true,
|
|
}
|
|
empty := miner.generateWork(ctx, emptyParams, witness)
|
|
if empty.err != nil {
|
|
return nil, empty.err
|
|
}
|
|
// Construct a payload object for return.
|
|
payload := newPayload(empty.block, empty.requests, empty.witness, payloadID)
|
|
|
|
// Spin up a routine for updating the payload in background. This strategy
|
|
// can maximum the revenue for including transactions with highest fee.
|
|
go func() {
|
|
var iteration int
|
|
bCtx, bSpan, bSpanEnd := telemetry.StartSpan(ctx, "miner.background",
|
|
telemetry.Int64Attribute("block.number", int64(empty.block.NumberU64())),
|
|
)
|
|
defer func() {
|
|
bSpan.SetAttributes(telemetry.Int64Attribute("iterations.total", int64(iteration)))
|
|
bSpanEnd(nil)
|
|
}()
|
|
|
|
// Setup the timer for re-building the payload. The initial clock is kept
|
|
// for triggering process immediately.
|
|
timer := time.NewTimer(0)
|
|
defer timer.Stop()
|
|
|
|
// Setup the timer for terminating the process if SECONDS_PER_SLOT (12s in
|
|
// the Mainnet configuration) have passed since the point in time identified
|
|
// by the timestamp parameter.
|
|
endTimer := time.NewTimer(time.Second * 12)
|
|
|
|
fullParams := &generateParams{
|
|
timestamp: args.Timestamp,
|
|
forceTime: true,
|
|
parentHash: args.Parent,
|
|
coinbase: args.FeeRecipient,
|
|
random: args.Random,
|
|
withdrawals: args.Withdrawals,
|
|
beaconRoot: args.BeaconRoot,
|
|
slotNum: args.SlotNum,
|
|
noTxs: false,
|
|
}
|
|
for {
|
|
select {
|
|
case <-timer.C:
|
|
// When block building takes close to the full recommit interval,
|
|
// the timer fires near-instantly on the next iteration. If the
|
|
// payload was resolved during that build, both timer.C and
|
|
// payload.stop are ready and Go's select picks one at random.
|
|
// Check payload.stop first to avoid an unnecessary generateWork.
|
|
select {
|
|
case <-payload.stop:
|
|
payload.updateSpanForDelivery(bSpan)
|
|
log.Info("Stopping work on payload", "id", payload.id, "reason", "delivery")
|
|
return
|
|
default:
|
|
}
|
|
start := time.Now()
|
|
iteration++
|
|
miner.runBuildIteration(bCtx, start, iteration, payload, fullParams, witness)
|
|
timer.Reset(max(0, miner.config.Recommit-time.Since(start)))
|
|
case <-payload.stop:
|
|
payload.updateSpanForDelivery(bSpan)
|
|
log.Info("Stopping work on payload", "id", payload.id, "reason", "delivery")
|
|
return
|
|
case <-endTimer.C:
|
|
bSpan.SetAttributes(telemetry.StringAttribute("exit.reason", "timeout"))
|
|
log.Info("Stopping work on payload", "id", payload.id, "reason", "timeout")
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
return payload, nil
|
|
}
|
|
|
|
func (payload *Payload) updateSpanForDelivery(bSpan trace.Span) {
|
|
payload.lock.Lock()
|
|
emptyDelivered := payload.full == nil
|
|
payload.lock.Unlock()
|
|
bSpan.SetAttributes(
|
|
telemetry.StringAttribute("exit.reason", "delivery"),
|
|
telemetry.BoolAttribute("empty.delivered", emptyDelivered),
|
|
)
|
|
}
|
|
|
|
// buildTestingBlock runs generateWork with the override flags used by the testing_
|
|
// namespace, producing a block whose contents are dictated by the caller rather than
|
|
// drawn from the local txpool.
|
|
func (miner *Miner) buildTestingBlock(args *BuildPayloadArgs, transactions []*types.Transaction, empty bool, extraData []byte) (*newPayloadResult, error) {
|
|
fullParams := &generateParams{
|
|
timestamp: args.Timestamp,
|
|
forceTime: true,
|
|
parentHash: args.Parent,
|
|
coinbase: args.FeeRecipient,
|
|
random: args.Random,
|
|
withdrawals: args.Withdrawals,
|
|
beaconRoot: args.BeaconRoot,
|
|
slotNum: args.SlotNum,
|
|
noTxs: empty,
|
|
forceOverrides: true,
|
|
overrideExtraData: extraData,
|
|
overrideTxs: transactions,
|
|
}
|
|
res := miner.generateWork(context.Background(), fullParams, false)
|
|
if res.err != nil {
|
|
return nil, res.err
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
// BuildTestingPayload is for testing_buildBlockV*. It creates a block with the exact content given
|
|
// by the parameters instead of using the locally available transactions.
|
|
func (miner *Miner) BuildTestingPayload(args *BuildPayloadArgs, transactions []*types.Transaction, empty bool, extraData []byte) (*engine.ExecutionPayloadEnvelope, error) {
|
|
res, err := miner.buildTestingBlock(args, transactions, empty, extraData)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return engine.BlockToExecutableData(res.block, res.fees, res.sidecars, res.requests), nil
|
|
}
|
|
|
|
// CommitTestingBlock is for testing_commitBlockV*. Like BuildTestingPayload it generates
|
|
// a block from the caller-supplied parameters, but returns the raw block so the caller
|
|
// can insert and canonicalize it without an ExecutableData round-trip.
|
|
func (miner *Miner) CommitTestingBlock(args *BuildPayloadArgs, transactions []*types.Transaction, empty bool, extraData []byte) (*types.Block, error) {
|
|
res, err := miner.buildTestingBlock(args, transactions, empty, extraData)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return res.block, nil
|
|
}
|