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    ApiEndpoint, ApiError, ApiVersion, CoreConsensusVersion, InputMeta, ModuleConsensusVersion,
31    ModuleInit, TransactionItemAmounts, admin_api_endpoint, public_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            admin_api_endpoint! {
417                SUBMIT_ENDPOINT,
418                ApiVersion::new(0, 0),
419                async |module: &Meta, context, request: SubmitRequest| -> () {
420
421                    let db = context.db();
422                    let mut dbtx = db.begin_transaction().await;
423                    module.handle_submit_request(&mut dbtx.to_ref_nc(), &request).await?;
424                    dbtx.commit_tx_result().await?;
425
426                    Ok(())
427                }
428            },
429            public_api_endpoint! {
430                GET_CONSENSUS_ENDPOINT,
431                ApiVersion::new(0, 0),
432                async |module: &Meta, context, request: GetConsensusRequest| -> Option<MetaConsensusValue> {
433                    let db = context.db();
434                    let mut dbtx = db.begin_transaction_nc().await;
435                    module.handle_get_consensus_request(&mut dbtx, &request).await
436                }
437            },
438            public_api_endpoint! {
439                GET_CONSENSUS_REV_ENDPOINT,
440                ApiVersion::new(0, 0),
441                async |module: &Meta, context, request: GetConsensusRequest| -> Option<u64> {
442                    let db = context.db();
443                    let mut dbtx = db.begin_transaction_nc().await;
444                    module.handle_get_consensus_revision_request(&mut dbtx, &request).await
445                }
446            },
447            admin_api_endpoint! {
448                GET_SUBMISSIONS_ENDPOINT,
449                ApiVersion::new(0, 0),
450                async |module: &Meta, context, request: GetSubmissionsRequest| -> GetSubmissionResponse {
451
452                    let db = context.db();
453                    let mut dbtx = db.begin_transaction_nc().await;
454                    module.handle_get_submissions_request(&mut dbtx, &request).await
455                }
456            },
457        ]
458    }
459}
460
461impl Meta {
462    async fn handle_submit_request(
463        &self,
464        dbtx: &mut DatabaseTransaction<'_, NonCommittable>,
465        req: &SubmitRequest,
466    ) -> Result<(), ApiError> {
467        let salt = thread_rng().r#gen();
468
469        info!(target: LOG_MODULE_META,
470             key = %req.key,
471             peer_id = %self.our_peer_id,
472             value_len = %req.value.as_slice().len(),
473             "Our own guardian submitted a value");
474
475        dbtx.insert_entry(
476            &MetaDesiredKey(req.key),
477            &MetaDesiredValue {
478                value: req.value.clone(),
479                salt,
480            },
481        )
482        .await;
483
484        Ok(())
485    }
486
487    async fn handle_get_consensus_request(
488        &self,
489        dbtx: &mut DatabaseTransaction<'_, NonCommittable>,
490        req: &GetConsensusRequest,
491    ) -> Result<Option<MetaConsensusValue>, ApiError> {
492        Ok(dbtx.get_value(&MetaConsensusKey(req.0)).await)
493    }
494
495    async fn handle_get_consensus_revision_request(
496        &self,
497        dbtx: &mut DatabaseTransaction<'_, NonCommittable>,
498        req: &GetConsensusRequest,
499    ) -> Result<Option<u64>, ApiError> {
500        Ok(dbtx
501            .get_value(&MetaConsensusKey(req.0))
502            .await
503            .map(|cv| cv.revision))
504    }
505
506    async fn handle_get_submissions_request(
507        &self,
508        dbtx: &mut DatabaseTransaction<'_, NonCommittable>,
509        req: &GetSubmissionsRequest,
510    ) -> Result<BTreeMap<PeerId, MetaValue>, ApiError> {
511        Ok(dbtx
512            .find_by_prefix(&MetaSubmissionsByKeyPrefix(req.0))
513            .await
514            .collect::<Vec<_>>()
515            .await
516            .into_iter()
517            .map(|(k, v)| (k.peer_id, v.value))
518            .collect())
519    }
520}
521
522// UI Methods for Meta Module
523
524impl Meta {
525    /// Submits a value after the server UI's `UserAuth` boundary authenticates
526    /// the caller.
527    pub async fn handle_submit_request_ui(&self, value: Value) -> Result<(), ApiError> {
528        let mut dbtx = self.db.begin_transaction().await;
529
530        self.handle_submit_request(
531            &mut dbtx.to_ref_nc(),
532            &SubmitRequest {
533                key: DEFAULT_META_KEY,
534                value: MetaValue::from(serde_json::to_vec(&value).unwrap().as_slice()),
535            },
536        )
537        .await?;
538
539        dbtx.commit_tx_result()
540            .await
541            .map_err(|e| ApiError::server_error(e.to_string()))?;
542
543        Ok(())
544    }
545
546    /// UI helper to get consensus data as a key-value map
547    pub async fn handle_get_consensus_request_ui(&self) -> Result<Option<Value>, ApiError> {
548        self.handle_get_consensus_request(
549            &mut self.db.begin_transaction_nc().await,
550            &GetConsensusRequest(DEFAULT_META_KEY),
551        )
552        .await?
553        .map(|value| serde_json::from_slice(value.value.as_slice()))
554        .transpose()
555        .map_err(|e| ApiError::server_error(e.to_string()))
556    }
557
558    /// UI helper to get consensus revision
559    pub async fn handle_get_consensus_revision_request_ui(&self) -> Result<u64, ApiError> {
560        self.handle_get_consensus_revision_request(
561            &mut self.db.begin_transaction_nc().await,
562            &GetConsensusRequest(DEFAULT_META_KEY),
563        )
564        .await
565        .map(|r| r.unwrap_or(0))
566    }
567
568    /// Gets submissions after the server UI's `UserAuth` boundary authenticates
569    /// the caller.
570    pub async fn handle_get_submissions_request_ui(
571        &self,
572    ) -> Result<BTreeMap<PeerId, Value>, ApiError> {
573        let mut submissions = BTreeMap::new();
574
575        let mut dbtx = self.db.begin_transaction_nc().await;
576
577        for (peer_id, value) in self
578            .handle_get_submissions_request(
579                &mut dbtx.to_ref_nc(),
580                &GetSubmissionsRequest(DEFAULT_META_KEY),
581            )
582            .await?
583        {
584            if let Ok(value) = serde_json::from_slice(value.as_slice()) {
585                submissions.insert(peer_id, value);
586            }
587        }
588
589        Ok(submissions)
590    }
591}