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