Skip to main content

stoffel/
config.rs

1//! MPC and network configuration types.
2//!
3//! Builders in this module perform fail-fast validation for party counts,
4//! thresholds, backend selection, network addresses, and preprocessing sizes.
5
6use 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    /// Multiplier `k` in the backend's deployment floor `parties >= k * threshold + 1`.
152    fn minimum_parties_multiplier(self) -> usize {
153        match self {
154            // SDK-only deployment floor: HoneyBadger is deployed at N >= 4t + 1.
155            // NOTE: upstream stoffelcrypto 0.1.1 enforces the SAME protocol bound
156            // t < ceil(n/3) (i.e. N >= 3t + 1) for BOTH backends
157            // (honeybadger/mod.rs:476, avss_mpc/mod.rs:220). The 4t + 1 used here is a
158            // conservative SDK deployment floor, NOT HoneyBadger's Byzantine threshold.
159            MpcBackend::HoneyBadger => 4,
160            MpcBackend::Avss { .. } => 3,
161        }
162    }
163
164    /// Minimum party count for a given `threshold`: AVSS needs `3 * t + 1`,
165    /// HoneyBadger needs `4 * t + 1` (SDK deployment floor).
166    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    /// Maximum tolerated `threshold` for a given party count: AVSS allows
178    /// `floor((n - 1) / 3)`, HoneyBadger allows `floor((n - 1) / 4)`.
179    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    /// Fail unless `parties` satisfies the backend's minimum for `threshold`.
194    /// The error names the selected backend and its formula.
195    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        // STO-1019: AVSS is deployable at n = 3t + 1.
1147        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        // STO-1020: summaries report backend-specific minimum_parties/maximum_threshold.
1190        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        // Just-below-bound party counts are rejected for both backends.
1199        assert!(config(6, 2, avss()).summary().is_err());
1200        assert!(config(8, 2, MpcBackend::HoneyBadger).summary().is_err());
1201
1202        // Serialization round-trip preserves the backend-specific summary values.
1203        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}