diff --git a/CHANGELOG.md b/CHANGELOG.md index c3974c934b6..1ee6a8d76a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - Fast Sync ### Additions and Improvements - Add TLS/mTLS options and configure the GraphQL HTTP service[#7910](https://github.com/hyperledger/besu/pull/7910) +- Allow plugins to propose transactions during block creation [#8268](https://github.com/hyperledger/besu/pull/8268) ### Bug fixes - Upgrade Netty to version 4.1.118 to fix CVE-2025-24970 [#8275](https://github.com/hyperledger/besu/pull/8275) - Add missing RPC method `debug_accountRange` to `RpcMethod.java` and implemented its handler. [#8153](https://github.com/hyperledger/besu/issues/8153) diff --git a/besu/src/main/java/org/hyperledger/besu/services/TransactionSelectionServiceImpl.java b/besu/src/main/java/org/hyperledger/besu/services/TransactionSelectionServiceImpl.java index 1ea8db7eab7..58859966561 100644 --- a/besu/src/main/java/org/hyperledger/besu/services/TransactionSelectionServiceImpl.java +++ b/besu/src/main/java/org/hyperledger/besu/services/TransactionSelectionServiceImpl.java @@ -14,11 +14,14 @@ */ package org.hyperledger.besu.services; +import static com.google.common.base.Preconditions.checkState; + +import org.hyperledger.besu.plugin.data.ProcessableBlockHeader; import org.hyperledger.besu.plugin.services.TransactionSelectionService; +import org.hyperledger.besu.plugin.services.txselection.BlockTransactionSelectionService; import org.hyperledger.besu.plugin.services.txselection.PluginTransactionSelector; import org.hyperledger.besu.plugin.services.txselection.PluginTransactionSelectorFactory; - -import java.util.Optional; +import org.hyperledger.besu.plugin.services.txselection.SelectorsStateManager; /** The Transaction Selection service implementation. */ public class TransactionSelectionServiceImpl implements TransactionSelectionService { @@ -26,18 +29,27 @@ public class TransactionSelectionServiceImpl implements TransactionSelectionServ /** Default Constructor. */ public TransactionSelectionServiceImpl() {} - private Optional factory = Optional.empty(); + private PluginTransactionSelectorFactory factory = PluginTransactionSelectorFactory.NO_OP_FACTORY; + + @Override + public PluginTransactionSelector createPluginTransactionSelector( + final SelectorsStateManager selectorsStateManager) { + return factory.create(selectorsStateManager); + } @Override - public PluginTransactionSelector createPluginTransactionSelector() { - return factory - .map(PluginTransactionSelectorFactory::create) - .orElse(PluginTransactionSelector.ACCEPT_ALL); + public void selectPendingTransactions( + final BlockTransactionSelectionService selectionService, + final ProcessableBlockHeader pendingBlockHeader) { + factory.selectPendingTransactions(selectionService, pendingBlockHeader); } @Override public void registerPluginTransactionSelectorFactory( final PluginTransactionSelectorFactory pluginTransactionSelectorFactory) { - factory = Optional.ofNullable(pluginTransactionSelectorFactory); + checkState( + factory == PluginTransactionSelectorFactory.NO_OP_FACTORY, + "PluginTransactionSelectorFactory was already registered"); + factory = pluginTransactionSelectorFactory; } } diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockCreator.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockCreator.java index 30a81f28ac3..9837f2f7c24 100644 --- a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockCreator.java +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockCreator.java @@ -55,8 +55,8 @@ import org.hyperledger.besu.evm.worldstate.WorldUpdater; import org.hyperledger.besu.plugin.services.exception.StorageException; import org.hyperledger.besu.plugin.services.securitymodule.SecurityModuleException; -import org.hyperledger.besu.plugin.services.tracer.BlockAwareOperationTracer; import org.hyperledger.besu.plugin.services.txselection.PluginTransactionSelector; +import org.hyperledger.besu.plugin.services.txselection.SelectorsStateManager; import java.util.List; import java.util.Optional; @@ -213,11 +213,15 @@ protected BlockCreationResult createBlock( throwIfStopped(); - final PluginTransactionSelector pluginTransactionSelector = - miningConfiguration.getTransactionSelectionService().createPluginTransactionSelector(); - - final BlockAwareOperationTracer operationTracer = - pluginTransactionSelector.getOperationTracer(); + final var selectorsStateManager = new SelectorsStateManager(); + final var pluginTransactionSelector = + miningConfiguration + .getTransactionSelectionService() + .createPluginTransactionSelector(selectorsStateManager); + final var operationTracer = pluginTransactionSelector.getOperationTracer(); + pluginTransactionSelector + .getOperationTracer() + .traceStartBlock(processableBlockHeader, miningBeneficiary); operationTracer.traceStartBlock(processableBlockHeader, miningBeneficiary); BlockProcessingContext blockProcessingContext = @@ -240,6 +244,7 @@ protected BlockCreationResult createBlock( miningBeneficiary, newProtocolSpec, pluginTransactionSelector, + selectorsStateManager, parentHeader); transactionResults.logSelectionStats(); timings.register("txsSelection"); @@ -362,6 +367,7 @@ private TransactionSelectionResults selectTransactions( final Address miningBeneficiary, final ProtocolSpec protocolSpec, final PluginTransactionSelector pluginTransactionSelector, + final SelectorsStateManager selectorsStateManager, final BlockHeader parentHeader) throws RuntimeException { final MainnetTransactionProcessor transactionProcessor = protocolSpec.getTransactionProcessor(); @@ -391,7 +397,8 @@ private TransactionSelectionResults selectTransactions( protocolSpec.getGasLimitCalculator(), protocolSpec.getBlockHashProcessor(), pluginTransactionSelector, - ethScheduler); + ethScheduler, + selectorsStateManager); if (transactions.isPresent()) { return selector.evaluateTransactions(transactions.get()); diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/BlockTransactionSelector.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/BlockTransactionSelector.java index 5873acb8b32..86259dad2ce 100644 --- a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/BlockTransactionSelector.java +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/BlockTransactionSelector.java @@ -50,10 +50,14 @@ import org.hyperledger.besu.evm.gascalculator.GasCalculator; import org.hyperledger.besu.evm.worldstate.WorldUpdater; import org.hyperledger.besu.plugin.data.TransactionSelectionResult; +import org.hyperledger.besu.plugin.services.TransactionSelectionService; import org.hyperledger.besu.plugin.services.tracer.BlockAwareOperationTracer; +import org.hyperledger.besu.plugin.services.txselection.BlockTransactionSelectionService; import org.hyperledger.besu.plugin.services.txselection.PluginTransactionSelector; +import org.hyperledger.besu.plugin.services.txselection.SelectorsStateManager; import java.time.Duration; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; @@ -88,7 +92,7 @@ * Once "used" this class must be discarded and another created. This class contains state which is * not cleared between executions of buildTransactionListForBlock(). */ -public class BlockTransactionSelector { +public class BlockTransactionSelector implements BlockTransactionSelectionService { private static final Logger LOG = LoggerFactory.getLogger(BlockTransactionSelector.class); private final Supplier isCancelled; private final MainnetTransactionProcessor transactionProcessor; @@ -99,13 +103,17 @@ public class BlockTransactionSelector { private final TransactionSelectionResults transactionSelectionResults = new TransactionSelectionResults(); private final List transactionSelectors; + private final SelectorsStateManager selectorsStateManager; + private final TransactionSelectionService transactionSelectionService; private final PluginTransactionSelector pluginTransactionSelector; private final BlockAwareOperationTracer operationTracer; private final EthScheduler ethScheduler; private final AtomicBoolean isTimeout = new AtomicBoolean(false); private final long blockTxsSelectionMaxTime; private WorldUpdater blockWorldStateUpdater; + private WorldUpdater txWorldStateUpdater; private volatile TransactionEvaluationContext currTxEvaluationContext; + private final List selectedTxPendingActions = new ArrayList<>(1); public BlockTransactionSelector( final MiningConfiguration miningConfiguration, @@ -123,7 +131,8 @@ public BlockTransactionSelector( final GasLimitCalculator gasLimitCalculator, final BlockHashProcessor blockHashProcessor, final PluginTransactionSelector pluginTransactionSelector, - final EthScheduler ethScheduler) { + final EthScheduler ethScheduler, + final SelectorsStateManager selectorsStateManager) { this.transactionProcessor = transactionProcessor; this.blockchain = blockchain; this.worldState = worldState; @@ -141,20 +150,24 @@ public BlockTransactionSelector( blobGasPrice, miningBeneficiary, transactionPool); - transactionSelectors = createTransactionSelectors(blockSelectionContext); + this.selectorsStateManager = selectorsStateManager; + this.transactionSelectionService = miningConfiguration.getTransactionSelectionService(); + this.transactionSelectors = + createTransactionSelectors(blockSelectionContext, selectorsStateManager); this.pluginTransactionSelector = pluginTransactionSelector; this.operationTracer = new InterruptibleOperationTracer(pluginTransactionSelector.getOperationTracer()); blockWorldStateUpdater = worldState.updater(); + txWorldStateUpdater = blockWorldStateUpdater.updater(); blockTxsSelectionMaxTime = miningConfiguration.getBlockTxsSelectionMaxTime(); } private List createTransactionSelectors( - final BlockSelectionContext context) { + final BlockSelectionContext context, final SelectorsStateManager selectorsStateManager) { return List.of( new SkipSenderTransactionSelector(context), - new BlockSizeTransactionSelector(context), - new BlobSizeTransactionSelector(context), + new BlockSizeTransactionSelector(context, selectorsStateManager), + new BlobSizeTransactionSelector(context, selectorsStateManager), new PriceTransactionSelector(context), new BlobPriceTransactionSelector(context), new MinPriorityFeePerGasTransactionSelector(context), @@ -171,10 +184,6 @@ private List createTransactionSelectors( * evaluation. */ public TransactionSelectionResults buildTransactionListForBlock() { - LOG.atDebug() - .setMessage("Transaction pool stats {}") - .addArgument(blockSelectionContext.transactionPool()::logStats) - .log(); timeLimitedSelection(); LOG.atTrace() .setMessage("Transaction selection result {}") @@ -186,10 +195,19 @@ public TransactionSelectionResults buildTransactionListForBlock() { private void timeLimitedSelection() { final var txSelectionTask = new FutureTask( - () -> - blockSelectionContext - .transactionPool() - .selectTransactions(this::evaluateTransaction), + () -> { + selectorsStateManager.blockSelectionStarted(); + + LOG.debug("Starting plugin transaction selection"); + transactionSelectionService.selectPendingTransactions( + this, blockSelectionContext.pendingBlockHeader()); + + LOG.atDebug() + .setMessage("Starting internal pool transaction selection, stats {}") + .addArgument(blockSelectionContext.transactionPool()::logStats) + .log(); + blockSelectionContext.transactionPool().selectTransactions(this::evaluateTransaction); + }, null); ethScheduler.scheduleBlockCreationTask(txSelectionTask); @@ -208,11 +226,18 @@ private void timeLimitedSelection() { cancelEvaluatingTxWithGraceTime(txSelectionTask); - LOG.warn( - "Interrupting the selection of transactions for block inclusion as it exceeds the maximum configured duration of " - + blockTxsSelectionMaxTime - + "ms", - e); + final var logBuilder = + LOG.atWarn() + .setMessage( + "Interrupting the selection of transactions for block inclusion as it exceeds" + + " the maximum configured duration of {}ms") + .addArgument(blockTxsSelectionMaxTime); + + if (LOG.isTraceEnabled()) { + logBuilder.setCause(e).log(); + } else { + logBuilder.log(); + } } } @@ -260,11 +285,25 @@ private void cancelEvaluatingTxWithGraceTime(final FutureTask txSelectionT * evaluations. */ public TransactionSelectionResults evaluateTransactions(final List transactions) { + selectorsStateManager.blockSelectionStarted(); + transactions.forEach( transaction -> evaluateTransaction(new PendingTransaction.Local.Priority(transaction))); return transactionSelectionResults; } + private TransactionSelectionResult evaluateTransaction( + final PendingTransaction pendingTransaction) { + final var evaluationResult = evaluatePendingTransaction(pendingTransaction); + + if (evaluationResult.selected()) { + return commit() ? evaluationResult : BLOCK_SELECTION_TIMEOUT; + } else { + rollback(); + return evaluationResult; + } + } + /** * Passed into the PendingTransactions, and is called on each transaction until sufficient * transactions are found which fill a block worth of gas. This function will continue to be @@ -275,10 +314,14 @@ public TransactionSelectionResults evaluateTransactions(final List * @return The result of the transaction evaluation process. * @throws CancellationException if the transaction selection process is cancelled. */ - private TransactionSelectionResult evaluateTransaction( - final PendingTransaction pendingTransaction) { + @Override + public TransactionSelectionResult evaluatePendingTransaction( + final org.hyperledger.besu.datatypes.PendingTransaction pendingTransaction) { + checkCancellation(); + LOG.atTrace().setMessage("Starting evaluation of {}").addArgument(pendingTransaction).log(); + final TransactionEvaluationContext evaluationContext = createTransactionEvaluationContext(pendingTransaction); currTxEvaluationContext = evaluationContext; @@ -288,27 +331,56 @@ private TransactionSelectionResult evaluateTransaction( return handleTransactionNotSelected(evaluationContext, selectionResult); } - final WorldUpdater txWorldStateUpdater = blockWorldStateUpdater.updater(); final TransactionProcessingResult processingResult = - processTransaction(evaluationContext.getTransaction(), txWorldStateUpdater); + processTransaction(evaluationContext.getTransaction()); + + txWorldStateUpdater.markTransactionBoundary(); var postProcessingSelectionResult = evaluatePostProcessing(evaluationContext, processingResult); - if (postProcessingSelectionResult.selected()) { - return handleTransactionSelected(evaluationContext, processingResult, txWorldStateUpdater); + return postProcessingSelectionResult.selected() + ? handleTransactionSelected(evaluationContext, processingResult) + : handleTransactionNotSelected(evaluationContext, postProcessingSelectionResult); + } + + @Override + public boolean commit() { + // only add this tx to the selected set if it is not too late, + // this need to be done synchronously to avoid that a concurrent timeout + // could start packing a block while we are updating the state here + synchronized (isTimeout) { + if (!isTimeout.get()) { + selectorsStateManager.commit(); + txWorldStateUpdater.commit(); + blockWorldStateUpdater.commit(); + for (final var pendingAction : selectedTxPendingActions) { + pendingAction.run(); + } + selectedTxPendingActions.clear(); + return true; + } } - return handleTransactionNotSelected( - evaluationContext, postProcessingSelectionResult, txWorldStateUpdater); + selectedTxPendingActions.clear(); + blockWorldStateUpdater = worldState.updater(); + txWorldStateUpdater = blockWorldStateUpdater.updater(); + return false; + } + + @Override + public void rollback() { + selectedTxPendingActions.clear(); + selectorsStateManager.rollback(); + txWorldStateUpdater = blockWorldStateUpdater.updater(); } private TransactionEvaluationContext createTransactionEvaluationContext( - final PendingTransaction pendingTransaction) { + final org.hyperledger.besu.datatypes.PendingTransaction pendingTransaction) { final Wei transactionGasPriceInBlock = blockSelectionContext .feeMarket() .getTransactionPriceCalculator() .price( - pendingTransaction.getTransaction(), + (Transaction) pendingTransaction.getTransaction(), blockSelectionContext.pendingBlockHeader().getBaseFee()); return new TransactionEvaluationContext( @@ -333,7 +405,7 @@ private TransactionSelectionResult evaluatePreProcessing( for (var selector : transactionSelectors) { TransactionSelectionResult result = - selector.evaluateTransactionPreProcessing(evaluationContext, transactionSelectionResults); + selector.evaluateTransactionPreProcessing(evaluationContext); if (!result.equals(SELECTED)) { return result; } @@ -357,8 +429,7 @@ private TransactionSelectionResult evaluatePostProcessing( for (var selector : transactionSelectors) { TransactionSelectionResult result = - selector.evaluateTransactionPostProcessing( - evaluationContext, transactionSelectionResults, processingResult); + selector.evaluateTransactionPostProcessing(evaluationContext, processingResult); if (!result.equals(SELECTED)) { return result; } @@ -371,17 +442,15 @@ private TransactionSelectionResult evaluatePostProcessing( * Processes a transaction * * @param transaction The transaction to be processed. - * @param worldStateUpdater The world state updater. * @return The result of the transaction processing. */ - private TransactionProcessingResult processTransaction( - final Transaction transaction, final WorldUpdater worldStateUpdater) { + private TransactionProcessingResult processTransaction(final Transaction transaction) { final BlockHashLookup blockHashLookup = blockSelectionContext .blockHashProcessor() .createBlockHashLookup(blockchain, blockSelectionContext.pendingBlockHeader()); return transactionProcessor.processTransaction( - worldStateUpdater, + txWorldStateUpdater, blockSelectionContext.pendingBlockHeader(), transaction, blockSelectionContext.miningBeneficiary(), @@ -399,56 +468,48 @@ private TransactionProcessingResult processTransaction( * * @param evaluationContext The current selection session data. * @param processingResult The result of the transaction processing. - * @param txWorldStateUpdater The world state updater. * @return The result of the transaction selection process. */ private TransactionSelectionResult handleTransactionSelected( final TransactionEvaluationContext evaluationContext, - final TransactionProcessingResult processingResult, - final WorldUpdater txWorldStateUpdater) { + final TransactionProcessingResult processingResult) { final Transaction transaction = evaluationContext.getTransaction(); final long gasUsedByTransaction = transaction.getGasLimit() - processingResult.getGasRemaining(); - final long cumulativeGasUsed = - transactionSelectionResults.getCumulativeGasUsed() + gasUsedByTransaction; - final long blobGasUsed = - blockSelectionContext.gasCalculator().blobGasCost(transaction.getBlobCount()); - final boolean tooLate; + // queue the creation of the receipt and the update of the final results + // there actions will be performed on commit if the pending tx is definitely selected + selectedTxPendingActions.add( + () -> { + final long cumulativeGasUsed = + transactionSelectionResults.getCumulativeGasUsed() + gasUsedByTransaction; - // only add this tx to the selected set if it is not too late, - // this need to be done synchronously to avoid that a concurrent timeout - // could start packing a block while we are updating the state here - synchronized (isTimeout) { - tooLate = isTimeout.get(); - if (!tooLate) { - txWorldStateUpdater.commit(); - blockWorldStateUpdater.commit(); - final TransactionReceipt receipt = - transactionReceiptFactory.create( - transaction.getType(), processingResult, worldState, cumulativeGasUsed); + final TransactionReceipt receipt = + transactionReceiptFactory.create( + transaction.getType(), processingResult, worldState, cumulativeGasUsed); - transactionSelectionResults.updateSelected( - transaction, receipt, gasUsedByTransaction, blobGasUsed); - } - } + transactionSelectionResults.updateSelected(transaction, receipt, gasUsedByTransaction); + + notifySelected(evaluationContext, processingResult); + LOG.atTrace() + .setMessage("Selected and commited {} for block creation") + .addArgument(transaction::toTraceLog) + .log(); + }); - if (tooLate) { + if (isTimeout.get()) { // even if this tx passed all the checks, it is too late to include it in this block, // so we need to treat it as not selected // do not rely on the presence of this result, since by the time it is added, the code // reading it could have been already executed by another thread - return handleTransactionNotSelected( - evaluationContext, BLOCK_SELECTION_TIMEOUT, txWorldStateUpdater); + return handleTransactionNotSelected(evaluationContext, BLOCK_SELECTION_TIMEOUT); } - notifySelected(evaluationContext, processingResult); - - blockWorldStateUpdater = worldState.updater(); LOG.atTrace() - .setMessage("Selected {} for block creation, evaluated in {}") + .setMessage( + "Potentially selected {} for block creation, evaluated in {}, waiting for commit") .addArgument(transaction::toTraceLog) .addArgument(evaluationContext.getEvaluationTimer()) .log(); @@ -479,12 +540,16 @@ private TransactionSelectionResult handleTransactionNotSelected( transactionSelectionResults.updateNotSelected(evaluationContext.getTransaction(), actualResult); notifyNotSelected(evaluationContext, actualResult); + LOG.atTrace() - .setMessage( - "Not selected {} for block creation with result {} (original result {}), evaluated in {}") + .setMessage("Not selected {} for block creation with result {}{}, evaluated in {}") .addArgument(pendingTransaction::toTraceLog) .addArgument(actualResult) - .addArgument(selectionResult) + .addArgument( + () -> + selectionResult.equals(actualResult) + ? "" + : " (original result " + selectionResult + ")") .addArgument(evaluationContext.getEvaluationTimer()) .log(); @@ -545,14 +610,6 @@ private boolean transactionTookTooLong( return false; } - private TransactionSelectionResult handleTransactionNotSelected( - final TransactionEvaluationContext evaluationContext, - final TransactionSelectionResult selectionResult, - final WorldUpdater txWorldStateUpdater) { - txWorldStateUpdater.revert(); - return handleTransactionNotSelected(evaluationContext, selectionResult); - } - private void notifySelected( final TransactionEvaluationContext evaluationContext, final TransactionProcessingResult processingResult) { diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/TransactionSelectionResults.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/TransactionSelectionResults.java index 5977b2f7b3e..f9d55849f33 100644 --- a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/TransactionSelectionResults.java +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/TransactionSelectionResults.java @@ -48,27 +48,20 @@ public class TransactionSelectionResults { new ConcurrentHashMap<>(); private long cumulativeGasUsed = 0; - private long cumulativeBlobGasUsed = 0; void updateSelected( - final Transaction transaction, - final TransactionReceipt receipt, - final long gasUsed, - final long blobGasUsed) { + final Transaction transaction, final TransactionReceipt receipt, final long gasUsed) { selectedTransactions.add(transaction); transactionsByType .computeIfAbsent(transaction.getType(), type -> new ArrayList<>()) .add(transaction); receipts.add(receipt); cumulativeGasUsed += gasUsed; - cumulativeBlobGasUsed += blobGasUsed; LOG.atTrace() - .setMessage( - "New selected transaction {}, total transactions {}, cumulative gas used {}, cumulative blob gas used {}") + .setMessage("New selected transaction {}, total transactions {}, cumulative gas used {}") .addArgument(transaction::toTraceLog) .addArgument(selectedTransactions::size) .addArgument(cumulativeGasUsed) - .addArgument(cumulativeBlobGasUsed) .log(); } @@ -93,10 +86,6 @@ public long getCumulativeGasUsed() { return cumulativeGasUsed; } - public long getCumulativeBlobGasUsed() { - return cumulativeBlobGasUsed; - } - public Map getNotSelectedTransactions() { return Map.copyOf(notSelectedTransactions); } @@ -135,7 +124,6 @@ public boolean equals(final Object o) { } TransactionSelectionResults that = (TransactionSelectionResults) o; return cumulativeGasUsed == that.cumulativeGasUsed - && cumulativeBlobGasUsed == that.cumulativeBlobGasUsed && selectedTransactions.equals(that.selectedTransactions) && notSelectedTransactions.equals(that.notSelectedTransactions) && receipts.equals(that.receipts); @@ -143,19 +131,12 @@ public boolean equals(final Object o) { @Override public int hashCode() { - return Objects.hash( - selectedTransactions, - notSelectedTransactions, - receipts, - cumulativeGasUsed, - cumulativeBlobGasUsed); + return Objects.hash(selectedTransactions, notSelectedTransactions, receipts, cumulativeGasUsed); } public String toTraceLog() { return "cumulativeGasUsed=" + cumulativeGasUsed - + ", cumulativeBlobGasUsed=" - + cumulativeBlobGasUsed + ", selectedTransactions=" + selectedTransactions.stream() .map(Transaction::toTraceLog) diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/AbstractStatefulTransactionSelector.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/AbstractStatefulTransactionSelector.java new file mode 100644 index 00000000000..00d143dec0e --- /dev/null +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/AbstractStatefulTransactionSelector.java @@ -0,0 +1,57 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.blockcreation.txselection.selectors; + +import org.hyperledger.besu.ethereum.blockcreation.txselection.BlockSelectionContext; +import org.hyperledger.besu.plugin.services.txselection.SelectorsStateManager; + +/** + * This class represents an abstract transaction selector which provides methods to access and set + * the selector working state. + * + * @param The type of state specified by the implementing selector, not to be confused with the + * world state. + */ +public abstract class AbstractStatefulTransactionSelector extends AbstractTransactionSelector { + private final SelectorsStateManager selectorsStateManager; + + public AbstractStatefulTransactionSelector( + final BlockSelectionContext context, + final SelectorsStateManager selectorsStateManager, + final S initialState, + final SelectorsStateManager.StateDuplicator stateDuplicator) { + super(context); + this.selectorsStateManager = selectorsStateManager; + selectorsStateManager.createSelectorState(this, initialState, stateDuplicator); + } + + /** + * Get the working state specific to this selector. + * + * @return the working state of the selector + */ + protected S getWorkingState() { + return selectorsStateManager.getSelectorWorkingState(this); + } + + /** + * Set the working state specific to this selector. + * + * @param newState the new state of the selector + */ + protected void setWorkingState(final S newState) { + selectorsStateManager.setSelectorWorkingState(this, newState); + } +} diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/AbstractTransactionSelector.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/AbstractTransactionSelector.java index 120a9c4f845..50ca099f864 100644 --- a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/AbstractTransactionSelector.java +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/AbstractTransactionSelector.java @@ -16,15 +16,15 @@ import org.hyperledger.besu.ethereum.blockcreation.txselection.BlockSelectionContext; import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionEvaluationContext; -import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionSelectionResults; import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; import org.hyperledger.besu.plugin.data.TransactionSelectionResult; +import org.hyperledger.besu.plugin.services.txselection.TransactionSelector; /** * This class represents an abstract transaction selector which provides methods to evaluate * transactions. */ -public abstract class AbstractTransactionSelector { +public abstract class AbstractTransactionSelector implements TransactionSelector { protected final BlockSelectionContext context; public AbstractTransactionSelector(final BlockSelectionContext context) { @@ -35,25 +35,21 @@ public AbstractTransactionSelector(final BlockSelectionContext context) { * Evaluates a transaction in the context of other transactions in the same block. * * @param evaluationContext The current selection session data. - * @param blockTransactionResults The results of other transaction evaluations in the same block. * @return The result of the transaction evaluation */ public abstract TransactionSelectionResult evaluateTransactionPreProcessing( - final TransactionEvaluationContext evaluationContext, - final TransactionSelectionResults blockTransactionResults); + final TransactionEvaluationContext evaluationContext); /** * Evaluates a transaction considering other transactions in the same block and a transaction * processing result. * * @param evaluationContext The current selection session data. - * @param blockTransactionResults The results of other transaction evaluations in the same block. * @param processingResult The result of transaction processing. * @return The result of the transaction evaluation */ public abstract TransactionSelectionResult evaluateTransactionPostProcessing( final TransactionEvaluationContext evaluationContext, - final TransactionSelectionResults blockTransactionResults, final TransactionProcessingResult processingResult); /** diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlobPriceTransactionSelector.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlobPriceTransactionSelector.java index 57d4e7dcafa..31433b1ab69 100644 --- a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlobPriceTransactionSelector.java +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlobPriceTransactionSelector.java @@ -16,7 +16,6 @@ import org.hyperledger.besu.ethereum.blockcreation.txselection.BlockSelectionContext; import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionEvaluationContext; -import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionSelectionResults; import org.hyperledger.besu.ethereum.core.Transaction; import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; import org.hyperledger.besu.plugin.data.TransactionSelectionResult; @@ -40,13 +39,11 @@ public BlobPriceTransactionSelector(final BlockSelectionContext context) { * Evaluates a transaction considering its blob price. * * @param evaluationContext The current selection session data. - * @param ignored The results of other transaction evaluations in the same block. * @return The result of the transaction selection. */ @Override public TransactionSelectionResult evaluateTransactionPreProcessing( - final TransactionEvaluationContext evaluationContext, - final TransactionSelectionResults ignored) { + final TransactionEvaluationContext evaluationContext) { if (transactionBlobPriceBelowMin(evaluationContext.getTransaction())) { return TransactionSelectionResult.BLOB_PRICE_BELOW_CURRENT_MIN; } @@ -56,7 +53,6 @@ public TransactionSelectionResult evaluateTransactionPreProcessing( @Override public TransactionSelectionResult evaluateTransactionPostProcessing( final TransactionEvaluationContext evaluationContext, - final TransactionSelectionResults blockTransactionResults, final TransactionProcessingResult processingResult) { // All necessary checks were done in the pre-processing method, so nothing to do here. return TransactionSelectionResult.SELECTED; diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlobSizeTransactionSelector.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlobSizeTransactionSelector.java index d5fd4212ec4..9a8dc44bee1 100644 --- a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlobSizeTransactionSelector.java +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlobSizeTransactionSelector.java @@ -16,9 +16,9 @@ import org.hyperledger.besu.ethereum.blockcreation.txselection.BlockSelectionContext; import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionEvaluationContext; -import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionSelectionResults; import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; import org.hyperledger.besu.plugin.data.TransactionSelectionResult; +import org.hyperledger.besu.plugin.services.txselection.SelectorsStateManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,11 +28,12 @@ * evaluating transactions based on blobs size. It checks if a transaction supports blobs, and if * so, checks that there is enough remaining blob gas in the block to fit the blobs of the tx. */ -public class BlobSizeTransactionSelector extends AbstractTransactionSelector { +public class BlobSizeTransactionSelector extends AbstractStatefulTransactionSelector { private static final Logger LOG = LoggerFactory.getLogger(BlobSizeTransactionSelector.class); - public BlobSizeTransactionSelector(final BlockSelectionContext context) { - super(context); + public BlobSizeTransactionSelector( + final BlockSelectionContext context, final SelectorsStateManager selectorsStateManager) { + super(context, selectorsStateManager, 0L, SelectorsStateManager.StateDuplicator::duplicateLong); } /** @@ -43,21 +44,19 @@ public BlobSizeTransactionSelector(final BlockSelectionContext context) { * number of blobs or not. * * @param evaluationContext The current selection session data. - * @param transactionSelectionResults The results of other transaction evaluations in the same - * block. * @return The result of the transaction selection. */ @Override public TransactionSelectionResult evaluateTransactionPreProcessing( - final TransactionEvaluationContext evaluationContext, - final TransactionSelectionResults transactionSelectionResults) { + final TransactionEvaluationContext evaluationContext) { final var tx = evaluationContext.getTransaction(); if (tx.getType().supportsBlob()) { + final var cumulativeBlobGasUsed = getWorkingState(); + final var remainingBlobGas = - context.gasLimitCalculator().currentBlobGasLimit() - - transactionSelectionResults.getCumulativeBlobGasUsed(); + context.gasLimitCalculator().currentBlobGasLimit() - cumulativeBlobGasUsed; if (remainingBlobGas == 0) { LOG.atTrace() @@ -88,9 +87,11 @@ public TransactionSelectionResult evaluateTransactionPreProcessing( @Override public TransactionSelectionResult evaluateTransactionPostProcessing( final TransactionEvaluationContext evaluationContext, - final TransactionSelectionResults blockTransactionResults, final TransactionProcessingResult processingResult) { - // All necessary checks were done in the pre-processing method, so nothing to do here. + final var tx = evaluationContext.getTransaction(); + if (tx.getType().supportsBlob()) { + setWorkingState(getWorkingState() + context.gasCalculator().blobGasCost(tx.getBlobCount())); + } return TransactionSelectionResult.SELECTED; } } diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockSizeTransactionSelector.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockSizeTransactionSelector.java index 0da2b1386d8..e57af5eb67e 100644 --- a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockSizeTransactionSelector.java +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockSizeTransactionSelector.java @@ -16,10 +16,10 @@ import org.hyperledger.besu.ethereum.blockcreation.txselection.BlockSelectionContext; import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionEvaluationContext; -import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionSelectionResults; import org.hyperledger.besu.ethereum.core.Transaction; import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; import org.hyperledger.besu.plugin.data.TransactionSelectionResult; +import org.hyperledger.besu.plugin.services.txselection.SelectorsStateManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,11 +29,15 @@ * evaluating transactions based on block size. It checks if a transaction is too large for the * block and determines the selection result accordingly. */ -public class BlockSizeTransactionSelector extends AbstractTransactionSelector { +public class BlockSizeTransactionSelector extends AbstractStatefulTransactionSelector { private static final Logger LOG = LoggerFactory.getLogger(BlockSizeTransactionSelector.class); - public BlockSizeTransactionSelector(final BlockSelectionContext context) { - super(context); + private final long blockGasLimit; + + public BlockSizeTransactionSelector( + final BlockSelectionContext context, final SelectorsStateManager selectorsStateManager) { + super(context, selectorsStateManager, 0L, SelectorsStateManager.StateDuplicator::duplicateLong); + this.blockGasLimit = context.pendingBlockHeader().getGasLimit(); } /** @@ -41,25 +45,23 @@ public BlockSizeTransactionSelector(final BlockSelectionContext context) { * too large for the block returns a selection result based on block occupancy. * * @param evaluationContext The current selection session data. - * @param transactionSelectionResults The results of other transaction evaluations in the same - * block. * @return The result of the transaction selection. */ @Override public TransactionSelectionResult evaluateTransactionPreProcessing( - final TransactionEvaluationContext evaluationContext, - final TransactionSelectionResults transactionSelectionResults) { + final TransactionEvaluationContext evaluationContext) { + + final long cumulativeGasUsed = getWorkingState(); - if (transactionTooLargeForBlock( - evaluationContext.getTransaction(), transactionSelectionResults)) { + if (transactionTooLargeForBlock(evaluationContext.getTransaction(), cumulativeGasUsed)) { LOG.atTrace() .setMessage("Transaction {} too large to select for block creation") .addArgument(evaluationContext.getPendingTransaction()::toTraceLog) .log(); - if (blockOccupancyAboveThreshold(transactionSelectionResults)) { + if (blockOccupancyAboveThreshold(cumulativeGasUsed)) { LOG.trace("Block occupancy above threshold, completing operation"); return TransactionSelectionResult.BLOCK_OCCUPANCY_ABOVE_THRESHOLD; - } else if (blockFull(transactionSelectionResults)) { + } else if (blockFull(cumulativeGasUsed)) { LOG.trace("Block full, completing operation"); return TransactionSelectionResult.BLOCK_FULL; } else { @@ -72,49 +74,42 @@ public TransactionSelectionResult evaluateTransactionPreProcessing( @Override public TransactionSelectionResult evaluateTransactionPostProcessing( final TransactionEvaluationContext evaluationContext, - final TransactionSelectionResults blockTransactionResults, final TransactionProcessingResult processingResult) { - // All necessary checks were done in the pre-processing method, so nothing to do here. + final long gasUsedByTransaction = + evaluationContext.getTransaction().getGasLimit() - processingResult.getGasRemaining(); + setWorkingState(getWorkingState() + gasUsedByTransaction); + return TransactionSelectionResult.SELECTED; } /** * Checks if the transaction is too large for the block. * - * @param transaction The transaction to be checked. - * @param transactionSelectionResults The results of other transaction evaluations in the same - * block. + * @param transaction The transaction to be checked. block. + * @param cumulativeGasUsed The cumulative gas used by previous txs. * @return True if the transaction is too large for the block, false otherwise. */ private boolean transactionTooLargeForBlock( - final Transaction transaction, - final TransactionSelectionResults transactionSelectionResults) { + final Transaction transaction, final long cumulativeGasUsed) { - return transaction.getGasLimit() - > context.pendingBlockHeader().getGasLimit() - - transactionSelectionResults.getCumulativeGasUsed(); + return transaction.getGasLimit() > blockGasLimit - cumulativeGasUsed; } /** * Checks if the block occupancy is above the threshold. * - * @param transactionSelectionResults The results of other transaction evaluations in the same - * block. + * @param cumulativeGasUsed The cumulative gas used by previous txs. * @return True if the block occupancy is above the threshold, false otherwise. */ - private boolean blockOccupancyAboveThreshold( - final TransactionSelectionResults transactionSelectionResults) { - final long gasAvailable = context.pendingBlockHeader().getGasLimit(); - - final long gasUsed = transactionSelectionResults.getCumulativeGasUsed(); - final long gasRemaining = gasAvailable - gasUsed; - final double occupancyRatio = (double) gasUsed / (double) gasAvailable; + private boolean blockOccupancyAboveThreshold(final long cumulativeGasUsed) { + final long gasRemaining = blockGasLimit - cumulativeGasUsed; + final double occupancyRatio = (double) cumulativeGasUsed / (double) blockGasLimit; LOG.trace( "Min block occupancy ratio {}, gas used {}, available {}, remaining {}, used/available {}", context.miningConfiguration().getMinBlockOccupancyRatio(), - gasUsed, - gasAvailable, + cumulativeGasUsed, + blockGasLimit, gasRemaining, occupancyRatio); @@ -124,15 +119,11 @@ private boolean blockOccupancyAboveThreshold( /** * Checks if the block is full. * - * @param transactionSelectionResults The results of other transaction evaluations in the same - * block. + * @param cumulativeGasUsed The cumulative gas used by previous txs. * @return True if the block is full, false otherwise. */ - private boolean blockFull(final TransactionSelectionResults transactionSelectionResults) { - final long gasAvailable = context.pendingBlockHeader().getGasLimit(); - final long gasUsed = transactionSelectionResults.getCumulativeGasUsed(); - - final long gasRemaining = gasAvailable - gasUsed; + private boolean blockFull(final long cumulativeGasUsed) { + final long gasRemaining = blockGasLimit - cumulativeGasUsed; if (gasRemaining < context.gasCalculator().getMinimumTransactionCost()) { LOG.trace( diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/MinPriorityFeePerGasTransactionSelector.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/MinPriorityFeePerGasTransactionSelector.java index 88f74e2d013..b3fbe31eba8 100644 --- a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/MinPriorityFeePerGasTransactionSelector.java +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/MinPriorityFeePerGasTransactionSelector.java @@ -17,7 +17,6 @@ import org.hyperledger.besu.datatypes.Wei; import org.hyperledger.besu.ethereum.blockcreation.txselection.BlockSelectionContext; import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionEvaluationContext; -import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionSelectionResults; import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; import org.hyperledger.besu.plugin.data.TransactionSelectionResult; @@ -37,15 +36,12 @@ public MinPriorityFeePerGasTransactionSelector(final BlockSelectionContext conte * Evaluates a transaction before processing. * * @param evaluationContext The current selection session data. - * @param transactionSelectionResults The results of other transaction evaluations in the same - * block. * @return TransactionSelectionResult. If the priority fee is below the minimum, it returns an * invalid transient result. Otherwise, it returns a selected result. */ @Override public TransactionSelectionResult evaluateTransactionPreProcessing( - final TransactionEvaluationContext evaluationContext, - final TransactionSelectionResults transactionSelectionResults) { + final TransactionEvaluationContext evaluationContext) { if (isPriorityFeePriceBelowMinimum(evaluationContext)) { return TransactionSelectionResult.PRIORITY_FEE_PER_GAS_BELOW_CURRENT_MIN; } @@ -82,7 +78,6 @@ private boolean isPriorityFeePriceBelowMinimum( @Override public TransactionSelectionResult evaluateTransactionPostProcessing( final TransactionEvaluationContext evaluationContext, - final TransactionSelectionResults blockTransactionResults, final TransactionProcessingResult processingResult) { return TransactionSelectionResult.SELECTED; } diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/PriceTransactionSelector.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/PriceTransactionSelector.java index 0ede0bfae6d..7f4cacbde2a 100644 --- a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/PriceTransactionSelector.java +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/PriceTransactionSelector.java @@ -16,7 +16,6 @@ import org.hyperledger.besu.ethereum.blockcreation.txselection.BlockSelectionContext; import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionEvaluationContext; -import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionSelectionResults; import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; import org.hyperledger.besu.plugin.data.TransactionSelectionResult; @@ -40,13 +39,11 @@ public PriceTransactionSelector(final BlockSelectionContext context) { * minimum, it returns a selection result indicating the reason. * * @param evaluationContext The current selection session data. - * @param ignored The results of other transaction evaluations in the same block. * @return The result of the transaction selection. */ @Override public TransactionSelectionResult evaluateTransactionPreProcessing( - final TransactionEvaluationContext evaluationContext, - final TransactionSelectionResults ignored) { + final TransactionEvaluationContext evaluationContext) { if (transactionCurrentPriceBelowMin(evaluationContext)) { return TransactionSelectionResult.CURRENT_TX_PRICE_BELOW_MIN; } @@ -56,7 +53,6 @@ public TransactionSelectionResult evaluateTransactionPreProcessing( @Override public TransactionSelectionResult evaluateTransactionPostProcessing( final TransactionEvaluationContext evaluationContext, - final TransactionSelectionResults blockTransactionResults, final TransactionProcessingResult processingResult) { // All necessary checks were done in the pre-processing method, so nothing to do here. return TransactionSelectionResult.SELECTED; diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/ProcessingResultTransactionSelector.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/ProcessingResultTransactionSelector.java index 120ad372a36..5d2d5a7b919 100644 --- a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/ProcessingResultTransactionSelector.java +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/ProcessingResultTransactionSelector.java @@ -16,7 +16,6 @@ import org.hyperledger.besu.ethereum.blockcreation.txselection.BlockSelectionContext; import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionEvaluationContext; -import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionSelectionResults; import org.hyperledger.besu.ethereum.core.Transaction; import org.hyperledger.besu.ethereum.mainnet.ValidationResult; import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; @@ -41,8 +40,7 @@ public ProcessingResultTransactionSelector(final BlockSelectionContext context) @Override public TransactionSelectionResult evaluateTransactionPreProcessing( - final TransactionEvaluationContext evaluationContext, - final TransactionSelectionResults blockTransactionResults) { + final TransactionEvaluationContext evaluationContext) { // All checks depend on processingResult and will be done in the post-processing method, so // nothing to do here. return TransactionSelectionResult.SELECTED; @@ -54,14 +52,12 @@ public TransactionSelectionResult evaluateTransactionPreProcessing( * result. * * @param evaluationContext The current selection session data. - * @param blockTransactionResults The results of other transaction evaluations in the same block. * @param processingResult The processing result of the transaction. * @return The result of the transaction selection. */ @Override public TransactionSelectionResult evaluateTransactionPostProcessing( final TransactionEvaluationContext evaluationContext, - final TransactionSelectionResults blockTransactionResults, final TransactionProcessingResult processingResult) { if (processingResult.isInvalid()) { diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/SkipSenderTransactionSelector.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/SkipSenderTransactionSelector.java index d29f045b7f6..a83627579b5 100644 --- a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/SkipSenderTransactionSelector.java +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/SkipSenderTransactionSelector.java @@ -17,34 +17,41 @@ import org.hyperledger.besu.datatypes.Address; import org.hyperledger.besu.ethereum.blockcreation.txselection.BlockSelectionContext; import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionEvaluationContext; -import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionSelectionResults; import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; import org.hyperledger.besu.plugin.data.TransactionSelectionResult; -import java.util.HashSet; -import java.util.Set; +import java.util.HashMap; +import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class SkipSenderTransactionSelector extends AbstractTransactionSelector { private static final Logger LOG = LoggerFactory.getLogger(SkipSenderTransactionSelector.class); - private final Set
skippedSenders = new HashSet<>(); + private final Map skippedSenderNonceMap = new HashMap<>(); public SkipSenderTransactionSelector(final BlockSelectionContext context) { super(context); } + /** + * Check if this pending tx belongs to a sender with a previous pending tx not selected, in which + * case we can safely skip the evaluation due to the nonce gap + * + * @param evaluationContext The current selection session data. + * @return SENDER_WITH_PREVIOUS_TX_NOT_SELECTED if there is a nonce gas for this sender + */ @Override public TransactionSelectionResult evaluateTransactionPreProcessing( - final TransactionEvaluationContext evaluationContext, - final TransactionSelectionResults ignored) { + final TransactionEvaluationContext evaluationContext) { final var sender = evaluationContext.getTransaction().getSender(); - if (skippedSenders.contains(sender)) { + final var skippedNonce = skippedSenderNonceMap.get(sender); + if (nonceGap(evaluationContext, skippedNonce)) { LOG.atTrace() - .setMessage("Not selecting tx {} since its sender {} is in the skip list") + .setMessage("Not selecting tx {} since its sender {} is in the skip list with nonce {}") .addArgument(() -> evaluationContext.getPendingTransaction().toTraceLog()) .addArgument(sender) + .addArgument(skippedNonce) .log(); return TransactionSelectionResult.SENDER_WITH_PREVIOUS_TX_NOT_SELECTED; @@ -52,10 +59,14 @@ public TransactionSelectionResult evaluateTransactionPreProcessing( return TransactionSelectionResult.SELECTED; } + private static boolean nonceGap( + final TransactionEvaluationContext evaluationContext, final Long skippedNonce) { + return skippedNonce != null && evaluationContext.getTransaction().getNonce() > skippedNonce; + } + @Override public TransactionSelectionResult evaluateTransactionPostProcessing( final TransactionEvaluationContext evaluationContext, - final TransactionSelectionResults blockTransactionResults, final TransactionProcessingResult processingResult) { // All necessary checks were done in the pre-processing method, so nothing to do here. return TransactionSelectionResult.SELECTED; @@ -66,7 +77,7 @@ public TransactionSelectionResult evaluateTransactionPostProcessing( * same sender, since it will never be selected due to the nonce gap, so we add the sender to the * skip list. * - * @param evaluationContext The current selection context + * @param evaluationContext The current evaluation context * @param transactionSelectionResult The transaction selection result */ @Override @@ -74,7 +85,27 @@ public void onTransactionNotSelected( final TransactionEvaluationContext evaluationContext, final TransactionSelectionResult transactionSelectionResult) { final var sender = evaluationContext.getTransaction().getSender(); - skippedSenders.add(sender); - LOG.trace("Sender {} added to the skip list", sender); + final var nonce = evaluationContext.getTransaction().getNonce(); + skippedSenderNonceMap.put(sender, nonce); + LOG.trace("Sender {} added to the skip list with nonce {}", sender, nonce); + } + + /** + * When a transaction is selected we can remove it from the list. This could happen when the same + * pending tx is present both in the internal pool and the plugin pool, and for example it is not + * selected by the plugin but could be later selected from the internal pool. + * + * @param evaluationContext The current evaluation context + * @param processingResult The transaction processing result + */ + @Override + public void onTransactionSelected( + final TransactionEvaluationContext evaluationContext, + final TransactionProcessingResult processingResult) { + final var sender = evaluationContext.getTransaction().getSender(); + final var skippedNonce = skippedSenderNonceMap.remove(sender); + if (skippedNonce != null) { + LOG.trace("Sender {} removed from the skip list, skipped nonce was {}", sender, skippedNonce); + } } } diff --git a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockTransactionSelectorTest.java b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockTransactionSelectorTest.java index 242694fa9c1..e42b07dce5a 100644 --- a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockTransactionSelectorTest.java +++ b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockTransactionSelectorTest.java @@ -90,6 +90,7 @@ import org.hyperledger.besu.plugin.services.TransactionSelectionService; import org.hyperledger.besu.plugin.services.txselection.PluginTransactionSelector; import org.hyperledger.besu.plugin.services.txselection.PluginTransactionSelectorFactory; +import org.hyperledger.besu.plugin.services.txselection.SelectorsStateManager; import org.hyperledger.besu.plugin.services.txselection.TransactionEvaluationContext; import org.hyperledger.besu.services.TransactionSelectionServiceImpl; import org.hyperledger.besu.services.kvstore.InMemoryKeyValueStorage; @@ -648,33 +649,41 @@ public void transactionSelectionPluginShouldWork_PreProcessing() { final Transaction notSelectedInvalid = createTransaction(2, Wei.of(10), 21_000, SENDER2); ensureTransactionIsValid(notSelectedInvalid, 21_000, 0); + final PluginTransactionSelector pluginTransactionSelector = + new PluginTransactionSelector() { + @Override + public TransactionSelectionResult evaluateTransactionPreProcessing( + final TransactionEvaluationContext evaluationContext) { + if (evaluationContext + .getPendingTransaction() + .getTransaction() + .equals(notSelectedTransient)) + return PluginTransactionSelectionResult.GENERIC_PLUGIN_INVALID_TRANSIENT; + if (evaluationContext + .getPendingTransaction() + .getTransaction() + .equals(notSelectedInvalid)) + return PluginTransactionSelectionResult.GENERIC_PLUGIN_INVALID; + return SELECTED; + } + + @Override + public TransactionSelectionResult evaluateTransactionPostProcessing( + final TransactionEvaluationContext evaluationContext, + final org.hyperledger.besu.plugin.data.TransactionProcessingResult processingResult) { + return SELECTED; + } + }; + final PluginTransactionSelectorFactory transactionSelectorFactory = - () -> - new PluginTransactionSelector() { - @Override - public TransactionSelectionResult evaluateTransactionPreProcessing( - final TransactionEvaluationContext evaluationContext) { - if (evaluationContext - .getPendingTransaction() - .getTransaction() - .equals(notSelectedTransient)) - return PluginTransactionSelectionResult.GENERIC_PLUGIN_INVALID_TRANSIENT; - if (evaluationContext - .getPendingTransaction() - .getTransaction() - .equals(notSelectedInvalid)) - return PluginTransactionSelectionResult.GENERIC_PLUGIN_INVALID; - return SELECTED; - } + new PluginTransactionSelectorFactory() { + @Override + public PluginTransactionSelector create( + final SelectorsStateManager selectorsStateManager) { + return pluginTransactionSelector; + } + }; - @Override - public TransactionSelectionResult evaluateTransactionPostProcessing( - final TransactionEvaluationContext evaluationContext, - final org.hyperledger.besu.plugin.data.TransactionProcessingResult - processingResult) { - return SELECTED; - } - }; transactionSelectionService.registerPluginTransactionSelectorFactory( transactionSelectorFactory); @@ -717,30 +726,35 @@ public void transactionSelectionPluginShouldWork_PostProcessing() { final Transaction notSelected = createTransaction(1, Wei.of(10), 30_000); ensureTransactionIsValid(notSelected, maxGasUsedByTransaction + 1, 0); - final Transaction selected3 = createTransaction(3, Wei.of(10), 21_000); - ensureTransactionIsValid(selected3, maxGasUsedByTransaction, 0); + final PluginTransactionSelector pluginTransactionSelector = + new PluginTransactionSelector() { + @Override + public TransactionSelectionResult evaluateTransactionPreProcessing( + final TransactionEvaluationContext evaluationContext) { + return SELECTED; + } + + @Override + public TransactionSelectionResult evaluateTransactionPostProcessing( + final TransactionEvaluationContext evaluationContext, + final org.hyperledger.besu.plugin.data.TransactionProcessingResult processingResult) { + // the transaction with max gas +1 should fail + if (processingResult.getEstimateGasUsedByTransaction() > maxGasUsedByTransaction) { + return PluginTransactionSelectionResult.GENERIC_PLUGIN_INVALID_TRANSIENT; + } + return SELECTED; + } + }; final PluginTransactionSelectorFactory transactionSelectorFactory = - () -> - new PluginTransactionSelector() { - @Override - public TransactionSelectionResult evaluateTransactionPreProcessing( - final TransactionEvaluationContext evaluationContext) { - return SELECTED; - } + new PluginTransactionSelectorFactory() { + @Override + public PluginTransactionSelector create( + final SelectorsStateManager selectorsStateManager) { + return pluginTransactionSelector; + } + }; - @Override - public TransactionSelectionResult evaluateTransactionPostProcessing( - final TransactionEvaluationContext evaluationContext, - final org.hyperledger.besu.plugin.data.TransactionProcessingResult - processingResult) { - // the transaction with max gas +1 should fail - if (processingResult.getEstimateGasUsedByTransaction() > maxGasUsedByTransaction) { - return PluginTransactionSelectionResult.GENERIC_PLUGIN_INVALID_TRANSIENT; - } - return SELECTED; - } - }; transactionSelectionService.registerPluginTransactionSelectorFactory( transactionSelectorFactory); @@ -776,7 +790,7 @@ public void transactionSelectionPluginShouldBeNotifiedWhenTransactionSelectionCo PluginTransactionSelector transactionSelector = mock(PluginTransactionSelector.class); when(transactionSelector.evaluateTransactionPreProcessing(any())).thenReturn(SELECTED); when(transactionSelector.evaluateTransactionPostProcessing(any(), any())).thenReturn(SELECTED); - when(transactionSelectorFactory.create()).thenReturn(transactionSelector); + when(transactionSelectorFactory.create(any())).thenReturn(transactionSelector); transactionSelectionService.registerPluginTransactionSelectorFactory( transactionSelectorFactory); @@ -1070,7 +1084,7 @@ private void internalBlockSelectionTimeoutSimulation( final PluginTransactionSelectorFactory transactionSelectorFactory = mock(PluginTransactionSelectorFactory.class); - when(transactionSelectorFactory.create()).thenReturn(transactionSelector); + when(transactionSelectorFactory.create(any())).thenReturn(transactionSelector); transactionSelectionService.registerPluginTransactionSelectorFactory( transactionSelectorFactory); @@ -1233,7 +1247,7 @@ private void internalBlockSelectionTimeoutSimulationInvalidTxs( final PluginTransactionSelectorFactory transactionSelectorFactory = mock(PluginTransactionSelectorFactory.class); - when(transactionSelectorFactory.create()).thenReturn(transactionSelector); + when(transactionSelectorFactory.create(any())).thenReturn(transactionSelector); transactionSelectionService.registerPluginTransactionSelectorFactory( transactionSelectorFactory); @@ -1320,6 +1334,7 @@ protected BlockTransactionSelector createBlockSelector( final Wei blobGasPrice, final TransactionSelectionService transactionSelectionService) { + final var selectorsStateManager = new SelectorsStateManager(); final BlockTransactionSelector selector = new BlockTransactionSelector( miningConfiguration, @@ -1336,8 +1351,9 @@ protected BlockTransactionSelector createBlockSelector( new LondonGasCalculator(), GasLimitCalculator.constant(), protocolSchedule.getByBlockHeader(blockHeader).getBlockHashProcessor(), - transactionSelectionService.createPluginTransactionSelector(), - ethScheduler); + transactionSelectionService.createPluginTransactionSelector(selectorsStateManager), + ethScheduler, + selectorsStateManager); return selector; } diff --git a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlobSizeTransactionSelectorTest.java b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlobSizeTransactionSelectorTest.java index 7d9292935e9..309343ac58b 100644 --- a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlobSizeTransactionSelectorTest.java +++ b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlobSizeTransactionSelectorTest.java @@ -16,10 +16,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; +import static org.hyperledger.besu.plugin.data.TransactionSelectionResult.BLOBS_FULL; +import static org.hyperledger.besu.plugin.data.TransactionSelectionResult.SELECTED; +import static org.hyperledger.besu.plugin.data.TransactionSelectionResult.TX_TOO_LARGE_FOR_REMAINING_BLOB_GAS; import static org.mockito.Answers.RETURNS_DEEP_STUBS; import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import org.hyperledger.besu.crypto.KeyPair; @@ -36,12 +37,13 @@ import org.hyperledger.besu.datatypes.Wei; import org.hyperledger.besu.ethereum.blockcreation.txselection.BlockSelectionContext; import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionEvaluationContext; -import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionSelectionResults; import org.hyperledger.besu.ethereum.core.Transaction; import org.hyperledger.besu.ethereum.core.TransactionTestFixture; import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; +import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; import org.hyperledger.besu.evm.gascalculator.CancunGasCalculator; import org.hyperledger.besu.plugin.data.TransactionSelectionResult; +import org.hyperledger.besu.plugin.services.txselection.SelectorsStateManager; import java.util.Optional; import java.util.stream.IntStream; @@ -50,6 +52,7 @@ import com.google.common.base.Suppliers; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes48; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -68,86 +71,103 @@ class BlobSizeTransactionSelectorTest { @Mock(answer = RETURNS_DEEP_STUBS) BlockSelectionContext blockSelectionContext; - @Mock TransactionSelectionResults selectionResults; + @Mock TransactionProcessingResult transactionProcessingResult; - @Test - void notBlobTransactionsAreSelectedWithoutAnyCheck() { - final var selector = new BlobSizeTransactionSelector(blockSelectionContext); - - final var nonBlobTx = createEIP1559PendingTransaction(); - - final var txEvaluationContext = - new TransactionEvaluationContext( - blockSelectionContext.pendingBlockHeader(), nonBlobTx, null, null, null); + SelectorsStateManager selectorsStateManager; + BlobSizeTransactionSelector selector; - final var result = - selector.evaluateTransactionPreProcessing(txEvaluationContext, selectionResults); - assertThat(result).isEqualTo(TransactionSelectionResult.SELECTED); - verifyNoInteractions(selectionResults); - } - - @Test - void firstBlobTransactionIsSelected() { + @BeforeEach + void setup() { when(blockSelectionContext.gasLimitCalculator().currentBlobGasLimit()).thenReturn(MAX_BLOB_GAS); when(blockSelectionContext.gasCalculator().blobGasCost(anyLong())) .thenAnswer(iom -> BLOB_GAS_PER_BLOB * iom.getArgument(0, Long.class)); - final var selector = new BlobSizeTransactionSelector(blockSelectionContext); + selectorsStateManager = new SelectorsStateManager(); + selector = new BlobSizeTransactionSelector(blockSelectionContext, selectorsStateManager); + } + @Test + void notBlobTransactionsAreAlwaysSelected() { + // this tx fills all the available blob space final var firstBlobTx = createBlobPendingTransaction(MAX_BLOBS); final var txEvaluationContext = new TransactionEvaluationContext( blockSelectionContext.pendingBlockHeader(), firstBlobTx, null, null, null); + selectorsStateManager.blockSelectionStarted(); + evaluateAndAssertSelected(txEvaluationContext); - when(selectionResults.getCumulativeBlobGasUsed()).thenReturn(0L); + // this non blob tx is selected regardless the blob space is already filled + final var nonBlobTx = createEIP1559PendingTransaction(); - final var result = - selector.evaluateTransactionPreProcessing(txEvaluationContext, selectionResults); - assertThat(result).isEqualTo(TransactionSelectionResult.SELECTED); - verify(selectionResults).getCumulativeBlobGasUsed(); + final var nonBlobTxEvaluationContext = + new TransactionEvaluationContext( + blockSelectionContext.pendingBlockHeader(), nonBlobTx, null, null, null); + selectorsStateManager.blockSelectionStarted(); + evaluateAndAssertSelected(nonBlobTxEvaluationContext); } @Test - void returnsBlobsFullWhenMaxNumberOfBlobsAlreadyPresent() { - when(blockSelectionContext.gasLimitCalculator().currentBlobGasLimit()).thenReturn(MAX_BLOB_GAS); - - final var selector = new BlobSizeTransactionSelector(blockSelectionContext); - - final var firstBlobTx = createBlobPendingTransaction(1); + void firstBlobTransactionIsSelected() { + final var firstBlobTx = createBlobPendingTransaction(MAX_BLOBS); final var txEvaluationContext = new TransactionEvaluationContext( blockSelectionContext.pendingBlockHeader(), firstBlobTx, null, null, null); + selectorsStateManager.blockSelectionStarted(); + evaluateAndAssertSelected(txEvaluationContext); + } + + @Test + void returnsBlobsFullWhenMaxNumberOfBlobsAlreadyPresent() { + final var blobTx1 = createBlobPendingTransaction(MAX_BLOBS); + final var txEvaluationContext1 = + new TransactionEvaluationContext( + blockSelectionContext.pendingBlockHeader(), blobTx1, null, null, null); - when(selectionResults.getCumulativeBlobGasUsed()).thenReturn(MAX_BLOB_GAS); + selectorsStateManager.blockSelectionStarted(); + evaluateAndAssertSelected(txEvaluationContext1); - final var result = - selector.evaluateTransactionPreProcessing(txEvaluationContext, selectionResults); - assertThat(result).isEqualTo(TransactionSelectionResult.BLOBS_FULL); - verify(selectionResults).getCumulativeBlobGasUsed(); + final var blobTx2 = createBlobPendingTransaction(1); + final var txEvaluationContext2 = + new TransactionEvaluationContext( + blockSelectionContext.pendingBlockHeader(), blobTx2, null, null, null); + selectorsStateManager.blockSelectionStarted(); + evaluateAndAssertNotSelected(txEvaluationContext2, BLOBS_FULL); } @Test void returnsTooLargeForRemainingBlobGas() { - when(blockSelectionContext.gasLimitCalculator().currentBlobGasLimit()).thenReturn(MAX_BLOB_GAS); - when(blockSelectionContext.gasCalculator().blobGasCost(anyLong())) - .thenAnswer(iom -> BLOB_GAS_PER_BLOB * iom.getArgument(0, Long.class)); - - final var selector = new BlobSizeTransactionSelector(blockSelectionContext); - - final var firstBlobTx = createBlobPendingTransaction(MAX_BLOBS); + // first tx only fill the space for one blob leaving space max MAX_BLOB_GAS-1 blobs to be added + // later + final var blobTx1 = createBlobPendingTransaction(1); + final var txEvaluationContext1 = + new TransactionEvaluationContext( + blockSelectionContext.pendingBlockHeader(), blobTx1, null, null, null); + selectorsStateManager.blockSelectionStarted(); + evaluateAndAssertSelected(txEvaluationContext1); - final var txEvaluationContext = + final var blobTx2 = createBlobPendingTransaction(MAX_BLOBS); + final var txEvaluationContext2 = new TransactionEvaluationContext( - blockSelectionContext.pendingBlockHeader(), firstBlobTx, null, null, null); + blockSelectionContext.pendingBlockHeader(), blobTx2, null, null, null); + selectorsStateManager.blockSelectionStarted(); + evaluateAndAssertNotSelected(txEvaluationContext2, TX_TOO_LARGE_FOR_REMAINING_BLOB_GAS); + } - when(selectionResults.getCumulativeBlobGasUsed()).thenReturn(MAX_BLOB_GAS - 1); + private void evaluateAndAssertSelected(final TransactionEvaluationContext txEvaluationContext) { + assertThat(selector.evaluateTransactionPreProcessing(txEvaluationContext)).isEqualTo(SELECTED); + assertThat( + selector.evaluateTransactionPostProcessing( + txEvaluationContext, transactionProcessingResult)) + .isEqualTo(SELECTED); + } - final var result = - selector.evaluateTransactionPreProcessing(txEvaluationContext, selectionResults); - assertThat(result).isEqualTo(TransactionSelectionResult.TX_TOO_LARGE_FOR_REMAINING_BLOB_GAS); - verify(selectionResults).getCumulativeBlobGasUsed(); + private void evaluateAndAssertNotSelected( + final TransactionEvaluationContext txEvaluationContext, + final TransactionSelectionResult preProcessedResult) { + assertThat(selector.evaluateTransactionPreProcessing(txEvaluationContext)) + .isEqualTo(preProcessedResult); } private PendingTransaction createEIP1559PendingTransaction() { diff --git a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockSizeTransactionSelectorTest.java b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockSizeTransactionSelectorTest.java new file mode 100644 index 00000000000..8463f5e0456 --- /dev/null +++ b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockSizeTransactionSelectorTest.java @@ -0,0 +1,272 @@ +/* + * Copyright contributors to Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.blockcreation.txselection.selectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hyperledger.besu.plugin.data.TransactionSelectionResult.BLOCK_FULL; +import static org.hyperledger.besu.plugin.data.TransactionSelectionResult.BLOCK_OCCUPANCY_ABOVE_THRESHOLD; +import static org.hyperledger.besu.plugin.data.TransactionSelectionResult.SELECTED; +import static org.hyperledger.besu.plugin.data.TransactionSelectionResult.TX_TOO_LARGE_FOR_REMAINING_GAS; +import static org.mockito.Answers.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.hyperledger.besu.crypto.KeyPair; +import org.hyperledger.besu.crypto.SignatureAlgorithm; +import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.TransactionType; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.blockcreation.txselection.BlockSelectionContext; +import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionEvaluationContext; +import org.hyperledger.besu.ethereum.core.MiningConfiguration; +import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.ethereum.core.TransactionTestFixture; +import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; +import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; +import org.hyperledger.besu.plugin.data.TransactionSelectionResult; +import org.hyperledger.besu.plugin.services.txselection.SelectorsStateManager; + +import java.util.Optional; +import java.util.stream.IntStream; + +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class BlockSizeTransactionSelectorTest { + private static final Supplier SIGNATURE_ALGORITHM = + Suppliers.memoize(SignatureAlgorithmFactory::getInstance); + private static final KeyPair KEYS = SIGNATURE_ALGORITHM.get().generateKeyPair(); + private static final long TRANSFER_GAS_LIMIT = 21_000L; + private static final long BLOCK_GAS_LIMIT = 1_000_000L; + + @Mock(answer = RETURNS_DEEP_STUBS) + BlockSelectionContext blockSelectionContext; + + SelectorsStateManager selectorsStateManager; + BlockSizeTransactionSelector selector; + MiningConfiguration miningConfiguration; + + @BeforeEach + void setup() { + miningConfiguration = MiningConfiguration.newDefault(); + when(blockSelectionContext.pendingBlockHeader().getGasLimit()).thenReturn(BLOCK_GAS_LIMIT); + when(blockSelectionContext.miningConfiguration()).thenReturn(miningConfiguration); + + selectorsStateManager = new SelectorsStateManager(); + selector = new BlockSizeTransactionSelector(blockSelectionContext, selectorsStateManager); + } + + @Test + void singleTransactionBelowBlockGasLimitIsSelected() { + final var tx = createPendingTransaction(TRANSFER_GAS_LIMIT); + + final var txEvaluationContext = + new TransactionEvaluationContext( + blockSelectionContext.pendingBlockHeader(), tx, null, null, null); + selectorsStateManager.blockSelectionStarted(); + evaluateAndAssertSelected(txEvaluationContext, remainingGas(0)); + + assertThat(selector.getWorkingState()).isEqualTo(TRANSFER_GAS_LIMIT); + } + + @Test + void singleTransactionAboveBlockGasLimitIsNotSelected() { + final var tx = createPendingTransaction(BLOCK_GAS_LIMIT + 1); + + final var txEvaluationContext = + new TransactionEvaluationContext( + blockSelectionContext.pendingBlockHeader(), tx, null, null, null); + selectorsStateManager.blockSelectionStarted(); + evaluateAndAssertNotSelected(txEvaluationContext, TX_TOO_LARGE_FOR_REMAINING_GAS); + + assertThat(selector.getWorkingState()).isEqualTo(0); + } + + @Test + void correctlyCumulatesOnlyTheEffectiveGasUsedAfterProcessing() { + final var tx = createPendingTransaction(TRANSFER_GAS_LIMIT * 2); + final long remainingGas = 100; + + final var txEvaluationContext = + new TransactionEvaluationContext( + blockSelectionContext.pendingBlockHeader(), tx, null, null, null); + selectorsStateManager.blockSelectionStarted(); + evaluateAndAssertSelected(txEvaluationContext, remainingGas(remainingGas)); + + assertThat(selector.getWorkingState()).isEqualTo(TRANSFER_GAS_LIMIT * 2 - remainingGas); + } + + @Test + void moreTransactionsBelowBlockGasLimitAreSelected() { + selectorsStateManager.blockSelectionStarted(); + + final int txCount = 10; + + IntStream.range(0, txCount) + .forEach( + unused -> { + final var tx = createPendingTransaction(TRANSFER_GAS_LIMIT); + + final var txEvaluationContext = + new TransactionEvaluationContext( + blockSelectionContext.pendingBlockHeader(), tx, null, null, null); + evaluateAndAssertSelected(txEvaluationContext, remainingGas(0)); + }); + + assertThat(selector.getWorkingState()).isEqualTo(TRANSFER_GAS_LIMIT * txCount); + } + + @Test + void moreTransactionsThanBlockCanFitOnlySomeAreSelected() { + selectorsStateManager.blockSelectionStarted(); + + final int txCount = 10; + + IntStream.range(0, txCount) + .forEach( + unused -> { + final var tx = createPendingTransaction(TRANSFER_GAS_LIMIT); + + final var txEvaluationContext = + new TransactionEvaluationContext( + blockSelectionContext.pendingBlockHeader(), tx, null, null, null); + evaluateAndAssertSelected(txEvaluationContext, remainingGas(0)); + }); + + assertThat(selector.getWorkingState()).isEqualTo(TRANSFER_GAS_LIMIT * txCount); + + // last tx is too big for the remaining gas + final long tooBigGasLimit = BLOCK_GAS_LIMIT - (TRANSFER_GAS_LIMIT * txCount) + 1; + + final var bigTxEvaluationContext = + new TransactionEvaluationContext( + blockSelectionContext.pendingBlockHeader(), + createPendingTransaction(tooBigGasLimit), + null, + null, + null); + evaluateAndAssertNotSelected(bigTxEvaluationContext, TX_TOO_LARGE_FOR_REMAINING_GAS); + + assertThat(selector.getWorkingState()).isEqualTo(TRANSFER_GAS_LIMIT * txCount); + } + + @Test + void identifyWhenBlockOccupancyIsAboveThreshold() { + selectorsStateManager.blockSelectionStarted(); + + // create 2 txs with a gas limit just above the min block occupancy ratio + // so the first is accepted while the second not + final long justAboveOccupancyRatioGasLimit = + (long) (BLOCK_GAS_LIMIT * miningConfiguration.getMinBlockOccupancyRatio()) + 100; + final var tx1 = createPendingTransaction(justAboveOccupancyRatioGasLimit); + + final var txEvaluationContext1 = + new TransactionEvaluationContext( + blockSelectionContext.pendingBlockHeader(), tx1, null, null, null); + evaluateAndAssertSelected(txEvaluationContext1, remainingGas(0)); + + assertThat(selector.getWorkingState()).isEqualTo(justAboveOccupancyRatioGasLimit); + + final var tx2 = createPendingTransaction(justAboveOccupancyRatioGasLimit); + + final var txEvaluationContext2 = + new TransactionEvaluationContext( + blockSelectionContext.pendingBlockHeader(), tx2, null, null, null); + evaluateAndAssertNotSelected(txEvaluationContext2, BLOCK_OCCUPANCY_ABOVE_THRESHOLD); + + assertThat(selector.getWorkingState()).isEqualTo(justAboveOccupancyRatioGasLimit); + } + + @Test + void identifyWhenBlockIsFull() { + when(blockSelectionContext.gasCalculator().getMinimumTransactionCost()) + .thenReturn(TRANSFER_GAS_LIMIT); + + selectorsStateManager.blockSelectionStarted(); + + // allow to completely fill the block + miningConfiguration.setMinBlockOccupancyRatio(1.0); + + // create 2 txs, where the first fill the block leaving less gas than the min required by a + // transfer + final long fillBlockGasLimit = BLOCK_GAS_LIMIT - TRANSFER_GAS_LIMIT + 1; + final var tx1 = createPendingTransaction(fillBlockGasLimit); + + final var txEvaluationContext1 = + new TransactionEvaluationContext( + blockSelectionContext.pendingBlockHeader(), tx1, null, null, null); + evaluateAndAssertSelected(txEvaluationContext1, remainingGas(0)); + + assertThat(selector.getWorkingState()).isEqualTo(fillBlockGasLimit); + + final var tx2 = createPendingTransaction(TRANSFER_GAS_LIMIT); + + final var txEvaluationContext2 = + new TransactionEvaluationContext( + blockSelectionContext.pendingBlockHeader(), tx2, null, null, null); + evaluateAndAssertNotSelected(txEvaluationContext2, BLOCK_FULL); + + assertThat(selector.getWorkingState()).isEqualTo(fillBlockGasLimit); + } + + private void evaluateAndAssertSelected( + final TransactionEvaluationContext txEvaluationContext, + final TransactionProcessingResult transactionProcessingResult) { + assertThat(selector.evaluateTransactionPreProcessing(txEvaluationContext)).isEqualTo(SELECTED); + assertThat( + selector.evaluateTransactionPostProcessing( + txEvaluationContext, transactionProcessingResult)) + .isEqualTo(SELECTED); + } + + private void evaluateAndAssertNotSelected( + final TransactionEvaluationContext txEvaluationContext, + final TransactionSelectionResult preProcessedResult) { + assertThat(selector.evaluateTransactionPreProcessing(txEvaluationContext)) + .isEqualTo(preProcessedResult); + } + + private PendingTransaction createPendingTransaction(final long gasLimit) { + return PendingTransaction.newPendingTransaction( + createTransaction(TransactionType.EIP1559, gasLimit), false, false); + } + + private Transaction createTransaction(final TransactionType type, final long gasLimit) { + + var tx = + new TransactionTestFixture() + .to(Optional.of(Address.fromHexString("0x634316eA0EE79c701c6F67C53A4C54cBAfd2316d"))) + .nonce(0) + .gasLimit(gasLimit) + .type(type) + .maxFeePerGas(Optional.of(Wei.of(1000))) + .maxPriorityFeePerGas(Optional.of(Wei.of(100))); + + return tx.createTransaction(KEYS); + } + + private TransactionProcessingResult remainingGas(final long remainingGas) { + final var txProcessingResult = mock(TransactionProcessingResult.class); + when(txProcessingResult.getGasRemaining()).thenReturn(remainingGas); + return txProcessingResult; + } +} diff --git a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/MinPriorityFeePerGasTransactionSelectorTest.java b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/MinPriorityFeePerGasTransactionSelectorTest.java similarity index 92% rename from ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/MinPriorityFeePerGasTransactionSelectorTest.java rename to ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/MinPriorityFeePerGasTransactionSelectorTest.java index 29cea9d0893..73d158a13a6 100644 --- a/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/MinPriorityFeePerGasTransactionSelectorTest.java +++ b/ethereum/blockcreation/src/test/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/MinPriorityFeePerGasTransactionSelectorTest.java @@ -12,7 +12,7 @@ * * SPDX-License-Identifier: Apache-2.0 */ -package org.hyperledger.besu.ethereum.blockcreation; +package org.hyperledger.besu.ethereum.blockcreation.txselection.selectors; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -22,8 +22,6 @@ import org.hyperledger.besu.datatypes.Wei; import org.hyperledger.besu.ethereum.blockcreation.txselection.BlockSelectionContext; import org.hyperledger.besu.ethereum.blockcreation.txselection.TransactionEvaluationContext; -import org.hyperledger.besu.ethereum.blockcreation.txselection.selectors.AbstractTransactionSelector; -import org.hyperledger.besu.ethereum.blockcreation.txselection.selectors.MinPriorityFeePerGasTransactionSelector; import org.hyperledger.besu.ethereum.core.MiningConfiguration; import org.hyperledger.besu.ethereum.core.ProcessableBlockHeader; import org.hyperledger.besu.ethereum.core.Transaction; @@ -85,8 +83,7 @@ public void shouldSelectWhenPrioritySender() { private void assertSelectionResult( final TransactionEvaluationContext evaluationContext, final TransactionSelectionResult expectedResult) { - var actualResult = - transactionSelector.evaluateTransactionPreProcessing(evaluationContext, null); + var actualResult = transactionSelector.evaluateTransactionPreProcessing(evaluationContext); assertThat(actualResult).isEqualTo(expectedResult); } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/MiningConfiguration.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/MiningConfiguration.java index 55ffd5ba7d7..4c543d22427 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/MiningConfiguration.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/MiningConfiguration.java @@ -16,9 +16,12 @@ import org.hyperledger.besu.datatypes.Address; import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.plugin.data.ProcessableBlockHeader; import org.hyperledger.besu.plugin.services.TransactionSelectionService; +import org.hyperledger.besu.plugin.services.txselection.BlockTransactionSelectionService; import org.hyperledger.besu.plugin.services.txselection.PluginTransactionSelector; import org.hyperledger.besu.plugin.services.txselection.PluginTransactionSelectorFactory; +import org.hyperledger.besu.plugin.services.txselection.SelectorsStateManager; import org.hyperledger.besu.util.number.PositiveNumber; import java.time.Duration; @@ -168,10 +171,16 @@ public PositiveNumber getPoaBlockTxsSelectionMaxTime() { public TransactionSelectionService getTransactionSelectionService() { return new TransactionSelectionService() { @Override - public PluginTransactionSelector createPluginTransactionSelector() { + public PluginTransactionSelector createPluginTransactionSelector( + final SelectorsStateManager selectorsStateManager) { return PluginTransactionSelector.ACCEPT_ALL; } + @Override + public void selectPendingTransactions( + final BlockTransactionSelectionService selectionService, + final ProcessableBlockHeader pendingBlockHeader) {} + @Override public void registerPluginTransactionSelectorFactory( final PluginTransactionSelectorFactory transactionSelectorFactory) {} diff --git a/plugin-api/build.gradle b/plugin-api/build.gradle index b95ba661af6..fcb26f533d0 100644 --- a/plugin-api/build.gradle +++ b/plugin-api/build.gradle @@ -71,7 +71,7 @@ Calculated : ${currentHash} tasks.register('checkAPIChanges', FileStateChecker) { description = "Checks that the API for the Plugin-API project does not change without deliberate thought" files = sourceSets.main.allJava.files - knownHash = 'I2IrN2aLU610031Vw8xNr3hcT8/wb25pDrclwZUggE4=' + knownHash = 'D2ZMRGb2HS/9FgDE2mcizdxTQhFsGD4LS9lgngG/TnU=' } check.dependsOn('checkAPIChanges') diff --git a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/TransactionSelectionService.java b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/TransactionSelectionService.java index daa07930d8c..21effcc5c99 100644 --- a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/TransactionSelectionService.java +++ b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/TransactionSelectionService.java @@ -15,8 +15,11 @@ package org.hyperledger.besu.plugin.services; import org.hyperledger.besu.plugin.Unstable; +import org.hyperledger.besu.plugin.data.ProcessableBlockHeader; +import org.hyperledger.besu.plugin.services.txselection.BlockTransactionSelectionService; import org.hyperledger.besu.plugin.services.txselection.PluginTransactionSelector; import org.hyperledger.besu.plugin.services.txselection.PluginTransactionSelectorFactory; +import org.hyperledger.besu.plugin.services.txselection.SelectorsStateManager; /** Transaction selection service interface */ @Unstable @@ -25,9 +28,23 @@ public interface TransactionSelectionService extends BesuService { /** * Create a transaction selector plugin * + * @param selectorsStateManager the selectors state manager * @return the transaction selector plugin */ - PluginTransactionSelector createPluginTransactionSelector(); + PluginTransactionSelector createPluginTransactionSelector( + SelectorsStateManager selectorsStateManager); + + /** + * Called during the block creation to allow plugins to propose their own pending transactions for + * block inclusion + * + * @param blockTransactionSelectionService the service used by the plugin to evaluate pending + * transactions and commit or rollback changes + * @param pendingBlockHeader the header of the block being created + */ + void selectPendingTransactions( + BlockTransactionSelectionService blockTransactionSelectionService, + final ProcessableBlockHeader pendingBlockHeader); /** * Registers the transaction selector factory with the service diff --git a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/AbstractStatefulPluginTransactionSelector.java b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/AbstractStatefulPluginTransactionSelector.java new file mode 100644 index 00000000000..9e8f6137b2b --- /dev/null +++ b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/AbstractStatefulPluginTransactionSelector.java @@ -0,0 +1,71 @@ +/* + * Copyright contributors to Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.plugin.services.txselection; + +/** + * This class represents an abstract plugin transaction selector which provides methods to manage + * the selector state. + * + * @param The type of the state used by the selector + */ +public abstract class AbstractStatefulPluginTransactionSelector + implements PluginTransactionSelector { + private final SelectorsStateManager selectorsStateManager; + + /** + * Initialize the plugin state to an initial value + * + * @param selectorsStateManager the selectors state manager + * @param initialState the initial value of the state + * @param stateDuplicator the function that duplicates the state + */ + public AbstractStatefulPluginTransactionSelector( + final SelectorsStateManager selectorsStateManager, + final S initialState, + final SelectorsStateManager.StateDuplicator stateDuplicator) { + this.selectorsStateManager = selectorsStateManager; + selectorsStateManager.createSelectorState(this, initialState, stateDuplicator); + } + + /** + * Get the working state for this selector. A working state contains changes that have not yet + * committed + * + * @return the working state of this selector + */ + protected S getWorkingState() { + return selectorsStateManager.getSelectorWorkingState(this); + } + + /** + * Set the working state for this selector. A working state contains changes that have not yet + * commited + * + * @param newState the new working state of this selector + */ + protected void setWorkingState(final S newState) { + selectorsStateManager.setSelectorWorkingState(this, newState); + } + + /** + * Get the commited state for this selector. A commited state contains changes that have been + * commited + * + * @return the commited state of this selector + */ + protected S getCommitedState() { + return selectorsStateManager.getSelectorCommittedState(this); + } +} diff --git a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/BlockTransactionSelectionService.java b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/BlockTransactionSelectionService.java new file mode 100644 index 00000000000..a4ecf9e7fa9 --- /dev/null +++ b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/BlockTransactionSelectionService.java @@ -0,0 +1,57 @@ +/* + * Copyright contributors to Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.plugin.services.txselection; + +import org.hyperledger.besu.datatypes.PendingTransaction; +import org.hyperledger.besu.plugin.data.ProcessableBlockHeader; +import org.hyperledger.besu.plugin.data.TransactionSelectionResult; + +/** + * A service that can be used by plugins to include their pending transactions during block + * creation. Proposed pending transactions need to be first evaluated and based on the result of the + * evaluation of one or more pending transactions the plugin can apply its logic to commit or not + * the inclusion of the evaluated in the block. + * + *

The process of including plugin proposed pending transactions starts when {@link + * PluginTransactionSelectorFactory#selectPendingTransactions(BlockTransactionSelectionService, + * ProcessableBlockHeader)} is called. + */ +public interface BlockTransactionSelectionService { + + /** + * Evaluate a plugin proposed pending transaction for block inclusion + * + * @param pendingTransaction the pending transaction proposed by the plugin + * @return the result of the evaluation + */ + TransactionSelectionResult evaluatePendingTransaction(PendingTransaction pendingTransaction); + + /** + * Commit the changes applied by all the evaluated pending transactions since the previous commit. + * As part of this {@link PluginTransactionSelector} state of commited. + * + * @return false only if a timeout occurred during the selection of the pending transaction, + * meaning that the pending transaction is not included in the current block + */ + boolean commit(); + + /** + * Rollback the changes applied by all the evaluated pending transactions since the previous + * commit. + * + *

As part of this {@link PluginTransactionSelector} state of rolled back. + */ + void rollback(); +} diff --git a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/PluginTransactionSelector.java b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/PluginTransactionSelector.java index 71503591db3..9c1e5ceab43 100644 --- a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/PluginTransactionSelector.java +++ b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/PluginTransactionSelector.java @@ -23,7 +23,7 @@ /** Interface for the transaction selector */ @Unstable -public interface PluginTransactionSelector { +public interface PluginTransactionSelector extends TransactionSelector { /** Plugin transaction selector that unconditionally select every transaction */ PluginTransactionSelector ACCEPT_ALL = new PluginTransactionSelector() { diff --git a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/PluginTransactionSelectorFactory.java b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/PluginTransactionSelectorFactory.java index e0b7acc5840..1cdec48c0ca 100644 --- a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/PluginTransactionSelectorFactory.java +++ b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/PluginTransactionSelectorFactory.java @@ -15,15 +15,39 @@ package org.hyperledger.besu.plugin.services.txselection; import org.hyperledger.besu.plugin.Unstable; +import org.hyperledger.besu.plugin.data.ProcessableBlockHeader; -/** Interface for a factory that creates transaction selectors */ +/** + * Interface for a factory that creates transaction selector and propose pending transaction for + * block inclusion + */ @Unstable public interface PluginTransactionSelectorFactory { + /** + * A default factory that does not propose any pending transactions and does not apply any filter + */ + PluginTransactionSelectorFactory NO_OP_FACTORY = new PluginTransactionSelectorFactory() {}; /** - * Create a transaction selector + * Create a plugin transaction selector, that can be used during block creation to apply custom + * filters to proposed pending transactions * + * @param selectorsStateManager the selectors state manager * @return the transaction selector */ - PluginTransactionSelector create(); + default PluginTransactionSelector create(final SelectorsStateManager selectorsStateManager) { + return PluginTransactionSelector.ACCEPT_ALL; + } + + /** + * Called during the block creation to allow plugins to propose their own pending transactions for + * block inclusion + * + * @param blockTransactionSelectionService the service used by the plugin to evaluate pending + * transactions and commit or rollback changes + * @param pendingBlockHeader the header of the block being created + */ + default void selectPendingTransactions( + final BlockTransactionSelectionService blockTransactionSelectionService, + final ProcessableBlockHeader pendingBlockHeader) {} } diff --git a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/SelectorsStateManager.java b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/SelectorsStateManager.java new file mode 100644 index 00000000000..b85bab7dc7d --- /dev/null +++ b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/SelectorsStateManager.java @@ -0,0 +1,222 @@ +/* + * Copyright contributors to Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.plugin.services.txselection; + +import static com.google.common.base.Preconditions.checkState; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; + +/** + * Manages the state of transaction selectors (including the plugin transaction selector {@link + * PluginTransactionSelector}) during the block creation process. Some selectors have a state, for + * example the amount of gas used by selected pending transactions so far, and changes made to the + * state must be only commited after the evaluated pending transaction has been definitely selected + * for inclusion, until that point it will be always possible to rollback the changes to the state + * and return the previous commited state. + */ +@SuppressWarnings("rawtypes") +public class SelectorsStateManager { + private final List>>> + uncommitedStates = new ArrayList<>(); + private Map>> committedState = + new HashMap<>(); + private volatile boolean blockSelectionStarted = false; + + /** Create an empty selectors state manager, here to make javadoc linter happy. */ + public SelectorsStateManager() {} + + /** + * Create, initialize and track the state for a selector. + * + *

Call to this method must be performed before the block selection is stated with {@link + * #blockSelectionStarted()}, otherwise it fails. + * + * @param selector the selector + * @param initialState the initial value of the state + * @param duplicator the state duplicator + * @param the type of the selector state + * @param the type of the state duplicator + */ + public > void createSelectorState( + final TransactionSelector selector, final S initialState, final D duplicator) { + checkState( + !blockSelectionStarted, "Cannot create selector state after block selection is started"); + committedState.put(selector, new SelectorState<>(initialState, duplicator)); + } + + /** + * Called at the start of block selection, when the initialization is done, to prepare a new + * working state based on the initial state. + * + *

After this method is called, it is not possible to call anymore {@link + * #createSelectorState(TransactionSelector, Object, StateDuplicator)} + */ + public void blockSelectionStarted() { + blockSelectionStarted = true; + uncommitedStates.add(duplicateLastState()); + } + + private Map>> + duplicateLastState() { + return getLast().entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().duplicated())); + } + + /** + * Get the working state for the specified selector + * + * @param selector the selector + * @return the working state of the selector + * @param the type of the selector state + */ + @SuppressWarnings("unchecked") + public T getSelectorWorkingState(final TransactionSelector selector) { + return (T) uncommitedStates.getLast().get(selector).get(); + } + + /** + * set the working state for the specified selector + * + * @param selector the selector + * @param newState the new state + * @param the type of the selector state + */ + @SuppressWarnings("unchecked") + public void setSelectorWorkingState(final TransactionSelector selector, final T newState) { + ((SelectorState>) uncommitedStates.getLast().get(selector)).set(newState); + } + + /** + * Get the commited state for the specified selector + * + * @param selector the selector + * @return the commited state of the selector + * @param the type of the selector state + */ + @SuppressWarnings("unchecked") + public T getSelectorCommittedState(final TransactionSelector selector) { + return (T) committedState.get(selector).get(); + } + + /** + * Commit the current working state and prepare a new working state based on the just commited + * state + */ + public void commit() { + committedState = getLast(); + uncommitedStates.clear(); + uncommitedStates.add(duplicateLastState()); + } + + /** + * Discards the current working state and prepare a new working state based on the just commited + * state + */ + public void rollback() { + uncommitedStates.clear(); + uncommitedStates.add(duplicateLastState()); + } + + private Map>> getLast() { + if (uncommitedStates.isEmpty()) { + return committedState; + } + return uncommitedStates.getLast(); + } + + /** + * A function that create a duplicate of the input object. The duplication must be a deep copy. + * + * @param the type of the object + */ + @FunctionalInterface + public interface StateDuplicator extends UnaryOperator { + /** + * Duplicate the input objet + * + * @param t the input object to duplicate + * @return a deep copy of the input object + */ + T duplicate(T t); + + @Override + default T apply(final T t) { + return duplicate(t); + } + + /** + * Utility to duplicate a long + * + * @param l a long + * @return a copy of the long + */ + static long duplicateLong(final long l) { + return l; + } + } + + /** + * A selector state object is one that is able to return, update and duplicate the state it + * contains + * + * @param the type of the state + */ + private static class SelectorState> { + private final D duplicator; + private S state; + + /** + * Create a selector state with the initial value + * + * @param initialState the initial initialState + */ + public SelectorState(final S initialState, final D duplicator) { + this.state = initialState; + this.duplicator = duplicator; + } + + /** + * The method that concrete classes must implement to create a deep copy of the state + * + * @return a new selector state with a deep copy of the state + */ + private SelectorState duplicated() { + return new SelectorState<>(duplicator.duplicate(state), duplicator); + } + + /** + * Get the current state + * + * @return the current state + */ + public S get() { + return state; + } + + /** + * Replace the current state with the new one + * + * @param newState the new state + */ + public void set(final S newState) { + this.state = newState; + } + } +} diff --git a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/TransactionSelector.java b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/TransactionSelector.java new file mode 100644 index 00000000000..fa9628f91be --- /dev/null +++ b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/txselection/TransactionSelector.java @@ -0,0 +1,21 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.plugin.services.txselection; + +import org.hyperledger.besu.plugin.Unstable; + +/** Interface for the transaction selector */ +@Unstable +public interface TransactionSelector {} diff --git a/util/src/main/java/org/hyperledger/besu/util/log4j/plugin/StackTraceMatchFilter.java b/util/src/main/java/org/hyperledger/besu/util/log4j/plugin/StackTraceMatchFilter.java new file mode 100644 index 00000000000..b280589a8d4 --- /dev/null +++ b/util/src/main/java/org/hyperledger/besu/util/log4j/plugin/StackTraceMatchFilter.java @@ -0,0 +1,120 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.util.log4j.plugin; + +import java.util.Arrays; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.Logger; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute; +import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory; +import org.apache.logging.log4j.core.filter.AbstractFilter; +import org.apache.logging.log4j.message.Message; + +/** Matches a text in the stack trace */ +@Plugin( + name = "StackTraceMatchFilter", + category = "Core", + elementType = "filter", + printObject = true) +public class StackTraceMatchFilter extends AbstractFilter { + private final String text; + + private StackTraceMatchFilter(final String text, final Result onMatch, final Result onMismatch) { + super(onMatch, onMismatch); + this.text = text; + } + + @Override + public Result filter( + final Logger logger, + final Level level, + final Marker marker, + final Object msg, + final Throwable t) { + return filter(t); + } + + @Override + public Result filter( + final Logger logger, + final Level level, + final Marker marker, + final Message msg, + final Throwable t) { + return filter(t); + } + + @Override + public Result filter(final LogEvent event) { + return filter(event.getThrown()); + } + + private Result filter(final Throwable t) { + if (t != null) { + return Arrays.stream(t.getStackTrace()) + .map(StackTraceElement::getClassName) + .anyMatch(cn -> cn.contains(text)) + ? onMatch + : onMismatch; + } + return Result.NEUTRAL; + } + + @Override + public String toString() { + return text; + } + + /** + * Create a new builder + * + * @return a new builder + */ + @PluginBuilderFactory + public static StackTraceMatchFilter.Builder newBuilder() { + return new StackTraceMatchFilter.Builder(); + } + + /** Builder for StackTraceMatchFilter */ + public static class Builder extends AbstractFilterBuilder + implements org.apache.logging.log4j.core.util.Builder { + @PluginBuilderAttribute private String text = ""; + + /** Default constructor */ + public Builder() { + // here to make javadoc happy + } + + /** + * Set the string to match in the stack trace + * + * @param text the match string + * @return this builder + */ + public StackTraceMatchFilter.Builder setMatchString(final String text) { + this.text = text; + return this; + } + + @Override + public StackTraceMatchFilter build() { + return new StackTraceMatchFilter(this.text, this.getOnMatch(), this.getOnMismatch()); + } + } +}