Skip to main content

fedimint_core/module/
version.rs

1//! Fedimint consensus and API versioning.
2//!
3//! ## Introduction
4//!
5//! Fedimint federations are expected to last and serve over time diverse set of
6//! clients running on various devices and platforms with different
7//! versions of the client software. To ensure broad interoperability core
8//! Fedimint logic and modules use consensus and API version scheme.
9//!
10//! ## Definitions
11//!
12//! * Fedimint *component* - either a core Fedimint logic or one of the modules
13//!
14//! ## Consensus versions
15//!
16//! By definition all instances of a given component on every peer inside a
17//! Federation must be running with the same consensus version at the same time.
18//!
19//! Each component in the Federation can only ever be in one consensus version.
20//! The set of all consensus versions of each component is a part of consensus
21//! config that is identical for all peers.
22//!
23//! The code implementing given component can however support multiple consensus
24//! versions at the same time, making it possible to use the same code for
25//! diverse set of Federations created at different times. The consensus
26//! version to run with is passed to the code during initialization.
27//!
28//! The client side components need track consensus versions of each Federation
29//! they use and be able to handle the currently running version of it.
30//!
31//! [`CoreConsensusVersion`] and [`ModuleConsensusVersion`] are used for
32//! consensus versioning.
33//!
34//! ## API versions
35//!
36//! Unlike consensus version which has to be single and identical across
37//! Federation, both server and client side components can advertise
38//! simultaneous support for multiple API versions. This is the main mechanism
39//! to ensure interoperability in the face of hard to control and predict
40//! software changes across all the involved software.
41//!
42//! Each peer in the Federation and each client can update the Fedimint software
43//! at their own pace without coordinating API changes.
44//!
45//! Each client is expected to survey Federation API support and discover the
46//! API version to use for each component.
47//!
48//! Notably the current consensus version of a software component is considered
49//! a prefix to the API version it advertises.
50//!
51//! Software components implementations are expected to provide a good multi-API
52//! support to ensure clients and Federations can always find common API
53//! versions to use.
54//!
55//! [`ApiVersion`] and [`MultiApiVersion`] is used for API versioning.
56use std::collections::BTreeMap;
57use std::{cmp, fmt, result};
58
59use serde::{Deserialize, Serialize};
60
61use crate::core::{ModuleInstanceId, ModuleKind};
62use crate::db::DatabaseVersion;
63use crate::encoding::{Decodable, Encodable};
64
65/// Consensus version of a core server
66///
67/// Breaking changes in the Fedimint's core consensus require incrementing it.
68///
69/// See [`ModuleConsensusVersion`] for more details on how it interacts with
70/// module's consensus.
71#[derive(
72    Debug, Copy, Clone, PartialOrd, Ord, Serialize, Deserialize, Encodable, Decodable, PartialEq, Eq,
73)]
74pub struct CoreConsensusVersion {
75    pub major: u32,
76    pub minor: u32,
77}
78
79impl CoreConsensusVersion {
80    pub const fn new(major: u32, minor: u32) -> Self {
81        Self { major, minor }
82    }
83}
84
85impl fmt::Display for CoreConsensusVersion {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        write!(f, "{}.{}", self.major, self.minor)
88    }
89}
90
91/// Globally declared core consensus version implemented/supported by this
92/// codebase
93pub const CORE_CONSENSUS_VERSION: CoreConsensusVersion = CoreConsensusVersion::new(2, 1);
94
95/// Consensus version of a specific module instance
96///
97/// Any breaking change to the module's consensus rules require incrementing the
98/// major part of it.
99///
100/// Any backwards-compatible changes with regards to clients require
101/// incrementing the minor part of it. Backwards compatible changes will
102/// typically be introducing new input/output/consensus item variants that old
103/// clients won't understand but can safely ignore while new clients can use new
104/// functionality. It's akin to soft forks in Bitcoin.
105///
106/// A module instance can run only in one consensus version, which must be the
107/// same (both major and minor) across all corresponding instances on other
108/// nodes of the federation.
109///
110/// When [`CoreConsensusVersion`] changes, this can but is not requires to be
111/// a breaking change for each module's [`ModuleConsensusVersion`].
112///
113/// For many modules it might be preferable to implement a new
114/// [`fedimint_core::core::ModuleKind`] "versions" (to be implemented at the
115/// time of writing this comment), and by running two instances of the module at
116/// the same time (each of different `ModuleKind` version), allow users to
117/// slowly migrate to a new one. This avoids complex and error-prone server-side
118/// consensus-migration logic.
119#[derive(
120    Debug,
121    Hash,
122    Copy,
123    Clone,
124    PartialEq,
125    Eq,
126    PartialOrd,
127    Ord,
128    Serialize,
129    Deserialize,
130    Encodable,
131    Decodable,
132)]
133pub struct ModuleConsensusVersion {
134    pub major: u32,
135    pub minor: u32,
136}
137
138impl ModuleConsensusVersion {
139    pub const fn new(major: u32, minor: u32) -> Self {
140        Self { major, minor }
141    }
142}
143
144impl fmt::Display for ModuleConsensusVersion {
145    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146        write!(f, "{}.{}", self.major, self.minor)
147    }
148}
149
150/// Api version supported by a core server or a client/server module at a given
151/// [`ModuleConsensusVersion`].
152///
153/// Changing [`ModuleConsensusVersion`] implies resetting the api versioning.
154///
155/// For a client and server to be able to communicate with each other:
156///
157/// * The client needs API version support for the [`ModuleConsensusVersion`]
158///   that the server is currently running with.
159/// * Within that [`ModuleConsensusVersion`] during handshake negotiation
160///   process client and server must find at least one `Api::major` version
161///   where client's `minor` is lower or equal server's `major` version.
162///
163/// A practical module implementation needs to implement large range of version
164/// backward compatibility on both client and server side to accommodate end
165/// user client devices receiving updates at a pace hard to control, and
166/// technical and coordination challenges of upgrading servers.
167#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq, Decodable, Encodable)]
168pub struct ApiVersion {
169    /// Major API version
170    ///
171    /// Each time [`ModuleConsensusVersion`] is incremented, this number (and
172    /// `minor` number as well) should be reset to `0`.
173    ///
174    /// Should be incremented each time the API was changed in a
175    /// backward-incompatible ways (while resetting `minor` to `0`).
176    pub major: u32,
177    /// Minor API version
178    ///
179    /// * For clients this means *minimum* supported minor version of the
180    ///   `major` version required by client implementation
181    /// * For servers this means *maximum* supported minor version of the
182    ///   `major` version implemented by the server implementation
183    pub minor: u32,
184}
185
186impl ApiVersion {
187    pub const fn new(major: u32, minor: u32) -> Self {
188        Self { major, minor }
189    }
190}
191
192impl fmt::Display for ApiVersion {
193    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194        write!(f, "{}.{}", self.major, self.minor)
195    }
196}
197
198/// ```
199/// use fedimint_core::module::ApiVersion;
200/// assert!(ApiVersion { major: 3, minor: 3 } < ApiVersion { major: 4, minor: 0 });
201/// assert!(ApiVersion { major: 3, minor: 3 } < ApiVersion { major: 3, minor: 5 });
202/// assert!(ApiVersion { major: 3, minor: 3 } == ApiVersion { major: 3, minor: 3 });
203/// ```
204impl cmp::PartialOrd for ApiVersion {
205    fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
206        Some(self.cmp(other))
207    }
208}
209
210impl cmp::Ord for ApiVersion {
211    fn cmp(&self, other: &Self) -> cmp::Ordering {
212        self.major
213            .cmp(&other.major)
214            .then(self.minor.cmp(&other.minor))
215    }
216}
217
218/// Multiple, disjoint, minimum required or maximum supported, [`ApiVersion`]s.
219///
220/// If a given component can (potentially) support multiple different (distinct
221/// major number), of an API, this type is used to express it.
222///
223/// All [`ApiVersion`] values are in the context of the current consensus
224/// version for the component in question.
225///
226/// Each element must have a distinct major api number, and means
227/// either minimum required API version of this major number (for the client),
228/// or maximum supported version of this major number (for the server).
229#[derive(Debug, Clone, Eq, PartialEq, Serialize, Default, Encodable, Decodable)]
230pub struct MultiApiVersion(Vec<ApiVersion>);
231
232impl MultiApiVersion {
233    pub fn new() -> Self {
234        Self::default()
235    }
236
237    /// Verify the invariant: sorted by unique major numbers
238    fn is_consistent(&self) -> bool {
239        self.0
240            .iter()
241            .fold((None, true), |(prev, is_sorted), next| {
242                (
243                    Some(*next),
244                    is_sorted && prev.is_none_or(|prev| prev.major < next.major),
245                )
246            })
247            .1
248    }
249
250    fn iter(&'_ self) -> MultiApiVersionIter<'_> {
251        MultiApiVersionIter(self.0.iter())
252    }
253
254    pub fn try_from_iter<T: IntoIterator<Item = ApiVersion>>(
255        iter: T,
256    ) -> result::Result<Self, ApiVersion> {
257        Result::from_iter(iter)
258    }
259
260    /// Insert `version` to the list of supported APIs
261    ///
262    /// Returns `Ok` if no existing element with the same `major` version was
263    /// found and new `version` was successfully inserted. Returns `Err` if
264    /// an existing element with the same `major` version was found, to allow
265    /// modifying its `minor` number. This is useful when merging required /
266    /// supported version sequences with each other.
267    fn try_insert(&mut self, version: ApiVersion) -> result::Result<(), &mut u32> {
268        match self
269            .0
270            .binary_search_by_key(&version.major, |version| version.major)
271        {
272            Ok(found_idx) => Err(self
273                .0
274                .get_mut(found_idx)
275                .map(|v| &mut v.minor)
276                .expect("element must exist - just checked")),
277            Err(insert_idx) => {
278                self.0.insert(insert_idx, version);
279                Ok(())
280            }
281        }
282    }
283
284    pub(crate) fn get_by_major(&self, major: u32) -> Option<ApiVersion> {
285        self.0
286            .binary_search_by_key(&major, |version| version.major)
287            .ok()
288            .map(|index| {
289                self.0
290                    .get(index)
291                    .copied()
292                    .expect("Must exist because binary_search_by_key told us so")
293            })
294    }
295}
296
297impl<'de> Deserialize<'de> for MultiApiVersion {
298    fn deserialize<D>(deserializer: D) -> result::Result<Self, D::Error>
299    where
300        D: serde::de::Deserializer<'de>,
301    {
302        use serde::de::Error;
303
304        let inner = Vec::<ApiVersion>::deserialize(deserializer)?;
305
306        let ret = Self(inner);
307
308        if !ret.is_consistent() {
309            return Err(D::Error::custom(
310                "Invalid MultiApiVersion value: inconsistent",
311            ));
312        }
313
314        Ok(ret)
315    }
316}
317
318impl fmt::Display for MultiApiVersion {
319    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
320        f.write_str("[")?;
321        for (i, v) in self.0.iter().enumerate() {
322            if 0 < i {
323                f.write_str(", ")?;
324            }
325            write!(f, "{v}")?;
326        }
327        f.write_str("]")
328    }
329}
330
331pub struct MultiApiVersionIter<'a>(std::slice::Iter<'a, ApiVersion>);
332
333impl Iterator for MultiApiVersionIter<'_> {
334    type Item = ApiVersion;
335
336    fn next(&mut self) -> Option<Self::Item> {
337        self.0.next().copied()
338    }
339}
340
341impl<'a> IntoIterator for &'a MultiApiVersion {
342    type Item = ApiVersion;
343
344    type IntoIter = MultiApiVersionIter<'a>;
345
346    fn into_iter(self) -> Self::IntoIter {
347        self.iter()
348    }
349}
350
351impl FromIterator<ApiVersion> for Result<MultiApiVersion, ApiVersion> {
352    fn from_iter<T: IntoIterator<Item = ApiVersion>>(iter: T) -> Self {
353        let mut s = MultiApiVersion::new();
354        for version in iter {
355            if s.try_insert(version).is_err() {
356                return Err(version);
357            }
358        }
359        Ok(s)
360    }
361}
362
363#[test]
364fn api_version_multi_sanity() {
365    let mut mav = MultiApiVersion::new();
366
367    assert_eq!(mav.try_insert(ApiVersion { major: 2, minor: 3 }), Ok(()));
368
369    assert_eq!(mav.get_by_major(0), None);
370    assert_eq!(mav.get_by_major(2), Some(ApiVersion { major: 2, minor: 3 }));
371
372    assert_eq!(
373        mav.try_insert(ApiVersion { major: 2, minor: 1 }),
374        Err(&mut 3)
375    );
376    *mav.try_insert(ApiVersion { major: 2, minor: 2 })
377        .expect_err("must be error, just like one line above") += 1;
378    assert_eq!(mav.try_insert(ApiVersion { major: 1, minor: 2 }), Ok(()));
379    assert_eq!(mav.try_insert(ApiVersion { major: 3, minor: 4 }), Ok(()));
380    assert_eq!(
381        mav.try_insert(ApiVersion { major: 2, minor: 0 }),
382        Err(&mut 4)
383    );
384    assert_eq!(mav.get_by_major(5), None);
385    assert_eq!(mav.get_by_major(3), Some(ApiVersion { major: 3, minor: 4 }));
386
387    debug_assert!(mav.is_consistent());
388}
389
390#[test]
391fn api_version_multi_from_iter_sanity() {
392    assert!(result::Result::<MultiApiVersion, ApiVersion>::from_iter([]).is_ok());
393    assert!(
394        result::Result::<MultiApiVersion, ApiVersion>::from_iter([ApiVersion {
395            major: 0,
396            minor: 0
397        }])
398        .is_ok()
399    );
400    assert!(
401        result::Result::<MultiApiVersion, ApiVersion>::from_iter([
402            ApiVersion { major: 0, minor: 1 },
403            ApiVersion { major: 1, minor: 2 }
404        ])
405        .is_ok()
406    );
407    assert!(
408        result::Result::<MultiApiVersion, ApiVersion>::from_iter([
409            ApiVersion { major: 0, minor: 1 },
410            ApiVersion { major: 1, minor: 2 },
411            ApiVersion { major: 0, minor: 1 },
412        ])
413        .is_err()
414    );
415}
416
417#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Encodable, Decodable)]
418pub struct SupportedCoreApiVersions {
419    pub core_consensus: CoreConsensusVersion,
420    /// Supported Api versions for this core consensus versions
421    pub api: MultiApiVersion,
422}
423
424impl SupportedCoreApiVersions {
425    /// Get minor supported version by consensus and major numbers
426    pub fn get_minor_api_version(
427        &self,
428        core_consensus: CoreConsensusVersion,
429        major: u32,
430    ) -> Option<u32> {
431        if self.core_consensus.major != core_consensus.major {
432            return None;
433        }
434
435        self.api.get_by_major(major).map(|v| {
436            debug_assert_eq!(v.major, major);
437            v.minor
438        })
439    }
440}
441
442#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Encodable, Decodable)]
443pub struct SupportedModuleApiVersions {
444    pub core_consensus: CoreConsensusVersion,
445    pub module_consensus: ModuleConsensusVersion,
446    /// Supported Api versions for this core & module consensus versions
447    pub api: MultiApiVersion,
448}
449
450impl SupportedModuleApiVersions {
451    /// Create `SupportedModuleApiVersions` from raw parts
452    ///
453    /// Panics if `api_version` parts conflict as per
454    /// [`SupportedModuleApiVersions`] invariants.
455    pub fn from_raw(core: (u32, u32), module: (u32, u32), api_versions: &[(u32, u32)]) -> Self {
456        Self {
457            core_consensus: CoreConsensusVersion::new(core.0, core.1),
458            module_consensus: ModuleConsensusVersion::new(module.0, module.1),
459            api: api_versions
460                .iter()
461                .copied()
462                .map(|(major, minor)| ApiVersion { major, minor })
463                .collect::<result::Result<MultiApiVersion, ApiVersion>>()
464            .expect(
465                "overlapping (conflicting) api versions when declaring SupportedModuleApiVersions",
466            ),
467        }
468    }
469
470    /// Get minor supported version by consensus and major numbers
471    pub fn get_minor_api_version(
472        &self,
473        core_consensus: CoreConsensusVersion,
474        module_consensus: ModuleConsensusVersion,
475        major: u32,
476    ) -> Option<u32> {
477        if self.core_consensus.major != core_consensus.major {
478            return None;
479        }
480
481        if self.module_consensus.major != module_consensus.major {
482            return None;
483        }
484
485        self.api.get_by_major(major).map(|v| {
486            debug_assert_eq!(v.major, major);
487            v.minor
488        })
489    }
490}
491
492impl fmt::Display for SupportedModuleApiVersions {
493    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
494        write!(
495            f,
496            "core={}, module={}, api={}",
497            self.core_consensus, self.module_consensus, self.api
498        )
499    }
500}
501
502#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Decodable, Encodable)]
503pub struct SupportedApiVersionsSummary {
504    pub core: SupportedCoreApiVersions,
505    pub modules: BTreeMap<ModuleInstanceId, SupportedModuleApiVersions>,
506}
507
508/// A summary of server API versions for core and all registered modules.
509#[derive(Serialize)]
510pub struct ServerApiVersionsSummary {
511    pub core: MultiApiVersion,
512    pub modules: BTreeMap<ModuleKind, MultiApiVersion>,
513}
514
515/// A summary of server database versions for all registered modules.
516#[derive(Serialize)]
517pub struct ServerDbVersionsSummary {
518    pub modules: BTreeMap<ModuleKind, DatabaseVersion>,
519}