Implements EIP-7623 (increase calldata cost)

Signed-off-by: Daniel Lehrner <daniel.lehrner@consensys.net>
Signed-off-by: Simon Dudley <simon.dudley@consensys.net>
Co-authored-by: Simon Dudley <simon.dudley@consensys.net>
This commit is contained in:
daniellehrner
2025-01-16 11:09:04 +01:00
committed by GitHub
parent b5fdcc096f
commit 1e7f6f6bf3
17 changed files with 433 additions and 107 deletions

View File

@@ -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)

View File

@@ -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));
}
}

View File

@@ -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<String, Object> contextVariablesBuilder =

View File

@@ -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()

View File

@@ -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<Arguments> 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<AccessListEntry> accessListEntries = transaction.getAccessList().orElse(List.of());
int accessListStorageCount = 0;
for (final var entry : accessListEntries) {
final List<Bytes32> storageKeys = entry.storageKeys();
accessListStorageCount += storageKeys.size();
}
final long accessListGas =
gasCalculator.accessListGasCost(accessListEntries.size(), accessListStorageCount);
final long codeDelegationGas =
gasCalculator.delegateCodeGasCost(transaction.codeDelegationListSize());
return accessListGas + codeDelegationGas;
}
}

View File

@@ -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<Wei> 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(

View File

@@ -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();

View File

@@ -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()

View File

@@ -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",

View File

@@ -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()) {

View File

@@ -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);
}
}

View File

@@ -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.
*

View File

@@ -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);
}
}

View File

@@ -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;
* </UL>
*/
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);
}
}

View File

@@ -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

View File

@@ -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<Address> 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);
}
}

View File

@@ -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<Address> 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);
}
}