From 74685b3638efd2d56f1b0d0d485d5e4f0f44935f Mon Sep 17 00:00:00 2001 From: Simon Dudley Date: Thu, 16 Jan 2025 12:49:57 +1000 Subject: [PATCH] Add PragueGasCalculator tests Signed-off-by: Simon Dudley --- .../gascalculator/FrontierGasCalculator.java | 36 ++--- .../gascalculator/PragueGasCalculator.java | 17 ++- .../FrontierGasCalculatorTest.java | 26 +++- .../PragueGasCalculatorTest.java | 135 ++++++++++++++++++ 4 files changed, 184 insertions(+), 30 deletions(-) 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 8d7112f0e4d..efa33ceeac6 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 @@ -138,12 +138,7 @@ public long transactionIntrinsicGasCost( if (dynamicIntrinsicGasCost == Long.MIN_VALUE || dynamicIntrinsicGasCost == Long.MAX_VALUE) { return dynamicIntrinsicGasCost; } - return clampedAdd(TX_BASE_COST, dynamicIntrinsicGasCost); - } - - @Override - public long transactionFloorCost(final Bytes payload) { - return 0L; + return clampedAdd(getMinimumTransactionCost(), dynamicIntrinsicGasCost); } /** @@ -222,6 +217,11 @@ protected long txCreateExtraGasCost() { return TX_CREATE_EXTRA_COST; } + @Override + public long transactionFloorCost(final Bytes payload) { + return 0L; + } + @Override public long codeDepositGasCost(final int codeSize) { return CODE_DEPOSIT_BYTE_COST * codeSize; @@ -637,33 +637,21 @@ public long getMinimumTransactionCost() { public long calculateGasRefund( final Transaction transaction, final MessageFrame initialFrame, - final long codeDelegationRefund) { + final long ignoredCodeDelegationRefund) { - final long refundAllowance = - calculateRefundAllowance(transaction, initialFrame, codeDelegationRefund); + final long refundAllowance = calculateRefundAllowance(transaction, initialFrame); return initialFrame.getRemainingGas() + refundAllowance; } - /** - * Calculate the refund allowance for a transaction. - * - * @param transaction the transaction - * @param initialFrame the initial frame - * @param codeDelegationRefund the code delegation refund - * @return the refund allowance - */ - protected long calculateRefundAllowance( - final Transaction transaction, - final MessageFrame initialFrame, - final long codeDelegationRefund) { + private long calculateRefundAllowance( + final Transaction transaction, final MessageFrame initialFrame) { final long selfDestructRefund = getSelfDestructRefundAmount() * initialFrame.getSelfDestructs().size(); - final long baseRefundGas = - initialFrame.getGasRefund() + selfDestructRefund + codeDelegationRefund; + final long executionRefund = initialFrame.getGasRefund() + selfDestructRefund; // Integer truncation takes care of the floor calculation needed after the divide. final long maxRefundAllowance = (transaction.getGasLimit() - initialFrame.getRemainingGas()) / getMaxRefundQuotient(); - return Math.min(maxRefundAllowance, baseRefundGas); + return Math.min(executionRefund, maxRefundAllowance); } } diff --git a/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/PragueGasCalculator.java b/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/PragueGasCalculator.java index f74288a487f..60f2b84938e 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/PragueGasCalculator.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/PragueGasCalculator.java @@ -81,6 +81,7 @@ public long calculateGasRefund( final Transaction transaction, final MessageFrame initialFrame, final long codeDelegationRefund) { + final long refundAllowance = calculateRefundAllowance(transaction, initialFrame, codeDelegationRefund); @@ -91,10 +92,24 @@ public long calculateGasRefund( return transaction.getGasLimit() - totalGasUsed; } + private long calculateRefundAllowance( + final Transaction transaction, + final MessageFrame initialFrame, + final long codeDelegationRefund) { + final long selfDestructRefund = + getSelfDestructRefundAmount() * initialFrame.getSelfDestructs().size(); + final long executionRefund = + initialFrame.getGasRefund() + selfDestructRefund + codeDelegationRefund; + // Integer truncation takes care of the floor calculation needed after the divide. + final long maxRefundAllowance = + (transaction.getGasLimit() - initialFrame.getRemainingGas()) / getMaxRefundQuotient(); + return Math.min(executionRefund, maxRefundAllowance); + } + @Override public long transactionFloorCost(final Bytes transactionPayload) { return clampedAdd( - TX_BASE_COST, + getMinimumTransactionCost(), tokensInCallData(transactionPayload.size(), zeroBytes(transactionPayload)) * TOTAL_COST_FLOOR_PER_TOKEN); } 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 index a657776b843..b0b949d3823 100644 --- a/evm/src/test/java/org/hyperledger/besu/evm/gascalculator/FrontierGasCalculatorTest.java +++ b/evm/src/test/java/org/hyperledger/besu/evm/gascalculator/FrontierGasCalculatorTest.java @@ -51,11 +51,11 @@ void shouldCalculateRefundWithNoSelfDestructs() { // Assert assertThat(refund) - .isEqualTo(6500L); // 5000 (remaining) + min(1500 (total refund), 19000 (max allowance)) + .isEqualTo(6000L); // 5000 (remaining) + min(1000 (execution refund), 47500 (max allowance)) } @Test - void shouldCalculateRefundWithMultipleSelfDestructs() { + void shouldCalculateRefundWithMultipleSelfDestructsAndIgnoreCodeDelegation() { // Arrange Set
selfDestructs = new HashSet<>(); selfDestructs.add(Address.wrap(Bytes.random(20))); @@ -71,7 +71,8 @@ void shouldCalculateRefundWithMultipleSelfDestructs() { // Assert assertThat(refund) - .isEqualTo(52500L); // 5000 (remaining) + min(47500 (total refund), 49500 (max allowance)) + .isEqualTo( + 52500L); // 5000 (remaining) + min(49500 (execution refund), 47500 (max allowance)) } @Test @@ -87,7 +88,8 @@ void shouldRespectMaxRefundAllowance() { // Assert assertThat(refund) - .isEqualTo(60000L); // 20000 (remaining) + min(101000 (total refund), 40000 (max allowance)) + .isEqualTo( + 60000L); // 20000 (remaining) + min(101000 (execution refund), 40000 (max allowance)) } @Test @@ -99,9 +101,23 @@ void shouldHandleZeroValuesCorrectly() { when(transaction.getGasLimit()).thenReturn(100000L); // Act - long refund = gasCalculator.calculateGasRefund(transaction, messageFrame, 0L); + long refund = + gasCalculator.calculateGasRefund( + transaction, + messageFrame, + 0L); // 0 (remaining) + min(0 (execution refund), 50000 (max allowance)) // Assert assertThat(refund).isEqualTo(0L); } + + @Test + void transactionFloorCostShouldAlwaysBeZero() { + assertThat(gasCalculator.transactionFloorCost(Bytes.EMPTY)).isEqualTo(0L); + assertThat(gasCalculator.transactionFloorCost(Bytes.random(256))).isEqualTo(0L); + assertThat(gasCalculator.transactionFloorCost(Bytes.repeat((byte) 0x0, Integer.MAX_VALUE))) + .isEqualTo(0L); + assertThat(gasCalculator.transactionFloorCost(Bytes.repeat((byte) 0x1, Integer.MAX_VALUE))) + .isEqualTo(0L); + } } diff --git a/evm/src/test/java/org/hyperledger/besu/evm/gascalculator/PragueGasCalculatorTest.java b/evm/src/test/java/org/hyperledger/besu/evm/gascalculator/PragueGasCalculatorTest.java index fdacad0e84e..e8ded745859 100644 --- a/evm/src/test/java/org/hyperledger/besu/evm/gascalculator/PragueGasCalculatorTest.java +++ b/evm/src/test/java/org/hyperledger/besu/evm/gascalculator/PragueGasCalculatorTest.java @@ -15,22 +15,36 @@ package org.hyperledger.besu.evm.gascalculator; import static org.assertj.core.api.Assertions.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.List; +import java.util.Set; +import org.apache.tuweni.bytes.Bytes; +import org.assertj.core.api.AssertionsForClassTypes; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; @TestInstance(TestInstance.Lifecycle.PER_CLASS) +@ExtendWith(MockitoExtension.class) class PragueGasCalculatorTest { private static final long TARGET_BLOB_GAS_PER_BLOCK_PRAGUE = 0xC0000; private final PragueGasCalculator pragueGasCalculator = new PragueGasCalculator(); + @Mock private Transaction transaction; + @Mock private MessageFrame messageFrame; @Test void testPrecompileSize() { @@ -66,4 +80,125 @@ Iterable blobGasses() { pragueGasCalculator.getBlobGasPerBlob()), Arguments.of(sixBlobTargetGas, newTargetCount, sixBlobTargetGas)); } + + @Test + void shouldCalculateRefundWithCodeDelegationAndNoSelfDestructs() { + // Arrange + when(messageFrame.getSelfDestructs()).thenReturn(Collections.emptySet()); + when(messageFrame.getGasRefund()).thenReturn(1000L); + when(messageFrame.getRemainingGas()).thenReturn(5000L); + when(transaction.getGasLimit()).thenReturn(100000L); + when(transaction.getPayload()).thenReturn(Bytes.EMPTY); + + // Act + long refund = pragueGasCalculator.calculateGasRefund(transaction, messageFrame, 500L); + + // Assert + // execution refund = 1000 + 0 (self destructs) + 500 (code delegation) = 1500 + AssertionsForClassTypes.assertThat(refund) + .isEqualTo(6500L); // 5000 (remaining) + min(1500 (execution 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); + when(transaction.getPayload()).thenReturn(Bytes.EMPTY); + + // Act + long refund = pragueGasCalculator.calculateGasRefund(transaction, messageFrame, 1000L); + + // Assert + // execution refund = 1000 + 0 (self destructs EIP-3529) + 1000 (code delegation) = 2000 + AssertionsForClassTypes.assertThat(refund) + .isEqualTo(7000L); // 5000 (remaining) + min(2000 (execution refund), 1900 (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); + when(transaction.getPayload()).thenReturn(Bytes.EMPTY); + + // Act + long refund = pragueGasCalculator.calculateGasRefund(transaction, messageFrame, 1000L); + + // Assert + // execution refund = 100000 + 1000 (code delegation) = 101000 + AssertionsForClassTypes.assertThat(refund) + .isEqualTo( + 36000L); // 20000 (remaining) + min(101000 (execution refund), 16000 (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); + when(transaction.getPayload()).thenReturn(Bytes.EMPTY); + + // Act + long refund = + pragueGasCalculator.calculateGasRefund( + transaction, + messageFrame, + 0L); // 0 (remaining) + min(0 (execution refund), 20000 (max allowance)) + + // Assert + AssertionsForClassTypes.assertThat(refund).isEqualTo(0L); + } + + @Test + void shouldRespectTransactionFloorCost() { + // Arrange + when(messageFrame.getSelfDestructs()).thenReturn(Collections.emptySet()); + when(messageFrame.getGasRefund()).thenReturn(100000L); + when(messageFrame.getRemainingGas()).thenReturn(90000L); + when(transaction.getGasLimit()).thenReturn(100000L); + when(transaction.getPayload()).thenReturn(Bytes.EMPTY); + + // Act + long refund = pragueGasCalculator.calculateGasRefund(transaction, messageFrame, 1000L); + + // Assert + // refund allowance = 16000 + // execution gas used = 100000 (gas limit) - 20000 (remaining gas) - 16000 (refund allowance) = + // 64000 + // floor cost = 21000 (base cost) + 0 (tokensInCallData * floor cost per token) = 21000 + AssertionsForClassTypes.assertThat(refund) + .isEqualTo( + 79000L); // 100000 (gas limit) - max(8000 (execution gas used), 21000 (floor cost)) + } + + @Test + void transactionFloorCostShouldBeAtLeastTransactionBaseCost() { + // floor cost = 21000 (base cost) + 0 + AssertionsForClassTypes.assertThat(pragueGasCalculator.transactionFloorCost(Bytes.EMPTY)) + .isEqualTo(21000); + // floor cost = 21000 (base cost) + 256 (tokensInCallData) * 10 (cost per token) + AssertionsForClassTypes.assertThat( + pragueGasCalculator.transactionFloorCost(Bytes.repeat((byte) 0x0, 256))) + .isEqualTo(23560L); + // floor cost = 21000 (base cost) + 256 * 4 (tokensInCallData) * 10 (cost per token) + AssertionsForClassTypes.assertThat( + pragueGasCalculator.transactionFloorCost(Bytes.repeat((byte) 0x1, 256))) + .isEqualTo(31240L); + // floor cost = 21000 (base cost) + 5 + (6 * 4) (tokensInCallData) * 10 (cost per token) + AssertionsForClassTypes.assertThat( + pragueGasCalculator.transactionFloorCost( + Bytes.fromHexString("0x0001000100010001000101"))) + .isEqualTo(21290L); + } }