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
145pub struct TransactionIdShortFmt<'a>(&'a TransactionId);
146pub struct TransactionIdFullFmt<'a>(&'a TransactionId);
147
148impl TransactionId {
149    pub fn fmt_short(&self) -> TransactionIdShortFmt<'_> {
150        TransactionIdShortFmt(self)
151    }
152
153    pub fn fmt_full(&self) -> TransactionIdFullFmt<'_> {
154        TransactionIdFullFmt(self)
155    }
156}
157
158impl std::fmt::Display for TransactionIdShortFmt<'_> {
159    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160        let bytes = &self.0[..];
161        format_hex(&bytes[..4], f)?;
162        f.write_str("_")?;
163        format_hex(&bytes[28..], f)
164    }
165}
166
167impl std::fmt::Display for TransactionIdFullFmt<'_> {
168    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169        let bytes = &self.0[..];
170        format_hex(bytes, f)
171    }
172}
173
174/// Bitcoin chain identifier
175///
176/// This is a newtype wrapper around [`bitcoin::BlockHash`] representing the
177/// block hash at height 1, which uniquely identifies a Bitcoin chain (mainnet,
178/// testnet, signet, regtest, or custom networks), unlike genesis block hash
179/// which is often the same for same types of networks (e.g. mutinynet vs
180/// signet4).
181///
182/// Using a distinct type instead of raw `BlockHash` provides type safety and
183/// makes the intent clearer when passing chain identifiers through APIs.
184#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Encodable, Decodable)]
185pub struct ChainId(pub bitcoin::BlockHash);
186
187impl ChainId {
188    /// Create a new `ChainId` from a `BlockHash`
189    pub fn new(block_hash: bitcoin::BlockHash) -> Self {
190        Self(block_hash)
191    }
192
193    /// Get the inner `BlockHash`
194    pub fn block_hash(&self) -> bitcoin::BlockHash {
195        self.0
196    }
197}
198
199impl From<bitcoin::BlockHash> for ChainId {
200    fn from(block_hash: bitcoin::BlockHash) -> Self {
201        Self(block_hash)
202    }
203}
204
205impl From<ChainId> for bitcoin::BlockHash {
206    fn from(chain_id: ChainId) -> Self {
207        chain_id.0
208    }
209}
210
211impl std::fmt::Display for ChainId {
212    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213        write!(f, "{}", self.0)
214    }
215}
216
217impl FromStr for ChainId {
218    type Err = bitcoin::hashes::hex::HexToArrayError;
219
220    fn from_str(s: &str) -> Result<Self, Self::Err> {
221        bitcoin::BlockHash::from_str(s).map(Self)
222    }
223}
224
225impl Serialize for ChainId {
226    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
227    where
228        S: Serializer,
229    {
230        self.0.serialize(serializer)
231    }
232}
233
234impl<'de> Deserialize<'de> for ChainId {
235    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
236    where
237        D: Deserializer<'de>,
238    {
239        bitcoin::BlockHash::deserialize(deserializer).map(Self)
240    }
241}
242
243/// Amount of bitcoin to send, or `All` to send all available funds
244#[derive(Debug, Eq, PartialEq, Copy, Hash, Clone)]
245pub enum BitcoinAmountOrAll {
246    All,
247    Amount(bitcoin::Amount),
248}
249
250impl std::fmt::Display for BitcoinAmountOrAll {
251    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252        match self {
253            Self::All => write!(f, "all"),
254            Self::Amount(amount) => write!(f, "{amount}"),
255        }
256    }
257}
258
259impl FromStr for BitcoinAmountOrAll {
260    type Err = anyhow::Error;
261
262    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
263        if s.eq_ignore_ascii_case("all") {
264            Ok(Self::All)
265        } else {
266            let amount = Amount::from_str(s)?;
267            Ok(Self::Amount(amount.try_into()?))
268        }
269    }
270}
271
272// Custom serde to handle both "all" and numbers/strings
273impl<'de> Deserialize<'de> for BitcoinAmountOrAll {
274    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
275    where
276        D: Deserializer<'de>,
277    {
278        use serde::de::Error;
279
280        struct Visitor;
281
282        impl serde::de::Visitor<'_> for Visitor {
283            type Value = BitcoinAmountOrAll;
284
285            fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
286                write!(f, "a bitcoin amount as number or 'all'")
287            }
288
289            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
290            where
291                E: Error,
292            {
293                if v.eq_ignore_ascii_case("all") {
294                    Ok(BitcoinAmountOrAll::All)
295                } else {
296                    let sat: u64 = v.parse().map_err(E::custom)?;
297                    Ok(BitcoinAmountOrAll::Amount(bitcoin::Amount::from_sat(sat)))
298                }
299            }
300
301            fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
302            where
303                E: Error,
304            {
305                Ok(BitcoinAmountOrAll::Amount(bitcoin::Amount::from_sat(v)))
306            }
307
308            fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
309            where
310                E: Error,
311            {
312                if v < 0 {
313                    return Err(E::custom("amount cannot be negative"));
314                }
315                Ok(BitcoinAmountOrAll::Amount(bitcoin::Amount::from_sat(
316                    v as u64,
317                )))
318            }
319        }
320
321        deserializer.deserialize_any(Visitor)
322    }
323}
324
325impl Serialize for BitcoinAmountOrAll {
326    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
327    where
328        S: Serializer,
329    {
330        match self {
331            Self::All => serializer.serialize_str("all"),
332            Self::Amount(a) => serializer.serialize_u64(a.to_sat()),
333        }
334    }
335}
336
337/// `InPoint` represents a globally unique input in a transaction
338///
339/// Hence, a transaction ID and the input index is required.
340#[derive(
341    Debug,
342    Clone,
343    Copy,
344    Eq,
345    PartialEq,
346    PartialOrd,
347    Ord,
348    Hash,
349    Deserialize,
350    Serialize,
351    Encodable,
352    Decodable,
353)]
354pub struct InPoint {
355    /// The referenced transaction ID
356    pub txid: TransactionId,
357    /// As a transaction may have multiple inputs, this refers to the index of
358    /// the input in a transaction
359    pub in_idx: u64,
360}
361
362impl std::fmt::Display for InPoint {
363    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
364        write!(f, "{}:{}", self.txid, self.in_idx)
365    }
366}
367
368/// `OutPoint` represents a globally unique output in a transaction
369///
370/// Hence, a transaction ID and the output index is required.
371#[derive(
372    Debug,
373    Clone,
374    Copy,
375    Eq,
376    PartialEq,
377    PartialOrd,
378    Ord,
379    Hash,
380    Deserialize,
381    Serialize,
382    Encodable,
383    Decodable,
384)]
385pub struct OutPoint {
386    /// The referenced transaction ID
387    pub txid: TransactionId,
388    /// As a transaction may have multiple outputs, this refers to the index of
389    /// the output in a transaction
390    pub out_idx: u64,
391}
392
393impl std::fmt::Display for OutPoint {
394    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
395        write!(f, "{}:{}", self.txid, self.out_idx)
396    }
397}
398
399/// A contiguous range of input/output indexes
400#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Encodable, Decodable)]
401pub struct IdxRange {
402    start: u64,
403    end: u64,
404}
405
406impl IdxRange {
407    pub fn new_single(start: u64) -> Option<Self> {
408        start.checked_add(1).map(|end| Self { start, end })
409    }
410
411    pub fn start(self) -> u64 {
412        self.start
413    }
414
415    pub fn count(self) -> usize {
416        self.into_iter().count()
417    }
418
419    pub fn from_inclusive(range: ops::RangeInclusive<u64>) -> Option<Self> {
420        range.end().checked_add(1).map(|end| Self {
421            start: *range.start(),
422            end,
423        })
424    }
425}
426
427impl From<Range<u64>> for IdxRange {
428    fn from(Range { start, end }: Range<u64>) -> Self {
429        Self { start, end }
430    }
431}
432
433impl IntoIterator for IdxRange {
434    type Item = u64;
435    type IntoIter = ops::Range<u64>;
436
437    fn into_iter(self) -> Self::IntoIter {
438        ops::Range {
439            start: self.start,
440            end: self.end,
441        }
442    }
443}
444
445/// Represents a range of output indices for a single transaction
446#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Encodable, Decodable)]
447pub struct OutPointRange {
448    pub txid: TransactionId,
449    idx_range: IdxRange,
450}
451
452impl OutPointRange {
453    pub fn new(txid: TransactionId, idx_range: IdxRange) -> Self {
454        Self { txid, idx_range }
455    }
456
457    pub fn new_single(txid: TransactionId, idx: u64) -> Option<Self> {
458        IdxRange::new_single(idx).map(|idx_range| Self { txid, idx_range })
459    }
460
461    pub fn start_idx(self) -> u64 {
462        self.idx_range.start()
463    }
464
465    pub fn out_idx_iter(self) -> impl Iterator<Item = u64> {
466        self.idx_range.into_iter()
467    }
468
469    pub fn count(self) -> usize {
470        self.idx_range.count()
471    }
472
473    pub fn start_out_point(self) -> OutPoint {
474        OutPoint {
475            txid: self.txid,
476            out_idx: self.idx_range.start(),
477        }
478    }
479
480    pub fn end_out_point(self) -> OutPoint {
481        OutPoint {
482            txid: self.txid,
483            out_idx: self.idx_range.end,
484        }
485    }
486
487    pub fn txid(&self) -> TransactionId {
488        self.txid
489    }
490}
491
492impl IntoIterator for OutPointRange {
493    type Item = OutPoint;
494    type IntoIter = OutPointRangeIter;
495
496    fn into_iter(self) -> Self::IntoIter {
497        OutPointRangeIter {
498            txid: self.txid,
499            inner: self.idx_range.into_iter(),
500        }
501    }
502}
503
504pub struct OutPointRangeIter {
505    txid: TransactionId,
506    inner: ops::Range<u64>,
507}
508
509impl Iterator for OutPointRangeIter {
510    type Item = OutPoint;
511
512    fn next(&mut self) -> Option<Self::Item> {
513        self.inner.next().map(|idx| OutPoint {
514            txid: self.txid,
515            out_idx: idx,
516        })
517    }
518}
519
520impl Encodable for TransactionId {
521    fn consensus_encode<W: std::io::Write>(&self, writer: &mut W) -> Result<(), Error> {
522        let bytes = &self[..];
523        writer.write_all(bytes)?;
524        Ok(())
525    }
526}
527
528impl Decodable for TransactionId {
529    fn consensus_decode_partial<D: std::io::Read>(
530        d: &mut D,
531        _modules: &ModuleDecoderRegistry,
532    ) -> Result<Self, DecodeError> {
533        let mut bytes = [0u8; 32];
534        d.read_exact(&mut bytes).map_err(DecodeError::from_err)?;
535        Ok(Self::from_byte_array(bytes))
536    }
537}
538
539#[derive(
540    Copy,
541    Clone,
542    Debug,
543    PartialEq,
544    Ord,
545    PartialOrd,
546    Eq,
547    Hash,
548    Serialize,
549    Deserialize,
550    Encodable,
551    Decodable,
552)]
553pub struct Feerate {
554    pub sats_per_kvb: u64,
555}
556
557impl fmt::Display for Feerate {
558    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
559        f.write_fmt(format_args!("{}sat/kvb", self.sats_per_kvb))
560    }
561}
562
563impl Feerate {
564    pub fn calculate_fee(&self, weight: u64) -> bitcoin::Amount {
565        let sats = weight_to_vbytes(weight) * self.sats_per_kvb / 1000;
566        bitcoin::Amount::from_sat(sats)
567    }
568}
569
570const WITNESS_SCALE_FACTOR: u64 = bitcoin::constants::WITNESS_SCALE_FACTOR as u64;
571
572/// Converts weight to virtual bytes, defined in [BIP-141] as weight / 4
573/// (rounded up to the next integer).
574///
575/// [BIP-141]: https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#transaction-size-calculations
576pub fn weight_to_vbytes(weight: u64) -> u64 {
577    weight.div_ceil(WITNESS_SCALE_FACTOR)
578}
579
580#[derive(Debug, Error)]
581pub enum CoreError {
582    #[error("Mismatching outcome variant: expected {0}, got {1}")]
583    MismatchingVariant(&'static str, &'static str),
584}
585
586// Encode features for a bolt11 invoice without encoding the length.
587// This functionality was available in `lightning` v0.0.123, but has since been
588// removed. See the original code here:
589// https://docs.rs/lightning/0.0.123/src/lightning/ln/features.rs.html#745-750
590// https://docs.rs/lightning/0.0.123/src/lightning/ln/features.rs.html#1008-1012
591pub fn encode_bolt11_invoice_features_without_length(features: &Bolt11InvoiceFeatures) -> Vec<u8> {
592    let mut feature_bytes = vec![];
593    for f in features.le_flags().iter().rev() {
594        f.write(&mut feature_bytes)
595            .expect("Writing to byte vec can't fail");
596    }
597    feature_bytes
598}
599
600/// Outputs hex into an object implementing `fmt::Write`.
601///
602/// Vendored from `bitcoin_hashes` v0.11.0:
603/// <https://docs.rs/bitcoin_hashes/0.11.0/src/bitcoin_hashes/hex.rs.html#173-189>
604pub fn format_hex(data: &[u8], f: &mut std::fmt::Formatter) -> std::fmt::Result {
605    let prec = f.precision().unwrap_or(2 * data.len());
606    let width = f.width().unwrap_or(2 * data.len());
607    for _ in (2 * data.len())..width {
608        f.write_str("0")?;
609    }
610    for ch in data.iter().take(prec / 2) {
611        write!(f, "{:02x}", *ch)?;
612    }
613    if prec < 2 * data.len() && prec % 2 == 1 {
614        write!(f, "{:x}", data[prec / 2] / 16)?;
615    }
616    Ok(())
617}
618
619/// Gets the (approximate) network from a bitcoin address.
620///
621/// This function mimics how `Address.network` is calculated in bitcoin v0.30.
622/// However, that field was removed in more recent versions in part because it
623/// can only distinguish between `Bitcoin`, `Testnet` and `Regtest`.
624///
625/// As of bitcoin v0.32.4, `Address::is_valid_for_network()` performs equality
626/// checks using `NetworkKind` and `KnownHrp`, which only distinguish between
627/// `Bitcoin`, `Testnet` and `Regtest`.
628/// <https://docs.rs/bitcoin/0.32.4/src/bitcoin/address/mod.rs.html#709-716>
629/// <https://docs.rs/bitcoin/0.32.4/src/bitcoin/network.rs.html#51-58>
630/// <https://docs.rs/bitcoin/0.32.4/src/bitcoin/address/mod.rs.html#200-209>
631pub fn get_network_for_address(address: &Address<NetworkUnchecked>) -> Network {
632    if address.is_valid_for_network(Network::Bitcoin) {
633        Network::Bitcoin
634    } else if address.is_valid_for_network(Network::Testnet) {
635        Network::Testnet
636    } else if address.is_valid_for_network(Network::Regtest) {
637        Network::Regtest
638    } else {
639        panic!("Address is not valid for any network");
640    }
641}
642
643/// Returns the default esplora server according to the network
644pub fn default_esplora_server(network: Network, port: Option<String>) -> BitcoinRpcConfig {
645    BitcoinRpcConfig {
646        kind: "esplora".to_string(),
647        url: match network {
648            Network::Bitcoin => SafeUrl::parse("https://mempool.space/api/"),
649            Network::Testnet => SafeUrl::parse("https://mempool.space/testnet/api/"),
650            Network::Testnet4 => SafeUrl::parse("https://mempool.space/testnet4/api/"),
651            Network::Signet => SafeUrl::parse("https://mutinynet.com/api/"),
652            Network::Regtest => SafeUrl::parse(&format!(
653                "http://127.0.0.1:{}/",
654                port.unwrap_or_else(|| String::from("50002"))
655            )),
656        }
657        .expect("Failed to parse default esplora server"),
658    }
659}
660
661#[cfg(test)]
662mod tests;