Skip to content

Commit

Permalink
Improve blob size transaction selector
Browse files Browse the repository at this point in the history
Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net>
  • Loading branch information
fab-10 committed Jul 16, 2024
1 parent 465fd1c commit 16b455b
Show file tree
Hide file tree
Showing 7 changed files with 300 additions and 9 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- `privacy-nonce-always-increments` option enables private transactions to always increment the nonce, even if the transaction is invalid [#6593](https://github.com/hyperledger/besu/pull/6593)
- Add `block-test` subcommand to the evmtool which runs blockchain reference tests [#7310](https://github.com/hyperledger/besu/pull/7310)
- Implement gnark-crypto for eip-2537 [#7316](https://github.com/hyperledger/besu/pull/7316)
- Improve blob size transaction selector [#7312](https://github.com/hyperledger/besu/pull/7312)

### Bug fixes
- Fix `eth_call` deserialization to correctly ignore unknown fields in the transaction object. [#7323](https://github.com/hyperledger/besu/pull/7323)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.hyperledger.besu.ethereum.GasLimitCalculator;
import org.hyperledger.besu.ethereum.blockcreation.txselection.selectors.AbstractTransactionSelector;
import org.hyperledger.besu.ethereum.blockcreation.txselection.selectors.BlobPriceTransactionSelector;
import org.hyperledger.besu.ethereum.blockcreation.txselection.selectors.BlobSizeTransactionSelector;
import org.hyperledger.besu.ethereum.blockcreation.txselection.selectors.BlockSizeTransactionSelector;
import org.hyperledger.besu.ethereum.blockcreation.txselection.selectors.MinPriorityFeePerGasTransactionSelector;
import org.hyperledger.besu.ethereum.blockcreation.txselection.selectors.PriceTransactionSelector;
Expand Down Expand Up @@ -146,6 +147,7 @@ private List<AbstractTransactionSelector> createTransactionSelectors(
final BlockSelectionContext context) {
return List.of(
new BlockSizeTransactionSelector(context),
new BlobSizeTransactionSelector(context),
new PriceTransactionSelector(context),
new BlobPriceTransactionSelector(context),
new MinPriorityFeePerGasTransactionSelector(context),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* 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.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.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* This class extends AbstractTransactionSelector and provides a specific implementation for
* evaluating transactions based on blobs size. It checks if a transaction supports blobs, and if
* so, checks that there is enough remaining data gas in the block to fit the blobs of the tx.
*/
public class BlobSizeTransactionSelector extends AbstractTransactionSelector {
private static final Logger LOG = LoggerFactory.getLogger(BlobSizeTransactionSelector.class);

public BlobSizeTransactionSelector(final BlockSelectionContext context) {
super(context);
}

/**
* Evaluates a transaction considering other transactions in the same block. If the tx does not
* support blobs, no check is performed, and SELECTED is returned, otherwise SELECTED is returned
* only if there is enough remaining blob gas to fit the blobs of the tx, otherwise a specific not
* selected result is returned, depending on the fact that the block already contains the max
* 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 var tx = evaluationContext.getTransaction();
if (tx.getType().supportsBlob()) {

final var remainingBlobGas =
context.gasLimitCalculator().currentBlobGasLimit()
- transactionSelectionResults.getCumulativeBlobGasUsed();

if (remainingBlobGas == 0) {
LOG.atTrace()
.setMessage("The block already contains the max number of allowed blobs")
.addArgument(evaluationContext.getPendingTransaction()::toTraceLog)
.log();
return TransactionSelectionResult.BLOBS_FULL;
}

final long requestedBlobGas = context.gasCalculator().blobGasCost(tx.getBlobCount());

if (requestedBlobGas > remainingBlobGas) {
LOG.atTrace()
.setMessage(
"There is not enough blob gas available to fit the blobs of the transaction {} in the block."
+ " Available {} / Requested {}")
.addArgument(evaluationContext.getPendingTransaction()::toTraceLog)
.addArgument(remainingBlobGas)
.addArgument(requestedBlobGas)
.log();
return TransactionSelectionResult.TX_TOO_LARGE_FOR_REMAINING_BLOB_GAS;
}
}
return TransactionSelectionResult.SELECTED;
}

@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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,8 @@ public TransactionSelectionResult evaluateTransactionPostProcessing(
private boolean transactionTooLargeForBlock(
final Transaction transaction,
final TransactionSelectionResults transactionSelectionResults) {
final long blobGasUsed = context.gasCalculator().blobGasCost(transaction.getBlobCount());

if (blobGasUsed
> context.gasLimitCalculator().currentBlobGasLimit()
- transactionSelectionResults.getCumulativeBlobGasUsed()) {
return true;
}

return transaction.getGasLimit() + blobGasUsed
return transaction.getGasLimit()
> context.processableBlockHeader().getGasLimit()
- transactionSelectionResults.getCumulativeGasUsed();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/*
* 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 static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.mockito.Answers.RETURNS_DEEP_STUBS;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
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.Blob;
import org.hyperledger.besu.datatypes.BlobsWithCommitments;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.datatypes.KZGCommitment;
import org.hyperledger.besu.datatypes.KZGProof;
import org.hyperledger.besu.datatypes.TransactionType;
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.evm.gascalculator.CancunGasCalculator;
import org.hyperledger.besu.plugin.data.TransactionSelectionResult;

import java.util.Optional;
import java.util.stream.IntStream;

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.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class BlobSizeTransactionSelectorTest {
private static final Supplier<SignatureAlgorithm> SIGNATURE_ALGORITHM =
Suppliers.memoize(SignatureAlgorithmFactory::getInstance);
private static final KeyPair KEYS = SIGNATURE_ALGORITHM.get().generateKeyPair();

private static final long BLOB_GAS_PER_BLOB = CancunGasCalculator.BLOB_GAS_PER_BLOB;
private static final int MAX_BLOBS = 6;
private static final long MAX_BLOB_GAS = BLOB_GAS_PER_BLOB * MAX_BLOBS;

@Mock(answer = RETURNS_DEEP_STUBS)
BlockSelectionContext blockSelectionContext;

@Mock TransactionSelectionResults selectionResults;

@Test
void notBlobTransactionsAreSelectedWithoutAnyCheck() {
final var selector = new BlobSizeTransactionSelector(blockSelectionContext);

final var nonBlobTx = createEIP1559PendingTransaction();

final var txEvaluationContext = new TransactionEvaluationContext(nonBlobTx, null, null, null);

final var result =
selector.evaluateTransactionPreProcessing(txEvaluationContext, selectionResults);
assertThat(result).isEqualTo(TransactionSelectionResult.SELECTED);
verifyNoInteractions(selectionResults);
}

@Test
void firstBlobTransactionIsSelected() {
when(blockSelectionContext.gasLimitCalculator().currentBlobGasLimit()).thenReturn(MAX_BLOB_GAS);
when(blockSelectionContext.gasCalculator().blobGasCost(anyInt()))
.thenAnswer(iom -> BLOB_GAS_PER_BLOB * iom.getArgument(0, Integer.class));

final var selector = new BlobSizeTransactionSelector(blockSelectionContext);

final var firstBlobTx = createBlobPendingTransaction(MAX_BLOBS);

final var txEvaluationContext = new TransactionEvaluationContext(firstBlobTx, null, null, null);

when(selectionResults.getCumulativeBlobGasUsed()).thenReturn(0L);

final var result =
selector.evaluateTransactionPreProcessing(txEvaluationContext, selectionResults);
assertThat(result).isEqualTo(TransactionSelectionResult.SELECTED);
verify(selectionResults).getCumulativeBlobGasUsed();
}

@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 = new TransactionEvaluationContext(firstBlobTx, null, null, null);

when(selectionResults.getCumulativeBlobGasUsed()).thenReturn(MAX_BLOB_GAS);

final var result =
selector.evaluateTransactionPreProcessing(txEvaluationContext, selectionResults);
assertThat(result).isEqualTo(TransactionSelectionResult.BLOBS_FULL);
verify(selectionResults).getCumulativeBlobGasUsed();
}

@Test
void returnsTooLargeForRemainingBlobGas() {
when(blockSelectionContext.gasLimitCalculator().currentBlobGasLimit()).thenReturn(MAX_BLOB_GAS);
when(blockSelectionContext.gasCalculator().blobGasCost(anyInt()))
.thenAnswer(iom -> BLOB_GAS_PER_BLOB * iom.getArgument(0, Integer.class));

final var selector = new BlobSizeTransactionSelector(blockSelectionContext);

final var firstBlobTx = createBlobPendingTransaction(MAX_BLOBS);

final var txEvaluationContext = new TransactionEvaluationContext(firstBlobTx, null, null, null);

when(selectionResults.getCumulativeBlobGasUsed()).thenReturn(MAX_BLOB_GAS - 1);

final var result =
selector.evaluateTransactionPreProcessing(txEvaluationContext, selectionResults);
assertThat(result).isEqualTo(TransactionSelectionResult.TX_TOO_LARGE_FOR_REMAINING_BLOB_GAS);
verify(selectionResults).getCumulativeBlobGasUsed();
}

private PendingTransaction createEIP1559PendingTransaction() {
return PendingTransaction.newPendingTransaction(
createTransaction(TransactionType.EIP1559, 0), false, false);
}

private PendingTransaction createBlobPendingTransaction(final int blobCount) {
return PendingTransaction.newPendingTransaction(
createTransaction(TransactionType.BLOB, blobCount), false, false);
}

private Transaction createTransaction(final TransactionType type, final int blobCount) {

var tx =
new TransactionTestFixture()
.to(Optional.of(Address.fromHexString("0x634316eA0EE79c701c6F67C53A4C54cBAfd2316d")))
.nonce(0)
.type(type);
tx.maxFeePerGas(Optional.of(Wei.of(1000))).maxPriorityFeePerGas(Optional.of(Wei.of(100)));
if (type.supportsBlob()) {
if (blobCount > 0) {
tx.maxFeePerBlobGas(Optional.of(Wei.of(10)));
final var versionHashes =
IntStream.range(0, blobCount)
.mapToObj(i -> new VersionedHash((byte) 1, Hash.ZERO))
.toList();
final var kgzCommitments =
IntStream.range(0, blobCount)
.mapToObj(i -> new KZGCommitment(Bytes48.random()))
.toList();
final var kzgProofs =
IntStream.range(0, blobCount).mapToObj(i -> new KZGProof(Bytes48.random())).toList();
final var blobs =
IntStream.range(0, blobCount).mapToObj(i -> new Blob(Bytes.random(32 * 4096))).toList();
tx.versionedHashes(Optional.of(versionHashes));
final var blobsWithCommitments =
new BlobsWithCommitments(kgzCommitments, blobs, kzgProofs, versionHashes);
tx.blobsWithCommitments(Optional.of(blobsWithCommitments));
} else {
fail("At least 1 blob is required for blob tx type");
}
}
return tx.createTransaction(KEYS);
}
}
2 changes: 1 addition & 1 deletion plugin-api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,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 = 'MOOKIka8Q1D64m0lU29Leq+zf1vcgeLoDVukBVQXhnY='
knownHash = 'vWxI4pduLCvzqU73k5kJsoeK9t7QTqOBC9IxksFuzBs='
}
check.dependsOn('checkAPIChanges')

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ protected interface Status {
private enum BaseStatus implements Status {
SELECTED,
BLOCK_FULL(true, false),
BLOBS_FULL(false, false),
BLOCK_OCCUPANCY_ABOVE_THRESHOLD(true, false),
BLOCK_SELECTION_TIMEOUT(true, false),
TX_EVALUATION_TOO_LONG(true, true),
Expand Down Expand Up @@ -96,6 +97,10 @@ public boolean discard() {
public static final TransactionSelectionResult BLOCK_FULL =
new TransactionSelectionResult(BaseStatus.BLOCK_FULL);

/** The block already contains the max number of allowed blobs. */
public static final TransactionSelectionResult BLOBS_FULL =
new TransactionSelectionResult(BaseStatus.BLOBS_FULL);

/** There was no more time to add transaction to the block */
public static final TransactionSelectionResult BLOCK_SELECTION_TIMEOUT =
new TransactionSelectionResult(BaseStatus.BLOCK_SELECTION_TIMEOUT);
Expand All @@ -118,6 +123,13 @@ public boolean discard() {
public static final TransactionSelectionResult TX_TOO_LARGE_FOR_REMAINING_GAS =
TransactionSelectionResult.invalidTransient("TX_TOO_LARGE_FOR_REMAINING_GAS");

/**
* The transaction has not been selected since there is not enough remaining blob gas in the block
* to fit the blobs of the tx, but selection should continue.
*/
public static final TransactionSelectionResult TX_TOO_LARGE_FOR_REMAINING_BLOB_GAS =
TransactionSelectionResult.invalidTransient("TX_TOO_LARGE_FOR_REMAINING_BLOB_GAS");

/**
* The transaction has not been selected since its current price is below the configured min
* price, but the selection should continue.
Expand Down

0 comments on commit 16b455b

Please sign in to comment.