1use 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 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}