feat: NUT14

This commit is contained in:
thesimplekid
2024-04-27 17:07:36 +01:00
parent a5a50f281c
commit db14c11714
16 changed files with 756 additions and 457 deletions

View File

@@ -26,6 +26,7 @@ CDK is a collection of rust crates for [Cashu](https://github.com/cashubtc) wall
- :heavy_check_mark: [NUT-11](https://github.com/cashubtc/nuts/blob/main/11.md)
- :heavy_check_mark: [NUT-12](https://github.com/cashubtc/nuts/blob/main/12.md)
- :heavy_check_mark: [NUT-13](https://github.com/cashubtc/nuts/blob/main/13.md)
- :heavy_check_mark: [NUT-14](https://github.com/cashubtc/nuts/blob/main/14.md)
## License

View File

@@ -234,7 +234,6 @@ impl HttpClient {
let value = res.json::<Value>().await?;
let response: Result<SwapResponse, serde_json::Error> =
serde_json::from_value(value.clone());
match response {
Ok(res) => Ok(res),
Err(_) => Err(ErrorResponse::from_json(&value.to_string())?.into()),

View File

@@ -9,6 +9,7 @@ use thiserror::Error;
use tokio::sync::RwLock;
use tracing::{debug, error, info};
use self::nut11::enforce_sig_flag;
use crate::cdk_database::{self, MintDatabase};
use crate::dhke::{hash_to_curve, sign_message, verify_message};
use crate::error::ErrorResponse;
@@ -49,6 +50,8 @@ pub enum Error {
NUT11(#[from] crate::nuts::nut11::Error),
#[error(transparent)]
Nut12(#[from] crate::nuts::nut12::Error),
#[error(transparent)]
Nut14(#[from] crate::nuts::nut14::Error),
/// Database Error
#[error(transparent)]
Database(#[from] crate::cdk_database::Error),
@@ -439,6 +442,15 @@ impl Mint {
return Err(Error::MultipleUnits);
}
let (sig_flag, pubkeys) = enforce_sig_flag(swap_request.inputs.clone());
if sig_flag.eq(&SigFlag::SigAll) {
let pubkeys = pubkeys.into_iter().collect();
for blinded_messaage in &swap_request.outputs {
blinded_messaage.verify_p2pk(&pubkeys, 1)?;
}
}
for proof in swap_request.inputs {
self.localstore.add_spent_proof(proof).await?;
}
@@ -461,11 +473,18 @@ impl Mint {
if let Ok(secret) =
<&crate::secret::Secret as TryInto<crate::nuts::nut10::Secret>>::try_into(&proof.secret)
{
// Verify if p2pk
if secret.kind.eq(&Kind::P2PK) {
proof.verify_p2pk()?;
} else {
return Err(Error::UnknownSecretKind);
// Checks and verifes known secret kinds.
// If it is an unknown secret kind it will be treated as a normal secret.
// Spending conditions will **not** be check. It is up to the wallet to ensure
// only supported secret kinds are used as there is no way for the mint to enforce
// only signing supported secrets as they are blinded at that point.
match secret.kind {
Kind::P2PK => {
proof.verify_p2pk()?;
}
Kind::HTLC => {
proof.verify_htlc()?;
}
}
}
@@ -552,6 +571,15 @@ impl Mint {
}
if let Some(outputs) = &melt_request.outputs {
let (sig_flag, pubkeys) = enforce_sig_flag(melt_request.inputs.clone());
if sig_flag.eq(&SigFlag::SigAll) {
let pubkeys = pubkeys.into_iter().collect();
for blinded_messaage in outputs {
blinded_messaage.verify_p2pk(&pubkeys, 1)?;
}
}
let output_keysets_ids: HashSet<Id> = outputs.iter().map(|b| b.keyset_id).collect();
for id in output_keysets_ids {
let keyset = self

View File

@@ -13,6 +13,7 @@ pub mod nut11;
pub mod nut12;
#[cfg(feature = "nut13")]
pub mod nut13;
pub mod nut14;
pub use nut00::{
BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod, PreMint, PreMintSecrets, Proof,
@@ -35,5 +36,5 @@ pub use nut06::{MintInfo, MintVersion, Nuts};
pub use nut07::{CheckStateRequest, CheckStateResponse, ProofState, State};
pub use nut09::{RestoreRequest, RestoreResponse};
pub use nut10::{Kind, Secret as Nut10Secret, SecretData};
pub use nut11::{P2PKConditions, SigFlag, Signatures, SigningKey, VerifyingKey};
pub use nut11::{Conditions, P2PKWitness, SigFlag, SigningKey, SpendingConditions, VerifyingKey};
pub use nut12::{BlindSignatureDleq, ProofDleq};

View File

@@ -14,11 +14,14 @@ use serde::{Deserialize, Deserializer, Serialize};
use thiserror::Error;
use url::Url;
use super::nut10;
use super::nut11::SpendingConditions;
use crate::dhke::blind_message;
use crate::nuts::nut01::{PublicKey, SecretKey};
use crate::nuts::nut11::{witness_deserialize, witness_serialize, Signatures};
use crate::nuts::nut11::{serde_p2pk_witness, P2PKWitness};
use crate::nuts::nut12::BlindSignatureDleq;
use crate::nuts::{Id, P2PKConditions, ProofDleq};
use crate::nuts::nut14::{serde_htlc_witness, HTLCWitness};
use crate::nuts::{Id, ProofDleq};
use crate::secret::Secret;
use crate::url::UncheckedUrl;
use crate::Amount;
@@ -77,11 +80,8 @@ pub struct BlindedMessage {
/// Witness
///
/// <https://github.com/cashubtc/nuts/blob/main/11.md>
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
//#[serde(serialize_with = "witness_serialize")]
//#[serde(deserialize_with = "witness_deserialize")]
pub witness: Option<Signatures>,
pub witness: Option<Witness>,
}
impl BlindedMessage {
@@ -97,9 +97,9 @@ impl BlindedMessage {
}
/// Add witness
pub fn witness(mut self, witness: Signatures) -> Self {
#[inline]
pub fn witness(&mut self, witness: Witness) {
self.witness = Some(witness);
self
}
}
@@ -123,11 +123,44 @@ pub struct BlindSignature {
/// DLEQ Proof
///
/// <https://github.com/cashubtc/nuts/blob/main/12.md>
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub dleq: Option<BlindSignatureDleq>,
}
/// Witness
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Witness {
/// P2PK Witness
#[serde(with = "serde_p2pk_witness")]
P2PKWitness(P2PKWitness),
/// HTLC Witness
#[serde(with = "serde_htlc_witness")]
HTLCWitness(HTLCWitness),
}
impl Witness {
pub fn add_signatures(&mut self, signatues: Vec<String>) {
match self {
Self::P2PKWitness(p2pk_witness) => p2pk_witness.signatures.extend(signatues),
Self::HTLCWitness(htlc_witness) => {
htlc_witness.signatures = htlc_witness.signatures.clone().map(|sigs| {
let mut sigs = sigs;
sigs.extend(signatues);
sigs
});
}
}
}
pub fn signatures(&self) -> Option<Vec<String>> {
match self {
Self::P2PKWitness(witness) => Some(witness.signatures.clone()),
Self::HTLCWitness(witness) => witness.signatures.clone(),
}
}
}
/// Proofs
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Proof {
@@ -142,12 +175,10 @@ pub struct Proof {
#[serde(rename = "C")]
pub c: PublicKey,
/// Witness
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(serialize_with = "witness_serialize")]
#[serde(deserialize_with = "witness_deserialize")]
pub witness: Option<Signatures>,
pub witness: Option<Witness>,
/// DLEQ Proof
#[serde(skip_serializing_if = "Option::is_none")]
pub dleq: Option<ProofDleq>,
}
@@ -381,17 +412,19 @@ impl PreMintSecrets {
Ok(PreMintSecrets { secrets: output })
}
pub fn with_p2pk_conditions(
pub fn with_conditions(
keyset_id: Id,
amount: Amount,
conditions: P2PKConditions,
conditions: SpendingConditions,
) -> Result<Self, Error> {
let amount_split = amount.split();
let mut output = Vec::with_capacity(amount_split.len());
for amount in amount_split {
let secret: Secret = conditions.clone().try_into()?;
let secret: nut10::Secret = conditions.clone().into();
let secret: Secret = secret.try_into()?;
let (blinded, r) = blind_message(&secret.to_bytes(), None)?;
let blinded_message = BlindedMessage::new(amount, keyset_id, blinded);

View File

@@ -67,20 +67,22 @@ pub struct MintBolt11Response {
}
/// Mint Method Settings
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct MintMethodSettings {
/// Payment Method e.g. bolt11
method: PaymentMethod,
/// Currency Unit e.g. sat
unit: CurrencyUnit,
/// Min Amount
min_amount: Amount,
#[serde(skip_serializing_if = "Option::is_none")]
min_amount: Option<Amount>,
/// Max Amount
max_amount: Amount,
#[serde(skip_serializing_if = "Option::is_none")]
max_amount: Option<Amount>,
}
/// Mint Settings
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Settings {
methods: Vec<MintMethodSettings>,
disabled: bool,

View File

@@ -74,20 +74,23 @@ pub struct MeltBolt11Response {
}
/// Melt Method Settings
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct MeltMethodSettings {
/// Payment Method e.g. bolt11
method: PaymentMethod,
/// Currency Unit e.g. sat
unit: CurrencyUnit,
/// Min Amount
min_amount: Amount,
#[serde(skip_serializing_if = "Option::is_none")]
min_amount: Option<Amount>,
/// Max Amount
max_amount: Amount,
#[serde(skip_serializing_if = "Option::is_none")]
max_amount: Option<Amount>,
}
/// Melt Settings
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Settings {
methods: Vec<MeltMethodSettings>,
disabled: bool,
}

View File

@@ -5,10 +5,10 @@
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use super::nut01::PublicKey;
use super::{nut04, nut05, nut07, nut08};
use super::{nut04, nut05};
/// Mint Version
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct MintVersion {
pub name: String,
pub version: String,
@@ -42,7 +42,7 @@ impl<'de> Deserialize<'de> for MintVersion {
}
/// Mint Info [NIP-09]
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct MintInfo {
/// name of the mint and should be recognizable
#[serde(skip_serializing_if = "Option::is_none")]
@@ -69,7 +69,8 @@ pub struct MintInfo {
pub motd: Option<String>,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
/// Supported nuts and settings
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Nuts {
#[serde(default)]
#[serde(rename = "4")]
@@ -79,21 +80,40 @@ pub struct Nuts {
pub nut05: nut05::Settings,
#[serde(default)]
#[serde(rename = "7")]
pub nut07: nut07::Settings,
pub nut07: SupportedSettings,
#[serde(default)]
#[serde(rename = "8")]
pub nut08: nut08::Settings,
// TODO: Change to nut settings
pub nut08: SupportedSettings,
#[serde(default)]
#[serde(rename = "9")]
pub nut09: nut07::Settings,
// TODO: Change to nut settings
pub nut09: SupportedSettings,
#[serde(rename = "10")]
#[serde(default)]
pub nut10: nut07::Settings,
// TODO: Change to nut settings
pub nut10: SupportedSettings,
#[serde(rename = "11")]
#[serde(default)]
pub nut11: SupportedSettings,
#[serde(default)]
#[serde(rename = "12")]
pub nut12: nut07::Settings,
pub nut12: SupportedSettings,
#[serde(default)]
#[serde(rename = "13")]
pub nut13: SupportedSettings,
#[serde(default)]
#[serde(rename = "14")]
pub nut14: SupportedSettings,
}
/// Check state Settings
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SupportedSettings {
supported: bool,
}
impl Default for SupportedSettings {
fn default() -> Self {
Self { supported: true }
}
}
#[cfg(test)]
@@ -150,7 +170,8 @@ mod tests {
"min_amount": 0,
"max_amount": 10000
}
]
],
"disabled": false
},
"7": {"supported": true},
"8": {"supported": true},

View File

@@ -39,9 +39,3 @@ pub struct ProofState {
pub struct CheckStateResponse {
pub states: Vec<ProofState>,
}
/// Spendable Settings
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Settings {
supported: bool,
}

View File

@@ -2,8 +2,6 @@
//!
//! <https://github.com/cashubtc/nuts/blob/main/08.md>
use serde::{Deserialize, Serialize};
use super::nut05::{MeltBolt11Request, MeltBolt11Response};
use crate::Amount;
@@ -22,9 +20,3 @@ impl MeltBolt11Response {
.map(|c| c.iter().map(|b| b.amount).sum())
}
}
/// Melt Settings
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Settings {
supported: bool,
}

View File

@@ -2,21 +2,23 @@
//!
//! <https://github.com/cashubtc/nuts/blob/main/10.md>
use core::str::FromStr;
use std::str::FromStr;
use serde::ser::SerializeTuple;
use serde::{Deserialize, Serialize, Serializer};
use crate::error::Error;
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
/// NUT10 Secret Kind
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Kind {
/// NUT-11 P2PK
#[default]
P2PK,
/// NUT-14 HTLC
HTLC,
}
#[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq, Serialize)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SecretData {
/// Unique random string
pub nonce: String,
@@ -27,23 +29,25 @@ pub struct SecretData {
pub tags: Vec<Vec<String>>,
}
#[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
pub struct Secret {
/// Kind of the spending condition
pub kind: Kind,
/// Secret Data
pub secret_data: SecretData,
}
impl Secret {
pub fn new<S>(kind: Kind, data: S, tags: Vec<Vec<String>>) -> Self
pub fn new<S, V>(kind: Kind, data: S, tags: V) -> Self
where
S: Into<String>,
V: Into<Vec<Vec<String>>>,
{
let nonce = crate::secret::Secret::generate().to_string();
let secret_data = SecretData {
nonce,
data: data.into(),
tags,
tags: tags.into(),
};
Self { kind, secret_data }

View File

@@ -2,7 +2,7 @@
//!
//! <https://github.com/cashubtc/nuts/blob/main/11.md>
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::fmt;
use std::ops::Deref;
use std::str::FromStr;
@@ -15,21 +15,26 @@ use bitcoin::secp256k1::{
};
use serde::de::Error as DeserializerError;
use serde::ser::SerializeSeq;
use serde::{de, ser, Deserialize, Deserializer, Serialize, Serializer};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use thiserror::Error;
use super::nut00::Witness;
use super::nut01::PublicKey;
use super::nut10::{Secret, SecretData};
use super::{Proof, SecretKey};
use super::{Kind, Nut10Secret, Proof, Proofs, SecretKey};
use crate::nuts::nut00::BlindedMessage;
use crate::util::{hex, unix_time};
use crate::SECP256K1;
pub mod serde_p2pk_witness;
#[derive(Debug, Error)]
pub enum Error {
/// Incorrect secret kind
#[error("Secret is not a p2pk secret")]
IncorrectSecretKind,
/// Incorrect secret kind
#[error("Witness is not a p2pk witness")]
IncorrectWitnessKind,
/// P2PK locktime has already passed
#[error("Locktime in past")]
LocktimeInPast,
@@ -39,14 +44,24 @@ pub enum Error {
/// Unknown tag in P2PK secret
#[error("Unknown Tag P2PK secret")]
UnknownTag,
/// Unknown Sigflag
#[error("Unknown Sigflag")]
UnknownSigFlag,
/// P2PK Spend conditions not meet
#[error("P2PK Spend conditions are not met")]
SpendConditionsNotMet,
/// Pubkey must be in data field of P2PK
#[error("P2PK Required in secret data")]
P2PKPubkeyRequired,
/// Unknown Kind
#[error("Kind not found")]
KindNotFound,
/// HTLC hash invalid
#[error("Invalid Hash")]
InvalidHash,
/// Witness Signatures not provided
#[error("Witness Signatures not provided")]
SignaturesNotProvided,
/// Parse Url Error
#[error(transparent)]
UrlParseError(#[from] url::ParseError),
@@ -70,66 +85,73 @@ pub enum Error {
Secret(#[from] crate::secret::Error),
}
/// P2Pk Witness
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Signatures {
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub struct P2PKWitness {
pub signatures: Vec<String>,
}
impl Signatures {
impl P2PKWitness {
#[inline]
pub fn is_empty(&self) -> bool {
self.signatures.is_empty()
}
}
/// Serialize [Signatures] as stringified JSON
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)?)
}
/// Serialize [Signatures] from stringified JSON
pub fn witness_deserialize<'de, D>(deserializer: D) -> Result<Option<Signatures>, D::Error>
where
D: Deserializer<'de>,
{
let s: String = String::deserialize(deserializer)?;
serde_json::from_str(&s).map_err(de::Error::custom)
}
impl Proof {
pub fn verify_p2pk(&self) -> Result<(), Error> {
if !self.secret.is_p2pk() {
return Err(Error::IncorrectSecretKind);
}
/// Sign [Proof]
pub fn sign_p2pk(&mut self, secret_key: SigningKey) -> Result<(), Error> {
let msg: Vec<u8> = self.secret.to_bytes();
let signature: Signature = secret_key.sign(&msg)?;
let secret: Secret = self.secret.clone().try_into()?;
let spending_conditions: P2PKConditions = secret.clone().try_into()?;
let signatures = vec![signature.to_string()];
match self.witness.as_mut() {
Some(witness) => {
witness.add_signatures(signatures);
}
None => {
let mut p2pk_witness = Witness::P2PKWitness(P2PKWitness::default());
p2pk_witness.add_signatures(signatures);
self.witness = Some(p2pk_witness);
}
};
Ok(())
}
/// Verify P2PK signature on [Proof]
pub fn verify_p2pk(&self) -> Result<(), Error> {
let secret: Nut10Secret = self.secret.clone().try_into()?;
let spending_conditions: Conditions = secret.secret_data.tags.try_into()?;
let msg: &[u8] = self.secret.as_bytes();
let mut valid_sigs = 0;
if let Some(witness) = &self.witness {
for signature in witness.signatures.iter() {
let mut pubkeys = spending_conditions.pubkeys.clone();
let witness_signatures = match &self.witness {
Some(witness) => witness.signatures(),
None => None,
};
pubkeys.push(VerifyingKey::from_str(&secret.secret_data.data)?);
let witness_signatures = witness_signatures.ok_or(Error::SignaturesNotProvided)?;
for v in &spending_conditions.pubkeys {
let sig = Signature::from_str(signature)?;
let mut pubkeys = spending_conditions.pubkeys.clone().unwrap_or_default();
if v.verify(msg, &sig).is_ok() {
valid_sigs += 1;
} else {
tracing::debug!(
"Could not verify signature: {sig} on message: {}",
self.secret.to_string()
)
}
if secret.kind.eq(&Kind::P2PK) {
pubkeys.push(VerifyingKey::from_str(&secret.secret_data.data)?);
}
for signature in witness_signatures.iter() {
for v in &pubkeys {
let sig = Signature::from_str(signature)?;
if v.verify(msg, &sig).is_ok() {
valid_sigs += 1;
} else {
tracing::debug!(
"Could not verify signature: {sig} on message: {}",
self.secret.to_string()
)
}
}
}
@@ -144,16 +166,13 @@ impl Proof {
) {
// If lock time has passed check if refund witness signature is valid
if locktime.lt(&unix_time()) {
if let Some(signatures) = &self.witness {
for s in &signatures.signatures {
for v in &refund_keys {
let sig =
Signature::from_str(s).map_err(|_| Error::InvalidSignature)?;
for s in witness_signatures.iter() {
for v in &refund_keys {
let sig = Signature::from_str(s).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(());
}
}
}
@@ -162,35 +181,46 @@ impl Proof {
Err(Error::SpendConditionsNotMet)
}
}
pub fn sign_p2pk(&mut self, secret_key: SigningKey) -> Result<(), Error> {
let msg: Vec<u8> = self.secret.to_bytes();
let signature: Signature = secret_key.sign(&msg)?;
/// Returns count of valid signatures
pub fn valid_signatures(msg: &[u8], pubkeys: &[VerifyingKey], signatures: &[Signature]) -> u64 {
let mut count = 0;
self.witness
.as_mut()
.unwrap_or(&mut Signatures::default())
.signatures
.push(signature.to_string());
Ok(())
for pubkey in pubkeys {
for signature in signatures {
if pubkey.verify(msg, signature).is_ok() {
count += 1;
}
}
}
count
}
impl BlindedMessage {
/// Sign [BlindedMessage]
pub fn sign_p2pk(&mut self, secret_key: SigningKey) -> Result<(), Error> {
let msg: [u8; 33] = self.blinded_secret.to_bytes();
let signature: Signature = secret_key.sign(&msg)?;
self.witness
.as_mut()
.unwrap_or(&mut Signatures::default())
.signatures
.push(signature.to_string());
let signatures = vec![signature.to_string()];
match self.witness.as_mut() {
Some(witness) => {
witness.add_signatures(signatures);
}
None => {
let mut p2pk_witness = Witness::P2PKWitness(P2PKWitness::default());
p2pk_witness.add_signatures(signatures);
self.witness = Some(p2pk_witness);
}
};
Ok(())
}
/// Verify P2PK conditions on [BlindedMessage]
pub fn verify_p2pk(
&self,
pubkeys: &Vec<VerifyingKey>,
@@ -198,7 +228,11 @@ impl BlindedMessage {
) -> Result<(), Error> {
let mut valid_sigs = 0;
if let Some(witness) = &self.witness {
for signature in &witness.signatures {
for signature in witness
.signatures()
.ok_or(Error::SignaturesNotProvided)?
.iter()
{
for v in pubkeys {
let msg = &self.blinded_secret.to_bytes();
let sig = Signature::from_str(signature)?;
@@ -223,11 +257,87 @@ impl BlindedMessage {
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct P2PKConditions {
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum SpendingConditions {
/// NUT11 Spending conditions
P2PKConditions {
data: VerifyingKey,
conditions: Conditions,
},
/// NUT14 Spending conditions
HTLCConditions {
data: Sha256Hash,
conditions: Conditions,
},
}
impl SpendingConditions {
/// New HTLC [SpendingConditions]
pub fn new_htlc(preimage: String, conditions: Conditions) -> Result<Self, Error> {
let htlc = Sha256Hash::hash(&hex::decode(preimage)?);
Ok(Self::HTLCConditions {
data: htlc,
conditions,
})
}
/// New P2PK [SpendingConditions]
pub fn new_p2pk(pubkey: VerifyingKey, conditions: Conditions) -> Self {
Self::P2PKConditions {
data: pubkey,
conditions,
}
}
/// Kind of [SpendingConditions]
pub fn kind(&self) -> Kind {
match self {
Self::P2PKConditions { .. } => Kind::P2PK,
Self::HTLCConditions { .. } => Kind::HTLC,
}
}
}
impl TryFrom<Nut10Secret> for SpendingConditions {
type Error = Error;
fn try_from(secret: Nut10Secret) -> Result<SpendingConditions, Error> {
match secret.kind {
Kind::P2PK => Ok(SpendingConditions::P2PKConditions {
data: VerifyingKey::from_str(&secret.secret_data.data)?,
conditions: secret.secret_data.tags.try_into()?,
}),
Kind::HTLC => Ok(Self::HTLCConditions {
data: Sha256Hash::from_str(&secret.secret_data.data)
.map_err(|_| Error::InvalidHash)?,
conditions: secret.secret_data.tags.try_into()?,
}),
}
}
}
impl From<SpendingConditions> for super::nut10::Secret {
fn from(conditions: SpendingConditions) -> super::nut10::Secret {
match conditions {
SpendingConditions::P2PKConditions { data, conditions } => super::nut10::Secret::new(
Kind::P2PK,
data.to_normalized_public_key().to_hex(),
conditions,
),
SpendingConditions::HTLCConditions { data, conditions } => {
super::nut10::Secret::new(Kind::HTLC, data.to_string(), conditions)
}
}
}
}
/// P2PK and HTLC spending conditions
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Conditions {
#[serde(skip_serializing_if = "Option::is_none")]
pub locktime: Option<u64>,
pub pubkeys: Vec<VerifyingKey>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pubkeys: Option<Vec<VerifyingKey>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub refund_keys: Option<Vec<VerifyingKey>>,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -235,10 +345,10 @@ pub struct P2PKConditions {
pub sig_flag: SigFlag,
}
impl P2PKConditions {
impl Conditions {
pub fn new(
locktime: Option<u64>,
pubkeys: Vec<VerifyingKey>,
pubkeys: Option<Vec<VerifyingKey>>,
refund_keys: Option<Vec<VerifyingKey>>,
num_sigs: Option<u64>,
sig_flag: Option<SigFlag>,
@@ -258,11 +368,9 @@ impl P2PKConditions {
})
}
}
impl TryFrom<P2PKConditions> for Secret {
type Error = Error;
fn try_from(conditions: P2PKConditions) -> Result<Secret, Self::Error> {
let P2PKConditions {
impl From<Conditions> for Vec<Vec<String>> {
fn from(conditions: Conditions) -> Vec<Vec<String>> {
let Conditions {
locktime,
pubkeys,
refund_keys,
@@ -270,17 +378,10 @@ impl TryFrom<P2PKConditions> for Secret {
sig_flag,
} = conditions;
let data = match pubkeys.first() {
Some(data) => data.to_string(),
None => return Err(Error::P2PKPubkeyRequired),
};
let data = data.to_string();
let mut tags = Vec::new();
if pubkeys.len().gt(&1) {
tags.push(Tag::PubKeys(pubkeys.into_iter().skip(1).collect()).as_vec());
if let Some(pubkeys) = pubkeys {
tags.push(Tag::PubKeys(pubkeys.into_iter().collect()).as_vec());
}
if let Some(locktime) = locktime {
@@ -295,48 +396,23 @@ impl TryFrom<P2PKConditions> for Secret {
tags.push(Tag::Refund(refund_keys).as_vec())
}
tags.push(Tag::SigFlag(sig_flag).as_vec());
Ok(Secret {
kind: super::nut10::Kind::P2PK,
secret_data: SecretData {
nonce: crate::secret::Secret::default().to_string(),
data,
tags,
},
})
tags
}
}
impl TryFrom<P2PKConditions> for crate::secret::Secret {
impl TryFrom<Vec<Vec<String>>> for Conditions {
type Error = Error;
fn try_from(conditions: P2PKConditions) -> Result<crate::secret::Secret, Self::Error> {
let secret: Secret = conditions.try_into()?;
secret.try_into().map_err(|_| Error::IncorrectSecretKind)
}
}
impl TryFrom<Secret> for P2PKConditions {
type Error = Error;
fn try_from(secret: Secret) -> Result<P2PKConditions, Self::Error> {
let tags: HashMap<TagKind, Tag> = secret
.clone()
.secret_data
.tags
fn try_from(tags: Vec<Vec<String>>) -> Result<Conditions, Self::Error> {
let tags: HashMap<TagKind, Tag> = tags
.into_iter()
.map(|t| Tag::try_from(t).unwrap())
.map(|t| (t.kind(), t))
.collect();
let mut pubkeys: Vec<VerifyingKey> = vec![];
if let Some(Tag::PubKeys(keys)) = tags.get(&TagKind::Pubkeys) {
let mut keys = keys.clone();
pubkeys.append(&mut keys);
}
let data_pubkey = VerifyingKey::from_str(&secret.secret_data.data)?;
pubkeys.push(data_pubkey);
let pubkeys = match tags.get(&TagKind::Pubkeys) {
Some(Tag::PubKeys(pubkeys)) => Some(pubkeys.clone()),
_ => None,
};
let locktime = if let Some(tag) = tags.get(&TagKind::Locktime) {
match tag {
@@ -374,7 +450,7 @@ impl TryFrom<Secret> for P2PKConditions {
None
};
Ok(P2PKConditions {
Ok(Conditions {
locktime,
pubkeys,
refund_keys,
@@ -384,7 +460,8 @@ impl TryFrom<Secret> for P2PKConditions {
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
// P2PK and HTLC Spending condition tags
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
#[serde(rename_all = "lowercase")]
pub enum TagKind {
/// Signature flag
@@ -436,7 +513,6 @@ pub enum SigFlag {
#[default]
SigInputs,
SigAll,
Custom(String),
}
impl fmt::Display for SigFlag {
@@ -444,25 +520,48 @@ impl fmt::Display for SigFlag {
match self {
Self::SigAll => write!(f, "SIG_ALL"),
Self::SigInputs => write!(f, "SIG_INPUTS"),
Self::Custom(flag) => write!(f, "{}", flag),
}
}
}
impl<S> From<S> for SigFlag
where
S: AsRef<str>,
{
fn from(tag: S) -> Self {
match tag.as_ref() {
"SIG_ALL" => Self::SigAll,
"SIG_INPUTS" => Self::SigInputs,
tag => Self::Custom(tag.to_string()),
impl FromStr for SigFlag {
type Err = Error;
fn from_str(tag: &str) -> Result<Self, Self::Err> {
match tag {
"SIG_ALL" => Ok(Self::SigAll),
"SIG_INPUTS" => Ok(Self::SigInputs),
_ => Err(Error::UnknownSigFlag),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub fn enforce_sig_flag(proofs: Proofs) -> (SigFlag, HashSet<VerifyingKey>) {
let mut sig_flag = SigFlag::SigInputs;
let mut pubkeys = HashSet::new();
for proof in proofs {
if let Ok(secret) = Nut10Secret::try_from(proof.secret) {
if secret.kind.eq(&Kind::P2PK) {
if let Ok(verifying_key) = VerifyingKey::from_str(&secret.secret_data.data) {
pubkeys.insert(verifying_key);
}
}
if let Ok(conditions) = Conditions::try_from(secret.secret_data.tags) {
if conditions.sig_flag.eq(&SigFlag::SigAll) {
sig_flag = SigFlag::SigAll;
}
if let Some(pubs) = conditions.pubkeys {
pubkeys.extend(pubs);
}
}
}
}
(sig_flag, pubkeys)
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum Tag {
SigFlag(SigFlag),
NSigs(u64),
@@ -501,7 +600,7 @@ where
};
match tag_kind {
TagKind::SigFlag => Ok(Tag::SigFlag(SigFlag::from(tag[1].as_ref()))),
TagKind::SigFlag => Ok(Tag::SigFlag(SigFlag::from_str(tag[1].as_ref())?)),
TagKind::NSigs => Ok(Tag::NSigs(tag[1].as_ref().parse()?)),
TagKind::Locktime => Ok(Tag::LockTime(tag[1].as_ref().parse()?)),
TagKind::Refund => {
@@ -535,7 +634,6 @@ impl From<Tag> for Vec<String> {
Tag::LockTime(locktime) => vec![TagKind::Locktime.to_string(), locktime.to_string()],
Tag::PubKeys(pubkeys) => {
let mut tag = vec![TagKind::Pubkeys.to_string()];
for pubkey in pubkeys.into_iter() {
let pubkey: PublicKey = pubkey.to_normalized_public_key();
tag.push(pubkey.to_string())
@@ -579,7 +677,7 @@ impl<'de> Deserialize<'de> for Tag {
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct VerifyingKey(XOnlyPublicKey);
@@ -706,17 +804,19 @@ mod tests {
use super::*;
use crate::nuts::Id;
use crate::secret::Secret;
use crate::Amount;
#[test]
fn test_secret_ser() {
let conditions = P2PKConditions {
let data = VerifyingKey::from_str(
"033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e",
)
.unwrap();
let conditions = Conditions {
locktime: Some(99999),
pubkeys: vec![
VerifyingKey::from_str(
"033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e",
)
.unwrap(),
pubkeys: Some(vec![
VerifyingKey::from_str(
"02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904",
)
@@ -725,7 +825,7 @@ mod tests {
"023192200a0cfd3867e48eb63b03ff599c7e46c8f4e41146b2d281173ca6c50c54",
)
.unwrap(),
],
]),
refund_keys: Some(vec![VerifyingKey::from_str(
"033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e",
)
@@ -734,11 +834,11 @@ mod tests {
sig_flag: SigFlag::SigAll,
};
let secret: Secret = conditions.try_into().unwrap();
let secret: Nut10Secret = Nut10Secret::new(Kind::P2PK, data.to_string(), conditions);
let secret_str = serde_json::to_string(&secret).unwrap();
let secret_der: Secret = serde_json::from_str(&secret_str).unwrap();
let secret_der: Nut10Secret = serde_json::from_str(&secret_str).unwrap();
assert_eq!(secret_der, secret);
}
@@ -763,15 +863,17 @@ mod tests {
let v_key_two: VerifyingKey = signing_key_two.verifying_key();
let v_key_three: VerifyingKey = signing_key_three.verifying_key();
let conditions = P2PKConditions {
let conditions = Conditions {
locktime: Some(21),
pubkeys: vec![v_key.clone(), v_key_two, v_key_three],
refund_keys: Some(vec![v_key]),
pubkeys: Some(vec![v_key_two, v_key_three]),
refund_keys: Some(vec![v_key.clone()]),
num_sigs: Some(2),
sig_flag: SigFlag::SigInputs,
};
let secret: super::Secret = conditions.try_into().unwrap();
let secret: Secret = Nut10Secret::new(Kind::P2PK, v_key.to_string(), conditions)
.try_into()
.unwrap();
let mut proof = Proof {
keyset_id: Id::from_str("009a1f293253e41e").unwrap(),
@@ -781,7 +883,7 @@ mod tests {
"02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904",
)
.unwrap(),
witness: Some(Signatures { signatures: vec![] }),
witness: Some(Witness::P2PKWitness(P2PKWitness { signatures: vec![] })),
dleq: None,
};

View File

@@ -0,0 +1,22 @@
//! Serde utils for P2PK Witness
use serde::{de, ser, Deserialize, Deserializer, Serializer};
use super::P2PKWitness;
/// Serialize [P2PKWitness] as stringified JSON
pub fn serialize<S>(x: &P2PKWitness, s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
s.serialize_str(&serde_json::to_string(&x).map_err(ser::Error::custom)?)
}
/// Deserialize [P2PKWitness] from stringified JSON
pub fn deserialize<'de, D>(deserializer: D) -> Result<P2PKWitness, D::Error>
where
D: Deserializer<'de>,
{
let s: String = String::deserialize(deserializer)?;
serde_json::from_str(&s).map_err(de::Error::custom)
}

View File

@@ -0,0 +1,130 @@
//! NUT-14: Hashed Time Lock Contacts (HTLC)
//!
//! <https://github.com/cashubtc/nuts/blob/main/14.md>
use std::str::FromStr;
use bitcoin::hashes::sha256::Hash as Sha256Hash;
use bitcoin::hashes::Hash;
use bitcoin::secp256k1::schnorr::Signature;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use super::nut00::Witness;
use super::nut10::Secret;
use super::nut11::valid_signatures;
use super::{Conditions, Proof};
use crate::util::unix_time;
pub mod serde_htlc_witness;
#[derive(Debug, Error)]
pub enum Error {
/// Incorrect secret kind
#[error("Secret is not a HTLC secret")]
IncorrectSecretKind,
/// HTLC locktime has already passed
#[error("Locktime in past")]
LocktimeInPast,
/// Hash Required
#[error("Hash Required")]
HashRequired,
/// Hash is not valid
#[error("Hash is not valid")]
InvalidHash,
/// Preimage does not match
#[error("Preimage does not match")]
Preimage,
/// Witness Signatures not provided
#[error("Witness did not provide signatures")]
SignaturesNotProvided,
/// NUT11 Error
#[error(transparent)]
NUT11(#[from] super::nut11::Error),
#[error(transparent)]
Serde(#[from] serde_json::Error),
}
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct HTLCWitness {
pub preimage: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub signatures: Option<Vec<String>>,
}
impl Proof {
/// Verify HTLC
pub fn verify_htlc(&self) -> Result<(), Error> {
let secret: Secret = self.secret.clone().try_into()?;
let conditions: Conditions = secret.secret_data.tags.try_into()?;
// Check locktime
if let Some(locktime) = conditions.locktime {
// If locktime is in passed and no refund keys provided anyone can spend
if locktime.lt(&unix_time()) && conditions.refund_keys.is_none() {
return Ok(());
}
// If refund keys are provided verify p2pk signatures
if let (Some(refund_key), Some(signatures)) = (conditions.refund_keys, &self.witness) {
let signatures: Vec<Signature> = signatures
.signatures()
.ok_or(Error::SignaturesNotProvided)?
.iter()
.flat_map(|s| Signature::from_str(s))
.collect();
// If secret includes refund keys check that there is a valid signature
if valid_signatures(self.secret.as_bytes(), &refund_key, &signatures).ge(&1) {
return Ok(());
}
}
}
if secret.kind.ne(&super::Kind::HTLC) {
return Err(Error::IncorrectSecretKind);
}
let htlc_witness = match &self.witness {
Some(Witness::HTLCWitness(witness)) => witness,
_ => return Err(Error::IncorrectSecretKind),
};
let hash_lock =
Sha256Hash::from_str(&secret.secret_data.data).map_err(|_| Error::InvalidHash)?;
let preimage_hash = Sha256Hash::hash(htlc_witness.preimage.as_bytes());
if hash_lock.ne(&preimage_hash) {
return Err(Error::Preimage);
}
// If pubkeys are present check there is a valid signature
if let Some(pubkey) = conditions.pubkeys {
let req_sigs = conditions.num_sigs.unwrap_or(1);
let signatures = htlc_witness
.signatures
.as_ref()
.ok_or(Error::SignaturesNotProvided)?;
let signatures: Vec<Signature> = signatures
.iter()
.flat_map(|s| Signature::from_str(s))
.collect();
if valid_signatures(self.secret.as_bytes(), &pubkey, &signatures).lt(&req_sigs) {
return Err(Error::IncorrectSecretKind);
}
}
Ok(())
}
#[inline]
pub fn add_preimage(&mut self, preimage: String) {
self.witness = Some(Witness::HTLCWitness(HTLCWitness {
preimage,
signatures: None,
}))
}
}

View File

@@ -0,0 +1,20 @@
use serde::{de, ser, Deserialize, Deserializer, Serializer};
use super::HTLCWitness;
/// Serialize [HTLCWitness] as stringified JSON
pub fn serialize<S>(x: &HTLCWitness, s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
s.serialize_str(&serde_json::to_string(&x).map_err(ser::Error::custom)?)
}
/// Deserialize [HTLCWitness] from stringified JSON
pub fn deserialize<'de, D>(deserializer: D) -> Result<HTLCWitness, D::Error>
where
D: Deserializer<'de>,
{
let s: String = String::deserialize(deserializer)?;
serde_json::from_str(&s).map_err(de::Error::custom)
}

View File

@@ -6,21 +6,22 @@ use std::str::FromStr;
use std::sync::Arc;
use bip39::Mnemonic;
use bitcoin::hashes::sha256::Hash as Sha256Hash;
use bitcoin::hashes::Hash;
use thiserror::Error;
use tracing::{debug, warn};
use crate::cdk_database::wallet_memory::WalletMemoryDatabase;
use crate::cdk_database::{self, WalletDatabase};
use crate::client::HttpClient;
use crate::dhke::{construct_proofs, hash_to_curve, unblind_message};
use crate::nuts::{
nut12, BlindSignature, CurrencyUnit, Id, KeySet, KeySetInfo, Keys, MintInfo, P2PKConditions,
nut12, BlindSignature, Conditions, CurrencyUnit, Id, KeySet, KeySetInfo, Keys, Kind, MintInfo,
PreMintSecrets, PreSwap, Proof, ProofState, Proofs, PublicKey, RestoreRequest, SigFlag,
SigningKey, State, SwapRequest, Token,
SigningKey, SpendingConditions, State, SwapRequest, Token, VerifyingKey,
};
use crate::types::{MeltQuote, Melted, MintQuote};
use crate::url::UncheckedUrl;
use crate::util::unix_time;
use crate::util::{hex, unix_time};
use crate::{Amount, Bolt11Invoice};
#[derive(Debug, Error)]
@@ -42,6 +43,8 @@ pub enum Error {
P2PKConditionsNotMet(String),
#[error("Invalid Spending Conditions: `{0}`")]
InvalidSpendConditions(String),
#[error("Preimage not provided")]
PreimageNotProvided,
#[error("Unknown Key")]
UnknownKey,
#[error(transparent)]
@@ -61,6 +64,8 @@ pub enum Error {
/// Database Error
#[error(transparent)]
Database(#[from] crate::cdk_database::Error),
#[error(transparent)]
Serde(#[from] serde_json::Error),
#[error("`{0}`")]
Custom(String),
}
@@ -136,7 +141,7 @@ impl Wallet {
{
Ok(mint_info) => Some(mint_info),
Err(err) => {
warn!("Could not get mint info {}", err);
tracing::warn!("Could not get mint info {}", err);
None
}
};
@@ -410,87 +415,6 @@ impl Wallet {
Ok(minted_amount)
}
/// Receive
pub async fn receive(&mut self, encoded_token: &str) -> Result<(), Error> {
let token_data = Token::from_str(encoded_token)?;
let unit = token_data.unit.unwrap_or_default();
// Verify the signature DLEQ is valid
// Verify that all proofs in the token have a valid DLEQ proof if one is supplied
{
for mint_proof in &token_data.token {
let mint_url = &mint_proof.mint;
let proofs = &mint_proof.proofs;
for proof in proofs {
let keys = self.get_keyset_keys(mint_url, proof.keyset_id).await?;
let key = keys.amount_key(proof.amount).ok_or(Error::UnknownKey)?;
match proof.verify_dleq(key) {
Ok(_) | Err(nut12::Error::MissingDleqProof) => continue,
Err(_) => return Err(Error::CouldNotVerifyDleq),
}
}
}
}
let mut proofs: HashMap<UncheckedUrl, Proofs> = HashMap::new();
for token in token_data.token {
if token.proofs.is_empty() {
continue;
}
let active_keyset_id = self.active_mint_keyset(&token.mint, &unit).await?;
// TODO: if none fetch keyset for mint
let keys = if let Some(keys) = self.localstore.get_keys(&active_keyset_id).await? {
keys
} else {
self.get_keyset_keys(&token.mint, active_keyset_id).await?;
self.localstore.get_keys(&active_keyset_id).await?.unwrap()
};
// Sum amount of all proofs
let amount: Amount = token.proofs.iter().map(|p| p.amount).sum();
let pre_swap = self
.create_swap(&token.mint, &unit, Some(amount), token.proofs)
.await?;
let swap_response = self
.client
.post_swap(token.mint.clone().try_into()?, pre_swap.swap_request)
.await?;
// Proof to keep
let p = construct_proofs(
swap_response.signatures,
pre_swap.pre_mint_secrets.rs(),
pre_swap.pre_mint_secrets.secrets(),
&keys,
)?;
#[cfg(feature = "nut13")]
if self.mnemonic.is_some() {
self.localstore
.increment_keyset_counter(&active_keyset_id, p.len() as u64)
.await?;
}
let mint_proofs = proofs.entry(token.mint).or_default();
mint_proofs.extend(p);
}
for (mint, p) in proofs {
self.add_mint(mint.clone()).await?;
self.localstore.add_proofs(mint, p).await?;
}
Ok(())
}
/// Create Swap Payload
async fn create_swap(
&mut self,
@@ -498,6 +422,7 @@ impl Wallet {
unit: &CurrencyUnit,
amount: Option<Amount>,
proofs: Proofs,
spending_conditions: Option<SpendingConditions>,
) -> Result<PreSwap, Error> {
let active_keyset_id = self.active_mint_keyset(mint_url, unit).await?;
@@ -505,59 +430,112 @@ impl Wallet {
let proofs_total = proofs.iter().map(|p| p.amount).sum();
let desired_amount = amount.unwrap_or(proofs_total);
let mut counter = None;
let change_amount = proofs_total - desired_amount;
let mut desired_messages;
let change_messages;
#[cfg(not(feature = "nut13"))]
{
desired_messages = PreMintSecrets::random(active_keyset_id, desired_amount)?;
(desired_messages, change_messages) = match spendig_conditions {
Some(conditions) => (
PreMintSecrets::with_conditions(active_keyset_id, desired_amount, conditions)?,
PreMintSecrets::random(active_keyset_id, change_amount),
),
None => (
PreMintSecrets::random(active_keyset_id, proofs_total)?,
PreMintSecrets::default(),
),
};
}
#[cfg(feature = "nut13")]
{
desired_messages = if let Some(mnemonic) = &self.mnemonic {
let count = self
.localstore
.get_keyset_counter(&active_keyset_id)
.await?;
(desired_messages, change_messages) = match &self.mnemonic {
Some(mnemonic) => match spending_conditions {
Some(conditions) => {
let count = self
.localstore
.get_keyset_counter(&active_keyset_id)
.await?;
let count = if let Some(count) = count {
count + 1
} else {
0
};
let count = if let Some(count) = count {
count + 1
} else {
0
};
let premint_secrets = PreMintSecrets::from_seed(
active_keyset_id,
count,
mnemonic,
desired_amount,
false,
)?;
let change_premint_secrets = PreMintSecrets::from_seed(
active_keyset_id,
count,
mnemonic,
change_amount,
false,
)?;
counter = Some(count + premint_secrets.len() as u64);
(
PreMintSecrets::with_conditions(
active_keyset_id,
desired_amount,
conditions,
)?,
change_premint_secrets,
)
}
None => {
let count = self
.localstore
.get_keyset_counter(&active_keyset_id)
.await?;
premint_secrets
} else {
PreMintSecrets::random(active_keyset_id, desired_amount)?
let count = if let Some(count) = count {
count + 1
} else {
0
};
let premint_secrets = PreMintSecrets::from_seed(
active_keyset_id,
count,
mnemonic,
desired_amount,
false,
)?;
let count = count + premint_secrets.len() as u64;
let change_premint_secrets = PreMintSecrets::from_seed(
active_keyset_id,
count,
mnemonic,
change_amount,
false,
)?;
(premint_secrets, change_premint_secrets)
}
},
None => match spending_conditions {
Some(conditions) => (
PreMintSecrets::with_conditions(
active_keyset_id,
desired_amount,
conditions,
)?,
PreMintSecrets::random(active_keyset_id, change_amount)?,
),
None => (
PreMintSecrets::random(active_keyset_id, desired_amount)?,
PreMintSecrets::random(active_keyset_id, change_amount)?,
),
},
};
}
if let Some(amt) = amount {
let change_amount = proofs_total - amt;
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)?
};
// Combine the BlindedMessages totoalling the desired amount with change
desired_messages.combine(change_messages);
// Sort the premint secrets to avoid finger printing
desired_messages.sort_secrets();
};
// Combine the BlindedMessages totoalling the desired amount with change
desired_messages.combine(change_messages);
// Sort the premint secrets to avoid finger printing
desired_messages.sort_secrets();
let swap_request = SwapRequest::new(proofs, desired_messages.blinded_messages());
@@ -633,11 +611,20 @@ impl Wallet {
mint_url: &UncheckedUrl,
unit: &CurrencyUnit,
amount: Amount,
conditions: Option<SpendingConditions>,
) -> Result<Proofs, Error> {
let proofs = self.select_proofs(mint_url.clone(), unit, amount).await?;
let input_proofs = self.select_proofs(mint_url.clone(), unit, amount).await?;
let active_keyset_id = self.active_mint_keyset(mint_url, unit).await?;
let pre_swap = self
.create_swap(mint_url, unit, Some(amount), proofs.clone())
.create_swap(
mint_url,
unit,
Some(amount),
input_proofs.clone(),
conditions,
)
.await?;
let swap_response = self
@@ -655,12 +642,10 @@ impl Wallet {
&self.active_keys(mint_url, unit).await?.unwrap(),
)?;
let active_keyset = self.active_mint_keyset(mint_url, unit).await?;
#[cfg(feature = "nut13")]
if self.mnemonic.is_some() {
self.localstore
.increment_keyset_counter(&active_keyset, post_swap_proofs.len() as u64)
.increment_keyset_counter(&active_keyset_id, post_swap_proofs.len() as u64)
.await?;
}
@@ -677,18 +662,19 @@ impl Wallet {
let send_amount: Amount = send_proofs.iter().map(|p| p.amount).sum();
if send_amount.ne(&amount) {
warn!(
tracing::warn!(
"Send amount proofs is {:?} expected {:?}",
send_amount, amount
send_amount,
amount
);
}
self.localstore
.remove_proofs(mint_url.clone(), &proofs)
.remove_proofs(mint_url.clone(), &input_proofs)
.await?;
self.localstore
.add_pending_proofs(mint_url.clone(), proofs)
.add_pending_proofs(mint_url.clone(), input_proofs)
.await?;
self.localstore
.add_pending_proofs(mint_url.clone(), send_proofs.clone())
@@ -874,7 +860,7 @@ impl Wallet {
};
if let Some(change_proofs) = change_proofs {
debug!(
tracing::debug!(
"Change amount returned from melt: {}",
change_proofs.iter().map(|p| p.amount).sum::<Amount>()
);
@@ -901,89 +887,13 @@ impl Wallet {
Ok(melted)
}
/// Create P2PK locked proofs
/// Uses a swap to swap proofs for locked p2pk conditions
pub async fn send_p2pk(
&mut self,
mint_url: &UncheckedUrl,
unit: &CurrencyUnit,
amount: Amount,
conditions: P2PKConditions,
) -> Result<Proofs, Error> {
let input_proofs = self.select_proofs(mint_url.clone(), unit, amount).await?;
let active_keyset_id = self.active_mint_keyset(mint_url, unit).await?;
let input_amount: Amount = input_proofs.iter().map(|p| p.amount).sum();
let change_amount = input_amount - amount;
let send_premint_secrets =
PreMintSecrets::with_p2pk_conditions(active_keyset_id, amount, conditions)?;
let change_premint_secrets = PreMintSecrets::random(active_keyset_id, change_amount)?;
let mut pre_mint_secrets = send_premint_secrets;
pre_mint_secrets.combine(change_premint_secrets);
let swap_request =
SwapRequest::new(input_proofs.clone(), pre_mint_secrets.blinded_messages());
let pre_swap = PreSwap {
pre_mint_secrets,
swap_request,
};
let swap_response = self
.client
.post_swap(mint_url.clone().try_into()?, pre_swap.swap_request)
.await?;
let post_swap_proofs = construct_proofs(
swap_response.signatures,
pre_swap.pre_mint_secrets.rs(),
pre_swap.pre_mint_secrets.secrets(),
&self.active_keys(mint_url, unit).await?.unwrap(),
)?;
let mut send_proofs = vec![];
let mut change_proofs = vec![];
for proof in post_swap_proofs {
let conditions: Result<crate::nuts::nut10::Secret, _> = (&proof.secret).try_into();
if conditions.is_ok() {
send_proofs.push(proof);
} else {
change_proofs.push(proof);
}
}
self.localstore
.remove_proofs(mint_url.clone(), &input_proofs)
.await?;
self.localstore
.add_pending_proofs(mint_url.clone(), input_proofs)
.await?;
self.localstore
.add_pending_proofs(mint_url.clone(), send_proofs.clone())
.await?;
self.localstore
.add_proofs(mint_url.clone(), change_proofs.clone())
.await?;
Ok(send_proofs)
}
/// Receive p2pk
pub async fn receive_p2pk(
/// Receive
pub async fn receive(
&mut self,
encoded_token: &str,
signing_keys: Vec<SigningKey>,
signing_keys: Option<Vec<SigningKey>>,
preimages: Option<Vec<String>>,
) -> Result<(), Error> {
let signing_key = signing_keys[0].clone();
let pubkey_secret_key: HashMap<String, SigningKey> = signing_keys
.into_iter()
.map(|s| (s.public_key().to_string(), s))
.collect();
let token_data = Token::from_str(encoded_token)?;
let unit = token_data.unit.unwrap_or_default();
@@ -1005,7 +915,27 @@ impl Wallet {
let mut proofs = token.proofs;
let mut sig_flag = None;
let mut sig_flag = SigFlag::SigInputs;
let pubkey_secret_key = match &signing_keys {
Some(signing_keys) => signing_keys
.iter()
.map(|s| (s.verifying_key().to_string(), s))
.collect(),
None => HashMap::new(),
};
// Map hash of preimage to preimage
let hashed_to_preimage = match preimages {
Some(ref preimages) => preimages
.iter()
.flat_map(|p| match hex::decode(p) {
Ok(hex_bytes) => Some((Sha256Hash::hash(&hex_bytes).to_string(), p)),
Err(_) => None,
})
.collect(),
None => HashMap::new(),
};
for proof in &mut proofs {
// Verify that proof DLEQ is valid
@@ -1020,29 +950,45 @@ impl Wallet {
proof.secret.clone(),
)
{
let conditions: Result<P2PKConditions, _> = secret.try_into();
let conditions: Result<Conditions, _> = secret.secret_data.tags.try_into();
if let Ok(conditions) = conditions {
let pubkeys = conditions.pubkeys;
let mut pubkeys = conditions.pubkeys.unwrap_or_default();
match secret.kind {
Kind::P2PK => {
let data_key = VerifyingKey::from_str(&secret.secret_data.data)?;
pubkeys.push(data_key);
}
Kind::HTLC => {
let hashed_preimage = &secret.secret_data.data;
let preimage = hashed_to_preimage
.get(hashed_preimage)
.ok_or(Error::PreimageNotProvided)?;
proof.add_preimage(preimage.to_string());
}
}
for pubkey in pubkeys {
if let Some(signing) = pubkey_secret_key.get(&pubkey.to_string()) {
proof.sign_p2pk(signing.clone())?;
proof.sign_p2pk(signing.to_owned().clone())?;
}
}
sig_flag = Some(conditions.sig_flag);
if conditions.sig_flag.eq(&SigFlag::SigAll) {
sig_flag = SigFlag::SigAll;
}
}
}
}
let mut pre_swap = self
.create_swap(&token.mint, &unit, Some(amount), proofs)
.create_swap(&token.mint, &unit, Some(amount), proofs, None)
.await?;
if let Some(sigflag) = sig_flag {
if sigflag.eq(&SigFlag::SigAll) {
for blinded_message in &mut pre_swap.swap_request.outputs {
blinded_message.sign_p2pk(signing_key.clone()).unwrap();
if sig_flag.eq(&SigFlag::SigAll) {
for blinded_message in &mut pre_swap.swap_request.outputs {
for signing_key in pubkey_secret_key.values() {
blinded_message.sign_p2pk(signing_key.to_owned().clone())?
}
}
}
@@ -1112,7 +1058,7 @@ impl Wallet {
start_counter + 100,
)?;
debug!(
tracing::debug!(
"Attempting to restore counter {}-{} for mint {} keyset {}",
start_counter,
start_counter + 100,
@@ -1158,7 +1104,7 @@ impl Wallet {
&keys,
)?;
debug!("Restored {} proofs", proofs.len());
tracing::debug!("Restored {} proofs", proofs.len());
#[cfg(feature = "nut13")]
self.localstore
@@ -1196,12 +1142,12 @@ impl Wallet {
pub fn verify_token_p2pk(
&self,
token: &Token,
spending_conditions: P2PKConditions,
spending_conditions: Conditions,
) -> Result<(), Error> {
use crate::nuts::nut10;
if spending_conditions.refund_keys.is_some() && spending_conditions.locktime.is_none() {
warn!(
tracing::warn!(
"Invalid spending conditions set: Locktime must be set if refund keys are allowed"
);
return Err(Error::InvalidSpendConditions(
@@ -1211,14 +1157,15 @@ impl Wallet {
for mint_proof in &token.token {
for proof in &mint_proof.proofs {
let secret: nut10::Secret = (&proof.secret).try_into().unwrap();
let secret: nut10::Secret = (&proof.secret).try_into()?;
let proof_conditions: P2PKConditions = secret.try_into().unwrap();
let proof_conditions: Conditions = secret.secret_data.tags.try_into()?;
if spending_conditions.num_sigs.ne(&proof_conditions.num_sigs) {
debug!(
tracing::debug!(
"Spending condition requires: {:?} sigs proof secret specifies: {:?}",
spending_conditions.num_sigs, proof_conditions.num_sigs
spending_conditions.num_sigs,
proof_conditions.num_sigs
);
return Err(Error::P2PKConditionsNotMet(
@@ -1226,17 +1173,17 @@ impl Wallet {
));
}
let spending_condition_pubkeys =
spending_conditions.pubkeys.clone().unwrap_or_default();
let proof_pubkeys = proof_conditions.pubkeys.unwrap_or_default();
// Check the Proof has the required pubkeys
if proof_conditions
.pubkeys
.len()
.ne(&spending_conditions.pubkeys.len())
|| !proof_conditions
.pubkeys
if proof_pubkeys.len().ne(&spending_condition_pubkeys.len())
|| !proof_pubkeys
.iter()
.all(|pubkey| spending_conditions.pubkeys.contains(pubkey))
.all(|pubkey| spending_condition_pubkeys.contains(pubkey))
{
debug!("Proof did not included Publickeys meeting condition");
tracing::debug!("Proof did not included Publickeys meeting condition");
return Err(Error::P2PKConditionsNotMet(
"Pubkeys in proof not allowed by spending condition".to_string(),
));