Skip to content
Merged
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
### Breaking Changes
### Upcoming Breaking Changes
### Additions and Improvements
- EIP-7825: Transaction gas limit cap [#8700](https://github.com/hyperledger/besu/pull/8700)

### Bug fixes

## 25.6.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ public static RpcErrorType convertTransactionInvalidReason(
return RpcErrorType.TRANSACTION_UPFRONT_COST_EXCEEDS_BALANCE;
case EXCEEDS_BLOCK_GAS_LIMIT:
return RpcErrorType.EXCEEDS_BLOCK_GAS_LIMIT;
case EXCEEDS_TRANSACTION_GAS_LIMIT:
return RpcErrorType.EXCEEDS_TRANSACTION_GAS_LIMIT;
case WRONG_CHAIN_ID:
return RpcErrorType.WRONG_CHAIN_ID;
case REPLAY_PROTECTED_SIGNATURES_NOT_SUPPORTED:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

import static org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.CallParameterUtil.validateAndGetCallParams;

import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.datatypes.StateOverrideMap;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcErrorConverter;
Expand Down Expand Up @@ -80,9 +79,7 @@ protected Object pendingResult(final JsonRpcRequestContext requestContext) {
final var maybeStateOverrides = getAddressStateOverrideMap(requestContext);
final var pendingBlockHeader = transactionSimulator.simulatePendingBlockHeader();
final var minTxCost = getBlockchainQueries().getMinimumTransactionCost(pendingBlockHeader);
final var gasLimitUpperBound =
calculateGasLimitUpperBound(
callParameter, pendingBlockHeader.getParentHash(), pendingBlockHeader.getGasLimit());
final var gasLimitUpperBound = calculateGasLimitUpperBound(callParameter, pendingBlockHeader);
if (gasLimitUpperBound < minTxCost) {
return errorResponse(requestContext, RpcErrorType.TRANSACTION_UPFRONT_COST_EXCEEDS_BALANCE);
}
Expand Down Expand Up @@ -118,9 +115,7 @@ private Object resultByBlockHeader(
final var validationParams = getTransactionValidationParams(callParameter);
final var maybeStateOverrides = getAddressStateOverrideMap(requestContext);
final var minTxCost = getBlockchainQueries().getMinimumTransactionCost(blockHeader);
final var gasLimitUpperBound =
calculateGasLimitUpperBound(
callParameter, blockHeader.getHash(), blockHeader.getGasLimit());
final var gasLimitUpperBound = calculateGasLimitUpperBound(callParameter, blockHeader);
if (gasLimitUpperBound < minTxCost) {
return errorResponse(requestContext, RpcErrorType.TRANSACTION_UPFRONT_COST_EXCEEDS_BALANCE);
}
Expand Down Expand Up @@ -235,18 +230,24 @@ protected boolean attemptOptimisticSimulationWithMinimumBlockGasUsed(
}

private long calculateGasLimitUpperBound(
final CallParameter callParameters, final Hash blockHash, final long maxTxGasLimit) {
final CallParameter callParameters, final ProcessableBlockHeader blockHeader) {

final var maxTxGasLimit =
Math.min(
getBlockchainQueries().getTransactionGasLimitCap(blockHeader),
blockHeader.getGasLimit());

if (callParameters.getSender().isPresent() && callParameters.getStrict().orElse(Boolean.TRUE)) {
final var sender = callParameters.getSender().get();
final var maxGasPrice = calculateTxMaxGasPrice(callParameters);
if (!maxGasPrice.equals(Wei.ZERO)) {
final var maybeBalance = getBlockchainQueries().accountBalance(sender, blockHash);
final var maybeBalance =
getBlockchainQueries().accountBalance(sender, blockHeader.getParentHash());
if (maybeBalance.isEmpty() || maybeBalance.get().equals(Wei.ZERO)) {
return 0;
}
final var balance = maybeBalance.get();
final var value = callParameters.getValue().orElse(Wei.ZERO);
final var balanceForGas = value == null ? balance : balance.subtract(value);
final var balanceForGas = callParameters.getValue().map(balance::subtract).orElse(balance);
final var gasLimitForBalance = balanceForGas.divide(maxGasPrice);
return gasLimitForBalance.fitsLong()
? Math.min(gasLimitForBalance.toLong(), maxTxGasLimit)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ public enum RpcErrorType implements RpcMethodError {
INTRINSIC_GAS_EXCEEDS_LIMIT(-32003, "Intrinsic gas exceeds gas limit"),
TRANSACTION_UPFRONT_COST_EXCEEDS_BALANCE(-32004, "Upfront cost exceeds account balance"),
EXCEEDS_BLOCK_GAS_LIMIT(-32005, "Transaction gas limit exceeds block gas limit"),
EXCEEDS_TRANSACTION_GAS_LIMIT(-32005, "Transaction gas limit cap exceeded"),
EXCEEDS_RPC_MAX_BLOCK_RANGE(-32005, "Requested range exceeds maximum RPC range limit"),
EXCEEDS_RPC_MAX_BATCH_SIZE(-32005, "Number of requests exceeds max batch size"),
NONCE_TOO_HIGH(-32006, "Nonce too high"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -767,6 +767,13 @@ public long getMinimumTransactionCost(final ProcessableBlockHeader header) {
return protocolSchedule.getByBlockHeader(header).getGasCalculator().getMinimumTransactionCost();
}

public long getTransactionGasLimitCap(final ProcessableBlockHeader header) {
return protocolSchedule
.getByBlockHeader(header)
.getGasLimitCalculator()
.transactionGasLimitCap();
}

/**
* Calculates the blob gas price for data in a transaction.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@

import java.util.Map;
import java.util.Optional;
import java.util.OptionalLong;

import org.apache.tuweni.bytes.Bytes;
import org.junit.jupiter.api.BeforeEach;
Expand All @@ -76,13 +77,13 @@ public class EthCallTest {
private EthCall method;

@Mock private Blockchain blockchain;
@Mock ChainHead chainHead;
@Mock private ChainHead chainHead;
@Mock private BlockchainQueries blockchainQueries;
@Mock private TransactionSimulator transactionSimulator;

@Mock private BlockHeader blockHeader;

@Captor ArgumentCaptor<PreCloseStateHandler<Optional<JsonRpcResponse>>> mapperCaptor;
@Captor ArgumentCaptor<CallParameter> callParameterCaptor;

@BeforeEach
public void setUp() {
Expand Down Expand Up @@ -463,40 +464,79 @@ public void shouldReturnBlockNotFoundWhenInvalidBlockNumberSpecified() {

@Test
public void shouldAutoSelectIsAllowedExceedingBalanceToTrueWhenGasPriceIsZero() {
final CallParameter callParameters = callParameter(Wei.ZERO, null, null);
final CallParameter callParameters = callParameter();
internalAutoSelectIsAllowedExceedingBalance(callParameters, Optional.empty(), true);
}

@Test
public void shouldAutoSelectIsAllowedExceedingBalanceToTrueWhenGasPriceIsZeroAfterEIP1559() {
final CallParameter callParameters = callParameter(Wei.ZERO, null, null);
final CallParameter callParameters = callParameter();
internalAutoSelectIsAllowedExceedingBalance(callParameters, Optional.of(Wei.ONE), true);
}

@Test
public void shouldAutoSelectIsAllowedExceedingBalanceToFalseWhenGasPriceIsNotZero() {
final CallParameter callParameters = callParameter(Wei.ONE, null, null);
final CallParameter callParameters =
callParameter(
Optional.of(Wei.ONE), Optional.empty(), Optional.empty(), OptionalLong.empty());
internalAutoSelectIsAllowedExceedingBalance(callParameters, Optional.empty(), false);
}

@Test
public void shouldAutoSelectIsAllowedExceedingBalanceToFalseWhenGasPriceIsNotZeroAfterEIP1559() {
final CallParameter callParameters = callParameter(Wei.ONE, null, null);
final CallParameter callParameters =
callParameter(
Optional.of(Wei.ONE), Optional.empty(), Optional.empty(), OptionalLong.empty());
internalAutoSelectIsAllowedExceedingBalance(callParameters, Optional.of(Wei.ONE), false);
}

@Test
public void shouldAutoSelectIsAllowedExceedingBalanceToTrueWhenFeesAreZero() {
final CallParameter callParameters = callParameter(null, Wei.ZERO, Wei.ZERO);
final CallParameter callParameters =
callParameter(
Optional.empty(), Optional.of(Wei.ZERO), Optional.of(Wei.ZERO), OptionalLong.empty());
internalAutoSelectIsAllowedExceedingBalance(callParameters, Optional.of(Wei.ONE), true);
}

@Test
public void shouldAutoSelectIsAllowedExceedingBalanceToFalseWhenFeesAreZero() {
final CallParameter callParameters = callParameter(null, Wei.ONE, Wei.ONE);
final CallParameter callParameters =
callParameter(
Optional.empty(), Optional.of(Wei.ONE), Optional.of(Wei.ONE), OptionalLong.empty());
internalAutoSelectIsAllowedExceedingBalance(callParameters, Optional.of(Wei.ONE), false);
}

@Test
public void shouldAllowTxGasLimitBiggerThanCap() {
final long txGasLimitCap = 100_000L;
final long txGasLimitParameter = txGasLimitCap * 2;
final CallParameter callParameters =
callParameter(
Optional.empty(),
Optional.of(Wei.ONE),
Optional.of(Wei.ONE),
OptionalLong.of(txGasLimitParameter));
final JsonRpcRequestContext request = ethCallRequest(callParameters, "latest");
final JsonRpcResponse expectedResponse =
new JsonRpcSuccessResponse(null, Bytes.of(1).toString());
mockTransactionProcessorSuccessResult(expectedResponse);
when(blockchainQueries.getBlockchain()).thenReturn(blockchain);
when(blockchainQueries.getTransactionGasLimitCap(any())).thenReturn(txGasLimitCap);
when(blockchain.getChainHead()).thenReturn(chainHead);

final BlockHeader blockHeader = mock(BlockHeader.class);
when(blockHeader.getBaseFee()).thenReturn(Optional.of(Wei.ZERO));
when(chainHead.getBlockHeader()).thenReturn(blockHeader);

final JsonRpcResponse response = method.response(request);

verify(transactionSimulator)
.process(callParameterCaptor.capture(), eq(Optional.empty()), any(), any(), any(), any());

assertThat(callParameterCaptor.getValue().getGas()).hasValue(txGasLimitParameter);
assertThat(response).usingRecursiveComparison().isEqualTo(expectedResponse);
}

private void internalAutoSelectIsAllowedExceedingBalance(
final CallParameter callParameter,
final Optional<Wei> baseFee,
Expand All @@ -523,17 +563,22 @@ private void internalAutoSelectIsAllowedExceedingBalance(
}

private CallParameter callParameter() {
return callParameter(Wei.ZERO, null, null);
return callParameter(
Optional.of(Wei.ZERO), Optional.empty(), Optional.empty(), OptionalLong.empty());
}

private CallParameter callParameter(
final Wei gasPrice, final Wei maxFeesPerGas, final Wei maxPriorityFeesPerGas) {
final Optional<Wei> gasPrice,
final Optional<Wei> maxFeesPerGas,
final Optional<Wei> maxPriorityFeesPerGas,
final OptionalLong gasLimit) {
return ImmutableCallParameter.builder()
.sender(Address.fromHexString("0x0"))
.to(Address.fromHexString("0x0"))
.gasPrice(Optional.ofNullable(gasPrice))
.maxFeePerGas(Optional.ofNullable(maxFeesPerGas))
.maxPriorityFeePerGas(Optional.ofNullable(maxPriorityFeesPerGas))
.gasPrice(gasPrice)
.maxFeePerGas(maxFeesPerGas)
.maxPriorityFeePerGas(maxPriorityFeesPerGas)
.gas(gasLimit)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,12 @@
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
public class EthCreateAccessListTest {
private static final long MIN_TX_GAS_COST = 21_000L;
private static final long TX_GAS_LIMIT_CAP = 1_000_000L;
private static final long BLOCK_GAS_LIMIT = 2_000_000L;
private static final String METHOD = "eth_createAccessList";

private final String METHOD = "eth_createAccessList";
private EthCreateAccessList method;
private static final long MIN_TX_GAS_COST = 21_000L;

@Mock private BlockHeader latestBlockHeader;
@Mock private BlockHeader finalizedBlockHeader;
Expand All @@ -88,14 +90,15 @@ public void setUp() {
.thenReturn(Optional.of(finalizedBlockHeader));
when(blockchainQueries.getMinimumTransactionCost(any())).thenReturn(MIN_TX_GAS_COST);
when(blockchainQueries.accountBalance(any(), any())).thenReturn(Optional.of(Wei.MAX_WEI));
when(genesisBlockHeader.getGasLimit()).thenReturn(Long.MAX_VALUE);
when(blockchainQueries.getTransactionGasLimitCap(any())).thenReturn(Long.MAX_VALUE);
when(genesisBlockHeader.getGasLimit()).thenReturn(BLOCK_GAS_LIMIT);
when(genesisBlockHeader.getNumber()).thenReturn(0L);
when(finalizedBlockHeader.getGasLimit()).thenReturn(Long.MAX_VALUE);
when(finalizedBlockHeader.getGasLimit()).thenReturn(BLOCK_GAS_LIMIT);
when(finalizedBlockHeader.getNumber()).thenReturn(1L);
when(blockchain.getChainHeadHeader()).thenReturn(latestBlockHeader);
when(latestBlockHeader.getGasLimit()).thenReturn(Long.MAX_VALUE);
when(latestBlockHeader.getGasLimit()).thenReturn(BLOCK_GAS_LIMIT);
when(latestBlockHeader.getNumber()).thenReturn(2L);
when(pendingBlockHeader.getGasLimit()).thenReturn(Long.MAX_VALUE);
when(pendingBlockHeader.getGasLimit()).thenReturn(BLOCK_GAS_LIMIT);
when(pendingBlockHeader.getNumber()).thenReturn(3L);
when(transactionSimulator.simulatePendingBlockHeader()).thenReturn(pendingBlockHeader);
when(worldStateArchive.isWorldStateAvailable(any(), any())).thenReturn(true);
Expand Down Expand Up @@ -345,13 +348,39 @@ public void shouldReturnAccessListWhenBlockNumberParamIsPresent() {

// Set TransactionSimulator.process response
mockTransactionSimulatorResult(true, false, MIN_TX_GAS_COST, genesisBlockHeader);

assertThat(responseWithMockTracer(request, tracer))
.usingRecursiveComparison()
.isEqualTo(expectedResponse);
verify(transactionSimulator, times(1))
.process(any(), eq(Optional.empty()), any(), any(), eq(genesisBlockHeader));
}

@Test
public void shouldUseTxGasLimitCapIfLessThanBlockGasLimit() {
when(blockchainQueries.getTransactionGasLimitCap(any())).thenReturn(TX_GAS_LIMIT_CAP);

final JsonRpcRequestContext request =
ethCreateAccessListRequest(
eip1559TransactionCallParameter(Optional.empty(), null, Bytes.ofUnsignedLong(1L)));
// Generate a random list with one access list entry
final List<AccessListEntry> expectedAccessList = generateRandomAccessList();

// expect a list with the mocked access list
final JsonRpcResponse expectedResponse =
new JsonRpcSuccessResponse(
null, new CreateAccessListResult(expectedAccessList, TX_GAS_LIMIT_CAP));
final AccessListOperationTracer tracer = createMockTracer(expectedAccessList);

mockTransactionSimulatorResult(true, false, TX_GAS_LIMIT_CAP, pendingBlockHeader);

assertThat(responseWithMockTracer(request, tracer))
.usingRecursiveComparison()
.isEqualTo(expectedResponse);
verify(transactionSimulator, times(2))
.processOnPending(any(), eq(Optional.empty()), any(), any(), eq(pendingBlockHeader));
}

private JsonRpcResponse responseWithMockTracer(
final JsonRpcRequestContext request, final AccessListOperationTracer tracer) {
try (final MockedStatic<AccessListOperationTracer> tracerMockedStatic =
Expand All @@ -374,7 +403,14 @@ private void mockTransactionSimulatorResult(
final boolean isReverted,
final long estimateGas,
final BlockHeader blockHeader) {
final TransactionProcessingResult mockResult = mock(TransactionProcessingResult.class);
when(mockResult.getRevertReason())
.thenReturn(isReverted ? Optional.of(Bytes.of(0)) : Optional.empty());
final TransactionSimulatorResult mockTxSimResult = mock(TransactionSimulatorResult.class);
when(mockTxSimResult.result()).thenReturn(mockResult);
when(mockTxSimResult.isSuccessful()).thenReturn(isSuccessful);
when(mockTxSimResult.getGasEstimate()).thenReturn(estimateGas);

if (blockHeader == pendingBlockHeader) {
when(transactionSimulator.processOnPending(
any(), eq(Optional.empty()), any(), any(), eq(blockHeader)))
Expand All @@ -383,12 +419,6 @@ private void mockTransactionSimulatorResult(
when(transactionSimulator.process(any(), eq(Optional.empty()), any(), any(), eq(blockHeader)))
.thenReturn(Optional.of(mockTxSimResult));
}
final TransactionProcessingResult mockResult = mock(TransactionProcessingResult.class);
when(mockResult.getEstimateGasUsedByTransaction()).thenReturn(estimateGas);
when(mockResult.getRevertReason())
.thenReturn(isReverted ? Optional.of(Bytes.of(0)) : Optional.empty());
when(mockTxSimResult.result()).thenReturn(mockResult);
when(mockTxSimResult.isSuccessful()).thenReturn(isSuccessful);
}

private CallParameter legacyTransactionCallParameter(final Wei gasPrice) {
Expand All @@ -404,28 +434,30 @@ private CallParameter legacyTransactionCallParameter(final Wei gasPrice) {
}

private CallParameter eip1559TransactionCallParameter() {
return eip1559TransactionCallParameter(Optional.empty(), null);
return eip1559TransactionCallParameter(Optional.empty(), null, Bytes.EMPTY);
}

private CallParameter eip1559TransactionCallParameter(final Optional<Wei> gasPrice) {
return eip1559TransactionCallParameter(gasPrice, null);
return eip1559TransactionCallParameter(gasPrice, null, Bytes.EMPTY);
}

private CallParameter eip1559TransactionCallParameter(
final List<AccessListEntry> accessListEntries) {
return eip1559TransactionCallParameter(Optional.empty(), accessListEntries);
return eip1559TransactionCallParameter(Optional.empty(), accessListEntries, Bytes.EMPTY);
}

private CallParameter eip1559TransactionCallParameter(
final Optional<Wei> gasPrice, final List<AccessListEntry> accessListEntries) {
final Optional<Wei> gasPrice,
final List<AccessListEntry> accessListEntries,
final Bytes payload) {
return ImmutableCallParameter.builder()
.sender(Address.fromHexString("0x0"))
.to(Address.fromHexString("0x0"))
.gasPrice(gasPrice)
.maxFeePerGas(Wei.fromHexString("0x10"))
.maxPriorityFeePerGas(Wei.fromHexString("0x10"))
.value(Wei.ZERO)
.input(Bytes.EMPTY)
.input(payload)
.strict(false)
.accessList(Optional.ofNullable(accessListEntries))
.build();
Expand Down
Loading
Loading