feat: signature on mint witness

This commit is contained in:
thesimplekid
2024-11-08 19:58:07 +00:00
parent 4c70dcb15a
commit 003a8f1b47
22 changed files with 1013 additions and 301 deletions

921
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -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?;

View File

@@ -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

View File

@@ -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?;

View File

@@ -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?;

View File

@@ -210,6 +210,7 @@ impl From<V1MintQuote> for MintQuote {
state: quote.state,
expiry: quote.expiry,
request_lookup_id: Bolt11Invoice::from_str(&quote.request).unwrap().to_string(),
pubkey: None,
}
}
}

View File

@@ -0,0 +1 @@
ALTER TABLE mint_quote ADD pubkey TEXT;

View File

@@ -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,
})
}

View File

@@ -0,0 +1 @@
ALTER TABLE mint_quote ADD secret_key TEXT;

View File

@@ -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,
})
}

View File

@@ -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,
}
}

View File

@@ -46,7 +46,8 @@ impl MintBuilder {
.nut10(true)
.nut11(true)
.nut12(true)
.nut14(true);
.nut14(true)
.nut20(true);
builder.mint_info.nuts = nuts;

View File

@@ -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()

View File

@@ -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,
}
}
}

View File

@@ -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,

View File

@@ -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,
})
}
}

View File

@@ -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

View 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());
}
}

View File

@@ -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?;

View File

@@ -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
View File

@@ -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": {

View File

@@ -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" ];
});