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