fedimint_dummy_server/
lib.rs

1#![deny(clippy::pedantic)]
2#![allow(clippy::cast_possible_wrap)]
3#![allow(clippy::missing_errors_doc)]
4#![allow(clippy::module_name_repetitions)]
5#![allow(clippy::must_use_candidate)]
6
7use std::collections::BTreeMap;
8
9use anyhow::bail;
10use async_trait::async_trait;
11use fedimint_core::config::{
12    ConfigGenModuleParams, DkgResult, ServerModuleConfig, ServerModuleConsensusConfig,
13    TypedServerModuleConfig, TypedServerModuleConsensusConfig,
14};
15use fedimint_core::core::ModuleInstanceId;
16use fedimint_core::db::{
17    CoreMigrationFn, DatabaseTransaction, DatabaseVersion, IDatabaseTransactionOpsCoreTyped,
18};
19use fedimint_core::module::audit::Audit;
20use fedimint_core::module::{
21    ApiEndpoint, CoreConsensusVersion, InputMeta, ModuleConsensusVersion, ModuleInit, PeerHandle,
22    ServerModuleInit, ServerModuleInitArgs, SupportedModuleApiVersions, TransactionItemAmount,
23    CORE_CONSENSUS_VERSION,
24};
25use fedimint_core::server::DynServerModule;
26use fedimint_core::{push_db_pair_items, Amount, OutPoint, PeerId, ServerModule};
27use fedimint_dummy_common::config::{
28    DummyClientConfig, DummyConfig, DummyConfigConsensus, DummyConfigLocal, DummyConfigPrivate,
29    DummyGenParams,
30};
31use fedimint_dummy_common::{
32    broken_fed_public_key, fed_public_key, DummyCommonInit, DummyConsensusItem, DummyInput,
33    DummyInputError, DummyModuleTypes, DummyOutput, DummyOutputError, DummyOutputOutcome,
34    MODULE_CONSENSUS_VERSION,
35};
36use futures::{FutureExt, StreamExt};
37use strum::IntoEnumIterator;
38
39use crate::db::{
40    migrate_to_v1, DbKeyPrefix, DummyFundsKeyV1, DummyFundsPrefixV1, DummyOutcomeKey,
41    DummyOutcomePrefix,
42};
43
44pub mod db;
45
46/// Generates the module
47#[derive(Debug, Clone)]
48pub struct DummyInit;
49
50// TODO: Boilerplate-code
51impl ModuleInit for DummyInit {
52    type Common = DummyCommonInit;
53
54    /// Dumps all database items for debugging
55    async fn dump_database(
56        &self,
57        dbtx: &mut DatabaseTransaction<'_>,
58        prefix_names: Vec<String>,
59    ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
60        // TODO: Boilerplate-code
61        let mut items: BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> = BTreeMap::new();
62        let filtered_prefixes = DbKeyPrefix::iter().filter(|f| {
63            prefix_names.is_empty() || prefix_names.contains(&f.to_string().to_lowercase())
64        });
65
66        for table in filtered_prefixes {
67            match table {
68                DbKeyPrefix::Funds => {
69                    push_db_pair_items!(
70                        dbtx,
71                        DummyFundsPrefixV1,
72                        DummyFundsKeyV1,
73                        Amount,
74                        items,
75                        "Dummy Funds"
76                    );
77                }
78                DbKeyPrefix::Outcome => {
79                    push_db_pair_items!(
80                        dbtx,
81                        DummyOutcomePrefix,
82                        DummyOutcomeKey,
83                        DummyOutputOutcome,
84                        items,
85                        "Dummy Outputs"
86                    );
87                }
88            }
89        }
90
91        Box::new(items.into_iter())
92    }
93}
94
95/// Implementation of server module non-consensus functions
96#[async_trait]
97impl ServerModuleInit for DummyInit {
98    type Params = DummyGenParams;
99
100    /// Returns the version of this module
101    fn versions(&self, _core: CoreConsensusVersion) -> &[ModuleConsensusVersion] {
102        &[MODULE_CONSENSUS_VERSION]
103    }
104
105    fn supported_api_versions(&self) -> SupportedModuleApiVersions {
106        SupportedModuleApiVersions::from_raw(
107            (CORE_CONSENSUS_VERSION.major, CORE_CONSENSUS_VERSION.minor),
108            (
109                MODULE_CONSENSUS_VERSION.major,
110                MODULE_CONSENSUS_VERSION.minor,
111            ),
112            &[(0, 0)],
113        )
114    }
115
116    /// Initialize the module
117    async fn init(&self, args: &ServerModuleInitArgs<Self>) -> anyhow::Result<DynServerModule> {
118        Ok(Dummy::new(args.cfg().to_typed()?).into())
119    }
120
121    /// Generates configs for all peers in a trusted manner for testing
122    fn trusted_dealer_gen(
123        &self,
124        peers: &[PeerId],
125        params: &ConfigGenModuleParams,
126    ) -> BTreeMap<PeerId, ServerModuleConfig> {
127        let params = self.parse_params(params).unwrap();
128        // Generate a config for each peer
129        peers
130            .iter()
131            .map(|&peer| {
132                let config = DummyConfig {
133                    local: DummyConfigLocal {},
134                    private: DummyConfigPrivate,
135                    consensus: DummyConfigConsensus {
136                        tx_fee: params.consensus.tx_fee,
137                    },
138                };
139                (peer, config.to_erased())
140            })
141            .collect()
142    }
143
144    /// Generates configs for all peers in an untrusted manner
145    async fn distributed_gen(
146        &self,
147        _peers: &PeerHandle,
148        params: &ConfigGenModuleParams,
149    ) -> DkgResult<ServerModuleConfig> {
150        let params = self.parse_params(params).unwrap();
151
152        Ok(DummyConfig {
153            local: DummyConfigLocal {},
154            private: DummyConfigPrivate,
155            consensus: DummyConfigConsensus {
156                tx_fee: params.consensus.tx_fee,
157            },
158        }
159        .to_erased())
160    }
161
162    /// Converts the consensus config into the client config
163    fn get_client_config(
164        &self,
165        config: &ServerModuleConsensusConfig,
166    ) -> anyhow::Result<DummyClientConfig> {
167        let config = DummyConfigConsensus::from_erased(config)?;
168        Ok(DummyClientConfig {
169            tx_fee: config.tx_fee,
170        })
171    }
172
173    fn validate_config(
174        &self,
175        _identity: &PeerId,
176        _config: ServerModuleConfig,
177    ) -> anyhow::Result<()> {
178        Ok(())
179    }
180
181    /// DB migrations to move from old to newer versions
182    fn get_database_migrations(&self) -> BTreeMap<DatabaseVersion, CoreMigrationFn> {
183        let mut migrations: BTreeMap<DatabaseVersion, CoreMigrationFn> = BTreeMap::new();
184        migrations.insert(DatabaseVersion(0), |ctx| migrate_to_v1(ctx).boxed());
185        migrations
186    }
187}
188
189/// Dummy module
190#[derive(Debug)]
191pub struct Dummy {
192    pub cfg: DummyConfig,
193}
194
195/// Implementation of consensus for the server module
196#[async_trait]
197impl ServerModule for Dummy {
198    /// Define the consensus types
199    type Common = DummyModuleTypes;
200    type Init = DummyInit;
201
202    async fn consensus_proposal(
203        &self,
204        _dbtx: &mut DatabaseTransaction<'_>,
205    ) -> Vec<DummyConsensusItem> {
206        Vec::new()
207    }
208
209    async fn process_consensus_item<'a, 'b>(
210        &'a self,
211        _dbtx: &mut DatabaseTransaction<'b>,
212        _consensus_item: DummyConsensusItem,
213        _peer_id: PeerId,
214    ) -> anyhow::Result<()> {
215        // WARNING: `process_consensus_item` should return an `Err` for items that do
216        // not change any internal consensus state. Failure to do so, will result in an
217        // (potentially significantly) increased consensus history size.
218        // If you are using this code as a template,
219        // make sure to read the [`ServerModule::process_consensus_item`] documentation,
220        bail!("The dummy module does not use consensus items");
221    }
222
223    async fn process_input<'a, 'b, 'c>(
224        &'a self,
225        dbtx: &mut DatabaseTransaction<'c>,
226        input: &'b DummyInput,
227    ) -> Result<InputMeta, DummyInputError> {
228        let current_funds = dbtx
229            .get_value(&DummyFundsKeyV1(input.account))
230            .await
231            .unwrap_or(Amount::ZERO);
232
233        // verify user has enough funds or is using the fed account
234        if input.amount > current_funds
235            && fed_public_key() != input.account
236            && broken_fed_public_key() != input.account
237        {
238            return Err(DummyInputError::NotEnoughFunds);
239        }
240
241        // Subtract funds from normal user, or print funds for the fed
242        let updated_funds = if fed_public_key() == input.account {
243            current_funds + input.amount
244        } else if broken_fed_public_key() == input.account {
245            // The printer is broken
246            current_funds
247        } else {
248            current_funds - input.amount
249        };
250
251        dbtx.insert_entry(&DummyFundsKeyV1(input.account), &updated_funds)
252            .await;
253
254        Ok(InputMeta {
255            amount: TransactionItemAmount {
256                amount: input.amount,
257                fee: self.cfg.consensus.tx_fee,
258            },
259            // IMPORTANT: include the pubkey to validate the user signed this tx
260            pub_key: input.account,
261        })
262    }
263
264    async fn process_output<'a, 'b>(
265        &'a self,
266        dbtx: &mut DatabaseTransaction<'b>,
267        output: &'a DummyOutput,
268        out_point: OutPoint,
269    ) -> Result<TransactionItemAmount, DummyOutputError> {
270        // Add output funds to the user's account
271        let current_funds = dbtx.get_value(&DummyFundsKeyV1(output.account)).await;
272        let updated_funds = current_funds.unwrap_or(Amount::ZERO) + output.amount;
273        dbtx.insert_entry(&DummyFundsKeyV1(output.account), &updated_funds)
274            .await;
275
276        // Update the output outcome the user can query
277        let outcome = DummyOutputOutcome(updated_funds, output.account);
278        dbtx.insert_entry(&DummyOutcomeKey(out_point), &outcome)
279            .await;
280
281        Ok(TransactionItemAmount {
282            amount: output.amount,
283            fee: self.cfg.consensus.tx_fee,
284        })
285    }
286
287    async fn output_status(
288        &self,
289        dbtx: &mut DatabaseTransaction<'_>,
290        out_point: OutPoint,
291    ) -> Option<DummyOutputOutcome> {
292        // check whether or not the output has been processed
293        dbtx.get_value(&DummyOutcomeKey(out_point)).await
294    }
295
296    async fn audit(
297        &self,
298        dbtx: &mut DatabaseTransaction<'_>,
299        audit: &mut Audit,
300        module_instance_id: ModuleInstanceId,
301    ) {
302        audit
303            .add_items(
304                dbtx,
305                module_instance_id,
306                &DummyFundsPrefixV1,
307                |k, v| match k {
308                    // the fed's test account is considered an asset (positive)
309                    // should be the bitcoin we own in a real module
310                    DummyFundsKeyV1(key)
311                        if key == fed_public_key() || key == broken_fed_public_key() =>
312                    {
313                        v.msats as i64
314                    }
315                    // a user's funds are a federation's liability (negative)
316                    DummyFundsKeyV1(_) => -(v.msats as i64),
317                },
318            )
319            .await;
320    }
321
322    fn api_endpoints(&self) -> Vec<ApiEndpoint<Self>> {
323        Vec::new()
324    }
325}
326
327impl Dummy {
328    /// Create new module instance
329    pub fn new(cfg: DummyConfig) -> Dummy {
330        Dummy { cfg }
331    }
332}