mirror of
https://github.com/aljazceru/cdk.git
synced 2025-12-20 14:14:49 +01:00
feat: signature on mint witness
This commit is contained in:
921
Cargo.lock
generated
921
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -160,6 +160,7 @@ pub async fn mint_proofs(
|
||||
amount,
|
||||
unit: CurrencyUnit::Sat,
|
||||
description,
|
||||
pubkey: None,
|
||||
};
|
||||
|
||||
let mint_quote = wallet_client.post_mint_quote(request).await?;
|
||||
@@ -192,6 +193,7 @@ pub async fn mint_proofs(
|
||||
let request = MintBolt11Request {
|
||||
quote: mint_quote.quote,
|
||||
outputs: premint_secrets.blinded_messages(),
|
||||
signature: None,
|
||||
};
|
||||
|
||||
let mint_response = wallet_client.post_mint(request).await?;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{bail, Result};
|
||||
use bip39::Mnemonic;
|
||||
use cdk::amount::SplitTarget;
|
||||
use cdk::cdk_database::WalletMemoryDatabase;
|
||||
use cdk::nuts::{
|
||||
CurrencyUnit, MeltBolt11Request, MeltQuoteState, MintQuoteState, NotificationPayload,
|
||||
PreMintSecrets, State,
|
||||
CurrencyUnit, MeltBolt11Request, MeltQuoteState, MintBolt11Request, MintQuoteState,
|
||||
NotificationPayload, PreMintSecrets, SecretKey, State,
|
||||
};
|
||||
use cdk::wallet::client::{HttpClient, MintConnector};
|
||||
use cdk::wallet::{Wallet, WalletSubscription};
|
||||
@@ -376,6 +376,105 @@ async fn test_fake_melt_change_in_quote() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
async fn test_fake_mint_with_witness() -> Result<()> {
|
||||
let wallet = Wallet::new(
|
||||
MINT_URL,
|
||||
CurrencyUnit::Sat,
|
||||
Arc::new(WalletMemoryDatabase::default()),
|
||||
&Mnemonic::generate(12)?.to_seed_normalized(""),
|
||||
None,
|
||||
)?;
|
||||
let mint_quote = wallet.mint_quote(100.into(), None).await?;
|
||||
|
||||
wait_for_mint_to_be_paid(&wallet, &mint_quote.id).await?;
|
||||
|
||||
let mint_amount = wallet
|
||||
.mint(&mint_quote.id, SplitTarget::default(), None)
|
||||
.await?;
|
||||
|
||||
assert!(mint_amount == 100.into());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
async fn test_fake_mint_without_witness() -> Result<()> {
|
||||
let wallet = Wallet::new(
|
||||
MINT_URL,
|
||||
CurrencyUnit::Sat,
|
||||
Arc::new(WalletMemoryDatabase::default()),
|
||||
&Mnemonic::generate(12)?.to_seed_normalized(""),
|
||||
None,
|
||||
)?;
|
||||
|
||||
let mint_quote = wallet.mint_quote(100.into(), None).await?;
|
||||
|
||||
wait_for_mint_to_be_paid(&wallet, &mint_quote.id).await?;
|
||||
|
||||
let http_client = HttpClient::new(MINT_URL.parse()?);
|
||||
|
||||
let active_keyset_id = wallet.get_active_mint_keyset().await?.id;
|
||||
|
||||
let premint_secrets =
|
||||
PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::default()).unwrap();
|
||||
|
||||
let request = MintBolt11Request {
|
||||
quote: mint_quote.id,
|
||||
outputs: premint_secrets.blinded_messages(),
|
||||
signature: None,
|
||||
};
|
||||
|
||||
let response = http_client.post_mint(request.clone()).await;
|
||||
|
||||
match response {
|
||||
Err(cdk::error::Error::SignatureMissingOrInvalid) => Ok(()),
|
||||
Err(err) => bail!("Wrong mint response for minting without witness: {}", err),
|
||||
Ok(_) => bail!("Minting should not have succeed without a witness"),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Rewrite this test to include witness wrong
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
async fn test_fake_mint_with_wrong_witness() -> Result<()> {
|
||||
let wallet = Wallet::new(
|
||||
MINT_URL,
|
||||
CurrencyUnit::Sat,
|
||||
Arc::new(WalletMemoryDatabase::default()),
|
||||
&Mnemonic::generate(12)?.to_seed_normalized(""),
|
||||
None,
|
||||
)?;
|
||||
|
||||
let mint_quote = wallet.mint_quote(100.into(), None).await?;
|
||||
|
||||
wait_for_mint_to_be_paid(&wallet, &mint_quote.id).await?;
|
||||
|
||||
let http_client = HttpClient::new(MINT_URL.parse()?);
|
||||
|
||||
let active_keyset_id = wallet.get_active_mint_keyset().await?.id;
|
||||
|
||||
let premint_secrets =
|
||||
PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::default()).unwrap();
|
||||
|
||||
let mut request = MintBolt11Request {
|
||||
quote: mint_quote.id,
|
||||
outputs: premint_secrets.blinded_messages(),
|
||||
signature: None,
|
||||
};
|
||||
|
||||
let secret_key = SecretKey::generate();
|
||||
|
||||
request.sign(secret_key)?;
|
||||
|
||||
let response = http_client.post_mint(request.clone()).await;
|
||||
|
||||
match response {
|
||||
Err(cdk::error::Error::SignatureMissingOrInvalid) => Ok(()),
|
||||
Err(err) => bail!("Wrong mint response for minting without witness: {}", err),
|
||||
Ok(_) => bail!("Minting should not have succeed without a witness"),
|
||||
}
|
||||
}
|
||||
|
||||
// Keep polling the state of the mint quote id until it's paid
|
||||
async fn wait_for_mint_to_be_paid(wallet: &Wallet, mint_quote_id: &str) -> Result<()> {
|
||||
let mut subscription = wallet
|
||||
|
||||
@@ -78,6 +78,7 @@ async fn mint_proofs(
|
||||
amount,
|
||||
unix_time() + 36000,
|
||||
request_lookup.to_string(),
|
||||
None,
|
||||
);
|
||||
|
||||
mint.localstore.add_mint_quote(quote.clone()).await?;
|
||||
@@ -90,6 +91,7 @@ async fn mint_proofs(
|
||||
let mint_request = MintBolt11Request {
|
||||
quote: quote.id,
|
||||
outputs: premint.blinded_messages(),
|
||||
signature: None,
|
||||
};
|
||||
|
||||
let after_mint = mint.process_mint_request(mint_request).await?;
|
||||
|
||||
@@ -381,11 +381,16 @@ async fn test_cached_mint() -> Result<()> {
|
||||
let premint_secrets =
|
||||
PreMintSecrets::random(active_keyset_id, 31.into(), &SplitTarget::default()).unwrap();
|
||||
|
||||
let request = MintBolt11Request {
|
||||
let mut request = MintBolt11Request {
|
||||
quote: quote.id,
|
||||
outputs: premint_secrets.blinded_messages(),
|
||||
signature: None,
|
||||
};
|
||||
|
||||
let secret_key = quote.secret_key;
|
||||
|
||||
request.sign(secret_key.expect("Secret key on quote"))?;
|
||||
|
||||
let response = http_client.post_mint(request.clone()).await?;
|
||||
let response1 = http_client.post_mint(request).await?;
|
||||
|
||||
|
||||
@@ -210,6 +210,7 @@ impl From<V1MintQuote> for MintQuote {
|
||||
state: quote.state,
|
||||
expiry: quote.expiry,
|
||||
request_lookup_id: Bolt11Invoice::from_str("e.request).unwrap().to_string(),
|
||||
pubkey: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE mint_quote ADD pubkey TEXT;
|
||||
@@ -207,8 +207,8 @@ WHERE active = 1
|
||||
let res = sqlx::query(
|
||||
r#"
|
||||
INSERT OR REPLACE INTO mint_quote
|
||||
(id, mint_url, amount, unit, request, state, expiry, request_lookup_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?);
|
||||
(id, mint_url, amount, unit, request, state, expiry, request_lookup_id, pubkey)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||
"#,
|
||||
)
|
||||
.bind(quote.id.to_string())
|
||||
@@ -219,6 +219,7 @@ VALUES (?, ?, ?, ?, ?, ?, ?, ?);
|
||||
.bind(quote.state.to_string())
|
||||
.bind(quote.expiry as i64)
|
||||
.bind(quote.request_lookup_id)
|
||||
.bind(quote.pubkey.map(|p| p.to_string()))
|
||||
.execute(&mut transaction)
|
||||
.await;
|
||||
|
||||
@@ -1265,6 +1266,7 @@ fn sqlite_row_to_mint_quote(row: SqliteRow) -> Result<MintQuote, Error> {
|
||||
let row_expiry: i64 = row.try_get("expiry").map_err(Error::from)?;
|
||||
let row_request_lookup_id: Option<String> =
|
||||
row.try_get("request_lookup_id").map_err(Error::from)?;
|
||||
let row_pubkey: Option<String> = row.try_get("pubkey").map_err(Error::from)?;
|
||||
|
||||
let request_lookup_id = match row_request_lookup_id {
|
||||
Some(id) => id,
|
||||
@@ -1274,6 +1276,10 @@ fn sqlite_row_to_mint_quote(row: SqliteRow) -> Result<MintQuote, Error> {
|
||||
},
|
||||
};
|
||||
|
||||
let pubkey = row_pubkey
|
||||
.map(|key| PublicKey::from_str(&key))
|
||||
.transpose()?;
|
||||
|
||||
Ok(MintQuote {
|
||||
id: row_id.into_uuid(),
|
||||
mint_url: MintUrl::from_str(&row_mint_url)?,
|
||||
@@ -1283,6 +1289,7 @@ fn sqlite_row_to_mint_quote(row: SqliteRow) -> Result<MintQuote, Error> {
|
||||
state: MintQuoteState::from_str(&row_state).map_err(Error::from)?,
|
||||
expiry: row_expiry as u64,
|
||||
request_lookup_id,
|
||||
pubkey,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE mint_quote ADD secret_key TEXT;
|
||||
@@ -10,7 +10,7 @@ use cdk::cdk_database::{self, WalletDatabase};
|
||||
use cdk::mint_url::MintUrl;
|
||||
use cdk::nuts::{
|
||||
CurrencyUnit, Id, KeySetInfo, Keys, MeltQuoteState, MintInfo, MintQuoteState, Proof, PublicKey,
|
||||
SpendingConditions, State,
|
||||
SecretKey, SpendingConditions, State,
|
||||
};
|
||||
use cdk::secret::Secret;
|
||||
use cdk::types::ProofInfo;
|
||||
@@ -347,8 +347,8 @@ WHERE id=?
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT OR REPLACE INTO mint_quote
|
||||
(id, mint_url, amount, unit, request, state, expiry)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?);
|
||||
(id, mint_url, amount, unit, request, state, expiry, secret_key)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?);
|
||||
"#,
|
||||
)
|
||||
.bind(quote.id.to_string())
|
||||
@@ -358,6 +358,7 @@ VALUES (?, ?, ?, ?, ?, ?, ?);
|
||||
.bind(quote.request)
|
||||
.bind(quote.state.to_string())
|
||||
.bind(quote.expiry as i64)
|
||||
.bind(quote.secret_key.map(|p| p.to_string()))
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(Error::from)?;
|
||||
@@ -832,9 +833,14 @@ fn sqlite_row_to_mint_quote(row: &SqliteRow) -> Result<MintQuote, Error> {
|
||||
let row_request: String = row.try_get("request").map_err(Error::from)?;
|
||||
let row_state: String = row.try_get("state").map_err(Error::from)?;
|
||||
let row_expiry: i64 = row.try_get("expiry").map_err(Error::from)?;
|
||||
let row_secret: Option<String> = row.try_get("secret_key").map_err(Error::from)?;
|
||||
|
||||
let state = MintQuoteState::from_str(&row_state)?;
|
||||
|
||||
let secret_key = row_secret
|
||||
.map(|key| SecretKey::from_str(&key))
|
||||
.transpose()?;
|
||||
|
||||
Ok(MintQuote {
|
||||
id: row_id,
|
||||
mint_url: MintUrl::from_str(&row_mint_url)?,
|
||||
@@ -843,6 +849,7 @@ fn sqlite_row_to_mint_quote(row: &SqliteRow) -> Result<MintQuote, Error> {
|
||||
request: row_request,
|
||||
state,
|
||||
expiry: row_expiry as u64,
|
||||
secret_key,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,9 @@ pub enum Error {
|
||||
/// Amount overflow
|
||||
#[error("Amount Overflow")]
|
||||
AmountOverflow,
|
||||
/// Witness missing or invalid
|
||||
#[error("Signature missing or invalid")]
|
||||
SignatureMissingOrInvalid,
|
||||
|
||||
// Mint Errors
|
||||
/// Minting is disabled
|
||||
@@ -176,7 +179,7 @@ pub enum Error {
|
||||
/// Parse int error
|
||||
#[error(transparent)]
|
||||
ParseInt(#[from] std::num::ParseIntError),
|
||||
/// Parse Url Error
|
||||
/// Parse 9rl Error
|
||||
#[error(transparent)]
|
||||
UrlParseError(#[from] url::ParseError),
|
||||
/// Utf8 parse error
|
||||
@@ -239,6 +242,9 @@ pub enum Error {
|
||||
/// NUT18 Error
|
||||
#[error(transparent)]
|
||||
NUT18(#[from] crate::nuts::nut18::Error),
|
||||
/// NUT20 Error
|
||||
#[error(transparent)]
|
||||
NUT20(#[from] crate::nuts::nut20::Error),
|
||||
/// Database Error
|
||||
#[cfg(any(feature = "wallet", feature = "mint"))]
|
||||
#[error(transparent)]
|
||||
@@ -373,6 +379,11 @@ impl From<Error> for ErrorResponse {
|
||||
error: Some(err.to_string()),
|
||||
detail: None,
|
||||
},
|
||||
Error::NUT20(err) => ErrorResponse {
|
||||
code: ErrorCode::WitnessMissingOrInvalid,
|
||||
error: Some(err.to_string()),
|
||||
detail: None,
|
||||
},
|
||||
_ => ErrorResponse {
|
||||
code: ErrorCode::Unknown(9999),
|
||||
error: Some(err.to_string()),
|
||||
@@ -402,6 +413,7 @@ impl From<ErrorResponse> for Error {
|
||||
Self::AmountOutofLimitRange(Amount::default(), Amount::default(), Amount::default())
|
||||
}
|
||||
ErrorCode::TokenPending => Self::TokenPending,
|
||||
ErrorCode::WitnessMissingOrInvalid => Self::SignatureMissingOrInvalid,
|
||||
_ => Self::UnknownErrorResponse(err.to_string()),
|
||||
}
|
||||
}
|
||||
@@ -443,6 +455,8 @@ pub enum ErrorCode {
|
||||
TransactionUnbalanced,
|
||||
/// Amount outside of allowed range
|
||||
AmountOutofLimitRange,
|
||||
/// Witness missing or invalid
|
||||
WitnessMissingOrInvalid,
|
||||
/// Unknown error code
|
||||
Unknown(u16),
|
||||
}
|
||||
@@ -467,6 +481,7 @@ impl ErrorCode {
|
||||
20005 => Self::QuotePending,
|
||||
20006 => Self::InvoiceAlreadyPaid,
|
||||
20007 => Self::QuoteExpired,
|
||||
20008 => Self::WitnessMissingOrInvalid,
|
||||
_ => Self::Unknown(code),
|
||||
}
|
||||
}
|
||||
@@ -490,6 +505,7 @@ impl ErrorCode {
|
||||
Self::QuotePending => 20005,
|
||||
Self::InvoiceAlreadyPaid => 20006,
|
||||
Self::QuoteExpired => 20007,
|
||||
Self::WitnessMissingOrInvalid => 20008,
|
||||
Self::Unknown(code) => *code,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,8 @@ impl MintBuilder {
|
||||
.nut10(true)
|
||||
.nut11(true)
|
||||
.nut12(true)
|
||||
.nut14(true);
|
||||
.nut14(true)
|
||||
.nut20(true);
|
||||
|
||||
builder.mint_info.nuts = nuts;
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@ impl Mint {
|
||||
amount,
|
||||
unit,
|
||||
description,
|
||||
pubkey,
|
||||
} = mint_quote_request;
|
||||
|
||||
self.check_mint_request_acceptable(amount, &unit)?;
|
||||
@@ -105,6 +106,7 @@ impl Mint {
|
||||
amount,
|
||||
create_invoice_response.expiry.unwrap_or(0),
|
||||
create_invoice_response.request_lookup_id.clone(),
|
||||
pubkey,
|
||||
);
|
||||
|
||||
tracing::debug!(
|
||||
@@ -150,6 +152,7 @@ impl Mint {
|
||||
request: quote.request,
|
||||
state,
|
||||
expiry: Some(quote.expiry),
|
||||
pubkey: quote.pubkey,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -281,6 +284,12 @@ impl Mint {
|
||||
MintQuoteState::Paid => (),
|
||||
}
|
||||
|
||||
// If the there is a public key provoided in mint quote request
|
||||
// verify the signature is provided for the mint request
|
||||
if let Some(pubkey) = mint_quote.pubkey {
|
||||
mint_request.verify_signature(pubkey)?;
|
||||
}
|
||||
|
||||
let blinded_messages: Vec<PublicKey> = mint_request
|
||||
.outputs
|
||||
.iter()
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::CurrencyUnit;
|
||||
use super::{CurrencyUnit, PublicKey};
|
||||
use crate::mint_url::MintUrl;
|
||||
use crate::nuts::{MeltQuoteState, MintQuoteState};
|
||||
use crate::Amount;
|
||||
@@ -27,6 +27,8 @@ pub struct MintQuote {
|
||||
pub expiry: u64,
|
||||
/// Value used by ln backend to look up state of request
|
||||
pub request_lookup_id: String,
|
||||
/// Pubkey
|
||||
pub pubkey: Option<PublicKey>,
|
||||
}
|
||||
|
||||
impl MintQuote {
|
||||
@@ -38,6 +40,7 @@ impl MintQuote {
|
||||
amount: Amount,
|
||||
expiry: u64,
|
||||
request_lookup_id: String,
|
||||
pubkey: Option<PublicKey>,
|
||||
) -> Self {
|
||||
let id = Uuid::new_v4();
|
||||
|
||||
@@ -50,6 +53,7 @@ impl MintQuote {
|
||||
state: MintQuoteState::Unpaid,
|
||||
expiry,
|
||||
request_lookup_id,
|
||||
pubkey,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ pub mod nut15;
|
||||
pub mod nut17;
|
||||
pub mod nut18;
|
||||
pub mod nut19;
|
||||
pub mod nut20;
|
||||
|
||||
pub use nut00::{
|
||||
BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod, PreMint, PreMintSecrets, Proof,
|
||||
|
||||
@@ -12,7 +12,7 @@ use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::nut00::{BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod};
|
||||
use super::MintQuoteState;
|
||||
use super::{MintQuoteState, PublicKey};
|
||||
use crate::Amount;
|
||||
|
||||
/// NUT04 Error
|
||||
@@ -35,7 +35,11 @@ pub struct MintQuoteBolt11Request {
|
||||
/// Unit wallet would like to pay with
|
||||
pub unit: CurrencyUnit,
|
||||
/// Memo to create the invoice with
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
/// NUT-19 Pubkey
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub pubkey: Option<PublicKey>,
|
||||
}
|
||||
|
||||
/// Possible states of a quote
|
||||
@@ -94,6 +98,9 @@ pub struct MintQuoteBolt11Response<Q> {
|
||||
pub state: MintQuoteState,
|
||||
/// Unix timestamp until the quote is valid
|
||||
pub expiry: Option<u64>,
|
||||
/// NUT-19 Pubkey
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub pubkey: Option<PublicKey>,
|
||||
}
|
||||
|
||||
impl<Q: ToString> MintQuoteBolt11Response<Q> {
|
||||
@@ -104,6 +111,7 @@ impl<Q: ToString> MintQuoteBolt11Response<Q> {
|
||||
request: self.request.clone(),
|
||||
state: self.state,
|
||||
expiry: self.expiry,
|
||||
pubkey: self.pubkey,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,6 +124,7 @@ impl From<MintQuoteBolt11Response<Uuid>> for MintQuoteBolt11Response<String> {
|
||||
request: value.request,
|
||||
state: value.state,
|
||||
expiry: value.expiry,
|
||||
pubkey: value.pubkey,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,6 +137,7 @@ impl From<crate::mint::MintQuote> for MintQuoteBolt11Response<Uuid> {
|
||||
request: mint_quote.request,
|
||||
state: mint_quote.state,
|
||||
expiry: Some(mint_quote.expiry),
|
||||
pubkey: mint_quote.pubkey,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -143,6 +153,9 @@ pub struct MintBolt11Request<Q> {
|
||||
/// Outputs
|
||||
#[cfg_attr(feature = "swagger", schema(max_items = 1_000))]
|
||||
pub outputs: Vec<BlindedMessage>,
|
||||
/// Signature
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub signature: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "mint")]
|
||||
@@ -153,6 +166,7 @@ impl TryFrom<MintBolt11Request<String>> for MintBolt11Request<Uuid> {
|
||||
Ok(Self {
|
||||
quote: Uuid::from_str(&value.quote)?,
|
||||
outputs: value.outputs,
|
||||
signature: value.signature,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,6 +247,10 @@ pub struct Nuts {
|
||||
#[serde(default)]
|
||||
#[serde(rename = "19")]
|
||||
pub nut19: nut19::Settings,
|
||||
/// NUT20 Settings
|
||||
#[serde(default)]
|
||||
#[serde(rename = "20")]
|
||||
pub nut20: SupportedSettings,
|
||||
}
|
||||
|
||||
impl Nuts {
|
||||
@@ -356,6 +360,14 @@ impl Nuts {
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Nut20 settings
|
||||
pub fn nut20(self, supported: bool) -> Self {
|
||||
Self {
|
||||
nut20: SupportedSettings { supported },
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check state Settings
|
||||
|
||||
151
crates/cdk/src/nuts/nut20.rs
Normal file
151
crates/cdk/src/nuts/nut20.rs
Normal file
@@ -0,0 +1,151 @@
|
||||
//! Mint Quote Signatures
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use bitcoin::secp256k1::schnorr::Signature;
|
||||
use thiserror::Error;
|
||||
|
||||
use super::{MintBolt11Request, PublicKey, SecretKey};
|
||||
|
||||
/// Nut19 Error
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
/// Signature not provided
|
||||
#[error("Signature not provided")]
|
||||
SignatureMissing,
|
||||
/// Quote signature invalid signature
|
||||
#[error("Quote signature invalid signature")]
|
||||
InvalidSignature,
|
||||
/// Nut01 error
|
||||
#[error(transparent)]
|
||||
NUT01(#[from] crate::nuts::nut01::Error),
|
||||
}
|
||||
|
||||
impl<Q> MintBolt11Request<Q>
|
||||
where
|
||||
Q: ToString,
|
||||
{
|
||||
/// Constructs the message to be signed according to NUT-20 specification.
|
||||
///
|
||||
/// The message is constructed by concatenating (as UTF-8 encoded bytes):
|
||||
/// 1. The quote ID (as UTF-8)
|
||||
/// 2. All blinded secrets (B_0 through B_n) converted to hex strings (as UTF-8)
|
||||
///
|
||||
/// Format: `quote_id || B_0 || B_1 || ... || B_n`
|
||||
/// where each component is encoded as UTF-8 bytes
|
||||
pub fn msg_to_sign(&self) -> Vec<u8> {
|
||||
// Pre-calculate capacity to avoid reallocations
|
||||
let quote_id = self.quote.to_string();
|
||||
let capacity = quote_id.len() + (self.outputs.len() * 66);
|
||||
let mut msg = Vec::with_capacity(capacity);
|
||||
msg.append(&mut quote_id.clone().into_bytes()); // String.into_bytes() produces UTF-8
|
||||
for output in &self.outputs {
|
||||
// to_hex() creates a hex string, into_bytes() converts it to UTF-8 bytes
|
||||
msg.append(&mut output.blinded_secret.to_hex().into_bytes());
|
||||
}
|
||||
msg
|
||||
}
|
||||
|
||||
/// Sign [`MintBolt11Request`]
|
||||
pub fn sign(&mut self, secret_key: SecretKey) -> Result<(), Error> {
|
||||
let msg = self.msg_to_sign();
|
||||
|
||||
let signature: Signature = secret_key.sign(&msg)?;
|
||||
|
||||
self.signature = Some(signature.to_string());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Verify signature on [`MintBolt11Request`]
|
||||
pub fn verify_signature(&self, pubkey: PublicKey) -> Result<(), Error> {
|
||||
let signature = self.signature.as_ref().ok_or(Error::SignatureMissing)?;
|
||||
|
||||
let signature = Signature::from_str(signature).map_err(|_| Error::InvalidSignature)?;
|
||||
|
||||
let msg_to_sign = self.msg_to_sign();
|
||||
|
||||
pubkey.verify(&msg_to_sign, &signature)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_msg_to_sign() {
|
||||
let request: MintBolt11Request<String> = serde_json::from_str(r#"{"quote":"9d745270-1405-46de-b5c5-e2762b4f5e00","outputs":[{"amount":1,"id":"00456a94ab4e1c46","B_":"0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834"},{"amount":1,"id":"00456a94ab4e1c46","B_":"032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4"},{"amount":1,"id":"00456a94ab4e1c46","B_":"033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"}],"signature":"cb2b8e7ea69362dfe2a07093f2bbc319226db33db2ef686c940b5ec976bcbfc78df0cd35b3e998adf437b09ee2c950bd66dfe9eb64abd706e43ebc7c669c36c3"}"#).unwrap();
|
||||
|
||||
// let expected_msg_to_sign = "9d745270-1405-46de-b5c5-e2762b4f5e000342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c31102be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b5302209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79";
|
||||
|
||||
let expected_msg_to_sign = [
|
||||
57, 100, 55, 52, 53, 50, 55, 48, 45, 49, 52, 48, 53, 45, 52, 54, 100, 101, 45, 98, 53,
|
||||
99, 53, 45, 101, 50, 55, 54, 50, 98, 52, 102, 53, 101, 48, 48, 48, 51, 52, 50, 101, 53,
|
||||
98, 99, 99, 55, 55, 102, 53, 98, 50, 97, 51, 99, 50, 97, 102, 98, 52, 48, 98, 98, 53,
|
||||
57, 49, 97, 49, 101, 50, 55, 100, 97, 56, 51, 99, 100, 100, 99, 57, 54, 56, 97, 98,
|
||||
100, 99, 48, 101, 99, 52, 57, 48, 52, 50, 48, 49, 97, 50, 48, 49, 56, 51, 52, 48, 51,
|
||||
50, 102, 100, 51, 99, 52, 100, 99, 52, 57, 97, 50, 56, 52, 52, 97, 56, 57, 57, 57, 56,
|
||||
100, 53, 101, 57, 100, 53, 98, 48, 102, 48, 98, 48, 48, 100, 100, 101, 57, 51, 49, 48,
|
||||
48, 54, 51, 97, 99, 98, 56, 97, 57, 50, 101, 50, 102, 100, 97, 102, 97, 52, 49, 50, 54,
|
||||
100, 52, 48, 51, 51, 98, 54, 102, 100, 101, 53, 48, 98, 54, 97, 48, 100, 102, 101, 54,
|
||||
49, 97, 100, 49, 52, 56, 102, 102, 102, 49, 54, 55, 97, 100, 57, 99, 102, 56, 51, 48,
|
||||
56, 100, 101, 100, 53, 102, 54, 102, 54, 98, 50, 102, 101, 48, 48, 48, 97, 48, 51, 54,
|
||||
99, 52, 54, 52, 99, 51, 49, 49, 48, 50, 98, 101, 53, 97, 53, 53, 102, 48, 51, 101, 53,
|
||||
99, 48, 97, 97, 101, 97, 55, 55, 53, 57, 53, 100, 53, 55, 52, 98, 99, 101, 57, 50, 99,
|
||||
54, 100, 53, 55, 97, 50, 97, 48, 102, 98, 50, 98, 53, 57, 53, 53, 99, 48, 98, 56, 55,
|
||||
101, 52, 53, 50, 48, 101, 48, 54, 98, 53, 51, 48, 50, 50, 48, 57, 102, 99, 50, 56, 55,
|
||||
51, 102, 50, 56, 53, 50, 49, 99, 98, 100, 100, 101, 55, 102, 55, 98, 51, 98, 98, 49,
|
||||
53, 50, 49, 48, 48, 50, 52, 54, 51, 102, 53, 57, 55, 57, 54, 56, 54, 102, 100, 49, 53,
|
||||
54, 102, 50, 51, 102, 101, 54, 97, 56, 97, 97, 50, 98, 55, 57,
|
||||
]
|
||||
.to_vec();
|
||||
|
||||
let request_msg_to_sign = request.msg_to_sign();
|
||||
|
||||
assert_eq!(expected_msg_to_sign, request_msg_to_sign);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_signature() {
|
||||
let pubkey = PublicKey::from_hex(
|
||||
"03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let request: MintBolt11Request<Uuid> = serde_json::from_str(r#"{"quote":"9d745270-1405-46de-b5c5-e2762b4f5e00","outputs":[{"amount":1,"id":"00456a94ab4e1c46","B_":"0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834"},{"amount":1,"id":"00456a94ab4e1c46","B_":"032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4"},{"amount":1,"id":"00456a94ab4e1c46","B_":"033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"}], "signature": "d4b386f21f7aa7172f0994ee6e4dd966539484247ea71c99b81b8e09b1bb2acbc0026a43c221fd773471dc30d6a32b04692e6837ddaccf0830a63128308e4ee0"}"#).unwrap();
|
||||
|
||||
assert!(request.verify_signature(pubkey).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mint_request_signature() {
|
||||
let mut request: MintBolt11Request<String> = serde_json::from_str(r#"{"quote":"9d745270-1405-46de-b5c5-e2762b4f5e00","outputs":[{"amount":1,"id":"00456a94ab4e1c46","B_":"0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834"},{"amount":1,"id":"00456a94ab4e1c46","B_":"032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4"},{"amount":1,"id":"00456a94ab4e1c46","B_":"033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"}]}"#).unwrap();
|
||||
|
||||
let secret =
|
||||
SecretKey::from_hex("50d7fd7aa2b2fe4607f41f4ce6f8794fc184dd47b8cdfbe4b3d1249aa02d35aa")
|
||||
.unwrap();
|
||||
|
||||
request.sign(secret.clone()).unwrap();
|
||||
|
||||
assert!(request.verify_signature(secret.public_key()).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_signature() {
|
||||
let pubkey = PublicKey::from_hex(
|
||||
"03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let request: MintBolt11Request<String> = serde_json::from_str(r#"{"quote":"9d745270-1405-46de-b5c5-e2762b4f5e00","outputs":[{"amount":1,"id":"00456a94ab4e1c46","B_":"0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834"},{"amount":1,"id":"00456a94ab4e1c46","B_":"032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4"},{"amount":1,"id":"00456a94ab4e1c46","B_":"033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"}],"signature":"cb2b8e7ea69362dfe2a07093f2bbc319226db33db2ef686c940b5ec976bcbfc78df0cd35b3e998adf437b09ee2c950bd66dfe9eb64abd706e43ebc7c669c36c3"}"#).unwrap();
|
||||
|
||||
// Signature is on a different quote id verification should fail
|
||||
assert!(request.verify_signature(pubkey).is_err());
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ use crate::dhke::construct_proofs;
|
||||
use crate::nuts::nut00::ProofsMethods;
|
||||
use crate::nuts::{
|
||||
nut12, MintBolt11Request, MintQuoteBolt11Request, MintQuoteBolt11Response, PreMintSecrets,
|
||||
SpendingConditions, State,
|
||||
SecretKey, SpendingConditions, State,
|
||||
};
|
||||
use crate::types::ProofInfo;
|
||||
use crate::util::unix_time;
|
||||
@@ -65,10 +65,13 @@ impl Wallet {
|
||||
}
|
||||
}
|
||||
|
||||
let secret_key = SecretKey::generate();
|
||||
|
||||
let request = MintQuoteBolt11Request {
|
||||
amount,
|
||||
unit: unit.clone(),
|
||||
description,
|
||||
pubkey: Some(secret_key.public_key()),
|
||||
};
|
||||
|
||||
let quote_res = self.client.post_mint_quote(request).await?;
|
||||
@@ -81,6 +84,7 @@ impl Wallet {
|
||||
request: quote_res.request,
|
||||
state: quote_res.state,
|
||||
expiry: quote_res.expiry.unwrap_or(0),
|
||||
secret_key: Some(secret_key),
|
||||
};
|
||||
|
||||
self.localstore.add_mint_quote(quote.clone()).await?;
|
||||
@@ -121,6 +125,7 @@ impl Wallet {
|
||||
let mint_quote_response = self.mint_quote_state(&mint_quote.id).await?;
|
||||
|
||||
if mint_quote_response.state == MintQuoteState::Paid {
|
||||
// TODO: Need to pass in keys here
|
||||
let amount = self
|
||||
.mint(&mint_quote.id, SplitTarget::default(), None)
|
||||
.await?;
|
||||
@@ -216,11 +221,16 @@ impl Wallet {
|
||||
)?,
|
||||
};
|
||||
|
||||
let request = MintBolt11Request {
|
||||
let mut request = MintBolt11Request {
|
||||
quote: quote_id.to_string(),
|
||||
outputs: premint_secrets.blinded_messages(),
|
||||
signature: None,
|
||||
};
|
||||
|
||||
if let Some(secret_key) = quote_info.secret_key {
|
||||
request.sign(secret_key)?;
|
||||
}
|
||||
|
||||
let mint_res = self.client.post_mint(request).await?;
|
||||
|
||||
let keys = self.get_keyset_keys(active_keyset_id).await?;
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::mint_url::MintUrl;
|
||||
use crate::nuts::{CurrencyUnit, MeltQuoteState, MintQuoteState};
|
||||
use crate::nuts::{CurrencyUnit, MeltQuoteState, MintQuoteState, SecretKey};
|
||||
use crate::Amount;
|
||||
|
||||
/// Mint Quote Info
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct MintQuote {
|
||||
/// Quote id
|
||||
pub id: String,
|
||||
@@ -23,6 +23,8 @@ pub struct MintQuote {
|
||||
pub state: MintQuoteState,
|
||||
/// Expiration time of quote
|
||||
pub expiry: u64,
|
||||
/// Secretkey for signing mint quotes [NUT-20]
|
||||
pub secret_key: Option<SecretKey>,
|
||||
}
|
||||
|
||||
/// Melt Quote Info
|
||||
|
||||
12
flake.lock
generated
12
flake.lock
generated
@@ -57,11 +57,11 @@
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1726560853,
|
||||
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -177,11 +177,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731378398,
|
||||
"narHash": "sha256-a0QWaiX8+AJ9/XBLGMDy6c90GD7HzpxKVdlFwCke5Pw=",
|
||||
"lastModified": 1731637922,
|
||||
"narHash": "sha256-6iuzRINXyPX4DfUQZIGafpJnzjFXjVRYMymB10/jFFY=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "0ae9fc2f2fe5361837d59c0bdebbda176427111e",
|
||||
"rev": "db10c66da18e816030b884388545add8cf096647",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
targets = [ "wasm32-unknown-unknown" ]; # wasm
|
||||
};
|
||||
|
||||
# Nightly for creating lock files
|
||||
# Nightly used for formatting
|
||||
nightly_toolchain = pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default.override {
|
||||
extensions = [ "rustfmt" "clippy" "rust-analyzer" ];
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user