diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionProcessor.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionProcessor.java index f559ec327a6..2ad18216ee0 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionProcessor.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionProcessor.java @@ -500,11 +500,8 @@ public TransactionProcessingResult processTransaction( // Refund the sender by what we should and pay the miner fee (note that we're doing them one // after the other so that if it is the same account somehow, we end up with the right result) - final long selfDestructRefund = - gasCalculator.getSelfDestructRefundAmount() * initialFrame.getSelfDestructs().size(); - final long baseRefundGas = - initialFrame.getGasRefund() + selfDestructRefund + codeDelegationRefund; - final long refundedGas = refunded(transaction, initialFrame.getRemainingGas(), baseRefundGas); + final long refundedGas = + gasCalculator.calculateGasRefund(transaction, initialFrame, codeDelegationRefund); final Wei refundedWei = transactionGasPrice.multiply(refundedGas); final Wei balancePriorToRefund = sender.getBalance(); sender.incrementBalance(refundedWei); @@ -635,15 +632,6 @@ public AbstractMessageProcessor getMessageProcessor(final MessageFrame.Type type }; } - protected long refunded( - final Transaction transaction, final long gasRemaining, final long gasRefund) { - // Integer truncation takes care of the floor calculation needed after the divide. - final long maxRefundAllowance = - (transaction.getGasLimit() - gasRemaining) / gasCalculator.getMaxRefundQuotient(); - final long refundAllowance = Math.min(maxRefundAllowance, gasRefund); - return gasRemaining + refundAllowance; - } - private String printableStackTraceFromThrowable(final RuntimeException re) { final StringBuilder builder = new StringBuilder(); diff --git a/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/FrontierGasCalculator.java b/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/FrontierGasCalculator.java index 0376903c64a..8db00c3dd51 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/FrontierGasCalculator.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/FrontierGasCalculator.java @@ -21,6 +21,7 @@ import static org.hyperledger.besu.evm.internal.Words.numWords; import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Transaction; import org.hyperledger.besu.datatypes.Wei; import org.hyperledger.besu.evm.account.Account; import org.hyperledger.besu.evm.frame.MessageFrame; @@ -566,4 +567,25 @@ public long getMaximumTransactionCost(final int size) { public long getMinimumTransactionCost() { return TX_BASE_COST; } + + @Override + public long calculateGasRefund( + final Transaction transaction, + final MessageFrame initialFrame, + final long codeDelegationRefund) { + final long selfDestructRefund = + getSelfDestructRefundAmount() * initialFrame.getSelfDestructs().size(); + final long baseRefundGas = + initialFrame.getGasRefund() + selfDestructRefund + codeDelegationRefund; + return refunded(transaction, initialFrame.getRemainingGas(), baseRefundGas); + } + + private long refunded( + final Transaction transaction, final long gasRemaining, final long gasRefund) { + // Integer truncation takes care of the floor calculation needed after the divide. + final long maxRefundAllowance = + (transaction.getGasLimit() - gasRemaining) / getMaxRefundQuotient(); + final long refundAllowance = Math.min(maxRefundAllowance, gasRefund); + return gasRemaining + refundAllowance; + } } diff --git a/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/GasCalculator.java b/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/GasCalculator.java index 8192bd1af88..ec8537cf9e3 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/GasCalculator.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/GasCalculator.java @@ -16,6 +16,7 @@ import org.hyperledger.besu.datatypes.AccessListEntry; import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Transaction; import org.hyperledger.besu.datatypes.Wei; import org.hyperledger.besu.evm.account.Account; import org.hyperledger.besu.evm.frame.MessageFrame; @@ -664,4 +665,15 @@ default long delegateCodeGasCost(final int delegateCodeListLength) { default long calculateDelegateCodeGasRefund(final long alreadyExistingAccountSize) { return 0L; } + + /** + * Calculate the gas refund for a transaction. + * + * @param transaction the transaction + * @param initialFrame the initial frame + * @param codeDelegationRefund the code delegation refund + * @return the gas refund + */ + long calculateGasRefund( + Transaction transaction, MessageFrame initialFrame, long codeDelegationRefund); } diff --git a/evm/src/test/java/org/hyperledger/besu/evm/gascalculator/FrontierGasCalculatorTest.java b/evm/src/test/java/org/hyperledger/besu/evm/gascalculator/FrontierGasCalculatorTest.java new file mode 100644 index 00000000000..a657776b843 --- /dev/null +++ b/evm/src/test/java/org/hyperledger/besu/evm/gascalculator/FrontierGasCalculatorTest.java @@ -0,0 +1,107 @@ +/* + * 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.evm.gascalculator; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.Mockito.when; + +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Transaction; +import org.hyperledger.besu.evm.frame.MessageFrame; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.apache.tuweni.bytes.Bytes; +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) +public class FrontierGasCalculatorTest { + private final GasCalculator gasCalculator = new FrontierGasCalculator(); + + @Mock private Transaction transaction; + @Mock private MessageFrame messageFrame; + + @Test + void shouldCalculateRefundWithNoSelfDestructs() { + // Arrange + when(messageFrame.getSelfDestructs()).thenReturn(Collections.emptySet()); + when(messageFrame.getGasRefund()).thenReturn(1000L); + when(messageFrame.getRemainingGas()).thenReturn(5000L); + when(transaction.getGasLimit()).thenReturn(100000L); + + // Act + long refund = gasCalculator.calculateGasRefund(transaction, messageFrame, 500L); + + // Assert + assertThat(refund) + .isEqualTo(6500L); // 5000 (remaining) + min(1500 (total refund), 19000 (max allowance)) + } + + @Test + void shouldCalculateRefundWithMultipleSelfDestructs() { + // Arrange + Set
selfDestructs = new HashSet<>(); + selfDestructs.add(Address.wrap(Bytes.random(20))); + selfDestructs.add(Address.wrap(Bytes.random(20))); + + when(messageFrame.getSelfDestructs()).thenReturn(selfDestructs); + when(messageFrame.getGasRefund()).thenReturn(1000L); + when(messageFrame.getRemainingGas()).thenReturn(5000L); + when(transaction.getGasLimit()).thenReturn(100000L); + + // Act + long refund = gasCalculator.calculateGasRefund(transaction, messageFrame, 500L); + + // Assert + assertThat(refund) + .isEqualTo(52500L); // 5000 (remaining) + min(47500 (total refund), 49500 (max allowance)) + } + + @Test + void shouldRespectMaxRefundAllowance() { + // Arrange + when(messageFrame.getSelfDestructs()).thenReturn(Collections.emptySet()); + when(messageFrame.getGasRefund()).thenReturn(100000L); + when(messageFrame.getRemainingGas()).thenReturn(20000L); + when(transaction.getGasLimit()).thenReturn(100000L); + + // Act + long refund = gasCalculator.calculateGasRefund(transaction, messageFrame, 1000L); + + // Assert + assertThat(refund) + .isEqualTo(60000L); // 20000 (remaining) + min(101000 (total refund), 40000 (max allowance)) + } + + @Test + void shouldHandleZeroValuesCorrectly() { + // Arrange + when(messageFrame.getSelfDestructs()).thenReturn(Collections.emptySet()); + when(messageFrame.getGasRefund()).thenReturn(0L); + when(messageFrame.getRemainingGas()).thenReturn(0L); + when(transaction.getGasLimit()).thenReturn(100000L); + + // Act + long refund = gasCalculator.calculateGasRefund(transaction, messageFrame, 0L); + + // Assert + assertThat(refund).isEqualTo(0L); + } +}