Search code examples
solidityrpchedera-hashgraphhedera

Large discrepancy in `gasUsed` values in near-identical transactions on Hedera - why?


I have noticed that there is a discrepancy between the amount of gas used by a transaction is almost identical:

I'm making a call to a smart contract, with the same parameters twice in a row, and the only difference between the 2 is that I'm setting gasLimit to the exact value returned by eth_estimateGas in the 1st one, and I'm setting gasLimit to eth_estimateGas * 1.1 in the 2nd one.

    // 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 110%  of estimated amount of gas
    const estimatedGas3 = (await expendGasSc.estimateGas.updateState(1_000_000_123n)).toBigInt();
    console.log('estimatedGas3', estimatedGas3);
    const gasLimit3 = estimatedGas3 * 11n / 10n;
    console.log('gasLimit3', gasLimit3);
    const txResponse3 = await (await expendGasSc
        .updateState(
            1_000_000_123n,
            { gasLimit: gasLimit3 },
        ))
        .wait();
    const gasUsed3 = txResponse3.gasUsed.toBigInt();
    console.log('gasUsed3', gasUsed3);

Here's the output, which shows that:

  • when gasLimit is set to 400,000, gasUsed is 320,000
  • when gasLimit is set to 440,000, gasUsed is 352,000
estimatedGas2 400000n
gasLimit2 400000n
gasUsed2 320000n
estimatedGas3 400000n
gasLimit3 440000n
gasUsed3 352000n

Why does the 2nd invocation expend an additional 32,000 in gas? (I expected the gasUsed value to be the same in both cases.)


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.


Solution

  • This is because of: HIP-185: Smart Contract Service Gas Based Throttling ​

    In order to ensure that the transactions can execute properly it is common to set a higher gas reservation than will be consumed by execution. In Ethereum Mainnet the entire reservation is charged to the account prior to execution and then unused portion of the reservation is credited back. Ethereum Mainnet, however, utilizes a memory pool and does transaction ordering at block production time, and that allows the block limit to be based only on used gas and not reserved gas. ​ Essentially gasLimit refers to the "higher gas reservation", and gasUsed is refers to the gas "consumed by execution". In Ethereum, the calculation is simply: ​

    gasUsed = gasConsumed
    

    ​ In other words, gasLimit is not a factor.

    However, in Hedera, this isn't possible, since Hedera's consensus nodes do not make use of a transaction pool, and then subsequently order transactions within a block. In fact, Hedera does not even have the concept of blocks within its consensus nodes, and only add this concept in its archival nodes (Mirror nodes) - see HIP-415: Introduction Of Blocks.

    Therefore Hedera needs an alternative mechanism to Ethereum's to enable it to limit over-reservation of gas. It has done so by disincentivisation: ​

    In order to prevent over-reservation of the smart contract services the gas credited back relative to the reservation will be limited to at most 20% of the reservation amount. From a different perspective the minimum amount of gas charged to a user based on the reservation will be a minimum of 80% of the reservation. This will incentivize transaction submitters to get within 25% of the actual gas used in order to not be charged for unused reservation. ​ So the computation for gasUsed in Hedera is: ​

    gasUsed = max(gasConsumed, gasLimit * 0.8)
    

    ​ In other words, gasLimit is a factor, when you specify one that is more than actual gas consumed. ​ This is how you get the additional 32,000 gasUsed when you specify an additional 40,000 in gasLimit.