feat: nut13 restore for active keyset

This commit is contained in:
thesimplekid
2024-03-08 16:45:13 +00:00
parent 357479827d
commit c4389079f3
9 changed files with 308 additions and 59 deletions

View File

@@ -14,9 +14,10 @@ default = ["mint", "wallet", "all-nuts", "redb"]
mint = ["cashu/mint"]
wallet = ["cashu/wallet", "dep:minreq", "dep:once_cell"]
gloo = ["dep:gloo"]
all-nuts = ["nut07", "nut08", "nut10", "nut11", "nut13"]
all-nuts = ["nut07", "nut08", "nut09", "nut10", "nut11", "nut13"]
nut07 = ["cashu/nut07"]
nut08 = ["cashu/nut08"]
nut09 = ["cashu/nut09"]
nut10 = ["cashu/nut10"]
nut11 = ["cashu/nut11"]
nut13 = ["cashu/nut13"]

View File

@@ -1,6 +1,8 @@
//! gloo wasm http Client
use async_trait::async_trait;
#[cfg(feature = "nut09")]
use cashu::nuts::nut09::{RestoreRequest, RestoreResponse};
use cashu::nuts::{
BlindedMessage, MeltBolt11Request, MeltBolt11Response, MintBolt11Request, MintBolt11Response,
MintInfo, PreMintSecrets, Proof, SwapRequest, SwapResponse, *,
@@ -254,4 +256,32 @@ impl Client for HttpClient {
Err(_) => Err(Error::from_json(&res.to_string())?),
}
}
/// Restore [NUT-09]
#[cfg(feature = "nut09")]
async fn post_check_state(
&self,
mint_url: Url,
request: RestoreRequest,
) -> Result<CheckStateResponse, Error> {
let url = join_url(mint_url, &["v1", "check"])?;
let res = Request::post(url.as_str())
.json(&request)
.map_err(|err| Error::Gloo(err.to_string()))?
.send()
.await
.map_err(|err| Error::Gloo(err.to_string()))?
.json::<Value>()
.await
.map_err(|err| Error::Gloo(err.to_string()))?;
let response: Result<RestoreRequest, serde_json::Error> =
serde_json::from_value(res.clone());
match response {
Ok(res) => Ok(res),
Err(_) => Err(Error::from_json(&res.to_string())?),
}
}
}

View File

@@ -2,6 +2,8 @@
use async_trait::async_trait;
use cashu::error::ErrorResponse;
#[cfg(feature = "nut09")]
use cashu::nuts::nut09::{RestoreRequest, RestoreResponse};
#[cfg(feature = "nut07")]
use cashu::nuts::PublicKey;
use cashu::nuts::{
@@ -221,4 +223,26 @@ impl Client for HttpClient {
Err(_) => Err(ErrorResponse::from_json(&res.to_string())?.into()),
}
}
#[cfg(feature = "nut09")]
async fn post_restore(
&self,
mint_url: Url,
request: RestoreRequest,
) -> Result<RestoreResponse, Error> {
let url = join_url(mint_url, &["v1", "restore"])?;
let res = minreq::post(url)
.with_json(&request)?
.send()?
.json::<Value>()?;
let response: Result<RestoreResponse, serde_json::Error> =
serde_json::from_value(res.clone());
match response {
Ok(res) => Ok(res),
Err(_) => Err(ErrorResponse::from_json(&res.to_string())?.into()),
}
}
}

View File

@@ -2,6 +2,8 @@
use async_trait::async_trait;
use cashu::error::ErrorResponse;
#[cfg(feature = "nut09")]
use cashu::nuts::nut09::{RestoreRequest, RestoreResponse};
#[cfg(feature = "nut07")]
use cashu::nuts::CheckStateResponse;
#[cfg(feature = "nut07")]
@@ -111,6 +113,13 @@ pub trait Client {
) -> Result<CheckStateResponse, Error>;
async fn get_mint_info(&self, mint_url: Url) -> Result<MintInfo, Error>;
#[cfg(feature = "nut09")]
async fn post_restore(
&self,
mint_url: Url,
restore_request: RestoreRequest,
) -> Result<RestoreResponse, Error>;
}
#[cfg(any(not(target_arch = "wasm32"), feature = "gloo"))]

View File

