fedimint_client_module/
api_version_discovery.rs

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