Repository: celestiaorg/celestia-app

Issue:
Delete the deprecated estimation methods
We should delete these methods as they're deprecated in v3:

- TxClient.EstimateGas
- Signer.QueryGasUsedAndPrice
- Signer.QueryGasPrice

_Originally posted by @cmwaters in https://github.com/celestiaorg/celestia-app/pull/4459#discussion_r2018626807_
            

Discussion:

Solution patch:
diff --git a/Makefile b/Makefile
index f80972ad..5c1ce7bd 100644
--- a/Makefile
+++ b/Makefile
@@ -165,7 +165,7 @@ test-race:
 # TODO: Remove the -skip flag once the following tests no longer contain data races.
 # https://github.com/celestiaorg/celestia-app/issues/1369
 	@echo "--> Running tests in race mode"
-	@go test -timeout 15m ./... -v -race -skip "TestPrepareProposalConsistency|TestIntegrationTestSuite|TestBlobstreamRPCQueries|TestSquareSizeIntegrationTest|TestStandardSDKIntegrationTestSuite|TestTxsimCommandFlags|TestTxsimCommandEnvVar|TestTxsimDefaultKeypath|TestMintIntegrationTestSuite|TestBlobstreamCLI|TestUpgrade|TestMaliciousTestNode|TestBigBlobSuite|TestQGBIntegrationSuite|TestSignerTestSuite|TestPriorityTestSuite|TestTimeInPrepareProposalContext|TestBlobstream|TestCLITestSuite|TestLegacyUpgrade|TestSignerTwins|TestConcurrentTxSubmission|TestTxClientTestSuite|Test_testnode|TestEvictions|TestEstimateGasUsed|TestEstimateGasPrice"
+	@go test -timeout 15m ./... -v -race -skip "TestPrepareProposalConsistency|TestIntegrationTestSuite|TestBlobstreamRPCQueries|TestSquareSizeIntegrationTest|TestStandardSDKIntegrationTestSuite|TestTxsimCommandFlags|TestTxsimCommandEnvVar|TestTxsimDefaultKeypath|TestMintIntegrationTestSuite|TestBlobstreamCLI|TestUpgrade|TestMaliciousTestNode|TestBigBlobSuite|TestQGBIntegrationSuite|TestSignerTestSuite|TestPriorityTestSuite|TestTimeInPrepareProposalContext|TestBlobstream|TestCLITestSuite|TestLegacyUpgrade|TestSignerTwins|TestConcurrentTxSubmission|TestTxClientTestSuite|Test_testnode|TestEvictions|TestEstimateGasUsed|TestEstimateGasPrice|TestWithEstimatorService"
 .PHONY: test-race
 
 ## test-bench: Run unit tests in bench mode.
diff --git a/README.md b/README.md
index f900f1a9..6d7ebe6d 100644
--- a/README.md
+++ b/README.md
@@ -141,6 +141,8 @@ celestia-appd start --timeout-commit 1s
 
 ## Contributing
 
