diff --git a/Cargo.lock b/Cargo.lock index f818f784de2..95bed4b9a30 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2902,6 +2902,7 @@ dependencies = [ "futures", "hex", "itertools 0.13.0", + "rayon", "serde", "serde-big-array", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index a067c89b0f5..54ec4c27731 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -160,6 +160,7 @@ ln-gateway = { package = "fedimint-ln-gateway", path = "./gateway/ln-gateway", v miniscript = "12.2.0" rand = "0.8.5" rand_chacha = "0.3.1" +rayon = "1.10.0" reqwest = { version = "0.12.9", features = [ "json", "rustls-tls", diff --git a/fedimint-server/Cargo.toml b/fedimint-server/Cargo.toml index f5c2c694e33..5f8bb9bad20 100644 --- a/fedimint-server/Cargo.toml +++ b/fedimint-server/Cargo.toml @@ -42,7 +42,7 @@ parity-scale-codec = "3.6.12" pin-project = "1.1.7" rand = { workspace = true } rand_chacha = { workspace = true } -rayon = "1.10.0" +rayon = { workspace = true } rcgen = "=0.13.1" serde = { workspace = true } serde_json = { workspace = true } diff --git a/modules/fedimint-mint-client/Cargo.toml b/modules/fedimint-mint-client/Cargo.toml index cbe44d986ed..327c222a5b4 100644 --- a/modules/fedimint-mint-client/Cargo.toml +++ b/modules/fedimint-mint-client/Cargo.toml @@ -43,6 +43,7 @@ fedimint-mint-common = { workspace = true } futures = { workspace = true } hex = { workspace = true } itertools = { workspace = true } +rayon = { workspace = true } serde = { workspace = true } serde-big-array = { workspace = true } serde_json = { workspace = true } diff --git a/modules/fedimint-mint-client/src/backup.rs b/modules/fedimint-mint-client/src/backup.rs index 976a46b38d7..69fc13f4edb 100644 --- a/modules/fedimint-mint-client/src/backup.rs +++ b/modules/fedimint-mint-client/src/backup.rs @@ -93,13 +93,40 @@ impl MintClientModule { MintClientStateMachines::Output(MintOutputStateMachine { common, state: crate::output::MintOutputStates::Created(created_state), - }) => Some(( - common.out_point, + }) => Some(vec![( + OutPoint { + txid: common.txid, + // MintOutputStates::Created always has one out_idx + out_idx: *common.out_idxs.start(), + }, created_state.amount, created_state.issuance_request, - )), + )]), + MintClientStateMachines::Output(MintOutputStateMachine { + common, + state: crate::output::MintOutputStates::CreatedMulti(created_state), + }) => Some( + common + .out_idxs + .map(|out_idx| { + let issuance_request = created_state + .issuance_requests + .get(&out_idx) + .expect("Must have corresponding out_idx"); + ( + OutPoint { + txid: common.txid, + out_idx, + }, + issuance_request.0, + issuance_request.1, + ) + }) + .collect(), + ), _ => None, }) + .flatten() .collect::>(); let mut idxes = vec![]; diff --git a/modules/fedimint-mint-client/src/backup/recovery.rs b/modules/fedimint-mint-client/src/backup/recovery.rs index d41298c5b92..d43e223a157 100644 --- a/modules/fedimint-mint-client/src/backup/recovery.rs +++ b/modules/fedimint-mint-client/src/backup/recovery.rs @@ -1,6 +1,6 @@ use std::cmp::max; use std::collections::BTreeMap; -use std::fmt; +use std::{fmt, ops}; use fedimint_client::module::init::recovery::{RecoveryFromHistory, RecoveryFromHistoryCommon}; use fedimint_client::module::init::ClientModuleRecoverArgs; @@ -217,7 +217,11 @@ impl RecoveryFromHistory for MintRecovery { MintOutputStateMachine { common: MintOutputCommon { operation_id: OperationId::new_random(), - out_point, + txid: out_point.txid, + out_idxs: ops::RangeInclusive::new( + out_point.out_idx, + out_point.out_idx, + ), }, state: crate::output::MintOutputStates::Created( MintOutputStatesCreated { diff --git a/modules/fedimint-mint-client/src/client_db.rs b/modules/fedimint-mint-client/src/client_db.rs index 4273e967942..3e9cf839863 100644 --- a/modules/fedimint-mint-client/src/client_db.rs +++ b/modules/fedimint-mint-client/src/client_db.rs @@ -13,6 +13,7 @@ use strum_macros::EnumIter; use crate::backup::recovery::MintRecoveryState; use crate::input::{MintInputCommon, MintInputStateMachine, MintInputStateMachineV1}; use crate::oob::{MintOOBStateMachine, MintOOBStateMachineV1, MintOOBStates, MintOOBStatesV1}; +use crate::output::{MintOutputCommon, MintOutputStateMachine, MintOutputStateMachineV1}; use crate::{MintClientStateMachines, SpendableNoteUndecoded}; #[repr(u8)] @@ -126,6 +127,18 @@ pub(crate) fn migrate_state_to_v2( let mint_client_state_machine_variant = u16::consensus_decode(cursor, &decoders)?; let new_mint_state_machine = match mint_client_state_machine_variant { + 0 => { + let old_state = MintOutputStateMachineV1::consensus_decode(cursor, &decoders)?; + MintClientStateMachines::Output(MintOutputStateMachine { + common: MintOutputCommon { + operation_id: old_state.common.operation_id, + txid: old_state.common.out_point.txid, + out_idxs: old_state.common.out_point.out_idx + ..=old_state.common.out_point.out_idx, + }, + state: old_state.state, + }) + } 1 => { let old_state = MintInputStateMachineV1::consensus_decode(cursor, &decoders)?; diff --git a/modules/fedimint-mint-client/src/lib.rs b/modules/fedimint-mint-client/src/lib.rs index f9288a2ba82..8f9c1164a5c 100644 --- a/modules/fedimint-mint-client/src/lib.rs +++ b/modules/fedimint-mint-client/src/lib.rs @@ -80,6 +80,7 @@ use futures::{pin_mut, StreamExt}; use hex::ToHex; use input::MintInputStateCreatedBundle; use oob::MintOOBStatesCreatedMulti; +use output::MintOutputStatesCreatedMulti; use serde::{Deserialize, Serialize}; use strum::IntoEnumIterator; use tbs::{AggregatePublicKey, Signature}; @@ -94,8 +95,7 @@ use crate::client_db::{ use crate::input::{MintInputCommon, MintInputStateMachine, MintInputStates}; use crate::oob::{MintOOBStateMachine, MintOOBStates}; use crate::output::{ - MintOutputCommon, MintOutputStateMachine, MintOutputStates, MintOutputStatesCreated, - NoteIssuanceRequest, + MintOutputCommon, MintOutputStateMachine, MintOutputStates, NoteIssuanceRequest, }; const MINT_E_CASH_TYPE_CHILD_ID: ChildId = ChildId(0); @@ -1064,7 +1064,7 @@ impl MintClientModule { ); let mut outputs = Vec::new(); - let mut output_states = Vec::new(); + let mut issuance_requests = Vec::new(); for (amount, num) in denominations.iter() { for _ in 0..num { @@ -1080,28 +1080,22 @@ impl MintClientModule { amount, }); - output_states.push(MintOutputStatesCreated { - amount, - issuance_request, - }); + issuance_requests.push((amount, issuance_request)); } } let state_generator = Arc::new(move |txid, out_idxs: RangeInclusive| { - out_idxs - .clone() - .flat_map(|out_idx| { - let output_i = (out_idx - out_idxs.clone().start()) as usize; - let output_state = output_states.get(output_i).copied().unwrap(); - vec![MintClientStateMachines::Output(MintOutputStateMachine { - common: MintOutputCommon { - operation_id, - out_point: OutPoint { txid, out_idx }, - }, - state: MintOutputStates::Created(output_state), - })] - }) - .collect() + assert_eq!(out_idxs.clone().count(), issuance_requests.len()); + vec![MintClientStateMachines::Output(MintOutputStateMachine { + common: MintOutputCommon { + operation_id, + txid, + out_idxs: out_idxs.clone(), + }, + state: MintOutputStates::CreatedMulti(MintOutputStatesCreatedMulti { + issuance_requests: out_idxs.zip(issuance_requests.clone()).collect(), + }), + })] }); assert!(!outputs.is_empty()); @@ -1145,7 +1139,9 @@ impl MintClientModule { return None; }; - if state.common.out_point != out_point { + if state.common.txid != out_point.txid + || !state.common.out_idxs.contains(&out_point.out_idx) + { return None; } @@ -1156,7 +1152,7 @@ impl MintClientModule { "Failed to finalize transaction: {}", failed.error ))), - MintOutputStates::Created(_) => None, + MintOutputStates::Created(_) | MintOutputStates::CreatedMulti(_) => None, } }); pin_mut!(stream); diff --git a/modules/fedimint-mint-client/src/output.rs b/modules/fedimint-mint-client/src/output.rs index 3e6791f8ce8..bd14a220d8e 100644 --- a/modules/fedimint-mint-client/src/output.rs +++ b/modules/fedimint-mint-client/src/output.rs @@ -14,11 +14,12 @@ use fedimint_core::encoding::{Decodable, Encodable}; use fedimint_core::module::ApiRequestErased; use fedimint_core::secp256k1::{Keypair, Secp256k1, Signing}; use fedimint_core::task::sleep; -use fedimint_core::{Amount, NumPeersExt, OutPoint, PeerId, Tiered}; +use fedimint_core::{Amount, NumPeersExt, OutPoint, PeerId, Tiered, TransactionId}; use fedimint_derive_secret::{ChildId, DerivableSecret}; use fedimint_logging::LOG_CLIENT_MODULE_MINT; use fedimint_mint_common::endpoint_constants::AWAIT_OUTPUT_OUTCOME_ENDPOINT; use fedimint_mint_common::{BlindNonce, MintOutputOutcome, Nonce}; +use rayon::iter::{IndexedParallelIterator, IntoParallelIterator as _, ParallelIterator as _}; use serde::{Deserialize, Serialize}; use tbs::{ aggregate_signature_shares, blind_message, unblind_signature, AggregatePublicKey, @@ -67,14 +68,29 @@ pub enum MintOutputStates { /// The issuance was completed successfully and the e-cash notes added to /// our wallet Succeeded(MintOutputStatesSucceeded), + /// Issuance request was created, we are waiting for blind signatures + CreatedMulti(MintOutputStatesCreatedMulti), } -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Decodable, Encodable)] -pub struct MintOutputCommon { +#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)] +pub struct MintOutputCommonV1 { pub(crate) operation_id: OperationId, pub(crate) out_point: OutPoint, } +#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)] +pub struct MintOutputCommon { + pub(crate) operation_id: OperationId, + pub(crate) txid: TransactionId, + pub(crate) out_idxs: std::ops::RangeInclusive, +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)] +pub struct MintOutputStateMachineV1 { + pub(crate) common: MintOutputCommonV1, + pub(crate) state: MintOutputStates, +} + #[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)] pub struct MintOutputStateMachine { pub(crate) common: MintOutputCommon, @@ -91,7 +107,10 @@ impl State for MintOutputStateMachine { ) -> Vec> { match &self.state { MintOutputStates::Created(created) => { - created.transitions(context, global_context, self.common) + created.transitions(context, global_context, &self.common) + } + MintOutputStates::CreatedMulti(created) => { + created.transitions(context, global_context, &self.common) } MintOutputStates::Aborted(_) | MintOutputStates::Failed(_) @@ -119,7 +138,7 @@ impl MintOutputStatesCreated { // TODO: make cheaper to clone (Arc?) context: &MintClientContext, global_context: &DynGlobalClientContext, - common: MintOutputCommon, + common: &MintOutputCommon, ) -> Vec> { let tbs_pks = context.tbs_pks.clone(); let client_ctx = context.client_ctx.clone(); @@ -127,14 +146,14 @@ impl MintOutputStatesCreated { vec![ // Check if transaction was rejected StateTransition::new( - Self::await_tx_rejected(global_context.clone(), common), + Self::await_tx_rejected(global_context.clone(), common.clone()), |_dbtx, (), state| Box::pin(async move { Self::transition_tx_rejected(&state) }), ), // Check for output outcome StateTransition::new( Self::await_outcome_ready( global_context.clone(), - common, + common.clone(), context.mint_decoder.clone(), self.amount, self.issuance_request.blinded_message(), @@ -154,11 +173,7 @@ impl MintOutputStatesCreated { } async fn await_tx_rejected(global_context: DynGlobalClientContext, common: MintOutputCommon) { - if global_context - .await_tx_accepted(common.out_point.txid) - .await - .is_err() - { + if global_context.await_tx_accepted(common.txid).await.is_err() { return; } std::future::pending::<()>().await; @@ -168,7 +183,7 @@ impl MintOutputStatesCreated { assert!(matches!(old_state.state, MintOutputStates::Created(_))); MintOutputStateMachine { - common: old_state.common, + common: old_state.common.clone(), state: MintOutputStates::Aborted(MintOutputStatesAborted), } } @@ -196,7 +211,10 @@ impl MintOutputStatesCreated { global_context.api().all_peers().to_num_peers(), ), AWAIT_OUTPUT_OUTCOME_ENDPOINT.to_owned(), - ApiRequestErased::new(common.out_point), + ApiRequestErased::new(OutPoint { + txid: common.txid, + out_idx: *common.out_idxs.start(), + }), ) .await { @@ -286,6 +304,199 @@ impl MintOutputStatesCreated { } } +/// See [`MintOutputStates`] +#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)] +pub struct MintOutputStatesCreatedMulti { + pub(crate) issuance_requests: BTreeMap, +} + +impl MintOutputStatesCreatedMulti { + fn transitions( + &self, + // TODO: make cheaper to clone (Arc?) + context: &MintClientContext, + global_context: &DynGlobalClientContext, + common: &MintOutputCommon, + ) -> Vec> { + let tbs_pks = context.tbs_pks.clone(); + let client_ctx = context.client_ctx.clone(); + + vec![ + // Check if transaction was rejected + StateTransition::new( + Self::await_tx_rejected(global_context.clone(), common.clone()), + |_dbtx, (), state| Box::pin(async move { Self::transition_tx_rejected(&state) }), + ), + // Check for output outcome + StateTransition::new( + Self::await_outcome_ready( + global_context.clone(), + common.clone(), + context.mint_decoder.clone(), + self.issuance_requests.clone(), + context.peer_tbs_pks.clone(), + ), + move |dbtx, blinded_signature_shares, old_state| { + Box::pin(Self::transition_outcome_ready( + client_ctx.clone(), + dbtx, + blinded_signature_shares, + old_state, + tbs_pks.clone(), + )) + }, + ), + ] + } + + async fn await_tx_rejected(global_context: DynGlobalClientContext, common: MintOutputCommon) { + if global_context.await_tx_accepted(common.txid).await.is_err() { + return; + } + std::future::pending::<()>().await; + } + + fn transition_tx_rejected(old_state: &MintOutputStateMachine) -> MintOutputStateMachine { + assert!(matches!(old_state.state, MintOutputStates::CreatedMulti(_))); + + MintOutputStateMachine { + common: old_state.common.clone(), + state: MintOutputStates::Aborted(MintOutputStatesAborted), + } + } + + async fn await_outcome_ready( + global_context: DynGlobalClientContext, + common: MintOutputCommon, + module_decoder: Decoder, + issuance_requests: BTreeMap, + peer_tbs_pks: BTreeMap>, + ) -> Vec<(u64, BTreeMap)> { + let mut ret = vec![]; + // NOTE: We need a new api endpoint that can confirm multiple notes at once? + // --dpc + for (out_idx, (amount, issuance_request)) in issuance_requests { + let blinded_sig_share = fedimint_core::util::retry( + "await and fetch output sigs", + fedimint_core::util::backoff_util::custom_backoff(RETRY_DELAY, RETRY_DELAY, None), + || async { + let decoder = module_decoder.clone(); + let pks = peer_tbs_pks.clone(); + + Ok(global_context + .api() + .request_with_strategy( + // this query collects a threshold of 2f + 1 valid blind signature + // shares + FilterMapThreshold::new( + move |peer, outcome| { + verify_blind_share( + peer, + &outcome, + amount, + issuance_request.blinded_message(), + &decoder, + &pks, + ) + }, + global_context.api().all_peers().to_num_peers(), + ), + AWAIT_OUTPUT_OUTCOME_ENDPOINT.to_owned(), + ApiRequestErased::new(OutPoint { + txid: common.txid, + out_idx, + }), + ) + .await?) + }, + ) + .await + .expect("Will retry forever"); + + ret.push((out_idx, blinded_sig_share)); + } + + ret + } + + async fn transition_outcome_ready( + client_ctx: ClientContext, + dbtx: &mut ClientSMDatabaseTransaction<'_, '_>, + blinded_signature_shares: Vec<(u64, BTreeMap)>, + old_state: MintOutputStateMachine, + tbs_pks: Tiered, + ) -> MintOutputStateMachine { + // we combine the shares, finalize the issuance request with the blind signature + // and store the resulting note in the database + + let mut amount_total = Amount::ZERO; + let MintOutputStates::CreatedMulti(created) = old_state.state else { + panic!("Unexpected prior state") + }; + + let mut spendable_notes: Vec<(Amount, SpendableNote)> = vec![]; + + // Note verification is relatively slow and CPU-bound, so parallelize them + blinded_signature_shares + .into_par_iter() + .map(|(out_idx, blinded_signature_shares)| { + let agg_blind_signature = aggregate_signature_shares( + &blinded_signature_shares + .into_iter() + .map(|(peer, share)| (peer.to_usize() as u64 + 1, share)) + .collect(), + ); + + // this implies that the mint client config's public keys are inconsistent + let (amount, issuance_request) = + created.issuance_requests.get(&out_idx).expect("Must have"); + + let amount_key = tbs_pks.tier(amount).expect("Must have keys for any amount"); + + let spendable_note = issuance_request.finalize(agg_blind_signature); + + assert!(spendable_note.note().verify(*amount_key), "We checked all signature shares in the trigger future, so the combined signature has to be valid"); + + (*amount, spendable_note) + }) + .collect_into_vec(&mut spendable_notes); + + for (amount, spendable_note) in spendable_notes { + debug!(target: LOG_CLIENT_MODULE_MINT, amount = %amount, note=%spendable_note, "Adding new note from transaction output"); + + client_ctx + .log_event( + &mut dbtx.module_tx(), + NoteCreated { + nonce: spendable_note.nonce(), + }, + ) + .await; + + amount_total += amount; + if let Some(note) = dbtx + .module_tx() + .insert_entry( + &NoteKey { + amount, + nonce: spendable_note.nonce(), + }, + &spendable_note.to_undecoded(), + ) + .await + { + error!(?note, "E-cash note was replaced in DB"); + } + } + MintOutputStateMachine { + common: old_state.common, + state: MintOutputStates::Succeeded(MintOutputStatesSucceeded { + amount: amount_total, + }), + } + } +} + /// # Panics /// If the given `outcome` is not a [`MintOutputOutcome::V0`] outcome. pub fn verify_blind_share(