@@ -6,6 +6,9 @@ use bip39::Mnemonic;
use cashu::dhke::{construct_proofs, unblind_message};
#[cfg(feature = "nut07")]
use cashu::nuts::nut07::ProofState;
use cashu::nuts::nut07::State;
#[cfg(feature = "nut09")]
use cashu::nuts::nut09::RestoreRequest;
use cashu::nuts::nut11::SigningKey;
#[cfg(feature = "nut07")]
use cashu::nuts::PublicKey;
@@ -18,7 +21,7 @@ use cashu::url::UncheckedUrl;
use cashu::{Amount, Bolt11Invoice};
use localstore::LocalStore;
use thiserror::Error;
use tracing::warn;
use tracing::{debug, warn};
use crate::client::Client;
use crate::utils::unix_time;
@@ -299,8 +302,13 @@ impl<C: Client, L: LocalStore> Wallet<C, L> {
let count = self
.localstore
.get_keyset_counter(&active_keyset_id)
.await?
.unwrap_or(0);
.await?;
let count = if let Some(count) = count {
count + 1
} else {
0
};
counter = Some(count);
PreMintSecrets::from_seed(
@@ -427,8 +435,14 @@ impl<C: Client, L: LocalStore> Wallet<C, L> {
let count = self
.localstore
.get_keyset_counter(&active_keyset_id)
.await?
.unwrap_or(0);
.await?;
let count = if let Some(count) = count {
count + 1
} else {
0
};
let premint_secrets = PreMintSecrets::from_seed(
active_keyset_id,
count,
@@ -444,11 +458,11 @@ impl<C: Client, L: LocalStore> Wallet<C, L> {
PreMintSecrets::random(active_keyset_id, desired_amount)?
};
if let (Some(amt), Some(mnemonic)) = (amount, &self.mnemonic) {
if let Some(amt) = amount {
let change_amount = proofs_total - amt;
let change_messages = if let Some(count) = counter {
PreMintSecrets::from_seed(active_keyset_id, count, mnemonic, desired_amount, false)?
let change_messages = if let (Some(count), Some(mnemonic)) = (counter, &self.mnemonic) {
PreMintSecrets::from_seed(active_keyset_id, count, mnemonic, change_amount, false)?
} else {
PreMintSecrets::random(active_keyset_id, change_amount)?
};
@@ -547,6 +561,22 @@ impl<C: Client, L: LocalStore> Wallet<C, L> {
&self.active_keys(mint_url, unit).await?.unwrap(),
)?;
let active_keyset = self.active_mint_keyset(mint_url, unit).await?;
if self.mnemonic.is_some() {
let count = self
.localstore
.get_keyset_counter(&active_keyset)
.await?
.unwrap_or(0);
let new_count = count + post_swap_proofs.len() as u64;
self.localstore
.add_keyset_counter(&active_keyset, new_count)
.await?;
}
post_swap_proofs.reverse();
for proof in post_swap_proofs {
@@ -700,8 +730,13 @@ impl<C: Client, L: LocalStore> Wallet<C, L> {
let count = self
.localstore
.get_keyset_counter(&active_keyset_id)
.await?
.unwrap_or(0);
.await?;
let count = if let Some(count) = count {
count + 1
} else {
0
};
counter = Some(count);
PreMintSecrets::from_seed(active_keyset_id, count, mnemonic, proofs_amount, true)?
@@ -930,6 +965,102 @@ impl<C: Client, L: LocalStore> Wallet<C, L> {
) -> Result<String, Error> {
Ok(Token::new(mint_url, proofs, memo, unit)?.to_string())
}
pub async fn restore(&mut self, mint_url: UncheckedUrl) -> Result<Amount, Error> {
// Check that mint is in store of mints
if self.localstore.get_mint(mint_url.clone()).await?.is_none() {
self.add_mint(mint_url.clone()).await?;
}
let active_keyset_id = &self
.active_mint_keyset(&mint_url, &CurrencyUnit::Sat)
.await?;
let keys = if let Some(keys) = self.localstore.get_keys(&active_keyset_id).await? {
keys
} else {
self.get_mint_keys(&mint_url, *active_keyset_id).await?;
self.localstore.get_keys(&active_keyset_id).await?.unwrap()
};
let mut empty_batch = 0;
let mut start_counter = 0;
let mut restored_value = Amount::ZERO;
while empty_batch.lt(&3) {
let premint_secrets = PreMintSecrets::restore_batch(
*active_keyset_id,
&self.mnemonic.clone().unwrap(),
start_counter,
start_counter + 100,
)?;
debug!(
"Attempting to restore counter {}-{} for mint {} keyset {}",
start_counter,
start_counter + 100,
mint_url,
active_keyset_id
);
let restore_request = RestoreRequest {
outputs: premint_secrets.blinded_messages(),
};
let response = self
.client
.post_restore(mint_url.clone().try_into()?, restore_request)
.await
.unwrap();
if response.signatures.is_empty() {
empty_batch += 1;
continue;
}
let premint_secrets: Vec<_> = premint_secrets
.secrets
.iter()
.filter(|p| response.outputs.contains(&p.blinded_message))
.collect();
// the response outputs and premint secrets should be the same after filtering
// blinded messages the mint did not have signatures for
assert_eq!(response.outputs.len(), premint_secrets.len());
let proofs = construct_proofs(
response.signatures,
premint_secrets.iter().map(|p| p.r.clone()).collect(),
premint_secrets.iter().map(|p| p.secret.clone()).collect(),
&keys,
)?;
self.localstore
.add_keyset_counter(active_keyset_id, start_counter + proofs.len() as u64)
.await?;
let states = self
.check_proofs_spent(mint_url.clone(), proofs.clone())
.await?;
let unspent_proofs: Vec<Proof> = proofs
.iter()
.zip(states)
.filter(|(_, state)| !state.state.eq(&State::Spent))
.map(|(p, _)| p)
.cloned()
.collect();
restored_value += unspent_proofs.iter().map(|p| p.amount).sum();
self.localstore
.add_proofs(mint_url.clone(), unspent_proofs)
.await?;
empty_batch = 0;
start_counter += 100;
}
Ok(restored_value)
}
}
/*

View File

@@ -31,10 +31,10 @@ pub struct BlindedMessage {
/// Witness
#[cfg(feature = "nut11")]
#[serde(default)]
#[serde(skip_serializing_if = "Signatures::is_empty")]
#[serde(serialize_with = "witness_serialize")]
#[serde(deserialize_with = "witness_deserialize")]
pub witness: Signatures,
#[serde(skip_serializing_if = "Option::is_none")]
//#[serde(serialize_with = "witness_serialize")]
//#[serde(deserialize_with = "witness_deserialize")]
pub witness: Option<Signatures>,
}
impl BlindedMessage {
@@ -44,7 +44,7 @@ impl BlindedMessage {
keyset_id,
b,
#[cfg(feature = "nut11")]
witness: Signatures::default(),
witness: None,
}
}
}
@@ -437,11 +437,12 @@ pub struct Proof {
pub c: PublicKey,
#[cfg(feature = "nut11")]
/// Witness
#[cfg(feature = "nut11")]
#[serde(default)]
#[serde(skip_serializing_if = "Signatures::is_empty")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(serialize_with = "witness_serialize")]
#[serde(deserialize_with = "witness_deserialize")]
pub witness: Signatures,
pub witness: Option<Signatures>,
}
impl Proof {
@@ -452,7 +453,7 @@ impl Proof {
secret,
c,
#[cfg(feature = "nut11")]
witness: Signatures::default(),
witness: None,
}
}
}

View File

@@ -17,5 +17,19 @@ pub struct RestoreResponse {
/// Outputs
pub outputs: Vec<BlindedMessage>,
/// Signatures
#[serde(rename = "promises")]
pub signatures: Vec<BlindedSignature>,
}
mod test {
#[test]
fn restore_response() {
use super::*;
let rs = r#"{"outputs":[{"B_":"0204bbffa045f28ec836117a29ea0a00d77f1d692e38cf94f72a5145bfda6d8f41","amount":0,"id":"00ffd48b8f5ecf80", "witness":null},{"B_":"025f0615ccba96f810582a6885ffdb04bd57c96dbc590f5aa560447b31258988d7","amount":0,"id":"00ffd48b8f5ecf80"}],"promises":[{"C_":"02e9701b804dc05a5294b5a580b428237a27c7ee1690a0177868016799b1761c81","amount":8,"dleq":null,"id":"00ffd48b8f5ecf80"},{"C_":"031246ee046519b15648f1b8d8ffcb8e537409c84724e148c8d6800b2e62deb795","amount":2,"dleq":null,"id":"00ffd48b8f5ecf80"}]}"#;
let res: RestoreResponse = serde_json::from_str(rs).unwrap();
println!("{:?}", res);
}
}

View File

@@ -32,14 +32,14 @@ impl Signatures {
}
}
pub fn witness_serialize<S>(x: &Signatures, s: S) -> Result<S::Ok, S::Error>
pub fn witness_serialize<S>(x: &Option<Signatures>, s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
s.serialize_str(&serde_json::to_string(x).map_err(ser::Error::custom)?)
s.serialize_str(&serde_json::to_string(&x).map_err(ser::Error::custom)?)
}
pub fn witness_deserialize<'de, D>(deserializer: D) -> Result<Signatures, D::Error>
pub fn witness_deserialize<'de, D>(deserializer: D) -> Result<Option<Signatures>, D::Error>
where
D: de::Deserializer<'de>,
{
@@ -60,22 +60,23 @@ impl Proof {
let mut valid_sigs = 0;
let msg = &self.secret.to_bytes();
if let Some(witness) = &self.witness {
for signature in &witness.signatures {
let mut pubkeys = spending_conditions.pubkeys.clone();
let data_key = VerifyingKey::from_str(&secret.secret_data.data)?;
pubkeys.push(data_key);
for v in &spending_conditions.pubkeys {
let sig = Signature::try_from(hex::decode(signature)?.as_slice())?;
for signature in &self.witness.signatures {
let mut pubkeys = spending_conditions.pubkeys.clone();
let data_key = VerifyingKey::from_str(&secret.secret_data.data)?;
pubkeys.push(data_key);
for v in &spending_conditions.pubkeys {
let sig = Signature::try_from(hex::decode(signature)?.as_slice())?;
if v.verify(msg, &sig).is_ok() {
valid_sigs += 1;
} else {
debug!(
"Could not verify signature: {} on message: {}",
hex::encode(sig.to_bytes()),
self.secret.to_string()
)
if v.verify(msg, &sig).is_ok() {
valid_sigs += 1;
} else {
debug!(
"Could not verify signature: {} on message: {}",
hex::encode(sig.to_bytes()),
self.secret.to_string()
)
}
}
}
}
@@ -84,19 +85,19 @@ impl Proof {
return Ok(());
}
println!("{:?}", spending_conditions.refund_keys);
if let Some(locktime) = spending_conditions.locktime {
// If lock time has passed check if refund witness signature is valid
if locktime.lt(&unix_time()) && !spending_conditions.refund_keys.is_empty() {
for s in &self.witness.signatures {
for v in &spending_conditions.refund_keys {
let sig = Signature::try_from(hex::decode(s)?.as_slice())
.map_err(|_| Error::InvalidSignature)?;
if let Some(signatures) = &self.witness {
for s in &signatures.signatures {
for v in &spending_conditions.refund_keys {
let sig = Signature::try_from(hex::decode(s)?.as_slice())
.map_err(|_| Error::InvalidSignature)?;
// As long as there is one valid refund signature it can be spent
if v.verify(msg, &sig).is_ok() {
return Ok(());
// As long as there is one valid refund signature it can be spent
if v.verify(msg, &sig).is_ok() {
return Ok(());
}
}
}
}
@@ -112,6 +113,8 @@ impl Proof {
let signature = secret_key.sign(msg_to_sign);
self.witness
.as_mut()
.unwrap_or(&mut Signatures::default())
.signatures
.push(hex::encode(signature.to_bytes()));
@@ -126,8 +129,11 @@ impl BlindedMessage {
let signature = secret_key.sign(&msg_to_sign);
self.witness
.as_mut()
.unwrap_or(&mut Signatures::default())
.signatures
.push(hex::encode(signature.to_bytes()));
Ok(())
}
@@ -137,19 +143,21 @@ impl BlindedMessage {
required_sigs: u64,
) -> Result<(), Error> {
let mut valid_sigs = 0;
for signature in &self.witness.signatures {
for v in pubkeys {
let msg = &self.b.to_bytes();
let sig = Signature::try_from(hex::decode(signature)?.as_slice())?;
if let Some(witness) = &self.witness {
for signature in &witness.signatures {
for v in pubkeys {
let msg = &self.b.to_bytes();
let sig = Signature::try_from(hex::decode(signature)?.as_slice())?;
if v.verify(msg, &sig).is_ok() {
valid_sigs += 1;
} else {
debug!(
"Could not verify signature: {} on message: {}",
hex::encode(sig.to_bytes()),
self.b.to_string()
)
if v.verify(msg, &sig).is_ok() {
valid_sigs += 1;
} else {
debug!(
"Could not verify signature: {} on message: {}",
hex::encode(sig.to_bytes()),
self.b.to_string()
)
}
}
}
}
@@ -749,7 +757,7 @@ mod tests {
"02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904",
)
.unwrap(),
witness: Signatures { signatures: vec![] },
witness: Some(Signatures { signatures: vec![] }),
};
proof.sign_p2pk(secret_key).unwrap();

View File

@@ -84,6 +84,37 @@ mod wallet {
Ok(pre_mint_secrets)
}
/// Generate blinded messages from predetermined secrets and blindings
/// factor
pub fn restore_batch(
keyset_id: Id,
mnemonic: &Mnemonic,
start_count: u64,
end_count: u64,
) -> Result<Self, wallet::Error> {
let mut pre_mint_secrets = PreMintSecrets::default();
for i in start_count..end_count {
let secret = Secret::from_seed(mnemonic, keyset_id, i)?;
let blinding_factor = SecretKey::from_seed(mnemonic, keyset_id, i)?;
let (blinded, r) = blind_message(&secret.to_bytes(), Some(blinding_factor.into()))?;
let blinded_message = BlindedMessage::new(Amount::ZERO, keyset_id, blinded);
let pre_mint = PreMint {
blinded_message,
secret: secret.clone(),
r: r.into(),
amount: Amount::ZERO,
};
pre_mint_secrets.secrets.push(pre_mint);
}
Ok(pre_mint_secrets)
}
}
}