mirror of
https://github.com/aljazceru/cdk.git
synced 2026-02-05 05:06:14 +01:00
feat(wallet): verify token dleq and p2pk conditions
This commit is contained in:
@@ -49,7 +49,7 @@ impl MemoryLocalStore {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
#[async_trait]
|
||||
impl LocalStore for MemoryLocalStore {
|
||||
async fn add_mint(
|
||||
&self,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -51,7 +51,7 @@ impl RedbLocalStore {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
#[async_trait]
|
||||
impl LocalStore for RedbLocalStore {
|
||||
async fn add_mint(
|
||||
&self,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user