+If you are a new contributor, please read [contributing to Celestia](https://github.com/celestiaorg/.github/blob/main/CONTRIBUTING.md).
+
 This repo attempts to conform to [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) so PR titles should ideally start with `fix:`, `feat:`, `build:`, `chore:`, `ci:`, `docs:`, `style:`, `refactor:`, `perf:`, or `test:` because this helps with semantic versioning and changelog generation. It is especially important to include an `!` (e.g. `feat!:`) if the PR includes a breaking change.
 
 This repo contains multiple go modules. When using it, rename `go.work.example` to `go.work` and run `go work sync`.
diff --git a/app/grpc/gasestimation/gas_estimator.go b/app/grpc/gasestimation/gas_estimator.go
index 3a529815..a81c0c0a 100644
--- a/app/grpc/gasestimation/gas_estimator.go
+++ b/app/grpc/gasestimation/gas_estimator.go
@@ -17,6 +17,10 @@ import (
 	gogogrpc "github.com/gogo/protobuf/grpc"
 )
 
+// gasMultiplier is the multiplier for the gas limit. It's used to account for the fact that
+// when the gas is simulated it will occasionally underestimate the real gas used by the transaction.
+const gasMultiplier = 1.1
+
 // baseAppSimulateFn is the signature of the Baseapp#Simulate function.
 type baseAppSimulateFn func(txBytes []byte) (sdk.GasInfo, *sdk.Result, error)
 
@@ -89,9 +93,11 @@ func (s *gasEstimatorServer) EstimateGasPriceAndUsage(ctx context.Context, reque
 	if err != nil {
 		return nil, err
 	}
+	estimatedGasUsed := uint64(math.Round(float64(gasUsedInfo.GasUsed) * gasMultiplier))
+
 	return &EstimateGasPriceAndUsageResponse{
 		EstimatedGasPrice: gasPrice,
-		EstimatedGasUsed:  gasUsedInfo.GasUsed,
+		EstimatedGasUsed:  estimatedGasUsed,
 	}, nil
 }
 
diff --git a/pkg/user/signer.go b/pkg/user/signer.go
index 224f2039..c405b901 100644
--- a/pkg/user/signer.go
+++ b/pkg/user/signer.go
@@ -305,6 +305,7 @@ func (s *Signer) getSignatureV2(sequence uint64, pubKey cryptotypes.PubKey, sign
 // QueryGasPrice takes a priority and an app gRPC client.
 // Returns the current network gas price corresponding to the provided priority.
 // More on the gas price estimation can be found in docs/architecture/adr-023-gas-used-and-gas-price-estimation.md
+// Deprecated: use TxClient.EstimateGasPrice
 func (s *Signer) QueryGasPrice(
 	ctx context.Context,
 	grpcClient *grpc.ClientConn,
@@ -325,6 +326,7 @@ func (s *Signer) QueryGasPrice(
 // Returns the current network gas price corresponding to the provided priority,
 // and the gas used estimation for the provided transaction bytes.
 // More on the gas estimation can be found in docs/architecture/adr-023-gas-used-and-gas-price-estimation.md
+// Deprecated: use TxClient.EstimateGasPriceAndUsage
 func (s *Signer) QueryGasUsedAndPrice(
 	ctx context.Context,
 	grpcClient *grpc.ClientConn,
diff --git a/pkg/user/tx_client.go b/pkg/user/tx_client.go
index 993290c5..2fb21467 100644
--- a/pkg/user/tx_client.go
+++ b/pkg/user/tx_client.go
@@ -11,6 +11,8 @@ import (
 	"sync"
 	"time"
 
+	"github.com/celestiaorg/celestia-app/v3/app/grpc/gasestimation"
+
 	"github.com/celestiaorg/go-square/v2/share"
 	"github.com/cosmos/cosmos-sdk/client"
 	nodeservice "github.com/cosmos/cosmos-sdk/client/grpc/node"
@@ -33,9 +35,8 @@ import (
 )
 
 const (
-	DefaultPollTime                  = 3 * time.Second
-	DefaultGasMultiplier     float64 = 1.1
-	txTrackerPruningInterval         = 10 * time.Minute
+	DefaultPollTime          = 3 * time.Second
+	txTrackerPruningInterval = 10 * time.Minute
 )
 
 type Option func(client *TxClient)
@@ -81,13 +82,6 @@ func (e *ExecutionError) Error() string {
 	return fmt.Sprintf("tx execution failed with code %d: %s", e.Code, e.ErrorLog)
 }
 
-// WithGasMultiplier is a functional option that allows configuring the gas multiplier.
-func WithGasMultiplier(multiplier float64) Option {
-	return func(c *TxClient) {
-		c.gasMultiplier = multiplier
-	}
-}
-
 // WithDefaultGasPrice sets the gas price.
 func WithDefaultGasPrice(price float64) Option {
 	return func(c *TxClient) {
@@ -127,6 +121,15 @@ func WithDefaultAccount(name string) Option {
 	}
 }
 
+// WithEstimatorService allows a user to provide a connection to a special gas
+// estimation service to be used by the TxClient for estimating gas price
+// and usage.
+func WithEstimatorService(conn *grpc.ClientConn) Option {
+	return func(c *TxClient) {
+		c.gasEstimationClient = gasestimation.NewGasEstimatorClient(conn)
+	}
+}
+
 // TxClient is an abstraction for building, signing, and broadcasting Celestia transactions
 // It supports multiple accounts. If none is specified, it will
 // try to use the default account.
@@ -138,15 +141,14 @@ type TxClient struct {
 	grpc     *grpc.ClientConn
 	// how often to poll the network for confirmation of a transaction
 	pollTime time.Duration
-	// gasMultiplier is used to increase gas limit as it is sometimes underestimated
-	gasMultiplier float64
 	// defaultGasPrice is the price used if no price is provided
 	defaultGasPrice float64
 	defaultAccount  string
 	defaultAddress  sdktypes.AccAddress
 	// txTracker maps the tx hash to the Sequence and signer of the transaction
 	// that was submitted to the chain
-	txTracker map[string]txInfo
+	txTracker           map[string]txInfo
+	gasEstimationClient gasestimation.GasEstimatorClient
 }
 
 // NewTxClient returns a new signer using the provided keyring
@@ -171,15 +173,15 @@ func NewTxClient(
 	}
 
 	txClient := &TxClient{
-		signer:          signer,
-		registry:        registry,
-		grpc:            conn,
-		pollTime:        DefaultPollTime,
-		gasMultiplier:   DefaultGasMultiplier,
-		defaultGasPrice: appconsts.DefaultMinGasPrice,
-		defaultAccount:  records[0].Name,
-		defaultAddress:  addr,
-		txTracker:       make(map[string]txInfo),
+		signer:              signer,
+		registry:            registry,
+		grpc:                conn,
+		pollTime:            DefaultPollTime,
+		defaultGasPrice:     appconsts.DefaultMinGasPrice,
+		defaultAccount:      records[0].Name,
+		defaultAddress:      addr,
+		txTracker:           make(map[string]txInfo),
+		gasEstimationClient: gasestimation.NewGasEstimatorClient(conn),
 	}
 
 	for _, opt := range options {
@@ -286,7 +288,7 @@ func (client *TxClient) BroadcastPayForBlobWithAccount(ctx context.Context, acco
 		blobSizes[i] = uint32(len(blob.Data()))
 	}
 
-	gasLimit := uint64(float64(types.DefaultEstimateGas(blobSizes)) * client.gasMultiplier)
+	gasLimit := uint64(float64(types.DefaultEstimateGas(blobSizes)))
 	fee := uint64(math.Ceil(appconsts.DefaultMinGasPrice * float64(gasLimit)))
 	// prepend calculated params, so it can be overwritten in case the user has specified it.
 	opts = append([]TxOption{SetGasLimit(gasLimit), SetFee(fee)}, opts...)
@@ -498,8 +500,9 @@ func (client *TxClient) deleteFromTxTracker(txHash string) {
 	delete(client.txTracker, txHash)
 }
 
-// EstimateGas simulates the transaction, calculating the amount of gas that was consumed during execution. The final
-// result will be multiplied by gasMultiplier(that is set in TxClient)
+// EstimateGas simulates the transaction, calculating the amount of gas that was
+// consumed during execution.
+// Deprecated: use EstimateGasPriceAndUsage
 func (client *TxClient) EstimateGas(ctx context.Context, msgs []sdktypes.Msg, opts ...TxOption) (uint64, error) {
 	client.mtx.Lock()
 	defer client.mtx.Unlock()
@@ -512,6 +515,61 @@ func (client *TxClient) EstimateGas(ctx context.Context, msgs []sdktypes.Msg, op
 	return client.estimateGas(ctx, txBuilder)
 }
 
+// EstimateGasPriceAndUsage returns the estimated gas price based on the provided priority,
+// and also the gas limit/used for the provided transaction.
+// The gas limit is calculated by simulating the transaction and then calculating the amount of gas that was consumed during execution.
+func (client *TxClient) EstimateGasPriceAndUsage(
+	ctx context.Context,
+	msgs []sdktypes.Msg,
+	priority gasestimation.TxPriority,
+	opts ...TxOption,
+) (gasPrice float64, gasUsed uint64, err error) {
+	client.mtx.Lock()
+	defer client.mtx.Unlock()
+
+	txBuilder, err := client.signer.txBuilder(msgs, opts...)
+	if err != nil {
+		return 0, 0, err
+	}
+
+	// add at least 1utia as fee to builder as it affects gas calculation.
+	txBuilder.SetFeeAmount(sdktypes.NewCoins(sdktypes.NewCoin(appconsts.BondDenom, sdktypes.NewInt(1))))
+
+	_, _, err = client.signer.signTransaction(txBuilder)
+	if err != nil {
+		return 0, 0, err
+	}
+	txBytes, err := client.signer.EncodeTx(txBuilder.GetTx())
+	if err != nil {
+		return 0, 0, err
+	}
+	resp, err := client.gasEstimationClient.EstimateGasPriceAndUsage(ctx, &gasestimation.EstimateGasPriceAndUsageRequest{
+		TxPriority: priority,
+		TxBytes:    txBytes,
+	})
+	if err != nil {
+		return 0, 0, fmt.Errorf("failed to estimate gas price and usage: %w", err)
+	}
+
+	gasUsed = uint64(float64(resp.EstimatedGasUsed))
+
+	return resp.EstimatedGasPrice, gasUsed, nil
+}
+
+// EstimateGasPrice calls the gas estimation endpoint to return the estimated gas price based on priority.
+func (client *TxClient) EstimateGasPrice(ctx context.Context, priority gasestimation.TxPriority) (float64, error) {
+	client.mtx.Lock()
+	defer client.mtx.Unlock()
+
+	resp, err := client.gasEstimationClient.EstimateGasPrice(ctx, &gasestimation.EstimateGasPriceRequest{
+		TxPriority: priority,
+	})
+	if err != nil {
+		return 0, err
+	}
+	return resp.EstimatedGasPrice, nil
+}
+
 func (client *TxClient) estimateGas(ctx context.Context, txBuilder client.TxBuilder) (uint64, error) {
 	// add at least 1utia as fee to builder as it affects gas calculation.
 	txBuilder.SetFeeAmount(sdktypes.NewCoins(sdktypes.NewCoin(appconsts.BondDenom, sdktypes.NewInt(1))))
@@ -524,14 +582,12 @@ func (client *TxClient) estimateGas(ctx context.Context, txBuilder client.TxBuil
 	if err != nil {
 		return 0, err
 	}
-	resp, err := sdktx.NewServiceClient(client.grpc).Simulate(ctx, &sdktx.SimulateRequest{
-		TxBytes: txBytes,
-	})
+	resp, err := client.gasEstimationClient.EstimateGasPriceAndUsage(ctx, &gasestimation.EstimateGasPriceAndUsageRequest{TxBytes: txBytes})
 	if err != nil {
 		return 0, err
 	}
 
-	gasLimit := uint64(float64(resp.GasInfo.GasUsed) * client.gasMultiplier)
+	gasLimit := uint64(float64(resp.EstimatedGasUsed))
 	return gasLimit, nil
 }
 
@@ -620,12 +676,6 @@ func (client *TxClient) SetDefaultGasPrice(price float64) {
 	client.defaultGasPrice = price
 }
 
-func (client *TxClient) SetGasMultiplier(multiplier float64) {
-	client.mtx.Lock()
-	defer client.mtx.Unlock()
-	client.gasMultiplier = multiplier
-}
-
 // QueryMinimumGasPrice queries both the nodes local and network wide
 // minimum gas prices, returning the maximum of the two.
 func QueryMinimumGasPrice(ctx context.Context, grpcConn *grpc.ClientConn) (float64, error) {



Test patch:
diff --git a/app/test/gas_estimation_test.go b/app/test/gas_estimation_test.go
index 5219735f..9ce9aa82 100644
--- a/app/test/gas_estimation_test.go
+++ b/app/test/gas_estimation_test.go
@@ -8,9 +8,18 @@ import (
 	"testing"
 	"time"
 
-	testutil "github.com/celestiaorg/celestia-app/v3/test/util"
 	"github.com/tendermint/tendermint/types"
 
+	testutil "github.com/celestiaorg/celestia-app/v3/test/util"
+
+	"github.com/celestiaorg/go-square/v2/share"
+	sdk "github.com/cosmos/cosmos-sdk/types"
+	banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	abci "github.com/tendermint/tendermint/abci/types"
+	tmrand "github.com/tendermint/tendermint/libs/rand"
+
 	"github.com/celestiaorg/celestia-app/v3/app"
 	"github.com/celestiaorg/celestia-app/v3/app/encoding"
 	"github.com/celestiaorg/celestia-app/v3/app/grpc/gasestimation"
@@ -20,13 +29,6 @@ import (
 	"github.com/celestiaorg/celestia-app/v3/test/util/testfactory"
 	"github.com/celestiaorg/celestia-app/v3/test/util/testnode"
 	blobtypes "github.com/celestiaorg/celestia-app/v3/x/blob/types"
-	"github.com/celestiaorg/go-square/v2/share"
-	sdk "github.com/cosmos/cosmos-sdk/types"
-	banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
-	abci "github.com/tendermint/tendermint/abci/types"
-	tmrand "github.com/tendermint/tendermint/libs/rand"
 )
 
 func TestSortAndExtractGasPrice(t *testing.T) {
@@ -203,7 +205,6 @@ func TestEstimateGasUsed(t *testing.T) {
 	encfg := encoding.MakeConfig(app.ModuleEncodingRegisters...)
 	txClient, err := user.SetupTxClient(cctx.GoContext(), cctx.Keyring, cctx.GRPCClient, encfg)
 	require.NoError(t, err)
-	txClient.SetGasMultiplier(1)
 	addr := testfactory.GetAddress(cctx.Keyring, "test")
 
 	// create a transfer transaction
diff --git a/pkg/user/tx_client_test.go b/pkg/user/tx_client_test.go
index 55d870b9..c37defec 100644
--- a/pkg/user/tx_client_test.go
+++ b/pkg/user/tx_client_test.go
@@ -2,11 +2,20 @@ package user_test
 
 import (
 	"context"
+	"errors"
+	"fmt"
+	"net"
 	"testing"
 	"time"
 
-	"github.com/celestiaorg/celestia-app/v3/app/grpc/gasestimation"
 	"github.com/stretchr/testify/assert"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/credentials/insecure"
+
+	"github.com/celestiaorg/celestia-app/v3/app/grpc/gasestimation"
+
+	sdk "github.com/cosmos/cosmos-sdk/types"
+	sdktx "github.com/cosmos/cosmos-sdk/types/tx"
 
 	"github.com/celestiaorg/celestia-app/v3/app"
 	"github.com/celestiaorg/celestia-app/v3/app/encoding"
@@ -14,8 +23,6 @@ import (
 	"github.com/celestiaorg/celestia-app/v3/pkg/user"
 	"github.com/celestiaorg/celestia-app/v3/test/util/blobfactory"
 	"github.com/celestiaorg/celestia-app/v3/test/util/testnode"
-	sdk "github.com/cosmos/cosmos-sdk/types"
-	sdktx "github.com/cosmos/cosmos-sdk/types/tx"
 
 	"github.com/cosmos/cosmos-sdk/x/authz"
 	bank "github.com/cosmos/cosmos-sdk/x/bank/types"
@@ -255,12 +262,36 @@ func TestEvictions(t *testing.T) {
 	require.Equal(t, seqBeforeEviction, seqAfterEviction)
 }
 
-func (suite *TxClientTestSuite) TestGasEstimation() {
+// TestWithEstimatorService ensures that if the WithEstimatorService
+// option is provided to the tx client, the separate gas estimator service is
+// used to estimate gas price and usage instead of the default connection.
+func TestWithEstimatorService(t *testing.T) {
+	mockEstimator := setupEstimatorService(t)
+	_, txClient, ctx := setupTxClient(t, testnode.DefaultTendermintConfig().Mempool.TTLDuration,
+		user.WithEstimatorService(mockEstimator.conn))
+
+	msg := bank.NewMsgSend(txClient.DefaultAddress(), testnode.RandomAddress().(sdk.AccAddress),
+		sdk.NewCoins(sdk.NewInt64Coin(app.BondDenom, 10)))
+	price, used, err := txClient.EstimateGasPriceAndUsage(ctx.GoContext(), []sdk.Msg{msg}, 1)
+	require.NoError(t, err)
+
+	assert.Equal(t, 0.02, price)
+	assert.Equal(t, uint64(70000), used)
+}
+
+func (suite *TxClientTestSuite) TestGasPriceAndUsageEstimation() {
 	addr := suite.txClient.DefaultAddress()
 	msg := bank.NewMsgSend(addr, testnode.RandomAddress().(sdk.AccAddress), sdk.NewCoins(sdk.NewInt64Coin(app.BondDenom, 10)))
-	gas, err := suite.txClient.EstimateGas(suite.ctx.GoContext(), []sdk.Msg{msg})
+	gasPrice, gasUsage, err := suite.txClient.EstimateGasPriceAndUsage(suite.ctx.GoContext(), []sdk.Msg{msg}, 1)
 	require.NoError(suite.T(), err)
-	require.Greater(suite.T(), gas, uint64(0))
+	require.Greater(suite.T(), gasPrice, float64(0))
+	require.Greater(suite.T(), gasUsage, uint64(0))
+}
+
+func (suite *TxClientTestSuite) TestGasPriceEstimation() {
+	gasPrice, err := suite.txClient.EstimateGasPrice(suite.ctx.GoContext(), 0)
+	require.NoError(suite.T(), err)
+	require.Equal(suite.T(), gasPrice, appconsts.DefaultMinGasPrice)
 }
 
 // TestGasConsumption verifies that the amount deducted from a user's balance is
@@ -338,8 +369,11 @@ func assertTxInTxTracker(t *testing.T, txClient *user.TxClient, txHash string, e
 	require.Equal(t, seqAfterBroadcast, seqBeforeBroadcast+1)
 }
 
-func setupTxClient(t *testing.T, ttlDuration time.Duration) (encoding.Config, *user.TxClient, testnode.Context) {
-	encCfg := encoding.MakeConfig(app.ModuleEncodingRegisters...)
+func setupTxClient(
+	t *testing.T,
+	ttlDuration time.Duration,
+	opts ...user.Option,
+) (encoding.Config, *user.TxClient, testnode.Context) {
 	defaultTmConfig := testnode.DefaultTendermintConfig()
 	defaultTmConfig.Mempool.TTLDuration = ttlDuration
 	testnodeConfig := testnode.DefaultConfig().
@@ -349,33 +383,64 @@ func setupTxClient(t *testing.T, ttlDuration time.Duration) (encoding.Config, *u
 	ctx, _, _ := testnode.NewNetwork(t, testnodeConfig)
 	_, err := ctx.WaitForHeight(1)
 	require.NoError(t, err)
-	txClient, err := user.SetupTxClient(ctx.GoContext(), ctx.Keyring, ctx.GRPCClient, encCfg, user.WithGasMultiplier(1.2))
+
+	encCfg := encoding.MakeConfig(app.ModuleEncodingRegisters...)
+
+	txClient, err := user.SetupTxClient(ctx.GoContext(), ctx.Keyring, ctx.GRPCClient, encCfg, opts...)
 	require.NoError(t, err)
+
 	return encCfg, txClient, ctx
 }
 
-func (suite *TxClientTestSuite) TestGasPriceAndUsedEstimate() {
-	t := suite.T()
-	ctx := context.Background()
-	signer := suite.txClient.Signer()
+type mockEstimatorServer struct {
+	*gasestimation.UnimplementedGasEstimatorServer
+	srv  *grpc.Server
+	conn *grpc.ClientConn
+	addr string
+}
 
-	t.Run("query the gas price from the app gRPC", func(t *testing.T) {
-		gasPrice, err := signer.QueryGasPrice(ctx, suite.ctx.GRPCClient, gasestimation.TxPriority_TX_PRIORITY_HIGH)
-		assert.NoError(t, err)
-		assert.Greater(t, gasPrice, float64(0))
-	})
+func (m *mockEstimatorServer) EstimateGasPriceAndUsage(
+	context.Context,
+	*gasestimation.EstimateGasPriceAndUsageRequest,
+) (*gasestimation.EstimateGasPriceAndUsageResponse, error) {
+	return &gasestimation.EstimateGasPriceAndUsageResponse{
+		EstimatedGasPrice: 0.02,
+		EstimatedGasUsed:  70000,
+	}, nil
+}
+
+func (m *mockEstimatorServer) stop() {
+	m.srv.GracefulStop()
+}
+
+func setupEstimatorService(t *testing.T) *mockEstimatorServer {
+	t.Helper()
 
-	t.Run("query the gas price and gas used from the app gRPC", func(t *testing.T) {
-		msg := bank.NewMsgSend(
-			suite.txClient.DefaultAddress(),
-			testnode.RandomAddress().(sdk.AccAddress),
-			sdk.NewCoins(sdk.NewInt64Coin(appconsts.BondDenom, 10)),
-		)
-		rawTx, err := signer.CreateTx([]sdk.Msg{msg})
+	freePort, err := testnode.GetFreePort()
+	require.NoError(t, err)
+	addr := fmt.Sprintf(":%d", freePort)
+	net, err := net.Listen("tcp", addr)
+	require.NoError(t, err)
+
+	grpcServer := grpc.NewServer()
+	mes := &mockEstimatorServer{srv: grpcServer, addr: addr}
+	gasestimation.RegisterGasEstimatorServer(grpcServer, mes)
+
+	go func() {
+		err := grpcServer.Serve(net)
+		if err != nil && !errors.Is(err, grpc.ErrServerStopped) {
+			panic(err)
+		}
+	}()
+
+	conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
+	require.NoError(t, err)
+	t.Cleanup(func() {
+		err = conn.Close()
 		require.NoError(t, err)
-		gasPrice, gasUsed, err := signer.QueryGasUsedAndPrice(ctx, suite.ctx.GRPCClient, gasestimation.TxPriority_TX_PRIORITY_HIGH, rawTx)
-		assert.NoError(t, err)
-		assert.Greater(t, gasPrice, float64(0))
-		assert.Greater(t, gasUsed, uint64(0))
 	})
+	mes.conn = conn
+
+	t.Cleanup(mes.stop)
+	return mes
 }
diff --git a/test/util/testnode/utils.go b/test/util/testnode/utils.go
index b17c0db6..f89129bd 100644
--- a/test/util/testnode/utils.go
+++ b/test/util/testnode/utils.go
@@ -7,10 +7,6 @@ import (
 	"os"
 	"path"
 
-	"github.com/celestiaorg/celestia-app/v3/app"
-	"github.com/celestiaorg/celestia-app/v3/app/encoding"
-	"github.com/celestiaorg/celestia-app/v3/pkg/appconsts"
-	"github.com/celestiaorg/celestia-app/v3/test/util/testfactory"
 	"github.com/cosmos/cosmos-sdk/client"
 	"github.com/cosmos/cosmos-sdk/crypto/hd"
 	"github.com/cosmos/cosmos-sdk/crypto/keyring"
@@ -19,6 +15,11 @@ import (
 	banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
 	tmrand "github.com/tendermint/tendermint/libs/rand"
 	rpctypes "github.com/tendermint/tendermint/rpc/core/types"
+
+	"github.com/celestiaorg/celestia-app/v3/app"
+	"github.com/celestiaorg/celestia-app/v3/app/encoding"
+	"github.com/celestiaorg/celestia-app/v3/pkg/appconsts"
+	"github.com/celestiaorg/celestia-app/v3/test/util/testfactory"
 )
 
 func TestAddress() sdk.AccAddress {
@@ -92,8 +93,8 @@ func GenerateAccounts(count int) []string {
 	return accs
 }
 
-// getFreePort returns a free port and optionally an error.
-func getFreePort() (int, error) {
+// GetFreePort returns a free port and optionally an error.
+func GetFreePort() (int, error) {
 	a, err := net.ResolveTCPAddr("tcp", "localhost:0")
 	if err != nil {
 		return 0, err
@@ -110,7 +111,7 @@ func getFreePort() (int, error) {
 // mustGetFreePort returns a free port. Panics if no free ports are available or
 // an error is encountered.
 func mustGetFreePort() int {
-	port, err := getFreePort()
+	port, err := GetFreePort()
 	if err != nil {
 		panic(err)
 	}