From 3b852842f90d46b510888e5416a7910eb6fd2aa4 Mon Sep 17 00:00:00 2001 From: Jared Wasinger Date: Tue, 24 Mar 2026 17:01:19 -0400 Subject: [PATCH] miner,tests: test payload construction as part of running spec tests --- beacon/engine/types.go | 18 ++- eth/catalyst/api_testing.go | 2 +- miner/miner.go | 13 ++ miner/payload_building.go | 38 +++++- miner/tx_source.go | 265 ++++++++++++++++++++++++++++++++++++ miner/worker.go | 80 +++++------ tests/block_test.go | 20 +++ tests/block_test_util.go | 90 ++++++++++-- 8 files changed, 462 insertions(+), 64 deletions(-) create mode 100644 miner/tx_source.go diff --git a/beacon/engine/types.go b/beacon/engine/types.go index 5c94e67de1..f33eb979a7 100644 --- a/beacon/engine/types.go +++ b/beacon/engine/types.go @@ -262,6 +262,18 @@ func ExecutableDataToBlock(data ExecutableData, versionedHashes []common.Hash, b // for stateless execution, so it skips checking if the executable data hashes to // the requested hash (stateless has to *compute* the root hash, it's not given). func ExecutableDataToBlockNoHash(data ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash, requests [][]byte) (*types.Block, error) { + var requestsHash *common.Hash + if requests != nil { + h := types.CalcRequestsHash(requests) + requestsHash = &h + } + + return ExecutableDataToBlockNoHashWithRequestsHash(data, versionedHashes, beaconRoot, requestsHash) +} + +// ExecutableDataToBlockNoHashWithRequestsHash does the same thing as +// ExecutableDataToBlockNoHash, but it takes a hash of the requests. +func ExecutableDataToBlockNoHashWithRequestsHash(data ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash, requestsHash *common.Hash) (*types.Block, error) { txs, err := DecodeTransactions(data.Transactions) if err != nil { return nil, err @@ -297,12 +309,6 @@ func ExecutableDataToBlockNoHash(data ExecutableData, versionedHashes []common.H withdrawalsRoot = &h } - var requestsHash *common.Hash - if requests != nil { - h := types.CalcRequestsHash(requests) - requestsHash = &h - } - header := &types.Header{ ParentHash: data.ParentHash, UncleHash: types.EmptyUncleHash, diff --git a/eth/catalyst/api_testing.go b/eth/catalyst/api_testing.go index 8586029468..0d80ee63a0 100644 --- a/eth/catalyst/api_testing.go +++ b/eth/catalyst/api_testing.go @@ -75,5 +75,5 @@ func (api *testingAPI) BuildBlockV1(parentHash common.Hash, payloadAttributes en Withdrawals: payloadAttributes.Withdrawals, BeaconRoot: payloadAttributes.BeaconRoot, } - return api.eth.Miner().BuildTestingPayload(args, txs, buildEmpty, extra) + return api.eth.Miner().BuildTestingPayload(args, txs, buildEmpty, extra, nil) } diff --git a/miner/miner.go b/miner/miner.go index 0ff0237a08..38ceed3d99 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -90,6 +90,19 @@ func New(eth Backend, config Config, engine consensus.Engine) *Miner { } } +// NewWithoutBackend creates a miner with the provided config. It is used in +// a testing context. +func NewWithoutBackend(chain *core.BlockChain, pool *txpool.TxPool, config Config, engine consensus.Engine) *Miner { + return &Miner{ + config: &config, + chainConfig: chain.Config(), + engine: engine, + txpool: pool, + chain: chain, + pending: &pending{}, + } +} + // Pending returns the currently pending block and associated receipts, logs // and statedb. The returned values can be nil in case the pending block is // not initialized. diff --git a/miner/payload_building.go b/miner/payload_building.go index ccaabec373..067735ec3a 100644 --- a/miner/payload_building.go +++ b/miner/payload_building.go @@ -20,6 +20,7 @@ import ( "context" "crypto/sha256" "encoding/binary" + "fmt" "math/big" "sync" "time" @@ -339,9 +340,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) buildPayloadWithOverrides(args *BuildPayloadArgs, transactions []*types.Transaction, empty bool, extraData []byte, gasLimit *uint64) (*newPayloadResult, error) { fullParams := &generateParams{ timestamp: args.Timestamp, forceTime: true, @@ -355,10 +354,43 @@ func (miner *Miner) BuildTestingPayload(args *BuildPayloadArgs, transactions []* forceOverrides: true, overrideExtraData: extraData, overrideTxs: transactions, + overrideGasLimit: gasLimit, } res := miner.generateWork(context.Background(), fullParams, false) if res.err != nil { return nil, res.err } + return res, nil +} + +// BuildPayloadWithOverrides creates a block with the content given by the parameters instead +// of using locally-available transactions. It attempts to include any transactions in the exact +// order they are specified. +func (miner *Miner) BuildPayloadWithOverrides(args *BuildPayloadArgs, transactions []*types.Transaction, empty bool, extraData []byte, gasLimit *uint64) (*engine.ExecutionPayloadEnvelope, error) { + res, err := miner.buildPayloadWithOverrides(args, transactions, empty, extraData, gasLimit) + if err != nil { + return nil, err + } + return engine.BlockToExecutableData(res.block, new(big.Int), res.sidecars, res.requests), 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. +// If a block cannot be constructed with the given parameters, an error is returned. +func (miner *Miner) BuildTestingPayload(args *BuildPayloadArgs, transactions []*types.Transaction, empty bool, extraData []byte, gasLimit *uint64) (*engine.ExecutionPayloadEnvelope, error) { + res, err := miner.buildPayloadWithOverrides(args, transactions, empty, extraData, gasLimit) + if err != nil { + return nil, err + } + // validate that the constructed block contains exactly the transactions specified, and in the specified order. + if len(res.block.Transactions()) != len(transactions) { + return nil, fmt.Errorf("constructed payload contained different number of txs than specified. want=%d, got=%d\n", len(transactions), len(res.block.Transactions())) + } + for i, expectedTx := range transactions { + if res.block.Transactions()[i].Hash() != expectedTx.Hash() { + return nil, fmt.Errorf("constructed block contained unexpected transaction at index %d: %x. expected %x\n", i, res.block.Transactions()[i].Hash(), expectedTx.Hash()) + } + } + return engine.BlockToExecutableData(res.block, new(big.Int), res.sidecars, res.requests), nil } diff --git a/miner/tx_source.go b/miner/tx_source.go new file mode 100644 index 0000000000..b2106691af --- /dev/null +++ b/miner/tx_source.go @@ -0,0 +1,265 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package miner + +import ( + "fmt" + "slices" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/txpool" + "github.com/ethereum/go-ethereum/core/types" +) + +// resolveableTransaction represents a transaction of which some of the fields +// are available, but the full transaction will be resolved from some source. +type resolveableTransaction interface { + Hash() common.Hash + Resolve() *types.Transaction + Gas() uint64 + BlobGas() uint64 +} + +// lazyTransaction wraps a txpool.LazyTransaction and implements resolveableTransaction. +type lazyTransaction struct { + *txpool.LazyTransaction +} + +func (l *lazyTransaction) Resolve() *types.Transaction { + return l.LazyTransaction.Resolve() +} + +func (l *lazyTransaction) Gas() uint64 { + return l.LazyTransaction.Gas +} + +func (l *lazyTransaction) Hash() common.Hash { + return l.LazyTransaction.Hash +} + +func (l *lazyTransaction) BlobGas() uint64 { + return l.LazyTransaction.BlobGas +} + +// resolvedTransaction implements resolveableTransaction for a transaction +// that was resolved in the first place. used for building payloads with +// tx set overrides +type resolvedTransaction struct { + *types.Transaction +} + +func (r *resolvedTransaction) Gas() uint64 { + return r.Transaction.Gas() +} + +func (r *resolvedTransaction) Resolve() *types.Transaction { + return r.Transaction +} + +// transactionQueue represents a list of transactions +// which are queued for inclusion. +type transactionQueue interface { + // Shift removes all transactions from the sender of the + // current highest-priority transaction for inclusion + Shift() + // Pop removes the highest-priority transaction from + // the queue. + Pop() + // HasBlobTxs returns true if the sender of the current + // highest-priority transaction has queued blob transactions. + HasBlobTxs() bool + // ClearBlobTxs removes all blob txs from the sender of the + // current highest-priority transaction from the queue. + ClearBlobTxs() +} + +// transactionSource is a source that the miner can +type transactionSource interface { + // Peek returns the next transaction which should be evaluated for inclusion + // and a transactionQueue for the sender account. + Peek() (resolveableTransaction, transactionQueue) +} + +// plainTxQueue implements transactionQueue for a set of non-blob transactions +type plainTxQueue struct { + txs *transactionsByPriceAndNonce +} + +func (q *plainTxQueue) Shift() { q.txs.Shift() } +func (q *plainTxQueue) Pop() { q.txs.Pop() } + +// HasBlobTxs always returns false: plain queues never carry blob transactions. +func (q *plainTxQueue) HasBlobTxs() bool { return false } + +// ClearBlobTxs is a no-op for plain queues and always returns false. +func (q *plainTxQueue) ClearBlobTxs() {} + +// blobTxQueue implements transactionQueue for a set of blob transactions +type blobTxQueue struct { + txs *transactionsByPriceAndNonce +} + +func (q *blobTxQueue) Shift() { + if q.txs != nil { + q.txs.Shift() + } +} + +func (q *blobTxQueue) Pop() { + if q.txs != nil { + q.txs.Pop() + } +} + +// HasBlobTxs always returns true: this queue exclusively holds blob transactions. +func (q *blobTxQueue) HasBlobTxs() bool { return true } + +func (q *blobTxQueue) ClearBlobTxs() { + if q.txs != nil { + q.txs.Clear() + } +} + +// feeOrderedTxSource implements transactionSource for the standard strategy +// of transaction inclusion: prioritize by profitability. +type feeOrderedTxSource struct { + plainQueue *plainTxQueue + blobQueue *blobTxQueue +} + +// newFeeOrderedTxSource creates a feeOrderedTxSource from separate plain and blob +// transaction sets. Either argument may be nil (e.g. when there are no blob +// transactions pending). +func newFeeOrderedTxSource(plain, blob *transactionsByPriceAndNonce) *feeOrderedTxSource { + s := &feeOrderedTxSource{ + plainQueue: &plainTxQueue{txs: plain}, + blobQueue: &blobTxQueue{txs: blob}, + } + + return s +} + +// Peek returns the next most profitable queued transaction +func (s *feeOrderedTxSource) Peek() (resolveableTransaction, transactionQueue) { + pTx, pTip := s.plainQueue.txs.Peek() + bTx, bTip := s.blobQueue.txs.Peek() + + switch { + case pTx == nil && bTx == nil: + return nil, nil + case pTx == nil: + return &lazyTransaction{bTx}, s.blobQueue + case bTx == nil: + return &lazyTransaction{pTx}, s.plainQueue + default: + if bTip.Gt(pTip) { + return &lazyTransaction{bTx}, s.blobQueue + } else { + return &lazyTransaction{pTx}, s.plainQueue + } + } +} + +// orderedTxSource implements transactionSource and transactionQueue. +// The transaction set and order is based on a pre-determined list. +type orderedTxSource struct { + // txs ordered as they are intended to be included in a payload + txs types.Transactions + // txs ordered by sender and nonce + orderedTxs map[common.Address][]*types.Transaction + signer types.Signer +} + +func newOrderedTxSource(txs types.Transactions, signer types.Signer) (*orderedTxSource, error) { + orderedTxs := make(map[common.Address][]*types.Transaction) + for _, tx := range txs { + from, _ := signer.Sender(tx) + if _, ok := orderedTxs[from]; !ok { + orderedTxs[from] = []*types.Transaction{tx} + continue + } + + // validate that no transactions from the same sender have conflicting nonces. + senderTxs := orderedTxs[from] + for _, senderTx := range senderTxs { + if tx.Nonce() == senderTx.Nonce() { + return nil, fmt.Errorf("conflicting transaction nonces") + } + } + + var ( + insertionPoint int + senderTx *types.Transaction + ) + for insertionPoint, senderTx = range senderTxs { + if senderTx.Nonce() > tx.Nonce() { + break + } + } + orderedTxs[from] = slices.Insert(senderTxs, insertionPoint, tx) + } + return &orderedTxSource{txs: txs, signer: signer, orderedTxs: orderedTxs}, nil +} + +func (s *orderedTxSource) Pop() { + from, _ := s.signer.Sender(s.txs[0]) + s.txs = slices.DeleteFunc(s.txs, func(tx *types.Transaction) bool { + txSender, _ := s.signer.Sender(tx) + return txSender == from + }) + delete(s.orderedTxs, from) +} + +func (s *orderedTxSource) Shift() { + delTx := s.txs[0] + sender, _ := s.signer.Sender(delTx) + + // application order of txs might not be by nonce, + // so deletion from the nonce-ordered set requires + // search + s.orderedTxs[sender] = slices.DeleteFunc(s.orderedTxs[sender], func(tx *types.Transaction) bool { + return delTx.Hash() == tx.Hash() + }) + + s.txs = s.txs[1:] +} + +func (s *orderedTxSource) HasBlobTxs() bool { + for _, tx := range s.txs { + if tx.Type() == types.BlobTxType { + return true + } + } + return false +} + +func (s *orderedTxSource) ClearBlobTxs() { + var remainingTxs types.Transactions + for _, tx := range s.txs { + if tx.Type() != types.BlobTxType { + remainingTxs = append(remainingTxs, tx) + } + } + s.txs = remainingTxs +} + +func (s *orderedTxSource) Peek() (resolveableTransaction, transactionQueue) { + if len(s.txs) == 0 { + return nil, nil + } + return &resolvedTransaction{s.txs[0]}, s +} diff --git a/miner/worker.go b/miner/worker.go index 39a61de318..035c3aa889 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -124,6 +124,7 @@ type generateParams struct { forceOverrides bool // Flag whether we should overwrite extraData and transactions overrideExtraData []byte overrideTxs []*types.Transaction + overrideGasLimit *uint64 } // generateWork generates a sealing block based on the given parameters. @@ -160,15 +161,15 @@ func (miner *Miner) generateWork(ctx context.Context, genParam *generateParams, work.size += uint64(genParam.withdrawals.Size()) if !genParam.noTxs { - // If forceOverrides is true and overrideTxs is not empty, commit the override transactions + // If forceOverrides is true, commit the override transactions // otherwise, fill the block with the current transactions from the txpool - if genParam.forceOverrides && len(genParam.overrideTxs) > 0 { - for _, tx := range genParam.overrideTxs { - work.state.SetTxContext(tx.Hash(), work.tcount) - if err := miner.commitTransaction(ctx, work, tx); err != nil { - // all passed transactions HAVE to be valid at this point - return &newPayloadResult{err: err} - } + if genParam.forceOverrides { + overrideSource, err := newOrderedTxSource(genParam.overrideTxs, work.signer) + if err != nil { + return &newPayloadResult{err: err} + } + if err := miner.commitTransactions(ctx, work, overrideSource, new(atomic.Int32)); err != nil { + log.Warn("block building failed", "err", err) } } else { interrupt := new(atomic.Int32) @@ -281,6 +282,9 @@ func (miner *Miner) prepareWork(ctx context.Context, genParams *generateParams, header.GasLimit = core.CalcGasLimit(parentGasLimit, miner.config.GasCeil) } } + if genParams.overrideGasLimit != nil { + header.GasLimit = *genParams.overrideGasLimit + } // Run the consensus preparation with the default or customized consensus engine. // Note that the `header.Time` may be changed. if err := miner.engine.Prepare(miner.chain, header); err != nil { @@ -410,7 +414,12 @@ func (miner *Miner) applyTransaction(env *environment, tx *types.Transaction) (* return receipt, nil } -func (miner *Miner) commitTransactions(ctx context.Context, env *environment, plainTxs, blobTxs *transactionsByPriceAndNonce, interrupt *atomic.Int32) error { +func (miner *Miner) commitTransactions( + ctx context.Context, + env *environment, + allTxs transactionSource, + interrupt *atomic.Int32, +) error { ctx, _, spanEnd := telemetry.StartSpan(ctx, "miner.commitTransactions") defer spanEnd(nil) isCancun := miner.chainConfig.IsCancun(env.header.Number, env.header.Time) @@ -426,39 +435,20 @@ func (miner *Miner) commitTransactions(ctx context.Context, env *environment, pl log.Trace("Not enough gas for further transactions", "have", env.gasPool, "want", params.TxGas) break } - // If we don't have enough blob space for any further blob transactions, - // skip that list altogether - if !blobTxs.Empty() && env.blobs >= miner.maxBlobsPerBlock(env.header.Time) { - log.Trace("Not enough blob space for further blob transactions") - blobTxs.Clear() - // Fall though to pick up any plain txs - } - // Retrieve the next transaction and abort if all done. - var ( - ltx *txpool.LazyTransaction - txs *transactionsByPriceAndNonce - ) - pltx, ptip := plainTxs.Peek() - bltx, btip := blobTxs.Peek() - switch { - case pltx == nil: - txs, ltx = blobTxs, bltx - case bltx == nil: - txs, ltx = plainTxs, pltx - default: - if ptip.Lt(btip) { - txs, ltx = blobTxs, bltx - } else { - txs, ltx = plainTxs, pltx - } - } + ltx, txs := allTxs.Peek() if ltx == nil { break } + + if txs.HasBlobTxs() && env.blobs >= miner.maxBlobsPerBlock(env.header.Time) { + log.Trace("Not enough blob space for further blob transactions") + txs.ClearBlobTxs() + continue + } // If we don't have enough space for the next transaction, skip the account. - if env.gasPool.Gas() < ltx.Gas { - log.Trace("Not enough gas left for transaction", "hash", ltx.Hash, "left", env.gasPool.Gas(), "needed", ltx.Gas) + if env.gasPool.Gas() < ltx.Gas() { + log.Trace("Not enough gas left for transaction", "hash", ltx.Hash(), "left", env.gasPool.Gas(), "needed", ltx.Gas()) txs.Pop() continue } @@ -468,8 +458,8 @@ func (miner *Miner) commitTransactions(ctx context.Context, env *environment, pl // a defined schedule, so we need to verify it's safe to call. if isCancun { left := miner.maxBlobsPerBlock(env.header.Time) - env.blobs - if left < int(ltx.BlobGas/params.BlobTxBlobGasPerBlob) { - log.Trace("Not enough blob space left for transaction", "hash", ltx.Hash, "left", left, "needed", ltx.BlobGas/params.BlobTxBlobGasPerBlob) + if left < int(ltx.BlobGas()/params.BlobTxBlobGasPerBlob) { + log.Trace("Not enough blob space left for transaction", "hash", ltx.Hash(), "left", left, "needed", ltx.BlobGas()/params.BlobTxBlobGasPerBlob) txs.Pop() continue } @@ -478,7 +468,7 @@ func (miner *Miner) commitTransactions(ctx context.Context, env *environment, pl // Transaction seems to fit, pull it up from the pool tx := ltx.Resolve() if tx == nil { - log.Trace("Ignoring evicted transaction", "hash", ltx.Hash) + log.Trace("Ignoring evicted transaction", "hash", ltx.Hash()) txs.Pop() continue } @@ -495,7 +485,7 @@ func (miner *Miner) commitTransactions(ctx context.Context, env *environment, pl // Check whether the tx is replay protected. If we're not in the EIP155 hf // phase, start ignoring the sender until we do. if tx.Protected() && !miner.chainConfig.IsEIP155(env.header.Number) { - log.Trace("Ignoring replay protected transaction", "hash", ltx.Hash, "eip155", miner.chainConfig.EIP155Block) + log.Trace("Ignoring replay protected transaction", "hash", ltx.Hash(), "eip155", miner.chainConfig.EIP155Block) txs.Pop() continue } @@ -506,7 +496,7 @@ func (miner *Miner) commitTransactions(ctx context.Context, env *environment, pl switch { case errors.Is(err, core.ErrNonceTooLow): // New head notification data race between the transaction pool and miner, shift - log.Trace("Skipping transaction with low nonce", "hash", ltx.Hash, "sender", from, "nonce", tx.Nonce()) + log.Trace("Skipping transaction with low nonce", "hash", ltx.Hash(), "sender", from, "nonce", tx.Nonce()) txs.Shift() case errors.Is(err, nil): @@ -516,7 +506,7 @@ func (miner *Miner) commitTransactions(ctx context.Context, env *environment, pl default: // Transaction is regarded as invalid, drop all consecutive transactions from // the same sender because of `nonce-too-high` clause. - log.Debug("Transaction failed, account skipped", "hash", ltx.Hash, "err", err) + log.Debug("Transaction failed, account skipped", "hash", ltx.Hash(), "err", err) txs.Pop() } } @@ -581,7 +571,7 @@ func (miner *Miner) fillTransactions(ctx context.Context, interrupt *atomic.Int3 plainTxs := newTransactionsByPriceAndNonce(env.signer, prioPlainTxs, env.header.BaseFee) blobTxs := newTransactionsByPriceAndNonce(env.signer, prioBlobTxs, env.header.BaseFee) - if err := miner.commitTransactions(ctx, env, plainTxs, blobTxs, interrupt); err != nil { + if err := miner.commitTransactions(ctx, env, newFeeOrderedTxSource(plainTxs, blobTxs), interrupt); err != nil { return err } } @@ -589,7 +579,7 @@ func (miner *Miner) fillTransactions(ctx context.Context, interrupt *atomic.Int3 plainTxs := newTransactionsByPriceAndNonce(env.signer, normalPlainTxs, env.header.BaseFee) blobTxs := newTransactionsByPriceAndNonce(env.signer, normalBlobTxs, env.header.BaseFee) - if err := miner.commitTransactions(ctx, env, plainTxs, blobTxs, interrupt); err != nil { + if err := miner.commitTransactions(ctx, env, newFeeOrderedTxSource(plainTxs, blobTxs), interrupt); err != nil { return err } } diff --git a/tests/block_test.go b/tests/block_test.go index c718b304b6..103216ab2d 100644 --- a/tests/block_test.go +++ b/tests/block_test.go @@ -60,6 +60,26 @@ func TestBlockchain(t *testing.T) { bt.skipLoad(`.*bcFrontierToHomestead/blockChainFrontierWithLargerTDvsHomesteadBlockchain.json`) bt.skipLoad(`.*bcFrontierToHomestead/blockChainFrontierWithLargerTDvsHomesteadBlockchain2.json`) + /* + skip these for the payload building only: + --- FAIL: TestExecutionSpecBlocktests/osaka/eip7934_block_rlp_limit/test_block_at_rlp_limit_with_logs.json (0.45s) + block_test.go:110: test with config {snapshotter:false, scheme:hash} failed: mismatch in block hash. wanted 51a93fb2f038278424742e6ceaf476f83fd30ab84580216f08d124e258554b00, got 8dddbd82ec3e09b201230202207069fa64b6fb21a89a3b5e8ab2421adac933ea + --- FAIL: TestExecutionSpecBlocktests/osaka/eip7934_block_rlp_limit/test_block_at_rlp_size_limit_boundary.json (1.30s) + --- FAIL: TestExecutionSpecBlocktests/osaka/eip7934_block_rlp_limit/test_block_at_rlp_size_limit_boundary.json/tests/osaka/eip7934_block_rlp_limit/test_max_block_rlp_size.py::test_block_at_rlp_size_limit_boundary[fork_Osaka-blockchain_test-max_rlp_size] (0.10s) + block_test.go:110: test with config {snapshotter:false, scheme:hash} failed: mismatch in block hash. wanted bf2a96a93b4b519a36cc17ba7e5b9de7a2a8efef8f15f00e97d63c067fcae532, got 99be2c1ff6a5ab3b7c80eca8ed12e933417efc4c9fbc73a23873017a4888ac30 + --- FAIL: TestExecutionSpecBlocktests/osaka/eip7934_block_rlp_limit/test_block_at_rlp_size_limit_boundary.json/tests/osaka/eip7934_block_rlp_limit/test_max_block_rlp_size.py::test_block_at_rlp_size_limit_boundary[fork_Osaka-blockchain_test-max_rlp_size_minus_1_byte] (0.09s) + block_test.go:110: test with config {snapshotter:false, scheme:hash} failed: mismatch in block hash. wanted f01227ceacf73b2fa7492cc36f307535d6734a0de468b42a136bd39561a613ec, got 5a5019a9d576c93e55ad94e2fe2b6ba8881d96c9df4b7d4ea8c4780520fa232e + --- FAIL: TestExecutionSpecBlocktests/osaka/eip7934_block_rlp_limit/test_block_rlp_size_at_limit_with_all_typed_transactions.json (2.51s) + --- FAIL: TestExecutionSpecBlocktests/osaka/eip7934_block_rlp_limit/test_block_rlp_size_at_limit_with_all_typed_transactions.json/tests/osaka/eip7934_block_rlp_limit/test_max_block_rlp_size.py::test_block_rlp_size_at_limit_with_all_typed_transactions[fork_Osaka-typed_transaction_0-blockchain_test] (0.09s) + block_test.go:110: test with config {snapshotter:false, scheme:hash} failed: mismatch in block hash. wanted e15a14788a8b960fa9951cdd8f059b9c53ac50ca12c2d63255a437fb2a10a93f, got 194fbad30f9fe3f0e60627a0fcc6eec78884ce117e9d13e04b7a44f8e14c0547 + --- FAIL: TestExecutionSpecBlocktests/osaka/eip7934_block_rlp_limit/test_block_rlp_size_at_limit_with_all_typed_transactions.json/tests/osaka/eip7934_block_rlp_limit/test_max_block_rlp_size.py::test_block_rlp_size_at_limit_with_all_typed_transactions[fork_Osaka-typed_transaction_1-blockchain_test] (0.09s) + block_test.go:110: test with config {snapshotter:false, scheme:hash} failed: mismatch in block hash. wanted 5ad6568d4e64aa88faa2ad73a249bc7fcd9a36a23629ee6e914a05fa75a87293, got 0f9f047ddfc220c18b53cfc82871a67c4166eebda65bb0c137af87cf20e6c25a + --- FAIL: TestExecutionSpecBlocktests/osaka/eip7934_block_rlp_limit/test_block_rlp_size_at_limit_with_all_typed_transactions.json/tests/osaka/eip7934_block_rlp_limit/test_max_block_rlp_size.py::test_block_rlp_size_at_limit_with_all_typed_transactions[fork_Osaka-typed_transaction_2-blockchain_test] (0.09s) + block_test.go:110: test with config {snapshotter:false, scheme:hash} failed: mismatch in block hash. wanted 1570cd16ac3b096866bf5765fdb28571140f70392bda5ac7fbb034e5e9a93acb, got 0d254beb06fe3b4d4ffbe9cd6fa914f402a809e8f60ce311f849431e485ba8d9 + --- FAIL: TestExecutionSpecBlocktests/osaka/eip7934_block_rlp_limit/test_block_rlp_size_at_limit_with_all_typed_transactions.json/tests/osaka/eip7934_block_rlp_limit/test_max_block_rlp_size.py::test_block_rlp_size_at_limit_with_all_typed_transactions[fork_Osaka-typed_transaction_4-blockchain_test] (0.11s) + block_test.go:110: test with config {snapshotter:false, scheme:hash} failed: mismatch in block hash. wanted 8a12b02aaf555c15e83ac9831205b35006c94e6c8aec7d2c99a135507fa4103b, got d65726606a9c28a68cea095b2682872d88cb535ff295b135d156f28d3c2c59f7 + */ + // With chain history removal, TDs become unavailable, this transition tests based on TTD are unrunnable bt.skipLoad(`.*bcArrowGlacierToParis/powToPosBlockRejection.json`) diff --git a/tests/block_test_util.go b/tests/block_test_util.go index 00411073e2..6870e2a18f 100644 --- a/tests/block_test_util.go +++ b/tests/block_test_util.go @@ -27,6 +27,10 @@ import ( "os" "reflect" + "github.com/ethereum/go-ethereum/beacon/engine" + "github.com/ethereum/go-ethereum/core/txpool" + "github.com/ethereum/go-ethereum/miner" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/math" @@ -230,21 +234,48 @@ func (t *BlockTest) genesis(config *params.ChainConfig) *core.Genesis { } } +// return a payload version for post-merge hard-forks. otherwise return an error. +func getPayloadVersion(header *types.Header, chainConfig *params.ChainConfig) engine.PayloadVersion { + switch { + case chainConfig.IsPostMerge(header.Number.Uint64(), header.Time): + return engine.PayloadV1 + case chainConfig.IsShanghai(header.Number, header.Time): + return engine.PayloadV2 + case chainConfig.IsPrague(header.Number, header.Time): + case chainConfig.IsCancun(header.Number, header.Time): + return engine.PayloadV3 + } + panic("invalid post-merge fork") +} + /* See https://ethereum-tests.readthedocs.io/en/latest/blockchain-ref.html - Whether a block is valid or not is a bit subtle, it's defined by presence of - blockHeader, transactions and uncleHeaders fields. If they are missing, the block is - invalid and we must verify that we do not accept it. +Whether a block is valid or not is a bit subtle, it's defined by presence of +blockHeader, transactions and uncleHeaders fields. If they are missing, the block is +invalid and we must verify that we do not accept it. - Since some tests mix valid and invalid blocks we need to check this for every block. +Since some tests mix valid and invalid blocks we need to check this for every block. - If a block is invalid it does not necessarily fail the test, if it's invalidness is - expected we are expected to ignore it and continue processing and then validate the - post state. +If a block is invalid it does not necessarily fail the test, if it's invalidness is +expected we are expected to ignore it and continue processing and then validate the +post state. + +If the block is post-merge, insertBlocks will construct payloads via the miner, +validating that they match exactly blocks in the test which: + - are past the merge block + - are not specified as invalid by the test + - do not contain blob transactions */ func (t *BlockTest) insertBlocks(blockchain *core.BlockChain) ([]btBlock, error) { - validBlocks := make([]btBlock, 0) + var ( + pool *txpool.TxPool + config = miner.Config{} + consensusEngine = beacon.New(ethash.NewFaker()) + builder = miner.NewWithoutBackend(blockchain, pool, config, consensusEngine) + validBlocks = make([]btBlock, 0) + ) + // insert the test blocks, which will execute all transactions for bi, b := range t.json.Blocks { cb, err := b.decode() @@ -257,7 +288,48 @@ func (t *BlockTest) insertBlocks(blockchain *core.BlockChain) ([]btBlock, error) } } // RLP decoding worked, try to insert into chain: - blocks := types.Blocks{cb} + var blocks types.Blocks + + var blockHasBlobTxs bool + for _, tx := range cb.Transactions() { + if tx.BlobHashes() != nil { + blockHasBlobTxs = true + break + } + } + + // test payload building on post-merge blocks that are valid and don't contain blob transactions + if !blockHasBlobTxs && b.BlockHeader != nil && (b.BlockHeader.Difficulty == nil || b.BlockHeader.Difficulty.Cmp(common.Big0) == 0) { + payloadVersion := getPayloadVersion(cb.Header(), blockchain.Config()) + + gasLimit := cb.GasLimit() + args := miner.BuildPayloadArgs{ + Parent: cb.ParentHash(), + Timestamp: cb.Time(), + FeeRecipient: cb.Coinbase(), + Random: cb.MixDigest(), + Withdrawals: cb.Withdrawals(), + BeaconRoot: cb.BeaconRoot(), + SlotNum: cb.SlotNumber(), + Version: payloadVersion, + } + envelope, err := builder.BuildPayloadWithOverrides(&args, cb.Transactions(), false, cb.Extra(), &gasLimit) + if err != nil { + return nil, err + } + + constructedBlock, err := engine.ExecutableDataToBlockNoHashWithRequestsHash(*envelope.ExecutionPayload, nil, cb.BeaconRoot(), cb.RequestsHash()) + if err != nil { + return nil, err + } + + if constructedBlock.Hash() != cb.Hash() { + return nil, fmt.Errorf("mismatch in block hash. wanted %x, got %x", cb.Hash(), constructedBlock.Hash()) + } + blocks = types.Blocks{constructedBlock} + } else { + blocks = types.Blocks{cb} + } i, err := blockchain.InsertChain(blocks) if err != nil { if b.BlockHeader == nil {