Skip to main content

fedimint_mintv2_client/
output.rs

1use std::collections::BTreeMap;
2
3use fedimint_client::DynGlobalClientContext;
4use fedimint_client_module::module::OutPointRange;
5use fedimint_client_module::sm::{ClientSMDatabaseTransaction, State, StateTransition};
6use fedimint_core::core::OperationId;
7use fedimint_core::db::IDatabaseTransactionOpsCoreTyped;
8use fedimint_core::encoding::{Decodable, Encodable};
9use fedimint_core::{PeerId, runtime};
10use fedimint_mintv2_common::{Denomination, verify_note};
11use serde::{Deserialize, Serialize};
12use tbs::{
13    AggregatePublicKey, BlindedSignature, BlindedSignatureShare, PublicKeyShare,
14    aggregate_signature_shares,
15};
16
17use crate::api::MintV2ModuleApi;
18use crate::client_db::SpendableNoteKey;
19use crate::{MintClientContext, NoteIssuanceRequest};
20
21#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
22pub struct MintOutputStateMachine {
23    pub common: OutputSMCommon,
24    pub state: OutputSMState,
25}
26
27#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
28pub struct OutputSMCommon {
29    pub operation_id: OperationId,
30    pub range: Option<OutPointRange>,
31    pub issuance_requests: Vec<NoteIssuanceRequest>,
32}
33
34#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
35pub enum OutputSMState {
36    /// Issuance request was created, we are waiting for blind signatures.
37    Pending,
38    /// The transaction containing the issuance was rejected, we can stop
39    /// looking for decryption shares.
40    Aborted,
41    /// The transaction containing the issuance was accepted but an unexpected
42    /// error occurred, this should never happen with a honest federation and
43    /// bug-free code.
44    Failure,
45    /// The issuance was completed successfully and the e-cash notes added to
46    /// our wallet.
47    Success,
48}
49
50impl State for MintOutputStateMachine {
51    type ModuleContext = MintClientContext;
52
53    fn transitions(
54        &self,
55        context: &Self::ModuleContext,
56        global_context: &DynGlobalClientContext,
57    ) -> Vec<StateTransition<Self>> {
58        let context = context.clone();
59
60        match &self.state {
61            OutputSMState::Pending => {
62                vec![StateTransition::new(
63                    Self::await_issuance(
64                        global_context.clone(),
65                        self.common.range,
66                        self.common.issuance_requests.clone(),
67                        context.tbs_pks.clone(),
68                        context.tbs_agg_pks.clone(),
69                    ),
70                    move |dbtx, outcome, old_state| {
71                        let balance_update_sender = context.balance_update_sender.clone();
72
73                        dbtx.module_tx()
74                            .on_commit(move || balance_update_sender.send_replace(()));
75
76                        Box::pin(Self::transition_issuance(dbtx, outcome, old_state))
77                    },
78                )]
79            }
80            OutputSMState::Aborted | OutputSMState::Failure | OutputSMState::Success => {
81                vec![]
82            }
83        }
84    }
85
86    fn operation_id(&self) -> OperationId {
87        self.common.operation_id
88    }
89}
90
91/// What the issuance trigger resolves to.
92///
93/// The shares are aggregated and the notes verified in the trigger, off the
94/// async workers, so the transition only unblinds and inserts them.
95#[derive(Debug, Serialize, Deserialize)]
96pub enum IssuanceOutcome {
97    /// The transaction containing the issuance was rejected.
98    Aborted,
99    /// The aggregated signatures of the notes that verified, in order, and
100    /// whether one did not: the verified ones are kept either way, the
101    /// machine fails on the invalid one.
102    Signatures {
103        signatures: Vec<BlindedSignature>,
104        invalid: bool,
105    },
106}
107
108impl MintOutputStateMachine {
109    async fn await_issuance(
110        global_context: DynGlobalClientContext,
111        range: Option<OutPointRange>,
112        issuance_requests: Vec<NoteIssuanceRequest>,
113        tbs_pks: BTreeMap<Denomination, BTreeMap<PeerId, PublicKeyShare>>,
114        tbs_agg_pks: BTreeMap<Denomination, AggregatePublicKey>,
115    ) -> IssuanceOutcome {
116        let signature_shares = if let Some(range) = range {
117            if global_context.await_tx_accepted(range.txid).await.is_err() {
118                return IssuanceOutcome::Aborted;
119            }
120
121            global_context
122                .module_api()
123                .fetch_signature_shares(range, issuance_requests.clone(), tbs_pks)
124                .await
125        } else {
126            global_context
127                .module_api()
128                .fetch_signature_shares_recovery(issuance_requests.clone(), tbs_pks)
129                .await
130        };
131
132        // Aggregating a note's shares is a Lagrange interpolation and verifying
133        // it two pairings; keep them off the async workers.
134        runtime::spawn_blocking(move || {
135            let mut signatures = Vec::with_capacity(issuance_requests.len());
136
137            for (i, request) in issuance_requests.iter().enumerate() {
138                let agg_blind_signature = aggregate_signature_shares(
139                    &signature_shares
140                        .iter()
141                        .map(|(peer, shares)| (peer.to_usize() as u64, shares[i]))
142                        .collect(),
143                );
144
145                let spendable_note = request.finalize(agg_blind_signature);
146
147                let pk = *tbs_agg_pks
148                    .get(&request.denomination)
149                    .expect("No aggregated pk found for denomination");
150
151                if !verify_note(spendable_note.note(), pk) {
152                    return IssuanceOutcome::Signatures {
153                        signatures,
154                        invalid: true,
155                    };
156                }
157
158                signatures.push(agg_blind_signature);
159            }
160
161            IssuanceOutcome::Signatures {
162                signatures,
163                invalid: false,
164            }
165        })
166        .await
167    }
168
169    async fn transition_issuance(
170        dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
171        outcome: IssuanceOutcome,
172        old_state: MintOutputStateMachine,
173    ) -> MintOutputStateMachine {
174        let (signatures, invalid) = match outcome {
175            IssuanceOutcome::Aborted => {
176                return MintOutputStateMachine {
177                    common: old_state.common,
178                    state: OutputSMState::Aborted,
179                };
180            }
181            IssuanceOutcome::Signatures {
182                signatures,
183                invalid,
184            } => (signatures, invalid),
185        };
186
187        for (request, signature) in old_state.common.issuance_requests.iter().zip(signatures) {
188            dbtx.module_tx()
189                .insert_new_entry(&SpendableNoteKey(request.finalize(signature)), &())
190                .await;
191        }
192
193        MintOutputStateMachine {
194            common: old_state.common,
195            state: if invalid {
196                OutputSMState::Failure
197            } else {
198                OutputSMState::Success
199            },
200        }
201    }
202}
203
204pub fn verify_blind_shares(
205    peer: PeerId,
206    signature_shares: Vec<BlindedSignatureShare>,
207    issuance_requests: &[NoteIssuanceRequest],
208    tbs_pks: &BTreeMap<Denomination, BTreeMap<PeerId, PublicKeyShare>>,
209) -> Result<Vec<BlindedSignatureShare>, VerifyBlindSharesError> {
210    if signature_shares.len() != issuance_requests.len() {
211        return Err(VerifyBlindSharesError::ShareCount);
212    }
213
214    for (request, share) in issuance_requests.iter().zip(signature_shares.iter()) {
215        let amount_key = tbs_pks
216            .get(&request.denomination)
217            .expect("No pk shares found for denomination")
218            .get(&peer)
219            .expect("No pk share found for peer");
220
221        if !tbs::verify_signature_share(request.blinded_message(), *share, *amount_key) {
222            return Err(VerifyBlindSharesError::InvalidShare);
223        }
224    }
225
226    Ok(signature_shares)
227}
228
229/// Why a guardian's blind signature shares were rejected.
230#[derive(Debug, thiserror::Error)]
231pub(crate) enum VerifyBlindSharesError {
232    /// The guardian sent a different number of shares than notes were
233    /// requested.
234    #[error("Invalid number of signatures shares")]
235    ShareCount,
236
237    /// A share does not verify against the guardian's public key share.
238    #[error("Invalid blind signature")]
239    InvalidShare,
240}