1use std::collections::BTreeSet;
8use std::ffi::OsString;
9use std::fmt;
10use std::net::{SocketAddr, ToSocketAddrs};
11use std::path::{Path, PathBuf};
12use std::process::{Child, Command, Stdio};
13use std::str::FromStr;
14use std::sync::atomic::{AtomicU8, Ordering};
15use std::sync::{Arc, Mutex};
16use std::time::Duration;
17
18use serde::{Deserialize, Serialize};
19
20use crate::backend::avss::AvssEngine;
21use crate::config::{
22 validate_socket_address, Curve, MpcBackend, MpcConfig, NetworkConfig, NetworkDeployment,
23 PreprocessingConfig,
24};
25use crate::consensus::VerifiedOrdering;
26use crate::error::{Error, Result};
27use crate::observability::{HealthStatus, ServerMetrics};
28use crate::program::Program;
29use crate::types::{GeneratedProgramManifest, PartyId};
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(rename_all = "snake_case")]
33pub enum ServerState {
34 Created,
35 Running,
36 Shutdown,
37}
38
39impl ServerState {
40 const fn as_u8(self) -> u8 {
41 match self {
42 ServerState::Created => 0,
43 ServerState::Running => 1,
44 ServerState::Shutdown => 2,
45 }
46 }
47
48 const fn from_u8(value: u8) -> Self {
49 match value {
50 1 => ServerState::Running,
51 2 => ServerState::Shutdown,
52 _ => ServerState::Created,
53 }
54 }
55}
56
57impl fmt::Display for ServerState {
58 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59 f.write_str(match self {
60 ServerState::Created => "created",
61 ServerState::Running => "running",
62 ServerState::Shutdown => "shutdown",
63 })
64 }
65}
66
67impl FromStr for ServerState {
68 type Err = Error;
69
70 fn from_str(value: &str) -> Result<Self> {
71 match value.trim() {
72 "created" => Ok(ServerState::Created),
73 "running" => Ok(ServerState::Running),
74 "shutdown" => Ok(ServerState::Shutdown),
75 state => Err(Error::Configuration(format!(
76 "unsupported server state '{state}'; expected 'created', 'running', or 'shutdown'"
77 ))),
78 }
79 }
80}
81
82#[derive(Debug, Clone)]
83pub struct ServerBuilder {
84 party_id: PartyId,
85 bind_addr: Option<String>,
86 peers: Vec<(usize, String)>,
87 program: Option<Program>,
88 triples: usize,
89 random_shares: usize,
90 expected_clients: usize,
91 consensus_timeout: Duration,
92 backend: MpcBackend,
93 mpc_config: Option<MpcConfig>,
94 avss_engine: Option<AvssEngine>,
95 verified_ordering: Option<VerifiedOrdering>,
96 runner_path: Option<PathBuf>,
97 bootstrap_addr: Option<String>,
98 advertise_addr: Option<SocketAddr>,
99 entry: String,
100 offchain_coordinator: Option<OffChainServerConfig>,
101 config_error: Option<String>,
102}
103
104impl ServerBuilder {
105 pub fn new(party_id: PartyId) -> Self {
106 Self {
107 party_id,
108 bind_addr: None,
109 peers: Vec::new(),
110 program: None,
111 triples: 1000,
112 random_shares: 500,
113 expected_clients: 0,
114 consensus_timeout: Duration::from_secs(60),
115 backend: MpcBackend::HoneyBadger,
116 mpc_config: None,
117 avss_engine: None,
118 verified_ordering: None,
119 runner_path: None,
120 bootstrap_addr: None,
121 advertise_addr: None,
122 entry: "main".to_owned(),
123 offchain_coordinator: None,
124 config_error: None,
125 }
126 }
127
128 pub fn bind<A: ToSocketAddrs>(mut self, addr: A) -> Self {
129 match addr.to_socket_addrs() {
130 Ok(mut addrs) => {
131 self.bind_addr = addrs.next().map(|addr| addr.to_string());
132 if self.bind_addr.is_none() {
133 self.config_error = Some("server bind address did not resolve".to_owned());
134 }
135 }
136 Err(error) => {
137 self.config_error = Some(format!("invalid server bind address: {error}"));
138 }
139 }
140 self
141 }
142
143 pub fn peers<I, S>(mut self, peers: I) -> Self
144 where
145 I: IntoIterator<Item = (usize, S)>,
146 S: Into<String>,
147 {
148 self.peers = peers
149 .into_iter()
150 .map(|(party_id, address)| (party_id, address.into()))
151 .collect();
152 self
153 }
154
155 pub fn with_peers(self, peers: &[(usize, &str)]) -> Self {
156 self.peers(peers.iter().copied())
157 }
158
159 pub fn peer(mut self, party_id: usize, address: impl Into<String>) -> Self {
160 self.peers.push((party_id, address.into()));
161 self
162 }
163
164 pub fn with_program(mut self, program: Program) -> Self {
165 self.program = Some(program);
166 self
167 }
168
169 pub fn with_preprocessing(mut self, triples: usize, random_shares: usize) -> Self {
170 self.triples = triples;
171 self.random_shares = random_shares;
172 self
173 }
174
175 pub fn expected_clients(mut self, n: usize) -> Self {
176 self.expected_clients = n;
177 self
178 }
179
180 pub fn consensus_timeout(mut self, duration: Duration) -> Self {
181 self.consensus_timeout = duration;
182 self
183 }
184
185 pub fn backend(mut self, backend: MpcBackend) -> Self {
186 self.backend = backend;
187 if let Some(config) = &mut self.mpc_config {
188 config.backend = backend;
189 }
190 self
191 }
192
193 pub fn manifest<M: GeneratedProgramManifest>(self) -> Self {
194 self.backend(M::BACKEND)
195 }
196
197 pub fn honeybadger(self) -> Self {
198 self.backend(MpcBackend::HoneyBadger)
199 }
200
201 pub fn avss(self, curve: Curve) -> Self {
202 self.backend(MpcBackend::Avss { curve })
203 }
204
205 pub fn mpc_config(mut self, config: &MpcConfig) -> Self {
206 self.backend = config.backend;
207 self.mpc_config = Some(config.clone());
208 self
209 }
210
211 pub fn with_avss_engine(mut self, engine: AvssEngine) -> Self {
217 self.avss_engine = Some(engine);
218 self
219 }
220
221 pub fn with_verified_ordering(mut self, ordering: VerifiedOrdering) -> Self {
222 self.verified_ordering = Some(ordering);
223 self
224 }
225
226 pub fn runner_path(mut self, path: impl AsRef<Path>) -> Self {
228 self.runner_path = Some(path.as_ref().to_path_buf());
229 self
230 }
231
232 pub fn entry(mut self, entry: impl Into<String>) -> Self {
234 self.entry = entry.into();
235 self
236 }
237
238 pub fn bootstrap(mut self, address: impl Into<String>) -> Self {
242 self.bootstrap_addr = Some(address.into());
243 self
244 }
245
246 pub fn advertise<A: ToSocketAddrs>(mut self, addr: A) -> Self {
258 match addr.to_socket_addrs() {
259 Ok(addrs) => {
260 let addrs: Vec<SocketAddr> = addrs.collect();
261 self.advertise_addr = addrs
262 .iter()
263 .find(|addr| addr.is_ipv4())
264 .or_else(|| addrs.first())
265 .copied();
266 if self.advertise_addr.is_none() {
267 self.config_error = Some("server advertise address did not resolve".to_owned());
268 }
269 }
270 Err(error) => {
271 self.config_error = Some(format!("invalid server advertise address: {error}"));
272 }
273 }
274 self
275 }
276
277 pub fn offchain_coordinator(mut self, config: OffChainServerConfig) -> Self {
281 self.offchain_coordinator = Some(config);
282 self
283 }
284
285 pub fn configured_party_id(&self) -> PartyId {
286 self.party_id
287 }
288
289 pub fn configured_bind_addr(&self) -> Option<&str> {
290 self.bind_addr.as_deref()
291 }
292
293 pub fn configured_peers(&self) -> &[(usize, String)] {
294 &self.peers
295 }
296
297 pub fn configured_program(&self) -> Option<&Program> {
298 self.program.as_ref()
299 }
300
301 pub fn has_configured_program(&self) -> bool {
302 self.program.is_some()
303 }
304
305 pub fn configured_preprocessing(&self) -> (usize, usize) {
306 (self.triples, self.random_shares)
307 }
308
309 pub fn configured_expected_clients(&self) -> usize {
310 self.expected_clients
311 }
312
313 pub fn configured_consensus_timeout(&self) -> Duration {
314 self.consensus_timeout
315 }
316
317 pub fn configured_backend(&self) -> MpcBackend {
318 self.backend
319 }
320
321 pub fn configured_mpc_config(&self) -> Option<&MpcConfig> {
322 self.mpc_config.as_ref()
323 }
324
325 pub fn configured_avss_engine(&self) -> Option<&AvssEngine> {
326 self.avss_engine.as_ref()
327 }
328
329 pub fn has_configured_avss_engine(&self) -> bool {
330 self.avss_engine.is_some()
331 }
332
333 pub fn configured_verified_ordering(&self) -> Option<&VerifiedOrdering> {
334 self.verified_ordering.as_ref()
335 }
336
337 pub fn has_configured_verified_ordering(&self) -> bool {
338 self.verified_ordering.is_some()
339 }
340
341 pub fn configured_runner_path(&self) -> Option<&Path> {
342 self.runner_path.as_deref()
343 }
344
345 pub fn configured_entry(&self) -> &str {
346 &self.entry
347 }
348
349 pub fn configured_bootstrap(&self) -> Option<&str> {
350 self.bootstrap_addr.as_deref()
351 }
352
353 pub fn configured_advertise(&self) -> Option<SocketAddr> {
354 self.advertise_addr
355 }
356
357 pub fn configured_offchain_coordinator(&self) -> Option<&OffChainServerConfig> {
358 self.offchain_coordinator.as_ref()
359 }
360
361 pub fn network_config(mut self, config: &NetworkConfig) -> Self {
362 if let Err(error) = config.validate_server_addresses() {
363 self.config_error = Some(error.to_string());
364 return self;
365 }
366 if config.network.party_id != self.party_id {
367 self.config_error = Some(format!(
368 "network config party_id {} does not match server builder party_id {}",
369 config.network.party_id, self.party_id
370 ));
371 return self;
372 }
373 self.bind_addr = Some(config.network.bind_address.clone());
374 self.peers = config
375 .network
376 .peers
377 .iter()
378 .map(|(party_id, address)| (*party_id, address.clone()))
379 .collect();
380 self.expected_clients = config.network.expected_clients;
381 self.consensus_timeout = Duration::from_millis(config.network.consensus_timeout_ms);
382 self.triples = config.preprocessing.triples;
383 self.random_shares = config.preprocessing.random_shares;
384 let instance_id = self
385 .mpc_config
386 .as_ref()
387 .map(|config| config.instance_id)
388 .unwrap_or_default();
389 match config.to_mpc_config(instance_id) {
390 Ok(mpc_config) => {
391 self.backend = mpc_config.backend;
392 self.mpc_config = Some(mpc_config);
393 }
394 Err(error) => self.config_error = Some(error.to_string()),
395 }
396 self
397 }
398
399 pub fn network_deployment(self, deployment: &NetworkDeployment) -> Self {
401 match deployment.config(self.party_id) {
402 Some(config) => self.network_config(config),
403 None => {
404 let party_id = self.party_id;
405 self.configuration_error(format!(
406 "network deployment does not contain party_id {party_id}"
407 ))
408 }
409 }
410 }
411
412 pub fn network_config_file(mut self, path: impl AsRef<Path>) -> Self {
413 match NetworkConfig::from_toml_file(path) {
414 Ok(config) => self.network_config(&config),
415 Err(error) => {
416 self.config_error = Some(error.to_string());
417 self
418 }
419 }
420 }
421
422 pub(crate) fn configuration_error(mut self, error: impl Into<String>) -> Self {
423 self.config_error = Some(error.into());
424 self
425 }
426
427 pub fn build(self) -> Result<StoffelServer> {
428 if let Some(error) = self.config_error {
429 return Err(Error::Configuration(error));
430 }
431 if self.bind_addr.is_none() {
432 return Err(Error::Configuration(
433 "server bind address is required".to_owned(),
434 ));
435 }
436 if let Some(advertise) = self.advertise_addr {
437 if advertise.ip().is_unspecified() {
438 return Err(Error::Configuration(format!(
439 "server advertise address must be a routable address, not {advertise}"
440 )));
441 }
442 if advertise.port() == 0 {
443 return Err(Error::Configuration(format!(
444 "server advertise address must have a non-zero port, not {advertise}"
445 )));
446 }
447 }
448 if self.consensus_timeout.is_zero() {
449 return Err(Error::Configuration(
450 "server consensus timeout must be greater than zero".to_owned(),
451 ));
452 }
453 PreprocessingConfig {
454 triples: self.triples,
455 random_shares: self.random_shares,
456 }
457 .validate()?;
458 let mut peer_ids = BTreeSet::new();
459 for (party_id, address) in &self.peers {
460 if !peer_ids.insert(*party_id) {
461 return Err(Error::Configuration(format!(
462 "duplicate peer party_id {party_id}"
463 )));
464 }
465 if *party_id == self.party_id {
466 return Err(Error::Configuration(format!(
467 "peer party_id {party_id} must not match this server's party_id"
468 )));
469 }
470 if address.trim().is_empty() {
471 return Err(Error::Configuration(format!(
472 "peer address for party_id {party_id} must not be empty"
473 )));
474 }
475 validate_socket_address(&format!("peer address for party_id {party_id}"), address)?;
476 }
477 if let Some(mpc_config) = &self.mpc_config {
478 mpc_config.validate()?;
479 if self.party_id >= mpc_config.parties {
480 return Err(Error::Configuration(format!(
481 "server party_id {} must be less than configured party count {}",
482 self.party_id, mpc_config.parties
483 )));
484 }
485 for peer_id in &peer_ids {
486 if *peer_id >= mpc_config.parties {
487 return Err(Error::Configuration(format!(
488 "peer party_id {peer_id} must be less than configured party count {}",
489 mpc_config.parties
490 )));
491 }
492 }
493 for party_id in 0..mpc_config.parties {
494 if party_id != self.party_id && !peer_ids.contains(&party_id) {
495 return Err(Error::Configuration(format!(
496 "missing peer address for configured party_id {party_id}"
497 )));
498 }
499 }
500 }
501 if let Some(program) = &self.program {
502 program.validate_expected_clients(self.expected_clients)?;
503 }
504 if let Some(config) = &self.offchain_coordinator {
505 config.validate(self.expected_clients)?;
506 }
507 if let Some(engine) = &self.avss_engine {
508 match self.backend {
509 MpcBackend::Avss { curve } if curve == engine.curve() => {}
510 MpcBackend::Avss { curve } => {
511 return Err(Error::Configuration(format!(
512 "configured AVSS engine curve {} does not match server backend curve {}",
513 engine.curve(),
514 curve
515 )));
516 }
517 MpcBackend::HoneyBadger => {
518 return Err(Error::Configuration(
519 "configured AVSS engine requires MpcBackend::Avss".to_owned(),
520 ));
521 }
522 }
523 }
524 let metrics = ServerMetrics::default();
525 metrics.record_preprocessing_remaining(self.triples as u64, self.random_shares as u64);
526 Ok(StoffelServer {
527 party_id: self.party_id,
528 bind_addr: self.bind_addr.unwrap(),
529 peers: self.peers,
530 program: self.program,
531 triples: self.triples,
532 random_shares: self.random_shares,
533 expected_clients: self.expected_clients,
534 consensus_timeout: self.consensus_timeout,
535 backend: self.backend,
536 mpc_config: self.mpc_config,
537 avss_engine: self.avss_engine,
538 state: Arc::new(AtomicU8::new(ServerState::Created.as_u8())),
539 verified_ordering: self.verified_ordering,
540 runner_path: self.runner_path,
541 bootstrap_addr: self.bootstrap_addr,
542 advertise_addr: self.advertise_addr,
543 entry: self.entry,
544 offchain_coordinator: self.offchain_coordinator,
545 process: Arc::new(Mutex::new(None)),
546 metrics,
547 })
548 }
549}
550
551#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
552pub struct OffChainServerConfig {
553 pub coordinator_address: String,
554 pub rpc_bind_address: String,
555 pub cert_path: PathBuf,
556 pub key_path: PathBuf,
557 pub timestamp: u64,
558 pub expected_client_certs: Vec<PathBuf>,
559}
560
561impl OffChainServerConfig {
562 pub fn builder() -> OffChainServerConfigBuilder {
563 OffChainServerConfigBuilder::default()
564 }
565
566 pub fn validate(&self, expected_clients: usize) -> Result<()> {
567 validate_socket_address("off-chain coordinator address", &self.coordinator_address)?;
568 validate_socket_address("off-chain node RPC bind address", &self.rpc_bind_address)?;
569 if self.timestamp == 0 {
570 return Err(Error::Configuration(
571 "off-chain coordinator timestamp must be greater than zero".to_owned(),
572 ));
573 }
574 validate_existing_file("off-chain server certificate", &self.cert_path)?;
575 validate_existing_file("off-chain server private key", &self.key_path)?;
576 for path in &self.expected_client_certs {
577 validate_existing_file("expected off-chain client certificate", path)?;
578 }
579 if expected_clients > 0 && self.expected_client_certs.len() != expected_clients {
580 return Err(Error::Configuration(format!(
581 "expected_clients is {expected_clients}, but {} expected client certificate(s) were configured",
582 self.expected_client_certs.len()
583 )));
584 }
585 Ok(())
586 }
587}
588
589#[derive(Debug, Clone, Default)]
590pub struct OffChainServerConfigBuilder {
591 coordinator_address: Option<String>,
592 rpc_bind_address: Option<String>,
593 cert_path: Option<PathBuf>,
594 key_path: Option<PathBuf>,
595 timestamp: Option<u64>,
596 expected_client_certs: Vec<PathBuf>,
597}
598
599impl OffChainServerConfigBuilder {
600 pub fn coordinator(mut self, address: impl Into<String>) -> Self {
601 self.coordinator_address = Some(address.into());
602 self
603 }
604
605 pub fn rpc_bind(mut self, address: impl Into<String>) -> Self {
606 self.rpc_bind_address = Some(address.into());
607 self
608 }
609
610 pub fn identity_files(
611 mut self,
612 cert_path: impl AsRef<Path>,
613 key_path: impl AsRef<Path>,
614 ) -> Self {
615 self.cert_path = Some(cert_path.as_ref().to_path_buf());
616 self.key_path = Some(key_path.as_ref().to_path_buf());
617 self
618 }
619
620 pub fn timestamp(mut self, timestamp: u64) -> Self {
621 self.timestamp = Some(timestamp);
622 self
623 }
624
625 pub fn expected_client_cert(mut self, path: impl AsRef<Path>) -> Self {
626 self.expected_client_certs.push(path.as_ref().to_path_buf());
627 self
628 }
629
630 pub fn expected_client_certs<I, P>(mut self, paths: I) -> Self
631 where
632 I: IntoIterator<Item = P>,
633 P: AsRef<Path>,
634 {
635 self.expected_client_certs = paths
636 .into_iter()
637 .map(|path| path.as_ref().to_path_buf())
638 .collect();
639 self
640 }
641
642 pub fn build(self) -> Result<OffChainServerConfig> {
643 let config = OffChainServerConfig {
644 coordinator_address: self.coordinator_address.ok_or_else(|| {
645 Error::Configuration("off-chain coordinator address is required".to_owned())
646 })?,
647 rpc_bind_address: self.rpc_bind_address.ok_or_else(|| {
648 Error::Configuration("off-chain node RPC bind address is required".to_owned())
649 })?,
650 cert_path: self.cert_path.ok_or_else(|| {
651 Error::Configuration("off-chain server certificate path is required".to_owned())
652 })?,
653 key_path: self.key_path.ok_or_else(|| {
654 Error::Configuration("off-chain server private key path is required".to_owned())
655 })?,
656 timestamp: self.timestamp.ok_or_else(|| {
657 Error::Configuration("off-chain coordinator timestamp is required".to_owned())
658 })?,
659 expected_client_certs: self.expected_client_certs,
660 };
661 config.validate(config.expected_client_certs.len())?;
662 Ok(config)
663 }
664}
665
666#[derive(Debug)]
667pub struct StoffelServer {
668 party_id: PartyId,
669 bind_addr: String,
670 peers: Vec<(usize, String)>,
671 program: Option<Program>,
672 triples: usize,
673 random_shares: usize,
674 expected_clients: usize,
675 consensus_timeout: Duration,
676 backend: MpcBackend,
677 mpc_config: Option<MpcConfig>,
678 avss_engine: Option<AvssEngine>,
679 state: Arc<AtomicU8>,
680 verified_ordering: Option<VerifiedOrdering>,
681 runner_path: Option<PathBuf>,
682 bootstrap_addr: Option<String>,
683 advertise_addr: Option<SocketAddr>,
684 entry: String,
685 offchain_coordinator: Option<OffChainServerConfig>,
686 process: Arc<Mutex<Option<ServerProcess>>>,
687 metrics: ServerMetrics,
688}
689
690#[derive(Debug)]
691struct ServerProcess {
692 child: Child,
693 _tempdir: tempfile::TempDir,
694}
695
696#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
697pub struct ServerSummary {
698 pub party_id: PartyId,
699 pub bind_addr: String,
700 pub peer_count: usize,
701 pub expected_clients: usize,
702 pub preprocessing_triples: usize,
703 pub preprocessing_random_shares: usize,
704 pub consensus_timeout_ms: u64,
705 pub backend: MpcBackend,
706 pub mpc_parties: Option<usize>,
707 pub mpc_threshold: Option<usize>,
708 pub mpc_instance_id: Option<u64>,
709 pub avss_engine_configured: bool,
710 pub avss_engine_live: bool,
711 pub has_verified_ordering: bool,
712 pub state: ServerState,
713 pub ready: bool,
714 pub health: HealthStatus,
715}
716
717impl StoffelServer {
718 pub fn builder(party_id: PartyId) -> ServerBuilder {
719 ServerBuilder::new(party_id)
720 }
721
722 #[tracing::instrument(skip_all, fields(party_id = self.party_id, bind_addr = %self.bind_addr))]
723 pub async fn start(&self) -> Result<()> {
724 if self.state() == ServerState::Shutdown {
725 return Err(Error::Configuration(
726 "cannot start a server after shutdown".to_owned(),
727 ));
728 }
729 if self.state() == ServerState::Running {
730 return Ok(());
731 }
732 let program = self.program.as_ref().ok_or_else(|| {
733 Error::Configuration("server start requires a compiled program".to_owned())
734 })?;
735 if (program.has_client_io() || self.expected_clients > 0)
736 && self.offchain_coordinator.is_none()
737 {
738 return Err(Error::Configuration(
739 "server start for ClientStore IO requires off-chain coordinator configuration"
740 .to_owned(),
741 ));
742 }
743 let mpc_config = self
744 .mpc_config
745 .as_ref()
746 .ok_or_else(|| Error::Configuration("MPC configuration is required".to_owned()))?;
747 if self.party_id != 0 && self.bootstrap_addr.is_none() {
748 return Err(Error::Configuration(
749 "non-leader server start requires a bootstrap address".to_owned(),
750 ));
751 }
752
753 let runner_path = resolve_stoffel_run_binary(self.runner_path.as_deref())?;
754 eprintln!(
755 "[stoffel] using stoffel-run binary: {}",
756 runner_path.display()
757 );
758 let tempdir = tempfile::tempdir()?;
759 let program_path = tempdir.path().join("program.stflb");
760 program.save_bytecode(&program_path)?;
761
762 let args = self.runner_args(&program_path, mpc_config)?;
763 if self.advertise_addr.is_none() && bind_ip_is_unspecified(&self.bind_addr) {
764 eprintln!(
765 "[stoffel] warning: party {} binds {} without an advertise address; parties on other hosts cannot dial it",
766 self.party_id, self.bind_addr
767 );
768 }
769
770 let mut command = Command::new(runner_path);
771 command
772 .args(args)
773 .stdout(Stdio::null())
774 .stderr(Stdio::null());
775
776 let child = command.spawn()?;
777 *self.process.lock().map_err(|_| {
778 Error::Computation("server process state lock was poisoned".to_owned())
779 })? = Some(ServerProcess {
780 child,
781 _tempdir: tempdir,
782 });
783 self.set_state(ServerState::Running);
784 Ok(())
785 }
786
787 fn runner_args(&self, program_path: &Path, mpc_config: &MpcConfig) -> Result<Vec<OsString>> {
789 let mut args: Vec<OsString> = vec![
790 program_path.into(),
791 (&self.entry).into(),
792 "--n-parties".into(),
793 mpc_config.parties.to_string().into(),
794 "--threshold".into(),
795 mpc_config.threshold.to_string().into(),
796 "--bind".into(),
797 (&self.bind_addr).into(),
798 "--mpc-backend".into(),
799 server_backend_name(self.backend).into(),
800 ];
801 if let Some(curve) = self.backend.curve() {
802 args.push("--mpc-curve".into());
803 args.push(curve.to_string().into());
804 }
805 if let Some(advertise) = self.advertise_addr {
806 args.push("--advertise".into());
807 args.push(advertise.to_string().into());
808 }
809 if let Some(config) = &self.offchain_coordinator {
810 config.validate(self.expected_clients)?;
811 args.push("--off-chain-coord".into());
812 args.push((&config.coordinator_address).into());
813 args.push("--rpc-bind".into());
814 args.push((&config.rpc_bind_address).into());
815 args.push("--key".into());
816 args.push((&config.key_path).into());
817 args.push("--cert".into());
818 args.push((&config.cert_path).into());
819 args.push("--timestamp".into());
820 args.push(config.timestamp.to_string().into());
821 if !config.expected_client_certs.is_empty() {
822 args.push("--expected-clients".into());
823 args.push(
824 config
825 .expected_client_certs
826 .iter()
827 .map(|path| path.display().to_string())
828 .collect::<Vec<_>>()
829 .join(",")
830 .into(),
831 );
832 }
833 }
834 if let Some(bootstrap) = &self.bootstrap_addr {
835 args.push("--party-id".into());
836 args.push(self.party_id.to_string().into());
837 args.push("--bootstrap".into());
838 args.push(bootstrap.into());
839 } else {
840 if self.party_id != 0 {
841 return Err(Error::Configuration(
842 "only party 0 can start as leader without bootstrap".to_owned(),
843 ));
844 }
845 args.push("--leader".into());
846 }
847 Ok(args)
848 }
849
850 #[tracing::instrument(skip_all, fields(party_id = self.party_id, bind_addr = %self.bind_addr))]
851 pub async fn run_forever(self) -> Result<()> {
852 self.start().await?;
853 let process = self
854 .process
855 .lock()
856 .map_err(|_| Error::Computation("server process state lock was poisoned".to_owned()))?
857 .take();
858 let status = if let Some(mut process) = process {
859 process.child.wait()?
860 } else {
861 std::future::pending::<std::process::ExitStatus>().await
862 };
863 self.set_state(ServerState::Shutdown);
864 if !status.success() {
865 return Err(Error::Computation(format!(
866 "stoffel-run for party {} exited unsuccessfully: {status}",
867 self.party_id
868 )));
869 }
870 Ok(())
871 }
872
873 #[tracing::instrument(skip_all, fields(party_id = self.party_id))]
874 pub async fn shutdown(self) -> Result<()> {
875 let process = self
876 .process
877 .lock()
878 .map_err(|_| Error::Computation("server process state lock was poisoned".to_owned()))?
879 .take();
880 if let Some(mut process) = process {
881 let _ = process.child.kill();
882 let _ = process.child.wait();
883 }
884 self.set_state(ServerState::Shutdown);
885 Ok(())
886 }
887
888 pub fn party_id(&self) -> PartyId {
889 self.party_id
890 }
891
892 pub fn state(&self) -> ServerState {
893 ServerState::from_u8(self.state.load(Ordering::Acquire))
894 }
895
896 pub fn verified_ordering(&self) -> Option<&VerifiedOrdering> {
897 self.verified_ordering.as_ref()
898 }
899
900 pub fn metrics(&self) -> &ServerMetrics {
901 &self.metrics
902 }
903
904 pub fn summary(&self) -> ServerSummary {
905 let (preprocessing_triples, preprocessing_random_shares) = self.preprocessing();
906 ServerSummary {
907 party_id: self.party_id,
908 bind_addr: self.bind_addr.clone(),
909 peer_count: self.peers.len(),
910 expected_clients: self.expected_clients,
911 preprocessing_triples,
912 preprocessing_random_shares,
913 consensus_timeout_ms: self
914 .consensus_timeout
915 .as_millis()
916 .try_into()
917 .unwrap_or(u64::MAX),
918 backend: self.backend,
919 mpc_parties: self.mpc_config.as_ref().map(|config| config.parties),
920 mpc_threshold: self.mpc_config.as_ref().map(|config| config.threshold),
921 mpc_instance_id: self.mpc_config.as_ref().map(|config| config.instance_id),
922 avss_engine_configured: self.avss_engine.is_some(),
923 avss_engine_live: self.avss_engine.as_ref().is_some_and(AvssEngine::is_live),
924 has_verified_ordering: self.verified_ordering.is_some(),
925 state: self.state(),
926 ready: self.ready(),
927 health: self.health(),
928 }
929 }
930
931 pub fn health(&self) -> HealthStatus {
932 match self.state() {
933 ServerState::Created => HealthStatus::Degraded {
934 reason: "server has been configured but not started by a live networking backend"
935 .to_owned(),
936 },
937 ServerState::Running => HealthStatus::Healthy,
938 ServerState::Shutdown => HealthStatus::Unhealthy {
939 reason: "server has been shut down".to_owned(),
940 },
941 }
942 }
943
944 pub fn ready(&self) -> bool {
945 self.state() == ServerState::Running
946 }
947
948 #[tracing::instrument(skip_all, fields(party_id = self.party_id))]
949 pub async fn create_avss_engine(&self) -> Result<AvssEngine> {
950 match self.backend {
951 MpcBackend::Avss { curve } => Ok(self
952 .avss_engine
953 .clone()
954 .unwrap_or_else(|| AvssEngine::unavailable(curve))),
955 MpcBackend::HoneyBadger => Err(Error::Configuration(
956 "AVSS engine requires MpcBackend::Avss".to_owned(),
957 )),
958 }
959 }
960
961 pub fn bind_addr(&self) -> &str {
962 &self.bind_addr
963 }
964
965 pub fn peers(&self) -> &[(usize, String)] {
966 &self.peers
967 }
968
969 pub fn program(&self) -> Option<&Program> {
970 self.program.as_ref()
971 }
972
973 pub fn preprocessing(&self) -> (usize, usize) {
974 (self.triples, self.random_shares)
975 }
976
977 pub fn expected_clients(&self) -> usize {
978 self.expected_clients
979 }
980
981 pub fn consensus_timeout(&self) -> Duration {
982 self.consensus_timeout
983 }
984
985 pub fn backend(&self) -> MpcBackend {
986 self.backend
987 }
988
989 pub fn mpc_config(&self) -> Option<&MpcConfig> {
990 self.mpc_config.as_ref()
991 }
992
993 pub fn avss_engine(&self) -> Option<&AvssEngine> {
994 self.avss_engine.as_ref()
995 }
996
997 pub fn runner_path(&self) -> Option<&Path> {
998 self.runner_path.as_deref()
999 }
1000
1001 pub fn bootstrap_addr(&self) -> Option<&str> {
1002 self.bootstrap_addr.as_deref()
1003 }
1004
1005 pub fn advertise_addr(&self) -> Option<SocketAddr> {
1007 self.advertise_addr
1008 }
1009
1010 pub fn offchain_coordinator(&self) -> Option<&OffChainServerConfig> {
1011 self.offchain_coordinator.as_ref()
1012 }
1013
1014 pub fn entry(&self) -> &str {
1015 &self.entry
1016 }
1017
1018 fn set_state(&self, state: ServerState) {
1019 self.state.store(state.as_u8(), Ordering::Release);
1020 }
1021}
1022
1023fn server_backend_name(backend: MpcBackend) -> &'static str {
1024 match backend {
1025 MpcBackend::HoneyBadger => "honeybadger",
1026 MpcBackend::Avss { .. } => "avss",
1027 }
1028}
1029
1030fn bind_ip_is_unspecified(bind_addr: &str) -> bool {
1031 bind_addr
1032 .parse::<SocketAddr>()
1033 .is_ok_and(|addr| addr.ip().is_unspecified())
1034}
1035
1036fn resolve_stoffel_run_binary(explicit_path: Option<&Path>) -> Result<PathBuf> {
1037 if let Some(path) = explicit_path {
1038 return path.exists().then(|| path.to_path_buf()).ok_or_else(|| {
1039 Error::Unsupported(format!(
1040 "server start requires an existing stoffel-run binary; configured path does not exist: {}",
1041 path.display()
1042 ))
1043 });
1044 }
1045 if let Some(path) = std::env::var_os("STOFFEL_RUN_BIN").map(PathBuf::from) {
1046 return path.exists().then_some(path.clone()).ok_or_else(|| {
1047 Error::Unsupported(format!(
1048 "server start requires an existing stoffel-run binary; STOFFEL_RUN_BIN points to a missing path: {}",
1049 path.display()
1050 ))
1051 });
1052 }
1053 let candidate = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1054 .parent()
1055 .and_then(Path::parent)
1056 .map(|root| root.join("target").join("debug").join("stoffel-run"));
1057 candidate
1058 .filter(|path| path.exists())
1059 .ok_or_else(|| {
1060 Error::Unsupported(
1061 "server start requires a built stoffel-run binary; set STOFFEL_RUN_BIN, call runner_path, or build `cargo build -p stoffel-vm-runner --bin stoffel-run`"
1062 .to_owned(),
1063 )
1064 })
1065}
1066
1067fn validate_existing_file(label: &str, path: &Path) -> Result<()> {
1068 if path.as_os_str().is_empty() {
1069 return Err(Error::Configuration(format!(
1070 "{label} path must not be empty"
1071 )));
1072 }
1073 if !path.is_file() {
1074 return Err(Error::Configuration(format!(
1075 "{label} path does not exist or is not a file: {}",
1076 path.display()
1077 )));
1078 }
1079 Ok(())
1080}
1081
1082#[cfg(test)]
1083mod tests {
1084 use super::*;
1085
1086 #[tokio::test]
1087 async fn configured_avss_engine_is_returned_by_server_api() -> Result<()> {
1088 let engine = AvssEngine::unavailable(Curve::Bls12_381);
1089 let server = StoffelServer::builder(0)
1090 .bind("127.0.0.1:1")
1091 .avss(Curve::Bls12_381)
1092 .with_avss_engine(engine)
1093 .build()?;
1094
1095 assert!(server.avss_engine().is_some());
1096 assert!(server.summary().avss_engine_configured);
1097 let returned = server.create_avss_engine().await?;
1098 assert_eq!(returned.curve(), Curve::Bls12_381);
1099 assert!(!returned.is_live());
1100 Ok(())
1101 }
1102
1103 #[test]
1104 fn configured_avss_engine_requires_matching_avss_backend() {
1105 let err = StoffelServer::builder(0)
1106 .bind("127.0.0.1:1")
1107 .honeybadger()
1108 .with_avss_engine(AvssEngine::unavailable(Curve::Bls12_381))
1109 .build()
1110 .unwrap_err();
1111 assert!(matches!(err, Error::Configuration(_)));
1112
1113 let err = StoffelServer::builder(0)
1114 .bind("127.0.0.1:1")
1115 .avss(Curve::Bn254)
1116 .with_avss_engine(AvssEngine::unavailable(Curve::Bls12_381))
1117 .build()
1118 .unwrap_err();
1119 assert!(matches!(err, Error::Configuration(_)));
1120 }
1121
1122 fn runner_args_for(server: &StoffelServer) -> Result<Vec<OsString>> {
1123 let mpc_config = server
1124 .mpc_config()
1125 .expect("test server has an MPC configuration");
1126 server.runner_args(Path::new("program.stflb"), mpc_config)
1127 }
1128
1129 fn five_party_builder(party_id: PartyId) -> ServerBuilder {
1130 let mpc_config = MpcConfig::default();
1131 StoffelServer::builder(party_id)
1132 .bind("127.0.0.1:19200")
1133 .mpc_config(&mpc_config)
1134 .peers(
1135 (0..5)
1136 .filter(|peer| *peer != party_id)
1137 .map(|peer| (peer, format!("127.0.0.1:{}", 19300 + peer))),
1138 )
1139 }
1140
1141 fn advertise_values(args: &[OsString]) -> Vec<&OsString> {
1142 args.iter()
1143 .zip(args.iter().skip(1))
1144 .filter(|(flag, _)| *flag == "--advertise")
1145 .map(|(_, value)| value)
1146 .collect()
1147 }
1148
1149 #[test]
1150 fn advertise_defaults_to_unset_and_is_not_forwarded() -> Result<()> {
1151 let builder = five_party_builder(0);
1152 assert_eq!(builder.configured_advertise(), None);
1153
1154 let server = builder.build()?;
1155 assert_eq!(server.advertise_addr(), None);
1156 let args = runner_args_for(&server)?;
1157 assert!(!args.iter().any(|arg| arg == "--advertise"));
1158 assert_eq!(
1159 args.last().map(OsString::as_os_str),
1160 Some("--leader".as_ref())
1161 );
1162 Ok(())
1163 }
1164
1165 #[test]
1166 fn advertise_round_trips_and_is_forwarded_for_leader_and_follower() -> Result<()> {
1167 let expected: SocketAddr = "10.0.0.5:20200".parse().expect("numeric socket address");
1168
1169 let builder = five_party_builder(0).advertise("10.0.0.5:20200");
1170 assert_eq!(builder.configured_advertise(), Some(expected));
1171 let leader = builder.build()?;
1172 assert_eq!(leader.advertise_addr(), Some(expected));
1173 let args = runner_args_for(&leader)?;
1174 assert_eq!(advertise_values(&args), ["10.0.0.5:20200"]);
1175 assert!(args.iter().any(|arg| arg == "--leader"));
1176
1177 let follower = five_party_builder(2)
1178 .bootstrap("10.0.0.4:19200")
1179 .advertise(expected)
1180 .build()?;
1181 let args = runner_args_for(&follower)?;
1182 assert_eq!(advertise_values(&args), ["10.0.0.5:20200"]);
1183 assert!(args.iter().any(|arg| arg == "--bootstrap"));
1184 assert!(!args.iter().any(|arg| arg == "--leader"));
1185 Ok(())
1186 }
1187
1188 #[test]
1189 fn advertise_accepts_loopback_for_single_host_networks() -> Result<()> {
1190 let server = five_party_builder(0).advertise("127.0.0.1:20200").build()?;
1191 assert_eq!(
1192 server.advertise_addr(),
1193 Some("127.0.0.1:20200".parse().expect("numeric socket address"))
1194 );
1195 Ok(())
1196 }
1197
1198 #[test]
1199 fn advertise_rejects_undialable_addresses() {
1200 for address in ["0.0.0.0:20200", "[::]:20200", "10.0.0.5:0"] {
1201 let err = five_party_builder(0)
1202 .advertise(address)
1203 .build()
1204 .unwrap_err();
1205 assert!(
1206 matches!(&err, Error::Configuration(message) if message.contains("advertise")),
1207 "{address}: {err}"
1208 );
1209 }
1210
1211 let err = five_party_builder(0)
1212 .advertise("not a socket address")
1213 .build()
1214 .unwrap_err();
1215 assert!(
1216 matches!(&err, Error::Configuration(message) if message.contains("invalid server advertise address")),
1217 "{err}"
1218 );
1219 }
1220
1221 #[test]
1222 fn non_leader_without_bootstrap_has_no_runner_args() -> Result<()> {
1223 let server = five_party_builder(1).build()?;
1224 let err = runner_args_for(&server).unwrap_err();
1225 assert!(matches!(err, Error::Configuration(_)));
1226 Ok(())
1227 }
1228
1229 #[test]
1230 fn unspecified_bind_detection_only_matches_wildcard_addresses() {
1231 assert!(bind_ip_is_unspecified("0.0.0.0:19200"));
1232 assert!(bind_ip_is_unspecified("[::]:19200"));
1233 assert!(!bind_ip_is_unspecified("127.0.0.1:19200"));
1234 assert!(!bind_ip_is_unspecified("10.0.0.5:19200"));
1235 }
1236}