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 6d0ff35a2a9..950c76a0300 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 @@ -518,9 +518,7 @@ public TransactionProcessingResult processTransaction( // For pre-Prague forks, floor cost is 0, so this returns just execution gas // For Prague+ forks with EIP-7778, this ensures block gas accounts for data floor // EIP-8037: Gas accounting with multidimensional gas support - final long floorCost = - gasCalculator.transactionFloorCost( - transaction.getPayload(), transaction.getPayloadZeroBytes()); + final long floorCost = gasCalculator.transactionFloorCost(transaction); final TransactionGasAccounting.GasResult gasResult = TransactionGasAccounting.builder() .txGasLimit(transaction.getGasLimit()) diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionValidator.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionValidator.java index af237e5a215..01fedd1fdbf 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionValidator.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionValidator.java @@ -261,8 +261,7 @@ private ValidationResult validateCostAndFee( final long intrinsicGasCostOrFloor = Math.max( gasCalculator.transactionIntrinsicGasCost(transaction, baselineGas), - gasCalculator.transactionFloorCost( - transaction.getPayload(), transaction.getPayloadZeroBytes())); + gasCalculator.transactionFloorCost(transaction)); if (Long.compareUnsigned(intrinsicGasCostOrFloor, transaction.getGasLimit()) > 0) { return ValidationResult.invalid( diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/AbstractBlockProcessorIntegrationTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/AbstractBlockProcessorIntegrationTest.java index 1d46305a59c..52fbf336515 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/AbstractBlockProcessorIntegrationTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/AbstractBlockProcessorIntegrationTest.java @@ -818,7 +818,7 @@ void processAccountReadThenUpdateTx(final BlockProcessor blockProcessor) { Block blockWithTransactions = createBlockWithTransactions( - "0x1f5501e9dccf8e5255e1fd4f4d0d215df77a979c60254813ac097bc3c4f0e673", + "0xa58e5254b57d4d748214c84a6ed2786b30325d951c4b322b33b0059506e5a97b", Wei.of(5), transactionTransfer, getcontractBalanceTransaction, @@ -886,7 +886,7 @@ void processAccountUpdateThenReadTx(final BlockProcessor blockProcessor) { Block blockWithTransactions = createBlockWithTransactions( - "0x2eeac8f0934e08deba88bea320eb0400101654cba1f10d5ff21d5f8186cb3789", + "0xfaaa10fddc61e91833311e3d14001e2f39b317022c41329074cc7e12e4a8fd68", Wei.of(5), transactionTransfer, sendEthFromContractTransaction, @@ -953,7 +953,7 @@ void processAccountReadThenUpdateTxWithTwoAccounts(final BlockProcessor blockPro Block blockWithTransactions = createBlockWithTransactions( - "0x7cf1e5035b25f95eb728b150992179afa5445bcdcba371ed16198ba3f909877e", + "0x9ac394b9d9927ee863719a706ddbfebce4c6462152836c3c70a84f3b538b2048", Wei.of(5), transactionTransfer, getcontractBalanceTransaction, @@ -1022,7 +1022,7 @@ void processAccountUpdateThenReadTxWithTwoAccounts(final BlockProcessor blockPro Block blockWithTransactions = createBlockWithTransactions( - "0x7cf1e5035b25f95eb728b150992179afa5445bcdcba371ed16198ba3f909877e", + "0x9ac394b9d9927ee863719a706ddbfebce4c6462152836c3c70a84f3b538b2048", Wei.of(5), transactionTransfer, sendEthFromContractTransaction, diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionValidatorTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionValidatorTest.java index c1e65ca4b68..f94ac98bd34 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionValidatorTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionValidatorTest.java @@ -158,7 +158,8 @@ public void shouldRejectTransactionIfFloorExceedsGasLimit_EIP_7623() { .chainId(Optional.empty()) .createTransaction(senderKeys); when(gasCalculator.transactionIntrinsicGasCost(any(), anyLong())).thenReturn(5L); - when(gasCalculator.transactionFloorCost(any(), anyLong())).thenReturn(51L); + when(gasCalculator.transactionFloorCost(any(org.hyperledger.besu.datatypes.Transaction.class))) + .thenReturn(51L); assertThat( validator.validate( diff --git a/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/AmsterdamGasCalculator.java b/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/AmsterdamGasCalculator.java index 4dc45ecf14b..a1026049ca4 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/AmsterdamGasCalculator.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/AmsterdamGasCalculator.java @@ -16,12 +16,14 @@ import static org.hyperledger.besu.evm.internal.Words.clampedAdd; +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; +import java.util.List; import java.util.function.Supplier; import org.apache.tuweni.bytes.Bytes; @@ -38,13 +40,19 @@ * */ public class AmsterdamGasCalculator extends OsakaGasCalculator { - // EIP-7976: floor cost of 64 gas per calldata byte (zero and non-zero alike) + // EIP-7976 / EIP-7981: floor cost of 64 gas per data byte (calldata or access list). private static final long TOTAL_COST_FLOOR_PER_BYTE = 64L; + // EIP-7981: data floor contribution of an access list entry. + // 20 address bytes * 64 gas/byte = 1280; each 32-byte storage key * 64 gas/byte = 2048. + private static final long ACCESS_LIST_ADDRESS_FLOOR_COST = 20L * TOTAL_COST_FLOOR_PER_BYTE; + private static final long ACCESS_LIST_STORAGE_KEY_FLOOR_COST = 32L * TOTAL_COST_FLOOR_PER_BYTE; + // EIP-8037: New regular gas constants for Amsterdam private static final long TX_CREATE_COST = 9_000L; @@ -110,6 +118,34 @@ public long transactionFloorCost(final Bytes transactionPayload, final long payl getMinimumTransactionCost(), transactionPayload.size() * TOTAL_COST_FLOOR_PER_BYTE); } + @Override + public long transactionFloorCost(final Transaction transaction) { + // EIP-7981: include access list bytes in the data floor so they can't be used to bypass it. + final long calldataBytes = transaction.getPayload().size(); + final long accessListBytes = + transaction.getAccessList().map(AmsterdamGasCalculator::accessListBytes).orElse(0L); + return clampedAdd( + getMinimumTransactionCost(), (calldataBytes + accessListBytes) * TOTAL_COST_FLOOR_PER_BYTE); + } + + @Override + public long accessListGasCost(final int addresses, final int storageSlots) { + // EIP-2930 baseline (2400 per address, 1900 per key) plus EIP-7981 data floor + // (1280 per address, 2048 per storage key), so access list data is always charged + // at floor rate regardless of which branch of the gasUsed max() wins. + return super.accessListGasCost(addresses, storageSlots) + + addresses * ACCESS_LIST_ADDRESS_FLOOR_COST + + storageSlots * ACCESS_LIST_STORAGE_KEY_FLOOR_COST; + } + + private static long accessListBytes(final List accessList) { + long bytes = 0L; + for (final AccessListEntry entry : accessList) { + bytes += 20L + 32L * entry.storageKeys().size(); + } + return bytes; + } + // --- EIP-8037 Gas Cost Overrides --- @Override @@ -203,8 +239,7 @@ public long calculateGasRefund( final long refundAllowance = Math.min(executionRefund, maxRefundAllowance); final long gasUsed = totalConsumed - refundAllowance; - final long floorCost = - transactionFloorCost(transaction.getPayload(), transaction.getPayloadZeroBytes()); + final long floorCost = transactionFloorCost(transaction); return gasLimit - Math.max(gasUsed, floorCost); } 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 97e8ebecd41..2f772909162 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 @@ -542,6 +542,18 @@ default long modExpGasCost(final Bytes input) { */ long transactionFloorCost(final Bytes transactionPayload, final long payloadZeroBytes); + /** + * Returns the floor gas cost of a transaction. Unlike the payload-only overload, this variant can + * incorporate additional transaction-level data (e.g. access list bytes under EIP-7981) when + * computing the floor. + * + * @param transaction The transaction + * @return the transaction's floor gas cost + */ + default long transactionFloorCost(final Transaction transaction) { + return transactionFloorCost(transaction.getPayload(), transaction.getPayloadZeroBytes()); + } + /** * Returns the gas cost of the explicitly declared access list. * diff --git a/evm/src/test/java/org/hyperledger/besu/evm/gascalculator/AmsterdamGasCalculatorTest.java b/evm/src/test/java/org/hyperledger/besu/evm/gascalculator/AmsterdamGasCalculatorTest.java index 95a82c799d3..6fc49148523 100644 --- a/evm/src/test/java/org/hyperledger/besu/evm/gascalculator/AmsterdamGasCalculatorTest.java +++ b/evm/src/test/java/org/hyperledger/besu/evm/gascalculator/AmsterdamGasCalculatorTest.java @@ -15,14 +15,29 @@ 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.AccessListEntry; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Transaction; + +import java.util.List; +import java.util.Optional; import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; 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 AmsterdamGasCalculatorTest { private final AmsterdamGasCalculator amsterdamGasCalculator = new AmsterdamGasCalculator(); + @Mock private Transaction transaction; + @Test void transactionFloorCostShouldBeAtLeastTransactionBaseCost() { // floor cost = 21000 (base cost) + 0 @@ -39,4 +54,38 @@ void transactionFloorCostShouldBeAtLeastTransactionBaseCost() { Bytes.fromHexString("0x0001000100010001000101"), 5)) .isEqualTo(21704L); } + + @Test + void accessListGasCostIncludesDataFloor() { + // EIP-2930: 2400/address + 1900/key; EIP-7981: +1280/address + 2048/key + // One address + zero keys = 2400 + 1280 = 3680 + assertThat(amsterdamGasCalculator.accessListGasCost(1, 0)).isEqualTo(3680L); + // One address + one key = 3680 + 1900 + 2048 = 7628 + assertThat(amsterdamGasCalculator.accessListGasCost(1, 1)).isEqualTo(7628L); + // Three addresses + five keys = 3*3680 + 5*(1900+2048) = 11040 + 19740 = 30780 + assertThat(amsterdamGasCalculator.accessListGasCost(3, 5)).isEqualTo(30780L); + } + + @Test + void transactionFloorCostWithoutAccessListMatchesCalldataOnlyFloor() { + when(transaction.getPayload()).thenReturn(Bytes.repeat((byte) 0x1, 256)); + when(transaction.getAccessList()).thenReturn(Optional.empty()); + + // 21000 + 256 * 64 = 37384 + assertThat(amsterdamGasCalculator.transactionFloorCost(transaction)).isEqualTo(37384L); + } + + @Test + void transactionFloorCostIncludesAccessListBytes() { + // 10 calldata bytes + 1 address (20 bytes) + 2 keys (2*32 = 64 bytes) = 94 bytes + // 21000 + 94 * 64 = 21000 + 6016 = 27016 + final AccessListEntry entry = + new AccessListEntry( + Address.fromHexString("0x00000000000000000000000000000000000000aa"), + List.of(Bytes32.ZERO, Bytes32.ZERO)); + when(transaction.getPayload()).thenReturn(Bytes.repeat((byte) 0x1, 10)); + when(transaction.getAccessList()).thenReturn(Optional.of(List.of(entry))); + + assertThat(amsterdamGasCalculator.transactionFloorCost(transaction)).isEqualTo(27016L); + } }