feat(wallet): verify token dleq and p2pk conditions

This commit is contained in:
thesimplekid
2024-04-03 00:57:55 +01:00
parent 1c045ff62a
commit ed117ef3a2
5 changed files with 136 additions and 30 deletions

View File

@@ -49,7 +49,7 @@ impl MemoryLocalStore {
}
}
#[async_trait(?Send)]
#[async_trait]
impl LocalStore for MemoryLocalStore {
async fn add_mint(
&self,

View File

@@ -39,7 +39,7 @@ pub enum Error {
Serde(#[from] serde_json::Error),
}
#[async_trait(?Send)]
#[async_trait]
pub trait LocalStore {
async fn add_mint(
&self,

View File

@@ -51,7 +51,7 @@ impl RedbLocalStore {
}
}
#[async_trait(?Send)]
#[async_trait]
impl LocalStore for RedbLocalStore {
async fn add_mint(
&self,

View File

@@ -1,6 +1,7 @@
//! Cashu Wallet
use std::collections::{HashMap, HashSet};
use std::str::FromStr;
use std::sync::Arc;
use bip39::Mnemonic;
use cashu::dhke::{construct_proofs, unblind_message};
@@ -52,21 +53,29 @@ pub enum Error {
Cashu(#[from] cashu::error::Error),
#[error("Could not verify Dleq")]
CouldNotVerifyDleq,
#[error("P2PK Condition Not met `{0}`")]
P2PKConditionsNotMet(String),
#[error("Invalid Spending Conditions: `{0}`")]
InvalidSpendConditions(String),
#[error("Unknown Key")]
UnknownKey,
#[error("`{0}`")]
Custom(String),
}
#[derive(Clone, Debug)]
pub struct Wallet<C: Client, L: LocalStore> {
pub client: C,
localstore: L,
#[derive(Clone)]
pub struct Wallet {
pub client: Arc<dyn Client + Send + Sync>,
pub localstore: Arc<dyn LocalStore + Send + Sync>,
mnemonic: Option<Mnemonic>,
}
impl<C: Client, L: LocalStore> Wallet<C, L> {
pub async fn new(client: C, localstore: L, mnemonic: Option<Mnemonic>) -> Self {
impl Wallet {
pub async fn new(
client: Arc<dyn Client + Sync + Send>,
localstore: Arc<dyn LocalStore + Send + Sync>,
mnemonic: Option<Mnemonic>,
) -> Self {
Self {
mnemonic,
client,
@@ -1134,19 +1143,115 @@ impl<C: Client, L: LocalStore> Wallet<C, L> {
Ok(restored_value)
}
/// Verify all proofs in token have meet the required spend
/// Can be used to allow a wallet to accept payments offline while reducing
/// the risk of claiming back to the limits let by the spending_conditions
#[cfg(feature = "nut11")]
pub fn verify_token_p2pk(
&self,
token: &Token,
spending_conditions: P2PKConditions,
) -> Result<(), Error> {
use cashu::nuts::nut10;
if spending_conditions.refund_keys.is_some() && spending_conditions.locktime.is_none() {
warn!(
"Invalid spending conditions set: Locktime must be set if refund keys are allowed"
);
return Err(Error::InvalidSpendConditions(
"Must set locktime".to_string(),
));
}
for mint_proof in &token.token {
for proof in &mint_proof.proofs {
let secret: nut10::Secret = (&proof.secret).try_into().unwrap();
let proof_conditions: P2PKConditions = secret.try_into().unwrap();
if spending_conditions.num_sigs.ne(&proof_conditions.num_sigs) {
debug!(
"Spending condition requires: {:?} sigs proof secret specifies: {:?}",
spending_conditions.num_sigs, proof_conditions.num_sigs
);
return Err(Error::P2PKConditionsNotMet(
"Num sigs did not match spending condition".to_string(),
));
}
// Check the Proof has the required pubkeys
if proof_conditions
.pubkeys
.len()
.ne(&spending_conditions.pubkeys.len())
|| !proof_conditions
.pubkeys
.iter()
.all(|pubkey| spending_conditions.pubkeys.contains(pubkey))
{
debug!("Proof did not included Publickeys meeting condition");
return Err(Error::P2PKConditionsNotMet(
"Pubkeys in proof not allowed by spending condition".to_string(),
));
}
// If spending condition refund keys is allowed (Some(Empty Vec))
// If spending conition refund keys is allowed to restricted set of keys check
// it is one of them Check that proof locktime is > condition
// locktime
if let Some(proof_refund_keys) = proof_conditions.refund_keys {
let proof_locktime = proof_conditions.locktime.unwrap();
if let (Some(condition_refund_keys), Some(condition_locktime)) = (
&spending_conditions.refund_keys,
spending_conditions.locktime,
) {
// Proof locktime must be greater then condition locktime to ensure it
// cannot be claimed back
if proof_locktime.lt(&condition_locktime) {
return Err(Error::P2PKConditionsNotMet(
"Proof locktime less then required".to_string(),
));
}
// A non empty condition refund key list is used as a restricted set of keys
// returns are allowed to An empty list means the
// proof can be refunded to anykey set in the secret
if !condition_refund_keys.is_empty()
&& !proof_refund_keys
.iter()
.all(|refund_key| condition_refund_keys.contains(refund_key))
{
return Err(Error::P2PKConditionsNotMet(
"Refund Key not allowed".to_string(),
));
}
} else {
// Spending conditions does not allow refund keys
return Err(Error::P2PKConditionsNotMet(
"Spending condition does not allow refund keys".to_string(),
));
}
}
}
}
Ok(())
}
/// Verify all proofs in token have a valid DLEQ proof
#[cfg(feature = "nut12")]
pub async fn verify_token_dleq(&self, token: Token) -> Result<(), Error> {
pub async fn verify_token_dleq(&self, token: &Token) -> Result<(), Error> {
let mut keys_cache: HashMap<Id, Keys> = HashMap::new();
for mint_proof in token.token {
let mint_url = mint_proof.mint;
for proof in mint_proof.proofs {
for mint_proof in &token.token {
for proof in &mint_proof.proofs {
let mint_pubkey = match keys_cache.get(&proof.keyset_id) {
Some(keys) => keys.amount_key(proof.amount),
None => {
let keys = self.get_keyset_keys(&mint_url, proof.keyset_id).await?;
let keys = self.localstore.get_keys(&proof.keyset_id).await?.unwrap();
let key = keys.amount_key(proof.amount);
keys_cache.insert(proof.keyset_id, keys);

View File

@@ -85,12 +85,15 @@ impl Proof {
return Ok(());
}
if let Some(locktime) = spending_conditions.locktime {
if let (Some(locktime), Some(refund_keys)) = (
spending_conditions.locktime,
spending_conditions.refund_keys,
) {
// If lock time has passed check if refund witness signature is valid
if locktime.lt(&unix_time()) && !spending_conditions.refund_keys.is_empty() {
if locktime.lt(&unix_time()) {
if let Some(signatures) = &self.witness {
for s in &signatures.signatures {
for v in &spending_conditions.refund_keys {
for v in &refund_keys {
let sig = Signature::try_from(hex::decode(s)?.as_slice())
.map_err(|_| Error::InvalidSignature)?;
@@ -175,9 +178,8 @@ pub struct P2PKConditions {
#[serde(skip_serializing_if = "Option::is_none")]
pub locktime: Option<u64>,
pub pubkeys: Vec<VerifyingKey>,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub refund_keys: Vec<VerifyingKey>,
#[serde(skip_serializing_if = "Option::is_none")]
pub refund_keys: Option<Vec<VerifyingKey>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub num_sigs: Option<u64>,
pub sig_flag: SigFlag,
@@ -187,7 +189,7 @@ impl P2PKConditions {
pub fn new(
locktime: Option<u64>,
pubkeys: Vec<VerifyingKey>,
refund_keys: Vec<VerifyingKey>,
refund_keys: Option<Vec<VerifyingKey>>,
num_sigs: Option<u64>,
sig_flag: Option<SigFlag>,
) -> Result<Self, Error> {
@@ -241,10 +243,9 @@ impl TryFrom<P2PKConditions> for Secret {
tags.push(Tag::NSigs(num_sigs).as_vec());
}
if !refund_keys.is_empty() {
if let Some(refund_keys) = refund_keys {
tags.push(Tag::Refund(refund_keys).as_vec())
}
tags.push(Tag::SigFlag(sig_flag).as_vec());
Ok(Secret {
@@ -300,11 +301,11 @@ impl TryFrom<Secret> for P2PKConditions {
let refund_keys = if let Some(tag) = tags.get(&TagKind::Refund) {
match tag {
Tag::Refund(keys) => keys.clone(),
_ => vec![],
Tag::Refund(keys) => Some(keys.clone()),
_ => None,
}
} else {
vec![]
None
};
let sig_flag = if let Some(tag) = tags.get(&TagKind::SigFlag) {
@@ -702,10 +703,10 @@ mod tests {
)
.unwrap(),
],
refund_keys: vec![VerifyingKey::from_str(
refund_keys: Some(vec![VerifyingKey::from_str(
"033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e",
)
.unwrap()],
.unwrap()]),
num_sigs: Some(2),
sig_flag: SigFlag::SigAll,
};
@@ -742,7 +743,7 @@ mod tests {
let conditions = P2PKConditions {
locktime: Some(21),
pubkeys: vec![v_key.clone(), v_key_two, v_key_three],
refund_keys: vec![v_key],
refund_keys: Some(vec![v_key]),
num_sigs: Some(2),
sig_flag: SigFlag::SigInputs,
};