This commit is contained in:
Chase Wright 2026-05-22 11:56:41 +08:00 committed by GitHub
commit 123b9dd15e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 162 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,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

@ -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
}