eth/catalyst, miner: add testing_commitBlockV1

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
This commit is contained in:
Chase Wright 2026-05-18 09:36:10 -05:00
parent d4027f3d46
commit bcca4858b5
No known key found for this signature in database
GPG key ID: 7EA41EDCB1BE04A7
3 changed files with 185 additions and 3 deletions

View file

@ -17,6 +17,7 @@
package catalyst
import (
"context"
"errors"
"github.com/ethereum/go-ethereum/beacon/engine"
@ -78,3 +79,52 @@ func (api *testingAPI) BuildBlockV1(parentHash common.Hash, payloadAttributes en
}
return api.eth.Miner().BuildTestingPayload(args, txs, buildEmpty, extra)
}
// CommitBlockV1 builds a block from the supplied attributes and transactions, inserts
// it into the chain, and sets it as the new canonical head. It is the equivalent of
// BuildBlockV1 followed by engine_newPayload + engine_forkchoiceUpdated, but skips the
// serialize/deserialize round-trip through ExecutableData. The block is built on top of
// the current head.
func (api *testingAPI) CommitBlockV1(ctx context.Context, payloadAttributes engine.PayloadAttributes, transactions *[]hexutil.Bytes, extraData *hexutil.Bytes) (common.Hash, error) {
parentHash := api.eth.BlockChain().CurrentBlock().Hash()
// If transactions is empty but not nil, build an empty block
// If the transactions is nil, build a block with the current transactions from the txpool
// If the transactions is not nil and not empty, build a block with the transactions
buildEmpty := transactions != nil && len(*transactions) == 0
var txs []*types.Transaction
if transactions != nil {
dec := make([][]byte, 0, len(*transactions))
for _, tx := range *transactions {
dec = append(dec, tx)
}
var err error
txs, err = engine.DecodeTransactions(dec)
if err != nil {
return common.Hash{}, err
}
}
extra := make([]byte, 0)
if extraData != nil {
extra = *extraData
}
args := &miner.BuildPayloadArgs{
Parent: parentHash,
Timestamp: payloadAttributes.Timestamp,
FeeRecipient: payloadAttributes.SuggestedFeeRecipient,
Random: payloadAttributes.Random,
Withdrawals: payloadAttributes.Withdrawals,
BeaconRoot: payloadAttributes.BeaconRoot,
SlotNum: payloadAttributes.SlotNumber,
}
block, err := api.eth.Miner().CommitTestingBlock(args, txs, buildEmpty, extra)
if err != nil {
return common.Hash{}, err
}
if _, err := api.eth.BlockChain().InsertBlockWithoutSetHead(ctx, block, false); err != nil {
return common.Hash{}, err
}
if _, err := api.eth.BlockChain().SetCanonical(block); err != nil {
return common.Hash{}, err
}
return block.Hash(), nil
}

View file

