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};
23pub use fedimint_dummy_common as common;
24use fedimint_dummy_common::config::{
25    DummyClientConfig, DummyConfig, DummyConfigConsensus, DummyConfigPrivate,
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::{
35    ConfigGenModuleArgs, ServerModule, ServerModuleInit, ServerModuleInitArgs,
36};
37use futures::{FutureExt, StreamExt};
38use strum::IntoEnumIterator;
39
40use crate::db::{
41    DbKeyPrefix, DummyFundsKeyV1, DummyFundsPrefixV1, DummyOutcomeKey, DummyOutcomePrefix,
42    migrate_to_v1, migrate_to_v2,
43};
44
45pub mod db;
46
47/// Generates the module
48#[derive(Debug, Clone)]
49pub struct DummyInit;
50
51// TODO: Boilerplate-code
52impl ModuleInit for DummyInit {
53    type Common = DummyCommonInit;
54
55    /// Dumps all database items for debugging
56    async fn dump_database(
57        &self,
58        dbtx: &mut DatabaseTransaction<'_>,
59        prefix_names: Vec<String>,
60    ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
61        // TODO: Boilerplate-code
62        let mut items: BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> = BTreeMap::new();
63        let filtered_prefixes = DbKeyPrefix::iter().filter(|f| {
64            prefix_names.is_empty() || prefix_names.contains(&f.to_string().to_lowercase())
65        });
66
67        for table in filtered_prefixes {
68            match table {
69                DbKeyPrefix::Funds => {
70                    push_db_pair_items!(
71                        dbtx,
72                        DummyFundsPrefixV1,
73                        DummyFundsKeyV1,
74                        Amount,
75                        items,
76                        "Dummy Funds"
77                    );
78                }
79                DbKeyPrefix::Outcome => {
80                    push_db_pair_items!(
81                        dbtx,
82                        DummyOutcomePrefix,
83                        DummyOutcomeKey,
84                        DummyOutputOutcome,
85                        items,
86                        "Dummy Outputs"
87                    );
88                }
89            }
90        }
91
92        Box::new(items.into_iter())
93    }
94}
95
96/// Implementation of server module non-consensus functions
97#[async_trait]
98impl ServerModuleInit for DummyInit {
99    type Module = Dummy;
100
101    /// Returns the version of this module
102    fn versions(&self, _core: CoreConsensusVersion) -> &[ModuleConsensusVersion] {
103        &[MODULE_CONSENSUS_VERSION]
104    }
105
106    fn supported_api_versions(&self) -> SupportedModuleApiVersions {
107        SupportedModuleApiVersions::from_raw(
108            (CORE_CONSENSUS_VERSION.major, CORE_CONSENSUS_VERSION.minor),
109            (
110                MODULE_CONSENSUS_VERSION.major,
111                MODULE_CONSENSUS_VERSION.minor,
112            ),
113            &[(0, 0)],
114        )
115    }
116
117    /// Initialize the module
118    async fn init(&self, args: &ServerModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
119        Ok(Dummy::new(args.cfg().to_typed()?))
120    }
121
122    /// Generates configs for all peers in a trusted manner for testing
123    fn trusted_dealer_gen(
124        &self,
125        peers: &[PeerId],
126        _args: &ConfigGenModuleArgs,
127    ) -> BTreeMap<PeerId, ServerModuleConfig> {
128        // Generate a config for each peer
129        peers
130            .iter()
131            .map(|&peer| {
132                let config = DummyConfig {
133                    private: DummyConfigPrivate,
134                    consensus: DummyConfigConsensus {
135                        tx_fee: Amount::ZERO,
136                    },
137                };
138                (peer, config.to_erased())
139            })
140            .collect()
141    }
142
143    /// Generates configs for all peers in an untrusted manner
144    async fn distributed_gen(
145        &self,
146        _peers: &(dyn PeerHandleOps + Send + Sync),
147        _args: &ConfigGenModuleArgs,
148    ) -> anyhow::Result<ServerModuleConfig> {
149        Ok(DummyConfig {
150            private: DummyConfigPrivate,
151            consensus: DummyConfigConsensus {
152                tx_fee: Amount::ZERO,
153            },
154        }
155        .to_erased())
156    }
157
158    /// Converts the consensus config into the client config
159    fn get_client_config(
160        &self,
161        config: &ServerModuleConsensusConfig,
162    ) -> anyhow::Result<DummyClientConfig> {
163        let config = DummyConfigConsensus::from_erased(config)?;
164        Ok(DummyClientConfig {
165            tx_fee: config.tx_fee,
166        })
167    }
168
169    fn validate_config(
170        &self,
171        _identity: &PeerId,
172        _config: ServerModuleConfig,
173    ) -> anyhow::Result<()> {
174        Ok(())
175    }
176
177    /// DB migrations to move from old to newer versions
178    fn get_database_migrations(
179        &self,
180    ) -> BTreeMap<DatabaseVersion, ServerModuleDbMigrationFn<Dummy>> {
181        let mut migrations: BTreeMap<DatabaseVersion, ServerModuleDbMigrationFn<Dummy>> =
182            BTreeMap::new();
183        migrations.insert(
184            DatabaseVersion(0),
185            Box::new(|ctx| migrate_to_v1(ctx).boxed()),
186        );
187        migrations.insert(
188            DatabaseVersion(1),
189            Box::new(|ctx| migrate_to_v2(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 DummyInput::V0(input) = input else {
236            return Err(DummyInputError::InvalidVersion);
237        };
238        let current_funds = dbtx
239            .get_value(&DummyFundsKeyV1(input.account))
240            .await
241            .unwrap_or(Amount::ZERO);
242
243        // verify user has enough funds or is using the fed account
244        if input.amount > current_funds
245            && fed_public_key() != input.account
246            && broken_fed_public_key() != input.account
247        {
248            return Err(DummyInputError::NotEnoughFunds);
249        }
250
251        // Subtract funds from normal user, or print funds for the fed
252        let updated_funds = if fed_public_key() == input.account {
253            current_funds + input.amount
254        } else if broken_fed_public_key() == input.account {
255            // The printer is broken
256            current_funds
257        } else {
258            current_funds.saturating_sub(input.amount)
259        };
260
261        dbtx.insert_entry(&DummyFundsKeyV1(input.account), &updated_funds)
262            .await;
263
264        Ok(InputMeta {
265            amount: TransactionItemAmounts {
266                amounts: Amounts::new_bitcoin(input.amount),
267                fees: Amounts::new_bitcoin(self.cfg.consensus.tx_fee),
268            },
269            // IMPORTANT: include the pubkey to validate the user signed this tx
270            pub_key: input.account,
271        })
272    }
273
274    async fn process_output<'a, 'b>(
275        &'a self,
276        dbtx: &mut DatabaseTransaction<'b>,
277        output: &'a DummyOutput,
278        out_point: OutPoint,
279    ) -> Result<TransactionItemAmounts, DummyOutputError> {
280        let DummyOutput::V0(output) = output else {
281            return Err(DummyOutputError::InvalidVersion);
282        };
283        // Add output funds to the user's account
284        let current_funds = dbtx.get_value(&DummyFundsKeyV1(output.account)).await;
285        let updated_funds = current_funds.unwrap_or(Amount::ZERO) + output.amount;
286        dbtx.insert_entry(&DummyFundsKeyV1(output.account), &updated_funds)
287            .await;
288
289        // Update the output outcome the user can query
290        let outcome = DummyOutputOutcome(updated_funds, output.unit, output.account);
291        dbtx.insert_entry(&DummyOutcomeKey(out_point), &outcome)
292            .await;
293
294        Ok(TransactionItemAmounts {
295            amounts: Amounts::new_bitcoin(output.amount),
296            fees: Amounts::new_bitcoin(self.cfg.consensus.tx_fee),
297        })
298    }
299
300    async fn output_status(
301        &self,
302        dbtx: &mut DatabaseTransaction<'_>,
303        out_point: OutPoint,
304    ) -> Option<DummyOutputOutcome> {
305        // check whether or not the output has been processed
306        dbtx.get_value(&DummyOutcomeKey(out_point)).await
307    }
308
309    async fn audit(
310        &self,
311        dbtx: &mut DatabaseTransaction<'_>,
312        audit: &mut Audit,
313        module_instance_id: ModuleInstanceId,
314    ) {
315        audit
316            .add_items(
317                dbtx,
318                module_instance_id,
319                &DummyFundsPrefixV1,
320                |k, v| match k {
321                    // the fed's test account is considered an asset (positive)
322                    // should be the bitcoin we own in a real module
323                    DummyFundsKeyV1(key)
324                        if key == fed_public_key() || key == broken_fed_public_key() =>
325                    {
326                        v.msats as i64
327                    }
328                    // a user's funds are a federation's liability (negative)
329                    DummyFundsKeyV1(_) => -(v.msats as i64),
330                },
331            )
332            .await;
333    }
334
335    fn api_endpoints(&self) -> Vec<ApiEndpoint<Self>> {
336        Vec::new()
337    }
338}
339
340impl Dummy {
341    /// Create new module instance
342    pub fn new(cfg: DummyConfig) -> Dummy {
343        Dummy { cfg }
344    }
345}