Skip to main content

fedimint_mint_common/
config.rs

1use std::collections::BTreeMap;
2
3use fedimint_core::config::ExcessiveRelativeFeeError;
4use fedimint_core::core::ModuleKind;
5use fedimint_core::encoding::{Decodable, Encodable};
6use fedimint_core::module::serde_json;
7use fedimint_core::{Amount, PeerId, Tiered, plugin_types_trait_impl_config};
8use serde::{Deserialize, Serialize};
9use tbs::{AggregatePublicKey, PublicKeyShare};
10
11use crate::MintCommonInit;
12
13#[derive(Clone, Debug, Serialize, Deserialize)]
14pub struct MintConfig {
15    pub private: MintConfigPrivate,
16    pub consensus: MintConfigConsensus,
17}
18
19#[derive(Clone, Debug, Serialize, Deserialize, Decodable, Encodable)]
20pub struct MintConfigLocal;
21
22#[derive(Clone, Debug, Serialize, Deserialize, Encodable, Decodable)]
23pub struct MintConfigConsensus {
24    /// The set of public keys for blind-signing all peers and note
25    /// denominations
26    pub peer_tbs_pks: BTreeMap<PeerId, Tiered<PublicKeyShare>>,
27    /// Fees charged for ecash transactions
28    pub fee_consensus: FeeConsensus,
29    /// The maximum amount of change a client can request
30    pub max_notes_per_denomination: u16,
31}
32
33#[derive(Clone, Debug, Serialize, Deserialize)]
34pub struct MintConfigPrivate {
35    /// Secret keys for blind-signing ecash of varying note denominations
36    pub tbs_sks: Tiered<tbs::SecretKeyShare>,
37}
38
39#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Encodable, Decodable, Hash)]
40pub struct MintClientConfig {
41    pub tbs_pks: Tiered<AggregatePublicKey>,
42    pub fee_consensus: FeeConsensus,
43    pub peer_tbs_pks: BTreeMap<PeerId, Tiered<tbs::PublicKeyShare>>,
44    pub max_notes_per_denomination: u16,
45}
46
47impl std::fmt::Display for MintClientConfig {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        write!(
50            f,
51            "MintClientConfig {}",
52            serde_json::to_string(self).map_err(|_e| std::fmt::Error)?
53        )
54    }
55}
56
57// Wire together the configs for this module
58plugin_types_trait_impl_config!(
59    MintCommonInit,
60    MintConfig,
61    MintConfigPrivate,
62    MintConfigConsensus,
63    MintClientConfig
64);
65
66#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, Encodable, Decodable)]
67pub struct FeeConsensus {
68    base: Amount,
69    parts_per_million: u64,
70}
71
72impl FeeConsensus {
73    /// The mint module will charge a non-configurable base fee of one hundred
74    /// millisatoshis per transaction input and output to account for the costs
75    /// incurred by the federation for processing the transaction. On top of
76    /// that the federation may charge a additional relative fee per input and
77    /// output of up to one thousand parts per million which is equal to one
78    /// tenth of one percent.
79    ///
80    /// # Errors
81    /// - Returns [`ExcessiveRelativeFeeError`] if the relative fee is in excess
82    ///   of one thousand parts per million.
83    pub fn new(parts_per_million: u64) -> Result<Self, ExcessiveRelativeFeeError> {
84        const MAX_PARTS_PER_MILLION: u64 = 1_000;
85
86        if parts_per_million > MAX_PARTS_PER_MILLION {
87            return Err(ExcessiveRelativeFeeError {
88                parts_per_million,
89                max: MAX_PARTS_PER_MILLION,
90            });
91        }
92
93        Ok(Self {
94            base: Amount::from_msats(100),
95            parts_per_million,
96        })
97    }
98
99    pub fn zero() -> Self {
100        Self {
101            base: Amount::ZERO,
102            parts_per_million: 0,
103        }
104    }
105
106    pub fn fee(&self, amount: Amount) -> Amount {
107        Amount::from_msats(self.fee_msats(amount.msats))
108    }
109
110    /// Returns the smallest denomination where the note's value is at least
111    /// 4x the base fee, rounded up to the next power of two msats. Notes
112    /// below this denomination are not economical since fees consume a
113    /// significant fraction of their value.
114    pub fn min_economical_denomination(&self) -> Amount {
115        Amount::from_msats(self.base.msats.saturating_mul(4).next_power_of_two())
116    }
117
118    /// Rounds an amount up to the nearest multiple of the smallest economical
119    /// denomination.
120    pub fn round_up(&self, amount: Amount) -> Amount {
121        let msats = amount
122            .msats
123            .next_multiple_of(self.base.msats.saturating_mul(4).next_power_of_two());
124
125        Amount::from_msats(msats)
126    }
127
128    fn fee_msats(&self, msats: u64) -> u64 {
129        msats
130            .saturating_mul(self.parts_per_million)
131            .saturating_div(1_000_000)
132            .checked_add(self.base.msats)
133            .expect("The division creates sufficient headroom to add the base fee")
134    }
135}
136
137#[test]
138fn test_fee_consensus() {
139    let fee_consensus = FeeConsensus::new(1_000).expect("Relative fee is within range");
140
141    assert_eq!(
142        fee_consensus.fee(Amount::from_msats(999)),
143        Amount::from_msats(100)
144    );
145
146    assert_eq!(
147        fee_consensus.fee(Amount::from_sats(1)),
148        Amount::from_msats(100) + Amount::from_msats(1)
149    );
150
151    assert_eq!(
152        fee_consensus.fee(Amount::from_sats(1000)),
153        Amount::from_sats(1) + Amount::from_msats(100)
154    );
155
156    assert_eq!(
157        fee_consensus.fee(Amount::from_bitcoins(1)),
158        Amount::from_sats(100_000) + Amount::from_msats(100)
159    );
160
161    assert_eq!(
162        fee_consensus.fee(Amount::from_bitcoins(100_000)),
163        Amount::from_bitcoins(100) + Amount::from_msats(100)
164    );
165}
166
167#[test]
168fn a_relative_fee_over_the_limit_is_rejected() {
169    let err: ExcessiveRelativeFeeError =
170        FeeConsensus::new(1_001).expect_err("A fee over one per mille is excessive");
171
172    assert_eq!(err.parts_per_million, 1_001);
173}