Skip to main content

fedimint_client_module/
api_version_discovery.rs

1use std::collections::BTreeMap;
2
3use fedimint_api_client::api::ApiVersionSet;
4use fedimint_core::PeerId;
5use fedimint_core::module::{
6    ApiVersion, SupportedApiVersionsSummary, SupportedCoreApiVersions, SupportedModuleApiVersions,
7};
8
9use crate::error::ApiVersionDiscoveryError;
10
11pub fn discover_common_core_api_version(
12    client_versions: &SupportedCoreApiVersions,
13    peer_versions: &BTreeMap<PeerId, SupportedCoreApiVersions>,
14) -> Option<ApiVersion> {
15    let mut best_major = None;
16    let mut best_major_peer_num = 0;
17
18    // Find major api version with highest peer number supporting it
19    for client_api_version in &client_versions.api {
20        let peers_compatible_num = peer_versions
21            .values()
22            .filter_map(|supported_versions| {
23                supported_versions
24                    .get_minor_api_version(client_versions.core_consensus, client_api_version.major)
25            })
26            .filter(|peer_minor| client_api_version.minor <= *peer_minor)
27            .count();
28
29        if best_major_peer_num < peers_compatible_num {
30            best_major = Some(client_api_version);
31            best_major_peer_num = peers_compatible_num;
32        }
33    }
34
35    // Adjust the minor version to the smallest supported by all matching peers
36    best_major.map(
37        |ApiVersion {
38             major: best_major,
39             minor: best_major_minor,
40         }| ApiVersion {
41            major: best_major,
42            minor: peer_versions
43                .values()
44                .filter_map(|supported| {
45                    supported.get_minor_api_version(client_versions.core_consensus, best_major)
46                })
47                .filter(|peer_minor| best_major_minor <= *peer_minor)
48                .min()
49                .expect("We must have at least one"),
50        },
51    )
52}
53
54#[test]
55fn discover_common_core_api_version_sanity() {
56    use fedimint_core::module::MultiApiVersion;
57
58    let core_consensus = fedimint_core::module::CoreConsensusVersion::new(0, 0);
59    let client_versions = SupportedCoreApiVersions {
60        core_consensus,
61        api: MultiApiVersion::try_from_iter([
62            ApiVersion { major: 2, minor: 3 },
63            ApiVersion { major: 3, minor: 1 },
64        ])
65        .unwrap(),
66    };
67
68    assert!(discover_common_core_api_version(&client_versions, &BTreeMap::from([])).is_none());
69    assert_eq!(
70        discover_common_core_api_version(
71            &client_versions,
72            &BTreeMap::from([(
73                PeerId::from(0),
74                SupportedCoreApiVersions {
75                    core_consensus: fedimint_core::module::CoreConsensusVersion::new(0, 0),
76                    api: MultiApiVersion::try_from_iter([ApiVersion { major: 2, minor: 3 }])
77                        .unwrap(),
78                }
79            )])
80        ),
81        Some(ApiVersion { major: 2, minor: 3 })
82    );
83    assert_eq!(
84        discover_common_core_api_version(
85            &client_versions,
86            &BTreeMap::from([(
87                PeerId::from(0),
88                SupportedCoreApiVersions {
89                    core_consensus: fedimint_core::module::CoreConsensusVersion::new(0, 1), /* different minor consensus version, we don't care */
90                    api: MultiApiVersion::try_from_iter([ApiVersion { major: 2, minor: 3 }])
91                        .unwrap(),
92                }
93            )])
94        ),
95        Some(ApiVersion { major: 2, minor: 3 })
96    );
97    assert_eq!(
98        discover_common_core_api_version(
99            &client_versions,
100            &BTreeMap::from([(
101                PeerId::from(0),
102                SupportedCoreApiVersions {
103                    core_consensus: fedimint_core::module::CoreConsensusVersion::new(1, 0), /* wrong consensus version */
104                    api: MultiApiVersion::try_from_iter([ApiVersion { major: 2, minor: 4 }])
105                        .unwrap(),
106                }
107            )])
108        ),
109        None
110    );
111    assert_eq!(
112        discover_common_core_api_version(
113            &client_versions,
114            &BTreeMap::from([
115                (
116                    PeerId::from(0),
117                    SupportedCoreApiVersions {
118                        core_consensus,
119                        api: MultiApiVersion::try_from_iter([ApiVersion { major: 2, minor: 2 }])
120                            .unwrap(),
121                    }
122                ),
123                (
124                    PeerId::from(1),
125                    SupportedCoreApiVersions {
126                        core_consensus,
127                        api: MultiApiVersion::try_from_iter([ApiVersion { major: 2, minor: 1 }])
128                            .unwrap(),
129                    }
130                ),
131                (
132                    PeerId::from(1),
133                    SupportedCoreApiVersions {
134                        core_consensus,
135                        api: MultiApiVersion::try_from_iter([ApiVersion { major: 3, minor: 1 }])
136                            .unwrap(),
137                    }
138                )
139            ])
140        ),
141        Some(ApiVersion { major: 3, minor: 1 })
142    );
143    assert_eq!(
144        discover_common_core_api_version(
145            &client_versions,
146            &BTreeMap::from([
147                (
148                    PeerId::from(0),
149                    SupportedCoreApiVersions {
150                        core_consensus,
151                        api: MultiApiVersion::try_from_iter([ApiVersion { major: 2, minor: 4 }])
152                            .unwrap(),
153                    }
154                ),
155                (
156                    PeerId::from(1),
157                    SupportedCoreApiVersions {
158                        core_consensus,
159                        api: MultiApiVersion::try_from_iter([ApiVersion { major: 2, minor: 5 }])
160                            .unwrap(),
161                    }
162                ),
163            ])
164        ),
165        Some(ApiVersion { major: 2, minor: 4 })
166    );
167}
168
169fn discover_common_module_api_version(
170    client_versions: &SupportedModuleApiVersions,
171    peer_versions: &BTreeMap<PeerId, SupportedModuleApiVersions>,
172) -> Option<ApiVersion> {
173    let mut best_major = None;
174    let mut best_major_peer_num = 0;
175
176    // Find major api version with highest peer number supporting it
177    for client_api_version in &client_versions.api {
178        let peers_compatible_num = peer_versions
179            .values()
180            .filter_map(|supported_versions| {
181                supported_versions.get_minor_api_version(
182                    client_versions.core_consensus,
183                    client_versions.module_consensus,
184                    client_api_version.major,
185                )
186            })
187            .filter(|peer_minor| client_api_version.minor <= *peer_minor)
188            .count();
189
190        if best_major_peer_num < peers_compatible_num {
191            best_major = Some(client_api_version);
192            best_major_peer_num = peers_compatible_num;
193        }
194    }
195
196    // Adjust the minor version to the smallest supported by all matching peers
197    best_major.map(
198        |ApiVersion {
199             major: best_major,
200             minor: best_major_minor,
201         }| ApiVersion {
202            major: best_major,
203            minor: peer_versions
204                .values()
205                .filter_map(|supported| {
206                    supported.get_minor_api_version(
207                        client_versions.core_consensus,
208                        client_versions.module_consensus,
209                        best_major,
210                    )
211                })
212                .filter(|peer_minor| best_major_minor <= *peer_minor)
213                .min()
214                .expect("We must have at least one"),
215        },
216    )
217}
218
219pub fn discover_common_api_versions_set(
220    client_versions: &SupportedApiVersionsSummary,
221    peer_versions: &BTreeMap<PeerId, SupportedApiVersionsSummary>,
222) -> Result<ApiVersionSet, ApiVersionDiscoveryError> {
223    Ok(ApiVersionSet {
224        core: discover_common_core_api_version(
225            &client_versions.core,
226            &peer_versions
227                .iter()
228                .map(|(peer_id, peer_supported_api_versions)| {
229                    (*peer_id, peer_supported_api_versions.core.clone())
230                })
231                .collect(),
232        )
233        .ok_or(ApiVersionDiscoveryError)?,
234        modules: client_versions
235            .modules
236            .iter()
237            .filter_map(
238                |(module_instance_id, client_supported_module_api_versions)| {
239                    let discover_common_module_api_version = discover_common_module_api_version(
240                        client_supported_module_api_versions,
241                        &peer_versions
242                            .iter()
243                            .filter_map(|(peer_id, peer_supported_api_versions_summary)| {
244                                peer_supported_api_versions_summary
245                                    .modules
246                                    .get(module_instance_id)
247                                    .map(|versions| (*peer_id, versions.clone()))
248                            })
249                            .collect(),
250                    );
251                    discover_common_module_api_version.map(|v| (*module_instance_id, v))
252                },
253            )
254            .collect(),
255    })
256}