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#[derive(Debug, Clone)]
65pub struct MetaInit;
66
67impl ModuleInit for MetaInit {
69 type Common = MetaCommonInit;
70
71 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 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#[async_trait]
124impl ServerModuleInit for MetaInit {
125 type Module = Meta;
126
127 fn versions(&self, _core: CoreConsensusVersion) -> &[ModuleConsensusVersion] {
129 &[MODULE_CONSENSUS_VERSION]
130 }
131
132 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 fn trusted_dealer_gen(
144 &self,
145 peers: &[PeerId],
146 _args: &ConfigGenModuleArgs,
147 ) -> BTreeMap<PeerId, ServerModuleConfig> {
148 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 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 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 fn get_database_migrations(
193 &self,
194 ) -> BTreeMap<DatabaseVersion, ServerModuleDbMigrationFn<Meta>> {
195 BTreeMap::new()
196 }
197}
198
199#[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#[async_trait]
260impl ServerModule for Meta {
261 type Common = MetaModuleTypes;
263 type Init = MetaInit;
264
265 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 } else if consensus_value.as_ref() == Some(&desired_value) {
296 if consensus_submission_value.is_none() {
297 } else {
301 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 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 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 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 dbtx.insert_entry(&MetaSubmissionsKey { key, peer_id }, &new_value)
351 .await;
352
353 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 <= 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
533impl Meta {
536 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 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 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 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}