From 8c540cb082bf60a4260c2fef8ea30375c802e7f7 Mon Sep 17 00:00:00 2001 From: Chase Wright Date: Wed, 17 Jun 2026 11:03:11 -0500 Subject: [PATCH] eth/catalyst: add testing_commitBlockV1 (#34995) 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 --- eth/catalyst/api_testing.go | 28 +++++++++- eth/catalyst/api_testing_test.go | 87 ++++++++++++++++++++++++++++++++ miner/payload_building.go | 6 +-- 3 files changed, 117 insertions(+), 4 deletions(-) diff --git a/eth/catalyst/api_testing.go b/eth/catalyst/api_testing.go index 2818d7f0bb..b017641a5b 100644 --- a/eth/catalyst/api_testing.go +++ b/eth/catalyst/api_testing.go @@ -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) diff --git a/eth/catalyst/api_testing_test.go b/eth/catalyst/api_testing_test.go index fd4d28d26a..8b20df13e4 100644 --- a/eth/catalyst/api_testing_test.go +++ b/eth/catalyst/api_testing_test.go @@ -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") + } + }) +} diff --git a/miner/payload_building.go b/miner/payload_building.go index a2cc8df9d0..c4322e3317 100644 --- a/miner/payload_building.go +++ b/miner/payload_building.go @@ -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 }