fedimint_meta_client/
lib.rs

1#![deny(clippy::pedantic)]
2#![allow(clippy::missing_errors_doc)]
3#![allow(clippy::module_name_repetitions)]
4
5pub mod api;
6#[cfg(feature = "cli")]
7pub mod cli;
8pub mod db;
9pub mod states;
10
11use std::collections::BTreeMap;
12use std::time::Duration;
13
14use api::MetaFederationApi;
15use common::{KIND, MetaConsensusValue, MetaKey, MetaValue};
16use db::DbKeyPrefix;
17use fedimint_api_client::api::{DynGlobalApi, DynModuleApi};
18use fedimint_client_module::db::ClientModuleMigrationFn;
19use fedimint_client_module::meta::{FetchKind, LegacyMetaSource, MetaSource, MetaValues};
20use fedimint_client_module::module::init::{ClientModuleInit, ClientModuleInitArgs};
21use fedimint_client_module::module::recovery::NoModuleBackup;
22use fedimint_client_module::module::{ClientModule, IClientModule};
23use fedimint_client_module::sm::Context;
24use fedimint_core::config::ClientConfig;
25use fedimint_core::core::{Decoder, ModuleKind};
26use fedimint_core::db::{DatabaseTransaction, DatabaseVersion};
27use fedimint_core::module::{
28    Amounts, ApiAuth, ApiVersion, ModuleCommon, ModuleInit, MultiApiVersion,
29};
30use fedimint_core::util::backoff_util::FibonacciBackoff;
31use fedimint_core::util::{backoff_util, retry};
32use fedimint_core::{PeerId, apply, async_trait_maybe_send};
33use fedimint_logging::LOG_CLIENT_MODULE_META;
34pub use fedimint_meta_common as common;
35use fedimint_meta_common::{DEFAULT_META_KEY, MetaCommonInit, MetaModuleTypes};
36use states::MetaStateMachine;
37use strum::IntoEnumIterator;
38use tracing::{debug, warn};
39
40#[derive(Debug)]
41pub struct MetaClientModule {
42    module_api: DynModuleApi,
43    admin_auth: Option<ApiAuth>,
44}
45
46impl MetaClientModule {
47    fn admin_auth(&self) -> anyhow::Result<ApiAuth> {
48        self.admin_auth
49            .clone()
50            .ok_or_else(|| anyhow::format_err!("Admin auth not set"))
51    }
52
53    /// Submit a meta consensus value
54    ///
55    /// When *threshold* amount of peers submits the exact same value it
56    /// becomes a new consensus value.
57    ///
58    /// To "cancel" previous vote, peer can submit a value equal to the current
59    /// consensus value.
60    pub async fn submit(&self, key: MetaKey, value: MetaValue) -> anyhow::Result<()> {
61        self.module_api
62            .submit(key, value, self.admin_auth()?)
63            .await?;
64
65        Ok(())
66    }
67
68    /// Get the current meta consensus value along with it's revision
69    ///
70    /// See [`Self::get_consensus_value_rev`] to use when checking for updates.
71    pub async fn get_consensus_value(
72        &self,
73        key: MetaKey,
74    ) -> anyhow::Result<Option<MetaConsensusValue>> {
75        Ok(self.module_api.get_consensus(key).await?)
76    }
77
78    /// Get the current meta consensus value revision
79    ///
80    /// Each time a meta consensus value changes, the revision increases,
81    /// so checking just the revision can save a lot of bandwidth in periodic
82    /// checks.
83    pub async fn get_consensus_value_rev(&self, key: MetaKey) -> anyhow::Result<Option<u64>> {
84        Ok(self.module_api.get_consensus_rev(key).await?)
85    }
86
87    /// Get current submissions to change the meta consensus value.
88    ///
89    /// Upon changing the consensus
90    pub async fn get_submissions(
91        &self,
92        key: MetaKey,
93    ) -> anyhow::Result<BTreeMap<PeerId, MetaValue>> {
94        Ok(self
95            .module_api
96            .get_submissions(key, self.admin_auth()?)
97            .await?)
98    }
99}
100
101/// Data needed by the state machine
102#[derive(Debug, Clone)]
103pub struct MetaClientContext {
104    pub meta_decoder: Decoder,
105}
106
107// TODO: Boiler-plate
108impl Context for MetaClientContext {
109    const KIND: Option<ModuleKind> = Some(KIND);
110}
111
112#[apply(async_trait_maybe_send!)]
113impl ClientModule for MetaClientModule {
114    type Init = MetaClientInit;
115    type Common = MetaModuleTypes;
116    type Backup = NoModuleBackup;
117    type ModuleStateMachineContext = MetaClientContext;
118    type States = MetaStateMachine;
119
120    fn context(&self) -> Self::ModuleStateMachineContext {
121        MetaClientContext {
122            meta_decoder: self.decoder(),
123        }
124    }
125
126    fn input_fee(
127        &self,
128        _amount: &Amounts,
129        _input: &<Self::Common as ModuleCommon>::Input,
130    ) -> Option<Amounts> {
131        unreachable!()
132    }
133
134    fn output_fee(
135        &self,
136        _amount: &Amounts,
137        _output: &<Self::Common as ModuleCommon>::Output,
138    ) -> Option<Amounts> {
139        unreachable!()
140    }
141
142    #[cfg(feature = "cli")]
143    async fn handle_cli_command(
144        &self,
145        args: &[std::ffi::OsString],
146    ) -> anyhow::Result<serde_json::Value> {
147        cli::handle_cli_command(self, args).await
148    }
149}
150
151#[derive(Debug, Clone)]
152pub struct MetaClientInit;
153
154// TODO: Boilerplate-code
155impl ModuleInit for MetaClientInit {
156    type Common = MetaCommonInit;
157
158    async fn dump_database(
159        &self,
160        _dbtx: &mut DatabaseTransaction<'_>,
161        prefix_names: Vec<String>,
162    ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
163        let items: BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> = BTreeMap::new();
164        let filtered_prefixes = DbKeyPrefix::iter().filter(|f| {
165            prefix_names.is_empty() || prefix_names.contains(&f.to_string().to_lowercase())
166        });
167
168        #[allow(clippy::never_loop)]
169        for table in filtered_prefixes {
170            match table {}
171        }
172
173        Box::new(items.into_iter())
174    }
175}
176
177/// Generates the client module
178#[apply(async_trait_maybe_send!)]
179impl ClientModuleInit for MetaClientInit {
180    type Module = MetaClientModule;
181
182    fn supported_api_versions(&self) -> MultiApiVersion {
183        MultiApiVersion::try_from_iter([ApiVersion { major: 0, minor: 0 }])
184            .expect("no version conflicts")
185    }
186
187    async fn init(&self, args: &ClientModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
188        Ok(MetaClientModule {
189            module_api: args.module_api().clone(),
190            admin_auth: args.admin_auth().cloned(),
191        })
192    }
193
194    fn get_database_migrations(&self) -> BTreeMap<DatabaseVersion, ClientModuleMigrationFn> {
195        BTreeMap::new()
196    }
197}
198
199/// Meta source fetching meta values from the meta module if available or the
200/// legacy meta source otherwise.
201#[derive(Clone, Debug, Default)]
202pub struct MetaModuleMetaSourceWithFallback<S = LegacyMetaSource> {
203    legacy: S,
204}
205
206impl<S> MetaModuleMetaSourceWithFallback<S> {
207    pub fn new(legacy: S) -> Self {
208        Self { legacy }
209    }
210}
211
212#[apply(async_trait_maybe_send!)]
213impl<S: MetaSource> MetaSource for MetaModuleMetaSourceWithFallback<S> {
214    async fn wait_for_update(&self) {
215        fedimint_core::runtime::sleep(Duration::from_secs(10 * 60)).await;
216    }
217
218    async fn fetch(
219        &self,
220        client_config: &ClientConfig,
221        api: &DynGlobalApi,
222        fetch_kind: fedimint_client_module::meta::FetchKind,
223        last_revision: Option<u64>,
224    ) -> anyhow::Result<fedimint_client_module::meta::MetaValues> {
225        let backoff = match fetch_kind {
226            // need to be fast the first time.
227            FetchKind::Initial => backoff_util::aggressive_backoff(),
228            FetchKind::Background => backoff_util::background_backoff(),
229        };
230
231        let maybe_meta_module_meta = get_meta_module_value(client_config, api, backoff)
232            .await
233            .map(|meta| {
234                Result::<_, anyhow::Error>::Ok(MetaValues {
235                    values: serde_json::from_slice(meta.value.as_slice())?,
236                    revision: meta.revision,
237                })
238            })
239            .transpose()?;
240
241        // If we couldn't fetch valid meta values from the meta module for any reason,
242        // fall back to the legacy meta source
243        if let Some(maybe_meta_module_meta) = maybe_meta_module_meta {
244            Ok(maybe_meta_module_meta)
245        } else {
246            self.legacy
247                .fetch(client_config, api, fetch_kind, last_revision)
248                .await
249        }
250    }
251}
252
253async fn get_meta_module_value(
254    client_config: &ClientConfig,
255    api: &DynGlobalApi,
256    backoff: FibonacciBackoff,
257) -> Option<MetaConsensusValue> {
258    match client_config.get_first_module_by_kind_cfg(KIND) {
259        Ok((instance_id, _)) => {
260            let meta_api = api.with_module(instance_id);
261
262            let overrides_res = retry("fetch_meta_values", backoff, || async {
263                Ok(meta_api.get_consensus(DEFAULT_META_KEY).await?)
264            })
265            .await;
266
267            match overrides_res {
268                Ok(Some(consensus)) => Some(consensus),
269                Ok(None) => {
270                    debug!(target: LOG_CLIENT_MODULE_META, "Meta module returned no consensus value");
271                    None
272                }
273                Err(e) => {
274                    warn!(target: LOG_CLIENT_MODULE_META, "Failed to fetch meta module consensus value: {}", e);
275                    None
276                }
277            }
278        }
279        _ => None,
280    }
281}