Skip to main content

fedimint_meta_server/
lib.rs

1#![deny(clippy::pedantic)]
2#![allow(clippy::module_name_repetitions)]
3#![allow(clippy::missing_panics_doc)]
4#![allow(clippy::missing_errors_doc)]
5
6pub use fedimint_meta_common as common;
7
8pub mod db;
9
10use std::collections::BTreeMap;
11use std::future;
12
13use async_trait::async_trait;
14use db::{
15    MetaConsensusKey, MetaDesiredKey, MetaDesiredValue, MetaSubmissionsByKeyPrefix,
16    MetaSubmissionsKey,
17};
18use fedimint_core::config::{
19    ServerModuleConfig, ServerModuleConsensusConfig, TypedServerModuleConfig,
20    TypedServerModuleConsensusConfig,
21};
22use fedimint_core::core::ModuleInstanceId;
23use fedimint_core::db::{
24    Database, DatabaseTransaction, DatabaseVersion, IDatabaseTransactionOpsCoreTyped,
25    NonCommittable,
26};
27use fedimint_core::module::audit::Audit;
28use fedimint_core::module::serde_json::Value;
29use fedimint_core::module::{
30    ApiAuth, ApiEndpoint, ApiError, ApiVersion, CoreConsensusVersion, InputMeta,
31    ModuleConsensusVersion, ModuleInit, TransactionItemAmounts, api_endpoint, serde_json,
32};
33use fedimint_core::{InPoint, NumPeers, OutPoint, PeerId, push_db_pair_items};
34use fedimint_logging::LOG_MODULE_META;
35use fedimint_meta_common::config::{
36    MetaClientConfig, MetaConfig, MetaConfigConsensus, MetaConfigPrivate,
37};
38use fedimint_meta_common::endpoint::{
39    GET_CONSENSUS_ENDPOINT, GET_CONSENSUS_REV_ENDPOINT, GET_SUBMISSIONS_ENDPOINT,
40    GetConsensusRequest, GetSubmissionResponse, GetSubmissionsRequest, SUBMIT_ENDPOINT,
41    SubmitRequest,
42};
43use fedimint_meta_common::{
44    DEFAULT_META_KEY, MODULE_CONSENSUS_VERSION, MetaCommonInit, MetaConsensusItem,
45    MetaConsensusValue, MetaInput, MetaInputError, MetaKey, MetaModuleTypes, MetaOutput,
46    MetaOutputError, MetaOutputOutcome, MetaValue,
47};
48use fedimint_server_core::config::PeerHandleOps;
49use fedimint_server_core::migration::ServerModuleDbMigrationFn;
50use fedimint_server_core::{
51    ConfigGenModuleArgs, ServerModule, ServerModuleInit, ServerModuleInitArgs,
52};
53use futures::StreamExt;
54use rand::{Rng, thread_rng};
55use strum::IntoEnumIterator;
56use tracing::{debug, info, trace};
57
58use crate::db::{
59    DbKeyPrefix, MetaConsensusKeyPrefix, MetaDesiredKeyPrefix, MetaSubmissionValue,
60    MetaSubmissionsKeyPrefix,
61};
62
63/// Generates the module
64#[derive(Debug, Clone)]
65pub struct MetaInit;
66
67// TODO: Boilerplate-code
68impl ModuleInit for MetaInit {
69    type Common = MetaCommonInit;
70
71    /// Dumps all database items for debugging
72    async fn dump_database(
73        &self,
74        dbtx: &mut DatabaseTransaction<'_>,
75        prefix_names: Vec<String>,
76    ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
77        // TODO: Boilerplate-code
78        let mut items: BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> = BTreeMap::new();
79        let filtered_prefixes = DbKeyPrefix::iter().filter(|f| {
80            prefix_names.is_empty() || prefix_names.contains(&f.to_string().to_lowercase())
81        });
82
83        for table in filtered_prefixes {
84            match table {
85                DbKeyPrefix::Desired => {
86                    push_db_pair_items!(
87                        dbtx,
88                        MetaDesiredKeyPrefix,
89                        MetaDesiredKey,
90                        MetaDesiredValue,
91                        items,
92                        "Meta Desired"
93                    );
94                }
95                DbKeyPrefix::Consensus => {
96                    push_db_pair_items!(
97                        dbtx,
98                        MetaConsensusKeyPrefix,
99                        MetaConsensusKey,
100                        MetaConsensusValue,
101                        items,
102                        "Meta Consensus"
103                    );
104                }
105                DbKeyPrefix::Submissions => {
106                    push_db_pair_items!(
107                        dbtx,
108                        MetaSubmissionsKeyPrefix,
109                        MetaSubmissionsKey,
110                        MetaSubmissionValue,
111                        items,
112                        "Meta Submissions"
113                    );
114                }
115            }
116        }
117
118        Box::new(items.into_iter())
119    }
120}
121
122/// Implementation of server module non-consensus functions
123#[async_trait]
124impl ServerModuleInit for MetaInit {
125    type Module = Meta;
126
127    /// Returns the version of this module
128    fn versions(&self, _core: CoreConsensusVersion) -> &[ModuleConsensusVersion] {
129        &[MODULE_CONSENSUS_VERSION]
130    }
131
132    /// Initialize the module
133    async fn init(&self, args: &ServerModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
134        Ok(Meta {
135            cfg: args.cfg().to_typed()?,
136            our_peer_id: args.our_peer_id(),
137            num_peers: args.num_peers(),
138            db: args.db().clone(),
139        })
140    }
141
142    /// Generates configs for all peers in a trusted manner for testing
143    fn trusted_dealer_gen(
144        &self,
145        peers: &[PeerId],
146        _args: &ConfigGenModuleArgs,
147    ) -> BTreeMap<PeerId, ServerModuleConfig> {
148        // Generate a config for each peer
149        peers
150            .iter()
151            .map(|&peer| {
152                let config = MetaConfig {
153                    private: MetaConfigPrivate,
154                    consensus: MetaConfigConsensus {},
155                };
156                (peer, config.to_erased())
157            })
158            .collect()
159    }
160
161    /// Generates configs for all peers in an untrusted manner
162    async fn distributed_gen(
163        &self,
164        _peers: &(dyn PeerHandleOps + Send + Sync),
165        _args: &ConfigGenModuleArgs,
166    ) -> anyhow::Result<ServerModuleConfig> {
167        Ok(MetaConfig {
168            private: MetaConfigPrivate,
169            consensus: MetaConfigConsensus {},
170        }
171        .to_erased())
172    }
173
174    /// Converts the consensus config into the client config
175    fn get_client_config(
176        &self,
177        config: &ServerModuleConsensusConfig,
178    ) -> anyhow::Result<MetaClientConfig> {
179        let _config = MetaConfigConsensus::from_erased(config)?;
180        Ok(MetaClientConfig {})
181    }
182
183    fn validate_config(
184        &self,
185        _identity: &PeerId,
186        _config: ServerModuleConfig,
187    ) -> anyhow::Result<()> {
188        Ok(())
189    }
190
191    /// DB migrations to move from old to newer versions
192    fn get_database_migrations(
193        &self,
194    ) -> BTreeMap<DatabaseVersion, ServerModuleDbMigrationFn<Meta>> {
195        BTreeMap::new()
196    }
197}
198
199/// Meta module
200#[derive(Debug)]
201pub struct Meta {
202    pub cfg: MetaConfig,
203    pub our_peer_id: PeerId,
204    pub num_peers: NumPeers,
205    pub db: Database,
206}
207
208impl Meta {
209    async fn get_desired(dbtx: &mut DatabaseTransaction<'_>) -> Vec<(MetaKey, MetaDesiredValue)> {
210        dbtx.find_by_prefix(&MetaDesiredKeyPrefix)
211            .await
212            .map(|(k, v)| (k.0, v))
213            .collect()
214            .await
215    }
216
217    async fn get_submission(
218        dbtx: &mut DatabaseTransaction<'_>,
219        key: MetaKey,
220        peer_id: PeerId,
221    ) -> Option<MetaSubmissionValue> {
222        dbtx.get_value(&MetaSubmissionsKey { key, peer_id }).await
223    }
224
225    async fn get_consensus(dbtx: &mut DatabaseTransaction<'_>, key: MetaKey) -> Option<MetaValue> {
226        dbtx.get_value(&MetaConsensusKey(key))
227            .await
228            .map(|consensus_value| consensus_value.value)
229    }
230
231    async fn change_consensus(
232        dbtx: &mut DatabaseTransaction<'_, NonCommittable>,
233        key: MetaKey,
234        value: MetaValue,
235        matching_submissions: Vec<PeerId>,
236    ) {
237        let value_len = value.as_slice().len();
238        let revision = dbtx
239            .get_value(&MetaConsensusKey(key))
240            .await
241            .map(|cv| cv.revision);
242        let revision = revision.map(|r| r.wrapping_add(1)).unwrap_or_default();
243        dbtx.insert_entry(
244            &MetaConsensusKey(key),
245            &MetaConsensusValue { revision, value },
246        )
247        .await;
248
249        info!(target: LOG_MODULE_META, %key, rev = %revision, len = %value_len, "New consensus value");
250
251        for peer_id in matching_submissions {
252            dbtx.remove_entry(&MetaSubmissionsKey { key, peer_id })
253                .await;
254        }
255    }
256}
257
258/// Implementation of consensus for the server module
259#[async_trait]
260impl ServerModule for Meta {
261    /// Define the consensus types
262    type Common = MetaModuleTypes;
263    type Init = MetaInit;
264
265    /// Check the difference between what's desired vs submitted and consensus.
266    ///
267    /// Returns:
268    /// Items to submit as our proposal.
269    async fn consensus_proposal(
270        &self,
271        dbtx: &mut DatabaseTransaction<'_>,
272    ) -> Vec<MetaConsensusItem> {
273        let desired: Vec<_> = Self::get_desired(dbtx).await;
274
275        let mut to_submit = vec![];
276
277        for (
278            key,
279            MetaDesiredValue {
280                value: desired_value,
281                salt,
282            },
283        ) in desired
284        {
285            let consensus_value = &Self::get_consensus(dbtx, key).await;
286            let consensus_submission_value =
287                Self::get_submission(dbtx, key, self.our_peer_id).await;
288            if consensus_submission_value.as_ref()
289                == Some(&MetaSubmissionValue {
290                    value: desired_value.clone(),
291                    salt,
292                })
293            {
294                // our submission is already registered, nothing to do
295            } else if consensus_value.as_ref() == Some(&desired_value) {
296                if consensus_submission_value.is_none() {
297                    // our desired value is equal to consensus and cleared our
298                    // submission (as it is equal the
299                    // consensus) so we don't need to propose it
300                } else {
301                    // we want to submit the same value as the current consensus, usually
302                    // to clear the previous submission that did not became the consensus (we were
303                    // outvoted)
304                    to_submit.push(MetaConsensusItem {
305                        key,
306                        value: desired_value,
307                        salt,
308                    });
309                }
310            } else {
311                to_submit.push(MetaConsensusItem {
312                    key,
313                    value: desired_value,
314                    salt,
315                });
316            }
317        }
318
319        trace!(target: LOG_MODULE_META, ?to_submit, "Desired actions");
320        to_submit
321    }
322
323    /// BUG: This implementation fails to return an `Err` on redundant consensus
324    /// items. If you are using this code as a template,
325    /// make sure to read the [`ServerModule::process_consensus_item`]
326    /// documentation,
327    async fn process_consensus_item<'a, 'b>(
328        &'a self,
329        dbtx: &mut DatabaseTransaction<'b>,
330        MetaConsensusItem { key, value, salt }: MetaConsensusItem,
331        peer_id: PeerId,
332    ) -> anyhow::Result<()> {
333        trace!(target: LOG_MODULE_META, %key, %value, %salt, "Processing consensus item proposal");
334
335        let new_value = MetaSubmissionValue { salt, value };
336        // first of all: any new submission overrides previous submission
337        if let Some(prev_value) = Self::get_submission(dbtx, key, peer_id).await
338            && prev_value != new_value
339        {
340            dbtx.remove_entry(&MetaSubmissionsKey { key, peer_id })
341                .await;
342        }
343        // then: if the submission is equal to the current consensus, it's ignored
344        if Some(&new_value.value) == Self::get_consensus(dbtx, key).await.as_ref() {
345            debug!(target: LOG_MODULE_META, %peer_id, %key, "Peer submitted a redundant value");
346            return Ok(());
347        }
348
349        // otherwise, new submission is recorded
350        dbtx.insert_entry(&MetaSubmissionsKey { key, peer_id }, &new_value)
351            .await;
352
353        // we check how many peers submitted the same value (including this peer)
354        let matching_submissions: Vec<PeerId> = dbtx
355            .find_by_prefix(&MetaSubmissionsByKeyPrefix(key))
356            .await
357            .filter(|(_submission_key, submission_value)| {
358                future::ready(new_value.value == submission_value.value)
359            })
360            .map(|(submission_key, _)| submission_key.peer_id)
361            .collect()
362            .await;
363
364        let threshold = self.num_peers.threshold();
365        info!(target: LOG_MODULE_META,
366             %peer_id,
367             %key,
368            value_len = %new_value.value.as_slice().len(),
369             matching = %matching_submissions.len(),
370            %threshold, "Peer submitted a value");
371
372        // if threshold or more, change the consensus value
373        if threshold <= matching_submissions.len() {
374            Self::change_consensus(dbtx, key, new_value.value, matching_submissions).await;
375        }
376
377        Ok(())
378    }
379
380    async fn process_input<'a, 'b, 'c>(
381        &'a self,
382        _dbtx: &mut DatabaseTransaction<'c>,
383        _input: &'b MetaInput,
384        _in_point: InPoint,
385    ) -> Result<InputMeta, MetaInputError> {
386        Err(MetaInputError::NotSupported)
387    }
388
389    async fn process_output<'a, 'b>(
390        &'a self,
391        _dbtx: &mut DatabaseTransaction<'b>,
392        _output: &'a MetaOutput,
393        _out_point: OutPoint,
394    ) -> Result<TransactionItemAmounts, MetaOutputError> {
395        Err(MetaOutputError::NotSupported)
396    }
397
398    async fn output_status(
399        &self,
400        _dbtx: &mut DatabaseTransaction<'_>,
401        _out_point: OutPoint,
402    ) -> Option<MetaOutputOutcome> {
403        None
404    }
405
406    async fn audit(
407        &self,
408        _dbtx: &mut DatabaseTransaction<'_>,
409        _audit: &mut Audit,
410        _module_instance_id: ModuleInstanceId,
411    ) {
412    }
413
414    fn api_endpoints(&self) -> Vec<ApiEndpoint<Self>> {
415        vec![
416            api_endpoint! {
417                SUBMIT_ENDPOINT,
418                ApiVersion::new(0, 0),
419                async |module: &Meta, context, request: SubmitRequest| -> () {
420
421                    match context.request_auth() {
422                        None => return Err(ApiError::bad_request("Missing password".to_string())),
423                        Some(auth) => {
424                            let db = context.db();
425                            let mut dbtx = db.begin_transaction().await;
426                            module.handle_submit_request(&mut dbtx.to_ref_nc(), &auth, &request).await?;
427                            dbtx.commit_tx_result().await?;
428                        }
429                    }
430
431                    Ok(())
432                }
433            },
434            api_endpoint! {
435                GET_CONSENSUS_ENDPOINT,
436                ApiVersion::new(0, 0),
437                async |module: &Meta, context, request: GetConsensusRequest| -> Option<MetaConsensusValue> {
438                    let db = context.db();
439                    let mut dbtx = db.begin_transaction_nc().await;
440                    module.handle_get_consensus_request(&mut dbtx, &request).await
441                }
442            },
443            api_endpoint! {
444                GET_CONSENSUS_REV_ENDPOINT,
445                ApiVersion::new(0, 0),
446                async |module: &Meta, context, request: GetConsensusRequest| -> Option<u64> {
447                    let db = context.db();
448                    let mut dbtx = db.begin_transaction_nc().await;
449                    module.handle_get_consensus_revision_request(&mut dbtx, &request).await
450                }
451            },
452            api_endpoint! {
453                GET_SUBMISSIONS_ENDPOINT,
454                ApiVersion::new(0, 0),
455                async |module: &Meta, context, request: GetSubmissionsRequest| -> GetSubmissionResponse {
456                    match context.request_auth() {
457                        None => return Err(ApiError::bad_request("Missing password".to_string())),
458                        Some(auth) => {
459                            let db = context.db();
460                            let mut dbtx = db.begin_transaction_nc().await;
461                            module.handle_get_submissions_request(&mut dbtx, &auth, &request).await
462                        }
463                    }
464                }
465            },
466        ]
467    }
468}
469
470impl Meta {
471    async fn handle_submit_request(
472        &self,
473        dbtx: &mut DatabaseTransaction<'_, NonCommittable>,
474        _auth: &ApiAuth,
475        req: &SubmitRequest,
476    ) -> Result<(), ApiError> {
477        let salt = thread_rng().r#gen();
478
479        info!(target: LOG_MODULE_META,
480             key = %req.key,
481             peer_id = %self.our_peer_id,
482             value_len = %req.value.as_slice().len(),
483             "Our own guardian submitted a value");
484
485        dbtx.insert_entry(
486            &MetaDesiredKey(req.key),
487            &MetaDesiredValue {
488                value: req.value.clone(),
489                salt,
490            },
491        )
492        .await;
493
494        Ok(())
495    }
496
497    async fn handle_get_consensus_request(
498        &self,
499        dbtx: &mut DatabaseTransaction<'_, NonCommittable>,
500        req: &GetConsensusRequest,
501    ) -> Result<Option<MetaConsensusValue>, ApiError> {
502        Ok(dbtx.get_value(&MetaConsensusKey(req.0)).await)
503    }
504
505    async fn handle_get_consensus_revision_request(
506        &self,
507        dbtx: &mut DatabaseTransaction<'_, NonCommittable>,
508        req: &GetConsensusRequest,
509    ) -> Result<Option<u64>, ApiError> {
510        Ok(dbtx
511            .get_value(&MetaConsensusKey(req.0))
512            .await
513            .map(|cv| cv.revision))
514    }
515
516    async fn handle_get_submissions_request(
517        &self,
518        dbtx: &mut DatabaseTransaction<'_, NonCommittable>,
519        _auth: &ApiAuth,
520        req: &GetSubmissionsRequest,
521    ) -> Result<BTreeMap<PeerId, MetaValue>, ApiError> {
522        Ok(dbtx
523            .find_by_prefix(&MetaSubmissionsByKeyPrefix(req.0))
524            .await
525            .collect::<Vec<_>>()
526            .await
527            .into_iter()
528            .map(|(k, v)| (k.peer_id, v.value))
529            .collect())
530    }
531}
532
533// UI Methods for Meta Module
534
535impl Meta {
536    /// UI helper to submit a value change with default auth
537    pub async fn handle_submit_request_ui(&self, value: Value) -> Result<(), ApiError> {
538        let mut dbtx = self.db.begin_transaction().await;
539
540        self.handle_submit_request(
541            &mut dbtx.to_ref_nc(),
542            &ApiAuth::new(String::new()),
543            &SubmitRequest {
544                key: DEFAULT_META_KEY,
545                value: MetaValue::from(serde_json::to_vec(&value).unwrap().as_slice()),
546            },
547        )
548        .await?;
549
550        dbtx.commit_tx_result()
551            .await
552            .map_err(|e| ApiError::server_error(e.to_string()))?;
553
554        Ok(())
555    }
556
557    /// UI helper to get consensus data as a key-value map
558    pub async fn handle_get_consensus_request_ui(&self) -> Result<Option<Value>, ApiError> {
559        self.handle_get_consensus_request(
560            &mut self.db.begin_transaction_nc().await,
561            &GetConsensusRequest(DEFAULT_META_KEY),
562        )
563        .await?
564        .map(|value| serde_json::from_slice(value.value.as_slice()))
565        .transpose()
566        .map_err(|e| ApiError::server_error(e.to_string()))
567    }
568
569    /// UI helper to get consensus revision
570    pub async fn handle_get_consensus_revision_request_ui(&self) -> Result<u64, ApiError> {
571        self.handle_get_consensus_revision_request(
572            &mut self.db.begin_transaction_nc().await,
573            &GetConsensusRequest(DEFAULT_META_KEY),
574        )
575        .await
576        .map(|r| r.unwrap_or(0))
577    }
578
579    /// Get the submissions for UI display,
580    pub async fn handle_get_submissions_request_ui(
581        &self,
582    ) -> Result<BTreeMap<PeerId, Value>, ApiError> {
583        let mut submissions = BTreeMap::new();
584
585        let mut dbtx = self.db.begin_transaction_nc().await;
586
587        for (peer_id, value) in self
588            .handle_get_submissions_request(
589                &mut dbtx.to_ref_nc(),
590                &ApiAuth::new(String::new()),
591                &GetSubmissionsRequest(DEFAULT_META_KEY),
592            )
593            .await?
594        {
595            if let Ok(value) = serde_json::from_slice(value.as_slice()) {
596                submissions.insert(peer_id, value);
597            }
598        }
599
600        Ok(submissions)
601    }
602}