Skip to content

Commit ceb5cff

Browse files
authored
persist recovery, check if proofs are spent, add docs for recovery (#532)
1 parent 548b23a commit ceb5cff

File tree

10 files changed

+268
-55
lines changed

10 files changed

+268
-55
lines changed

crates/bcr-ebill-api/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,11 @@ secp256k1.workspace = true
3434
bcr-wdc-webapi = { git = "https://github.com/BitcreditProtocol/wildcat", rev = "4437c3b809105df69ed459a9698cac8deb8d9210" }
3535
bcr-wdc-quote-client = { git = "https://github.com/BitcreditProtocol/wildcat", rev = "4437c3b809105df69ed459a9698cac8deb8d9210" }
3636
bcr-wdc-key-client = { git = "https://github.com/BitcreditProtocol/wildcat", rev = "4437c3b809105df69ed459a9698cac8deb8d9210" }
37+
bcr-wdc-swap-client = { git = "https://github.com/BitcreditProtocol/wildcat", rev = "4437c3b809105df69ed459a9698cac8deb8d9210" }
3738
cashu = { version = "0.9", default-features = false }
3839
rand = { version = "0.8" }
40+
hex = { version = "0.4" }
41+
3942

4043
[target.'cfg(target_arch = "wasm32")'.dependencies]
4144
reqwest = { workspace = true, features = ["json"] }

crates/bcr-ebill-api/src/external/mint.rs

Lines changed: 93 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::str::FromStr;
22

3+
use crate::{constants::CURRENCY_CRSAT, util};
34
use async_trait::async_trait;
45
use bcr_ebill_core::{
56
PostalAddress, ServiceTraitBounds,
@@ -9,8 +10,9 @@ use bcr_ebill_core::{
910
};
1011
use bcr_wdc_key_client::KeyClient;
1112
use bcr_wdc_quote_client::QuoteClient;
13+
use bcr_wdc_swap_client::SwapClient;
1214
use bcr_wdc_webapi::quotes::{BillInfo, ResolveOffer, StatusReply};
13-
use cashu::{nut00 as cdk00, nut01 as cdk01, nut02 as cdk02};
15+
use cashu::{ProofsMethods, State, nut00 as cdk00, nut01 as cdk01, nut02 as cdk02};
1416
use thiserror::Error;
1517

1618
/// Generic result type
@@ -43,34 +45,51 @@ pub enum Error {
4345
/// all errors originating from invalid keyset ids
4446
#[error("External Mint Invalid KeySet Id Error")]
4547
InvalidKeySetId,
48+
/// all errors originating from invalid tokens
49+
#[error("External Mint Invalid Token Error")]
50+
InvalidToken,
51+
/// all errors originating from tokens and mints not matching
52+
#[error("External Mint Token and Mint don't match Error")]
53+
TokenAndMintDontMatch,
4654
/// all errors originating from blind message generation
4755
#[error("External Mint BlindMessage Error")]
4856
BlindMessage,
57+
/// an error constructing proofs from minting
58+
#[error("External Mint ProofConstruction Error")]
59+
ProofConstruction,
60+
/// an error minting
61+
#[error("External Mint Minting Error")]
62+
Minting,
4963
/// all errors originating from the quote client
5064
#[error("External Mint Quote Client Error")]
5165
QuoteClient,
5266
/// all errors originating from the key client
5367
#[error("External Mint Key Client Error")]
5468
KeyClient,
69+
/// all errors originating from the swap client
70+
#[error("External Mint Swap Client Error")]
71+
SwapClient,
5572
}
5673

5774
#[cfg(test)]
5875
use mockall::automock;
5976

60-
use crate::{constants::CURRENCY_CRSAT, util};
61-
6277
#[cfg_attr(test, automock)]
6378
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
6479
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
6580
pub trait MintClientApi: ServiceTraitBounds {
81+
/// Check if the given proofs were already spent
82+
async fn check_if_proofs_are_spent(&self, mint_url: &str, proofs: &str) -> Result<bool>;
6683
/// Mint and return encoded token
6784
async fn mint(
6885
&self,
6986
mint_url: &str,
7087
keyset: cdk02::KeySet,
71-
discounted_amount: u64,
7288
quote_id: &str,
7389
private_key: &str,
90+
blinded_messages: Vec<cashu::BlindedMessage>,
91+
secrets: Vec<cashu::secret::Secret>,
92+
rs: Vec<cashu::SecretKey>,
7493
) -> Result<String>;
7594
/// Check keyset info for a given keyset id with a given mint
7695
async fn get_keyset_info(&self, mint_url: &str, keyset_id: &str) -> Result<cdk02::KeySet>;
@@ -125,47 +144,86 @@ impl MintClient {
125144
);
126145
Ok(key_client)
127146
}
147+
148+
pub fn swap_client(&self, mint_url: &str) -> Result<SwapClient> {
149+
let swap_client = bcr_wdc_swap_client::SwapClient::new(
150+
reqwest::Url::parse(mint_url).map_err(|_| Error::InvalidMintUrl)?,
151+
);
152+
Ok(swap_client)
153+
}
128154
}
129155

130156
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
131157
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
132158
impl MintClientApi for MintClient {
159+
async fn check_if_proofs_are_spent(&self, mint_url: &str, proofs: &str) -> Result<bool> {
160+
let token_mint_url =
161+
cashu::MintUrl::from_str(mint_url).map_err(|_| Error::InvalidMintUrl)?;
162+
let token = cashu::Token::from_str(proofs).map_err(|_| Error::InvalidToken)?;
163+
164+
if let cashu::Token::TokenV3(token_v3) = token {
165+
if let Some(token_for_mint) = token_v3
166+
.token
167+
.into_iter()
168+
.find(|t| t.mint == token_mint_url)
169+
{
170+
let ys = token_for_mint.proofs.ys().map_err(|_| Error::PubKey)?;
171+
let proof_states =
172+
self.swap_client(mint_url)?
173+
.check_state(ys)
174+
.await
175+
.map_err(|e| {
176+
log::error!("Error checking if proofs are spent at {mint_url}: {e}");
177+
Error::SwapClient
178+
})?;
179+
// all proofs have to be spent
180+
let proofs_spent = proof_states
181+
.iter()
182+
.all(|ps| matches!(ps.state, State::Spent));
183+
Ok(proofs_spent)
184+
} else {
185+
Err(Error::InvalidToken.into())
186+
}
187+
} else {
188+
Err(Error::InvalidToken.into())
189+
}
190+
}
191+
133192
async fn mint(
134193
&self,
135194
mint_url: &str,
136195
keyset: cdk02::KeySet,
137-
discounted_amount: u64,
138196
quote_id: &str,
139197
private_key: &str,
198+
blinded_messages: Vec<cashu::BlindedMessage>,
199+
secrets: Vec<cashu::secret::Secret>,
200+
rs: Vec<cashu::SecretKey>,
140201
) -> Result<String> {
202+
let token_mint_url =
203+
cashu::MintUrl::from_str(mint_url).map_err(|_| Error::InvalidMintUrl)?;
141204
let secret_key = cdk01::SecretKey::from_hex(private_key).map_err(|_| Error::PrivateKey)?;
142205
let qid = uuid::Uuid::from_str(quote_id).map_err(|_| Error::InvalidMintRequestId)?;
143206

144-
// create blinded messages
145-
let amounts: Vec<cashu::Amount> = cashu::Amount::from(discounted_amount).split();
146-
let blinds = generate_blinds(keyset.id, &amounts)?;
147-
let blinded_messages = blinds.iter().map(|b| b.0.clone()).collect::<Vec<_>>();
148-
149207
// mint
150208
let blinded_signatures = self
151209
.key_client(mint_url)?
152210
.mint(qid, blinded_messages, secret_key)
153211
.await
154212
.map_err(|e| {
155213
log::error!("Error minting at mint {mint_url}: {e}");
156-
Error::KeyClient
214+
Error::Minting
157215
})?;
158216

159217
// create proofs
160-
let secrets = blinds.iter().map(|b| b.1.clone()).collect::<Vec<_>>();
161-
let rs = blinds.iter().map(|b| b.2.clone()).collect::<Vec<_>>();
162-
let proofs =
163-
cashu::dhke::construct_proofs(blinded_signatures, rs, secrets, &keyset.keys).unwrap();
218+
let proofs = cashu::dhke::construct_proofs(blinded_signatures, rs, secrets, &keyset.keys)
219+
.map_err(|e| {
220+
log::error!("Couldn't construct proofs for {quote_id}: {e}");
221+
Error::ProofConstruction
222+
})?;
164223

165224
// generate token from proofs
166-
let mint_url = cashu::MintUrl::from_str(mint_url).map_err(|_| Error::InvalidMintUrl)?;
167225
let token = cdk00::Token::new(
168-
mint_url,
226+
token_mint_url,
169227
proofs,
170228
None,
171229
cashu::CurrencyUnit::Custom(CURRENCY_CRSAT.into()),
@@ -276,20 +334,25 @@ impl MintClientApi for MintClient {
276334

277335
pub fn generate_blinds(
278336
keyset_id: cashu::Id,
279-
amounts: &[cashu::Amount],
280-
) -> Result<
281-
Vec<(
282-
cashu::BlindedMessage,
283-
cashu::secret::Secret,
284-
cashu::SecretKey,
285-
)>,
286-
> {
287-
let mut blinds = Vec::new();
337+
discounted_amount: u64,
338+
) -> Result<(
339+
Vec<cashu::BlindedMessage>,
340+
Vec<cashu::secret::Secret>,
341+
Vec<cashu::SecretKey>,
342+
)> {
343+
let amounts: Vec<cashu::Amount> = cashu::Amount::from(discounted_amount).split();
344+
let mut blinded_messages = Vec::with_capacity(amounts.len());
345+
let mut secrets = Vec::with_capacity(amounts.len());
346+
let mut rs = Vec::with_capacity(amounts.len());
347+
288348
for amount in amounts {
289-
let blind = generate_blind(keyset_id, *amount)?;
290-
blinds.push(blind);
349+
let blind = generate_blind(keyset_id, amount)?;
350+
blinded_messages.push(blind.0);
351+
secrets.push(blind.1);
352+
rs.push(blind.2);
291353
}
292-
Ok(blinds)
354+
355+
Ok((blinded_messages, secrets, rs))
293356
}
294357

295358
pub fn generate_blind(
@@ -300,7 +363,7 @@ pub fn generate_blind(
300363
cashu::secret::Secret,
301364
cashu::SecretKey,
302365
)> {
303-
let secret = cashu::secret::Secret::new(rand::random::<u64>().to_string());
366+
let secret = cashu::secret::Secret::new(hex::encode(rand::random::<[u8; 32]>()));
304367
let (b_, r) =
305368
cashu::dhke::blind_message(secret.as_bytes(), None).map_err(|_| Error::BlindMessage)?;
306369
Ok((cashu::BlindedMessage::new(amount, kid, b_), secret, r))

crates/bcr-ebill-api/src/service/bill_service/service.rs

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,8 @@ impl BillService {
302302
.get_offer(&mint_request.mint_request_id)
303303
.await
304304
{
305-
if offer.proofs.is_none() {
305+
// only if there are no proofs yet and proofs are not spent
306+
if offer.proofs.is_none() && !offer.proofs_spent {
306307
debug!(
307308
"Checking for keyset info for {}",
308309
&mint_request.mint_request_id
@@ -351,15 +352,39 @@ impl BillService {
351352
"Keyset found and minting for {}",
352353
&mint_request.mint_request_id
353354
);
355+
// generate blinds
356+
let (blinded_messages, secrets, rs) =
357+
external::mint::generate_blinds(
358+
keyset_info.id,
359+
offer.discounted_sum,
360+
)?;
361+
// persist recovery data in case something goes wrong after minting
362+
// getting here a second time for this offer will fail, which is OK
363+
// since it means that either minting, or proof creation failed and we can't
364+
// detect which one reliably
365+
// with the secrets and rs, we can re-create the blinded messages and get
366+
// blinded signatures from the mint using the mint, OR the recovery endpoint
367+
self.mint_store
368+
.add_recovery_data_to_offer(
369+
&mint_request.mint_request_id,
370+
&secrets
371+
.iter()
372+
.map(|s| s.to_string())
373+
.collect::<Vec<String>>(),
374+
&rs.iter().map(|r| r.to_string()).collect::<Vec<String>>(),
375+
)
376+
.await?;
354377
// mint and generate proofs
355378
let proofs = self
356379
.mint_client
357380
.mint(
358381
&mint_cfg.default_mint_url,
359382
keyset_info,
360-
offer.discounted_sum,
361383
&mint_request.mint_request_id,
362384
&private_key,
385+
blinded_messages,
386+
secrets,
387+
rs,
363388
)
364389
.await?;
365390
// store proofs on the offer
@@ -371,6 +396,40 @@ impl BillService {
371396
info!("No keyset available for {}", mint_request.mint_request_id);
372397
}
373398
};
399+
} else if offer.proofs.is_some() && !offer.proofs_spent {
400+
debug!(
401+
"Checking if proofs for {} are spent",
402+
&mint_request.mint_request_id
403+
);
404+
if let Some(proofs) = offer.proofs {
405+
match self
406+
.mint_client
407+
.check_if_proofs_are_spent(&mint_cfg.default_mint_url, &proofs)
408+
.await
409+
{
410+
Ok(spent) => {
411+
if spent {
412+
debug!(
413+
"Proofs for {} are spent - updating",
414+
&mint_request.mint_request_id
415+
);
416+
self.mint_store
417+
.set_proofs_to_spent_for_offer(
418+
&mint_request.mint_request_id,
419+
)
420+
.await?;
421+
}
422+
}
423+
Err(e) => {
424+
error!(
425+
"Could not check if proofs are spent for {}: {e}",
426+
&mint_request.mint_request_id
427+
);
428+
}
429+
}
430+
}
431+
} else {
432+
// proofs spent - nothing to do
374433
}
375434
}
376435
}

crates/bcr-ebill-api/src/tests/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,13 @@ pub mod tests {
9292
new_status: &MintRequestStatus,
9393
) -> Result<()>;
9494
async fn add_proofs_to_offer(&self, mint_request_id: &str, proofs: &str) -> Result<()>;
95+
async fn add_recovery_data_to_offer(
96+
&self,
97+
mint_request_id: &str,
98+
secrets: &[String],
99+
rs: &[String],
100+
) -> Result<()>;
101+
async fn set_proofs_to_spent_for_offer(&self, mint_request_id: &str) -> Result<()>;
95102
async fn add_offer(
96103
&self,
97104
mint_request_id: &str,

crates/bcr-ebill-core/src/mint/mod.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,22 @@ pub struct MintOffer {
4646
pub discounted_sum: u64,
4747
/// The proofs, encoded as a custom Tokenv3
4848
pub proofs: Option<String>,
49+
/// Whether the proofs were spent according to the mint
50+
pub proofs_spent: bool,
51+
/// The recovery data, if something goes wrong between minting and token generation
52+
pub recovery_data: Option<MintOfferRecoveryData>,
4953
}
5054

55+
/// Mint offer recovery data
56+
#[derive(Debug, Clone)]
57+
pub struct MintOfferRecoveryData {
58+
/// The secrets of the blinds we used
59+
pub secrets: Vec<String>,
60+
/// The rs of the blinds we used
61+
pub rs: Vec<String>,
62+
}
63+
64+
/// The state of a mint request
5165
#[derive(Debug, Clone)]
5266
pub struct MintRequestState {
5367
/// There always is a request

crates/bcr-ebill-persistence/src/constants.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ pub const DB_STATUS_ACCEPTED: &str = "status_accepted";
2828
pub const DB_MINT_NODE_ID: &str = "mint_node_id";
2929
pub const DB_MINT_REQUEST_ID: &str = "mint_request_id";
3030
pub const DB_PROOFS: &str = "proofs";
31+
pub const DB_RECOVERY_DATA: &str = "recovery_data";
32+
pub const DB_PROOFS_SPENT: &str = "proofs_spent";
3133
pub const DB_MINT_REQUESTER_NODE_ID: &str = "requester_node_id";
3234
pub const DB_SEARCH_TERM: &str = "search_term";
3335

0 commit comments

Comments
 (0)