mirror of
https://github.com/aljazceru/cdk.git
synced 2026-02-23 14:06:56 +01:00
feat: NUT14
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
22
crates/cdk/src/nuts/nut11/serde_p2pk_witness.rs
Normal file
22
crates/cdk/src/nuts/nut11/serde_p2pk_witness.rs
Normal 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)
|
||||
}
|
||||
130
crates/cdk/src/nuts/nut14/mod.rs
Normal file
130
crates/cdk/src/nuts/nut14/mod.rs
Normal 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,
|
||||
}))
|
||||
}
|
||||
}
|
||||
20
crates/cdk/src/nuts/nut14/serde_htlc_witness.rs
Normal file
20
crates/cdk/src/nuts/nut14/serde_htlc_witness.rs
Normal 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)
|
||||
}
|
||||
@@ -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(),
|
||||
));
|
||||
|
||||
Reference in New Issue
Block a user