/ fedimint-core / src / module / version.rs
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.
 56  use std::collections::BTreeMap;
 57  use std::{cmp, result};
 58  
 59  use serde::{Deserialize, Serialize};
 60  
 61  use crate::core::{ModuleInstanceId, ModuleKind};
 62  use crate::db::DatabaseVersion;
 63  use 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)]
 72  pub struct CoreConsensusVersion {
 73      pub major: u32,
 74      pub minor: u32,
 75  }
 76  
 77  impl CoreConsensusVersion {
 78      pub const fn new(major: u32, minor: u32) -> Self {
 79          Self { major, minor }
 80      }
 81  }
 82  
 83  /// Globally declared core consensus version
 84  pub 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)]
111  pub struct ModuleConsensusVersion {
112      pub major: u32,
113      pub minor: u32,
114  }
115  
116  impl 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)]
140  pub 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  
158  impl 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  /// ```
170  impl cmp::PartialOrd for ApiVersion {
171      fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
172          Some(self.cmp(other))
173      }
174  }
175  
176  impl 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, Serialize, Default)]
196  pub struct MultiApiVersion(Vec<ApiVersion>);
197  
198  impl MultiApiVersion {
199      pub fn new() -> Self {
200          Default::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(|prev| prev.major < next.major).unwrap_or(true),
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  
265  impl<'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  
286  pub struct MultiApiVersionIter<'a>(std::slice::Iter<'a, ApiVersion>);
287  
288  impl<'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  
296  impl<'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  
306  impl 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.into_iter() {
310              if s.try_insert(version).is_err() {
311                  return Err(version);
312              }
313          }
314          Ok(s)
315      }
316  }
317  
318  #[test]
319  fn 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]
346  fn 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, Serialize, Deserialize)]
369  pub struct SupportedCoreApiVersions {
370      pub core_consensus: CoreConsensusVersion,
371      /// Supported Api versions for this core consensus versions
372      pub api: MultiApiVersion,
373  }
374  
375  impl 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, Serialize, Deserialize)]
394  pub 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  
401  impl 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: result::Result::<MultiApiVersion, ApiVersion>::from_iter(
411                  api_versions
412                      .iter()
413                      .copied()
414                      .map(|(major, minor)| ApiVersion { major, minor }),
415              )
416              .expect(
417                  "overlapping (conflicting) api versions when declaring SupportedModuleApiVersions",
418              ),
419          }
420      }
421  
422      /// Get minor supported version by consensus and major numbers
423      pub fn get_minor_api_version(
424          &self,
425          core_consensus: CoreConsensusVersion,
426          module_consensus: ModuleConsensusVersion,
427          major: u32,
428      ) -> Option<u32> {
429          if self.core_consensus.major != core_consensus.major {
430              return None;
431          }
432  
433          if self.module_consensus.major != module_consensus.major {
434              return None;
435          }
436  
437          self.api.get_by_major(major).map(|v| {
438              debug_assert_eq!(v.major, major);
439              v.minor
440          })
441      }
442  }
443  
444  #[derive(Debug, Clone, Serialize, Deserialize)]
445  pub struct SupportedApiVersionsSummary {
446      pub core: SupportedCoreApiVersions,
447      pub modules: BTreeMap<ModuleInstanceId, SupportedModuleApiVersions>,
448  }
449  
450  /// A summary of server API versions for core and all registered modules.
451  #[derive(Serialize)]
452  pub struct ServerApiVersionsSummary {
453      pub core: MultiApiVersion,
454      pub modules: BTreeMap<ModuleKind, MultiApiVersion>,
455  }
456  
457  /// A summary of server database versions for all registered modules.
458  #[derive(Serialize)]
459  pub struct ServerDbVersionsSummary {
460      pub modules: BTreeMap<ModuleKind, DatabaseVersion>,
461  }