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