diff --git a/eth/catalyst/api_testing.go b/eth/catalyst/api_testing.go index 2818d7f0bb..c44d0252d7 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" @@ -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 +} 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 db8126828a..1258843978 100644 --- a/miner/payload_building.go +++ b/miner/payload_building.go @@ -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 +}