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