Skip to main content

fedimint_core/
lib.rs

1#![deny(clippy::pedantic, clippy::nursery)]
2#![allow(clippy::cast_possible_truncation)]
3#![allow(clippy::cast_possible_wrap)]
4#![allow(clippy::cast_precision_loss)]
5#![allow(clippy::cast_sign_loss)]
6#![allow(clippy::cognitive_complexity)]
7#![allow(clippy::doc_markdown)]
8#![allow(clippy::future_not_send)]
9#![allow(clippy::missing_const_for_fn)]
10#![allow(clippy::missing_errors_doc)]
11#![allow(clippy::missing_panics_doc)]
12#![allow(clippy::module_name_repetitions)]
13#![allow(clippy::must_use_candidate)]
14#![allow(clippy::needless_lifetimes)]
15#![allow(clippy::redundant_pub_crate)]
16#![allow(clippy::return_self_not_must_use)]
17#![allow(clippy::similar_names)]
18#![allow(clippy::transmute_ptr_to_ptr)]
19#![allow(clippy::unsafe_derive_deserialize)]
20
21//! Fedimint Core library
22//!
23//! `fedimint-core` contains commonly used types, utilities and primitives,
24//! shared between both client and server code.
25//!
26//! Things that are server-side only typically live in `fedimint-server`, and
27//! client-side only in `fedimint-client`.
28//!
29//! ### Wasm support
30//!
31//! All code in `fedimint-core` needs to compile on Wasm, and `fedimint-core`
32//! includes helpers and wrappers around non-wasm-safe utitlies.
33//!
34//! In particular:
35//!
36//! * [`fedimint_core::task`] for task spawning and control
37//! * [`fedimint_core::time`] for time-related operations
38
39extern crate self as fedimint_core;
40
41use std::fmt::{self, Debug};
42use std::io::Error;
43use std::ops::{self, Range};
44use std::str::FromStr;
45
46pub use amount::*;
47/// Mostly re-exported for [`Decodable`] macros.
48pub use anyhow;
49use bitcoin::address::NetworkUnchecked;
50pub use bitcoin::hashes::Hash as BitcoinHash;
51use bitcoin::{Address, Network};
52use envs::BitcoinRpcConfig;
53use lightning::util::ser::Writeable;
54use lightning_types::features::Bolt11InvoiceFeatures;
55pub use macro_rules_attribute::apply;
56pub use peer_id::*;
57use serde::{Deserialize, Deserializer, Serialize, Serializer};
58use thiserror::Error;
59pub use tiered::Tiered;
60pub use tiered_multi::*;
61use util::SafeUrl;
62pub use {bitcoin, hex, secp256k1};
63
64use crate::encoding::{Decodable, DecodeError, Encodable};
65use crate::module::registry::ModuleDecoderRegistry;
66
67/// Admin (guardian) client types
68pub mod admin_client;
69/// Bitcoin amount types
70mod amount;
71/// Federation-stored client backups
72pub mod backup;
73/// Legacy serde encoding for `bls12_381`
74pub mod bls12_381_serde;
75/// Federation configuration
76pub mod config;
77/// Fundamental types
78pub mod core;
79/// Database handling
80pub mod db;
81/// Consensus encoding
82pub mod encoding;
83pub mod endpoint_constants;
84/// Common environment variables
85pub mod envs;
86pub mod epoch;
87/// Formatting helpers
88pub mod fmt_utils;
89/// Federation invite code
90pub mod invite_code;
91pub mod log;
92/// Common macros
93#[macro_use]
94pub mod macros;
95/// Base 32 encoding
96pub mod base32;
97/// Extendable module sysystem
98pub mod module;
99/// Peer networking
100pub mod net;
101/// `PeerId` type
102mod peer_id;
103/// Runtime (wasm32 vs native) differences handling
104pub mod runtime;
105/// Rustls support
106pub mod rustls;
107/// Peer setup code for setup ceremony
108pub mod setup_code;
109/// Task handling, including wasm safe logic
110pub mod task;
111/// Types handling per-denomination values
112pub mod tiered;
113/// Types handling multiple per-denomination values
114pub mod tiered_multi;
115/// Time handling, wasm safe functionality
116pub mod time;
117/// Timing helpers
118pub mod timing;
119/// Fedimint transaction (inpus + outputs + signature) types
120pub mod transaction;
121/// Peg-in txo proofs
122pub mod txoproof;
123/// General purpose utilities
124pub mod util;
125/// Version
126pub mod version;
127
128/// Atomic BFT unit containing consensus items
129pub mod session_outcome;
130
131// It's necessary to wrap `hash_newtype!` in a module because the generated code
132// references a module called "core", but we export a conflicting module in this
133// file.
134mod txid {
135    use bitcoin::hashes::hash_newtype;
136    use bitcoin::hashes::sha256::Hash as Sha256;
137
138    hash_newtype!(
139        /// A transaction id for peg-ins, peg-outs and reissuances
140        pub struct TransactionId(Sha256);
141    );
142}
143pub use txid::TransactionId;
144
145/// Amount of bitcoin to send, or `All` to send all available funds
146#[derive(Debug, Eq, PartialEq, Copy, Hash, Clone)]
147pub enum BitcoinAmountOrAll {
148    All,
149    Amount(bitcoin::Amount),
150}
151
152impl std::fmt::Display for BitcoinAmountOrAll {
153    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154        match self {
155            Self::All => write!(f, "all"),
156            Self::Amount(amount) => write!(f, "{amount}"),
157        }
158    }
159}
160
161impl FromStr for BitcoinAmountOrAll {
162    type Err = anyhow::Error;
163
164    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
165        if s.eq_ignore_ascii_case("all") {
166            Ok(Self::All)
167        } else {
168            let amount = Amount::from_str(s)?;
169            Ok(Self::Amount(amount.try_into()?))
170        }
171    }
172}
173
174// Custom serde to handle both "all" and numbers/strings
175impl<'de> Deserialize<'de> for BitcoinAmountOrAll {
176    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
177    where
178        D: Deserializer<'de>,
179    {
180        use serde::de::Error;
181
182        struct Visitor;
183
184        impl serde::de::Visitor<'_> for Visitor {
185            type Value = BitcoinAmountOrAll;
186
187            fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
188                write!(f, "a bitcoin amount as number or 'all'")
189            }
190
191            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
192            where
193                E: Error,
194            {
195                if v.eq_ignore_ascii_case("all") {
196                    Ok(BitcoinAmountOrAll::All)
197                } else {
198                    let sat: u64 = v.parse().map_err(E::custom)?;
199                    Ok(BitcoinAmountOrAll::Amount(bitcoin::Amount::from_sat(sat)))
200                }
201            }
202
203            fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
204            where
205                E: Error,
206            {
207                Ok(BitcoinAmountOrAll::Amount(bitcoin::Amount::from_sat(v)))
208            }
209
210            fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
211            where
212                E: Error,
213            {
214                if v < 0 {
215                    return Err(E::custom("amount cannot be negative"));
216                }
217                Ok(BitcoinAmountOrAll::Amount(bitcoin::Amount::from_sat(
218                    v as u64,
219                )))
220            }
221        }
222
223        deserializer.deserialize_any(Visitor)
224    }
225}
226
227impl Serialize for BitcoinAmountOrAll {
228    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
229    where
230        S: Serializer,
231    {
232        match self {
233            Self::All => serializer.serialize_str("all"),
234            Self::Amount(a) => serializer.serialize_u64(a.to_sat()),
235        }
236    }
237}
238
239/// `InPoint` represents a globally unique input in a transaction
240///
241/// Hence, a transaction ID and the input index is required.
242#[derive(
243    Debug,
244    Clone,
245    Copy,
246    Eq,
247    PartialEq,
248    PartialOrd,
249    Ord,
250    Hash,
251    Deserialize,
252    Serialize,
253    Encodable,
254    Decodable,
255)]
256pub struct InPoint {
257    /// The referenced transaction ID
258    pub txid: TransactionId,
259    /// As a transaction may have multiple inputs, this refers to the index of
260    /// the input in a transaction
261    pub in_idx: u64,
262}
263
264impl std::fmt::Display for InPoint {
265    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
266        write!(f, "{}:{}", self.txid, self.in_idx)
267    }
268}
269
270/// `OutPoint` represents a globally unique output in a transaction
271///
272/// Hence, a transaction ID and the output index is required.
273#[derive(
274    Debug,
275    Clone,
276    Copy,
277    Eq,
278    PartialEq,
279    PartialOrd,
280    Ord,
281    Hash,
282    Deserialize,
283    Serialize,
284    Encodable,
285    Decodable,
286)]
287pub struct OutPoint {
288    /// The referenced transaction ID
289    pub txid: TransactionId,
290    /// As a transaction may have multiple outputs, this refers to the index of
291    /// the output in a transaction
292    pub out_idx: u64,
293}
294
295impl std::fmt::Display for OutPoint {
296    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
297        write!(f, "{}:{}", self.txid, self.out_idx)
298    }
299}
300
301/// A contiguous range of input/output indexes
302#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Encodable, Decodable)]
303pub struct IdxRange {
304    start: u64,
305    end: u64,
306}
307
308impl IdxRange {
309    pub fn new_single(start: u64) -> Option<Self> {
310        start.checked_add(1).map(|end| Self { start, end })
311    }
312
313    pub fn start(self) -> u64 {
314        self.start
315    }
316
317    pub fn count(self) -> usize {
318        self.into_iter().count()
319    }
320
321    /// Number of indexes in the range, or `None` if the range is descending or
322    /// does not fit into a `usize`.
323    ///
324    /// Ranges are deserialized verbatim from untrusted API requests, so a
325    /// caller validating one has to go through this rather than
326    /// [`Self::count`], which reports a descending range as empty and panics on
327    /// `usize` overflow.
328    pub fn checked_count(self) -> Option<usize> {
329        usize::try_from(self.end.checked_sub(self.start)?).ok()
330    }
331
332    pub fn from_inclusive(range: ops::RangeInclusive<u64>) -> Option<Self> {
333        range.end().checked_add(1).map(|end| Self {
334            start: *range.start(),
335            end,
336        })
337    }
338}
339
340impl From<Range<u64>> for IdxRange {
341    fn from(Range { start, end }: Range<u64>) -> Self {
342        Self { start, end }
343    }
344}
345
346impl IntoIterator for IdxRange {
347    type Item = u64;
348    type IntoIter = ops::Range<u64>;
349
350    fn into_iter(self) -> Self::IntoIter {
351        ops::Range {
352            start: self.start,
353            end: self.end,
354        }
355    }
356}
357
358/// Represents a range of output indices for a single transaction
359#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Encodable, Decodable)]
360pub struct OutPointRange {
361    pub txid: TransactionId,
362    idx_range: IdxRange,
363}
364
365impl OutPointRange {
366    pub fn new(txid: TransactionId, idx_range: IdxRange) -> Self {
367        Self { txid, idx_range }
368    }
369
370    pub fn new_single(txid: TransactionId, idx: u64) -> Option<Self> {
371        IdxRange::new_single(idx).map(|idx_range| Self { txid, idx_range })
372    }
373
374    pub fn start_idx(self) -> u64 {
375        self.idx_range.start()
376    }
377
378    pub fn out_idx_iter(self) -> impl Iterator<Item = u64> {
379        self.idx_range.into_iter()
380    }
381
382    pub fn count(self) -> usize {
383        self.idx_range.count()
384    }
385
386    /// See [`IdxRange::checked_count`].
387    pub fn checked_count(self) -> Option<usize> {
388        self.idx_range.checked_count()
389    }
390
391    pub fn txid(&self) -> TransactionId {
392        self.txid
393    }
394}
395
396impl IntoIterator for OutPointRange {
397    type Item = OutPoint;
398    type IntoIter = OutPointRangeIter;
399
400    fn into_iter(self) -> Self::IntoIter {
401        OutPointRangeIter {
402            txid: self.txid,
403            inner: self.idx_range.into_iter(),
404        }
405    }
406}
407
408pub struct OutPointRangeIter {
409    txid: TransactionId,
410    inner: ops::Range<u64>,
411}
412
413impl Iterator for OutPointRangeIter {
414    type Item = OutPoint;
415
416    fn next(&mut self) -> Option<Self::Item> {
417        self.inner.next().map(|idx| OutPoint {
418            txid: self.txid,
419            out_idx: idx,
420        })
421    }
422}
423
424impl Encodable for TransactionId {
425    fn consensus_encode<W: std::io::Write>(&self, writer: &mut W) -> Result<(), Error> {
426        let bytes = &self[..];
427        writer.write_all(bytes)?;
428        Ok(())
429    }
430}
431
432impl Decodable for TransactionId {
433    fn consensus_decode_partial<D: std::io::Read>(
434        d: &mut D,
435        _modules: &ModuleDecoderRegistry,
436    ) -> Result<Self, DecodeError> {
437        let mut bytes = [0u8; 32];
438        d.read_exact(&mut bytes).map_err(DecodeError::from_err)?;
439        Ok(Self::from_byte_array(bytes))
440    }
441}
442
443#[derive(
444    Copy,
445    Clone,
446    Debug,
447    PartialEq,
448    Ord,
449    PartialOrd,
450    Eq,
451    Hash,
452    Serialize,
453    Deserialize,
454    Encodable,
455    Decodable,
456)]
457pub struct Feerate {
458    pub sats_per_kvb: u64,
459}
460
461impl fmt::Display for Feerate {
462    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
463        f.write_fmt(format_args!("{}sat/kvb", self.sats_per_kvb))
464    }
465}
466
467impl Feerate {
468    pub fn calculate_fee(&self, weight: u64) -> bitcoin::Amount {
469        let sats = weight_to_vbytes(weight) * self.sats_per_kvb / 1000;
470        bitcoin::Amount::from_sat(sats)
471    }
472
473    /// Fee for a transaction of the given weight, reproducing the wrapping the
474    /// unchecked multiplication used to do in release profiles.
475    ///
476    /// Consensus behaviour must not depend on the build profile, and
477    /// [`Self::calculate_fee`]'s `*` panics with overflow checks on and wraps
478    /// without them. Sessions ordered before the fee computation was checked
479    /// have to replay as release binaries ran them, so the wrap is explicit.
480    pub fn wrapping_calculate_fee(&self, weight: u64) -> bitcoin::Amount {
481        let sats = weight_to_vbytes(weight).wrapping_mul(self.sats_per_kvb) / 1000;
482        bitcoin::Amount::from_sat(sats)
483    }
484
485    /// Fee for a transaction of the given weight, or `None` if the rate and
486    /// weight do not describe a fee that can exist on chain.
487    ///
488    /// [`Self::calculate_fee`] multiplies unchecked, which wraps to an
489    /// arbitrarily small fee in release profiles. Callers that decide whether
490    /// to accept a transaction must use this instead.
491    pub fn checked_calculate_fee(&self, weight: u64) -> Option<bitcoin::Amount> {
492        let sats = weight_to_vbytes(weight).checked_mul(self.sats_per_kvb)? / 1000;
493        (sats <= bitcoin::Amount::MAX_MONEY.to_sat()).then(|| bitcoin::Amount::from_sat(sats))
494    }
495}
496
497const WITNESS_SCALE_FACTOR: u64 = bitcoin::constants::WITNESS_SCALE_FACTOR as u64;
498
499/// Converts weight to virtual bytes, defined in [BIP-141] as weight / 4
500/// (rounded up to the next integer).
501///
502/// [BIP-141]: https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#transaction-size-calculations
503pub fn weight_to_vbytes(weight: u64) -> u64 {
504    weight.div_ceil(WITNESS_SCALE_FACTOR)
505}
506
507#[derive(Debug, Error)]
508pub enum CoreError {
509    #[error("Mismatching outcome variant: expected {0}, got {1}")]
510    MismatchingVariant(&'static str, &'static str),
511}
512
513// Encode features for a bolt11 invoice without encoding the length.
514// This functionality was available in `lightning` v0.0.123, but has since been
515// removed. See the original code here:
516// https://docs.rs/lightning/0.0.123/src/lightning/ln/features.rs.html#745-750
517// https://docs.rs/lightning/0.0.123/src/lightning/ln/features.rs.html#1008-1012
518pub fn encode_bolt11_invoice_features_without_length(features: &Bolt11InvoiceFeatures) -> Vec<u8> {
519    let mut feature_bytes = vec![];
520    for f in features.le_flags().iter().rev() {
521        f.write(&mut feature_bytes)
522            .expect("Writing to byte vec can't fail");
523    }
524    feature_bytes
525}
526
527/// Outputs hex into an object implementing `fmt::Write`.
528///
529/// Vendored from `bitcoin_hashes` v0.11.0:
530/// <https://docs.rs/bitcoin_hashes/0.11.0/src/bitcoin_hashes/hex.rs.html#173-189>
531pub fn format_hex(data: &[u8], f: &mut std::fmt::Formatter) -> std::fmt::Result {
532    let prec = f.precision().unwrap_or(2 * data.len());
533    let width = f.width().unwrap_or(2 * data.len());
534    for _ in (2 * data.len())..width {
535        f.write_str("0")?;
536    }
537    for ch in data.iter().take(prec / 2) {
538        write!(f, "{:02x}", *ch)?;
539    }
540    if prec < 2 * data.len() && prec % 2 == 1 {
541        write!(f, "{:x}", data[prec / 2] / 16)?;
542    }
543    Ok(())
544}
545
546/// Gets the (approximate) network from a bitcoin address.
547///
548/// This function mimics how `Address.network` is calculated in bitcoin v0.30.
549/// However, that field was removed in more recent versions in part because it
550/// can only distinguish between `Bitcoin`, `Testnet` and `Regtest`.
551///
552/// As of bitcoin v0.32.4, `Address::is_valid_for_network()` performs equality
553/// checks using `NetworkKind` and `KnownHrp`, which only distinguish between
554/// `Bitcoin`, `Testnet` and `Regtest`.
555/// <https://docs.rs/bitcoin/0.32.4/src/bitcoin/address/mod.rs.html#709-716>
556/// <https://docs.rs/bitcoin/0.32.4/src/bitcoin/network.rs.html#51-58>
557/// <https://docs.rs/bitcoin/0.32.4/src/bitcoin/address/mod.rs.html#200-209>
558pub fn get_network_for_address(address: &Address<NetworkUnchecked>) -> Network {
559    if address.is_valid_for_network(Network::Bitcoin) {
560        Network::Bitcoin
561    } else if address.is_valid_for_network(Network::Testnet) {
562        Network::Testnet
563    } else if address.is_valid_for_network(Network::Regtest) {
564        Network::Regtest
565    } else {
566        panic!("Address is not valid for any network");
567    }
568}
569
570/// Returns the default esplora server according to the network
571pub fn default_esplora_server(network: Network, port: Option<String>) -> BitcoinRpcConfig {
572    BitcoinRpcConfig {
573        kind: "esplora".to_string(),
574        url: match network {
575            Network::Bitcoin => SafeUrl::parse("https://mempool.space/api/"),
576            Network::Testnet => SafeUrl::parse("https://mempool.space/testnet/api/"),
577            Network::Testnet4 => SafeUrl::parse("https://mempool.space/testnet4/api/"),
578            Network::Signet => SafeUrl::parse("https://mutinynet.com/api/"),
579            Network::Regtest => SafeUrl::parse(&format!(
580                "http://127.0.0.1:{}/",
581                port.unwrap_or_else(|| String::from("50002"))
582            )),
583            _ => panic!("Failed to parse default esplora server"),
584        }
585        .expect("Failed to parse default esplora server"),
586    }
587}
588
589#[cfg(test)]
590mod tests;