use std::collections::BTreeMap;
use std::hash;
use std::time::Duration;
use anyhow::{anyhow, bail};
use fedimint_api_client::api::{deserialize_outcome, FederationApiExt, SerdeOutputOutcome};
use fedimint_api_client::query::FilterMapThreshold;
use fedimint_client::module::ClientContext;
use fedimint_client::sm::{ClientSMDatabaseTransaction, State, StateTransition};
use fedimint_client::DynGlobalClientContext;
use fedimint_core::bitcoin_migration::{
bitcoin30_to_bitcoin32_keypair, bitcoin32_to_bitcoin30_secp256k1_pubkey,
};
use fedimint_core::core::{Decoder, OperationId};
use fedimint_core::db::IDatabaseTransactionOpsCoreTyped;
use fedimint_core::encoding::{Decodable, Encodable};
use fedimint_core::module::ApiRequestErased;
use fedimint_core::secp256k1::{Secp256k1, Signing};
use fedimint_core::secp256k1_29::Keypair;
use fedimint_core::task::sleep;
use fedimint_core::{Amount, NumPeersExt, OutPoint, PeerId, Tiered};
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 serde::{Deserialize, Serialize};
use tbs::{
aggregate_signature_shares, blind_message, unblind_signature, AggregatePublicKey,
BlindedMessage, BlindedSignature, BlindedSignatureShare, BlindingKey, PublicKeyShare,
};
use tracing::{debug, error};
use crate::client_db::NoteKey;
use crate::event::NoteCreated;
use crate::{MintClientContext, MintClientModule, SpendableNote};
const RETRY_DELAY: Duration = Duration::from_secs(1);
const SPEND_KEY_CHILD_ID: ChildId = ChildId(0);
const BLINDING_KEY_CHILD_ID: ChildId = ChildId(1);
#[cfg_attr(doc, aquamarine::aquamarine)]
#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
pub enum MintOutputStates {
Created(MintOutputStatesCreated),
Aborted(MintOutputStatesAborted),
Failed(MintOutputStatesFailed),
Succeeded(MintOutputStatesSucceeded),
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
pub struct MintOutputCommon {
pub(crate) operation_id: OperationId,
pub(crate) out_point: OutPoint,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
pub struct MintOutputStateMachine {
pub(crate) common: MintOutputCommon,
pub(crate) state: MintOutputStates,
}
impl State for MintOutputStateMachine {
type ModuleContext = MintClientContext;
fn transitions(
&self,
context: &Self::ModuleContext,
global_context: &DynGlobalClientContext,
) -> Vec<StateTransition<Self>> {
match &self.state {
MintOutputStates::Created(created) => {
created.transitions(context, global_context, self.common)
}
MintOutputStates::Aborted(_)
| MintOutputStates::Failed(_)
| MintOutputStates::Succeeded(_) => {
vec![]
}
}
}
fn operation_id(&self) -> OperationId {
self.common.operation_id
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
pub struct MintOutputStatesCreated {
pub(crate) amount: Amount,
pub(crate) issuance_request: NoteIssuanceRequest,
}
impl MintOutputStatesCreated {
fn transitions(
&self,
context: &MintClientContext,
global_context: &DynGlobalClientContext,
common: MintOutputCommon,
) -> Vec<StateTransition<MintOutputStateMachine>> {
let tbs_pks = context.tbs_pks.clone();
let client_ctx = context.client_ctx.clone();
vec![
StateTransition::new(
Self::await_tx_rejected(global_context.clone(), common),
|_dbtx, (), state| Box::pin(async move { Self::transition_tx_rejected(&state) }),
),
StateTransition::new(
Self::await_outcome_ready(
global_context.clone(),
common,
context.mint_decoder.clone(),
self.amount,
self.issuance_request.blinded_message(),
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.out_point.txid)
.await
.is_err()
{
return;
}
std::future::pending::<()>().await;
}
fn transition_tx_rejected(old_state: &MintOutputStateMachine) -> MintOutputStateMachine {
assert!(matches!(old_state.state, MintOutputStates::Created(_)));
MintOutputStateMachine {
common: old_state.common,
state: MintOutputStates::Aborted(MintOutputStatesAborted),
}
}
async fn await_outcome_ready(
global_context: DynGlobalClientContext,
common: MintOutputCommon,
module_decoder: Decoder,
amount: Amount,
message: BlindedMessage,
peer_tbs_pks: BTreeMap<PeerId, Tiered<PublicKeyShare>>,
) -> BTreeMap<PeerId, BlindedSignatureShare> {
loop {
let decoder = module_decoder.clone();
let pks = peer_tbs_pks.clone();
match global_context
.api()
.request_with_strategy(
FilterMapThreshold::new(
move |peer, outcome| {
verify_blind_share(peer, &outcome, amount, message, &decoder, &pks)
},
global_context.api().all_peers().to_num_peers(),
),
AWAIT_OUTPUT_OUTCOME_ENDPOINT.to_owned(),
ApiRequestErased::new(common.out_point),
)
.await
{
Ok(outcome) => return outcome,
Err(error) => {
error.report_if_important();
sleep(RETRY_DELAY).await;
}
};
}
}
async fn transition_outcome_ready(
client_ctx: ClientContext<MintClientModule>,
dbtx: &mut ClientSMDatabaseTransaction<'_, '_>,
blinded_signature_shares: BTreeMap<PeerId, BlindedSignatureShare>,
old_state: MintOutputStateMachine,
tbs_pks: Tiered<AggregatePublicKey>,
) -> MintOutputStateMachine {
let MintOutputStates::Created(created) = old_state.state else {
panic!("Unexpected prior state")
};
let agg_blind_signature = aggregate_signature_shares(
&blinded_signature_shares
.into_iter()
.map(|(peer, share)| (peer.to_usize() as u64 + 1, share))
.collect(),
);
let amount_key = tbs_pks
.tier(&created.amount)
.expect("We obtained this amount from tbs_pks when we created the output");
if !tbs::verify_blinded_signature(
created.issuance_request.blinded_message(),
agg_blind_signature,
*amount_key,
) {
return MintOutputStateMachine {
common: old_state.common,
state: MintOutputStates::Failed(MintOutputStatesFailed {
error: "Invalid blind signature".to_string(),
}),
};
}
let spendable_note = created.issuance_request.finalize(agg_blind_signature);
assert!(spendable_note.note().verify(*amount_key));
debug!(target: LOG_CLIENT_MODULE_MINT, amount = %created.amount, note=%spendable_note, "Adding new note from transaction output");
client_ctx
.log_event(
&mut dbtx.module_tx(),
NoteCreated {
nonce: spendable_note.nonce(),
},
)
.await;
if let Some(note) = dbtx
.module_tx()
.insert_entry(
&NoteKey {
amount: created.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: created.amount,
}),
}
}
}
pub fn verify_blind_share(
peer: PeerId,
outcome: &SerdeOutputOutcome,
amount: Amount,
blinded_message: BlindedMessage,
decoder: &Decoder,
peer_tbs_pks: &BTreeMap<PeerId, Tiered<PublicKeyShare>>,
) -> anyhow::Result<BlindedSignatureShare> {
let outcome = deserialize_outcome::<MintOutputOutcome>(outcome, decoder)?;
let blinded_signature_share = outcome
.ensure_v0_ref()
.expect("We only process output outcome versions created by ourselves")
.0;
let amount_key = peer_tbs_pks
.get(&peer)
.ok_or(anyhow!("Unknown peer"))?
.tier(&amount)
.map_err(|_| anyhow!("Invalid Amount Tier"))?;
if !tbs::verify_blind_share(blinded_message, blinded_signature_share, *amount_key) {
bail!("Invalid blind signature")
}
Ok(blinded_signature_share)
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
pub struct MintOutputStatesAborted;
#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
pub struct MintOutputStatesFailed {
pub error: String,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
pub struct MintOutputStatesSucceeded {
pub amount: Amount,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, Encodable, Decodable)]
pub struct NoteIssuanceRequest {
spend_key: Keypair,
blinding_key: BlindingKey,
}
impl hash::Hash for NoteIssuanceRequest {
fn hash<H: hash::Hasher>(&self, state: &mut H) {
self.spend_key.hash(state);
}
}
impl NoteIssuanceRequest {
pub fn new<C>(ctx: &Secp256k1<C>, secret: &DerivableSecret) -> (NoteIssuanceRequest, BlindNonce)
where
C: Signing,
{
let spend_key = secret.child_key(SPEND_KEY_CHILD_ID).to_secp_key(ctx);
let nonce = Nonce(spend_key.public_key());
let blinding_key = BlindingKey(secret.child_key(BLINDING_KEY_CHILD_ID).to_bls12_381_key());
let blinded_nonce = blind_message(nonce.to_message(), blinding_key);
let cr = NoteIssuanceRequest {
spend_key: bitcoin30_to_bitcoin32_keypair(&spend_key),
blinding_key,
};
(cr, BlindNonce(blinded_nonce))
}
pub fn nonce(&self) -> Nonce {
Nonce(bitcoin32_to_bitcoin30_secp256k1_pubkey(
&self.spend_key.public_key(),
))
}
pub fn blinded_message(&self) -> BlindedMessage {
blind_message(self.nonce().to_message(), self.blinding_key)
}
pub fn finalize(&self, blinded_signature: BlindedSignature) -> SpendableNote {
SpendableNote {
signature: unblind_signature(self.blinding_key, blinded_signature),
spend_key: self.spend_key,
}
}
}