eth/catalyst: add testing_commitBlockV1 (#34995)
Some checks are pending
/ Linux Build (push) Waiting to run
/ Linux Build (arm) (push) Waiting to run
/ Keeper Build (push) Waiting to run
/ Windows Build (push) Waiting to run
/ Docker Image (push) Waiting to run

Adds `testing_commitBlockV1`. It is the write companion of `testing_buildBlockV1`:
it builds a block from the provided payload attributes and transactions on
top of the current canonical head, inserts it, and sets it as the new
head, returning the new head hash.

---------

Co-authored-by: MariusVanDerWijden <m.vanderwijden@live.de>
This commit is contained in:
Chase Wright 2026-06-17 11:03:11 -05:00 committed by GitHub
parent 7122ecc3eb
commit 8c540cb082
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 117 additions and 4 deletions

View file

@ -17,6 +17,7 @@
package catalyst
import (
"context"
"errors"
"github.com/ethereum/go-ethereum/beacon/engine"
@ -47,6 +48,31 @@ func (api *testingAPI) BuildBlockV1(parentHash common.Hash, payloadAttributes en
if api.eth.BlockChain().CurrentBlock().Hash() != parentHash {
return nil, errors.New("parentHash is not current head")
}
_, block, err := api.buildTestingBlock(payloadAttributes, transactions, extraData)
return block, err
}
// 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) {
block, _, err := api.buildTestingBlock(payloadAttributes, transactions, extraData)
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
}
func (api *testingAPI) buildTestingBlock(payloadAttributes engine.PayloadAttributes, transactions *[]hexutil.Bytes, extraData *hexutil.Bytes) (*types.Block, *engine.ExecutionPayloadEnvelope, 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
@ -60,7 +86,7 @@ func (api *testingAPI) BuildBlockV1(parentHash common.Hash, payloadAttributes en
var err error
txs, err = engine.DecodeTransactions(dec)
if err != nil {
return nil, err
return nil, nil, err
}
}
extra := make([]byte, 0)

View file

@ -119,3 +119,90 @@ 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("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

@ -341,7 +341,7 @@ 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) {
func (miner *Miner) BuildTestingPayload(args *BuildPayloadArgs, transactions []*types.Transaction, empty bool, extraData []byte) (*types.Block, *engine.ExecutionPayloadEnvelope, error) {
fullParams := &generateParams{
timestamp: args.Timestamp,
forceTime: true,
@ -358,7 +358,7 @@ func (miner *Miner) BuildTestingPayload(args *BuildPayloadArgs, transactions []*
}
res := miner.generateWork(context.Background(), fullParams, false)
if res.err != nil {
return nil, res.err
return nil, nil, res.err
}
return engine.BlockToExecutableData(res.block, res.fees, res.sidecars, res.requests), nil
return res.block, engine.BlockToExecutableData(res.block, res.fees, res.sidecars, res.requests), nil
}