Allow plugins to propose transactions during block creation (#8268)

* Allow plugins to propose transactions during block creation

Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net>

* Apply suggestions from code review

Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net>

* Document AbstractStatefulTransactionSelector

Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net>

* Unit tests for BlockSizeTransactionSelectorTest

Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net>

---------

Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net>
This commit is contained in:
Fabio Di Fabio
2025-02-12 15:59:59 +01:00
committed by GitHub
parent 0ad85ecada
commit 428a638569
28 changed files with 1287 additions and 324 deletions

View File

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

View File

@@ -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<PluginTransactionSelectorFactory> factory = Optional.empty();
private PluginTransactionSelectorFactory factory = PluginTransactionSelectorFactory.NO_OP_FACTORY;
@Override
public PluginTransactionSelector createPluginTransactionSelector() {
return factory
.map(PluginTransactionSelectorFactory::create)
.orElse(PluginTransactionSelector.ACCEPT_ALL);
public PluginTransactionSelector createPluginTransactionSelector(
final SelectorsStateManager selectorsStateManager) {
return factory.create(selectorsStateManager);
}
@Override
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;
}
}

View File

@@ -55,8 +55,8 @@ import org.hyperledger.besu.evm.account.MutableAccount;
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 @@ public abstract class AbstractBlockCreator implements AsyncBlockCreator {
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 @@ public abstract class AbstractBlockCreator implements AsyncBlockCreator {
miningBeneficiary,
newProtocolSpec,
pluginTransactionSelector,
selectorsStateManager,
parentHeader);
transactionResults.logSelectionStats();
timings.register("txsSelection");
@@ -362,6 +367,7 @@ public abstract class AbstractBlockCreator implements AsyncBlockCreator {
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 @@ public abstract class AbstractBlockCreator implements AsyncBlockCreator {
protocolSpec.getGasLimitCalculator(),
protocolSpec.getBlockHashProcessor(),
pluginTransactionSelector,
ethScheduler);
ethScheduler,
selectorsStateManager);
if (transactions.isPresent()) {
return selector.evaluateTransactions(transactions.get());

View File

@@ -50,10 +50,14 @@ import org.hyperledger.besu.evm.blockhash.BlockHashLookup;
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 @@ import org.slf4j.LoggerFactory;
* 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<Boolean> isCancelled;
private final MainnetTransactionProcessor transactionProcessor;
@@ -99,13 +103,17 @@ public class BlockTransactionSelector {
private final TransactionSelectionResults transactionSelectionResults =
new TransactionSelectionResults();
private final List<AbstractTransactionSelector> 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<Runnable> selectedTxPendingActions = new ArrayList<>(1);
public BlockTransactionSelector(
final MiningConfiguration miningConfiguration,
@@ -123,7 +131,8 @@ public class 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 class 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<AbstractTransactionSelector> 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 @@ public class BlockTransactionSelector {
* 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 class BlockTransactionSelector {
private void timeLimitedSelection() {
final var txSelectionTask =
new FutureTask<Void>(
() ->
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 @@ public class BlockTransactionSelector {
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 @@ public class BlockTransactionSelector {
* evaluations.
*/
public TransactionSelectionResults evaluateTransactions(final List<Transaction> 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 class BlockTransactionSelector {
* @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 @@ public class BlockTransactionSelector {
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 @@ public class BlockTransactionSelector {
for (var selector : transactionSelectors) {
TransactionSelectionResult result =
selector.evaluateTransactionPreProcessing(evaluationContext, transactionSelectionResults);
selector.evaluateTransactionPreProcessing(evaluationContext);
if (!result.equals(SELECTED)) {
return result;
}
@@ -357,8 +429,7 @@ public class BlockTransactionSelector {
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 @@ public class BlockTransactionSelector {
* 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 @@ public class BlockTransactionSelector {
*
* @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);
if (tooLate) {
notifySelected(evaluationContext, processingResult);
LOG.atTrace()
.setMessage("Selected and commited {} for block creation")
.addArgument(transaction::toTraceLog)
.log();
});
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 @@ public class BlockTransactionSelector {
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 @@ public class BlockTransactionSelector {
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) {

View File

@@ -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 class TransactionSelectionResults {
return cumulativeGasUsed;
}
public long getCumulativeBlobGasUsed() {
return cumulativeBlobGasUsed;
}
public Map<Transaction, TransactionSelectionResult> getNotSelectedTransactions() {
return Map.copyOf(notSelectedTransactions);
}
@@ -135,7 +124,6 @@ public class TransactionSelectionResults {
}
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 class TransactionSelectionResults {
@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)

View File

@@ -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 <S> The type of state specified by the implementing selector, not to be confused with the
* world state.
*/
public abstract class AbstractStatefulTransactionSelector<S> extends AbstractTransactionSelector {
private final SelectorsStateManager selectorsStateManager;
public AbstractStatefulTransactionSelector(
final BlockSelectionContext context,
final SelectorsStateManager selectorsStateManager,
final S initialState,
final SelectorsStateManager.StateDuplicator<S> 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);
}
}

View File

@@ -16,15 +16,15 @@ package org.hyperledger.besu.ethereum.blockcreation.txselection.selectors;
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 abstract class AbstractTransactionSelector {
* 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);
/**

View File

@@ -16,7 +16,6 @@ package org.hyperledger.besu.ethereum.blockcreation.txselection.selectors;
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 class BlobPriceTransactionSelector extends AbstractTransactionSelector {
* 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 class BlobPriceTransactionSelector extends AbstractTransactionSelector {
@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;

View File

@@ -16,9 +16,9 @@ package org.hyperledger.besu.ethereum.blockcreation.txselection.selectors;
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 @@ import org.slf4j.LoggerFactory;
* 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<Long> {
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 class BlobSizeTransactionSelector extends AbstractTransactionSelector {
* 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 class BlobSizeTransactionSelector extends AbstractTransactionSelector {
@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;
}
}

View File

@@ -16,10 +16,10 @@ package org.hyperledger.besu.ethereum.blockcreation.txselection.selectors;
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 @@ import org.slf4j.LoggerFactory;
* 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<Long> {
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 class BlockSizeTransactionSelector extends AbstractTransactionSelector {
* 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) {
if (transactionTooLargeForBlock(
evaluationContext.getTransaction(), transactionSelectionResults)) {
final long cumulativeGasUsed = getWorkingState();
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 class BlockSizeTransactionSelector extends AbstractTransactionSelector {
@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 @@ public class BlockSizeTransactionSelector extends AbstractTransactionSelector {
/**
* 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(

View File

@@ -17,7 +17,6 @@ package org.hyperledger.besu.ethereum.blockcreation.txselection.selectors;
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 class MinPriorityFeePerGasTransactionSelector extends AbstractTransaction
* 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 @@ public class MinPriorityFeePerGasTransactionSelector extends AbstractTransaction
@Override
public TransactionSelectionResult evaluateTransactionPostProcessing(
final TransactionEvaluationContext evaluationContext,
final TransactionSelectionResults blockTransactionResults,
final TransactionProcessingResult processingResult) {
return TransactionSelectionResult.SELECTED;
}

View File

@@ -16,7 +16,6 @@ package org.hyperledger.besu.ethereum.blockcreation.txselection.selectors;
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 class PriceTransactionSelector extends AbstractTransactionSelector {
* 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 class PriceTransactionSelector extends AbstractTransactionSelector {
@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;

View File

@@ -16,7 +16,6 @@ package org.hyperledger.besu.ethereum.blockcreation.txselection.selectors;
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 class ProcessingResultTransactionSelector extends AbstractTransactionSele
@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 class ProcessingResultTransactionSelector extends AbstractTransactionSele
* 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()) {

View File

@@ -17,34 +17,41 @@ package org.hyperledger.besu.ethereum.blockcreation.txselection.selectors;
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<Address> skippedSenders = new HashSet<>();
private final Map<Address, Long> 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 class SkipSenderTransactionSelector extends AbstractTransactionSelector {
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 class SkipSenderTransactionSelector extends AbstractTransactionSelector {
* 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 class SkipSenderTransactionSelector extends AbstractTransactionSelector {
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);
}
}
}

View File

@@ -90,6 +90,7 @@ import org.hyperledger.besu.plugin.services.MetricsSystem;
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 abstract class AbstractBlockTransactionSelectorTest {
final Transaction notSelectedInvalid = createTransaction(2, Wei.of(10), 21_000, SENDER2);
ensureTransactionIsValid(notSelectedInvalid, 21_000, 0);
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;
}
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 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 abstract class AbstractBlockTransactionSelectorTest {
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 abstract class AbstractBlockTransactionSelectorTest {
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 @@ public abstract class AbstractBlockTransactionSelectorTest {
final PluginTransactionSelectorFactory transactionSelectorFactory =
mock(PluginTransactionSelectorFactory.class);
when(transactionSelectorFactory.create()).thenReturn(transactionSelector);
when(transactionSelectorFactory.create(any())).thenReturn(transactionSelector);
transactionSelectionService.registerPluginTransactionSelectorFactory(
transactionSelectorFactory);
@@ -1233,7 +1247,7 @@ public abstract class AbstractBlockTransactionSelectorTest {
final PluginTransactionSelectorFactory transactionSelectorFactory =
mock(PluginTransactionSelectorFactory.class);
when(transactionSelectorFactory.create()).thenReturn(transactionSelector);
when(transactionSelectorFactory.create(any())).thenReturn(transactionSelector);
transactionSelectionService.registerPluginTransactionSelectorFactory(
transactionSelectorFactory);
@@ -1320,6 +1334,7 @@ public abstract class AbstractBlockTransactionSelectorTest {
final Wei blobGasPrice,
final TransactionSelectionService transactionSelectionService) {
final var selectorsStateManager = new SelectorsStateManager();
final BlockTransactionSelector selector =
new BlockTransactionSelector(
miningConfiguration,
@@ -1336,8 +1351,9 @@ public abstract class AbstractBlockTransactionSelectorTest {
new LondonGasCalculator(),
GasLimitCalculator.constant(),
protocolSchedule.getByBlockHeader(blockHeader).getBlockHashProcessor(),
transactionSelectionService.createPluginTransactionSelector(),
ethScheduler);
transactionSelectionService.createPluginTransactionSelector(selectorsStateManager),
ethScheduler,
selectorsStateManager);
return selector;
}

View File

@@ -16,10 +16,11 @@ package org.hyperledger.besu.ethereum.blockcreation.txselection.selectors;
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.VersionedHash;
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.Supplier;
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;
SelectorsStateManager selectorsStateManager;
BlobSizeTransactionSelector selector;
@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));
selectorsStateManager = new SelectorsStateManager();
selector = new BlobSizeTransactionSelector(blockSelectionContext, selectorsStateManager);
}
@Test
void notBlobTransactionsAreSelectedWithoutAnyCheck() {
final var selector = new BlobSizeTransactionSelector(blockSelectionContext);
final var nonBlobTx = createEIP1559PendingTransaction();
void notBlobTransactionsAreAlwaysSelected() {
// this tx fills all the available blob space
final var firstBlobTx = createBlobPendingTransaction(MAX_BLOBS);
final var txEvaluationContext =
new TransactionEvaluationContext(
blockSelectionContext.pendingBlockHeader(), nonBlobTx, null, null, null);
blockSelectionContext.pendingBlockHeader(), firstBlobTx, null, null, null);
selectorsStateManager.blockSelectionStarted();
evaluateAndAssertSelected(txEvaluationContext);
final var result =
selector.evaluateTransactionPreProcessing(txEvaluationContext, selectionResults);
assertThat(result).isEqualTo(TransactionSelectionResult.SELECTED);
verifyNoInteractions(selectionResults);
// this non blob tx is selected regardless the blob space is already filled
final var nonBlobTx = createEIP1559PendingTransaction();
final var nonBlobTxEvaluationContext =
new TransactionEvaluationContext(
blockSelectionContext.pendingBlockHeader(), nonBlobTx, null, null, null);
selectorsStateManager.blockSelectionStarted();
evaluateAndAssertSelected(nonBlobTxEvaluationContext);
}
@Test
void firstBlobTransactionIsSelected() {
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);
final var txEvaluationContext =
new TransactionEvaluationContext(
blockSelectionContext.pendingBlockHeader(), firstBlobTx, null, null, null);
when(selectionResults.getCumulativeBlobGasUsed()).thenReturn(0L);
final var result =
selector.evaluateTransactionPreProcessing(txEvaluationContext, selectionResults);
assertThat(result).isEqualTo(TransactionSelectionResult.SELECTED);
verify(selectionResults).getCumulativeBlobGasUsed();
selectorsStateManager.blockSelectionStarted();
evaluateAndAssertSelected(txEvaluationContext);
}
@Test
void returnsBlobsFullWhenMaxNumberOfBlobsAlreadyPresent() {
when(blockSelectionContext.gasLimitCalculator().currentBlobGasLimit()).thenReturn(MAX_BLOB_GAS);
final var selector = new BlobSizeTransactionSelector(blockSelectionContext);
final var firstBlobTx = createBlobPendingTransaction(1);
final var txEvaluationContext =
final var blobTx1 = createBlobPendingTransaction(MAX_BLOBS);
final var txEvaluationContext1 =
new TransactionEvaluationContext(
blockSelectionContext.pendingBlockHeader(), firstBlobTx, null, null, null);
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);
final var txEvaluationContext =
// 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(), firstBlobTx, null, null, null);
blockSelectionContext.pendingBlockHeader(), blobTx1, null, null, null);
selectorsStateManager.blockSelectionStarted();
evaluateAndAssertSelected(txEvaluationContext1);
when(selectionResults.getCumulativeBlobGasUsed()).thenReturn(MAX_BLOB_GAS - 1);
final var blobTx2 = createBlobPendingTransaction(MAX_BLOBS);
final var txEvaluationContext2 =
new TransactionEvaluationContext(
blockSelectionContext.pendingBlockHeader(), blobTx2, null, null, null);
selectorsStateManager.blockSelectionStarted();
evaluateAndAssertNotSelected(txEvaluationContext2, TX_TOO_LARGE_FOR_REMAINING_BLOB_GAS);
}
final var result =
selector.evaluateTransactionPreProcessing(txEvaluationContext, selectionResults);
assertThat(result).isEqualTo(TransactionSelectionResult.TX_TOO_LARGE_FOR_REMAINING_BLOB_GAS);
verify(selectionResults).getCumulativeBlobGasUsed();
private void evaluateAndAssertSelected(final TransactionEvaluationContext txEvaluationContext) {
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 createEIP1559PendingTransaction() {

View File

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

View File

@@ -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 static org.mockito.Mockito.when;
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 class MinPriorityFeePerGasTransactionSelectorTest {
private void assertSelectionResult(
final TransactionEvaluationContext evaluationContext,
final TransactionSelectionResult expectedResult) {
var actualResult =
transactionSelector.evaluateTransactionPreProcessing(evaluationContext, null);
var actualResult = transactionSelector.evaluateTransactionPreProcessing(evaluationContext);
assertThat(actualResult).isEqualTo(expectedResult);
}

View File

@@ -16,9 +16,12 @@ package org.hyperledger.besu.ethereum.core;
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 abstract class MiningConfiguration {
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) {}

View File

@@ -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')

View File

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

View File

@@ -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 <S> The type of the state used by the selector
*/
public abstract class AbstractStatefulPluginTransactionSelector<S>
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<S> 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);
}
}

View File

@@ -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.
*
* <p>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.
*
* <p>As part of this {@link PluginTransactionSelector} state of rolled back.
*/
void rollback();
}

View File

@@ -23,7 +23,7 @@ import org.hyperledger.besu.plugin.services.tracer.BlockAwareOperationTracer;
/** 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() {

View File

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

View File

@@ -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<Map<TransactionSelector, SelectorState<?, ? extends StateDuplicator<?>>>>
uncommitedStates = new ArrayList<>();
private Map<TransactionSelector, SelectorState<?, ? extends StateDuplicator<?>>> 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.
*
* <p>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 <S> the type of the selector state
* @param <D> the type of the state duplicator
*/
public <S, D extends StateDuplicator<S>> 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.
*
* <p>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<TransactionSelector, SelectorState<?, ? extends StateDuplicator<?>>>
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 <T> the type of the selector state
*/
@SuppressWarnings("unchecked")
public <T> 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 <T> the type of the selector state
*/
@SuppressWarnings("unchecked")
public <T> void setSelectorWorkingState(final TransactionSelector selector, final T newState) {
((SelectorState<T, StateDuplicator<T>>) 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 <T> the type of the selector state
*/
@SuppressWarnings("unchecked")
public <T> 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<TransactionSelector, SelectorState<?, ? extends StateDuplicator<?>>> 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 <T> the type of the object
*/
@FunctionalInterface
public interface StateDuplicator<T> extends UnaryOperator<T> {
/**
* 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 <S> the type of the state
*/
private static class SelectorState<S, D extends StateDuplicator<S>> {
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<S, D> 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;
}
}
}

View File

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

View File

@@ -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<StackTraceMatchFilter.Builder>
implements org.apache.logging.log4j.core.util.Builder<StackTraceMatchFilter> {
@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());
}
}
}