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