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