Repository: celestiaorg/celestia-app

Issue:
Using multiple endpoints increments the sequence each time
### Summary

As was pointed out in this [PR](https://github.com/celestiaorg/celestia-app/pull/5776#discussion_r2353727083), when we use multiple endpoints, we incorrectly increment the sequence for each submission/endpoint. 

Discussion:

Solution patch:
diff --git a/pkg/user/tx_client.go b/pkg/user/tx_client.go
index ccebfcf4..1f644edd 100644
--- a/pkg/user/tx_client.go
+++ b/pkg/user/tx_client.go
@@ -547,7 +547,7 @@ func (client *TxClient) broadcastMulti(ctx context.Context, txBytes []byte, sign
 		go func(conn *grpc.ClientConn) {
 			defer wg.Done()
 
-			resp, err := client.broadcastTxAndIncrementSequence(ctx, conn, txBytes, signer)
+			resp, err := client.broadcastTx(ctx, conn, txBytes, signer)
 			if err != nil {
 				errCh <- err
 				return
@@ -569,6 +569,11 @@ func (client *TxClient) broadcastMulti(ctx context.Context, txBytes []byte, sign
 
 	// Return first successful response, if any
 	if resp, ok := <-respCh; ok {
+		client.trackTransaction(signer, resp.TxHash, txBytes)
+
+		if err := client.signer.IncrementSequence(signer); err != nil {
+			return nil, fmt.Errorf("increment sequencing: %w", err)
+		}
 		return resp, nil
 	}
 



Test patch:
diff --git a/pkg/user/tx_client_test.go b/pkg/user/tx_client_test.go
index 5b25655f..cb5b3880 100644
--- a/pkg/user/tx_client_test.go
+++ b/pkg/user/tx_client_test.go
@@ -794,3 +794,75 @@ func (suite *TxClientTestSuite) TestMultiConnBroadcast() {
 		})
 	}
 }
+
+// TestSequenceIncrementOnlyOnceInMultiConnBroadcast specifically tests that the sequence
+// is incremented exactly once even when multiple connections succeed simultaneously.
+// This test would fail if we naively called broadcastTxAndIncrementSequence in each goroutine.
+func (suite *TxClientTestSuite) TestSequenceIncrementOnlyOnceInMultiConnBroadcast() {
+	t := suite.T()
+
+	// Create mock services that all succeed immediately
+	mockSvc1 := &grpctest.MockTxService{
+		BroadcastHandler: func(ctx context.Context, req *sdktx.BroadcastTxRequest) (*sdktx.BroadcastTxResponse, error) {
+			return &sdktx.BroadcastTxResponse{TxResponse: &sdk.TxResponse{Code: abci.CodeTypeOK, TxHash: "SUCCESS1"}}, nil
+		},
+	}
+	mockSvc2 := &grpctest.MockTxService{
+		BroadcastHandler: func(ctx context.Context, req *sdktx.BroadcastTxRequest) (*sdktx.BroadcastTxResponse, error) {
+			return &sdktx.BroadcastTxResponse{TxResponse: &sdk.TxResponse{Code: abci.CodeTypeOK, TxHash: "SUCCESS2"}}, nil
+		},
+	}
+	mockSvc3 := &grpctest.MockTxService{
+		BroadcastHandler: func(ctx context.Context, req *sdktx.BroadcastTxRequest) (*sdktx.BroadcastTxResponse, error) {
+			return &sdktx.BroadcastTxResponse{TxResponse: &sdk.TxResponse{Code: abci.CodeTypeOK, TxHash: "SUCCESS3"}}, nil
+		},
+	}
+
+	conn1 := grpctest.StartMockServer(t, mockSvc1)
+	conn2 := grpctest.StartMockServer(t, mockSvc2)
+	conn3 := grpctest.StartMockServer(t, mockSvc3)
+
+	// Create a TxClient with multiple connections
+	origSigner := suite.txClient.Signer()
+	origAcc := origSigner.Account(suite.txClient.DefaultAccountName()).Copy()
+	signer, err := user.NewSigner(suite.ctx.Keyring, suite.encCfg.TxConfig, origSigner.ChainID(), origAcc)
+	require.NoError(t, err)
+
+	multiConnClient, err := user.NewTxClient(
+		suite.encCfg.Codec,
+		signer,
+		conn1,
+		suite.encCfg.InterfaceRegistry,
+		user.WithAdditionalCoreEndpoints([]*grpc.ClientConn{conn2, conn3}),
+	)
+	require.NoError(t, err)
+
+	// Capture sequence before broadcast
+	seqBefore := multiConnClient.Signer().Account(multiConnClient.DefaultAccountName()).Sequence()
+
+	// Create a simple message
+	msg := bank.NewMsgSend(
+		multiConnClient.DefaultAddress(),
+		multiConnClient.DefaultAddress(),
+		sdk.NewCoins(sdk.NewCoin(appconsts.BondDenom, sdkmath.NewInt(1))),
+	)
+
+	// Broadcast the transaction
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	resp, err := multiConnClient.BroadcastTx(ctx, []sdk.Msg{msg}, user.SetGasLimit(100000), user.SetFee(1000))
+	require.NoError(t, err, "BroadcastTx should succeed")
+	require.NotNil(t, resp, "Response should not be nil")
+	require.Equal(t, abci.CodeTypeOK, resp.Code, "Response code should be OK")
+
+	// Verify sequence was incremented by exactly 1
+	seqAfter := multiConnClient.Signer().Account(multiConnClient.DefaultAccountName()).Sequence()
+	require.Equal(t, seqBefore+1, seqAfter, "Sequence should be incremented by exactly 1, not by number of connections")
+
+	// Verify the transaction is tracked
+	trackedSeq, trackedSigner, exists := multiConnClient.GetTxFromTxTracker(resp.TxHash)
+	require.True(t, exists, "Transaction should be in tracker")
+	require.Equal(t, seqBefore, trackedSeq, "Tracked sequence should be the sequence before increment")
+	require.Equal(t, multiConnClient.DefaultAccountName(), trackedSigner, "Tracked signer should match")
+}