Skip to main content

fedimint_mintv2_client/
api.rs

1use std::collections::BTreeMap;
2use std::sync::Arc;
3use std::time::Duration;
4
5use bitcoin_hashes::sha256;
6use fedimint_api_client::api::{DynModuleApi, FederationApiExt, FederationResult, ServerError};
7use fedimint_api_client::query::FilterMapThreshold;
8use fedimint_core::module::ApiRequestErased;
9use fedimint_core::util::FmtCompact as _;
10use fedimint_core::{NumPeersExt, OutPointRange, PeerId, apply, async_trait_maybe_send, runtime};
11use fedimint_mintv2_common::endpoint_constants::{
12    RECOVERY_COUNT_ENDPOINT, RECOVERY_SLICE_ENDPOINT, RECOVERY_SLICE_HASH_ENDPOINT,
13    SIGNATURE_SHARES_ENDPOINT, SIGNATURE_SHARES_RECOVERY_ENDPOINT,
14};
15use fedimint_mintv2_common::{Denomination, RecoveryItem};
16use tbs::{BlindedMessage, BlindedSignatureShare, PublicKeyShare};
17
18use crate::NoteIssuanceRequest;
19use crate::output::verify_blind_shares;
20
21#[apply(async_trait_maybe_send!)]
22pub trait MintV2ModuleApi {
23    async fn fetch_signature_shares(
24        &self,
25        range: OutPointRange,
26        issuance_requests: Vec<NoteIssuanceRequest>,
27        tbs_pks: BTreeMap<Denomination, BTreeMap<PeerId, PublicKeyShare>>,
28    ) -> BTreeMap<PeerId, Vec<BlindedSignatureShare>>;
29
30    async fn fetch_signature_shares_recovery(
31        &self,
32        issuance_requests: Vec<NoteIssuanceRequest>,
33        tbs_pks: BTreeMap<Denomination, BTreeMap<PeerId, PublicKeyShare>>,
34    ) -> BTreeMap<PeerId, Vec<BlindedSignatureShare>>;
35
36    async fn fetch_recovery_count(&self) -> FederationResult<u64>;
37
38    async fn fetch_recovery_slice_hash(&self, start: u64, end: u64) -> sha256::Hash;
39
40    async fn fetch_recovery_slice(
41        &self,
42        peer: PeerId,
43        timeout: Duration,
44        start: u64,
45        end: u64,
46    ) -> Result<Vec<RecoveryItem>, FetchRecoverySliceError>;
47}
48
49#[apply(async_trait_maybe_send!)]
50impl MintV2ModuleApi for DynModuleApi {
51    async fn fetch_signature_shares(
52        &self,
53        range: OutPointRange,
54        issuance_requests: Vec<NoteIssuanceRequest>,
55        tbs_pks: BTreeMap<Denomination, BTreeMap<PeerId, PublicKeyShare>>,
56    ) -> BTreeMap<PeerId, Vec<BlindedSignatureShare>> {
57        let issuance_requests = Arc::new(issuance_requests);
58        let tbs_pks = Arc::new(tbs_pks);
59
60        self.request_with_strategy_retry(
61            // This query collects a threshold of 2f + 1 valid blind signature shares
62            FilterMapThreshold::new(
63                move |peer, signature_shares: Vec<BlindedSignatureShare>| {
64                    let issuance_requests = issuance_requests.clone();
65                    let tbs_pks = tbs_pks.clone();
66
67                    // Two pairings per note; keep them off the async workers.
68                    runtime::spawn_blocking(move || {
69                        verify_blind_shares(peer, signature_shares, &issuance_requests, &tbs_pks)
70                            .map_err(|err| {
71                                ServerError::InvalidResponse(err.fmt_compact().to_string())
72                            })
73                    })
74                },
75                self.all_peers().to_num_peers(),
76            ),
77            SIGNATURE_SHARES_ENDPOINT.to_owned(),
78            ApiRequestErased::new(range),
79        )
80        .await
81    }
82
83    async fn fetch_signature_shares_recovery(
84        &self,
85        issuance_requests: Vec<NoteIssuanceRequest>,
86        tbs_pks: BTreeMap<Denomination, BTreeMap<PeerId, PublicKeyShare>>,
87    ) -> BTreeMap<PeerId, Vec<BlindedSignatureShare>> {
88        let blinded_messages: Vec<BlindedMessage> = issuance_requests
89            .iter()
90            .map(NoteIssuanceRequest::blinded_message)
91            .collect();
92
93        let issuance_requests = Arc::new(issuance_requests);
94        let tbs_pks = Arc::new(tbs_pks);
95
96        self.request_with_strategy_retry(
97            // This query collects a threshold of 2f + 1 valid blind signature shares
98            FilterMapThreshold::new(
99                move |peer, signature_shares: Vec<BlindedSignatureShare>| {
100                    let issuance_requests = issuance_requests.clone();
101                    let tbs_pks = tbs_pks.clone();
102
103                    // Two pairings per note; keep them off the async workers.
104                    runtime::spawn_blocking(move || {
105                        verify_blind_shares(peer, signature_shares, &issuance_requests, &tbs_pks)
106                            .map_err(|err| {
107                                ServerError::InvalidResponse(err.fmt_compact().to_string())
108                            })
109                    })
110                },
111                self.all_peers().to_num_peers(),
112            ),
113            SIGNATURE_SHARES_RECOVERY_ENDPOINT.to_owned(),
114            ApiRequestErased::new(blinded_messages),
115        )
116        .await
117    }
118
119    async fn fetch_recovery_count(&self) -> FederationResult<u64> {
120        self.request_current_consensus::<u64>(
121            RECOVERY_COUNT_ENDPOINT.to_string(),
122            ApiRequestErased::default(),
123        )
124        .await
125    }
126
127    async fn fetch_recovery_slice_hash(&self, start: u64, end: u64) -> sha256::Hash {
128        self.request_current_consensus_retry(
129            RECOVERY_SLICE_HASH_ENDPOINT.to_owned(),
130            ApiRequestErased::new((start, end)),
131        )
132        .await
133    }
134
135    async fn fetch_recovery_slice(
136        &self,
137        peer: PeerId,
138        timeout: Duration,
139        start: u64,
140        end: u64,
141    ) -> Result<Vec<RecoveryItem>, FetchRecoverySliceError> {
142        let result = runtime::timeout(
143            timeout,
144            self.request_single_peer::<Vec<RecoveryItem>>(
145                RECOVERY_SLICE_ENDPOINT.to_owned(),
146                ApiRequestErased::new((start, end)),
147                peer,
148            ),
149        )
150        .await??;
151
152        Ok(result)
153    }
154}
155
156/// Why a guardian did not serve a slice of the recovery log.
157#[derive(Debug, thiserror::Error)]
158pub(crate) enum FetchRecoverySliceError {
159    /// The guardian did not answer in time.
160    #[error(transparent)]
161    Timeout(#[from] runtime::Elapsed),
162
163    /// The guardian answered with an error.
164    #[error(transparent)]
165    Peer(#[from] ServerError),
166}