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