1use std::collections::{BTreeMap, BTreeSet};
7use std::fmt;
8use std::net::ToSocketAddrs;
9use std::path::{Path, PathBuf};
10use std::str::FromStr;
11use std::time::Duration;
12use std::time::{SystemTime, UNIX_EPOCH};
13
14use serde::{Deserialize, Deserializer, Serialize, Serializer};
15
16use crate::backend::Backend;
17use crate::error::{Error, Result};
18use crate::types::GeneratedProgramManifest;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "snake_case")]
22pub enum Curve {
23 Bls12_381,
24 Bn254,
25 Curve25519,
26 Ed25519,
27 Secp256k1,
28 Secp256r1,
29}
30
31impl fmt::Display for Curve {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 f.write_str(match self {
34 Curve::Bls12_381 => "bls12_381",
35 Curve::Bn254 => "bn254",
36 Curve::Curve25519 => "curve25519",
37 Curve::Ed25519 => "ed25519",
38 Curve::Secp256k1 => "secp256k1",
39 Curve::Secp256r1 => "p-256",
40 })
41 }
42}
43
44impl FromStr for Curve {
45 type Err = Error;
46
47 fn from_str(value: &str) -> Result<Self> {
48 match normalize_identifier(value).as_str() {
49 "bls12381" => Ok(Curve::Bls12_381),
50 "bn254" => Ok(Curve::Bn254),
51 "curve25519" => Ok(Curve::Curve25519),
52 "ed25519" => Ok(Curve::Ed25519),
53 "secp256k1" => Ok(Curve::Secp256k1),
54 "p256" | "nistp256" | "secp256r1" => Ok(Curve::Secp256r1),
55 curve => Err(Error::Configuration(format!(
56 "unsupported curve '{curve}'; expected one of bls12_381, bn254, curve25519, ed25519, secp256k1, p-256"
57 ))),
58 }
59 }
60}
61
62#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
63pub enum MpcBackend {
64 #[default]
65 HoneyBadger,
66 Avss {
67 curve: Curve,
68 },
69}
70
71impl Serialize for MpcBackend {
72 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
73 where
74 S: Serializer,
75 {
76 serializer.serialize_str(&self.to_string())
77 }
78}
79
80impl<'de> Deserialize<'de> for MpcBackend {
81 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
82 where
83 D: Deserializer<'de>,
84 {
85 let value = String::deserialize(deserializer)?;
86 value.parse().map_err(serde::de::Error::custom)
87 }
88}
89
90impl MpcBackend {
91 pub(crate) fn compiler_backend(self) -> stoffel_vm_types::compiled_binary::MpcBackend {
92 match self {
93 MpcBackend::HoneyBadger => stoffel_vm_types::compiled_binary::MpcBackend::HoneyBadger,
94 MpcBackend::Avss { .. } => stoffel_vm_types::compiled_binary::MpcBackend::Avss,
95 }
96 }
97
98 pub(crate) fn compiler_curve(self) -> stoffel_vm_types::compiled_binary::MpcCurve {
99 match self {
100 MpcBackend::HoneyBadger
101 | MpcBackend::Avss {
102 curve: Curve::Bls12_381,
103 } => stoffel_vm_types::compiled_binary::MpcCurve::Bls12_381,
104 MpcBackend::Avss {
105 curve: Curve::Bn254,
106 } => stoffel_vm_types::compiled_binary::MpcCurve::Bn254,
107 MpcBackend::Avss {
108 curve: Curve::Curve25519,
109 } => stoffel_vm_types::compiled_binary::MpcCurve::Curve25519,
110 MpcBackend::Avss {
111 curve: Curve::Ed25519,
112 } => stoffel_vm_types::compiled_binary::MpcCurve::Ed25519,
113 MpcBackend::Avss {
114 curve: Curve::Secp256k1,
115 } => stoffel_vm_types::compiled_binary::MpcCurve::Secp256k1,
116 MpcBackend::Avss {
117 curve: Curve::Secp256r1,
118 } => stoffel_vm_types::compiled_binary::MpcCurve::Secp256r1,
119 }
120 }
121
122 pub fn is_honeybadger(self) -> bool {
123 matches!(self, MpcBackend::HoneyBadger)
124 }
125
126 pub fn is_avss(self) -> bool {
127 matches!(self, MpcBackend::Avss { .. })
128 }
129
130 pub fn curve(self) -> Option<Curve> {
131 match self {
132 MpcBackend::HoneyBadger => None,
133 MpcBackend::Avss { curve } => Some(curve),
134 }
135 }
136
137 pub fn minimum_reconstruction_shares(self, threshold: usize) -> Result<usize> {
138 match self {
139 MpcBackend::HoneyBadger => checked_threshold_expression(
140 threshold,
141 2,
142 1,
143 "HoneyBadger reconstruction threshold",
144 ),
145 MpcBackend::Avss { .. } => {
146 checked_threshold_expression(threshold, 1, 1, "AVSS reconstruction threshold")
147 }
148 }
149 }
150
151 fn minimum_parties_multiplier(self) -> usize {
153 match self {
154 MpcBackend::HoneyBadger => 4,
160 MpcBackend::Avss { .. } => 3,
161 }
162 }
163
164 pub fn minimum_parties(self, threshold: usize) -> Result<usize> {
167 match self {
168 MpcBackend::HoneyBadger => {
169 checked_threshold_expression(threshold, 4, 1, "honeybadger minimum parties")
170 }
171 MpcBackend::Avss { .. } => {
172 checked_threshold_expression(threshold, 3, 1, "avss minimum parties")
173 }
174 }
175 }
176
177 pub fn maximum_threshold(self, parties: usize) -> Result<usize> {
180 let multiplier = self.minimum_parties_multiplier();
181 let minimum = multiplier + 1;
182 if parties < minimum {
183 return Err(Error::Configuration(format!(
184 "{} requires parties (N) >= {} to support a threshold (T) of at least 1 (got N = {})",
185 self.name(),
186 minimum,
187 parties
188 )));
189 }
190 Ok((parties - 1) / multiplier)
191 }
192
193 pub fn ensure_minimum_parties(self, parties: usize, threshold: usize) -> Result<()> {
196 let minimum = self.minimum_parties(threshold)?;
197 if parties < minimum {
198 return Err(Error::Configuration(format!(
199 "{} requires parties (N) >= {} * threshold (T) + 1 (got N = {}, T = {})",
200 self.name(),
201 self.minimum_parties_multiplier(),
202 parties,
203 threshold
204 )));
205 }
206 Ok(())
207 }
208}
209
210impl fmt::Display for MpcBackend {
211 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
212 match self {
213 MpcBackend::HoneyBadger => f.write_str("honeybadger"),
214 MpcBackend::Avss { curve } => write!(f, "avss:{curve}"),
215 }
216 }
217}
218
219impl FromStr for MpcBackend {
220 type Err = Error;
221
222 fn from_str(value: &str) -> Result<Self> {
223 let (protocol, curve) = value
224 .trim()
225 .split_once(':')
226 .map_or((value.trim(), None), |(protocol, curve)| {
227 (protocol.trim(), Some(curve.trim()))
228 });
229
230 match normalize_identifier(protocol).as_str() {
231 "honeybadger" if curve.is_none() => Ok(MpcBackend::HoneyBadger),
232 "honeybadger" => Err(Error::Configuration(
233 "HoneyBadger does not take a curve; use 'honeybadger'".to_owned(),
234 )),
235 "avss" => Ok(MpcBackend::Avss {
236 curve: curve
237 .map(Curve::from_str)
238 .transpose()?
239 .unwrap_or(Curve::Bls12_381),
240 }),
241 protocol => Err(Error::Configuration(format!(
242 "unsupported MPC protocol '{protocol}'; expected 'honeybadger' or 'avss'"
243 ))),
244 }
245 }
246}
247
248#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
249pub struct MpcConfig {
250 pub parties: usize,
251 pub threshold: usize,
252 pub instance_id: u64,
253 pub backend: MpcBackend,
254}
255
256#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
257pub struct MpcConfigSummary {
258 pub parties: usize,
259 pub threshold: usize,
260 pub instance_id: u64,
261 pub backend: MpcBackend,
262 pub minimum_parties: usize,
263 pub maximum_threshold: usize,
264 pub minimum_reconstruction_shares: usize,
265}
266
267impl Default for MpcConfig {
268 fn default() -> Self {
269 Self {
270 parties: 5,
271 threshold: 1,
272 instance_id: default_instance_id(),
273 backend: MpcBackend::default(),
274 }
275 }
276}
277
278impl MpcConfig {
279 pub fn builder() -> MpcConfigBuilder {
280 MpcConfigBuilder::default()
281 }
282
283 pub fn minimum_parties_for_threshold(backend: MpcBackend, threshold: usize) -> Result<usize> {
284 backend.minimum_parties(threshold)
285 }
286
287 pub fn maximum_threshold_for_parties(backend: MpcBackend, parties: usize) -> Result<usize> {
288 backend.maximum_threshold(parties)
289 }
290
291 pub fn validate(&self) -> Result<()> {
292 self.backend
293 .ensure_minimum_parties(self.parties, self.threshold)
294 }
295
296 pub fn minimum_parties(&self) -> Result<usize> {
297 self.backend.minimum_parties(self.threshold)
298 }
299
300 pub fn maximum_threshold(&self) -> Result<usize> {
301 self.backend.maximum_threshold(self.parties)
302 }
303
304 pub fn minimum_reconstruction_shares(&self) -> Result<usize> {
305 self.backend.minimum_reconstruction_shares(self.threshold)
306 }
307
308 pub fn summary(&self) -> Result<MpcConfigSummary> {
309 self.validate()?;
310 Ok(MpcConfigSummary {
311 parties: self.parties,
312 threshold: self.threshold,
313 instance_id: self.instance_id,
314 backend: self.backend,
315 minimum_parties: self.minimum_parties()?,
316 maximum_threshold: self.maximum_threshold()?,
317 minimum_reconstruction_shares: self.minimum_reconstruction_shares()?,
318 })
319 }
320
321 pub fn to_vm_topology(
322 &self,
323 party_id: usize,
324 ) -> Result<stoffel_vm::net::mpc_engine::MpcSessionTopology> {
325 self.validate()?;
326 stoffel_vm::net::mpc_engine::MpcSessionTopology::try_new(
327 self.instance_id,
328 party_id,
329 self.parties,
330 self.threshold,
331 )
332 .map_err(|error| Error::Configuration(error.to_string()))
333 }
334}
335
336#[derive(Debug, Clone, Default)]
337pub struct MpcConfigBuilder {
338 config: MpcConfig,
339}
340
341impl MpcConfigBuilder {
342 pub fn parties(mut self, parties: usize) -> Self {
343 self.config.parties = parties;
344 self
345 }
346
347 pub fn threshold(mut self, threshold: usize) -> Self {
348 self.config.threshold = threshold;
349 self
350 }
351
352 pub fn instance_id(mut self, instance_id: u64) -> Self {
353 self.config.instance_id = instance_id;
354 self
355 }
356
357 pub fn backend(mut self, backend: MpcBackend) -> Self {
358 self.config.backend = backend;
359 self
360 }
361
362 pub fn manifest<M: GeneratedProgramManifest>(self) -> Self {
363 self.backend(M::BACKEND)
364 }
365
366 pub fn honeybadger(mut self) -> Self {
367 self.config.backend = MpcBackend::HoneyBadger;
368 self
369 }
370
371 pub fn avss(mut self, curve: Curve) -> Self {
372 self.config.backend = MpcBackend::Avss { curve };
373 self
374 }
375
376 pub fn curve(mut self, curve: Curve) -> Self {
377 self.config.backend = MpcBackend::Avss { curve };
378 self
379 }
380
381 pub fn build(self) -> Result<MpcConfig> {
382 self.config.validate()?;
383 Ok(self.config)
384 }
385}
386
387fn default_instance_id() -> u64 {
388 SystemTime::now()
389 .duration_since(UNIX_EPOCH)
390 .map(|duration| duration.as_nanos() as u64)
391 .unwrap_or(0)
392}
393
394#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
395pub struct NetworkConfig {
396 #[serde(default)]
397 pub network: NetworkSection,
398 #[serde(default)]
399 pub mpc: MpcSection,
400 #[serde(default)]
401 pub preprocessing: PreprocessingConfig,
402}
403
404#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
405pub struct NetworkConfigSummary {
406 pub party_id: usize,
407 pub bind_address: String,
408 pub expected_parties: usize,
409 pub expected_clients: usize,
410 pub peer_count: usize,
411 pub consensus_timeout_ms: u64,
412 pub threshold: usize,
413 pub backend: MpcBackend,
414 pub minimum_reconstruction_shares: usize,
415 pub preprocessing_triples: usize,
416 pub preprocessing_random_shares: usize,
417}
418
419#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
420pub struct NetworkDeployment {
421 configs: Vec<NetworkConfig>,
422}
423
424#[derive(Debug, Clone)]
425pub struct NetworkDeploymentBuilder {
426 addresses: Vec<String>,
427 expected_clients: usize,
428 consensus_timeout_ms: u64,
429 threshold: usize,
430 backend: MpcBackend,
431 preprocessing: PreprocessingConfig,
432}
433
434impl NetworkConfig {
435 pub fn builder() -> NetworkConfigBuilder {
436 NetworkConfigBuilder::default()
437 }
438
439 pub fn from_toml_str(toml: &str) -> Result<Self> {
440 let config: Self = toml::from_str(toml)?;
441 config.validate()?;
442 Ok(config)
443 }
444
445 pub fn from_toml_file(path: impl AsRef<Path>) -> Result<Self> {
446 let toml = std::fs::read_to_string(path)?;
447 Self::from_toml_str(&toml)
448 }
449
450 pub fn to_toml_string(&self) -> Result<String> {
451 self.validate()?;
452 Ok(toml::to_string_pretty(self)?)
453 }
454
455 pub fn save_toml_file(&self, path: impl AsRef<Path>) -> Result<()> {
456 std::fs::write(path, self.to_toml_string()?)?;
457 Ok(())
458 }
459
460 pub fn validate(&self) -> Result<()> {
461 let backend = self.mpc_backend()?;
462 backend.ensure_minimum_parties(self.network.expected_parties, self.mpc.threshold)?;
463 if self.network.party_id >= self.network.expected_parties {
464 return Err(Error::Configuration(format!(
465 "party_id {} must be less than expected_parties {}",
466 self.network.party_id, self.network.expected_parties
467 )));
468 }
469 if self.network.bind_address.trim().is_empty() {
470 return Err(Error::Configuration(
471 "bind_address must not be empty".to_owned(),
472 ));
473 }
474 validate_socket_address("bind_address", &self.network.bind_address)?;
475 for (party_id, address) in &self.network.peers {
476 if *party_id >= self.network.expected_parties {
477 return Err(Error::Configuration(format!(
478 "peer party_id {party_id} must be less than expected_parties {}",
479 self.network.expected_parties
480 )));
481 }
482 if *party_id == self.network.party_id {
483 return Err(Error::Configuration(format!(
484 "peer party_id {party_id} must not match this node's party_id"
485 )));
486 }
487 if address.trim().is_empty() {
488 return Err(Error::Configuration(format!(
489 "peer address for party_id {party_id} must not be empty"
490 )));
491 }
492 validate_socket_address(&format!("peer address for party_id {party_id}"), address)?;
493 }
494 if self.network.consensus_timeout_ms == 0 {
495 return Err(Error::Configuration(
496 "consensus_timeout_ms must be greater than 0".to_owned(),
497 ));
498 }
499 self.preprocessing.validate()?;
500 self.mpc_backend()?;
501 Ok(())
502 }
503
504 pub fn mpc_backend(&self) -> Result<MpcBackend> {
505 self.mpc.protocol.parse()
506 }
507
508 pub fn minimum_reconstruction_shares(&self) -> Result<usize> {
509 self.mpc_backend()?
510 .minimum_reconstruction_shares(self.mpc.threshold)
511 }
512
513 pub fn summary(&self) -> Result<NetworkConfigSummary> {
514 self.validate()?;
515 Ok(NetworkConfigSummary {
516 party_id: self.network.party_id,
517 bind_address: self.network.bind_address.clone(),
518 expected_parties: self.network.expected_parties,
519 expected_clients: self.network.expected_clients,
520 peer_count: self.network.peers.len(),
521 consensus_timeout_ms: self.network.consensus_timeout_ms,
522 threshold: self.mpc.threshold,
523 backend: self.mpc_backend()?,
524 minimum_reconstruction_shares: self.minimum_reconstruction_shares()?,
525 preprocessing_triples: self.preprocessing.triples,
526 preprocessing_random_shares: self.preprocessing.random_shares,
527 })
528 }
529
530 pub fn to_mpc_config(&self, instance_id: u64) -> Result<MpcConfig> {
531 self.validate()?;
532 let config = MpcConfig {
533 parties: self.network.expected_parties,
534 threshold: self.mpc.threshold,
535 instance_id,
536 backend: self.mpc_backend()?,
537 };
538 config.validate()?;
539 Ok(config)
540 }
541
542 pub fn consensus_timeout(&self) -> Duration {
543 Duration::from_millis(self.network.consensus_timeout_ms)
544 }
545
546 pub fn validate_server_addresses(&self) -> Result<()> {
547 self.validate()?;
548 let mut addresses = self.network.peers.clone();
549 addresses.insert(self.network.party_id, self.network.bind_address.clone());
550 for party_id in 0..self.network.expected_parties {
551 if !addresses.contains_key(&party_id) {
552 return Err(Error::Configuration(format!(
553 "missing server address for party_id {party_id}"
554 )));
555 }
556 }
557 Ok(())
558 }
559
560 pub fn server_addresses(&self) -> Result<Vec<String>> {
561 Ok(self.server_address_map()?.into_values().collect())
562 }
563
564 pub fn server_address_map(&self) -> Result<BTreeMap<usize, String>> {
565 self.validate_server_addresses()?;
566 let mut addresses = self.network.peers.clone();
567 addresses.insert(self.network.party_id, self.network.bind_address.clone());
568 Ok(addresses)
569 }
570
571 pub fn server_address(&self, party_id: usize) -> Result<Option<String>> {
572 self.validate()?;
573 if party_id >= self.network.expected_parties {
574 return Err(Error::Configuration(format!(
575 "party_id {party_id} must be less than expected_parties {}",
576 self.network.expected_parties
577 )));
578 }
579 if party_id == self.network.party_id {
580 return Ok(Some(self.network.bind_address.clone()));
581 }
582 Ok(self.network.peers.get(&party_id).cloned())
583 }
584
585 pub fn bind_address(&self) -> &str {
586 &self.network.bind_address
587 }
588
589 pub fn party_id(&self) -> usize {
590 self.network.party_id
591 }
592
593 pub fn expected_parties(&self) -> usize {
594 self.network.expected_parties
595 }
596
597 pub fn expected_clients(&self) -> usize {
598 self.network.expected_clients
599 }
600
601 pub fn peer_addresses(&self) -> &BTreeMap<usize, String> {
602 &self.network.peers
603 }
604
605 pub fn preprocessing(&self) -> &PreprocessingConfig {
606 &self.preprocessing
607 }
608
609 pub fn threshold(&self) -> usize {
610 self.mpc.threshold
611 }
612
613 pub fn protocol(&self) -> &str {
614 &self.mpc.protocol
615 }
616
617 pub fn consensus_timeout_ms(&self) -> u64 {
618 self.network.consensus_timeout_ms
619 }
620
621 pub fn to_quic_config(&self) -> Result<stoffelnet::transports::quic::QuicNetworkConfig> {
622 self.validate()?;
623 let config = stoffelnet::transports::quic::QuicNetworkConfig {
624 expected_parties: Some(self.network.expected_parties),
625 expected_clients: Some(self.network.expected_clients),
626 consensus_timeout_ms: self.network.consensus_timeout_ms,
627 ..Default::default()
628 };
629 config
630 .validate()
631 .map_err(|error| Error::Configuration(error.to_string()))?;
632 Ok(config)
633 }
634
635 pub fn to_quic_manager(&self) -> Result<stoffelnet::transports::quic::QuicNetworkManager> {
636 let config = self.to_quic_config()?;
637 stoffelnet::transports::quic::QuicNetworkManager::try_with_config(config)
638 .map_err(|error| Error::Configuration(error.to_string()))
639 }
640}
641
642impl NetworkDeployment {
643 pub fn builder<I, S>(addresses: I) -> NetworkDeploymentBuilder
644 where
645 I: IntoIterator<Item = S>,
646 S: Into<String>,
647 {
648 NetworkDeploymentBuilder::new(addresses)
649 }
650
651 pub fn len(&self) -> usize {
652 self.configs.len()
653 }
654
655 pub fn is_empty(&self) -> bool {
656 self.configs.is_empty()
657 }
658
659 pub fn configs(&self) -> &[NetworkConfig] {
660 &self.configs
661 }
662
663 pub fn into_configs(self) -> Vec<NetworkConfig> {
664 self.configs
665 }
666
667 pub fn config(&self, party_id: usize) -> Option<&NetworkConfig> {
668 self.configs.get(party_id)
669 }
670
671 pub fn server_addresses(&self) -> Vec<String> {
672 self.configs
673 .iter()
674 .map(|config| config.network.bind_address.clone())
675 .collect()
676 }
677
678 pub fn summaries(&self) -> Result<Vec<NetworkConfigSummary>> {
679 self.configs.iter().map(NetworkConfig::summary).collect()
680 }
681
682 pub fn to_toml_strings(&self) -> Result<Vec<String>> {
683 self.configs
684 .iter()
685 .map(NetworkConfig::to_toml_string)
686 .collect()
687 }
688
689 pub fn save_toml_files(&self, directory: impl AsRef<Path>) -> Result<Vec<PathBuf>> {
690 self.save_toml_files_with_prefix(directory, "party")
691 }
692
693 pub fn save_toml_files_with_prefix(
694 &self,
695 directory: impl AsRef<Path>,
696 prefix: &str,
697 ) -> Result<Vec<PathBuf>> {
698 let directory = directory.as_ref();
699 std::fs::create_dir_all(directory)?;
700 let prefix = prefix.trim();
701 if prefix.is_empty() {
702 return Err(Error::Configuration(
703 "deployment config file prefix must not be empty".to_owned(),
704 ));
705 }
706
707 self.configs
708 .iter()
709 .map(|config| {
710 let path = directory.join(format!("{prefix}-{}.toml", config.network.party_id));
711 config.save_toml_file(&path)?;
712 Ok(path)
713 })
714 .collect()
715 }
716}
717
718impl NetworkDeploymentBuilder {
719 pub fn new<I, S>(addresses: I) -> Self
720 where
721 I: IntoIterator<Item = S>,
722 S: Into<String>,
723 {
724 Self {
725 addresses: addresses.into_iter().map(Into::into).collect(),
726 expected_clients: 0,
727 consensus_timeout_ms: 60_000,
728 threshold: 1,
729 backend: MpcBackend::HoneyBadger,
730 preprocessing: PreprocessingConfig::default(),
731 }
732 }
733
734 pub fn expected_clients(mut self, expected_clients: usize) -> Self {
735 self.expected_clients = expected_clients;
736 self
737 }
738
739 pub fn consensus_timeout_ms(mut self, timeout_ms: u64) -> Self {
740 self.consensus_timeout_ms = timeout_ms;
741 self
742 }
743
744 pub fn consensus_timeout(mut self, timeout: Duration) -> Self {
745 self.consensus_timeout_ms = duration_to_millis(timeout);
746 self
747 }
748
749 pub fn threshold(mut self, threshold: usize) -> Self {
750 self.threshold = threshold;
751 self
752 }
753
754 pub fn backend(mut self, backend: MpcBackend) -> Self {
755 self.backend = backend;
756 self
757 }
758
759 pub fn manifest<M: GeneratedProgramManifest>(self) -> Self {
760 self.backend(M::BACKEND)
761 }
762
763 pub fn honeybadger(mut self) -> Self {
764 self.backend = MpcBackend::HoneyBadger;
765 self
766 }
767
768 pub fn avss(mut self, curve: Curve) -> Self {
769 self.backend = MpcBackend::Avss { curve };
770 self
771 }
772
773 pub fn preprocessing(mut self, triples: usize, random_shares: usize) -> Self {
774 self.preprocessing = PreprocessingConfig {
775 triples,
776 random_shares,
777 };
778 self
779 }
780
781 pub fn build(self) -> Result<NetworkDeployment> {
782 if self.addresses.len() < 5 {
783 return Err(Error::Configuration(
784 "network deployment requires at least 5 party addresses".to_owned(),
785 ));
786 }
787 let mut seen = BTreeSet::new();
788 for (party_id, address) in self.addresses.iter().enumerate() {
789 if address.trim().is_empty() {
790 return Err(Error::Configuration(format!(
791 "deployment address for party_id {party_id} must not be empty"
792 )));
793 }
794 validate_socket_address(
795 &format!("deployment address for party_id {party_id}"),
796 address,
797 )?;
798 if !seen.insert(address.as_str()) {
799 return Err(Error::Configuration(format!(
800 "duplicate deployment address '{address}'"
801 )));
802 }
803 }
804
805 let expected_parties = self.addresses.len();
806 let configs = self
807 .addresses
808 .iter()
809 .enumerate()
810 .map(|(party_id, bind_address)| {
811 let peers = self
812 .addresses
813 .iter()
814 .enumerate()
815 .filter(|(peer_id, _)| *peer_id != party_id)
816 .map(|(peer_id, address)| (peer_id, address.clone()))
817 .collect();
818 let config = NetworkConfig {
819 network: NetworkSection {
820 party_id,
821 bind_address: bind_address.clone(),
822 expected_parties,
823 expected_clients: self.expected_clients,
824 consensus_timeout_ms: self.consensus_timeout_ms,
825 peers,
826 },
827 mpc: MpcSection {
828 threshold: self.threshold,
829 protocol: self.backend.to_string(),
830 },
831 preprocessing: self.preprocessing.clone(),
832 };
833 config.validate()?;
834 Ok(config)
835 })
836 .collect::<Result<Vec<_>>>()?;
837
838 Ok(NetworkDeployment { configs })
839 }
840}
841
842impl TryFrom<&NetworkConfig> for stoffelnet::transports::quic::QuicNetworkConfig {
843 type Error = Error;
844
845 fn try_from(config: &NetworkConfig) -> Result<Self> {
846 config.to_quic_config()
847 }
848}
849
850impl TryFrom<NetworkConfig> for stoffelnet::transports::quic::QuicNetworkConfig {
851 type Error = Error;
852
853 fn try_from(config: NetworkConfig) -> Result<Self> {
854 config.to_quic_config()
855 }
856}
857
858impl TryFrom<&NetworkConfig> for MpcBackend {
859 type Error = Error;
860
861 fn try_from(config: &NetworkConfig) -> Result<Self> {
862 config.mpc_backend()
863 }
864}
865
866impl TryFrom<NetworkConfig> for MpcBackend {
867 type Error = Error;
868
869 fn try_from(config: NetworkConfig) -> Result<Self> {
870 config.mpc_backend()
871 }
872}
873
874#[derive(Debug, Clone, Default)]
875pub struct NetworkConfigBuilder {
876 config: NetworkConfig,
877}
878
879impl NetworkConfigBuilder {
880 pub fn party_id(mut self, party_id: usize) -> Self {
881 self.config.network.party_id = party_id;
882 self
883 }
884
885 pub fn bind_address(mut self, bind_address: impl Into<String>) -> Self {
886 self.config.network.bind_address = bind_address.into();
887 self
888 }
889
890 pub fn expected_parties(mut self, expected_parties: usize) -> Self {
891 self.config.network.expected_parties = expected_parties;
892 self
893 }
894
895 pub fn expected_clients(mut self, expected_clients: usize) -> Self {
896 self.config.network.expected_clients = expected_clients;
897 self
898 }
899
900 pub fn consensus_timeout_ms(mut self, timeout_ms: u64) -> Self {
901 self.config.network.consensus_timeout_ms = timeout_ms;
902 self
903 }
904
905 pub fn consensus_timeout(mut self, timeout: Duration) -> Self {
906 self.config.network.consensus_timeout_ms = duration_to_millis(timeout);
907 self
908 }
909
910 pub fn peer(mut self, party_id: usize, address: impl Into<String>) -> Self {
911 self.config.network.peers.insert(party_id, address.into());
912 self
913 }
914
915 pub fn peers<I, S>(mut self, peers: I) -> Self
916 where
917 I: IntoIterator<Item = (usize, S)>,
918 S: Into<String>,
919 {
920 self.config.network.peers = peers
921 .into_iter()
922 .map(|(party_id, address)| (party_id, address.into()))
923 .collect();
924 self
925 }
926
927 pub fn with_peers(self, peers: &[(usize, &str)]) -> Self {
928 self.peers(peers.iter().copied())
929 }
930
931 pub fn threshold(mut self, threshold: usize) -> Self {
932 self.config.mpc.threshold = threshold;
933 self
934 }
935
936 pub fn protocol(mut self, protocol: impl Into<String>) -> Self {
937 self.config.mpc.protocol = protocol.into();
938 self
939 }
940
941 pub fn backend(mut self, backend: MpcBackend) -> Self {
942 self.config.mpc.protocol = backend.to_string();
943 self
944 }
945
946 pub fn manifest<M: GeneratedProgramManifest>(self) -> Self {
947 self.backend(M::BACKEND)
948 }
949
950 pub fn honeybadger(mut self) -> Self {
951 self.config.mpc.protocol = MpcBackend::HoneyBadger.to_string();
952 self
953 }
954
955 pub fn avss(mut self, curve: Curve) -> Self {
956 self.config.mpc.protocol = MpcBackend::Avss { curve }.to_string();
957 self
958 }
959
960 pub fn preprocessing(mut self, triples: usize, random_shares: usize) -> Self {
961 self.config.preprocessing.triples = triples;
962 self.config.preprocessing.random_shares = random_shares;
963 self
964 }
965
966 pub fn build(self) -> Result<NetworkConfig> {
967 self.config.validate()?;
968 Ok(self.config)
969 }
970}
971
972#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
973pub struct NetworkSection {
974 pub party_id: usize,
975 pub bind_address: String,
976 pub expected_parties: usize,
977 pub expected_clients: usize,
978 pub consensus_timeout_ms: u64,
979 #[serde(default)]
980 #[serde(with = "peer_map")]
981 pub peers: BTreeMap<usize, String>,
982}
983
984impl Default for NetworkSection {
985 fn default() -> Self {
986 Self {
987 party_id: 0,
988 bind_address: "127.0.0.1:19200".to_owned(),
989 expected_parties: 5,
990 expected_clients: 0,
991 consensus_timeout_ms: 60_000,
992 peers: BTreeMap::new(),
993 }
994 }
995}
996
997mod peer_map {
998 use std::collections::BTreeMap;
999
1000 use serde::{Deserialize, Deserializer, Serialize, Serializer};
1001
1002 pub fn serialize<S>(
1003 peers: &BTreeMap<usize, String>,
1004 serializer: S,
1005 ) -> std::result::Result<S::Ok, S::Error>
1006 where
1007 S: Serializer,
1008 {
1009 let as_strings = peers
1010 .iter()
1011 .map(|(party_id, address)| (party_id.to_string(), address.clone()))
1012 .collect::<BTreeMap<_, _>>();
1013 as_strings.serialize(serializer)
1014 }
1015
1016 pub fn deserialize<'de, D>(
1017 deserializer: D,
1018 ) -> std::result::Result<BTreeMap<usize, String>, D::Error>
1019 where
1020 D: Deserializer<'de>,
1021 {
1022 let raw = BTreeMap::<String, String>::deserialize(deserializer)?;
1023 raw.into_iter()
1024 .map(|(party_id, address)| {
1025 let party_id = party_id
1026 .parse::<usize>()
1027 .map_err(serde::de::Error::custom)?;
1028 Ok((party_id, address))
1029 })
1030 .collect()
1031 }
1032}
1033
1034#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1035pub struct MpcSection {
1036 pub threshold: usize,
1037 pub protocol: String,
1038}
1039
1040impl Default for MpcSection {
1041 fn default() -> Self {
1042 Self {
1043 threshold: 1,
1044 protocol: "honeybadger".to_owned(),
1045 }
1046 }
1047}
1048
1049#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1050pub struct PreprocessingConfig {
1051 pub triples: usize,
1052 pub random_shares: usize,
1053}
1054
1055impl Default for PreprocessingConfig {
1056 fn default() -> Self {
1057 Self {
1058 triples: 1000,
1059 random_shares: 500,
1060 }
1061 }
1062}
1063
1064impl PreprocessingConfig {
1065 pub fn validate(&self) -> Result<()> {
1066 if self.triples == 0 {
1067 return Err(Error::Configuration(
1068 "preprocessing.triples must be greater than 0".to_owned(),
1069 ));
1070 }
1071 if self.random_shares == 0 {
1072 return Err(Error::Configuration(
1073 "preprocessing.random_shares must be greater than 0".to_owned(),
1074 ));
1075 }
1076 Ok(())
1077 }
1078}
1079
1080fn normalize_identifier(value: &str) -> String {
1081 value
1082 .trim()
1083 .chars()
1084 .filter(|character| !matches!(character, '-' | '_'))
1085 .flat_map(char::to_lowercase)
1086 .collect()
1087}
1088
1089pub(crate) fn validate_socket_address(label: &str, address: &str) -> Result<()> {
1090 let trimmed = address.trim();
1091 if trimmed.is_empty() {
1092 return Err(Error::Configuration(format!("{label} must not be empty")));
1093 }
1094 if trimmed
1095 .to_socket_addrs()
1096 .map_err(|error| Error::Configuration(format!("{label} '{address}' is invalid: {error}")))?
1097 .next()
1098 .is_none()
1099 {
1100 return Err(Error::Configuration(format!(
1101 "{label} '{address}' did not resolve to a socket address"
1102 )));
1103 }
1104 Ok(())
1105}
1106
1107fn checked_threshold_expression(
1108 threshold: usize,
1109 multiplier: usize,
1110 addend: usize,
1111 label: &str,
1112) -> Result<usize> {
1113 threshold
1114 .checked_mul(multiplier)
1115 .and_then(|value| value.checked_add(addend))
1116 .ok_or_else(|| {
1117 Error::Configuration(format!("{label} overflows usize for threshold {threshold}"))
1118 })
1119}
1120
1121fn duration_to_millis(duration: Duration) -> u64 {
1122 duration.as_millis().try_into().unwrap_or(u64::MAX)
1123}
1124
1125#[cfg(test)]
1126mod backend_topology_tests {
1127 use super::*;
1128
1129 fn avss() -> MpcBackend {
1130 MpcBackend::Avss {
1131 curve: Curve::Bls12_381,
1132 }
1133 }
1134
1135 fn config(parties: usize, threshold: usize, backend: MpcBackend) -> MpcConfig {
1136 MpcConfig {
1137 parties,
1138 threshold,
1139 instance_id: 1,
1140 backend,
1141 }
1142 }
1143
1144 #[test]
1145 fn avss_validate_accepts_three_t_plus_one_topologies() {
1146 config(4, 1, avss())
1148 .validate()
1149 .expect("AVSS (n=4, t=1) must validate");
1150 config(7, 2, avss())
1151 .validate()
1152 .expect("AVSS (n=7, t=2) must validate");
1153 }
1154
1155 #[test]
1156 fn avss_validate_rejects_below_three_t_plus_one() {
1157 for (parties, threshold) in [(3, 1), (6, 2)] {
1158 let err = config(parties, threshold, avss()).validate().unwrap_err();
1159 let Error::Configuration(message) = err else {
1160 panic!("expected configuration error for AVSS ({parties}, {threshold})");
1161 };
1162 assert!(
1163 message.contains("avss requires parties (N) >= 3 * threshold (T) + 1"),
1164 "AVSS rejection must name the backend and its formula: {message}"
1165 );
1166 }
1167 }
1168
1169 #[test]
1170 fn honeybadger_validate_uses_four_t_plus_one_floor() {
1171 config(5, 1, MpcBackend::HoneyBadger)
1172 .validate()
1173 .expect("HoneyBadger (n=5, t=1) must validate");
1174
1175 let err = config(4, 1, MpcBackend::HoneyBadger)
1176 .validate()
1177 .unwrap_err();
1178 let Error::Configuration(message) = err else {
1179 panic!("expected configuration error for HoneyBadger (4, 1)");
1180 };
1181 assert!(
1182 message.contains("honeybadger requires parties (N) >= 4 * threshold (T) + 1"),
1183 "HoneyBadger rejection must name the backend and its formula: {message}"
1184 );
1185 }
1186
1187 #[test]
1188 fn summaries_are_backend_specific_and_round_trip() -> Result<()> {
1189 let avss_summary = config(7, 2, avss()).summary()?;
1191 assert_eq!(avss_summary.minimum_parties, 7);
1192 assert_eq!(avss_summary.maximum_threshold, 2);
1193
1194 let hb_summary = config(9, 2, MpcBackend::HoneyBadger).summary()?;
1195 assert_eq!(hb_summary.minimum_parties, 9);
1196 assert_eq!(hb_summary.maximum_threshold, 2);
1197
1198 assert!(config(6, 2, avss()).summary().is_err());
1200 assert!(config(8, 2, MpcBackend::HoneyBadger).summary().is_err());
1201
1202 let serialized = toml::to_string(&avss_summary).expect("summary serializes");
1204 let reparsed: MpcConfigSummary = toml::from_str(&serialized).expect("summary deserializes");
1205 assert_eq!(reparsed, avss_summary);
1206 assert_eq!(reparsed.minimum_parties, 7);
1207 assert_eq!(reparsed.maximum_threshold, 2);
1208 Ok(())
1209 }
1210}