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    ServerModuleConfig, ServerModuleConsensusConfig, TypedServerModuleConfig,
13    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    Amounts, ApiEndpoint, CORE_CONSENSUS_VERSION, CoreConsensusVersion, InputMeta,
20    ModuleConsensusVersion, ModuleInit, SupportedModuleApiVersions, TransactionItemAmounts,
21};
22use fedimint_core::{Amount, InPoint, OutPoint, PeerId, push_db_pair_items};
23use fedimint_dummy_common::config::{
24    DummyClientConfig, DummyConfig, DummyConfigConsensus, DummyConfigPrivate,
25};
26use fedimint_dummy_common::{
27    DummyCommonInit, DummyConsensusItem, DummyInput, DummyInputError, DummyModuleTypes,
28    DummyOutput, DummyOutputError, DummyOutputOutcome, MODULE_CONSENSUS_VERSION,
29    broken_fed_public_key, fed_public_key,
30};
31use fedimint_server_core::config::PeerHandleOps;
32use fedimint_server_core::migration::ServerModuleDbMigrationFn;
33use fedimint_server_core::{
34    ConfigGenModuleArgs, ServerModule, ServerModuleInit, ServerModuleInitArgs,
35};
36use futures::{FutureExt, StreamExt};
37use strum::IntoEnumIterator;
38
39use crate::db::{
40    DbKeyPrefix, DummyFundsKeyV1, DummyFundsPrefixV1, DummyOutcomeKey, DummyOutcomePrefix,
41    migrate_to_v1, migrate_to_v2,
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 Module = Dummy;
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        _args: &ConfigGenModuleArgs,
126    ) -> BTreeMap<PeerId, ServerModuleConfig> {
127        // Generate a config for each peer
128        peers
129            .iter()
130            .map(|&peer| {
131                let config = DummyConfig {
132                    private: DummyConfigPrivate,
133                    consensus: DummyConfigConsensus {
134                        tx_fee: Amount::ZERO,
135                    },
136                };
137                (peer, config.to_erased())
138            })
139            .collect()
140    }
141
142    /// Generates configs for all peers in an untrusted manner
143    async fn distributed_gen(
144        &self,
145        _peers: &(dyn PeerHandleOps + Send + Sync),
146        _args: &ConfigGenModuleArgs,
147    ) -> anyhow::Result<ServerModuleConfig> {
148        Ok(DummyConfig {
149            private: DummyConfigPrivate,
150            consensus: DummyConfigConsensus {
151                tx_fee: Amount::ZERO,
152            },
153        }
154        .to_erased())
155    }
156
157    /// Converts the consensus config into the client config
158    fn get_client_config(
159        &self,
160        config: &ServerModuleConsensusConfig,
161    ) -> anyhow::Result<DummyClientConfig> {
162        let config = DummyConfigConsensus::from_erased(config)?;
163        Ok(DummyClientConfig {
164            tx_fee: config.tx_fee,
165        })
166    }
167
168    fn validate_config(
169        &self,
170        _identity: &PeerId,
171        _config: ServerModuleConfig,
172    ) -> anyhow::Result<()> {
173        Ok(())
174    }
175
176    /// DB migrations to move from old to newer versions
177    fn get_database_migrations(
178        &self,
179    ) -> BTreeMap<DatabaseVersion, ServerModuleDbMigrationFn<Dummy>> {
180        let mut migrations: BTreeMap<DatabaseVersion, ServerModuleDbMigrationFn<Dummy>> =
181            BTreeMap::new();
182        migrations.insert(
183            DatabaseVersion(0),
184            Box::new(|ctx| migrate_to_v1(ctx).boxed()),
185        );
186        migrations.insert(
187            DatabaseVersion(1),
188            Box::new(|ctx| migrate_to_v2(ctx).boxed()),
189        );
190        migrations
191    }
192}
193
194/// Dummy module
195#[derive(Debug)]
196pub struct Dummy {
197    pub cfg: DummyConfig,
198}
199
200/// Implementation of consensus for the server module
201#[async_trait]
202impl ServerModule for Dummy {
203    /// Define the consensus types
204    type Common = DummyModuleTypes;
205    type Init = DummyInit;
206
207    async fn consensus_proposal(
208        &self,
209        _dbtx: &mut DatabaseTransaction<'_>,
210    ) -> Vec<DummyConsensusItem> {
211        Vec::new()
212    }
213
214    async fn process_consensus_item<'a, 'b>(
215        &'a self,
216        _dbtx: &mut DatabaseTransaction<'b>,
217        _consensus_item: DummyConsensusItem,
218        _peer_id: PeerId,
219    ) -> anyhow::Result<()> {
220        // WARNING: `process_consensus_item` should return an `Err` for items that do
221        // not change any internal consensus state. Failure to do so, will result in an
222        // (potentially significantly) increased consensus history size.
223        // If you are using this code as a template,
224        // make sure to read the [`ServerModule::process_consensus_item`] documentation,
225        bail!("The dummy module does not use consensus items");
226    }
227
228    async fn process_input<'a, 'b, 'c>(
229        &'a self,
230        dbtx: &mut DatabaseTransaction<'c>,
231        input: &'b DummyInput,
232        _in_point: InPoint,
233    ) -> Result<InputMeta, DummyInputError> {
234        let DummyInput::V0(input) = input else {
235            return Err(DummyInputError::InvalidVersion);
236        };
237        let current_funds = dbtx
238            .get_value(&DummyFundsKeyV1(input.account))
239            .await
240            .unwrap_or(Amount::ZERO);
241
242        // verify user has enough funds or is using the fed account
243        if input.amount > current_funds
244            && fed_public_key() != input.account
245            && broken_fed_public_key() != input.account
246        {
247            return Err(DummyInputError::NotEnoughFunds);
248        }
249
250        // Subtract funds from normal user, or print funds for the fed
251        let updated_funds = if fed_public_key() == input.account {
252            current_funds + input.amount
253        } else if broken_fed_public_key() == input.account {
254            // The printer is broken
255            current_funds
256        } else {
257            current_funds.saturating_sub(input.amount)
258        };
259
260        dbtx.insert_entry(&DummyFundsKeyV1(input.account), &updated_funds)
261            .await;
262
263        Ok(InputMeta {
264            amount: TransactionItemAmounts {
265                amounts: Amounts::new_bitcoin(input.amount),
266                fees: Amounts::new_bitcoin(self.cfg.consensus.tx_fee),
267            },
268            // IMPORTANT: include the pubkey to validate the user signed this tx
269            pub_key: input.account,
270        })
271    }
272
273    async fn process_output<'a, 'b>(
274        &'a self,
275        dbtx: &mut DatabaseTransaction<'b>,
276        output: &'a DummyOutput,
277        out_point: OutPoint,
278    ) -> Result<TransactionItemAmounts, DummyOutputError> {
279        let DummyOutput::V0(output) = output else {
280            return Err(DummyOutputError::InvalidVersion);
281        };
282        // Add output funds to the user's account
283        let current_funds = dbtx.get_value(&DummyFundsKeyV1(output.account)).await;
284        let updated_funds = current_funds.unwrap_or(Amount::ZERO) + output.amount;
285        dbtx.insert_entry(&DummyFundsKeyV1(output.account), &updated_funds)
286            .await;
287
288        // Update the output outcome the user can query
289        let outcome = DummyOutputOutcome(updated_funds, output.unit, output.account);
290        dbtx.insert_entry(&DummyOutcomeKey(out_point), &outcome)
291            .await;
292
293        Ok(TransactionItemAmounts {
294            amounts: Amounts::new_bitcoin(output.amount),
295            fees: Amounts::new_bitcoin(self.cfg.consensus.tx_fee),
296        })
297    }
298
299    async fn output_status(
300        &self,
301        dbtx: &mut DatabaseTransaction<'_>,
302        out_point: OutPoint,
303    ) -> Option<DummyOutputOutcome> {
304        // check whether or not the output has been processed
305        dbtx.get_value(&DummyOutcomeKey(out_point)).await
306    }
307
308    async fn audit(
309        &self,
310        dbtx: &mut DatabaseTransaction<'_>,
311        audit: &mut Audit,
312        module_instance_id: ModuleInstanceId,
313    ) {
314        audit
315            .add_items(
316                dbtx,
317                module_instance_id,
318                &DummyFundsPrefixV1,
319                |k, v| match k {
320                    // the fed's test account is considered an asset (positive)
321                    // should be the bitcoin we own in a real module
322                    DummyFundsKeyV1(key)
323                        if key == fed_public_key() || key == broken_fed_public_key() =>
324                    {
325                        v.msats as i64
326                    }
327                    // a user's funds are a federation's liability (negative)
328                    DummyFundsKeyV1(_) => -(v.msats as i64),
329                },
330            )
331            .await;
332    }
333
334    fn api_endpoints(&self) -> Vec<ApiEndpoint<Self>> {
335        Vec::new()
336    }
337}
338
339impl Dummy {
340    /// Create new module instance
341    pub fn new(cfg: DummyConfig) -> Dummy {
342        Dummy { cfg }
343    }
344}