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, ServerModuleConfig, ServerModuleConsensusConfig,
13    TypedServerModuleConfig, TypedServerModuleConsensusConfig,
14};
15use fedimint_core::core::ModuleInstanceId;
16use fedimint_core::db::{DatabaseTransaction, DatabaseVersion, IDatabaseTransactionOpsCoreTyped};
17use fedimint_core::module::audit::Audit;
18use fedimint_core::module::{
19    ApiEndpoint, CORE_CONSENSUS_VERSION, CoreConsensusVersion, InputMeta, ModuleConsensusVersion,
20    ModuleInit, SupportedModuleApiVersions, TransactionItemAmount,
21};
22use fedimint_core::{Amount, InPoint, OutPoint, PeerId, push_db_pair_items};
23use fedimint_dummy_common::config::{
24    DummyClientConfig, DummyConfig, DummyConfigConsensus, DummyConfigLocal, DummyConfigPrivate,
25    DummyGenParams,
26};
27use fedimint_dummy_common::{
28    DummyCommonInit, DummyConsensusItem, DummyInput, DummyInputError, DummyModuleTypes,
29    DummyOutput, DummyOutputError, DummyOutputOutcome, MODULE_CONSENSUS_VERSION,
30    broken_fed_public_key, fed_public_key,
31};
32use fedimint_server_core::config::PeerHandleOps;
33use fedimint_server_core::migration::ServerModuleDbMigrationFn;
34use fedimint_server_core::{ServerModule, ServerModuleInit, ServerModuleInitArgs};
35use futures::{FutureExt, StreamExt};
36use strum::IntoEnumIterator;
37
38use crate::db::{
39    DbKeyPrefix, DummyFundsKeyV1, DummyFundsPrefixV1, DummyOutcomeKey, DummyOutcomePrefix,
40    migrate_to_v1,
41};
42
43pub mod db;
44
45/// Generates the module
46#[derive(Debug, Clone)]
47pub struct DummyInit;
48
49// TODO: Boilerplate-code
50impl ModuleInit for DummyInit {
51    type Common = DummyCommonInit;
52
53    /// Dumps all database items for debugging
54    async fn dump_database(
55        &self,
56        dbtx: &mut DatabaseTransaction<'_>,
57        prefix_names: Vec<String>,
58    ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
59        // TODO: Boilerplate-code
60        let mut items: BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> = BTreeMap::new();
61        let filtered_prefixes = DbKeyPrefix::iter().filter(|f| {
62            prefix_names.is_empty() || prefix_names.contains(&f.to_string().to_lowercase())
63        });
64
65        for table in filtered_prefixes {
66            match table {
67                DbKeyPrefix::Funds => {
68                    push_db_pair_items!(
69                        dbtx,
70                        DummyFundsPrefixV1,
71                        DummyFundsKeyV1,
72                        Amount,
73                        items,
74                        "Dummy Funds"
75                    );
76                }
77                DbKeyPrefix::Outcome => {
78                    push_db_pair_items!(
79                        dbtx,
80                        DummyOutcomePrefix,
81                        DummyOutcomeKey,
82                        DummyOutputOutcome,
83                        items,
84                        "Dummy Outputs"
85                    );
86                }
87            }
88        }
89
90        Box::new(items.into_iter())
91    }
92}
93
94/// Implementation of server module non-consensus functions
95#[async_trait]
96impl ServerModuleInit for DummyInit {
97    type Module = Dummy;
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<Self::Module> {
118        Ok(Dummy::new(args.cfg().to_typed()?))
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: &(dyn PeerHandleOps + Send + Sync),
148        params: &ConfigGenModuleParams,
149    ) -> anyhow::Result<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(
183        &self,
184    ) -> BTreeMap<DatabaseVersion, ServerModuleDbMigrationFn<Dummy>> {
185        let mut migrations: BTreeMap<DatabaseVersion, ServerModuleDbMigrationFn<Dummy>> =
186            BTreeMap::new();
187        migrations.insert(
188            DatabaseVersion(0),
189            Box::new(|ctx| migrate_to_v1(ctx).boxed()),
190        );
191        migrations
192    }
193}
194
195/// Dummy module
196#[derive(Debug)]
197pub struct Dummy {
198    pub cfg: DummyConfig,
199}
200
201/// Implementation of consensus for the server module
202#[async_trait]
203impl ServerModule for Dummy {
204    /// Define the consensus types
205    type Common = DummyModuleTypes;
206    type Init = DummyInit;
207
208    async fn consensus_proposal(
209        &self,
210        _dbtx: &mut DatabaseTransaction<'_>,
211    ) -> Vec<DummyConsensusItem> {
212        Vec::new()
213    }
214
215    async fn process_consensus_item<'a, 'b>(
216        &'a self,
217        _dbtx: &mut DatabaseTransaction<'b>,
218        _consensus_item: DummyConsensusItem,
219        _peer_id: PeerId,
220    ) -> anyhow::Result<()> {
221        // WARNING: `process_consensus_item` should return an `Err` for items that do
222        // not change any internal consensus state. Failure to do so, will result in an
223        // (potentially significantly) increased consensus history size.
224        // If you are using this code as a template,
225        // make sure to read the [`ServerModule::process_consensus_item`] documentation,
226        bail!("The dummy module does not use consensus items");
227    }
228
229    async fn process_input<'a, 'b, 'c>(
230        &'a self,
231        dbtx: &mut DatabaseTransaction<'c>,
232        input: &'b DummyInput,
233        _in_point: InPoint,
234    ) -> Result<InputMeta, DummyInputError> {
235        let current_funds = dbtx
236            .get_value(&DummyFundsKeyV1(input.account))
237            .await
238            .unwrap_or(Amount::ZERO);
239
240        // verify user has enough funds or is using the fed account
241        if input.amount > current_funds
242            && fed_public_key() != input.account
243            && broken_fed_public_key() != input.account
244        {
245            return Err(DummyInputError::NotEnoughFunds);
246        }
247
248        // Subtract funds from normal user, or print funds for the fed
249        let updated_funds = if fed_public_key() == input.account {
250            current_funds + input.amount
251        } else if broken_fed_public_key() == input.account {
252            // The printer is broken
253            current_funds
254        } else {
255            current_funds.saturating_sub(input.amount)
256        };
257
258        dbtx.insert_entry(&DummyFundsKeyV1(input.account), &updated_funds)
259            .await;
260
261        Ok(InputMeta {
262            amount: TransactionItemAmount {
263                amount: input.amount,
264                fee: self.cfg.consensus.tx_fee,
265            },
266            // IMPORTANT: include the pubkey to validate the user signed this tx
267            pub_key: input.account,
268        })
269    }
270
271    async fn process_output<'a, 'b>(
272        &'a self,
273        dbtx: &mut DatabaseTransaction<'b>,
274        output: &'a DummyOutput,
275        out_point: OutPoint,
276    ) -> Result<TransactionItemAmount, DummyOutputError> {
277        // Add output funds to the user's account
278        let current_funds = dbtx.get_value(&DummyFundsKeyV1(output.account)).await;
279        let updated_funds = current_funds.unwrap_or(Amount::ZERO) + output.amount;
280        dbtx.insert_entry(&DummyFundsKeyV1(output.account), &updated_funds)
281            .await;
282
283        // Update the output outcome the user can query
284        let outcome = DummyOutputOutcome(updated_funds, output.account);
285        dbtx.insert_entry(&DummyOutcomeKey(out_point), &outcome)
286            .await;
287
288        Ok(TransactionItemAmount {
289            amount: output.amount,
290            fee: self.cfg.consensus.tx_fee,
291        })
292    }
293
294    async fn output_status(
295        &self,
296        dbtx: &mut DatabaseTransaction<'_>,
297        out_point: OutPoint,
298    ) -> Option<DummyOutputOutcome> {
299        // check whether or not the output has been processed
300        dbtx.get_value(&DummyOutcomeKey(out_point)).await
301    }
302
303    async fn audit(
304        &self,
305        dbtx: &mut DatabaseTransaction<'_>,
306        audit: &mut Audit,
307        module_instance_id: ModuleInstanceId,
308    ) {
309        audit
310            .add_items(
311                dbtx,
312                module_instance_id,
313                &DummyFundsPrefixV1,
314                |k, v| match k {
315                    // the fed's test account is considered an asset (positive)
316                    // should be the bitcoin we own in a real module
317                    DummyFundsKeyV1(key)
318                        if key == fed_public_key() || key == broken_fed_public_key() =>
319                    {
320                        v.msats as i64
321                    }
322                    // a user's funds are a federation's liability (negative)
323                    DummyFundsKeyV1(_) => -(v.msats as i64),
324                },
325            )
326            .await;
327    }
328
329    fn api_endpoints(&self) -> Vec<ApiEndpoint<Self>> {
330        Vec::new()
331    }
332}
333
334impl Dummy {
335    /// Create new module instance
336    pub fn new(cfg: DummyConfig) -> Dummy {
337        Dummy { cfg }
338    }
339}