Skip to main content

stoffel/
client.rs

1//! Client-side builders and computation handles.
2//!
3//! The SDK validates client configuration and program input arity here. Live
4//! submission and ordering verification remain delegated to `stoffel-networking`
5//! and the MPC runtime instead of being simulated in the SDK.
6
7use std::collections::BTreeSet;
8use std::fmt;
9use std::net::SocketAddr;
10use std::path::Path;
11use std::str::FromStr;
12use std::sync::{Arc, Mutex};
13use std::time::Duration;
14
15use ark_bls12_381::{Fr, G1Projective};
16use ark_ff::{PrimeField, Zero};
17use serde::{Deserialize, Serialize};
18use stoffel_mpc_coordinator_off_chain::{
19    node_rpc::NodeRPCClient as OffChainNodeRPCClient, OffChainCoordinatorClient,
20};
21use stoffel_mpc_coordinator_shared::Coordinator as _;
22use stoffel_vm_types::core_types::ShareType;
23use stoffelmpc_mpc::common::share::feldman::FeldmanShamirShare;
24use stoffelmpc_mpc::honeybadger::robust_interpolate::robust_interpolate::RobustShare;
25use stoffelnet::network_utils::Network as _;
26use stoffelnet::transports::quic::QuicNetworkManager;
27
28use crate::config::{validate_socket_address, Curve, MpcBackend, NetworkConfig, NetworkDeployment};
29use crate::consensus::VerifiedOrdering;
30use crate::error::{Error, Result};
31use crate::program::Program;
32use crate::types::{
33    ClientId, ClientValueType, GeneratedProgramManifest, TypedClientInputs, TypedClientOutputs,
34    Value,
35};
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(rename_all = "snake_case")]
39pub enum ClientState {
40    Disconnected,
41    Connected,
42}
43
44impl fmt::Display for ClientState {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        f.write_str(match self {
47            ClientState::Disconnected => "disconnected",
48            ClientState::Connected => "connected",
49        })
50    }
51}
52
53impl FromStr for ClientState {
54    type Err = Error;
55
56    fn from_str(value: &str) -> Result<Self> {
57        match value.trim() {
58            "disconnected" => Ok(ClientState::Disconnected),
59            "connected" => Ok(ClientState::Connected),
60            state => Err(Error::Configuration(format!(
61                "unsupported client state '{state}'; expected 'disconnected' or 'connected'"
62            ))),
63        }
64    }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
68#[serde(rename_all = "snake_case")]
69pub enum ComputationStatus {
70    Pending,
71    Completed,
72    Cancelled,
73}
74
75impl fmt::Display for ComputationStatus {
76    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77        f.write_str(match self {
78            ComputationStatus::Pending => "pending",
79            ComputationStatus::Completed => "completed",
80            ComputationStatus::Cancelled => "cancelled",
81        })
82    }
83}
84
85impl FromStr for ComputationStatus {
86    type Err = Error;
87
88    fn from_str(value: &str) -> Result<Self> {
89        match value.trim() {
90            "pending" => Ok(ComputationStatus::Pending),
91            "completed" => Ok(ComputationStatus::Completed),
92            "cancelled" => Ok(ComputationStatus::Cancelled),
93            status => Err(Error::Configuration(format!(
94                "unsupported computation status '{status}'; expected 'pending', 'completed', or 'cancelled'"
95            ))),
96        }
97    }
98}
99
100#[derive(Debug, Clone)]
101pub struct ClientBuilder {
102    servers: Vec<String>,
103    program: Option<Program>,
104    client_id: ClientId,
105    verified_ordering: Option<VerifiedOrdering>,
106    connection_timeout: Duration,
107    offchain_io: Option<OffChainClientConfig>,
108    config_error: Option<String>,
109}
110
111impl ClientBuilder {
112    pub fn new() -> Self {
113        Self {
114            servers: Vec::new(),
115            program: None,
116            client_id: 0,
117            verified_ordering: None,
118            connection_timeout: Duration::from_secs(10),
119            offchain_io: None,
120            config_error: None,
121        }
122    }
123
124    pub fn servers<I, S>(mut self, servers: I) -> Self
125    where
126        I: IntoIterator<Item = S>,
127        S: Into<String>,
128    {
129        self.servers = servers.into_iter().map(Into::into).collect();
130        self
131    }
132
133    pub fn with_servers(self, servers: &[&str]) -> Self {
134        self.servers(servers.iter().copied())
135    }
136
137    pub fn server(mut self, server: impl Into<String>) -> Self {
138        self.servers.push(server.into());
139        self
140    }
141
142    pub fn client_id(mut self, client_id: ClientId) -> Self {
143        self.client_id = client_id;
144        self
145    }
146
147    pub fn with_program(mut self, program: Program) -> Self {
148        self.program = Some(program);
149        self
150    }
151
152    pub fn with_verified_ordering(mut self, ordering: VerifiedOrdering) -> Self {
153        self.verified_ordering = Some(ordering);
154        self
155    }
156
157    pub fn connection_timeout(mut self, timeout: Duration) -> Self {
158        self.connection_timeout = timeout;
159        self
160    }
161
162    pub fn offchain_io(mut self, config: OffChainClientConfig) -> Self {
163        self.offchain_io = Some(config);
164        self
165    }
166
167    pub fn configured_servers(&self) -> &[String] {
168        &self.servers
169    }
170
171    pub fn configured_client_id(&self) -> ClientId {
172        self.client_id
173    }
174
175    pub fn configured_program(&self) -> Option<&Program> {
176        self.program.as_ref()
177    }
178
179    pub fn has_configured_program(&self) -> bool {
180        self.program.is_some()
181    }
182
183    pub fn configured_verified_ordering(&self) -> Option<&VerifiedOrdering> {
184        self.verified_ordering.as_ref()
185    }
186
187    pub fn has_configured_verified_ordering(&self) -> bool {
188        self.verified_ordering.is_some()
189    }
190
191    pub fn configured_connection_timeout(&self) -> Duration {
192        self.connection_timeout
193    }
194
195    pub fn configured_offchain_io(&self) -> Option<&OffChainClientConfig> {
196        self.offchain_io.as_ref()
197    }
198
199    pub fn has_configured_offchain_io(&self) -> bool {
200        self.offchain_io.is_some()
201    }
202
203    pub fn network_config(mut self, config: &NetworkConfig) -> Self {
204        match config.server_addresses() {
205            Ok(addresses) => {
206                self.servers = addresses;
207            }
208            Err(error) => {
209                self.config_error = Some(error.to_string());
210            }
211        }
212        self
213    }
214
215    /// Use all server addresses from a validated deployment plan.
216    ///
217    /// This only configures the client builder; live connection and submission
218    /// still belong to `stoffel-networking` integration.
219    pub fn network_deployment(mut self, deployment: &NetworkDeployment) -> Self {
220        self.servers = deployment.server_addresses();
221        self
222    }
223
224    pub fn network_config_file(mut self, path: impl AsRef<Path>) -> Self {
225        match NetworkConfig::from_toml_file(path) {
226            Ok(config) => self.network_config(&config),
227            Err(error) => {
228                self.config_error = Some(error.to_string());
229                self
230            }
231        }
232    }
233
234    pub(crate) fn configuration_error(mut self, error: impl Into<String>) -> Self {
235        self.config_error = Some(error.into());
236        self
237    }
238
239    pub fn build(self) -> Result<StoffelClient> {
240        if let Some(error) = self.config_error {
241            return Err(Error::Configuration(error));
242        }
243        if self.connection_timeout.is_zero() {
244            return Err(Error::Configuration(
245                "client connection timeout must be greater than zero".to_owned(),
246            ));
247        }
248        if let Some(config) = &self.offchain_io {
249            config.validate()?;
250        }
251        if self.servers.is_empty() {
252            return Err(Error::Configuration(
253                "client requires at least one server address".to_owned(),
254            ));
255        }
256        if let Some(index) = self
257            .servers
258            .iter()
259            .position(|server| server.trim().is_empty())
260        {
261            return Err(Error::Configuration(format!(
262                "client server address at index {index} must not be empty"
263            )));
264        }
265        for (index, server) in self.servers.iter().enumerate() {
266            validate_socket_address(&format!("client server address at index {index}"), server)?;
267        }
268        let mut server_addresses = BTreeSet::new();
269        for server in &self.servers {
270            if !server_addresses.insert(server.as_str()) {
271                return Err(Error::Configuration(format!(
272                    "duplicate client server address '{server}'"
273                )));
274            }
275        }
276        Ok(StoffelClient {
277            servers: self.servers,
278            program: self.program,
279            client_id: self.client_id,
280            transport_client_id: None,
281            verified_ordering: self.verified_ordering,
282            network: None,
283            offchain_io: self.offchain_io,
284            state: ClientState::Disconnected,
285        })
286    }
287
288    #[tracing::instrument(skip_all, fields(server_count = self.servers.len()))]
289    pub async fn connect(self) -> Result<StoffelClient> {
290        let connection_timeout = self.connection_timeout;
291        let mut client = self.build()?;
292        let _ = rustls::crypto::ring::default_provider().install_default();
293        let mut network = QuicNetworkManager::new();
294
295        for server in &client.servers {
296            let address = server.parse::<SocketAddr>().map_err(|error| {
297                Error::Configuration(format!("invalid client server address '{server}': {error}"))
298            })?;
299            match tokio::time::timeout(connection_timeout, network.connect_as_client(address)).await
300            {
301                Ok(Ok(_)) => {}
302                Ok(Err(error)) => {
303                    return Err(Error::NetworkConnection(format!(
304                        "failed to connect to server '{server}': {error}"
305                    )));
306                }
307                Err(_) => {
308                    return Err(Error::NetworkConnection(format!(
309                        "timed out after {:?} connecting to server '{server}'",
310                        connection_timeout
311                    )));
312                }
313            }
314        }
315
316        client.transport_client_id = Some(network.local_derived_id());
317        client.network = Some(network);
318        client.state = ClientState::Connected;
319        Ok(client)
320    }
321}
322
323#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
324pub struct OffChainClientConfig {
325    pub coordinator_host: String,
326    pub coordinator_port: u16,
327    pub timestamp: u64,
328    #[serde(default)]
329    pub client_slot: u64,
330    #[serde(default)]
331    pub input_start_index: u64,
332    pub parties: usize,
333    pub threshold: usize,
334    pub backend: MpcBackend,
335    pub node_rpc_addresses: Vec<String>,
336    pub cert_der: Vec<u8>,
337    pub key_der: Vec<u8>,
338    pub output_count: u64,
339    pub input_types: Vec<ShareType>,
340    pub output_types: Vec<ShareType>,
341    #[serde(with = "duration_millis")]
342    pub timeout: Duration,
343}
344
345impl OffChainClientConfig {
346    pub fn builder() -> OffChainClientConfigBuilder {
347        OffChainClientConfigBuilder::default()
348    }
349
350    pub fn validate(&self) -> Result<()> {
351        if self.coordinator_host.trim().is_empty() {
352            return Err(Error::Configuration(
353                "off-chain coordinator host must not be empty".to_owned(),
354            ));
355        }
356        if self.timestamp == 0 {
357            return Err(Error::Configuration(
358                "off-chain coordinator timestamp must be greater than zero".to_owned(),
359            ));
360        }
361        if self.threshold == 0 {
362            return Err(Error::Configuration(
363                "off-chain client threshold must be greater than zero".to_owned(),
364            ));
365        }
366        if let MpcBackend::Avss { curve } = self.backend {
367            if curve != Curve::Bls12_381 {
368                return Err(Error::Unsupported(
369                    "off-chain client IO currently supports AVSS over bls12_381".to_owned(),
370                ));
371            }
372        }
373        self.backend
374            .ensure_minimum_parties(self.parties, self.threshold)?;
375        if self.node_rpc_addresses.is_empty() {
376            return Err(Error::Configuration(
377                "off-chain client IO requires at least one node RPC address".to_owned(),
378            ));
379        }
380        for (index, address) in self.node_rpc_addresses.iter().enumerate() {
381            validate_socket_address(&format!("node RPC address at index {index}"), address)?;
382        }
383        if self.cert_der.is_empty() {
384            return Err(Error::Configuration(
385                "off-chain client IO requires a client certificate DER".to_owned(),
386            ));
387        }
388        if self.key_der.is_empty() {
389            return Err(Error::Configuration(
390                "off-chain client IO requires a client key DER".to_owned(),
391            ));
392        }
393        if self.timeout.is_zero() {
394            return Err(Error::Configuration(
395                "off-chain client IO timeout must be greater than zero".to_owned(),
396            ));
397        }
398        if !self.output_types.is_empty() && self.output_count != self.output_types.len() as u64 {
399            return Err(Error::Configuration(format!(
400                "off-chain client output_count {} does not match {} configured output type(s)",
401                self.output_count,
402                self.output_types.len()
403            )));
404        }
405        Ok(())
406    }
407
408    fn node_rpc_endpoints(&self) -> Result<Vec<(String, u16)>> {
409        self.node_rpc_addresses
410            .iter()
411            .map(|address| {
412                let parsed = address.parse::<SocketAddr>().map_err(|error| {
413                    Error::Configuration(format!("invalid node RPC address '{address}': {error}"))
414                })?;
415                Ok((parsed.ip().to_string(), parsed.port()))
416            })
417            .collect()
418    }
419}
420
421#[derive(Debug, Clone)]
422pub struct OffChainClientConfigBuilder {
423    coordinator_host: String,
424    coordinator_port: Option<u16>,
425    timestamp: Option<u64>,
426    client_slot: u64,
427    input_start_index: u64,
428    parties: usize,
429    threshold: usize,
430    backend: MpcBackend,
431    node_rpc_addresses: Vec<String>,
432    cert_der: Option<Vec<u8>>,
433    key_der: Option<Vec<u8>>,
434    output_count: u64,
435    input_types: Vec<ShareType>,
436    output_types: Vec<ShareType>,
437    timeout: Duration,
438    config_error: Option<String>,
439}
440
441impl OffChainClientConfigBuilder {
442    pub fn coordinator(mut self, host: impl Into<String>, port: u16) -> Self {
443        self.coordinator_host = host.into();
444        self.coordinator_port = Some(port);
445        self
446    }
447
448    pub fn timestamp(mut self, timestamp: u64) -> Self {
449        self.timestamp = Some(timestamp);
450        self
451    }
452
453    pub fn client_slot(mut self, client_slot: u64) -> Self {
454        self.client_slot = client_slot;
455        self
456    }
457
458    pub fn input_start_index(mut self, input_start_index: u64) -> Self {
459        self.input_start_index = input_start_index;
460        self
461    }
462
463    pub fn parties(mut self, parties: usize) -> Self {
464        self.parties = parties;
465        self
466    }
467
468    pub fn threshold(mut self, threshold: usize) -> Self {
469        self.threshold = threshold;
470        self
471    }
472
473    pub fn backend(mut self, backend: MpcBackend) -> Self {
474        self.backend = backend;
475        self
476    }
477
478    pub fn manifest<M: GeneratedProgramManifest>(self) -> Self {
479        self.backend(M::BACKEND)
480    }
481
482    pub fn honeybadger(mut self) -> Self {
483        self.backend = MpcBackend::HoneyBadger;
484        self
485    }
486
487    pub fn avss(mut self, curve: Curve) -> Self {
488        self.backend = MpcBackend::Avss { curve };
489        self
490    }
491
492    pub fn node_rpc_addresses<I, S>(mut self, addresses: I) -> Self
493    where
494        I: IntoIterator<Item = S>,
495        S: Into<String>,
496    {
497        self.node_rpc_addresses = addresses.into_iter().map(Into::into).collect();
498        self
499    }
500
501    pub fn node_rpc_address(mut self, address: impl Into<String>) -> Self {
502        self.node_rpc_addresses.push(address.into());
503        self
504    }
505
506    pub fn identity_der(mut self, cert_der: Vec<u8>, key_der: Vec<u8>) -> Self {
507        self.cert_der = Some(cert_der);
508        self.key_der = Some(key_der);
509        self
510    }
511
512    pub fn identity_files(
513        mut self,
514        cert_path: impl AsRef<Path>,
515        key_path: impl AsRef<Path>,
516    ) -> Self {
517        match std::fs::read(cert_path)
518            .and_then(|cert| std::fs::read(key_path).map(|key| (cert, key)))
519        {
520            Ok((cert, key)) => {
521                self.cert_der = Some(cert);
522                self.key_der = Some(key);
523            }
524            Err(error) => {
525                self.config_error = Some(error.to_string());
526            }
527        }
528        self
529    }
530
531    pub fn output_count(mut self, output_count: u64) -> Self {
532        self.output_count = output_count;
533        self
534    }
535
536    pub fn input_types<I>(mut self, input_types: I) -> Self
537    where
538        I: IntoIterator<Item = ShareType>,
539    {
540        self.input_types = input_types.into_iter().collect();
541        self
542    }
543
544    pub fn output_types<I>(mut self, output_types: I) -> Self
545    where
546        I: IntoIterator<Item = ShareType>,
547    {
548        self.output_types = output_types.into_iter().collect();
549        self.output_count = self.output_types.len() as u64;
550        self
551    }
552
553    pub fn timeout(mut self, timeout: Duration) -> Self {
554        self.timeout = timeout;
555        self
556    }
557
558    pub fn build(self) -> Result<OffChainClientConfig> {
559        if let Some(error) = self.config_error {
560            return Err(Error::Io(std::io::Error::other(error)));
561        }
562        let config = OffChainClientConfig {
563            coordinator_host: self.coordinator_host,
564            coordinator_port: self.coordinator_port.ok_or_else(|| {
565                Error::Configuration("off-chain coordinator port is required".to_owned())
566            })?,
567            timestamp: self.timestamp.ok_or_else(|| {
568                Error::Configuration("off-chain coordinator timestamp is required".to_owned())
569            })?,
570            client_slot: self.client_slot,
571            input_start_index: self.input_start_index,
572            parties: self.parties,
573            threshold: self.threshold,
574            backend: self.backend,
575            node_rpc_addresses: self.node_rpc_addresses,
576            cert_der: self.cert_der.ok_or_else(|| {
577                Error::Configuration("off-chain client certificate DER is required".to_owned())
578            })?,
579            key_der: self.key_der.ok_or_else(|| {
580                Error::Configuration("off-chain client key DER is required".to_owned())
581            })?,
582            output_count: self.output_count,
583            input_types: self.input_types,
584            output_types: self.output_types,
585            timeout: self.timeout,
586        };
587        config.validate()?;
588        Ok(config)
589    }
590}
591
592impl Default for OffChainClientConfigBuilder {
593    fn default() -> Self {
594        Self {
595            coordinator_host: "127.0.0.1".to_owned(),
596            coordinator_port: None,
597            timestamp: None,
598            client_slot: 0,
599            input_start_index: 0,
600            parties: 5,
601            threshold: 1,
602            backend: MpcBackend::HoneyBadger,
603            node_rpc_addresses: Vec::new(),
604            cert_der: None,
605            key_der: None,
606            output_count: 0,
607            input_types: Vec::new(),
608            output_types: Vec::new(),
609            timeout: Duration::from_secs(30),
610            config_error: None,
611        }
612    }
613}
614
615impl Default for ClientBuilder {
616    fn default() -> Self {
617        Self::new()
618    }
619}
620
621#[derive(Debug, Clone)]
622pub struct StoffelClient {
623    servers: Vec<String>,
624    program: Option<Program>,
625    client_id: ClientId,
626    transport_client_id: Option<ClientId>,
627    verified_ordering: Option<VerifiedOrdering>,
628    network: Option<QuicNetworkManager>,
629    offchain_io: Option<OffChainClientConfig>,
630    state: ClientState,
631}
632
633#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
634pub struct ClientSummary {
635    pub client_id: ClientId,
636    pub transport_client_id: Option<ClientId>,
637    pub server_count: usize,
638    pub has_program: bool,
639    pub has_verified_ordering: bool,
640    pub has_offchain_io: bool,
641    pub connected: bool,
642    pub state: ClientState,
643}
644
645impl StoffelClient {
646    pub fn builder() -> ClientBuilder {
647        ClientBuilder::new()
648    }
649
650    #[tracing::instrument(skip_all, fields(server_count = servers.len()))]
651    pub async fn connect(servers: &[&str]) -> Result<Self> {
652        ClientBuilder::new()
653            .servers(servers.iter().copied())
654            .connect()
655            .await
656    }
657
658    #[tracing::instrument(skip_all, fields(client_id = self.client_id))]
659    pub async fn disconnect(self) -> Result<()> {
660        Ok(())
661    }
662
663    #[tracing::instrument(skip_all, fields(client_id = self.client_id, input_count = inputs.len()))]
664    pub async fn run<V>(&self, inputs: &[V]) -> Result<Vec<Value>>
665    where
666        V: Clone + Into<Value>,
667    {
668        self.run_function("main", inputs).await
669    }
670
671    #[tracing::instrument(skip_all, fields(client_id = self.client_id, function = name, input_count = inputs.len()))]
672    pub async fn run_function<V>(&self, name: &str, inputs: &[V]) -> Result<Vec<Value>>
673    where
674        V: Clone + Into<Value>,
675    {
676        let inputs = inputs
677            .iter()
678            .map(|value| value.clone().into())
679            .collect::<Vec<Value>>();
680        self.validate_submission_inputs(name, inputs.len())?;
681        self.run_offchain_inputs(&inputs).await
682    }
683
684    #[tracing::instrument(skip_all, fields(client_id = self.client_id))]
685    pub async fn run_typed<I, O>(&self, inputs: I) -> Result<O>
686    where
687        I: TypedClientInputs,
688        O: TypedClientOutputs,
689    {
690        self.run_function_typed("main", inputs).await
691    }
692
693    #[tracing::instrument(skip_all, fields(client_id = self.client_id))]
694    pub async fn run_typed_with_manifest<M, I, O>(&self, inputs: I) -> Result<O>
695    where
696        M: GeneratedProgramManifest,
697        I: TypedClientInputs,
698        O: TypedClientOutputs,
699    {
700        self.run_function_typed_with_manifest::<M, I, O>("main", inputs)
701            .await
702    }
703
704    #[tracing::instrument(skip_all, fields(client_id = self.client_id, function = name))]
705    pub async fn run_function_typed<I, O>(&self, name: &str, inputs: I) -> Result<O>
706    where
707        I: TypedClientInputs,
708        O: TypedClientOutputs,
709    {
710        self.validate_typed_submission(name, &I::value_types(), &O::value_types())?;
711        let outputs = self.run_offchain_inputs(&inputs.into_values()).await?;
712        O::from_values(outputs)
713    }
714
715    #[tracing::instrument(skip_all, fields(client_id = self.client_id, function = name))]
716    pub async fn run_function_typed_with_manifest<M, I, O>(
717        &self,
718        name: &str,
719        inputs: I,
720    ) -> Result<O>
721    where
722        M: GeneratedProgramManifest,
723        I: TypedClientInputs,
724        O: TypedClientOutputs,
725    {
726        self.validate_generated_typed_submission::<M>(name, &I::value_types(), &O::value_types())?;
727        let outputs = self.run_offchain_inputs(&inputs.into_values()).await?;
728        O::from_values(outputs)
729    }
730
731    #[tracing::instrument(skip_all, fields(client_id = self.client_id, input_count = inputs.len()))]
732    pub async fn submit<V>(&self, inputs: &[V]) -> Result<ComputationHandle>
733    where
734        V: Clone + Into<Value>,
735    {
736        self.submit_function("main", inputs).await
737    }
738
739    #[tracing::instrument(skip_all, fields(client_id = self.client_id, function = name, input_count = inputs.len()))]
740    pub async fn submit_function<V>(&self, name: &str, inputs: &[V]) -> Result<ComputationHandle>
741    where
742        V: Clone + Into<Value>,
743    {
744        let inputs = inputs
745            .iter()
746            .map(|value| value.clone().into())
747            .collect::<Vec<Value>>();
748        self.validate_submission_inputs(name, inputs.len())?;
749        let config = self.offchain_io.as_ref().cloned().ok_or_else(|| {
750            Error::Configuration(
751                "client run/submit requires off-chain client IO configuration".to_owned(),
752            )
753        })?;
754        let handle = ComputationHandle::submitted();
755        let task_handle = handle.clone();
756        tokio::spawn(async move {
757            let result = run_offchain_inputs_with_config(&config, &inputs).await;
758            task_handle.complete(result);
759        });
760        Ok(handle)
761    }
762
763    #[tracing::instrument(skip_all, fields(client_id = self.client_id))]
764    pub async fn submit_typed<I>(&self, inputs: I) -> Result<ComputationHandle>
765    where
766        I: TypedClientInputs,
767    {
768        self.submit_function_typed("main", inputs).await
769    }
770
771    #[tracing::instrument(skip_all, fields(client_id = self.client_id))]
772    pub async fn submit_typed_with_manifest<M, I>(&self, inputs: I) -> Result<ComputationHandle>
773    where
774        M: GeneratedProgramManifest,
775        I: TypedClientInputs,
776    {
777        self.submit_function_typed_with_manifest::<M, I>("main", inputs)
778            .await
779    }
780
781    #[tracing::instrument(skip_all, fields(client_id = self.client_id, function = name))]
782    pub async fn submit_function_typed<I>(&self, name: &str, inputs: I) -> Result<ComputationHandle>
783    where
784        I: TypedClientInputs,
785    {
786        self.validate_typed_inputs(name, &I::value_types())?;
787        let inputs = inputs.into_values();
788        let config = self.offchain_io.as_ref().cloned().ok_or_else(|| {
789            Error::Configuration(
790                "client run/submit requires off-chain client IO configuration".to_owned(),
791            )
792        })?;
793        let handle = ComputationHandle::submitted();
794        let task_handle = handle.clone();
795        tokio::spawn(async move {
796            let result = run_offchain_inputs_with_config(&config, &inputs).await;
797            task_handle.complete(result);
798        });
799        Ok(handle)
800    }
801
802    #[tracing::instrument(skip_all, fields(client_id = self.client_id, function = name))]
803    pub async fn submit_function_typed_with_manifest<M, I>(
804        &self,
805        name: &str,
806        inputs: I,
807    ) -> Result<ComputationHandle>
808    where
809        M: GeneratedProgramManifest,
810        I: TypedClientInputs,
811    {
812        self.validate_generated_typed_inputs::<M>(name, &I::value_types())?;
813        let inputs = inputs.into_values();
814        let config = self.offchain_io.as_ref().cloned().ok_or_else(|| {
815            Error::Configuration(
816                "client run/submit requires off-chain client IO configuration".to_owned(),
817            )
818        })?;
819        let handle = ComputationHandle::submitted();
820        let task_handle = handle.clone();
821        tokio::spawn(async move {
822            let result = run_offchain_inputs_with_config(&config, &inputs).await;
823            task_handle.complete(result);
824        });
825        Ok(handle)
826    }
827
828    #[tracing::instrument(skip_all, fields(client_id = self.client_id))]
829    pub async fn verify_ordering(&self) -> Result<VerifiedOrdering> {
830        if let Some(ordering) = &self.verified_ordering {
831            return Ok(ordering.clone());
832        }
833        if let Some(ordering) = self
834            .network
835            .as_ref()
836            .and_then(|network| network.verified_ordering())
837        {
838            return Ok(ordering);
839        }
840        Err(Error::Unsupported(
841            "ordering verification is performed by stoffel-networking over live QUIC connections"
842                .to_owned(),
843        ))
844    }
845
846    pub fn state(&self) -> ClientState {
847        self.state
848    }
849
850    pub fn summary(&self) -> ClientSummary {
851        ClientSummary {
852            client_id: self.client_id,
853            transport_client_id: self.transport_client_id,
854            server_count: self.servers.len(),
855            has_program: self.has_program(),
856            has_verified_ordering: self.verified_ordering.is_some(),
857            has_offchain_io: self.offchain_io.is_some(),
858            connected: self.is_connected(),
859            state: self.state,
860        }
861    }
862
863    pub fn client_id(&self) -> ClientId {
864        self.client_id
865    }
866
867    pub fn transport_client_id(&self) -> Option<ClientId> {
868        self.transport_client_id
869    }
870
871    pub fn servers(&self) -> &[String] {
872        &self.servers
873    }
874
875    pub fn program(&self) -> Option<&Program> {
876        self.program.as_ref()
877    }
878
879    pub fn verified_ordering(&self) -> Option<&VerifiedOrdering> {
880        self.verified_ordering.as_ref()
881    }
882
883    pub fn network_manager(&self) -> Option<&QuicNetworkManager> {
884        self.network.as_ref()
885    }
886
887    pub fn offchain_io(&self) -> Option<&OffChainClientConfig> {
888        self.offchain_io.as_ref()
889    }
890
891    pub fn is_connected(&self) -> bool {
892        self.state == ClientState::Connected && self.network.is_some()
893    }
894
895    pub fn has_program(&self) -> bool {
896        self.program.is_some()
897    }
898
899    async fn run_offchain_inputs(&self, inputs: &[Value]) -> Result<Vec<Value>> {
900        let config = self.offchain_io.as_ref().ok_or_else(|| {
901            Error::Configuration(
902                "client run/submit requires off-chain client IO configuration".to_owned(),
903            )
904        })?;
905        run_offchain_inputs_with_config(config, inputs).await
906    }
907
908    fn validate_submission_inputs(&self, name: &str, input_count: usize) -> Result<()> {
909        if let Some(program) = &self.program {
910            if program.has_client_io() {
911                let client_slot = u64::try_from(self.client_id).map_err(|_| {
912                    Error::InvalidInput(format!(
913                        "client id {} cannot be represented as a ClientStore slot",
914                        self.client_id
915                    ))
916                })?;
917                let client = program.client(client_slot).ok_or_else(|| {
918                    Error::InvalidInput(format!(
919                        "program does not declare ClientStore metadata for client slot {client_slot}"
920                    ))
921                })?;
922                if client.input_count() != input_count {
923                    return Err(Error::InvalidInput(format!(
924                        "client slot {client_slot} expects {} inputs, got {input_count}",
925                        client.input_count()
926                    )));
927                }
928                return Ok(());
929            }
930        }
931        self.validate_function_inputs(name, input_count)
932    }
933
934    fn validate_typed_inputs(&self, name: &str, input_types: &[ClientValueType]) -> Result<()> {
935        if let Some(program) = &self.program {
936            if program.has_client_io() {
937                let client_slot = self.client_slot()?;
938                let client = program.client(client_slot).ok_or_else(|| {
939                    Error::InvalidInput(format!(
940                        "program does not declare ClientStore metadata for client slot {client_slot}"
941                    ))
942                })?;
943                validate_client_value_types("input", client_slot, client.inputs(), input_types)?;
944                return Ok(());
945            }
946        }
947        self.validate_function_inputs(name, input_types.len())
948    }
949
950    fn validate_typed_submission(
951        &self,
952        name: &str,
953        input_types: &[ClientValueType],
954        output_types: &[ClientValueType],
955    ) -> Result<()> {
956        if let Some(program) = &self.program {
957            if program.has_client_io() {
958                let client_slot = self.client_slot()?;
959                let client = program.client(client_slot).ok_or_else(|| {
960                    Error::InvalidInput(format!(
961                        "program does not declare ClientStore metadata for client slot {client_slot}"
962                    ))
963                })?;
964                validate_client_value_types("input", client_slot, client.inputs(), input_types)?;
965                validate_client_value_types("output", client_slot, client.outputs(), output_types)?;
966                return Ok(());
967            }
968        }
969        self.validate_function_inputs(name, input_types.len())
970    }
971
972    fn validate_generated_typed_inputs<M: GeneratedProgramManifest>(
973        &self,
974        name: &str,
975        input_types: &[ClientValueType],
976    ) -> Result<()> {
977        let client_slot = self.client_slot()?;
978        let expected_inputs = M::client_input_types(client_slot).ok_or_else(|| {
979            Error::InvalidInput(format!(
980                "generated manifest does not declare client slot {client_slot}"
981            ))
982        })?;
983        validate_client_value_types_from_values(
984            "input",
985            client_slot,
986            expected_inputs,
987            input_types,
988        )?;
989        self.validate_generated_backend::<M>()?;
990        self.validate_typed_inputs(name, input_types)
991    }
992
993    fn validate_generated_typed_submission<M: GeneratedProgramManifest>(
994        &self,
995        name: &str,
996        input_types: &[ClientValueType],
997        output_types: &[ClientValueType],
998    ) -> Result<()> {
999        let client_slot = self.client_slot()?;
1000        let expected_inputs = M::client_input_types(client_slot).ok_or_else(|| {
1001            Error::InvalidInput(format!(
1002                "generated manifest does not declare client slot {client_slot}"
1003            ))
1004        })?;
1005        let expected_outputs = M::client_output_types(client_slot).ok_or_else(|| {
1006            Error::InvalidInput(format!(
1007                "generated manifest does not declare client slot {client_slot}"
1008            ))
1009        })?;
1010        validate_client_value_types_from_values(
1011            "input",
1012            client_slot,
1013            expected_inputs,
1014            input_types,
1015        )?;
1016        validate_client_value_types_from_values(
1017            "output",
1018            client_slot,
1019            expected_outputs,
1020            output_types,
1021        )?;
1022        self.validate_generated_backend::<M>()?;
1023        self.validate_typed_submission(name, input_types, output_types)
1024    }
1025
1026    fn validate_generated_backend<M: GeneratedProgramManifest>(&self) -> Result<()> {
1027        if let Some(program) = &self.program {
1028            let program_backend = sdk_backend_from_program(program);
1029            if program_backend != M::BACKEND {
1030                return Err(Error::InvalidInput(format!(
1031                    "generated manifest backend {} does not match loaded program backend {}",
1032                    M::BACKEND,
1033                    program_backend
1034                )));
1035            }
1036        }
1037        Ok(())
1038    }
1039
1040    fn client_slot(&self) -> Result<u64> {
1041        u64::try_from(self.client_id).map_err(|_| {
1042            Error::InvalidInput(format!(
1043                "client id {} cannot be represented as a ClientStore slot",
1044                self.client_id
1045            ))
1046        })
1047    }
1048
1049    fn validate_function_inputs(&self, name: &str, input_count: usize) -> Result<()> {
1050        if let Some(program) = &self.program {
1051            let function = program
1052                .function(name)
1053                .ok_or_else(|| Error::FunctionNotFound(name.to_owned()))?;
1054            if function.arg_count() != input_count {
1055                return Err(Error::InvalidInput(format!(
1056                    "function '{name}' expects {} inputs, got {input_count}",
1057                    function.arg_count()
1058                )));
1059            }
1060        }
1061        Ok(())
1062    }
1063}
1064
1065fn validate_client_value_types(
1066    direction: &str,
1067    client_slot: u64,
1068    share_types: &[ShareType],
1069    value_types: &[ClientValueType],
1070) -> Result<()> {
1071    if share_types.len() != value_types.len() {
1072        return Err(Error::InvalidInput(format!(
1073            "client slot {client_slot} expects {} typed {direction}s, got {}",
1074            share_types.len(),
1075            value_types.len()
1076        )));
1077    }
1078    for (ordinal, (share_type, value_type)) in share_types.iter().zip(value_types).enumerate() {
1079        if !value_type.is_compatible_with_share_type(*share_type) {
1080            return Err(Error::InvalidInput(format!(
1081                "client slot {client_slot} {direction} {ordinal} expects {share_type:?}, got {value_type:?}"
1082            )));
1083        }
1084    }
1085    Ok(())
1086}
1087
1088fn validate_client_value_types_from_values(
1089    direction: &str,
1090    client_slot: u64,
1091    expected: &[ClientValueType],
1092    actual: &[ClientValueType],
1093) -> Result<()> {
1094    if expected.len() != actual.len() {
1095        return Err(Error::InvalidInput(format!(
1096            "generated manifest client slot {client_slot} expects {} typed {direction}s, got {}",
1097            expected.len(),
1098            actual.len()
1099        )));
1100    }
1101    for (ordinal, (expected_type, actual_type)) in expected.iter().zip(actual).enumerate() {
1102        if expected_type != actual_type {
1103            return Err(Error::InvalidInput(format!(
1104                "generated manifest client slot {client_slot} {direction} {ordinal} expects {expected_type:?}, got {actual_type:?}"
1105            )));
1106        }
1107    }
1108    Ok(())
1109}
1110
1111fn sdk_backend_from_program(program: &Program) -> MpcBackend {
1112    match program.bytecode_backend() {
1113        stoffel_vm_types::compiled_binary::MpcBackend::HoneyBadger => MpcBackend::HoneyBadger,
1114        stoffel_vm_types::compiled_binary::MpcBackend::Avss => MpcBackend::Avss {
1115            curve: match program.bytecode_curve() {
1116                stoffel_vm_types::compiled_binary::MpcCurve::Bls12_381 => Curve::Bls12_381,
1117                stoffel_vm_types::compiled_binary::MpcCurve::Bn254 => Curve::Bn254,
1118                stoffel_vm_types::compiled_binary::MpcCurve::Curve25519 => Curve::Curve25519,
1119                stoffel_vm_types::compiled_binary::MpcCurve::Ed25519 => Curve::Ed25519,
1120                stoffel_vm_types::compiled_binary::MpcCurve::Secp256k1 => Curve::Secp256k1,
1121                stoffel_vm_types::compiled_binary::MpcCurve::Secp256r1 => Curve::Secp256r1,
1122            },
1123        },
1124    }
1125}
1126
1127async fn run_offchain_inputs_with_config(
1128    config: &OffChainClientConfig,
1129    inputs: &[Value],
1130) -> Result<Vec<Value>> {
1131    match config.backend {
1132        MpcBackend::HoneyBadger => run_offchain_with_share::<RobustShare<Fr>>(config, inputs).await,
1133        MpcBackend::Avss {
1134            curve: Curve::Bls12_381,
1135        } => run_offchain_with_share::<FeldmanShamirShare<Fr, G1Projective>>(config, inputs).await,
1136        MpcBackend::Avss { curve } => Err(Error::Unsupported(format!(
1137            "off-chain client IO does not support AVSS curve {curve}"
1138        ))),
1139    }
1140}
1141
1142async fn run_offchain_with_share<S>(
1143    config: &OffChainClientConfig,
1144    inputs: &[Value],
1145) -> Result<Vec<Value>>
1146where
1147    S: stoffel_mpc_coordinator_shared::ShareBound<Fr, ValueType = Fr>,
1148{
1149    tokio::time::timeout(config.timeout, async {
1150        let coord = OffChainCoordinatorClient::<Fr, S>::start_rpc_client(
1151            &config.coordinator_host,
1152            config.coordinator_port,
1153            config.threshold as u64,
1154            config.parties as u64,
1155            config.output_count,
1156            config.cert_der.clone(),
1157            config.key_der.clone(),
1158        )
1159        .await?;
1160        if config.input_types.len() != inputs.len() {
1161            return Err(Error::InvalidInput(format!(
1162                "off-chain client config for slot {} has {} input type(s), got {} input value(s)",
1163                config.client_slot,
1164                config.input_types.len(),
1165                inputs.len()
1166            )));
1167        }
1168        let mut coord = coord;
1169        for ordinal in 0..inputs.len() {
1170            let reserved_index = config.input_start_index + ordinal as u64;
1171            coord.reserve_mask_index(reserved_index).await?;
1172        }
1173        let node_rpc = OffChainNodeRPCClient::<Fr, S>::start_rpc_client(
1174            config.parties,
1175            config.threshold,
1176            config.node_rpc_endpoints()?,
1177            config.cert_der.clone(),
1178            config.key_der.clone(),
1179        )
1180        .await?;
1181        let mut masks = Vec::with_capacity(inputs.len());
1182        for _ in 0..inputs.len() {
1183            masks.push(node_rpc.receive_mask().await?);
1184        }
1185        for (ordinal, (input, share_type)) in
1186            inputs.iter().zip(config.input_types.iter()).enumerate()
1187        {
1188            let reserved_index = config.input_start_index + ordinal as u64;
1189            let mask = masks.get(ordinal).ok_or_else(|| {
1190                Error::Coordinator(stoffel_mpc_coordinator_shared::CoordinatorError::JSONError(
1191                    format!("missing assigned mask for input ordinal {ordinal}"),
1192                ))
1193            })?;
1194            let field_input = value_to_field(input, *share_type)?;
1195            coord
1196                .send_masked_input(field_input + mask, reserved_index)
1197                .await?;
1198        }
1199        outputs_to_values(coord.obtain_outputs().await?, &config.output_types)
1200    })
1201    .await
1202    .map_err(|_| {
1203        Error::NetworkConnection(format!(
1204            "off-chain client IO timed out after {:?}",
1205            config.timeout
1206        ))
1207    })?
1208}
1209
1210fn value_to_field(value: &Value, share_type: ShareType) -> Result<Fr> {
1211    match (share_type, value) {
1212        (ShareType::SecretInt { bit_length: 1 }, Value::Bool(value)) => Ok(Fr::from(*value as u64)),
1213        (ShareType::SecretInt { bit_length: 1 }, Value::I64(value)) => {
1214            Ok(Fr::from((*value != 0) as u64))
1215        }
1216        (ShareType::SecretInt { .. }, Value::I64(value)) => Ok(i64_to_field(*value)),
1217        (ShareType::SecretInt { .. }, Value::U64(value)) => {
1218            let value = i64::try_from(*value).map_err(|_| {
1219                Error::InvalidInput("u64 secret integer input exceeds i64 range".to_owned())
1220            })?;
1221            Ok(i64_to_field(value))
1222        }
1223        (ShareType::SecretUInt { bit_length }, Value::U64(value)) => {
1224            validate_secret_uint_range(*value, bit_length)?;
1225            Ok(Fr::from(*value))
1226        }
1227        (ShareType::SecretUInt { bit_length }, Value::I64(value)) => {
1228            let value = u64::try_from(*value).map_err(|_| {
1229                Error::InvalidInput(
1230                    "signed input for secret unsigned integer must be non-negative".to_owned(),
1231                )
1232            })?;
1233            validate_secret_uint_range(value, bit_length)?;
1234            Ok(Fr::from(value))
1235        }
1236        (ShareType::SecretFixedPoint { .. }, Value::Float(value)) => {
1237            encode_fixed_point(*value, share_type)
1238        }
1239        (ShareType::SecretFixedPoint { .. }, Value::I64(value)) => {
1240            encode_fixed_point(*value as f64, share_type)
1241        }
1242        (ShareType::SecretFixedPoint { .. }, Value::U64(value)) => {
1243            let value = i64::try_from(*value).map_err(|_| {
1244                Error::InvalidInput("u64 fixed-point input exceeds i64 range".to_owned())
1245            })?;
1246            encode_fixed_point(value as f64, share_type)
1247        }
1248        _ => Err(Error::InvalidInput(format!(
1249            "value kind '{}' is not compatible with share type {share_type:?}",
1250            value.kind()
1251        ))),
1252    }
1253}
1254
1255fn validate_secret_uint_range(value: u64, bit_length: usize) -> Result<()> {
1256    if bit_length >= 64 {
1257        return Ok(());
1258    }
1259    let max = (1u64 << bit_length) - 1;
1260    if value <= max {
1261        Ok(())
1262    } else {
1263        Err(Error::InvalidInput(format!(
1264            "secret unsigned integer input {value} does not fit in {bit_length} bit(s)"
1265        )))
1266    }
1267}
1268
1269fn encode_fixed_point(value: f64, share_type: ShareType) -> Result<Fr> {
1270    let ShareType::SecretFixedPoint { precision } = share_type else {
1271        return Err(Error::InvalidInput(format!(
1272            "cannot encode fixed-point value with share type {share_type:?}"
1273        )));
1274    };
1275    let scale = 2f64.powi(precision.fractional_bits() as i32);
1276    Ok(i64_to_field((value * scale).round() as i64))
1277}
1278
1279fn i64_to_field(value: i64) -> Fr {
1280    if value >= 0 {
1281        Fr::from(value as u64)
1282    } else {
1283        -Fr::from(value.unsigned_abs())
1284    }
1285}
1286
1287fn field_to_i64(value: Fr) -> Result<i64> {
1288    let positive = value.into_bigint();
1289    if positive.as_ref()[1..].iter().all(|limb| *limb == 0)
1290        && positive.as_ref()[0] <= i64::MAX as u64
1291    {
1292        return Ok(positive.as_ref()[0] as i64);
1293    }
1294
1295    let negative = (-value).into_bigint();
1296    if negative.as_ref()[1..].iter().all(|limb| *limb == 0)
1297        && negative.as_ref()[0] <= i64::MAX as u64 + 1
1298    {
1299        let magnitude = negative.as_ref()[0];
1300        return if magnitude == (i64::MAX as u64 + 1) {
1301            Ok(i64::MIN)
1302        } else {
1303            Ok(-(magnitude as i64))
1304        };
1305    }
1306
1307    Err(Error::InvalidInput(
1308        "field output cannot be represented as i64".to_owned(),
1309    ))
1310}
1311
1312fn field_to_u64(value: Fr, bit_length: usize) -> Result<u64> {
1313    let positive = value.into_bigint();
1314    if positive.as_ref()[1..].iter().all(|limb| *limb == 0) {
1315        let value = positive.as_ref()[0];
1316        validate_secret_uint_range(value, bit_length)?;
1317        return Ok(value);
1318    }
1319
1320    Err(Error::InvalidInput(
1321        "field output cannot be represented as u64".to_owned(),
1322    ))
1323}
1324
1325fn field_to_value(value: Fr, share_type: ShareType) -> Result<Value> {
1326    match share_type {
1327        ShareType::SecretInt { bit_length: 1 } => Ok(Value::Bool(!value.is_zero())),
1328        ShareType::SecretInt { .. } => Ok(Value::I64(field_to_i64(value)?)),
1329        ShareType::SecretUInt { bit_length } => Ok(Value::U64(field_to_u64(value, bit_length)?)),
1330        ShareType::SecretFixedPoint { precision } => {
1331            let scaled = field_to_i64(value)?;
1332            let scale = 2f64.powi(precision.fractional_bits() as i32);
1333            Ok(Value::Float(scaled as f64 / scale))
1334        }
1335    }
1336}
1337
1338fn outputs_to_values(outputs: Vec<Fr>, output_types: &[ShareType]) -> Result<Vec<Value>> {
1339    if outputs.len() != output_types.len() {
1340        return Err(Error::InvalidInput(format!(
1341            "expected {} outputs, got {}",
1342            output_types.len(),
1343            outputs.len()
1344        )));
1345    }
1346    outputs
1347        .into_iter()
1348        .zip(output_types.iter().copied())
1349        .map(|(output, share_type)| field_to_value(output, share_type))
1350        .collect()
1351}
1352
1353#[allow(dead_code)]
1354fn bound_outputs_to_values(
1355    outputs: Vec<(u64, Fr)>,
1356    output_types: &[ShareType],
1357) -> Result<Vec<Value>> {
1358    if outputs.len() != output_types.len() {
1359        return Err(Error::InvalidInput(format!(
1360            "expected {} assigned outputs, got {}",
1361            output_types.len(),
1362            outputs.len()
1363        )));
1364    }
1365    let mut values = outputs
1366        .into_iter()
1367        .map(|(output_ordinal, share)| {
1368            let share_type = output_types
1369                .get(output_ordinal as usize)
1370                .copied()
1371                .ok_or_else(|| {
1372                    Error::InvalidInput(format!(
1373                        "assigned output ordinal {} is out of range",
1374                        output_ordinal
1375                    ))
1376                })?;
1377            let value = field_to_value(share, share_type)?;
1378            Ok((output_ordinal, value))
1379        })
1380        .collect::<Result<Vec<_>>>()?;
1381    values.sort_by_key(|(ordinal, _)| *ordinal);
1382    Ok(values.into_iter().map(|(_, value)| value).collect())
1383}
1384
1385mod duration_millis {
1386    use std::time::Duration;
1387
1388    use serde::{Deserialize, Deserializer, Serializer};
1389
1390    pub fn serialize<S>(duration: &Duration, serializer: S) -> std::result::Result<S::Ok, S::Error>
1391    where
1392        S: Serializer,
1393    {
1394        serializer.serialize_u64(
1395            duration
1396                .as_millis()
1397                .try_into()
1398                .map_err(|_| serde::ser::Error::custom("duration milliseconds exceed u64 range"))?,
1399        )
1400    }
1401
1402    pub fn deserialize<'de, D>(deserializer: D) -> std::result::Result<Duration, D::Error>
1403    where
1404        D: Deserializer<'de>,
1405    {
1406        let millis = u64::deserialize(deserializer)?;
1407        Ok(Duration::from_millis(millis))
1408    }
1409}
1410
1411#[derive(Debug, Clone)]
1412pub struct ComputationHandle {
1413    state: Arc<Mutex<ComputationState>>,
1414    notify: Arc<tokio::sync::Notify>,
1415}
1416
1417#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1418pub struct ComputationSummary {
1419    pub status: ComputationStatus,
1420    pub has_result: bool,
1421    pub result_count: usize,
1422}
1423
1424#[derive(Debug)]
1425struct ComputationState {
1426    status: ComputationStatus,
1427    result: Option<Result<Vec<Value>>>,
1428    awaitable: bool,
1429}
1430
1431impl ComputationHandle {
1432    pub fn pending() -> Self {
1433        Self {
1434            state: Arc::new(Mutex::new(ComputationState {
1435                status: ComputationStatus::Pending,
1436                result: None,
1437                awaitable: false,
1438            })),
1439            notify: Arc::new(tokio::sync::Notify::new()),
1440        }
1441    }
1442
1443    fn submitted() -> Self {
1444        Self {
1445            state: Arc::new(Mutex::new(ComputationState {
1446                status: ComputationStatus::Pending,
1447                result: None,
1448                awaitable: true,
1449            })),
1450            notify: Arc::new(tokio::sync::Notify::new()),
1451        }
1452    }
1453
1454    pub fn completed(result: Vec<Value>) -> Self {
1455        Self {
1456            state: Arc::new(Mutex::new(ComputationState {
1457                status: ComputationStatus::Completed,
1458                result: Some(Ok(result)),
1459                awaitable: true,
1460            })),
1461            notify: Arc::new(tokio::sync::Notify::new()),
1462        }
1463    }
1464
1465    #[tracing::instrument(skip_all)]
1466    pub async fn await_result(self) -> Result<Vec<Value>> {
1467        loop {
1468            let result = {
1469                let state = self.state.lock().map_err(|_| {
1470                    Error::Computation("computation handle state lock was poisoned".to_owned())
1471                })?;
1472                match (state.status, state.result.as_ref()) {
1473                    (ComputationStatus::Cancelled, _) => Some(Err(Error::Computation(
1474                        "computation was cancelled".to_owned(),
1475                    ))),
1476                    (_, Some(Ok(result))) => Some(Ok(result.clone())),
1477                    (_, Some(Err(error))) => Some(Err(clone_error_for_handle(error))),
1478                    (ComputationStatus::Pending, None) if !state.awaitable => {
1479                        Some(Err(Error::Unsupported(
1480                            "computation has not been submitted to a live network".to_owned(),
1481                        )))
1482                    }
1483                    _ => None,
1484                }
1485            };
1486            if let Some(result) = result {
1487                return result;
1488            }
1489            self.notify.notified().await;
1490        }
1491    }
1492
1493    pub fn cancel(&self) {
1494        if let Ok(mut state) = self.state.lock() {
1495            if state.status == ComputationStatus::Pending {
1496                state.status = ComputationStatus::Cancelled;
1497                state.result = None;
1498                self.notify.notify_waiters();
1499            }
1500        }
1501    }
1502
1503    pub(crate) fn complete(&self, result: Result<Vec<Value>>) {
1504        if let Ok(mut state) = self.state.lock() {
1505            if state.status == ComputationStatus::Pending {
1506                state.status = ComputationStatus::Completed;
1507                state.result = Some(result);
1508                self.notify.notify_waiters();
1509            }
1510        }
1511    }
1512
1513    pub fn status(&self) -> ComputationStatus {
1514        self.state
1515            .lock()
1516            .map(|state| state.status)
1517            .unwrap_or(ComputationStatus::Cancelled)
1518    }
1519
1520    pub fn summary(&self) -> ComputationSummary {
1521        self.state
1522            .lock()
1523            .map(|state| ComputationSummary {
1524                status: state.status,
1525                has_result: state.result.as_ref().is_some_and(|result| result.is_ok()),
1526                result_count: state
1527                    .result
1528                    .as_ref()
1529                    .and_then(|result| result.as_ref().ok())
1530                    .map_or(0, Vec::len),
1531            })
1532            .unwrap_or(ComputationSummary {
1533                status: ComputationStatus::Cancelled,
1534                has_result: false,
1535                result_count: 0,
1536            })
1537    }
1538
1539    pub fn is_pending(&self) -> bool {
1540        self.status() == ComputationStatus::Pending
1541    }
1542
1543    pub fn is_completed(&self) -> bool {
1544        self.status() == ComputationStatus::Completed
1545    }
1546
1547    pub fn is_cancelled(&self) -> bool {
1548        self.status() == ComputationStatus::Cancelled
1549    }
1550}
1551
1552fn clone_error_for_handle(error: &Error) -> Error {
1553    match error {
1554        Error::Compilation(message) => Error::Compilation(message.clone()),
1555        Error::Configuration(message) => Error::Configuration(message.clone()),
1556        Error::Network(error) => Error::NetworkConnection(error.to_string()),
1557        Error::NetworkConnection(message) => Error::NetworkConnection(message.clone()),
1558        Error::Consensus(error) => Error::Computation(error.to_string()),
1559        Error::Coordinator(error) => Error::Coordinator(error.clone()),
1560        Error::Preprocessing(message) => Error::Preprocessing(message.clone()),
1561        Error::Computation(message) => Error::Computation(message.clone()),
1562        Error::FunctionNotFound(name) => Error::FunctionNotFound(name.clone()),
1563        Error::InvalidInput(message) => Error::InvalidInput(message.clone()),
1564        Error::Unsupported(message) => Error::Unsupported(message.clone()),
1565        Error::Io(error) => Error::Io(std::io::Error::new(error.kind(), error.to_string())),
1566        Error::Bytecode(message) => Error::Bytecode(message.clone()),
1567        Error::ConfigParse(error) => Error::Configuration(error.to_string()),
1568        Error::ConfigSerialize(error) => Error::Configuration(error.to_string()),
1569    }
1570}
1571
1572#[cfg(test)]
1573mod tests {
1574    use super::*;
1575
1576    #[test]
1577    fn secret_uint_values_encode_and_decode_as_unsigned() -> Result<()> {
1578        let share_type = ShareType::secret_uint(8);
1579        let field = value_to_field(&Value::U64(255), share_type)?;
1580        assert_eq!(field_to_value(field, share_type)?, Value::U64(255));
1581        Ok(())
1582    }
1583
1584    #[test]
1585    fn secret_uint_encoding_rejects_negative_or_out_of_range_values() {
1586        let share_type = ShareType::secret_uint(8);
1587
1588        assert!(value_to_field(&Value::I64(-1), share_type).is_err());
1589        assert!(value_to_field(&Value::U64(256), share_type).is_err());
1590    }
1591}