diff --git a/beacon/engine/types.go b/beacon/engine/types.go index ddb276ab09..e1015d0006 100644 --- a/beacon/engine/types.go +++ b/beacon/engine/types.go @@ -237,8 +237,8 @@ func decodeTransactions(enc [][]byte) ([]*types.Transaction, error) { // and that the blockhash of the constructed block matches the parameters. Nil // Withdrawals value will propagate through the returned block. Empty // Withdrawals value must be passed via non-nil, length 0 value in data. -func ExecutableDataToBlock(data ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash, requests [][]byte) (*types.Block, error) { - block, err := ExecutableDataToBlockNoHash(data, versionedHashes, beaconRoot, requests) +func ExecutableDataToBlock(data ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash, requests [][]byte, inclusionListTxs []*types.Transaction) (*types.Block, error) { + block, err := ExecutableDataToBlockNoHash(data, versionedHashes, beaconRoot, requests, inclusionListTxs) if err != nil { return nil, err } @@ -251,7 +251,7 @@ func ExecutableDataToBlock(data ExecutableData, versionedHashes []common.Hash, b // ExecutableDataToBlockNoHash is analogous to ExecutableDataToBlock, but is used // 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) { +func ExecutableDataToBlockNoHash(data ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash, requests [][]byte, inclusionListTxs []*types.Transaction) (*types.Block, error) { txs, err := decodeTransactions(data.Transactions) if err != nil { return nil, err @@ -317,7 +317,8 @@ func ExecutableDataToBlockNoHash(data ExecutableData, versionedHashes []common.H } return types.NewBlockWithHeader(header). WithBody(types.Body{Transactions: txs, Uncles: nil, Withdrawals: data.Withdrawals}). - WithWitness(data.ExecutionWitness), + WithWitness(data.ExecutionWitness). + WithInclusionListTransactions(inclusionListTxs), nil } diff --git a/core/blockchain.go b/core/blockchain.go index b7acd12aca..b6bec600b8 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -1741,7 +1741,7 @@ func (bc *BlockChain) InsertChain(chain types.Blocks) (int, error) { } defer bc.chainmu.Unlock() - _, n, err := bc.insertChain(chain, true, false) // No witness collection for mass inserts (would get super large) + _, n, _, err := bc.insertChain(chain, true, false) // No witness collection for mass inserts (would get super large) return n, err } @@ -1753,10 +1753,10 @@ func (bc *BlockChain) InsertChain(chain types.Blocks) (int, error) { // racey behaviour. If a sidechain import is in progress, and the historic state // is imported, but then new canon-head is added before the actual sidechain // completes, then the historic state could be pruned again -func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool, makeWitness bool) (*stateless.Witness, int, error) { +func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool, makeWitness bool) (*stateless.Witness, int, *bool, error) { // If the chain is terminating, don't even bother starting up. if bc.insertStopped() { - return nil, 0, nil + return nil, 0, nil, nil } if atomic.AddInt32(&bc.blockProcCounter, 1) == 1 { @@ -1821,7 +1821,7 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool, makeWitness for block != nil && bc.skipBlock(err, it) { log.Debug("Writing previously known block", "number", block.Number(), "hash", block.Hash()) if err := bc.writeKnownBlock(block); err != nil { - return nil, it.index, err + return nil, it.index, nil, err } lastCanon = block @@ -1840,7 +1840,7 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool, makeWitness // We're post-merge and the parent is pruned, try to recover the parent state log.Debug("Pruned ancestor", "number", block.Number(), "hash", block.Hash()) _, err := bc.recoverAncestors(block, makeWitness) - return nil, it.index, err + return nil, it.index, nil, err } // Some other error(except ErrKnownBlock) occurred, abort. // ErrKnownBlock is allowed here since some known blocks @@ -1848,7 +1848,7 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool, makeWitness case err != nil && !errors.Is(err, ErrKnownBlock): stats.ignored += len(it.chain) bc.reportBlock(block, nil, err) - return nil, it.index, err + return nil, it.index, nil, err } // Track the singleton witness from this chain insertion (if any) var witness *stateless.Witness @@ -1889,7 +1889,7 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool, makeWitness "hash", block.Hash(), "number", block.NumberU64()) } if err := bc.writeKnownBlock(block); err != nil { - return nil, it.index, err + return nil, it.index, nil, err } stats.processed++ if bc.logger != nil && bc.logger.OnSkippedBlock != nil { @@ -1913,7 +1913,7 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool, makeWitness start := time.Now() res, err := bc.ProcessBlock(parent.Root, block, setHead, makeWitness && len(chain) == 1) if err != nil { - return nil, it.index, err + return nil, it.index, nil, err } // Report the import stats before returning the various results stats.processed++ @@ -1934,7 +1934,7 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool, makeWitness // After merge we expect few side chains. Simply count // all blocks the CL gives us for GC processing time bc.gcproc += res.procTime - return witness, it.index, nil // Direct block insertion of a single block + return witness, it.index, res.inclusionListSatisfied, nil // Direct block insertion of a single block } switch res.status { case CanonStatTy: @@ -1965,16 +1965,17 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool, makeWitness } stats.ignored += it.remaining() - return witness, it.index, err + return witness, it.index, nil, err } // blockProcessingResult is a summary of block processing // used for updating the stats. type blockProcessingResult struct { - usedGas uint64 - procTime time.Duration - status WriteStatus - witness *stateless.Witness + usedGas uint64 + procTime time.Duration + status WriteStatus + witness *stateless.Witness + inclusionListSatisfied *bool } func (bpr *blockProcessingResult) Witness() *stateless.Witness { @@ -2179,10 +2180,11 @@ func (bc *BlockChain) ProcessBlock(parentRoot common.Hash, block *types.Block, s chainMgaspsMeter.Update(time.Duration(mgasps)) return &blockProcessingResult{ - usedGas: res.GasUsed, - procTime: proctime, - status: status, - witness: witness, + usedGas: res.GasUsed, + procTime: proctime, + status: status, + witness: witness, + inclusionListSatisfied: res.InclusionListSatisfied, }, nil } @@ -2193,7 +2195,7 @@ func (bc *BlockChain) ProcessBlock(parentRoot common.Hash, block *types.Block, s // The method writes all (header-and-body-valid) blocks to disk, then tries to // switch over to the new chain if the TD exceeded the current chain. // insertSideChain is only used pre-merge. -func (bc *BlockChain) insertSideChain(block *types.Block, it *insertIterator, makeWitness bool) (*stateless.Witness, int, error) { +func (bc *BlockChain) insertSideChain(block *types.Block, it *insertIterator, makeWitness bool) (*stateless.Witness, int, *bool, error) { var current = bc.CurrentBlock() // The first sidechain block error is already verified to be ErrPrunedAncestor. @@ -2222,13 +2224,13 @@ func (bc *BlockChain) insertSideChain(block *types.Block, it *insertIterator, ma // If someone legitimately side-mines blocks, they would still be imported as usual. However, // we cannot risk writing unverified blocks to disk when they obviously target the pruning // mechanism. - return nil, it.index, errors.New("sidechain ghost-state attack") + return nil, it.index, nil, errors.New("sidechain ghost-state attack") } } if !bc.HasBlock(block.Hash(), block.NumberU64()) { start := time.Now() if err := bc.writeBlockWithoutState(block); err != nil { - return nil, it.index, err + return nil, it.index, nil, err } log.Debug("Injected sidechain block", "number", block.Number(), "hash", block.Hash(), "diff", block.Difficulty(), "elapsed", common.PrettyDuration(time.Since(start)), @@ -2245,7 +2247,7 @@ func (bc *BlockChain) insertSideChain(block *types.Block, it *insertIterator, ma for parent != nil && !bc.HasState(parent.Root) { if bc.stateRecoverable(parent.Root) { if err := bc.triedb.Recover(parent.Root); err != nil { - return nil, 0, err + return nil, 0, nil, err } break } @@ -2255,7 +2257,7 @@ func (bc *BlockChain) insertSideChain(block *types.Block, it *insertIterator, ma parent = bc.GetHeader(parent.ParentHash, parent.Number.Uint64()-1) } if parent == nil { - return nil, it.index, errors.New("missing parent") + return nil, it.index, nil, errors.New("missing parent") } // Import all the pruned blocks to make the state available var ( @@ -2274,15 +2276,15 @@ func (bc *BlockChain) insertSideChain(block *types.Block, it *insertIterator, ma // memory here. if len(blocks) >= 2048 || memory > 64*1024*1024 { log.Info("Importing heavy sidechain segment", "blocks", len(blocks), "start", blocks[0].NumberU64(), "end", block.NumberU64()) - if _, _, err := bc.insertChain(blocks, true, false); err != nil { - return nil, 0, err + if _, _, _, err := bc.insertChain(blocks, true, false); err != nil { + return nil, 0, nil, err } blocks, memory = blocks[:0], 0 // If the chain is terminating, stop processing blocks if bc.insertStopped() { log.Debug("Abort during blocks processing") - return nil, 0, nil + return nil, 0, nil, nil } } } @@ -2290,7 +2292,7 @@ func (bc *BlockChain) insertSideChain(block *types.Block, it *insertIterator, ma log.Info("Importing sidechain segment", "start", blocks[0].NumberU64(), "end", blocks[len(blocks)-1].NumberU64()) return bc.insertChain(blocks, true, makeWitness) } - return nil, 0, nil + return nil, 0, nil, nil } // recoverAncestors finds the closest ancestor with available state and re-execute @@ -2337,7 +2339,7 @@ func (bc *BlockChain) recoverAncestors(block *types.Block, makeWitness bool) (co } else { b = bc.GetBlock(hashes[i], numbers[i]) } - if _, _, err := bc.insertChain(types.Blocks{b}, false, makeWitness && i == 0); err != nil { + if _, _, _, err := bc.insertChain(types.Blocks{b}, false, makeWitness && i == 0); err != nil { return b.ParentHash(), err } } @@ -2564,14 +2566,14 @@ func (bc *BlockChain) reorg(oldHead *types.Header, newHead *types.Header) error // The key difference between the InsertChain is it won't do the canonical chain // updating. It relies on the additional SetCanonical call to finalize the entire // procedure. -func (bc *BlockChain) InsertBlockWithoutSetHead(block *types.Block, makeWitness bool) (*stateless.Witness, error) { +func (bc *BlockChain) InsertBlockWithoutSetHead(block *types.Block, makeWitness bool) (*stateless.Witness, *bool, error) { if !bc.chainmu.TryLock() { - return nil, errChainStopped + return nil, nil, errChainStopped } defer bc.chainmu.Unlock() - witness, _, err := bc.insertChain(types.Blocks{block}, false, makeWitness) - return witness, err + witness, _, inclusionListSatisfied, err := bc.insertChain(types.Blocks{block}, false, makeWitness) + return witness, inclusionListSatisfied, err } // SetCanonical rewinds the chain to set the new head block as the specified diff --git a/core/blockchain_test.go b/core/blockchain_test.go index b749798f9c..ed18fa52e3 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -3456,7 +3456,7 @@ func testSetCanonical(t *testing.T, scheme string) { gen.AddTx(tx) }) for _, block := range side { - _, err := chain.InsertBlockWithoutSetHead(block, false) + _, _, err := chain.InsertBlockWithoutSetHead(block, false) if err != nil { t.Fatalf("Failed to insert into chain: %v", err) } diff --git a/core/state_processor.go b/core/state_processor.go index b66046f501..0f085b21be 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -108,6 +108,9 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg receipts = append(receipts, receipt) allLogs = append(allLogs, receipt.Logs...) } + + isInclusionListSatisfied := ValidateInclusionListTransactions(evm, block, statedb, block.InclusionListTransactions()) + // Read requests if Prague is enabled. var requests [][]byte if config.IsPrague(block.Number(), block.Time()) { @@ -130,10 +133,11 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg p.chain.Engine().Finalize(p.chain, header, tracingStateDB, block.Body()) return &ProcessResult{ - Receipts: receipts, - Requests: requests, - Logs: allLogs, - GasUsed: *usedGas, + Receipts: receipts, + Requests: requests, + Logs: allLogs, + GasUsed: *usedGas, + InclusionListSatisfied: &isInclusionListSatisfied, }, nil } @@ -315,6 +319,66 @@ func processRequestsSystemCall(requests *[][]byte, evm *vm.EVM, requestType byte return nil } +// ValidateInclusionListTransactions verifies that all transactions in the inclusion list +// are either included in the block or cannot be appended at the end of the block. +// If any appendable transaction is found, the block fails to meet the inclusion list constraints. +func ValidateInclusionListTransactions(evm *vm.EVM, block *types.Block, statedb *state.StateDB, inclusionListTxs []*types.Transaction) bool { + // Create a map of transaction hashes present in the block. + isIncludedTx := make(map[common.Hash]bool) + for _, tx := range block.Transactions() { + isIncludedTx[tx.Hash()] = true + } + + // Get the block's gas limit and gas left. + gasLimit := block.GasLimit() + gasLeft := gasLimit - block.GasUsed() + + // Make a singer to get transaction senders. + signer := types.MakeSigner(evm.ChainConfig(), block.Number(), block.Time()) + + // Iterate over each transaction in the inclusion list and check if it is either included in the block or cannot be placed at the end of the block. + for _, tx := range inclusionListTxs { + // Check if the transaction is included in the block. + if isIncludedTx[tx.Hash()] { + continue + } + + // EIP-7805 doesn't support blob transactions in inclusion list + if tx.Type() == types.BlobTxType { + continue + } + + // Check if there is not enough gas left to execute the transaction. + if tx.Gas() > gasLeft { + continue + } + + // Get the transaction sender. + from, err := types.Sender(signer, tx) + if err != nil { + continue + } + + // Check if the sender has not enough balance to cover the transaction cost. + balance := statedb.GetBalance(from).ToBig() + cost := tx.Cost() + if balance.Cmp(cost) < 0 { + continue + } + + // Check if the sender has a nonce that doesn't match with the transaction nonce. + nonce := statedb.GetNonce(from) + if nonce != tx.Nonce() { + continue + } + + // This transaction could have been appended at the end of the block. The block fails to satisfy the inclusion list constraints. + return false + } + + return true +} + var depositTopic = common.HexToHash("0x649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c5") // ParseDepositLogs extracts the EIP-6110 deposit values from logs emitted by diff --git a/core/types.go b/core/types.go index bed20802ab..7dbc511e52 100644 --- a/core/types.go +++ b/core/types.go @@ -53,8 +53,9 @@ type Processor interface { // ProcessResult contains the values computed by Process. type ProcessResult struct { - Receipts types.Receipts - Requests [][]byte - Logs []*types.Log - GasUsed uint64 + Receipts types.Receipts + Requests [][]byte + Logs []*types.Log + GasUsed uint64 + InclusionListSatisfied *bool } diff --git a/core/types/block.go b/core/types/block.go index b5b6468a13..aa84c89aa6 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -214,6 +214,11 @@ type Block struct { // that process it. witness *ExecutionWitness + // inclusion list transactions are not an encoded part of the block body. + // It is held in Block in order for easy relaying to the places + // that process it. + inclusionListTxs Transactions + // caches hash atomic.Pointer[common.Hash] size atomic.Uint64 @@ -432,6 +437,9 @@ func (b *Block) BlobGasUsed() *uint64 { // ExecutionWitness returns the verkle execution witneess + proof for a block func (b *Block) ExecutionWitness() *ExecutionWitness { return b.witness } +// InclusionListTransactions returns the inclusion list transactions for a block +func (b *Block) InclusionListTransactions() Transactions { return b.inclusionListTxs } + // Size returns the true RLP encoded storage size of the block, either by encoding // and returning it, or returning a previously cached value. func (b *Block) Size() uint64 { @@ -490,11 +498,12 @@ func NewBlockWithHeader(header *Header) *Block { // the sealed one. func (b *Block) WithSeal(header *Header) *Block { return &Block{ - header: CopyHeader(header), - transactions: b.transactions, - uncles: b.uncles, - withdrawals: b.withdrawals, - witness: b.witness, + header: CopyHeader(header), + transactions: b.transactions, + uncles: b.uncles, + withdrawals: b.withdrawals, + witness: b.witness, + inclusionListTxs: b.inclusionListTxs, } } @@ -502,11 +511,12 @@ func (b *Block) WithSeal(header *Header) *Block { // provided body. func (b *Block) WithBody(body Body) *Block { block := &Block{ - header: b.header, - transactions: slices.Clone(body.Transactions), - uncles: make([]*Header, len(body.Uncles)), - withdrawals: slices.Clone(body.Withdrawals), - witness: b.witness, + header: b.header, + transactions: slices.Clone(body.Transactions), + uncles: make([]*Header, len(body.Uncles)), + withdrawals: slices.Clone(body.Withdrawals), + witness: b.witness, + inclusionListTxs: b.inclusionListTxs, } for i := range body.Uncles { block.uncles[i] = CopyHeader(body.Uncles[i]) @@ -516,11 +526,23 @@ func (b *Block) WithBody(body Body) *Block { func (b *Block) WithWitness(witness *ExecutionWitness) *Block { return &Block{ - header: b.header, - transactions: b.transactions, - uncles: b.uncles, - withdrawals: b.withdrawals, - witness: witness, + header: b.header, + transactions: b.transactions, + uncles: b.uncles, + withdrawals: b.withdrawals, + witness: witness, + inclusionListTxs: b.inclusionListTxs, + } +} + +func (b *Block) WithInclusionListTransactions(inclusionListTxs Transactions) *Block { + return &Block{ + header: b.header, + transactions: b.transactions, + uncles: b.uncles, + withdrawals: b.withdrawals, + witness: b.witness, + inclusionListTxs: inclusionListTxs, } } diff --git a/eth/catalyst/api.go b/eth/catalyst/api.go index f097f77faf..6f56f48130 100644 --- a/eth/catalyst/api.go +++ b/eth/catalyst/api.go @@ -20,6 +20,7 @@ package catalyst import ( "errors" "fmt" + "math/big" "reflect" "strconv" "sync" @@ -31,6 +32,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/eth" "github.com/ethereum/go-ethereum/eth/ethconfig" @@ -768,7 +770,8 @@ func (api *ConsensusAPI) newPayload(params engine.ExecutableData, versionedHashe defer api.newPayloadLock.Unlock() log.Trace("Engine API request received", "method", "NewPayload", "number", params.Number, "hash", params.BlockHash) - block, err := engine.ExecutableDataToBlock(params, versionedHashes, beaconRoot, requests) + inclusionListTxs := api.getValidInclusionListTransactions(inclusionList) + block, err := engine.ExecutableDataToBlock(params, versionedHashes, beaconRoot, requests, inclusionListTxs) if err != nil { bgu := "nil" if params.BlobGasUsed != nil { @@ -841,7 +844,7 @@ func (api *ConsensusAPI) newPayload(params engine.ExecutableData, versionedHashe return engine.PayloadStatusV1{Status: engine.ACCEPTED}, nil } log.Trace("Inserting block without sethead", "hash", block.Hash(), "number", block.Number()) - proofs, err := api.eth.BlockChain().InsertBlockWithoutSetHead(block, witness) + proofs, inclusionListSatisfied, err := api.eth.BlockChain().InsertBlockWithoutSetHead(block, witness) if err != nil { log.Warn("NewPayload: inserting block failed", "error", err) @@ -852,6 +855,13 @@ func (api *ConsensusAPI) newPayload(params engine.ExecutableData, versionedHashe return api.invalid(err, parent.Header()), nil } + if inclusionListSatisfied != nil && !*inclusionListSatisfied { + return engine.PayloadStatusV1{ + Status: engine.INCLUSION_LIST_UNSATISFIED, + LatestValidHash: nil, + ValidationError: nil, + }, nil + } hash := block.Hash() // If witness collection was requested, inject that into the result too @@ -1110,6 +1120,34 @@ func (api *ConsensusAPI) GetPayloadBodiesByRangeV2(start, count hexutil.Uint64) return api.getBodiesByRange(start, count) } +func (api *ConsensusAPI) getValidInclusionListTransactions(inclusionList types.InclusionList) []*types.Transaction { + txs := types.InclusionListToTransactions(inclusionList) + + head := api.eth.BlockChain().CurrentBlock() + signer := types.MakeSigner(api.config(), head.Number, head.Time) + + validTxs := make([]*types.Transaction, 0, len(txs)) + for _, tx := range txs { + opts := &txpool.ValidationOptions{ + Config: api.config(), + Accept: 0 | + 1<