Skip to main content

fedimint_walletv2_common/
config.rs

1use std::collections::BTreeMap;
2
3use bitcoin::Network;
4use bitcoin::hashes::{Hash, sha256};
5use fedimint_core::config::ExcessiveRelativeFeeError;
6use fedimint_core::core::ModuleKind;
7use fedimint_core::encoding::{Decodable, Encodable};
8use fedimint_core::{Amount, PeerId, plugin_types_trait_impl_config, weight_to_vbytes};
9use secp256k1::{PublicKey, SecretKey};
10use serde::{Deserialize, Serialize};
11
12use crate::{WalletCommonInit, descriptor};
13
14plugin_types_trait_impl_config!(
15    WalletCommonInit,
16    WalletConfig,
17    WalletConfigPrivate,
18    WalletConfigConsensus,
19    WalletClientConfig
20);
21
22#[derive(Clone, Debug, Serialize, Deserialize)]
23pub struct WalletConfig {
24    pub private: WalletConfigPrivate,
25    pub consensus: WalletConfigConsensus,
26}
27
28#[derive(Clone, Debug, Serialize, Deserialize)]
29pub struct WalletConfigPrivate {
30    pub bitcoin_sk: SecretKey,
31}
32
33#[derive(Clone, Debug, Serialize, Deserialize, Encodable, Decodable)]
34pub struct WalletConfigConsensus {
35    /// The public keys for the bitcoin multisig
36    pub bitcoin_pks: BTreeMap<PeerId, PublicKey>,
37    /// The kind of descriptor the federation uses for the multisig.
38    pub descriptor: WalletDescriptor,
39    /// Total vbytes of a pegout bitcoin transaction
40    pub send_tx_vbytes: u64,
41    /// Total vbytes of a pegin bitcoin transaction
42    pub receive_tx_vbytes: u64,
43    /// The minimum feerate doubles for each pending transaction in the stack,
44    /// protecting against catastrophic feerate estimation errors
45    pub feerate_base: u64,
46    /// The minimum amount a user can send on chain
47    pub dust_limit: bitcoin::Amount,
48    /// Fees taken by the guardians to process wallet inputs and outputs
49    pub fee_consensus: FeeConsensus,
50    /// Bitcoin network (e.g. testnet, bitcoin)
51    pub network: Network,
52}
53
54impl WalletConfigConsensus {
55    /// The constructor will derive the following number of vbytes for a send
56    /// and receive transaction with respect to the number of guardians:
57    ///
58    /// | Guardians | Send | Receive |
59    /// |-----------|------|---------|
60    /// | 1         | 166  | 192     |
61    /// | 4         | 228  | 316     |
62    /// | 5         | 255  | 369     |
63    /// | 6         | 281  | 423     |
64    /// | 7         | 290  | 440     |
65    /// | 8         | 317  | 494     |
66    /// | 9         | 344  | 548     |
67    /// | 10        | 352  | 565     |
68    /// | 11        | 379  | 618     |
69    /// | 12        | 406  | 672     |
70    /// | 13        | 414  | 689     |
71    /// | 14        | 441  | 742     |
72    /// | 15        | 468  | 796     |
73    /// | 16        | 476  | 813     |
74    /// | 17        | 503  | 867     |
75    /// | 18        | 530  | 920     |
76    /// | 19        | 539  | 937     |
77    /// | 20        | 565  | 991     |
78    pub fn new(
79        bitcoin_pks: BTreeMap<PeerId, PublicKey>,
80        fee_consensus: FeeConsensus,
81        network: Network,
82    ) -> Self {
83        let tx_overhead_weight = 4 * 4 // nVersion
84            + 1 // SegWit marker
85            + 1 // SegWit flag
86            + 4 // up to 2 inputs
87            + 4 // up to 2 outputs
88            + 4 * 4; // nLockTime
89
90        let change_witness_weight = descriptor(&bitcoin_pks, &sha256::Hash::all_zeros())
91            .max_weight_to_satisfy()
92            .expect("Cannot satisfy the change descriptor.")
93            .to_wu();
94
95        let change_input_weight = 32 * 4 // txid
96            + 4 * 4 // vout
97            + 4 // Script length
98            + 4 * 4 // nSequence
99            + change_witness_weight;
100
101        let change_output_weight = 8 * 4 // nValue
102            + 4 // scriptPubKey length
103            + 34 * 4; // scriptPubKey
104
105        let destination_output_weight = 8 * 4 // nValue
106            + 4 // scriptPubKey length
107            + 34 * 4; // scriptPubKey
108
109        Self {
110            bitcoin_pks,
111            descriptor: WalletDescriptor::Wsh,
112            send_tx_vbytes: weight_to_vbytes(
113                tx_overhead_weight
114                    + change_input_weight
115                    + change_output_weight
116                    + destination_output_weight,
117            ),
118            receive_tx_vbytes: weight_to_vbytes(
119                tx_overhead_weight
120                    + change_input_weight
121                    + change_input_weight
122                    + change_output_weight,
123            ),
124            // This is intentionally lower than the 1 sat/vB minimum feerate
125            // vote floor. This allows for at least three pending transactions
126            // which only pay the consensus feerate before the exponential
127            // doubling kicks in.
128            feerate_base: 250,
129            dust_limit: bitcoin::Amount::from_sat(10_000),
130            fee_consensus,
131            network,
132        }
133    }
134}
135
136#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, Encodable, Decodable)]
137pub struct FeeConsensus {
138    pub base: Amount,
139    pub parts_per_million: u64,
140}
141
142impl FeeConsensus {
143    /// The wallet module will charge a non-configurable base fee of one hundred
144    /// satoshis per transaction input and output to account for the costs
145    /// incurred by the federation for processing the transaction. On top of
146    /// that the federation may charge an additional relative fee per input and
147    /// output of up to ten thousand parts per million which equals one
148    /// percent.
149    ///
150    /// # Errors
151    /// - Returns [`ExcessiveRelativeFeeError`] if the relative fee is in excess
152    ///   of ten thousand parts per million.
153    pub fn new(parts_per_million: u64) -> Result<Self, ExcessiveRelativeFeeError> {
154        const MAX_PARTS_PER_MILLION: u64 = 10_000;
155
156        if parts_per_million > MAX_PARTS_PER_MILLION {
157            return Err(ExcessiveRelativeFeeError {
158                parts_per_million,
159                max: MAX_PARTS_PER_MILLION,
160            });
161        }
162
163        Ok(Self {
164            base: Amount::from_sats(100),
165            parts_per_million,
166        })
167    }
168
169    pub fn fee(&self, amount: Amount) -> Amount {
170        Amount::from_msats(self.fee_msats(amount.msats))
171    }
172
173    fn fee_msats(&self, msats: u64) -> u64 {
174        msats
175            .saturating_mul(self.parts_per_million)
176            .saturating_div(1_000_000)
177            .checked_add(self.base.msats)
178            .expect("The division creates sufficient headroom to add the base fee")
179    }
180}
181
182#[test]
183fn a_relative_fee_over_the_limit_is_rejected() {
184    let err: ExcessiveRelativeFeeError =
185        FeeConsensus::new(10_001).expect_err("A fee over one percent is excessive");
186
187    assert_eq!(err.parts_per_million, 10_001);
188    assert_eq!(err.max, 10_000);
189}
190
191#[test]
192fn test_fee_consensus() {
193    let fee_consensus = FeeConsensus::new(10_000).expect("Relative fee is within range");
194
195    assert_eq!(
196        fee_consensus.fee(Amount::from_msats(99)),
197        Amount::from_sats(100)
198    );
199
200    assert_eq!(
201        fee_consensus.fee(Amount::from_sats(1)),
202        Amount::from_msats(10) + Amount::from_sats(100)
203    );
204
205    assert_eq!(
206        fee_consensus.fee(Amount::from_sats(1000)),
207        Amount::from_sats(10) + Amount::from_sats(100)
208    );
209
210    assert_eq!(
211        fee_consensus.fee(Amount::from_bitcoins(1)),
212        Amount::from_sats(1_000_000) + Amount::from_sats(100)
213    );
214
215    assert_eq!(
216        fee_consensus.fee(Amount::from_bitcoins(10_000)),
217        Amount::from_bitcoins(100) + Amount::from_sats(100)
218    );
219}
220
221/// Which kind of bitcoin descriptor the federation uses. Currently only `Wsh`
222/// is defined, we can expand in the future.
223#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize, Encodable, Decodable)]
224pub enum WalletDescriptor {
225    Wsh,
226}
227
228#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize, Encodable, Decodable)]
229pub struct WalletClientConfig {
230    /// The public keys for the bitcoin multisig
231    pub bitcoin_pks: BTreeMap<PeerId, PublicKey>,
232    /// The kind of descriptor the federation uses for the multisig.
233    pub descriptor: WalletDescriptor,
234    /// Total vbytes of a pegout bitcoin transaction
235    pub send_tx_vbytes: u64,
236    /// Total vbytes of a pegin bitcoin transaction
237    pub receive_tx_vbytes: u64,
238    /// The minimum feerate doubles for each pending transaction in the stack,
239    /// protecting against catastrophic feerate estimation errors
240    pub feerate_base: u64,
241    /// The minimum amount a user can send on chain
242    pub dust_limit: bitcoin::Amount,
243    /// Fees taken by the guardians to process wallet inputs and outputs
244    pub fee_consensus: FeeConsensus,
245    /// Bitcoin network (e.g. testnet, bitcoin)
246    pub network: Network,
247}
248
249impl std::fmt::Display for WalletClientConfig {
250    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
251        write!(f, "WalletClientConfig {self:?}")
252    }
253}