diff --git a/CHANGELOG.md b/CHANGELOG.md index 676f53cfbde..5d982f5465b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ - Fix QBFT `RLPException` when decoding proposals from pre-26.1.0 nodes that do not include the `blockAccessList` field [#9977](https://github.com/hyperledger/besu/pull/9977) - Fix eth_simulateV1 discrepancy [9960] (https://github.com/besu-eth/besu/issues/9960) eth_simulateV1 now accepts calls where both input and data are provided with different values, using input as per the execution-apis spec instead of returning an error. +- Fix eth_simulateV1 returning wrong error code when transaction gas exceeds block gas limit: now correctly returns -38015 (BLOCK_GAS_LIMIT_EXCEEDED) instead of -38014 or succeeding [#10073](https://github.com/besu-eth/besu/pull/10073) - Wait for peers before starting chain download. Prevents an OutOfMemory (OOM) error when the node has zero peers [#9979](https://github.com/hyperledger/besu/pull/9979) ### Additions and Improvements diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockSimulator.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockSimulator.java index d7dc6e0967b..2279a766652 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockSimulator.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockSimulator.java @@ -377,6 +377,14 @@ protected BlockStateCallSimulationResult processTransactions( } } + if (callParameter.getGas().isPresent() + && callParameter.getGas().getAsLong() + > blockStateCallSimulationResult.getRemainingGas()) { + throw new BlockStateCallException( + BlockStateCallError.BLOCK_GAS_LIMIT_EXCEEDED.getMessage(), + BlockStateCallError.BLOCK_GAS_LIMIT_EXCEEDED); + } + long gasLimit = transactionSimulator.calculateSimulationGasCap( blockHeader, diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/exceptions/BlockStateCallError.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/exceptions/BlockStateCallError.java index 1c4de32d86e..1cd6f703f04 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/exceptions/BlockStateCallError.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/exceptions/BlockStateCallError.java @@ -32,6 +32,8 @@ public enum BlockStateCallError { DUPLICATED_PRECOMPILE_TARGET(-38023, "Duplicated move precompile target"), /** The nonce is invalid. */ INVALID_NONCES(-32602, "Invalid nonces"), + /** Block gas limit exceeded by the block's transactions. */ + BLOCK_GAS_LIMIT_EXCEEDED(-38015, "Transaction gas exceeds block gas limit"), /** Upfront cost exceeds balance. */ UPFRONT_COST_EXCEEDS_BALANCE(-38014, "Upfront cost exceeds balance"), /** Gas price too low. */ diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/transaction/BlockSimulatorTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/transaction/BlockSimulatorTest.java index ad92b1eecac..0eaba2b13dd 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/transaction/BlockSimulatorTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/transaction/BlockSimulatorTest.java @@ -29,6 +29,7 @@ import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.datatypes.StateOverride; import org.hyperledger.besu.datatypes.StateOverrideMap; +import org.hyperledger.besu.datatypes.TransactionType; import org.hyperledger.besu.datatypes.Wei; import org.hyperledger.besu.datatypes.parameters.UnsignedLongParameter; import org.hyperledger.besu.ethereum.GasLimitCalculator; @@ -38,12 +39,16 @@ import org.hyperledger.besu.ethereum.core.Difficulty; import org.hyperledger.besu.ethereum.core.MiningConfiguration; import org.hyperledger.besu.ethereum.core.MutableWorldState; +import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.ethereum.core.TransactionReceipt; +import org.hyperledger.besu.ethereum.mainnet.AbstractBlockProcessor; import org.hyperledger.besu.ethereum.mainnet.MiningBeneficiaryCalculator; import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec; import org.hyperledger.besu.ethereum.mainnet.ValidationResult; import org.hyperledger.besu.ethereum.mainnet.blockhash.PreExecutionProcessor; import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket; +import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; import org.hyperledger.besu.ethereum.transaction.exceptions.BlockStateCallError; import org.hyperledger.besu.ethereum.transaction.exceptions.BlockStateCallException; import org.hyperledger.besu.ethereum.trie.pathbased.common.provider.WorldStateQueryParams; @@ -57,6 +62,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.OptionalLong; import java.util.Set; import org.apache.tuweni.bytes.Bytes; @@ -145,6 +151,7 @@ public void shouldStopWhenTransactionSimulationIsInvalid() { when(mutableWorldState.updater()).thenReturn(updater); CallParameter callParameter = mock(CallParameter.class); + when(callParameter.getGas()).thenReturn(OptionalLong.empty()); BlockStateCall blockStateCall = new BlockStateCall(List.of(callParameter), null, null); TransactionSimulatorResult transactionSimulatorResult = mock(TransactionSimulatorResult.class); @@ -179,6 +186,7 @@ public void shouldStopWhenTransactionSimulationIsEmpty() { when(mutableWorldState.updater()).thenReturn(updater); CallParameter callParameter = mock(CallParameter.class); + when(callParameter.getGas()).thenReturn(OptionalLong.empty()); BlockStateCall blockStateCall = new BlockStateCall(List.of(callParameter), null, null); when(transactionSimulator.processWithWorldUpdater( @@ -307,6 +315,120 @@ public void shouldAllowDuplicatePrecompileTargetAddresses() { assertThat(validationResult).isEmpty(); } + @Test + public void shouldThrowBlockGasLimitExceededWhenTxGasExceedsBlockLimitWithValidationDisabled() { + when(mutableWorldState.updater()).thenReturn(updater); + + // gasLimitCalculator.nextGasLimit returns 1L (from setUp), so block gas limit = 1 + CallParameter callParameter = mock(CallParameter.class); + when(callParameter.getGas()).thenReturn(OptionalLong.of(1_000_000L)); + BlockStateCall blockStateCall = new BlockStateCall(List.of(callParameter), null, null); + + BlockSimulationParameter parameter = + new BlockSimulationParameter.BlockSimulationParameterBuilder() + .blockStateCalls(List.of(blockStateCall)) + .validation(false) + .build(); + + BlockStateCallException exception = + assertThrows( + BlockStateCallException.class, + () -> blockSimulator.process(blockHeader, parameter, mutableWorldState)); + + assertThat(exception.getError()).isEqualTo(BlockStateCallError.BLOCK_GAS_LIMIT_EXCEEDED); + assertThat(exception.getError().getCode()).isEqualTo(-38015); + } + + @Test + public void shouldThrowBlockGasLimitExceededWhenTxGasExceedsBlockLimitWithValidationEnabled() { + when(mutableWorldState.updater()).thenReturn(updater); + + // gasLimitCalculator.nextGasLimit returns 1L (from setUp), so block gas limit = 1 + CallParameter callParameter = mock(CallParameter.class); + when(callParameter.getGas()).thenReturn(OptionalLong.of(1_000_000L)); + BlockStateCall blockStateCall = new BlockStateCall(List.of(callParameter), null, null); + + BlockSimulationParameter parameter = + new BlockSimulationParameter.BlockSimulationParameterBuilder() + .blockStateCalls(List.of(blockStateCall)) + .validation(true) + .build(); + + BlockStateCallException exception = + assertThrows( + BlockStateCallException.class, + () -> blockSimulator.process(blockHeader, parameter, mutableWorldState)); + + assertThat(exception.getError()).isEqualTo(BlockStateCallError.BLOCK_GAS_LIMIT_EXCEEDED); + assertThat(exception.getError().getCode()).isEqualTo(-38015); + } + + @Test + public void + shouldThrowBlockGasLimitExceededWhenSecondTxGasExceedsRemainingAfterFirstTxConsumed() { + // Block gas limit = 30,000. First tx consumes 21,000 (leaving 9,000 remaining). + // Second tx explicitly requests 10,000 gas, which exceeds the 9,000 remaining. + GasLimitCalculator gasLimitCalculator = mock(GasLimitCalculator.class); + when(protocolSpec.getGasLimitCalculator()).thenReturn(gasLimitCalculator); + when(gasLimitCalculator.nextGasLimit(anyLong(), anyLong(), anyLong())).thenReturn(30_000L); + when(gasLimitCalculator.computeExcessBlobGas(anyLong(), anyLong(), anyLong())).thenReturn(0L); + + WorldUpdater transactionUpdater = mock(WorldUpdater.class); + when(mutableWorldState.updater()).thenReturn(updater); + when(updater.updater()).thenReturn(transactionUpdater); + + CallParameter firstCallParam = mock(CallParameter.class); + when(firstCallParam.getGas()).thenReturn(OptionalLong.empty()); + when(firstCallParam.getGasPrice()).thenReturn(Optional.empty()); + when(firstCallParam.getMaxFeePerGas()).thenReturn(Optional.empty()); + when(firstCallParam.getMaxPriorityFeePerGas()).thenReturn(Optional.empty()); + + CallParameter secondCallParam = mock(CallParameter.class); + when(secondCallParam.getGas()).thenReturn(OptionalLong.of(10_000L)); + + BlockStateCall blockStateCall = + new BlockStateCall(List.of(firstCallParam, secondCallParam), null, null); + + Transaction tx = mock(Transaction.class); + when(tx.getType()).thenReturn(TransactionType.FRONTIER); + + TransactionProcessingResult processingResult = mock(TransactionProcessingResult.class); + when(processingResult.getPartialBlockAccessView()).thenReturn(Optional.empty()); + + TransactionSimulatorResult firstTxResult = mock(TransactionSimulatorResult.class); + when(firstTxResult.isInvalid()).thenReturn(false); + when(firstTxResult.getGasEstimate()).thenReturn(21_000L); + when(firstTxResult.transaction()).thenReturn(tx); + when(firstTxResult.result()).thenReturn(processingResult); + + when(transactionSimulator.processWithWorldUpdater( + any(), any(), any(), any(), any(), any(), any(), anyLong(), any(), any(), any(), any(), + any())) + .thenReturn(Optional.of(firstTxResult)); + + AbstractBlockProcessor.TransactionReceiptFactory receiptFactory = + mock(AbstractBlockProcessor.TransactionReceiptFactory.class); + when(protocolSpec.getTransactionReceiptFactory()).thenReturn(receiptFactory); + TransactionReceipt receipt = mock(TransactionReceipt.class); + when(receipt.getLogsList()).thenReturn(List.of()); + when(receiptFactory.create(any(TransactionType.class), any(), any(), anyLong())) + .thenReturn(receipt); + + BlockSimulationParameter parameter = + new BlockSimulationParameter.BlockSimulationParameterBuilder() + .blockStateCalls(List.of(blockStateCall)) + .validation(false) + .build(); + + BlockStateCallException exception = + assertThrows( + BlockStateCallException.class, + () -> blockSimulator.process(blockHeader, parameter, mutableWorldState)); + + assertThat(exception.getError()).isEqualTo(BlockStateCallError.BLOCK_GAS_LIMIT_EXCEEDED); + assertThat(exception.getError().getCode()).isEqualTo(-38015); + } + private BlockSimulationParameter createSimulationParameter(final BlockStateCall blockStateCall) { return new BlockSimulationParameter.BlockSimulationParameterBuilder() .blockStateCalls(List.of(blockStateCall))