Skip to main content

sp1_sdk/network/
client.rs

1//! # Network Client
2//!
3//! This module provides a client for directly interacting with the network prover service.
4
5use std::{
6    result::Result::Ok as StdOk,
7    str::FromStr,
8    sync::Arc,
9    time::{Duration, SystemTime, UNIX_EPOCH},
10};
11
12use alloy_primitives::{Address, B256, U256};
13use anyhow::{Context, Ok, Result};
14use async_trait::async_trait;
15use reqwest_middleware::ClientWithMiddleware as HttpClientWithMiddleware;
16use serde::{de::DeserializeOwned, Serialize};
17use sp1_core_machine::io::SP1Stdin;
18use sp1_prover::{HashableKey, SP1VerifyingKey};
19use tokio::sync::OnceCell;
20use tonic::{transport::Channel, Code};
21
22use super::{
23    grpc,
24    retry::{self, RetryableRpc, DEFAULT_RETRY_TIMEOUT},
25    signer::NetworkSigner,
26    utils::{sign_message, Signable},
27    NetworkMode, MAINNET_EXPLORER_URL, RESERVED_EXPLORER_URL,
28};
29use crate::network::proto::{
30    artifact::{artifact_store_client::ArtifactStoreClient, ArtifactType, CreateArtifactRequest},
31    // Import the clients for both auction and base.
32    auction_network::prover_network_client::ProverNetworkClient as AuctionProverNetworkClient,
33    // Import auction and base specific types for requests.
34    auction_types::{
35        CancelRequestRequest as AuctionCancelRequestRequest,
36        CancelRequestRequestBody as AuctionCancelRequestRequestBody,
37        GetBalanceRequest as AuctionGetBalanceRequest,
38        GetFilteredProofRequestsRequest as AuctionGetFilteredProofRequestsRequest,
39        GetNonceRequest as AuctionGetNonceRequest, GetProgramRequest as AuctionGetProgramRequest,
40        GetProofRequestParamsRequest as AuctionGetProofRequestParamsRequest,
41        GetProofRequestStatusRequest as AuctionGetProofRequestStatusRequest,
42        GetProversByUptimeRequest as AuctionGetProversByUptimeRequest,
43        MessageFormat as AuctionMessageFormat, RequestProofRequest as AuctionRequestProofRequest,
44        RequestProofRequestBody as AuctionRequestProofRequestBody,
45        TransactionVariant as AuctionTransactionVariant,
46    },
47    base_network::prover_network_client::ProverNetworkClient as BaseProverNetworkClient,
48    base_types::{
49        GetBalanceRequest as BaseGetBalanceRequest,
50        GetFilteredProofRequestsRequest as BaseGetFilteredProofRequestsRequest,
51        GetNonceRequest as BaseGetNonceRequest, GetProgramRequest as BaseGetProgramRequest,
52        GetProofRequestStatusRequest as BaseGetProofRequestStatusRequest,
53        MessageFormat as BaseMessageFormat, RequestProofRequest as BaseRequestProofRequest,
54        RequestProofRequestBody as BaseRequestProofRequestBody,
55    },
56    // Import standard types (auction by default for backwards compatibility).
57    types::{
58        CreateProgramRequest, CreateProgramRequestBody, CreateProgramResponse, FulfillmentStatus,
59        FulfillmentStrategy, GetProofRequestDetailsRequest, GetProofRequestDetailsResponse,
60        MessageFormat, ProofMode,
61    },
62    CancelRequestResponse,
63    GetBalanceResponse,
64    GetFilteredProofRequestsResponse,
65    // Import unified switchable response types.
66    GetNonceResponse,
67    GetProgramResponse,
68    GetProofRequestParamsResponse,
69    GetProofRequestStatusResponse,
70    RequestProofResponse,
71};
72
73/// A client for interacting with the network.
74#[derive(Clone)]
75pub struct NetworkClient {
76    pub(crate) signer: NetworkSigner,
77    pub(crate) http: HttpClientWithMiddleware,
78    pub(crate) rpc_url: String,
79    pub(crate) network_mode: NetworkMode,
80    /// Lazily-established gRPC channel, shared across clones and reused across calls.
81    pub(crate) channel: Arc<OnceCell<Channel>>,
82}
83
84#[async_trait]
85impl RetryableRpc for NetworkClient {
86    /// Execute an operation with retries using default timeout.
87    async fn with_retry<'a, T, F, Fut>(&'a self, operation: F, operation_name: &str) -> Result<T>
88    where
89        F: Fn() -> Fut + Send + Sync + 'a,
90        Fut: std::future::Future<Output = Result<T>> + Send,
91        T: Send,
92    {
93        self.with_retry_timeout(operation, DEFAULT_RETRY_TIMEOUT, operation_name).await
94    }
95
96    /// Execute an operation with retries using the specified timeout.
97    async fn with_retry_timeout<'a, T, F, Fut>(
98        &'a self,
99        operation: F,
100        timeout: Duration,
101        operation_name: &str,
102    ) -> Result<T>
103    where
104        F: Fn() -> Fut + Send + Sync + 'a,
105        Fut: std::future::Future<Output = Result<T>> + Send,
106        T: Send,
107    {
108        retry::retry_operation(operation, Some(timeout), operation_name).await
109    }
110}
111
112impl NetworkClient {
113    /// Creates a new [`NetworkClient`] with the given signer, rpc url, and network mode.
114    pub fn new(
115        signer: NetworkSigner,
116        rpc_url: impl Into<String>,
117        network_mode: NetworkMode,
118    ) -> Self {
119        let client = reqwest::Client::builder()
120            .pool_max_idle_per_host(0)
121            .pool_idle_timeout(Duration::from_secs(240))
122            .build()
123            .unwrap();
124        Self {
125            signer,
126            http: client.into(),
127            rpc_url: rpc_url.into(),
128            network_mode,
129            channel: Arc::new(OnceCell::new()),
130        }
131    }
132
133    /// Get the explorer URL for the current network mode.
134    #[must_use]
135    pub fn get_explorer_url(&self) -> &'static str {
136        match self.network_mode {
137            NetworkMode::Mainnet => MAINNET_EXPLORER_URL,
138            NetworkMode::Reserved => RESERVED_EXPLORER_URL,
139        }
140    }
141
142    /// Get the latest nonce for this account's address.
143    pub async fn get_nonce(&self) -> Result<u64> {
144        let response = self.get_nonce_response().await?;
145        Ok(response.nonce())
146    }
147
148    /// Get the full nonce response (internal helper).
149    async fn get_nonce_response(&self) -> Result<GetNonceResponse> {
150        match self.network_mode {
151            NetworkMode::Mainnet => {
152                self.with_retry(
153                    || async {
154                        let mut rpc = self.auction_prover_network_client().await?;
155                        let res = rpc
156                            .get_nonce(AuctionGetNonceRequest {
157                                address: self.signer.address().to_vec(),
158                            })
159                            .await?;
160                        Ok(GetNonceResponse::from(res.into_inner()))
161                    },
162                    "getting nonce",
163                )
164                .await
165            }
166            NetworkMode::Reserved => {
167                self.with_retry(
168                    || async {
169                        let mut rpc = self.base_prover_network_client().await?;
170                        let res = rpc
171                            .get_nonce(BaseGetNonceRequest {
172                                address: self.signer.address().to_vec(),
173                            })
174                            .await?;
175                        Ok(GetNonceResponse::from(res.into_inner()))
176                    },
177                    "getting nonce",
178                )
179                .await
180            }
181        }
182    }
183
184    /// Get the credit balance of your account.
185    ///
186    /// # Details
187    /// Uses the key that the client was initialized with.
188    pub async fn get_balance(&self) -> Result<U256> {
189        let response = self.get_balance_response().await?;
190        Ok(U256::from_str(response.balance()).unwrap())
191    }
192
193    /// Get the full balance response (internal helper).
194    async fn get_balance_response(&self) -> Result<GetBalanceResponse> {
195        match self.network_mode {
196            NetworkMode::Mainnet => {
197                self.with_retry(
198                    || async {
199                        let mut rpc = self.auction_prover_network_client().await?;
200                        let res = rpc
201                            .get_balance(AuctionGetBalanceRequest {
202                                address: self.signer.address().to_vec(),
203                            })
204                            .await?;
205                        Ok(GetBalanceResponse::from(res.into_inner()))
206                    },
207                    "getting balance",
208                )
209                .await
210            }
211            NetworkMode::Reserved => {
212                self.with_retry(
213                    || async {
214                        let mut rpc = self.base_prover_network_client().await?;
215                        let res = rpc
216                            .get_balance(BaseGetBalanceRequest {
217                                address: self.signer.address().to_vec(),
218                            })
219                            .await?;
220                        Ok(GetBalanceResponse::from(res.into_inner()))
221                    },
222                    "getting balance",
223                )
224                .await
225            }
226        }
227    }
228
229    /// Get the verifying key hash from a verifying key.
230    ///
231    /// # Details
232    /// The verifying key hash is used to identify a program.
233    pub fn get_vk_hash(vk: &SP1VerifyingKey) -> Result<B256> {
234        let vk_hash = vk.hash_bytes();
235        Ok(B256::from_slice(&vk_hash))
236    }
237
238    /// Registers a program with the network if it is not already registered.
239    pub async fn register_program(&self, vk: &SP1VerifyingKey, elf: &[u8]) -> Result<B256> {
240        let vk_hash = Self::get_vk_hash(vk)?;
241
242        // Try to get the existing program.
243        if (self.get_program(vk_hash).await?).is_some() {
244            // The program already exists.
245            Ok(vk_hash)
246        } else {
247            // The program doesn't exist, create it.
248            self.create_program(vk_hash, vk, elf).await?;
249            tracing::info!("Registered program {:?}", vk_hash);
250            Ok(vk_hash)
251        }
252    }
253
254    /// Attempts to get the program on the network.
255    ///
256    /// # Details
257    /// Returns `None` if the program does not exist.
258    pub async fn get_program(&self, vk_hash: B256) -> Result<Option<GetProgramResponse>> {
259        match self.network_mode {
260            NetworkMode::Mainnet => {
261                self.with_retry(
262                    || async {
263                        let mut rpc = self.auction_prover_network_client().await?;
264                        match rpc
265                            .get_program(AuctionGetProgramRequest { vk_hash: vk_hash.to_vec() })
266                            .await
267                        {
268                            StdOk(response) => {
269                                Ok(Some(GetProgramResponse::from(response.into_inner())))
270                            }
271                            Err(status) if status.code() == Code::NotFound => Ok(None),
272                            Err(e) => Err(e.into()),
273                        }
274                    },
275                    "getting program",
276                )
277                .await
278            }
279            NetworkMode::Reserved => {
280                self.with_retry(
281                    || async {
282                        let mut rpc = self.base_prover_network_client().await?;
283                        match rpc
284                            .get_program(BaseGetProgramRequest { vk_hash: vk_hash.to_vec() })
285                            .await
286                        {
287                            StdOk(response) => {
288                                Ok(Some(GetProgramResponse::from(response.into_inner())))
289                            }
290                            Err(status) if status.code() == Code::NotFound => Ok(None),
291                            Err(e) => Err(e.into()),
292                        }
293                    },
294                    "getting program",
295                )
296                .await
297            }
298        }
299    }
300
301    /// Creates a new program on the network.
302    pub async fn create_program(
303        &self,
304        vk_hash: B256,
305        vk: &SP1VerifyingKey,
306        elf: &[u8],
307    ) -> Result<CreateProgramResponse> {
308        // Create the program artifact.
309        let program_uri = self.create_artifact_with_content(ArtifactType::Program, &elf).await?;
310
311        // Serialize the verifying key.
312        let vk_encoded = bincode::serialize(&vk)?;
313
314        // Send the request.
315        self.with_retry(
316            || async {
317                let mut rpc = self.prover_network_client().await?;
318                let nonce = self.get_nonce().await?;
319                let request_body = CreateProgramRequestBody {
320                    nonce,
321                    vk_hash: vk_hash.to_vec(),
322                    vk: vk_encoded.clone(),
323                    program_uri: program_uri.clone(),
324                };
325
326                Ok(rpc
327                    .create_program(CreateProgramRequest {
328                        format: MessageFormat::Binary.into(),
329                        signature: request_body.sign(&self.signer).await?,
330                        body: Some(request_body),
331                    })
332                    .await?
333                    .into_inner())
334            },
335            "creating program",
336        )
337        .await
338    }
339
340    /// Gets the proof request parameters from the network.
341    /// This is only available in Mainnet (auction) mode.
342    pub async fn get_proof_request_params(
343        &self,
344        mode: ProofMode,
345    ) -> Result<GetProofRequestParamsResponse> {
346        match self.network_mode {
347            NetworkMode::Mainnet => {
348                self.with_retry(
349                    || async {
350                        let mut rpc = self.auction_prover_network_client().await?;
351                        let response = rpc
352                            .get_proof_request_params(AuctionGetProofRequestParamsRequest {
353                                mode: mode.into(),
354                            })
355                            .await?
356                            .into_inner();
357                        Ok(GetProofRequestParamsResponse::from(response))
358                    },
359                    "getting proof request parameters",
360                )
361                .await
362            }
363            NetworkMode::Reserved => Ok(GetProofRequestParamsResponse::Unsupported),
364        }
365    }
366
367    /// Get all the proof requests that meet the filter criteria.
368    #[allow(clippy::too_many_arguments)]
369    pub async fn get_filtered_proof_requests(
370        &self,
371        version: Option<String>,
372        fulfillment_status: Option<i32>,
373        execution_status: Option<i32>,
374        minimum_deadline: Option<u64>,
375        vk_hash: Option<Vec<u8>>,
376        requester: Option<Vec<u8>>,
377        fulfiller: Option<Vec<u8>>,
378        from: Option<u64>,
379        to: Option<u64>,
380        limit: Option<u32>,
381        page: Option<u32>,
382        mode: Option<i32>,
383        not_bid_by: Option<Vec<u8>>,
384        execute_fail_cause: Option<i32>,
385        settlement_status: Option<i32>,
386        error: Option<i32>,
387    ) -> Result<GetFilteredProofRequestsResponse> {
388        match self.network_mode {
389            NetworkMode::Mainnet => {
390                self.with_retry(
391                    || {
392                        let version = version.clone();
393                        let vk_hash = vk_hash.clone();
394                        let requester = requester.clone();
395                        let fulfiller = fulfiller.clone();
396                        let not_bid_by = not_bid_by.clone();
397
398                        async move {
399                            let mut rpc = self.auction_prover_network_client().await?;
400                            let response = rpc
401                                .get_filtered_proof_requests(
402                                    AuctionGetFilteredProofRequestsRequest {
403                                        version,
404                                        fulfillment_status,
405                                        execution_status,
406                                        minimum_deadline,
407                                        vk_hash,
408                                        requester,
409                                        fulfiller,
410                                        from,
411                                        to,
412                                        limit,
413                                        page,
414                                        mode,
415                                        not_bid_by,
416                                        execute_fail_cause,
417                                        settlement_status,
418                                        error,
419                                        ..Default::default()
420                                    },
421                                )
422                                .await?
423                                .into_inner();
424                            Ok(GetFilteredProofRequestsResponse::from(response))
425                        }
426                    },
427                    "getting filtered proof requests",
428                )
429                .await
430            }
431            NetworkMode::Reserved => {
432                self.with_retry(
433                    || {
434                        let version = version.clone();
435                        let vk_hash = vk_hash.clone();
436                        let requester = requester.clone();
437                        let fulfiller = fulfiller.clone();
438                        let not_bid_by = not_bid_by.clone();
439
440                        async move {
441                            let mut rpc = self.base_prover_network_client().await?;
442                            let response = rpc
443                                .get_filtered_proof_requests(BaseGetFilteredProofRequestsRequest {
444                                    version,
445                                    fulfillment_status,
446                                    execution_status,
447                                    minimum_deadline,
448                                    vk_hash,
449                                    requester,
450                                    fulfiller,
451                                    from,
452                                    to,
453                                    limit,
454                                    page,
455                                    mode,
456                                    not_bid_by,
457                                    execute_fail_cause,
458                                    settlement_status,
459                                    error,
460                                    ..Default::default()
461                                })
462                                .await?
463                                .into_inner();
464                            Ok(GetFilteredProofRequestsResponse::from(response))
465                        }
466                    },
467                    "getting filtered proof requests",
468                )
469                .await
470            }
471        }
472    }
473
474    /// Get the status of a given proof.
475    ///
476    /// # Details
477    /// If the status is Fulfilled, the proof is also returned.
478    pub async fn get_proof_request_status<P: DeserializeOwned>(
479        &self,
480        request_id: B256,
481        timeout: Option<Duration>,
482    ) -> Result<(GetProofRequestStatusResponse, Option<P>)> {
483        // Get the status.
484        let res = match self.network_mode {
485            NetworkMode::Mainnet => {
486                let auction_response = self
487                    .with_retry_timeout(
488                        || async {
489                            let mut rpc = self.auction_prover_network_client().await?;
490                            Ok(rpc
491                                .get_proof_request_status(AuctionGetProofRequestStatusRequest {
492                                    request_id: request_id.to_vec(),
493                                })
494                                .await?
495                                .into_inner())
496                        },
497                        timeout.unwrap_or(DEFAULT_RETRY_TIMEOUT),
498                        "getting proof request status",
499                    )
500                    .await?;
501                GetProofRequestStatusResponse::from(auction_response)
502            }
503            NetworkMode::Reserved => {
504                let base_response = self
505                    .with_retry_timeout(
506                        || async {
507                            let mut rpc = self.base_prover_network_client().await?;
508                            Ok(rpc
509                                .get_proof_request_status(BaseGetProofRequestStatusRequest {
510                                    request_id: request_id.to_vec(),
511                                })
512                                .await?
513                                .into_inner())
514                        },
515                        timeout.unwrap_or(DEFAULT_RETRY_TIMEOUT),
516                        "getting proof request status",
517                    )
518                    .await?;
519                GetProofRequestStatusResponse::from(base_response)
520            }
521        };
522
523        let status = FulfillmentStatus::try_from(res.fulfillment_status())?;
524        let proof = match status {
525            FulfillmentStatus::Fulfilled => {
526                let proof_uri =
527                    res.proof_uri().ok_or_else(|| anyhow::anyhow!("No proof URI provided"))?;
528                let proof_bytes = self.download_artifact(proof_uri).await?;
529                Some(bincode::deserialize(&proof_bytes).context("Failed to deserialize proof")?)
530            }
531            _ => None,
532        };
533
534        Ok((res, proof))
535    }
536
537    /// Get the details of a given proof request.
538    pub async fn get_proof_request_details(
539        &self,
540        request_id: B256,
541        timeout: Option<Duration>,
542    ) -> Result<GetProofRequestDetailsResponse> {
543        let res = self
544            .with_retry_timeout(
545                || async {
546                    let mut rpc = self.prover_network_client().await?;
547                    Ok(rpc
548                        .get_proof_request_details(GetProofRequestDetailsRequest {
549                            request_id: request_id.to_vec(),
550                        })
551                        .await?
552                        .into_inner())
553                },
554                timeout.unwrap_or(DEFAULT_RETRY_TIMEOUT),
555                "getting proof request details",
556            )
557            .await?;
558
559        Ok(res)
560    }
561
562    /// Creates a proof request with the given verifying key hash and stdin.
563    ///
564    /// # Details
565    /// * `vk_hash`: The verifying key hash of the program to prove. Used to identify the program.
566    /// * `stdin`: The standard input to provide to the program.
567    /// * `mode`: The [`ProofMode`] to use.
568    /// * `version`: The version of the SP1 circuits to use.
569    /// * `strategy`: The [`FulfillmentStrategy`] to use.
570    /// * `timeout_secs`: The timeout for the proof request in seconds.
571    /// * `cycle_limit`: The cycle limit for the proof request.
572    /// * `gas_limit`: The gas limit for the proof request.
573    /// * `min_auction_period`: The minimum auction period for the proof request in seconds.
574    /// * `whitelist`: The auction whitelist for the proof request.
575    /// * `auctioneer`: The auctioneer for the proof request.
576    /// * `executor`: The executor for the proof request.
577    /// * `verifier`: The verifier for the proof request.
578    /// * `treasury`: The treasury for the proof request.
579    /// * `public_values_hash`: The hash of the public values to use for the proof.
580    /// * `base_fee`: The base fee to use for the proof request.
581    /// * `max_price_per_pgu`: The maximum price per PGU to use for the proof request.
582    /// * `domain`: The domain bytes to use for the proof request.
583    #[allow(clippy::too_many_arguments)]
584    #[allow(unused_variables)]
585    pub async fn request_proof(
586        &self,
587        vk_hash: B256,
588        stdin: &SP1Stdin,
589        mode: ProofMode,
590        version: &str,
591        strategy: FulfillmentStrategy,
592        timeout_secs: u64,
593        cycle_limit: u64,
594        gas_limit: u64,
595        min_auction_period: u64,
596        whitelist: Option<Vec<Address>>,
597        auctioneer: Address,
598        executor: Address,
599        verifier: Address,
600        treasury: Address,
601        public_values_hash: Option<Vec<u8>>,
602        base_fee: u64,
603        max_price_per_pgu: u64,
604        domain: Vec<u8>,
605        private_stdin: bool,
606    ) -> Result<RequestProofResponse> {
607        // Calculate the deadline.
608        let start = SystemTime::now();
609        let since_the_epoch = start.duration_since(UNIX_EPOCH).expect("Invalid start time");
610        let deadline = since_the_epoch.as_secs() + timeout_secs;
611
612        let stdin_uri = self
613            .create_artifact_with_content(
614                if private_stdin { ArtifactType::PrivateStdin } else { ArtifactType::Stdin },
615                &stdin,
616            )
617            .await?;
618
619        // Send the request.
620        match self.network_mode {
621            NetworkMode::Mainnet => {
622                self.with_retry(
623                    || async {
624                        let mut rpc = self.auction_prover_network_client().await?;
625                        let nonce = self.get_nonce().await?;
626
627                        let whitelist = if let Some(whitelist) = &whitelist {
628                            whitelist.iter().map(|addr| addr.to_vec()).collect()
629                        } else {
630                            let result = rpc
631                                .get_provers_by_uptime(AuctionGetProversByUptimeRequest {
632                                    high_availability_only: false,
633                                })
634                                .await?;
635                            result.into_inner().provers
636                        };
637
638                        let request_body = AuctionRequestProofRequestBody {
639                            nonce,
640                            version: format!("sp1-{version}"),
641                            vk_hash: vk_hash.to_vec(),
642                            mode: mode.into(),
643                            strategy: strategy.into(),
644                            stdin_uri: stdin_uri.clone(),
645                            deadline,
646                            cycle_limit,
647                            gas_limit,
648                            min_auction_period,
649                            whitelist,
650                            domain: domain.clone(),
651                            auctioneer: auctioneer.to_vec(),
652                            executor: executor.to_vec(),
653                            verifier: verifier.to_vec(),
654                            treasury: treasury.to_vec(),
655                            public_values_hash: public_values_hash.clone(),
656                            base_fee: base_fee.to_string(),
657                            max_price_per_pgu: max_price_per_pgu.to_string(),
658                            variant: AuctionTransactionVariant::RequestVariant.into(),
659                            stdin_private: private_stdin,
660                        };
661
662                        let request_response = rpc
663                            .request_proof(AuctionRequestProofRequest {
664                                format: AuctionMessageFormat::Binary.into(),
665                                signature: request_body.sign(&self.signer).await?,
666                                body: Some(request_body),
667                            })
668                            .await?
669                            .into_inner();
670
671                        Ok(RequestProofResponse::from(request_response))
672                    },
673                    "requesting proof",
674                )
675                .await
676            }
677            NetworkMode::Reserved => {
678                self.with_retry(
679                    || async {
680                        let mut rpc = self.base_prover_network_client().await?;
681                        let nonce = self.get_nonce().await?;
682
683                        let request_body = BaseRequestProofRequestBody {
684                            nonce,
685                            version: format!("sp1-{version}"),
686                            vk_hash: vk_hash.to_vec(),
687                            mode: mode.into(),
688                            strategy: strategy.into(),
689                            stdin_uri: stdin_uri.clone(),
690                            deadline,
691                            cycle_limit,
692                            gas_limit,
693                            min_auction_period,
694                            whitelist: whitelist
695                                .clone()
696                                .map(|list| list.into_iter().map(|addr| addr.to_vec()).collect())
697                                .unwrap_or_default(),
698                            stdin_private: private_stdin,
699                        };
700
701                        let request_response = rpc
702                            .request_proof(BaseRequestProofRequest {
703                                format: BaseMessageFormat::Binary.into(),
704                                signature: request_body.sign(&self.signer).await?,
705                                body: Some(request_body),
706                            })
707                            .await?
708                            .into_inner();
709
710                        Ok(RequestProofResponse::from(request_response))
711                    },
712                    "requesting proof",
713                )
714                .await
715            }
716        }
717    }
718
719    // NetworkMode-aware generic client for shared operations (create_program,
720    // get_proof_request_details).
721    pub(crate) async fn prover_network_client(
722        &self,
723    ) -> Result<AuctionProverNetworkClient<Channel>> {
724        // For shared operations, we use the auction client type as it provides the default types.
725        // The actual network routing is handled by the RPC URL which is correctly set based on
726        // network_mode.
727        self.auction_prover_network_client().await
728    }
729
730    /// Returns the shared gRPC channel, built at most once.
731    ///
732    /// `OnceCell` rather than `new()` since `new()` is infallible but `configure_endpoint` isn't.
733    /// `connect_lazy` reconnects transparently, so the connection is reused across polls and a
734    /// dropped one self-heals on next use.
735    async fn channel(&self) -> Result<Channel> {
736        self.channel
737            .get_or_try_init(|| async {
738                tracing::debug!(rpc_url = %self.rpc_url, "establishing gRPC channel");
739                Ok(grpc::configure_endpoint(&self.rpc_url)?.connect_lazy())
740            })
741            .await
742            .cloned()
743    }
744
745    // Helper methods for runtime proto type selection.
746    pub(crate) async fn auction_prover_network_client(
747        &self,
748    ) -> Result<AuctionProverNetworkClient<Channel>> {
749        Ok(AuctionProverNetworkClient::new(self.channel().await?))
750    }
751
752    pub(crate) async fn base_prover_network_client(
753        &self,
754    ) -> Result<BaseProverNetworkClient<Channel>> {
755        Ok(BaseProverNetworkClient::new(self.channel().await?))
756    }
757
758    pub(crate) async fn artifact_store_client(&self) -> Result<ArtifactStoreClient<Channel>> {
759        Ok(ArtifactStoreClient::new(self.channel().await?))
760    }
761
762    pub(crate) async fn create_artifact_with_content<T: Serialize + Send + Sync>(
763        &self,
764        artifact_type: ArtifactType,
765        item: &T,
766    ) -> Result<String> {
767        // Acquire the store inside the retry so a transient channel-connect failure is retried.
768        let response = self
769            .with_retry(
770                || async {
771                    let mut store = self.artifact_store_client().await?;
772                    let signature =
773                        sign_message("create_artifact".as_bytes(), &self.signer).await?;
774                    let request =
775                        CreateArtifactRequest { artifact_type: artifact_type.into(), signature };
776                    Ok(store.create_artifact(request).await?.into_inner())
777                },
778                "creating artifact",
779            )
780            .await?;
781
782        let presigned_url = response.artifact_presigned_url;
783        let uri = response.artifact_uri;
784
785        // Serialize and compress the content once before retrying uploads.
786        // Using compression level 3 for a good balance of speed and compression ratio.
787        let serialized = bincode::serialize::<T>(item)?;
788        let compressed = zstd::encode_all(&serialized[..], 3)
789            .map_err(|e| anyhow::anyhow!("Failed to compress artifact: {e}"))?;
790
791        // Upload the compressed content.
792        self.with_retry(
793            || async {
794                let response =
795                    self.http.put(&presigned_url).body(compressed.clone()).send().await?;
796
797                if !response.status().is_success() {
798                    return Err(anyhow::anyhow!(
799                        "Failed to upload artifact: HTTP {}",
800                        response.status()
801                    ));
802                }
803                Ok(())
804            },
805            "uploading artifact content",
806        )
807        .await?;
808
809        Ok(uri)
810    }
811
812    pub(crate) async fn download_artifact(&self, uri: &str) -> Result<Vec<u8>> {
813        self.with_retry(
814            || async {
815                let response =
816                    self.http.get(uri).send().await.context("Failed to download from URI")?;
817
818                if !response.status().is_success() {
819                    return Err(anyhow::anyhow!(
820                        "Failed to download artifact: HTTP {}",
821                        response.status()
822                    ));
823                }
824
825                Ok(response.bytes().await.context("Failed to read response body")?.to_vec())
826            },
827            "downloading artifact",
828        )
829        .await
830    }
831
832    /// Cancel a proof request. This is only available in Mainnet (auction) mode.
833    pub async fn cancel_request(&self, request_id: B256) -> Result<CancelRequestResponse> {
834        match self.network_mode {
835            NetworkMode::Mainnet => {
836                self.with_retry(
837                    || async {
838                        let mut rpc = self.auction_prover_network_client().await?;
839                        let nonce = self.get_nonce().await?;
840
841                        let request_body = AuctionCancelRequestRequestBody {
842                            nonce,
843                            request_id: request_id.to_vec(),
844                        };
845
846                        let response = rpc
847                            .cancel_request(AuctionCancelRequestRequest {
848                                format: AuctionMessageFormat::Binary.into(),
849                                signature: request_body.sign(&self.signer).await?,
850                                body: Some(request_body),
851                            })
852                            .await?
853                            .into_inner();
854
855                        Ok(CancelRequestResponse::from(response))
856                    },
857                    "cancelling request",
858                )
859                .await
860            }
861            NetworkMode::Reserved => Ok(CancelRequestResponse::Unsupported),
862        }
863    }
864}
865
866#[cfg(test)]
867mod test {
868    use crate::network::{signer::NetworkSigner, NetworkMode, RESERVED_RPC_URL};
869
870    #[test]
871    fn test_can_create_network_client_with_0x_bytes() {
872        let private_key = hex::encode(alloy_signer_local::PrivateKeySigner::random().to_bytes());
873        let signer = NetworkSigner::local(&private_key).unwrap();
874        let _ = super::NetworkClient::new(signer, RESERVED_RPC_URL, NetworkMode::Reserved);
875    }
876}