From 1e7f6f6bf3e05be961422427110a3e9f7c2fde94 Mon Sep 17 00:00:00 2001 From: daniellehrner Date: Thu, 16 Jan 2025 11:09:04 +0100 Subject: [PATCH] EIP-7623 (#8093) Implements EIP-7623 (increase calldata cost) Signed-off-by: Daniel Lehrner Signed-off-by: Simon Dudley Co-authored-by: Simon Dudley --- CHANGELOG.md | 1 + .../eea/PluginEeaSendRawTransaction.java | 2 +- .../mainnet/MainnetTransactionProcessor.java | 19 +-- .../mainnet/MainnetTransactionValidator.java | 20 ++- .../ethereum/mainnet/IntrinsicGasTest.java | 51 ++++++- .../MainnetTransactionValidatorTest.java | 28 +++- .../besu/evmtool/EvmToolCommand.java | 16 ++- .../hyperledger/besu/evmtool/T8nExecutor.java | 2 +- .../besu/evmtool/t8n/prague-deposit.json | 6 +- .../besu/ethereum/core/TransactionTest.java | 11 +- .../gascalculator/FrontierGasCalculator.java | 112 ++++++++++++--- .../besu/evm/gascalculator/GasCalculator.java | 24 ++-- .../gascalculator/IstanbulGasCalculator.java | 25 +--- .../gascalculator/PragueGasCalculator.java | 50 +++++++ .../gascalculator/ShanghaiGasCalculator.java | 12 +- .../FrontierGasCalculatorTest.java | 26 +++- .../PragueGasCalculatorTest.java | 135 ++++++++++++++++++ 17 files changed, 433 insertions(+), 107 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff0185dac..ec39f72e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ - Improve debug_traceBlock calls performance and reduce output size [#8076](https://github.com/hyperledger/besu/pull/8076) - Add support for EIP-7702 transaction in the txpool [#8018](https://github.com/hyperledger/besu/pull/8018) [#7984](https://github.com/hyperledger/besu/pull/7984) - Add support for `movePrecompileToAddress` in `StateOverrides` (`eth_call`)[8115](https://github.com/hyperledger/besu/pull/8115) +- Add EIP-7623 - Increase calldata cost [#8093](https://github.com/hyperledger/besu/pull/8093) ### Bug fixes - Fix serialization of state overrides when `movePrecompileToAddress` is present [#8204](https://github.com/hyperledger/besu/pull/8024) diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/privacy/methods/eea/PluginEeaSendRawTransaction.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/privacy/methods/eea/PluginEeaSendRawTransaction.java index 146ef2c72..2d6af314f 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/privacy/methods/eea/PluginEeaSendRawTransaction.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/privacy/methods/eea/PluginEeaSendRawTransaction.java @@ -84,6 +84,6 @@ public class PluginEeaSendRawTransaction extends AbstractEeaSendRawTransaction { // choose the highest of the two options return Math.max( privateTransaction.getGasLimit(), - gasCalculator.transactionIntrinsicGasCost(Bytes.fromBase64String(pmtPayload), false)); + gasCalculator.transactionIntrinsicGasCost(Bytes.fromBase64String(pmtPayload), false, 0)); } } 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 46c71b435..a25267079 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 @@ -18,6 +18,7 @@ import static org.hyperledger.besu.ethereum.mainnet.PrivateStateUtils.KEY_IS_PER import static org.hyperledger.besu.ethereum.mainnet.PrivateStateUtils.KEY_PRIVATE_METADATA_UPDATER; import static org.hyperledger.besu.ethereum.mainnet.PrivateStateUtils.KEY_TRANSACTION; import static org.hyperledger.besu.ethereum.mainnet.PrivateStateUtils.KEY_TRANSACTION_HASH; +import static org.hyperledger.besu.evm.internal.Words.clampedAdd; import org.hyperledger.besu.collections.trie.BytesTrieSet; import org.hyperledger.besu.datatypes.AccessListEntry; @@ -351,22 +352,22 @@ public class MainnetTransactionProcessor { warmAddressList.add(miningBeneficiary); } - final long intrinsicGas = - gasCalculator.transactionIntrinsicGasCost( - transaction.getPayload(), transaction.isContractCreation()); final long accessListGas = gasCalculator.accessListGasCost(accessListEntries.size(), accessListStorageCount); final long codeDelegationGas = gasCalculator.delegateCodeGasCost(transaction.codeDelegationListSize()); - final long gasAvailable = - transaction.getGasLimit() - intrinsicGas - accessListGas - codeDelegationGas; + final long intrinsicGas = + gasCalculator.transactionIntrinsicGasCost( + transaction.getPayload(), + transaction.isContractCreation(), + clampedAdd(accessListGas, codeDelegationGas)); + + final long gasAvailable = transaction.getGasLimit() - intrinsicGas; LOG.trace( - "Gas available for execution {} = {} - {} - {} - {} (limit - intrinsic - accessList - codeDelegation)", + "Gas available for execution {} = {} - {} (limit - intrinsic)", gasAvailable, transaction.getGasLimit(), - intrinsicGas, - accessListGas, - codeDelegationGas); + intrinsicGas); final WorldUpdater worldUpdater = evmWorldUpdater.updater(); final ImmutableMap.Builder contextVariablesBuilder = 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 200fd167c..0bef4d569 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 @@ -15,6 +15,7 @@ package org.hyperledger.besu.ethereum.mainnet; import static org.hyperledger.besu.evm.account.Account.MAX_NONCE; +import static org.hyperledger.besu.evm.internal.Words.clampedAdd; import static org.hyperledger.besu.evm.worldstate.DelegateCodeHelper.hasDelegatedCode; import org.hyperledger.besu.crypto.SECPSignature; @@ -250,17 +251,22 @@ public class MainnetTransactionValidator implements TransactionValidator { } } - final long intrinsicGasCost = - gasCalculator.transactionIntrinsicGasCost( - transaction.getPayload(), transaction.isContractCreation()) - + (transaction.getAccessList().map(gasCalculator::accessListGasCost).orElse(0L)) - + gasCalculator.delegateCodeGasCost(transaction.codeDelegationListSize()); - if (Long.compareUnsigned(intrinsicGasCost, transaction.getGasLimit()) > 0) { + final long baselineGas = + clampedAdd( + transaction.getAccessList().map(gasCalculator::accessListGasCost).orElse(0L), + gasCalculator.delegateCodeGasCost(transaction.codeDelegationListSize())); + final long intrinsicGasCostOrFloor = + Math.max( + gasCalculator.transactionIntrinsicGasCost( + transaction.getPayload(), transaction.isContractCreation(), baselineGas), + gasCalculator.transactionFloorCost(transaction.getPayload())); + + if (Long.compareUnsigned(intrinsicGasCostOrFloor, transaction.getGasLimit()) > 0) { return ValidationResult.invalid( TransactionInvalidReason.INTRINSIC_GAS_EXCEEDS_GAS_LIMIT, String.format( "intrinsic gas cost %s exceeds gas limit %s", - intrinsicGasCost, transaction.getGasLimit())); + intrinsicGasCostOrFloor, transaction.getGasLimit())); } if (transaction.calculateUpfrontGasCost(transaction.getMaxGasPrice(), Wei.ZERO, 0).bitLength() diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/IntrinsicGasTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/IntrinsicGasTest.java index bb9ce2b49..84af8547b 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/IntrinsicGasTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/IntrinsicGasTest.java @@ -16,15 +16,21 @@ package org.hyperledger.besu.ethereum.mainnet; import static org.assertj.core.api.Assertions.assertThat; +import org.hyperledger.besu.datatypes.AccessListEntry; import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; import org.hyperledger.besu.ethereum.rlp.RLP; import org.hyperledger.besu.evm.gascalculator.FrontierGasCalculator; import org.hyperledger.besu.evm.gascalculator.GasCalculator; import org.hyperledger.besu.evm.gascalculator.IstanbulGasCalculator; +import org.hyperledger.besu.evm.gascalculator.PragueGasCalculator; +import org.hyperledger.besu.evm.gascalculator.ShanghaiGasCalculator; +import java.util.List; import java.util.stream.Stream; import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -36,6 +42,8 @@ public class IntrinsicGasTest { public static Stream data() { final GasCalculator frontier = new FrontierGasCalculator(); final GasCalculator istanbul = new IstanbulGasCalculator(); + final GasCalculator shanghai = new ShanghaiGasCalculator(); + final GasCalculator prague = new PragueGasCalculator(); return Stream.of( // EnoughGAS Arguments.of( @@ -81,16 +89,36 @@ public class IntrinsicGasTest { Arguments.of( istanbul, 21116L, - "0xf87c80018261a894095e7baea6a6c7c4c2dfeb977efac326af552d870a9d00000000000000000000000000000000000000000000000000000000001ba048b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353a01fffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804")); + "0xf87c80018261a894095e7baea6a6c7c4c2dfeb977efac326af552d870a9d00000000000000000000000000000000000000000000000000000000001ba048b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353a01fffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804"), + // CallData Gas Increase + Arguments.of( + prague, + 21116L, + "0xf87c80018261a894095e7baea6a6c7c4c2dfeb977efac326af552d870a9d00000000000000000000000000000000000000000000000000000000001ba048b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353a01fffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804"), + // AccessList + Arguments.of( + shanghai, + 25300L, + "0x01f89a018001826a4094095e7baea6a6c7c4c2dfeb977efac326af552d878080f838f794a95e7baea6a6c7c4c2dfeb977efac326af552d87e1a0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80a05cbd172231fc0735e0fb994dd5b1a4939170a260b36f0427a8a80866b063b948a07c230f7f578dd61785c93361b9871c0706ebfa6d06e3f4491dc9558c5202ed36")); } @ParameterizedTest @MethodSource("data") public void validateGasCost( final GasCalculator gasCalculator, final long expectedGas, final String txRlp) { - Transaction t = Transaction.readFrom(RLP.input(Bytes.fromHexString(txRlp))); + Bytes rlp = Bytes.fromHexString(txRlp); + + // non-frontier transactions need to be opaque for parsing to work + if (rlp.get(0) > 0) { + final BytesValueRLPOutput output = new BytesValueRLPOutput(); + output.writeBytes(rlp); + rlp = output.encoded(); + } + + Transaction t = Transaction.readFrom(RLP.input(rlp)); Assertions.assertThat( - gasCalculator.transactionIntrinsicGasCost(t.getPayload(), t.isContractCreation())) + gasCalculator.transactionIntrinsicGasCost( + t.getPayload(), t.isContractCreation(), baselineGas(gasCalculator, t))) .isEqualTo(expectedGas); } @@ -100,4 +128,21 @@ public class IntrinsicGasTest { .withFailMessage("This test is here so gradle --dry-run executes this class") .isTrue(); } + + long baselineGas(final GasCalculator gasCalculator, final Transaction transaction) { + final List accessListEntries = transaction.getAccessList().orElse(List.of()); + + int accessListStorageCount = 0; + for (final var entry : accessListEntries) { + final List storageKeys = entry.storageKeys(); + accessListStorageCount += storageKeys.size(); + } + final long accessListGas = + gasCalculator.accessListGasCost(accessListEntries.size(), accessListStorageCount); + + final long codeDelegationGas = + gasCalculator.delegateCodeGasCost(transaction.codeDelegationListSize()); + + return accessListGas + codeDelegationGas; + } } 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 c673bf08f..86dcc2ff6 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 @@ -128,7 +128,27 @@ public class MainnetTransactionValidatorTest { .gasLimit(10) .chainId(Optional.empty()) .createTransaction(senderKeys); - when(gasCalculator.transactionIntrinsicGasCost(any(), anyBoolean())).thenReturn(50L); + when(gasCalculator.transactionIntrinsicGasCost(any(), anyBoolean(), anyLong())).thenReturn(50L); + + assertThat( + validator.validate( + transaction, Optional.empty(), Optional.empty(), transactionValidationParams)) + .isEqualTo( + ValidationResult.invalid(TransactionInvalidReason.INTRINSIC_GAS_EXCEEDS_GAS_LIMIT)); + } + + @Test + public void shouldRejectTransactionIfFloorExceedsGasLimit_EIP_7623() { + final TransactionValidator validator = + createTransactionValidator( + gasCalculator, GasLimitCalculator.constant(), false, Optional.empty()); + final Transaction transaction = + new TransactionTestFixture() + .gasLimit(10) + .chainId(Optional.empty()) + .createTransaction(senderKeys); + when(gasCalculator.transactionIntrinsicGasCost(any(), anyBoolean(), anyLong())).thenReturn(5L); + when(gasCalculator.transactionFloorCost(any())).thenReturn(51L); assertThat( validator.validate( @@ -398,7 +418,7 @@ public class MainnetTransactionValidatorTest { transaction, Optional.empty(), Optional.empty(), transactionValidationParams)) .isEqualTo(ValidationResult.invalid(INVALID_TRANSACTION_FORMAT)); - when(gasCalculator.transactionIntrinsicGasCost(any(), anyBoolean())).thenReturn(0L); + when(gasCalculator.transactionIntrinsicGasCost(any(), anyBoolean(), anyLong())).thenReturn(0L); assertThat( eip1559Validator.validate( @@ -475,7 +495,7 @@ public class MainnetTransactionValidatorTest { .chainId(Optional.of(BigInteger.ONE)) .createTransaction(senderKeys); final Optional basefee = Optional.of(Wei.of(150000L)); - when(gasCalculator.transactionIntrinsicGasCost(any(), anyBoolean())).thenReturn(50L); + when(gasCalculator.transactionIntrinsicGasCost(any(), anyBoolean(), anyLong())).thenReturn(50L); assertThat( validator.validate(transaction, basefee, Optional.empty(), transactionValidationParams)) @@ -500,7 +520,7 @@ public class MainnetTransactionValidatorTest { .type(TransactionType.EIP1559) .chainId(Optional.of(BigInteger.ONE)) .createTransaction(senderKeys); - when(gasCalculator.transactionIntrinsicGasCost(any(), anyBoolean())).thenReturn(50L); + when(gasCalculator.transactionIntrinsicGasCost(any(), anyBoolean(), anyLong())).thenReturn(50L); assertThat( validator.validate( diff --git a/ethereum/evmtool/src/main/java/org/hyperledger/besu/evmtool/EvmToolCommand.java b/ethereum/evmtool/src/main/java/org/hyperledger/besu/evmtool/EvmToolCommand.java index 0ff6a21da..7dde74946 100644 --- a/ethereum/evmtool/src/main/java/org/hyperledger/besu/evmtool/EvmToolCommand.java +++ b/ethereum/evmtool/src/main/java/org/hyperledger/besu/evmtool/EvmToolCommand.java @@ -402,16 +402,20 @@ public class EvmToolCommand implements Runnable { long txGas = gas; if (chargeIntrinsicGas) { - final long intrinsicGasCost = - protocolSpec - .getGasCalculator() - .transactionIntrinsicGasCost(tx.getPayload(), tx.isContractCreation()); - txGas -= intrinsicGasCost; final long accessListCost = tx.getAccessList() .map(list -> protocolSpec.getGasCalculator().accessListGasCost(list)) .orElse(0L); - txGas -= accessListCost; + + final long delegateCodeCost = + protocolSpec.getGasCalculator().delegateCodeGasCost(tx.codeDelegationListSize()); + + final long intrinsicGasCost = + protocolSpec + .getGasCalculator() + .transactionIntrinsicGasCost( + tx.getPayload(), tx.isContractCreation(), accessListCost + delegateCodeCost); + txGas -= intrinsicGasCost; } final EVM evm = protocolSpec.getEvm(); diff --git a/ethereum/evmtool/src/main/java/org/hyperledger/besu/evmtool/T8nExecutor.java b/ethereum/evmtool/src/main/java/org/hyperledger/besu/evmtool/T8nExecutor.java index 15c400bc0..7053961a1 100644 --- a/ethereum/evmtool/src/main/java/org/hyperledger/besu/evmtool/T8nExecutor.java +++ b/ethereum/evmtool/src/main/java/org/hyperledger/besu/evmtool/T8nExecutor.java @@ -437,7 +437,7 @@ public class T8nExecutor { gasUsed += transactionGasUsed; long intrinsicGas = gasCalculator.transactionIntrinsicGasCost( - transaction.getPayload(), transaction.getTo().isEmpty()); + transaction.getPayload(), transaction.getTo().isEmpty(), 0); TransactionReceipt receipt = protocolSpec .getTransactionReceiptFactory() diff --git a/ethereum/evmtool/src/test/resources/org/hyperledger/besu/evmtool/t8n/prague-deposit.json b/ethereum/evmtool/src/test/resources/org/hyperledger/besu/evmtool/t8n/prague-deposit.json index 6c704e0e2..58f845f5c 100644 --- a/ethereum/evmtool/src/test/resources/org/hyperledger/besu/evmtool/t8n/prague-deposit.json +++ b/ethereum/evmtool/src/test/resources/org/hyperledger/besu/evmtool/t8n/prague-deposit.json @@ -70,7 +70,7 @@ }, "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { "nonce": "0x00", - "balance": "0xad78ebc5ac62000000", + "balance": "0xaa00be18c288efd690", "code": "0x", "storage": {} } @@ -194,7 +194,7 @@ "nonce": "0x1" }, "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { - "balance": "0xaa00be18c288efd690", + "balance": "0xa688906bd8afdfad20", "nonce": "0x2" } }, @@ -204,7 +204,7 @@ "requests": [ "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000200405973070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000200405973070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030100000000000000" ], - "stateRoot": "0x6471f6d90b87f759176a0ad62a7096f69d0d24fd873bdb6b6ced57d04a71e274", + "stateRoot": "0xc769f83dbad9b87a209216d18c4b19cb12b61838594a2e8270898438f4e147af", "txRoot": "0x2b790bf82ef7259a0e4513d1b89a77d81e99672ba68758ef2ba3fde32851d023", "receiptsRoot": "0x9c8d7a917ecb3ff2566f264abbf39131e51b08b07eb2b69cb46989d79d985593", "logsHash": "0x43e31613bfefc1f55d8b3ca2b61f933f3838d523dc11cb5d7ffdd2ecf0ab5d49", diff --git a/ethereum/referencetests/src/reference-test/java/org/hyperledger/besu/ethereum/core/TransactionTest.java b/ethereum/referencetests/src/reference-test/java/org/hyperledger/besu/ethereum/core/TransactionTest.java index 9aa04bd0f..a3758422c 100644 --- a/ethereum/referencetests/src/reference-test/java/org/hyperledger/besu/ethereum/core/TransactionTest.java +++ b/ethereum/referencetests/src/reference-test/java/org/hyperledger/besu/ethereum/core/TransactionTest.java @@ -192,10 +192,13 @@ public class TransactionTest { assertThat(transaction.getSender()).isEqualTo(expected.getSender()); assertThat(transaction.getHash()).isEqualTo(expected.getHash()); - final long intrinsicGasCost = - gasCalculator.transactionIntrinsicGasCost( - transaction.getPayload(), transaction.isContractCreation()) - + (transaction.getAccessList().map(gasCalculator::accessListGasCost).orElse(0L)); + final long baselineGas = + transaction.getAccessList().map(gasCalculator::accessListGasCost).orElse(0L) + + gasCalculator.delegateCodeGasCost(transaction.codeDelegationListSize()); + final long intrinsicGasCost = gasCalculator.transactionIntrinsicGasCost( + transaction.getPayload(), + transaction.isContractCreation(), + baselineGas); assertThat(intrinsicGasCost).isEqualTo(expected.getIntrinsicGas()); } catch (final Exception e) { if (expected.isSucceeds()) { 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 8db00c3dd..efa33ceea 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 @@ -39,7 +39,8 @@ public class FrontierGasCalculator implements GasCalculator { private static final long TX_DATA_NON_ZERO_COST = 68L; - private static final long TX_BASE_COST = 21_000L; + /** Minimum base cost that every transaction needs to pay */ + protected static final long TX_BASE_COST = 21_000L; private static final long TX_CREATE_EXTRA_COST = 0L; @@ -129,18 +130,82 @@ public class FrontierGasCalculator implements GasCalculator { } @Override - public long transactionIntrinsicGasCost(final Bytes payload, final boolean isContractCreate) { + public long transactionIntrinsicGasCost( + final Bytes payload, final boolean isContractCreation, final long baselineGas) { + final long dynamicIntrinsicGasCost = + dynamicIntrinsicGasCost(payload, isContractCreation, baselineGas); + + if (dynamicIntrinsicGasCost == Long.MIN_VALUE || dynamicIntrinsicGasCost == Long.MAX_VALUE) { + return dynamicIntrinsicGasCost; + } + return clampedAdd(getMinimumTransactionCost(), dynamicIntrinsicGasCost); + } + + /** + * Calculates the dynamic part of the intrinsic gas cost + * + * @param payload the call data payload + * @param isContractCreation whether the transaction is a contract creation + * @param baselineGas how much gas is used by access lists and code delegations + * @return the dynamic part of the intrinsic gas cost + */ + protected long dynamicIntrinsicGasCost( + final Bytes payload, final boolean isContractCreation, final long baselineGas) { + final int payloadSize = payload.size(); + final long zeroBytes = zeroBytes(payload); + long cost = clampedAdd(callDataCost(payloadSize, zeroBytes), baselineGas); + + if (cost == Long.MIN_VALUE || cost == Long.MAX_VALUE) { + return cost; + } + + if (isContractCreation) { + cost = clampedAdd(cost, contractCreationCost(payloadSize)); + + if (cost == Long.MIN_VALUE || cost == Long.MAX_VALUE) { + return cost; + } + } + + return cost; + } + + /** + * Calculates the cost of the call data + * + * @param payloadSize the total size of the payload + * @param zeroBytes the number of zero bytes in the payload + * @return the cost of the call data + */ + protected long callDataCost(final long payloadSize, final long zeroBytes) { + return clampedAdd( + TX_DATA_NON_ZERO_COST * (payloadSize - zeroBytes), TX_DATA_ZERO_COST * zeroBytes); + } + + /** + * Counts the zero bytes in the payload + * + * @param payload the payload + * @return the number of zero bytes in the payload + */ + protected static long zeroBytes(final Bytes payload) { int zeros = 0; for (int i = 0; i < payload.size(); i++) { if (payload.get(i) == 0) { - ++zeros; + zeros += 1; } } - final int nonZeros = payload.size() - zeros; + return zeros; + } - final long cost = TX_BASE_COST + TX_DATA_ZERO_COST * zeros + TX_DATA_NON_ZERO_COST * nonZeros; - - return isContractCreate ? (cost + txCreateExtraGasCost()) : cost; + /** + * Returns the gas cost for contract creation transactions + * + * @param ignored the size of the contract creation code (ignored in Frontier) + * @return the gas cost for contract creation transactions + */ + protected long contractCreationCost(final int ignored) { + return txCreateExtraGasCost(); } /** @@ -152,6 +217,11 @@ public class FrontierGasCalculator implements GasCalculator { 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; @@ -558,11 +628,6 @@ public class FrontierGasCalculator implements GasCalculator { return clampedAdd(clampedMultiply(MEMORY_WORD_GAS_COST, length), base); } - @Override - public long getMaximumTransactionCost(final int size) { - return TX_BASE_COST + TX_DATA_NON_ZERO_COST * size; - } - @Override public long getMinimumTransactionCost() { return TX_BASE_COST; @@ -572,20 +637,21 @@ public class FrontierGasCalculator implements GasCalculator { 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); + final long ignoredCodeDelegationRefund) { + + final long refundAllowance = calculateRefundAllowance(transaction, initialFrame); + + return initialFrame.getRemainingGas() + refundAllowance; } - private long refunded( - final Transaction transaction, final long gasRemaining, final long gasRefund) { + private long calculateRefundAllowance( + final Transaction transaction, final MessageFrame initialFrame) { + final long selfDestructRefund = + getSelfDestructRefundAmount() * initialFrame.getSelfDestructs().size(); + final long executionRefund = initialFrame.getGasRefund() + selfDestructRefund; // 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; + (transaction.getGasLimit() - initialFrame.getRemainingGas()) / getMaxRefundQuotient(); + return Math.min(executionRefund, maxRefundAllowance); } } 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 ec8537cf9..9462a6f31 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 @@ -543,10 +543,21 @@ public interface GasCalculator { * encoded binary representation when stored on-chain. * * @param transactionPayload The encoded transaction, as bytes - * @param isContractCreate Is this transaction a contract creation transaction? + * @param isContractCreation Is this transaction a contract creation transaction? + * @param baselineGas The gas used by access lists and code delegation authorizations * @return the transaction's intrinsic gas cost */ - long transactionIntrinsicGasCost(Bytes transactionPayload, boolean isContractCreate); + long transactionIntrinsicGasCost( + Bytes transactionPayload, boolean isContractCreation, long baselineGas); + + /** + * Returns the floor gas cost of a transaction payload, i.e. the minimum gas cost that a + * transaction will be charged based on its calldata. Introduced in EIP-7623 in Prague. + * + * @param transactionPayload The encoded transaction, as bytes + * @return the transaction's floor gas cost + */ + long transactionFloorCost(final Bytes transactionPayload); /** * Returns the gas cost of the explicitly declared access list. @@ -580,15 +591,6 @@ public interface GasCalculator { return 2; } - /** - * Maximum Cost of a Transaction of a certain length. - * - * @param size the length of the transaction, in bytes - * @return the maximum gas cost - */ - // what would be the gas for a PMT with hash of all non-zeros - long getMaximumTransactionCost(int size); - /** * Minimum gas cost of a transaction. * diff --git a/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/IstanbulGasCalculator.java b/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/IstanbulGasCalculator.java index a997f7fe1..5df0c168b 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/IstanbulGasCalculator.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/IstanbulGasCalculator.java @@ -14,9 +14,10 @@ */ package org.hyperledger.besu.evm.gascalculator; +import static org.hyperledger.besu.evm.internal.Words.clampedAdd; + import java.util.function.Supplier; -import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.units.bigints.UInt256; /** The Istanbul gas calculator. */ @@ -24,7 +25,6 @@ public class IstanbulGasCalculator extends PetersburgGasCalculator { private static final long TX_DATA_ZERO_COST = 4L; private static final long ISTANBUL_TX_DATA_NON_ZERO_COST = 16L; - private static final long TX_BASE_COST = 21_000L; private static final long SLOAD_GAS = 800L; private static final long BALANCE_OPERATION_GAS_COST = 700L; @@ -42,19 +42,9 @@ public class IstanbulGasCalculator extends PetersburgGasCalculator { public IstanbulGasCalculator() {} @Override - public long transactionIntrinsicGasCost(final Bytes payload, final boolean isContractCreation) { - int zeros = 0; - for (int i = 0; i < payload.size(); i++) { - if (payload.get(i) == 0) { - ++zeros; - } - } - final int nonZeros = payload.size() - zeros; - - final long cost = - TX_BASE_COST + (TX_DATA_ZERO_COST * zeros) + (ISTANBUL_TX_DATA_NON_ZERO_COST * nonZeros); - - return isContractCreation ? (cost + txCreateExtraGasCost()) : cost; + protected long callDataCost(final long payloadSize, final long zeroBytes) { + return clampedAdd( + ISTANBUL_TX_DATA_NON_ZERO_COST * (payloadSize - zeroBytes), TX_DATA_ZERO_COST * zeroBytes); } @Override @@ -136,9 +126,4 @@ public class IstanbulGasCalculator extends PetersburgGasCalculator { public long extCodeHashOperationGasCost() { return EXTCODE_HASH_COST; } - - @Override - public long getMaximumTransactionCost(final int size) { - return TX_BASE_COST + (ISTANBUL_TX_DATA_NON_ZERO_COST * size); - } } 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 268a58061..60f2b8493 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 @@ -15,8 +15,13 @@ package org.hyperledger.besu.evm.gascalculator; import static org.hyperledger.besu.datatypes.Address.BLS12_MAP_FP2_TO_G2; +import static org.hyperledger.besu.evm.internal.Words.clampedAdd; import org.hyperledger.besu.datatypes.CodeDelegation; +import org.hyperledger.besu.datatypes.Transaction; +import org.hyperledger.besu.evm.frame.MessageFrame; + +import org.apache.tuweni.bytes.Bytes; /** * Gas Calculator for Prague @@ -26,6 +31,8 @@ import org.hyperledger.besu.datatypes.CodeDelegation; * */ public class PragueGasCalculator extends CancunGasCalculator { + private static final long TOTAL_COST_FLOOR_PER_TOKEN = 10L; + final long existingAccountGasRefund; /** @@ -68,4 +75,47 @@ public class PragueGasCalculator extends CancunGasCalculator { public long calculateDelegateCodeGasRefund(final long alreadyExistingAccounts) { return existingAccountGasRefund * alreadyExistingAccounts; } + + @Override + public long calculateGasRefund( + final Transaction transaction, + final MessageFrame initialFrame, + final long codeDelegationRefund) { + + final long refundAllowance = + calculateRefundAllowance(transaction, initialFrame, codeDelegationRefund); + + final long executionGasUsed = + transaction.getGasLimit() - initialFrame.getRemainingGas() - refundAllowance; + final long transactionFloorCost = transactionFloorCost(transaction.getPayload()); + final long totalGasUsed = Math.max(executionGasUsed, transactionFloorCost); + 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( + getMinimumTransactionCost(), + tokensInCallData(transactionPayload.size(), zeroBytes(transactionPayload)) + * TOTAL_COST_FLOOR_PER_TOKEN); + } + + private long tokensInCallData(final long payloadSize, final long zeroBytes) { + // as defined in https://eips.ethereum.org/EIPS/eip-7623#specification + return clampedAdd(zeroBytes, (payloadSize - zeroBytes) * 4); + } } diff --git a/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/ShanghaiGasCalculator.java b/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/ShanghaiGasCalculator.java index a44300f06..aa0dd080d 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/ShanghaiGasCalculator.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/ShanghaiGasCalculator.java @@ -14,11 +14,8 @@ */ package org.hyperledger.besu.evm.gascalculator; -import static org.hyperledger.besu.evm.internal.Words.clampedAdd; import static org.hyperledger.besu.evm.internal.Words.numWords; -import org.apache.tuweni.bytes.Bytes; - /** The Shanghai gas calculator. */ public class ShanghaiGasCalculator extends LondonGasCalculator { @@ -39,13 +36,8 @@ public class ShanghaiGasCalculator extends LondonGasCalculator { } @Override - public long transactionIntrinsicGasCost(final Bytes payload, final boolean isContractCreation) { - long intrinsicGasCost = super.transactionIntrinsicGasCost(payload, isContractCreation); - if (isContractCreation) { - return clampedAdd(intrinsicGasCost, initcodeCost(payload.size())); - } else { - return intrinsicGasCost; - } + protected long contractCreationCost(final int initCodeLength) { + return txCreateExtraGasCost() + initcodeCost(initCodeLength); } @Override 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 a657776b8..b0b949d38 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 @@ public class FrontierGasCalculatorTest { // 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 @@ public class FrontierGasCalculatorTest { // 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 @@ public class FrontierGasCalculatorTest { // 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 @@ public class FrontierGasCalculatorTest { 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 fdacad0e8..e8ded7458 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 @@ class PragueGasCalculatorTest { 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); + } }