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