@ -119,3 +119,113 @@ func TestBuildBlockV1(t *testing.T) {
}
})
}
func TestCommitBlockV1(t *testing.T) {
genesis, blocks := generateMergeChain(5, true)
n, ethservice := startEthService(t, genesis, blocks)
defer n.Close()
api := &testingAPI{eth: ethservice}
ctx := context.Background()
nextAttrs := func() engine.PayloadAttributes {
head := ethservice.BlockChain().CurrentBlock()
return engine.PayloadAttributes{
Timestamp: head.Time + 1,
Random: crypto.Keccak256Hash([]byte("commit-test")),
SuggestedFeeRecipient: head.Coinbase,
}
}
t.Run("commitEmptyBlock", func(t *testing.T) {
parent := ethservice.BlockChain().CurrentBlock()
emptyTxs := []hexutil.Bytes{}
hash, err := api.CommitBlockV1(ctx, nextAttrs(), &emptyTxs, nil)
if err != nil {
t.Fatalf("CommitBlockV1 failed: %v", err)
}
head := ethservice.BlockChain().CurrentBlock()
if head.Hash() != hash {
t.Errorf("head hash mismatch: got %x want %x", head.Hash(), hash)
}
if head.Number.Uint64() != parent.Number.Uint64()+1 {
t.Errorf("head number mismatch: got %d want %d", head.Number.Uint64(), parent.Number.Uint64()+1)
}
block := ethservice.BlockChain().GetBlockByHash(hash)
if block == nil {
t.Fatal("committed block not found in chain")
}
if len(block.Transactions()) != 0 {
t.Errorf("expected empty block, got %d transactions", len(block.Transactions()))
}
})
t.Run("commitBlockWithTransactions", func(t *testing.T) {
parent := ethservice.BlockChain().CurrentBlock()
nonce, _ := ethservice.APIBackend.GetPoolNonce(ctx, testAddr)
tx, _ := types.SignTx(types.NewTransaction(nonce, testAddr, big.NewInt(1), params.TxGas, big.NewInt(params.InitialBaseFee*2), nil), types.LatestSigner(ethservice.BlockChain().Config()), testKey)
enc, _ := tx.MarshalBinary()
txs := []hexutil.Bytes{enc}
hash, err := api.CommitBlockV1(ctx, nextAttrs(), &txs, nil)
if err != nil {
t.Fatalf("CommitBlockV1 failed: %v", err)
}
head := ethservice.BlockChain().CurrentBlock()
if head.Hash() != hash {
t.Errorf("head hash mismatch: got %x want %x", head.Hash(), hash)
}
if head.Number.Uint64() != parent.Number.Uint64()+1 {
t.Errorf("head number mismatch: got %d want %d", head.Number.Uint64(), parent.Number.Uint64()+1)
}
block := ethservice.BlockChain().GetBlockByHash(hash)
if block == nil {
t.Fatal("committed block not found in chain")
}
if len(block.Transactions()) != 1 {
t.Fatalf("expected 1 transaction, got %d", len(block.Transactions()))
}
if block.Transactions()[0].Hash() != tx.Hash() {
t.Errorf("transaction hash mismatch: got %x want %x", block.Transactions()[0].Hash(), tx.Hash())
}
})
t.Run("commitBlockWithTransactionsFromTxPool", func(t *testing.T) {
parent := ethservice.BlockChain().CurrentBlock()
nonce, _ := ethservice.APIBackend.GetPoolNonce(ctx, testAddr)
tx, _ := types.SignTx(types.NewTransaction(nonce, testAddr, big.NewInt(1), params.TxGas, big.NewInt(params.InitialBaseFee*2), nil), types.LatestSigner(ethservice.BlockChain().Config()), testKey)
ethservice.TxPool().Add([]*types.Transaction{tx}, true)
hash, err := api.CommitBlockV1(ctx, nextAttrs(), nil, nil)
if err != nil {
t.Fatalf("CommitBlockV1 failed: %v", err)
}
head := ethservice.BlockChain().CurrentBlock()
if head.Hash() != hash {
t.Errorf("head hash mismatch: got %x want %x", head.Hash(), hash)
}
if head.Number.Uint64() != parent.Number.Uint64()+1 {
t.Errorf("head number mismatch: got %d want %d", head.Number.Uint64(), parent.Number.Uint64()+1)
}
block := ethservice.BlockChain().GetBlockByHash(hash)
if len(block.Transactions()) != 1 {
t.Fatalf("expected 1 transaction, got %d", len(block.Transactions()))
}
})
t.Run("commitWithExtraData", func(t *testing.T) {
extra := hexutil.Bytes([]byte("hello"))
emptyTxs := []hexutil.Bytes{}
hash, err := api.CommitBlockV1(ctx, nextAttrs(), &emptyTxs, &extra)
if err != nil {
t.Fatalf("CommitBlockV1 failed: %v", err)
}
block := ethservice.BlockChain().GetBlockByHash(hash)
if block == nil {
t.Fatal("committed block not found in chain")
}
if string(block.Extra()) != "hello" {
t.Errorf("extraData mismatch: got %q want %q", block.Extra(), "hello")
}
})
}

View file

@ -339,9 +339,10 @@ func (payload *Payload) updateSpanForDelivery(bSpan trace.Span) {
)
}
// 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) {
// 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,
@ -360,5 +361,26 @@ func (miner *Miner) BuildTestingPayload(args *BuildPayloadArgs, transactions []*
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
}