besu icon indicating copy to clipboard operation
besu copied to clipboard

Besu doesn't apply EIP-7623 during gas estimation

Open daniellehrner opened this issue 7 months ago • 0 comments

We received the following report:

EIP-7623 increases calldata costs for data-heavy transactions. The issue arises from the fact that while Besu applies EIP-7623 during block processing, it does not apply it during gas estimation.

This happens due to passing gasUsedByTransaction to TransactionProcessResult in the MainnetTransactionProcessor.processTransaction function. One such occurrence is demonstrated in the referenced code. The gasUsedByTransaction value is computed as:

final long gasUsedByTransaction = transaction.getGasLimit() - initialFrame.getRemainingGas();

This value does not account for EIP-7623 and, therefore, might be off by a significant margin. The POC demonstrates a case where Besu returns a gas estimate of 659116, while the actual value is approximately 1630000.

This issue could result in highly inaccurate responses to eth_estimateGas calls, potentially leading to fund losses for users relying on Besu nodes to set transaction gas limit values.

Proof of Concept

Run the following test with Besu and other execution clients:

package main

import (
	"context"
	"fmt"
	"testing"

	"github.com/ethereum/go-ethereum"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/ethclient"
	"github.com/stretchr/testify/require"
)

func TestPocBesuEstimateGasEip7623(t *testing.T) {
	// 1. Generate huge calldata
	data := make([]byte, 40000)
	for i := range data {
		data[i] = byte(i)
	}

	// 2. Prepare message
	msg := ethereum.CallMsg{
		To:   &common.Address{},
		From: common.Address{},
		Data: data,
	}

	// 3. Check the gas estimate for all RPCs. The RPCs correspond to Geth, Besu, and Nethermind nodes, respectively.
	rpcs := []string{"http://127.0.0.1:32913", "http://127.0.0.1:32918", "http://127.0.0.1:32923"}
	for _, rpc := range rpcs {
		client, err := ethclient.Dial(rpc)
		require.NoError(t, err)
		gas, err := client.EstimateGas(context.Background(), msg)
		require.NoError(t, err)
		fmt.Println(gas)
	}
}

In my case, the result looks as follows:

=== RUN   TestPocBesuEstimateGasEip7623
1630285
659116
1629578
--- PASS: TestPocBesuEstimateGasEip7623 (0.01s)
PASS
ok      github.com/alexfilippov314/gop  0.027s

Recommendation

I believe the historical reason for using this value as the gas estimate was to exclude gas refunds, which makes sense since refunds can only decrease the required gas limit. However, after implementing EIP-7623 in the PragueGasCalculator.calculateGasRefund function, this logic now seems problematic, as the required gas limit could be significantly greater than the estimate. Consider refactoring this logic to account for EIP-7623 in the gas estimate.

https://github.com/hyperledger/besu/blob/7e5cf71a053c1768a8d6391a6c377c0c37b798e3/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionProcessor.java#L540

daniellehrner avatar Apr 24 '25 12:04 daniellehrner