Repository: anza-xyz/agave
Pull request #5830: RPC/STS: use client UdpSocket created in client_info
Author: login: KirillLykov
Labels: name: automerge
description: automerge Merge this Pull Request automatically once CI passes
Created: 2025-04-15T16:34:17Z | Merged: 2025-04-16T10:25:20Z

#### Problem

Currently, server-side sockets are created in the cluster_info module while client-side sockets are created everywhere. This makes it hard to track that they use the bind address specified by user instead of UNSPECIFIED and generally complicated tracking of opened sockets.

#### Summary of Changes

Create socket for the TpuClient  in SendTransactionService in cluster_info where all the other sockets are opened.


Diff:
diff --git a/Cargo.lock b/Cargo.lock
index 4ad3827b3ebc0d..e305621bac0ad3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -9944,6 +9944,7 @@ dependencies = [
  "solana-logger",
  "solana-measure",
  "solana-metrics",
+ "solana-net-utils",
  "solana-runtime",
  "solana-sdk",
  "solana-tpu-client-next",
diff --git a/core/src/validator.rs b/core/src/validator.rs
index be345f7d9196b9..cefb3ac5981aae 100644
--- a/core/src/validator.rs
+++ b/core/src/validator.rs
@@ -1182,7 +1182,10 @@ impl Validator {
                 max_complete_rewards_slot,
                 prioritization_fee_cache: prioritization_fee_cache.clone(),
                 client_option: if config.use_tpu_client_next {
-                    ClientOption::TpuClientNext(Arc::as_ref(&identity_keypair))
+                    ClientOption::TpuClientNext(
+                        Arc::as_ref(&identity_keypair),
+                        node.sockets.rpc_sts_client,
+                    )
                 } else {
                     ClientOption::ConnectionCache(connection_cache.clone())
                 },
diff --git a/gossip/src/cluster_info.rs b/gossip/src/cluster_info.rs
index ebb3b0c5803137..2143049196873f 100644
--- a/gossip/src/cluster_info.rs
+++ b/gossip/src/cluster_info.rs
@@ -2388,6 +2388,8 @@ pub struct Sockets {
     pub quic_forwards_client: UdpSocket,
     /// Connection cache endpoint for QUIC-based Vote
     pub quic_vote_client: UdpSocket,
+    /// Client-side socket for RPC/SendTransactionService.
+    pub rpc_sts_client: UdpSocket,
 }
 
 pub struct NodeConfig {
@@ -2476,6 +2478,7 @@ impl Node {
         let tpu_vote_forwards_client = bind_to_localhost().unwrap();
         let quic_forwards_client = bind_to_localhost().unwrap();
         let quic_vote_client = bind_to_localhost().unwrap();
+        let rpc_sts_client = bind_to_localhost().unwrap();
 
         let mut info = ContactInfo::new(
             *pubkey,
@@ -2556,6 +2559,7 @@ impl Node {
                 tpu_vote_forwards_client,
                 quic_forwards_client,
                 quic_vote_client,
+                rpc_sts_client,
             },
         }
     }
@@ -2663,6 +2667,7 @@ impl Node {
         let tpu_vote_forwards_client = bind_to_with_config(bind_ip_addr, 0, socket_config).unwrap();
         let quic_forwards_client = bind_to_with_config(bind_ip_addr, 0, socket_config).unwrap();
         let quic_vote_client = bind_to_with_config(bind_ip_addr, 0, socket_config).unwrap();
+        let rpc_sts_client = bind_to_with_config(bind_ip_addr, 0, socket_config).unwrap();
 
         let addr = gossip_addr.ip();
         let mut info = ContactInfo::new(
@@ -2728,6 +2733,7 @@ impl Node {
                 tpu_vote_forwards_client,
                 quic_vote_client,
                 quic_forwards_client,
+                rpc_sts_client,
             },
         }
     }
@@ -2835,6 +2841,7 @@ impl Node {
         let tpu_vote_forwards_client = bind_to_with_config(bind_ip_addr, 0, socket_config).unwrap();
         let quic_forwards_client = bind_to_with_config(bind_ip_addr, 0, socket_config).unwrap();
         let quic_vote_client = bind_to_with_config(bind_ip_addr, 0, socket_config).unwrap();
+        let rpc_sts_client = bind_to_with_config(bind_ip_addr, 0, socket_config).unwrap();
 
         let mut info = ContactInfo::new(
             *pubkey,
@@ -2882,6 +2889,7 @@ impl Node {
             tpu_vote_forwards_client,
             quic_vote_client,
             quic_forwards_client,
+            rpc_sts_client,
         };
         info!("Bound all network sockets as follows: {:#?}", &sockets);
         Node { info, sockets }
diff --git a/rpc/src/rpc_service.rs b/rpc/src/rpc_service.rs
index df6cca5ea5cff8..0ff742f8ec3f1f 100644
--- a/rpc/src/rpc_service.rs
+++ b/rpc/src/rpc_service.rs
@@ -44,7 +44,7 @@ use {
     },
     solana_storage_bigtable::CredentialType,
     std::{
-        net::SocketAddr,
+        net::{SocketAddr, UdpSocket},
         path::{Path, PathBuf},
         sync::{
             atomic::{AtomicBool, AtomicU64, Ordering},
@@ -409,7 +409,7 @@ pub struct JsonRpcServiceConfig<'a> {
 ///       requires a reference to a [`Keypair`].
 pub enum ClientOption<'a> {
     ConnectionCache(Arc<ConnectionCache>),
-    TpuClientNext(&'a Keypair),
+    TpuClientNext(&'a Keypair, UdpSocket),
 }
 
 impl JsonRpcService {
@@ -466,7 +466,7 @@ impl JsonRpcService {
                 )?;
                 Ok(json_rpc_service)
             }
-            ClientOption::TpuClientNext(identity_keypair) => {
+            ClientOption::TpuClientNext(identity_keypair, tpu_client_socket) => {
                 let my_tpu_address = config
                     .cluster_info
                     .my_contact_info()
@@ -482,6 +482,7 @@ impl JsonRpcService {
                     leader_info,
                     config.send_transaction_service_config.leader_forward_count,
                     Some(identity_keypair),
+                    tpu_client_socket,
                 );
 
                 let json_rpc_service = Self::new_with_client(
diff --git a/send-transaction-service/Cargo.toml b/send-transaction-service/Cargo.toml
index e9851d1591475e..99a1cf40d51607 100644
--- a/send-transaction-service/Cargo.toml
+++ b/send-transaction-service/Cargo.toml
@@ -19,6 +19,7 @@ solana-connection-cache = { workspace = true }
 solana-keypair = { workspace = true }
 solana-measure = { workspace = true }
 solana-metrics = { workspace = true }
+solana-net-utils = { workspace = true, optional = true }
 solana-runtime = { workspace = true }
 solana-sdk = { workspace = true }
 solana-tpu-client-next = { workspace = true, features = ["metrics"] }
@@ -27,10 +28,11 @@ tokio-util = { workspace = true }
 
 [dev-dependencies]
 solana-logger = { workspace = true }
+solana-net-utils = { workspace = true }
 solana-runtime = { workspace = true, features = ["dev-context-only-utils"] }
 
 [features]
-dev-context-only-utils = []
+dev-context-only-utils = ["solana-net-utils"]
 
 [package.metadata.docs.rs]
 targets = ["x86_64-unknown-linux-gnu"]
diff --git a/send-transaction-service/src/test_utils.rs b/send-transaction-service/src/test_utils.rs
index 915c2655451058..d2b455429a3cae 100644
--- a/send-transaction-service/src/test_utils.rs
+++ b/send-transaction-service/src/test_utils.rs
@@ -52,6 +52,8 @@ impl CreateClient for TpuClientNextClient {
     ) -> Self {
         let runtime_handle =
             maybe_runtime.expect("Runtime should be provided for the TpuClientNextClient.");
+        let bind_socket = solana_net_utils::bind_to_localhost()
+            .expect("Should be able to open UdpSocket for tests.");
         Self::new::<NullTpuInfo>(
             runtime_handle,
             my_tpu_address,
@@ -59,6 +61,7 @@ impl CreateClient for TpuClientNextClient {
             None,
             leader_forward_count,
             None,
+            bind_socket,
         )
     }
 }
diff --git a/send-transaction-service/src/transaction_client.rs b/send-transaction-service/src/transaction_client.rs
index 381e68adaf537c..40a2ad492b1701 100644
--- a/send-transaction-service/src/transaction_client.rs
+++ b/send-transaction-service/src/transaction_client.rs
@@ -14,7 +14,7 @@ use {
         ConnectionWorkersScheduler, ConnectionWorkersSchedulerError,
     },
     std::{
-        net::{Ipv4Addr, SocketAddr},
+        net::{SocketAddr, UdpSocket},
         sync::{atomic::Ordering, Arc, Mutex},
         time::{Duration, Instant},
     },
@@ -227,16 +227,34 @@ where
 /// * Update the validator identity keypair and propagate the changes to the
 ///   scheduler. Most of the complexity of this structure arises from this
 ///   functionality.
-#[derive(Clone)]
 pub struct TpuClientNextClient {
     runtime_handle: Handle,
     sender: mpsc::Sender<TransactionBatch>,
+    bind_socket: UdpSocket,
     // This handle is needed to implement `NotifyKeyUpdate` trait. It's only
     // method takes &self and thus we need to wrap with Mutex.
     join_and_cancel: Arc<Mutex<(Option<TpuClientJoinHandle>, CancellationToken)>>,
     leader_forward_count: u64,
 }
 
+// Implement Clone manually because `UdpSocket` implements only `try_clone`.
+impl Clone for TpuClientNextClient {
+    fn clone(&self) -> Self {
+        let bind_socket = self
+            .bind_socket
+            .try_clone()
+            .expect("Cloning bind socket should always finish successfully.");
+
+        TpuClientNextClient {
+            runtime_handle: self.runtime_handle.clone(),
+            sender: self.sender.clone(),
+            bind_socket,
+            join_and_cancel: self.join_and_cancel.clone(),
+            leader_forward_count: self.leader_forward_count,
+        }
+    }
+}
+
 type TpuClientJoinHandle =
     TokioJoinHandle<Result<ConnectionWorkersScheduler, ConnectionWorkersSchedulerError>>;
 
@@ -249,6 +267,7 @@ impl TpuClientNextClient {
         leader_info: Option<T>,
         leader_forward_count: u64,
         identity: Option<&Keypair>,
+        bind_socket: UdpSocket,
     ) -> Self
     where
         T: TpuInfoWithSendStatic + Clone,
@@ -266,7 +285,12 @@ impl TpuClientNextClient {
                 my_tpu_address,
                 tpu_peers,
             };
-        let config = Self::create_config(identity, leader_forward_count as usize);
+        let config = {
+            let bind_socket = bind_socket
+                .try_clone()
+                .expect("Cloning bind socket should always finish successfully.");
+            Self::create_config(bind_socket, identity, leader_forward_count as usize)
+        };
         let scheduler = ConnectionWorkersScheduler::new(Box::new(leader_updater), receiver);
         // leaking handle to this task, as it will run until the cancel signal is received
         runtime_handle.spawn(scheduler.get_stats().report_to_influxdb(
@@ -280,16 +304,17 @@ impl TpuClientNextClient {
             join_and_cancel: Arc::new(Mutex::new((Some(handle), cancel))),
             sender,
             leader_forward_count,
+            bind_socket,
         }
     }
 
     fn create_config(
+        bind_socket: UdpSocket,
         stake_identity: Option<&Keypair>,
         leader_forward_count: usize,
     ) -> ConnectionWorkersSchedulerConfig {
-        let address = SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), 0);
         ConnectionWorkersSchedulerConfig {
-            bind: BindTarget::Address(address),
+            bind: BindTarget::Socket(bind_socket),
             stake_identity: stake_identity.map(Into::into),
             num_connections: MAX_CONNECTIONS,
             skip_check_transaction_age: true,
@@ -314,7 +339,15 @@ impl TpuClientNextClient {
 
     async fn do_update_key(&self, identity: &Keypair) -> Result<(), Box<dyn std::error::Error>> {
         let runtime_handle = self.runtime_handle.clone();
-        let config = Self::create_config(Some(identity), self.leader_forward_count as usize);
+        let bind_socket = self
+            .bind_socket
+            .try_clone()
+            .expect("Cloning bind socket should always finish successfully.");
+        let config = Self::create_config(
+            bind_socket,
+            Some(identity),
+            self.leader_forward_count as usize,
+        );
         let handle = self.join_and_cancel.clone();
 
         let join_handle = {


Reviews:
user: login: alexpyattaev
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-16T07:41:30Z
user: login: KirillLykov
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-16T07:46:52Z
user: login: alexpyattaev
body: Thank you!
state: APPROVED
author_association: NONE
submitted_at: 2025-04-16T09:04:03Z

Review comments:
diff_hunk: @@ -249,6 +267,7 @@ impl TpuClientNextClient {
         leader_info: Option<T>,
         leader_forward_count: u64,
         identity: Option<&Keypair>,
+        bind_socket: UdpSocket,
path: send-transaction-service/src/transaction_client.rs
user: login: alexpyattaev
body: maybe call it `client_socket` or just `socket`? There is nothing particularly special about it.
created_at: 2025-04-16T07:41:30Z
author_association: NONE
diff_hunk: @@ -249,6 +267,7 @@ impl TpuClientNextClient {
         leader_info: Option<T>,
         leader_forward_count: u64,
         identity: Option<&Keypair>,
+        bind_socket: UdpSocket,
path: send-transaction-service/src/transaction_client.rs
user: login: KirillLykov
body: we use consistently "bind" as a term for the client side socket here and in other places. I would like to stick with that for consistency. 
created_at: 2025-04-16T07:46:51Z
author_association: NONE

PR comments:
user: login: alexpyattaev
created_at: 2025-04-15T16:38:54Z
body: A bit of context for these - binding to UNSPECIFIED (0.0.0.0) means the outgoing traffic will bypass doublezero network, even if it is configured, and will generally ignore bind_address CLA.
author_association: NONE
user: login: codecov-commenter
created_at: 2025-04-15T17:27:59Z
body: ## [Codecov](https://app.codecov.io/gh/anza-xyz/agave/pull/5830?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=anza-xyz) Report
:x: Patch coverage is `64.58333%` with `17 lines` in your changes missing coverage. Please review.
:white_check_mark: Project coverage is 83.0%. Comparing base ([`7eff32c`](https://app.codecov.io/gh/anza-xyz/agave/commit/7eff32c386bc5a47424e90a7939c3e34692cec74?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=anza-xyz)) to head ([`554d561`](https://app.codecov.io/gh/anza-xyz/agave/commit/554d561e778c080bad8ef5823e5f3caa80cb0d87?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=anza-xyz)).
:warning: Report is 3619 commits behind head on master.

<details><summary>Additional details and impacted files</summary>



```diff
@@           Coverage Diff            @@
##           master    #5830    +/-   ##
========================================
  Coverage    83.0%    83.0%            
========================================
  Files         828      828            
  Lines      375684   375726    +42     
========================================
+ Hits       311855   311971   +116     
+ Misses      63829    63755    -74     
```
</details>
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE