Skip to main content

fedimint_core/
config.rs

1#[cfg(feature = "uniffi")]
2use std::collections::HashMap;
3use std::collections::{BTreeMap, BTreeSet};
4use std::fmt::{Debug, Display};
5use std::hash::Hash;
6use std::path::{Path, PathBuf};
7use std::str::FromStr;
8
9use anyhow::Context;
10use bitcoin::hashes::sha256::HashEngine;
11use bitcoin::hashes::{Hash as BitcoinHash, hex, sha256};
12use bls12_381::Scalar;
13use fedimint_core::core::{ModuleInstanceId, ModuleKind};
14use fedimint_core::encoding::{DynRawFallback, Encodable};
15use fedimint_core::module::registry::ModuleRegistry;
16use fedimint_core::util::SafeUrl;
17use fedimint_core::{ModuleDecoderRegistry, format_hex};
18use fedimint_logging::LOG_CORE;
19use hex::FromHex;
20use secp256k1::PublicKey;
21use serde::de::DeserializeOwned;
22use serde::{Deserialize, Deserializer, Serialize, Serializer};
23use serde_json::json;
24use thiserror::Error;
25use threshold_crypto::{G1Projective, G2Projective};
26use tracing::warn;
27
28use crate::core::DynClientConfig;
29use crate::encoding::{Decodable, DecodeError};
30use crate::module::{
31    CoreConsensusVersion, DynCommonModuleInit, IDynCommonModuleInit, ModuleConsensusVersion,
32    SerdeModuleEncoding,
33};
34use crate::session_outcome::SignedSessionOutcome;
35use crate::{PeerId, maybe_add_send_sync, secp256k1};
36
37// TODO: make configurable
38/// This limits the RAM consumption of a AlephBFT Unit to roughly 50kB
39pub const ALEPH_BFT_UNIT_BYTE_LIMIT: usize = 50_000;
40
41/// [`serde_json::Value`] that must contain `kind: String` field
42///
43/// TODO: enforce at ser/deserialization
44/// TODO: make inside prive and enforce `kind` on construction, to
45/// other functions non-falliable
46#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
47pub struct JsonWithKind {
48    kind: ModuleKind,
49    #[serde(flatten)]
50    value: serde_json::Value,
51}
52
53impl JsonWithKind {
54    pub fn new(kind: ModuleKind, value: serde_json::Value) -> Self {
55        Self { kind, value }
56    }
57
58    /// Workaround for a serde `flatten` quirk
59    ///
60    /// We serialize config with no fields as: eg. `{ kind: "ln" }`.
61    ///
62    /// When `kind` gets removed and `value` is parsed, it will
63    /// parse as `Value::Object` that is empty.
64    ///
65    /// However empty module structs, like `struct FooConfigLocal;` (unit
66    /// struct), will fail to deserialize with this value, as they expect
67    /// `Value::Null`.
68    ///
69    /// We can turn manually empty object into null, and that's what
70    /// we do in this function. This fixes the deserialization into
71    /// unit type, but in turn breaks deserialization into `struct Foo{}`,
72    /// which is arguably much less common, but valid.
73    ///
74    /// TODO: In the future, we should have a typed and erased versions of
75    /// module construction traits, and then we can try with and
76    /// without the workaround to have both cases working.
77    /// See <https://github.com/fedimint/fedimint/issues/1303>
78    pub fn with_fixed_empty_value(self) -> Self {
79        if let serde_json::Value::Object(ref o) = self.value
80            && o.is_empty()
81        {
82            return Self {
83                kind: self.kind,
84                value: serde_json::Value::Null,
85            };
86        }
87
88        self
89    }
90
91    pub fn value(&self) -> &serde_json::Value {
92        &self.value
93    }
94
95    pub fn kind(&self) -> &ModuleKind {
96        &self.kind
97    }
98
99    pub fn is_kind(&self, kind: &ModuleKind) -> bool {
100        &self.kind == kind
101    }
102}
103
104#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, Encodable, Decodable)]
105#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
106pub struct PeerUrl {
107    /// The peer's public URL (e.g. `wss://fedimint-server-1:5000`)
108    pub url: SafeUrl,
109    /// The peer's name
110    pub name: String,
111}
112
113/// Total client config v0 (<0.4.0). Does not contain broadcast public keys.
114///
115/// This includes global settings and client-side module configs.
116#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Encodable, Decodable)]
117pub struct ClientConfigV0 {
118    #[serde(flatten)]
119    pub global: GlobalClientConfigV0,
120    #[serde(deserialize_with = "de_int_key")]
121    pub modules: BTreeMap<ModuleInstanceId, ClientModuleConfig>,
122}
123
124/// `uniffi::custom_type!` only accepts a bare identifier for the custom
125/// type, and `BTreeMap` isn't natively uniffi-representable (unlike
126/// `HashMap`, which is), so this map shape needs a type alias bridged to
127/// its `HashMap` equivalent.
128#[cfg(feature = "uniffi")]
129type ClientModulesMap = BTreeMap<ModuleInstanceId, ClientModuleConfig>;
130
131#[cfg(feature = "uniffi")]
132uniffi::custom_type!(ClientModulesMap, HashMap<ModuleInstanceId, ClientModuleConfig>, {
133    remote,
134    lower: |m| m.into_iter().collect(),
135    try_lift: |h| Ok(h.into_iter().collect()),
136});
137
138/// Total client config
139///
140/// This includes global settings and client-side module configs.
141#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Encodable, Decodable)]
142#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
143pub struct ClientConfig {
144    #[serde(flatten)]
145    pub global: GlobalClientConfig,
146    #[serde(deserialize_with = "de_int_key")]
147    pub modules: BTreeMap<ModuleInstanceId, ClientModuleConfig>,
148}
149
150#[cfg(feature = "uniffi")]
151uniffi::custom_type!(PublicKey, String, {
152    remote,
153    lower: |pk| pk.to_string(),
154    try_lift: |s| PublicKey::from_str(&s).map_err(|e| anyhow::anyhow!(e)),
155});
156
157// FIXME: workaround for https://github.com/serde-rs/json/issues/989
158fn de_int_key<'de, D, K, V>(deserializer: D) -> Result<BTreeMap<K, V>, D::Error>
159where
160    D: Deserializer<'de>,
161    K: Eq + Ord + FromStr,
162    K::Err: Display,
163    V: Deserialize<'de>,
164{
165    let string_map = <BTreeMap<String, V>>::deserialize(deserializer)?;
166    let map = string_map
167        .into_iter()
168        .map(|(key_str, value)| {
169            let key = K::from_str(&key_str).map_err(serde::de::Error::custom)?;
170            Ok((key, value))
171        })
172        .collect::<Result<BTreeMap<_, _>, _>>()?;
173    Ok(map)
174}
175
176fn optional_de_int_key<'de, D, K, V>(deserializer: D) -> Result<Option<BTreeMap<K, V>>, D::Error>
177where
178    D: Deserializer<'de>,
179    K: Eq + Ord + FromStr,
180    K::Err: Display,
181    V: Deserialize<'de>,
182{
183    let Some(string_map) = <Option<BTreeMap<String, V>>>::deserialize(deserializer)? else {
184        return Ok(None);
185    };
186
187    let map = string_map
188        .into_iter()
189        .map(|(key_str, value)| {
190            let key = K::from_str(&key_str).map_err(serde::de::Error::custom)?;
191            Ok((key, value))
192        })
193        .collect::<Result<BTreeMap<_, _>, _>>()?;
194
195    Ok(Some(map))
196}
197
198/// Client config that cannot be cryptographically verified but is easier to
199/// parse by external tools
200#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
201pub struct JsonClientConfig {
202    pub global: GlobalClientConfig,
203    pub modules: BTreeMap<ModuleInstanceId, JsonWithKind>,
204}
205
206/// Federation-wide client config
207#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Encodable, Decodable)]
208pub struct GlobalClientConfigV0 {
209    /// API endpoints for each federation member
210    #[serde(deserialize_with = "de_int_key")]
211    pub api_endpoints: BTreeMap<PeerId, PeerUrl>,
212    /// Core consensus version
213    pub consensus_version: CoreConsensusVersion,
214    // TODO: make it a String -> serde_json::Value map?
215    /// Additional config the federation wants to transmit to the clients
216    pub meta: BTreeMap<String, String>,
217}
218
219/// Federation-wide client config
220#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Encodable, Decodable)]
221#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
222pub struct GlobalClientConfig {
223    /// API endpoints for each federation member
224    #[serde(deserialize_with = "de_int_key")]
225    pub api_endpoints: BTreeMap<PeerId, PeerUrl>,
226    /// Signing session keys for each federation member
227    /// Optional for 0.3.x backwards compatibility
228    #[serde(default, deserialize_with = "optional_de_int_key")]
229    pub broadcast_public_keys: Option<BTreeMap<PeerId, PublicKey>>,
230    /// Core consensus version
231    pub consensus_version: CoreConsensusVersion,
232    // TODO: make it a String -> serde_json::Value map?
233    /// Additional config the federation wants to transmit to the clients
234    pub meta: BTreeMap<String, String>,
235}
236
237/// `uniffi::custom_type!` only accepts a bare identifier for the custom
238/// type, and `BTreeMap` isn't natively uniffi-representable (unlike
239/// `HashMap`, which is), so each map shape needs a type alias bridged to its
240/// `HashMap` equivalent.
241#[cfg(feature = "uniffi")]
242type ApiEndpointsMap = BTreeMap<PeerId, PeerUrl>;
243#[cfg(feature = "uniffi")]
244type BroadcastPublicKeysMap = BTreeMap<PeerId, PublicKey>;
245#[cfg(feature = "uniffi")]
246type MetaMap = BTreeMap<String, String>;
247
248#[cfg(feature = "uniffi")]
249uniffi::custom_type!(ApiEndpointsMap, HashMap<PeerId, PeerUrl>, {
250    remote,
251    lower: |m| m.into_iter().collect(),
252    try_lift: |h| Ok(h.into_iter().collect()),
253});
254
255#[cfg(feature = "uniffi")]
256uniffi::custom_type!(BroadcastPublicKeysMap, HashMap<PeerId, PublicKey>, {
257    remote,
258    lower: |m| m.into_iter().collect(),
259    try_lift: |h| Ok(h.into_iter().collect()),
260});
261
262#[cfg(feature = "uniffi")]
263uniffi::custom_type!(MetaMap, HashMap<String, String>, {
264    remote,
265    lower: |m| m.into_iter().collect(),
266    try_lift: |h| Ok(h.into_iter().collect()),
267});
268
269impl GlobalClientConfig {
270    /// 0.4.0 and later uses a hash of broadcast public keys to calculate the
271    /// federation id. 0.3.x and earlier use a hash of api endpoints
272    pub fn calculate_federation_id(&self) -> FederationId {
273        FederationId(self.api_endpoints.consensus_hash())
274    }
275
276    /// Federation name from config metadata (if set)
277    pub fn federation_name(&self) -> Option<&str> {
278        self.meta.get(META_FEDERATION_NAME_KEY).map(|x| &**x)
279    }
280}
281
282impl ClientConfig {
283    /// See [`DynRawFallback::redecode_raw`].
284    pub fn redecode_raw(
285        self,
286        modules: &ModuleDecoderRegistry,
287    ) -> Result<Self, crate::encoding::DecodeError> {
288        Ok(Self {
289            modules: self
290                .modules
291                .into_iter()
292                .map(|(module_id, v)| {
293                    // Assuming this isn't running in any hot path it's better to have the debug
294                    // info than saving one allocation
295                    let kind = v.kind.clone();
296
297                    v.redecode_raw(modules)
298                        .context(format!("redecode_raw: instance: {module_id}, kind: {kind}"))
299                        .map(|v| (module_id, v))
300                })
301                .collect::<Result<_, _>>()?,
302            ..self
303        })
304    }
305
306    pub fn calculate_federation_id(&self) -> FederationId {
307        self.global.calculate_federation_id()
308    }
309
310    /// Get the value of a given meta field
311    pub fn meta<V: serde::de::DeserializeOwned + 'static>(
312        &self,
313        key: &str,
314    ) -> Result<Option<V>, ModuleConfigError> {
315        let Some(str_value) = self.global.meta.get(key) else {
316            return Ok(None);
317        };
318        let res =
319            serde_json::from_str(str_value)
320                .map(Some)
321                .map_err(|source| ModuleConfigError::Meta {
322                    key: key.to_owned(),
323                    source,
324                });
325
326        // In the past we encoded some string fields as "just a string" without quotes,
327        // this code ensures that old meta values still parse since config is hard to
328        // change
329        if res.is_err() && std::any::TypeId::of::<V>() == std::any::TypeId::of::<String>() {
330            let string_ret = Box::new(str_value.clone());
331            let ret = unsafe {
332                // We can transmute a String to V because we know that V==String
333                std::mem::transmute::<Box<String>, Box<V>>(string_ret)
334            };
335            Ok(Some(*ret))
336        } else {
337            res
338        }
339    }
340
341    /// Converts a consensus-encoded client config struct to a client config
342    /// struct that when encoded as JSON shows the fields of module configs
343    /// instead of a consensus-encoded hex string.
344    ///
345    /// In case of unknown module the config value is a hex string.
346    pub fn to_json(&self) -> JsonClientConfig {
347        JsonClientConfig {
348            global: self.global.clone(),
349            modules: self
350                .modules
351                .iter()
352                .map(|(&module_instance_id, module_config)| {
353                    let module_config_json = JsonWithKind {
354                        kind: module_config.kind.clone(),
355                        value: module_config.config
356                            .clone()
357                            .decoded()
358                            .and_then(|dyn_cfg| dyn_cfg.to_json())
359                            .unwrap_or_else(|| json!({
360                            "unknown_module_hex": module_config.config.consensus_encode_to_hex()
361                        })),
362                    };
363                    (module_instance_id, module_config_json)
364                })
365                .collect(),
366        }
367    }
368}
369
370/// The federation id is a copy of the authentication threshold public key of
371/// the federation
372///
373/// Stable id so long as guardians membership does not change
374/// Unique id so long as guardians do not all collude
375#[derive(
376    Debug,
377    Copy,
378    Serialize,
379    Deserialize,
380    Clone,
381    Eq,
382    Hash,
383    PartialEq,
384    Encodable,
385    Decodable,
386    Ord,
387    PartialOrd,
388)]
389pub struct FederationId(pub sha256::Hash);
390
391#[cfg(feature = "uniffi")]
392uniffi::custom_type!(FederationId, String, {
393    lower: |obj| obj.to_string(),
394    try_lift: |bytes| {
395        let obj: FederationId = bytes.parse().map_err(|e| anyhow::anyhow!("Failed to parse FederationId from string: {e}"))?;
396        Ok(obj)
397    },
398});
399
400#[derive(
401    Debug,
402    Copy,
403    Serialize,
404    Deserialize,
405    Clone,
406    Eq,
407    Hash,
408    PartialEq,
409    Encodable,
410    Decodable,
411    Ord,
412    PartialOrd,
413)]
414/// Prefix of the [`FederationId`], useful for UX improvements
415///
416/// Intentionally compact to save on the encoding. With 4 billion
417/// combinations real-life non-malicious collisions should never
418/// happen.
419pub struct FederationIdPrefix([u8; 4]);
420
421impl Display for FederationIdPrefix {
422    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
423        format_hex(&self.0, f)
424    }
425}
426
427impl Display for FederationId {
428    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
429        format_hex(&self.0.to_byte_array(), f)
430    }
431}
432
433impl FromStr for FederationIdPrefix {
434    type Err = hex::HexToArrayError;
435
436    fn from_str(s: &str) -> Result<Self, Self::Err> {
437        Ok(Self(<[u8; 4]>::from_hex(s)?))
438    }
439}
440
441impl FederationIdPrefix {
442    pub fn to_bytes(&self) -> Vec<u8> {
443        self.0.to_vec()
444    }
445}
446
447/// Display as a hex encoding
448impl FederationId {
449    /// Random dummy id for testing
450    pub fn dummy() -> Self {
451        Self(sha256::Hash::from_byte_array([42; 32]))
452    }
453
454    pub(crate) fn from_byte_array(bytes: [u8; 32]) -> Self {
455        Self(sha256::Hash::from_byte_array(bytes))
456    }
457
458    pub fn to_prefix(&self) -> FederationIdPrefix {
459        FederationIdPrefix(self.0[..4].try_into().expect("can't fail"))
460    }
461
462    /// Converts a federation id to a public key to which we know but discard
463    /// the private key.
464    ///
465    /// Clients MUST never use this private key for any signing operations!
466    ///
467    /// That is ok because we only use the public key for adding a route
468    /// hint to LN invoices that tells fedimint clients that the invoice can
469    /// only be paid internally. Since no LN node with that pub key can exist
470    /// other LN senders will know that they cannot pay the invoice.
471    pub fn to_fake_ln_pub_key(
472        &self,
473        secp: &bitcoin::secp256k1::Secp256k1<bitcoin::secp256k1::All>,
474    ) -> Result<bitcoin::secp256k1::PublicKey, bitcoin::secp256k1::Error> {
475        let sk = bitcoin::secp256k1::SecretKey::from_slice(&self.0.to_byte_array())?;
476        Ok(bitcoin::secp256k1::PublicKey::from_secret_key(secp, &sk))
477    }
478}
479
480impl FromStr for FederationId {
481    type Err = hex::HexToArrayError;
482
483    fn from_str(s: &str) -> Result<Self, Self::Err> {
484        Ok(Self::from_byte_array(<[u8; 32]>::from_hex(s)?))
485    }
486}
487
488impl ClientConfig {
489    /// Returns the consensus hash for a given client config
490    pub fn consensus_hash(&self) -> sha256::Hash {
491        let mut engine = HashEngine::default();
492        self.consensus_encode(&mut engine)
493            .expect("Consensus hashing should never fail");
494        sha256::Hash::from_engine(engine)
495    }
496
497    pub fn get_module<T: Decodable + 'static>(
498        &self,
499        id: ModuleInstanceId,
500    ) -> Result<&T, ModuleConfigError> {
501        self.modules.get(&id).map_or_else(
502            || Err(ModuleConfigError::ModuleNotFound { id }),
503            |client_cfg| client_cfg.cast(),
504        )
505    }
506
507    // TODO: rename this and one above
508    pub fn get_module_cfg(
509        &self,
510        id: ModuleInstanceId,
511    ) -> Result<ClientModuleConfig, ModuleConfigError> {
512        self.modules.get(&id).map_or_else(
513            || Err(ModuleConfigError::ModuleNotFound { id }),
514            |client_cfg| Ok(client_cfg.clone()),
515        )
516    }
517
518    /// (soft-deprecated): Get the first instance of a module of a given kind in
519    /// defined in config
520    ///
521    /// Since module ids are numerical and for time being we only support 1:1
522    /// mint, wallet, ln module code in the client, this is useful, but
523    /// please write any new code that avoids assumptions about available
524    /// modules.
525    pub fn get_first_module_by_kind<T: Decodable + 'static>(
526        &self,
527        kind: impl Into<ModuleKind>,
528    ) -> Result<(ModuleInstanceId, &T), ModuleConfigError> {
529        let kind: ModuleKind = kind.into();
530        let Some((id, module_cfg)) = self.modules.iter().find(|(_, v)| v.is_kind(&kind)) else {
531            return Err(ModuleConfigError::KindNotFound { kind });
532        };
533        Ok((*id, module_cfg.cast()?))
534    }
535
536    // TODO: rename this and above
537    pub fn get_first_module_by_kind_cfg(
538        &self,
539        kind: impl Into<ModuleKind>,
540    ) -> Result<(ModuleInstanceId, ClientModuleConfig), ModuleConfigError> {
541        let kind: ModuleKind = kind.into();
542        self.modules
543            .iter()
544            .find(|(_, v)| v.is_kind(&kind))
545            .map(|(id, v)| (*id, v.clone()))
546            .ok_or(ModuleConfigError::KindNotFound { kind })
547    }
548}
549
550#[derive(Clone, Debug)]
551pub struct ModuleInitRegistry<M>(BTreeMap<ModuleKind, M>);
552
553/// Legacy module ordering used before alphabetical ordering was introduced.
554/// This ordering was: ln, mint, wallet, lnv2, meta, unknown
555const LEGACY_MODULE_ORDER: &[&str] = &["ln", "mint", "wallet", "lnv2", "meta", "unknown"];
556
557impl<M> ModuleInitRegistry<M> {
558    pub fn iter(&self) -> impl Iterator<Item = (&ModuleKind, &M)> {
559        self.0.iter()
560    }
561
562    /// Iterate over modules in the legacy insertion order for backwards
563    /// compatibility. Modules not in the legacy order list are appended
564    /// at the end in alphabetical order.
565    pub fn iter_legacy_order(&self) -> Vec<(&ModuleKind, &M)> {
566        let mut ordered: Vec<(&ModuleKind, &M)> = Vec::new();
567
568        // First add modules in legacy order
569        for kind_str in LEGACY_MODULE_ORDER {
570            let kind = ModuleKind::from_static_str(kind_str);
571            if let Some((k, m)) = self.0.get_key_value(&kind) {
572                ordered.push((k, m));
573            }
574        }
575
576        // Then add any remaining modules in alphabetical order
577        for (kind, module) in &self.0 {
578            if !LEGACY_MODULE_ORDER.contains(&kind.as_str()) {
579                ordered.push((kind, module));
580            }
581        }
582
583        ordered
584    }
585}
586
587impl<M> Default for ModuleInitRegistry<M> {
588    fn default() -> Self {
589        Self(BTreeMap::new())
590    }
591}
592
593pub type CommonModuleInitRegistry = ModuleInitRegistry<DynCommonModuleInit>;
594
595impl<M> From<Vec<M>> for ModuleInitRegistry<M>
596where
597    M: AsRef<dyn IDynCommonModuleInit + Send + Sync + 'static>,
598{
599    fn from(value: Vec<M>) -> Self {
600        Self(
601            value
602                .into_iter()
603                .map(|i| (i.as_ref().module_kind(), i))
604                .collect::<BTreeMap<_, _>>(),
605        )
606    }
607}
608
609impl<M> FromIterator<M> for ModuleInitRegistry<M>
610where
611    M: AsRef<maybe_add_send_sync!(dyn IDynCommonModuleInit + 'static)>,
612{
613    fn from_iter<T: IntoIterator<Item = M>>(iter: T) -> Self {
614        Self(
615            iter.into_iter()
616                .map(|i| (i.as_ref().module_kind(), i))
617                .collect::<BTreeMap<_, _>>(),
618        )
619    }
620}
621
622impl<M> ModuleInitRegistry<M> {
623    pub fn new() -> Self {
624        Self::default()
625    }
626
627    pub fn attach<T>(&mut self, r#gen: T)
628    where
629        T: Into<M> + 'static + Send + Sync,
630        M: AsRef<dyn IDynCommonModuleInit + 'static + Send + Sync>,
631    {
632        let r#gen: M = r#gen.into();
633        let kind = r#gen.as_ref().module_kind();
634        assert!(
635            self.0.insert(kind.clone(), r#gen).is_none(),
636            "Can't insert module of same kind twice: {kind}"
637        );
638    }
639
640    pub fn kinds(&self) -> BTreeSet<ModuleKind> {
641        self.0.keys().cloned().collect()
642    }
643
644    pub fn get(&self, k: &ModuleKind) -> Option<&M> {
645        self.0.get(k)
646    }
647}
648
649impl<M> ModuleInitRegistry<M>
650where
651    M: AsRef<dyn IDynCommonModuleInit + Send + Sync + 'static>,
652{
653    #[deprecated(
654        note = "You probably want `available_decoders` to support missing module kinds. If you really want a strict behavior, use `decoders_strict`"
655    )]
656    pub fn decoders<'a>(
657        &self,
658        modules: impl Iterator<Item = (ModuleInstanceId, &'a ModuleKind)>,
659    ) -> Result<ModuleDecoderRegistry, ModuleConfigError> {
660        self.decoders_strict(modules)
661    }
662
663    /// Get decoders for `modules` and fail if any is unsupported
664    pub fn decoders_strict<'a>(
665        &self,
666        modules: impl Iterator<Item = (ModuleInstanceId, &'a ModuleKind)>,
667    ) -> Result<ModuleDecoderRegistry, ModuleConfigError> {
668        let mut decoders = BTreeMap::new();
669        for (id, kind) in modules {
670            let Some(init) = self.0.get(kind) else {
671                return Err(ModuleConfigError::UnsupportedModule {
672                    id,
673                    kind: kind.clone(),
674                });
675            };
676
677            decoders.insert(id, (kind.clone(), init.as_ref().decoder()));
678        }
679        Ok(ModuleDecoderRegistry::from(decoders))
680    }
681
682    /// Get decoders for `modules` and skip unsupported ones
683    pub fn available_decoders<'a>(
684        &self,
685        modules: impl Iterator<Item = (ModuleInstanceId, &'a ModuleKind)>,
686    ) -> ModuleDecoderRegistry {
687        let mut decoders = BTreeMap::new();
688        for (id, kind) in modules {
689            let Some(init) = self.0.get(kind) else {
690                warn!(target: LOG_CORE, "Unsupported module id: {id}, kind: {kind}");
691                continue;
692            };
693
694            decoders.insert(id, (kind.clone(), init.as_ref().decoder()));
695        }
696        ModuleDecoderRegistry::from(decoders)
697    }
698}
699
700#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Encodable, Decodable)]
701pub struct ServerModuleConsensusConfig {
702    pub kind: ModuleKind,
703    pub version: ModuleConsensusVersion,
704    #[serde(with = "::hex::serde")]
705    pub config: Vec<u8>,
706}
707
708/// `uniffi::custom_type!` only accepts a bare identifier for the custom
709/// type, so this bridges the type-erased `DynRawFallback<DynClientConfig>`
710/// to its consensus-encoded hex representation. Since no
711/// `ModuleDecoderRegistry` is available across the FFI boundary, lifting
712/// always produces the `Raw` variant; callers can call `redecode_raw`
713/// afterwards once a decoder registry is available.
714#[cfg(feature = "uniffi")]
715type ClientModuleConfigRaw = DynRawFallback<DynClientConfig>;
716
717#[cfg(feature = "uniffi")]
718uniffi::custom_type!(ClientModuleConfigRaw, String, {
719    lower: |v| v.consensus_encode_to_hex(),
720    try_lift: |s| {
721        ClientModuleConfigRaw::consensus_decode_hex(&s, &ModuleDecoderRegistry::default())
722            .map_err(|e| anyhow::anyhow!("Failed to decode client module config: {e}"))
723    },
724});
725
726/// Config for the client-side of a particular Federation module
727///
728/// Since modules are (tbd.) pluggable into Federations,
729/// it needs to be some form of an abstract type-erased-like
730/// value.
731#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Encodable, Decodable)]
732#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
733pub struct ClientModuleConfig {
734    pub kind: ModuleKind,
735    pub version: ModuleConsensusVersion,
736    #[serde(with = "::fedimint_core::encoding::as_hex")]
737    pub config: DynRawFallback<DynClientConfig>,
738}
739
740impl ClientModuleConfig {
741    pub fn from_typed<T: fedimint_core::core::ClientConfig>(
742        module_instance_id: ModuleInstanceId,
743        kind: ModuleKind,
744        version: ModuleConsensusVersion,
745        value: T,
746    ) -> Self {
747        Self {
748            kind,
749            version,
750            config: fedimint_core::core::DynClientConfig::from_typed(module_instance_id, value)
751                .into(),
752        }
753    }
754
755    pub fn redecode_raw(
756        self,
757        modules: &ModuleDecoderRegistry,
758    ) -> Result<Self, crate::encoding::DecodeError> {
759        Ok(Self {
760            config: self.config.redecode_raw(modules)?,
761            ..self
762        })
763    }
764
765    pub fn is_kind(&self, kind: &ModuleKind) -> bool {
766        &self.kind == kind
767    }
768
769    pub fn kind(&self) -> &ModuleKind {
770        &self.kind
771    }
772}
773
774impl ClientModuleConfig {
775    pub fn cast<T>(&self) -> Result<&T, ModuleConfigError>
776    where
777        T: 'static,
778    {
779        match &self.config {
780            DynRawFallback::Decoded(config) => {
781                config
782                    .as_any()
783                    .downcast_ref::<T>()
784                    .ok_or_else(|| ModuleConfigError::WrongType {
785                        kind: self.kind.clone(),
786                        expected: std::any::type_name::<T>(),
787                    })
788            }
789            DynRawFallback::Raw {
790                module_instance_id, ..
791            } => Err(ModuleConfigError::NotDecoded {
792                id: *module_instance_id,
793                kind: self.kind.clone(),
794            }),
795        }
796    }
797}
798
799/// Config for the server-side of a particular Federation module
800///
801/// See [`ClientModuleConfig`].
802#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
803pub struct ServerModuleConfig {
804    pub private: JsonWithKind,
805    pub consensus: ServerModuleConsensusConfig,
806}
807
808impl ServerModuleConfig {
809    pub fn from(private: JsonWithKind, consensus: ServerModuleConsensusConfig) -> Self {
810        Self { private, consensus }
811    }
812
813    pub fn to_typed<T: TypedServerModuleConfig>(&self) -> Result<T, ModuleConfigError> {
814        let private = serde_json::from_value(self.private.value().clone())?;
815        let consensus = <T::Consensus>::consensus_decode_whole(
816            &self.consensus.config[..],
817            &ModuleRegistry::default(),
818        )?;
819
820        Ok(TypedServerModuleConfig::from_parts(private, consensus))
821    }
822}
823
824/// Consensus-critical part of a server side module config
825pub trait TypedServerModuleConsensusConfig:
826    DeserializeOwned + Serialize + Encodable + Decodable
827{
828    fn kind(&self) -> ModuleKind;
829
830    fn version(&self) -> ModuleConsensusVersion;
831
832    fn from_erased(erased: &ServerModuleConsensusConfig) -> Result<Self, ModuleConfigError> {
833        Ok(Self::consensus_decode_whole(
834            &erased.config[..],
835            &ModuleRegistry::default(),
836        )?)
837    }
838}
839
840/// Module (server side) config, typed
841pub trait TypedServerModuleConfig: DeserializeOwned + Serialize {
842    /// Private for this federation member data that are security sensitive and
843    /// will be encrypted at rest
844    type Private: DeserializeOwned + Serialize;
845    /// Shared consensus-critical config
846    type Consensus: TypedServerModuleConsensusConfig;
847
848    /// Assemble from the three functionally distinct parts
849    fn from_parts(private: Self::Private, consensus: Self::Consensus) -> Self;
850
851    /// Split the config into its two functionally distinct parts
852    fn to_parts(self) -> (ModuleKind, Self::Private, Self::Consensus);
853
854    /// Turn the typed config into type-erased version
855    fn to_erased(self) -> ServerModuleConfig {
856        let (kind, private, consensus) = self.to_parts();
857
858        ServerModuleConfig {
859            private: JsonWithKind::new(
860                kind,
861                serde_json::to_value(private).expect("serialization can't fail"),
862            ),
863            consensus: ServerModuleConsensusConfig {
864                kind: consensus.kind(),
865                version: consensus.version(),
866                config: consensus.consensus_encode_to_vec(),
867            },
868        }
869    }
870}
871
872#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Encodable, Decodable)]
873pub enum P2PMessage {
874    Aleph(Vec<u8>),
875    SessionSignature(secp256k1::schnorr::Signature),
876    SessionIndex(u64),
877    SignedSessionOutcome(SerdeModuleEncoding<SignedSessionOutcome>),
878    Checksum(sha256::Hash),
879    DkgG1(DkgMessageG1),
880    DkgG2(DkgMessageG2),
881    Encodable(Vec<u8>),
882    #[encodable_default]
883    Default {
884        variant: u64,
885        bytes: Vec<u8>,
886    },
887}
888
889#[derive(Debug, PartialEq, Eq, Clone, Encodable, Decodable)]
890pub enum DkgMessageG1 {
891    Hash(sha256::Hash),
892    Commitment(Vec<G1Projective>),
893    Share(Scalar),
894}
895
896#[derive(Debug, PartialEq, Eq, Clone, Encodable, Decodable)]
897pub enum DkgMessageG2 {
898    Hash(sha256::Hash),
899    Commitment(Vec<G2Projective>),
900    Share(Scalar),
901}
902
903// TODO: Remove the Serde encoding as soon as the p2p layer drops it as
904// requirement
905impl Serialize for DkgMessageG1 {
906    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
907    where
908        S: Serializer,
909    {
910        self.consensus_encode_to_hex().serialize(serializer)
911    }
912}
913
914impl<'de> Deserialize<'de> for DkgMessageG1 {
915    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
916    where
917        D: Deserializer<'de>,
918    {
919        Self::consensus_decode_hex(
920            &String::deserialize(deserializer)?,
921            &ModuleDecoderRegistry::default(),
922        )
923        .map_err(serde::de::Error::custom)
924    }
925}
926
927// TODO: Remove the Serde encoding as soon as the p2p layer drops it as
928// requirement
929impl Serialize for DkgMessageG2 {
930    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
931    where
932        S: Serializer,
933    {
934        self.consensus_encode_to_hex().serialize(serializer)
935    }
936}
937
938impl<'de> Deserialize<'de> for DkgMessageG2 {
939    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
940    where
941        D: Deserializer<'de>,
942    {
943        Self::consensus_decode_hex(
944            &String::deserialize(deserializer)?,
945            &ModuleDecoderRegistry::default(),
946        )
947        .map_err(serde::de::Error::custom)
948    }
949}
950
951/// Key under which the federation name can be sent to client in the `meta` part
952/// of the config
953pub const META_FEDERATION_NAME_KEY: &str = "federation_name";
954
955pub fn load_from_file<T: DeserializeOwned>(path: &Path) -> Result<T, ConfigFileError> {
956    let contents = std::fs::read(path).map_err(|source| ConfigFileError::Io {
957        path: path.to_owned(),
958        source,
959    })?;
960
961    serde_json::from_slice(&contents).map_err(|source| ConfigFileError::Json {
962        path: path.to_owned(),
963        source,
964    })
965}
966
967/// Failure to look up or cast a module configuration.
968#[derive(Debug, Error)]
969#[non_exhaustive]
970pub enum ModuleConfigError {
971    /// No module with this instance id exists in the config.
972    #[error("Client config for module id {id} not found")]
973    ModuleNotFound { id: ModuleInstanceId },
974    /// No module of this kind exists in the config.
975    #[error("Module kind {kind} not found")]
976    KindNotFound { kind: ModuleKind },
977    /// The decoded module config is not of the requested type.
978    #[error("Client module config of kind {kind} is not a {expected}")]
979    WrongType {
980        kind: ModuleKind,
981        expected: &'static str,
982    },
983    /// The module config is still in its raw, undecoded form because no
984    /// decoder for its kind was available; see
985    /// [`ClientModuleConfig::redecode_raw`].
986    #[error("Client module config for module id {id} of kind {kind} has not been decoded")]
987    NotDecoded {
988        id: ModuleInstanceId,
989        kind: ModuleKind,
990    },
991    /// The config references a module kind this build has no support for.
992    #[error("Detected configuration for unsupported module id: {id}, kind: {kind}")]
993    UnsupportedModule {
994        id: ModuleInstanceId,
995        kind: ModuleKind,
996    },
997    /// A meta field exists but does not deserialize to the requested type.
998    #[error("Decoding meta field '{key}' failed")]
999    Meta {
1000        key: String,
1001        #[source]
1002        source: serde_json::Error,
1003    },
1004    /// A JSON part of the config failed to (de)serialize.
1005    #[error("Invalid JSON in module config")]
1006    Json(#[from] serde_json::Error),
1007    /// A consensus-encoded part of the config failed to decode.
1008    #[error("Invalid consensus encoding in module config")]
1009    Decode(#[from] DecodeError),
1010}
1011
1012/// Failure to load a JSON config file.
1013#[derive(Debug, Error)]
1014#[non_exhaustive]
1015pub enum ConfigFileError {
1016    /// The file could not be opened or read.
1017    #[error("Failed to read config file {}", path.display())]
1018    Io {
1019        path: PathBuf,
1020        #[source]
1021        source: std::io::Error,
1022    },
1023    /// The file is not valid JSON for the requested type.
1024    #[error("Invalid JSON in config file {}", path.display())]
1025    Json {
1026        path: PathBuf,
1027        #[source]
1028        source: serde_json::Error,
1029    },
1030}
1031
1032#[cfg(test)]
1033mod tests;