I am aware that eth_estimateGas
is not intended to be exact,
but currently, I'm getting actual gasUsed
values
that are approximately 6% of the value returned by eth_estimateGas
.
In the following example, I invoke the same smart contract with the exact same inputs twice, with only 1 difference:
gasLimit = eth_estimateGas
gasLimit = eth_estimateGas * 0.064
// with exact estimated amount of gas
const estimatedGas2 = (await expendGasSc.estimateGas.updateState(1_000_000_123n)).toBigInt();
console.log('estimatedGas2', estimatedGas2);
const gasLimit2 = estimatedGas2 * 1n;
console.log('gasLimit2', gasLimit2);
const txResponse2 = await (await expendGasSc
.updateState(
1_000_000_123n,
{ gasLimit: gasLimit2 },
))
.wait();
const gasUsed2 = txResponse2.gasUsed.toBigInt();
console.log('gasUsed2', gasUsed2);
// with small fraction of estimated amount of gas
const estimatedGas4 = (await expendGasSc.estimateGas.updateState(1_000_000_123n)).toBigInt();
console.log('estimatedGas4', estimatedGas4);
const gasLimit4 = estimatedGas4 * 64n / 1000n; // <--- 🚨🚨🚨 6.4% 🚨🚨🚨
console.log('gasLimit4', gasLimit4);
const txResponse4 = await (await expendGasSc
.updateState(
1_000_000_123n,
{ gasLimit: gasLimit4 },
))
.wait();
console.log('txResponse4', txResponse4);
const gasUsed4 = txResponse4.gasUsed.toBigInt();
console.log('gasUsed4', gasUsed4);
Here are the results:
gasLimit
is 400,000, gasUsed
is 320,000
gasLimit
,
indicating that HIP-185's gas over-reservation penalty *is likely to have kicked in.gasLimit
is 25,600, gasUsed
is 23,816
gasLimit
,
indicating that HIP-185's gas over-reservation penalty has not kicked inestimatedGas2 400000n
gasLimit2 400000n
gasUsed2 320000n
estimatedGas4 400000n
gasLimit4 25600n
gasUsed4 23816n
Therefore, I am expecting eth_estimateGas
to return a value
that is much closer to 23,816 than 400,000.
Why is it returning such an unexpectedly high estimate
compared to the actual?
Here's the smart contract:
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.18;
contract ExpendSomeGasDemo {
uint256 public state;
function updateState(
uint256 newState
)
public
returns (uint256 updatedState)
{
state = newState;
updatedState = newState;
}
}
Note that this contract is deployed on Hedera Testnet:
0x9C58D0159495F7a8853A24574f2B8F348a72424c
Note that the Javascript example above is using ethers.js.
Note that this question is a follow up to my previous one:
Large discrepancy in gasUsed
values in near-identical transactions on Hedera - why?
You're querying eth_estimateGas
via the Hedera JSON-RPC relay,
which currently uses static values in lieu of actual gas estimation, this is what's responsible for the consistent estimations.
Note, that this will soon be replaced with an actual gas estimation algorithm, when the implementation of HIP-584: Mirror EVM Archive Node is released.
Specifically, you can see packages/relay/src/lib/constants.ts
,
to see the the gas-cost-related values defined as constants:
{
// ...
TX_BASE_COST: 21_000,
TX_HOLLOW_ACCOUNT_CREATION_GAS: 587_000,
TX_DEFAULT_GAS_DEFAULT: 400_000,
TX_CREATE_EXTRA: 32_000,
TX_DATA_ZERO_COST: 4,
// ...
}
These are used by the estimateGas
function in packages/relay/src/lib/eth.ts
in their return values.
Note, that no actual gas estimation algorithm is being run at the moment.
This line - return this.defaultGas;
, which maps to TX_DEFAULT_GAS_DEFAULT
above -
is also why you will get the same value of 400,000
anytime you perform eth_estimateGas
for a smart contract invocation.
Here's the code snippet below
async estimateGas(transaction: any, _blockParam: string | null, requestId?: string) {
const requestIdPrefix = formatRequestIdMessage(requestId);
this.logger.trace(`${requestIdPrefix} estimateGas(transaction=${JSON.stringify(transaction)}, _blockParam=${_blockParam})`);
// this checks whether this is a transfer transaction and not a contract function execution
if (transaction && transaction.to && (!transaction.data || transaction.data === '0x')) {
const value = Number(transaction.value);
if (value > 0) {
const accountCacheKey = `${constants.CACHE_KEY.ACCOUNT}_${transaction.to}`;
let toAccount: object | null = this.cache.get(accountCacheKey);
if (!toAccount) {
toAccount = await this.mirrorNodeClient.getAccount(transaction.to, requestId);
}
// when account exists return default base gas, otherwise return the minimum amount of gas to create an account entity
if (toAccount) {
this.logger.trace(`${requestIdPrefix} caching ${accountCacheKey}:${JSON.stringify(toAccount)} for ${constants.CACHE_TTL.ONE_HOUR} ms`);
this.cache.set(accountCacheKey, toAccount);
return EthImpl.gasTxBaseCost;
}
return EthImpl.gasTxHollowAccountCreation;
}
return predefined.INVALID_PARAMETER(0, `Invalid 'value' field in transaction param. Value must be greater than 0`);
} else {
return this.defaultGas;
}
}